@bounded-sh/core 0.0.58 → 0.0.60
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-B30-XpaX.js → index-BIdD2yUB.js} +957 -312
- package/dist/index-BIdD2yUB.js.map +1 -0
- package/dist/{index-DYbzK8YT.mjs → index-BQ6e4MeP.mjs} +952 -313
- package/dist/index-BQ6e4MeP.mjs.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{realtime-store-BpQsQYhl.js → realtime-store-Bz4Ob6K6.js} +2 -2
- package/dist/{realtime-store-BpQsQYhl.js.map → realtime-store-Bz4Ob6K6.js.map} +1 -1
- package/dist/{realtime-store-BSuV-Vyr.mjs → realtime-store-DKngqviG.mjs} +2 -2
- package/dist/{realtime-store-BSuV-Vyr.mjs.map → realtime-store-DKngqviG.mjs.map} +1 -1
- package/dist/{solana-message-DKOPPJwk.js → solana-message-B0NoE4nC.js} +2 -2
- package/dist/{solana-message-DKOPPJwk.js.map → solana-message-B0NoE4nC.js.map} +1 -1
- package/dist/{solana-message-C2_VvNX7.mjs → solana-message-QUfJi1Qm.mjs} +2 -2
- package/dist/{solana-message-C2_VvNX7.mjs.map → solana-message-QUfJi1Qm.mjs.map} +1 -1
- package/dist/{solana-operations-CuYFYlyN.mjs → solana-operations-B2Oy9T7D.mjs} +2 -2
- package/dist/{solana-operations-CuYFYlyN.mjs.map → solana-operations-B2Oy9T7D.mjs.map} +1 -1
- package/dist/{solana-operations-BOKHbEuy.js → solana-operations-ngqIkN9W.js} +2 -2
- package/dist/{solana-operations-BOKHbEuy.js.map → solana-operations-ngqIkN9W.js.map} +1 -1
- package/dist/utils/auth-token-freshness.d.ts +79 -0
- package/dist/utils/rn-session-manager.d.ts +19 -2
- package/dist/utils/session-events.d.ts +31 -0
- package/dist/utils/session-mutation.d.ts +124 -0
- package/dist/utils/utils.d.ts +18 -1
- package/dist/utils/web-session-manager.d.ts +43 -2
- package/package.json +2 -2
- package/dist/index-B30-XpaX.js.map +0 -1
- package/dist/index-DYbzK8YT.mjs.map +0 -1
|
@@ -2909,19 +2909,19 @@ async function createSessionWithSignature(address, message, signature, authMetho
|
|
|
2909
2909
|
// Note: module-level state is shared across requests in SSR/Node — acceptable
|
|
2910
2910
|
// since there is only one active session per server instance (same pattern as
|
|
2911
2911
|
// refreshInFlight in api.ts).
|
|
2912
|
-
let refreshInFlight
|
|
2912
|
+
let refreshInFlight = null;
|
|
2913
2913
|
let refreshInFlightToken = null;
|
|
2914
2914
|
async function refreshSession(refreshToken, issuer) {
|
|
2915
2915
|
// Dedupe per (issuer, token): the rotating token is single-use, so two
|
|
2916
2916
|
// concurrent refreshes of the SAME token must share one in-flight request or
|
|
2917
2917
|
// the second would look like reuse and revoke the family.
|
|
2918
2918
|
const dedupeKey = `${issuer || ''}::${refreshToken}`;
|
|
2919
|
-
if (refreshInFlight
|
|
2920
|
-
return refreshInFlight
|
|
2919
|
+
if (refreshInFlight && refreshInFlightToken === dedupeKey) {
|
|
2920
|
+
return refreshInFlight;
|
|
2921
2921
|
}
|
|
2922
2922
|
refreshInFlightToken = dedupeKey;
|
|
2923
2923
|
let currentFlight;
|
|
2924
|
-
currentFlight = refreshInFlight
|
|
2924
|
+
currentFlight = refreshInFlight = (async () => {
|
|
2925
2925
|
try {
|
|
2926
2926
|
const config = await getConfig();
|
|
2927
2927
|
const appId = config.appId;
|
|
@@ -2938,13 +2938,13 @@ async function refreshSession(refreshToken, issuer) {
|
|
|
2938
2938
|
finally {
|
|
2939
2939
|
// Only clear if we're still the active in-flight request.
|
|
2940
2940
|
// A second call with a different token may have replaced us.
|
|
2941
|
-
if (refreshInFlight
|
|
2942
|
-
refreshInFlight
|
|
2941
|
+
if (refreshInFlight === currentFlight) {
|
|
2942
|
+
refreshInFlight = null;
|
|
2943
2943
|
refreshInFlightToken = null;
|
|
2944
2944
|
}
|
|
2945
2945
|
}
|
|
2946
2946
|
})();
|
|
2947
|
-
return refreshInFlight
|
|
2947
|
+
return refreshInFlight;
|
|
2948
2948
|
}
|
|
2949
2949
|
class SessionRevokeError extends Error {
|
|
2950
2950
|
constructor(message, cause) {
|
|
@@ -3029,7 +3029,248 @@ function refreshedTokenMayReplaceStored(newIdToken, storedAddress, decodeBase64U
|
|
|
3029
3029
|
return tokenWallet === storedAddress;
|
|
3030
3030
|
}
|
|
3031
3031
|
|
|
3032
|
+
/**
|
|
3033
|
+
* The one signal core sends upward when a stored session stops existing.
|
|
3034
|
+
*
|
|
3035
|
+
* `tarobase-core` cannot import `js-sdk` (that would be a cycle), and core has no
|
|
3036
|
+
* event bus, so a session dying inside a session manager used to be invisible to
|
|
3037
|
+
* the SDK: `setCurrentUser(null)` was only ever called by `logout()` and `init()`.
|
|
3038
|
+
* The result was a UI that kept showing a connected wallet long after the session
|
|
3039
|
+
* it was showing had been wiped, which is exactly what let the Solana lab freeze a
|
|
3040
|
+
* review against a wallet the server no longer knew about.
|
|
3041
|
+
*
|
|
3042
|
+
* `js-sdk` registers a handler at init; core fires it ONLY when storage was
|
|
3043
|
+
* actually cleared, never on a failed-but-preserved refresh. Firing on a
|
|
3044
|
+
* preserved session would log people out on a transient network blip.
|
|
3045
|
+
*/
|
|
3046
|
+
let handlers = [];
|
|
3047
|
+
/**
|
|
3048
|
+
* Register a listener for "the stored session is gone".
|
|
3049
|
+
* Returns an unsubscribe function.
|
|
3050
|
+
*/
|
|
3051
|
+
function onSessionCleared(handler) {
|
|
3052
|
+
handlers.push(handler);
|
|
3053
|
+
return () => {
|
|
3054
|
+
handlers = handlers.filter((h) => h !== handler);
|
|
3055
|
+
};
|
|
3056
|
+
}
|
|
3057
|
+
/**
|
|
3058
|
+
* Announce that session storage was cleared.
|
|
3059
|
+
*
|
|
3060
|
+
* Call this ONLY after a removal actually happened. Handlers are best-effort: a
|
|
3061
|
+
* throwing listener must not turn a successful clear into a failed one, because
|
|
3062
|
+
* the storage mutation has already committed by this point.
|
|
3063
|
+
*/
|
|
3064
|
+
function notifySessionCleared() {
|
|
3065
|
+
for (const handler of handlers) {
|
|
3066
|
+
try {
|
|
3067
|
+
handler();
|
|
3068
|
+
}
|
|
3069
|
+
catch (_a) {
|
|
3070
|
+
// A listener's failure cannot un-clear storage; keep notifying the rest.
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
/**
|
|
3076
|
+
* The one protocol every session-storage mutation goes through.
|
|
3077
|
+
*
|
|
3078
|
+
* WHY THIS EXISTS
|
|
3079
|
+
*
|
|
3080
|
+
* `localStorage` is shared by every tab on an origin but offers no atomic
|
|
3081
|
+
* compare-and-set spanning a read and a write, and each tab is a separate JS
|
|
3082
|
+
* realm, so an in-memory guard cannot serialize them. The old code relied on
|
|
3083
|
+
* doing its read → check → write inside one synchronous turn, which is only
|
|
3084
|
+
* atomic *within* a realm.
|
|
3085
|
+
*
|
|
3086
|
+
* That is not a theoretical gap. `REFRESH_REUSE_GRACE_MS` on the issuer is 60s,
|
|
3087
|
+
* so two tabs presenting the same refresh token inside that window BOTH succeed
|
|
3088
|
+
* and BOTH receive the same successor refresh token — while each response mints
|
|
3089
|
+
* its own distinct idToken. Concurrent cross-tab commits are a designed-for
|
|
3090
|
+
* occurrence, not an edge case.
|
|
3091
|
+
*
|
|
3092
|
+
* Two failures follow from an unlocked read-then-write, and both are closed here:
|
|
3093
|
+
*
|
|
3094
|
+
* 1. Lost update / resurrection. Tab A reads, tab B logs in (or logs out), tab A
|
|
3095
|
+
* writes its stale blob back. B's login is silently replaced, or a logged-out
|
|
3096
|
+
* session comes back to life.
|
|
3097
|
+
* 2. Spurious "the user changed". Tab B's commit finds tab A's blob, which
|
|
3098
|
+
* matches neither B's snapshot nor B's own result because the idTokens
|
|
3099
|
+
* differ — even though it is the same user and the same login.
|
|
3100
|
+
*
|
|
3101
|
+
* THE CRITICAL SECTION IS THE COMMIT, NOT THE NETWORK CALL
|
|
3102
|
+
*
|
|
3103
|
+
* The lock covers read → compare → write only. The refresh POST happens outside
|
|
3104
|
+
* it: holding a cross-tab lock across a 30s network call would serialize every
|
|
3105
|
+
* tab behind the slowest one. This is safe because the snapshot captured before
|
|
3106
|
+
* the network call is used only for COMPARISON. What actually gets written is
|
|
3107
|
+
* decided against storage re-read inside the lock, so a login or logout that lands
|
|
3108
|
+
* during the network call is always observed.
|
|
3109
|
+
*
|
|
3110
|
+
* NO BEST-EFFORT FALLBACK
|
|
3111
|
+
*
|
|
3112
|
+
* A `localStorage` lease was considered and rejected: its own acquire sequence is
|
|
3113
|
+
* a non-atomic read-then-write across realms, and a backgrounded tab can be frozen
|
|
3114
|
+
* past any lease timeout and then resume and write. Both leave exactly the windows
|
|
3115
|
+
* this module exists to close, so a browser without Web Locks fails closed instead.
|
|
3116
|
+
* Absence of `navigator.locks` does NOT imply a single realm — such a browser still
|
|
3117
|
+
* has tabs. Single realm is determined by the absence of `localStorage`.
|
|
3118
|
+
*/
|
|
3119
|
+
const SESSION_LOCK_NAME = "bounded_session_mutation";
|
|
3120
|
+
const UNSUPPORTED_ENVIRONMENT_CODE = "unsupported_environment";
|
|
3121
|
+
/**
|
|
3122
|
+
* Thrown when authenticated session work is attempted in a browser that shares
|
|
3123
|
+
* `localStorage` across tabs but cannot serialize access to it. Reads and
|
|
3124
|
+
* anonymous requests are unaffected; only session mutation is refused.
|
|
3125
|
+
*/
|
|
3126
|
+
class SessionEnvironmentError extends Error {
|
|
3127
|
+
constructor(message) {
|
|
3128
|
+
super(message);
|
|
3129
|
+
this.code = UNSUPPORTED_ENVIRONMENT_CODE;
|
|
3130
|
+
this.name = "SessionEnvironmentError";
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
/**
|
|
3134
|
+
* True when nothing outside this realm can touch session storage.
|
|
3135
|
+
*
|
|
3136
|
+
* Keyed on `localStorage`, not on `window`: React Native defines `window` but
|
|
3137
|
+
* stores sessions through a per-process StorageAdapter, and Node/SSR has neither.
|
|
3138
|
+
* A browser is the only multi-realm case, and `localStorage` is what makes it one.
|
|
3139
|
+
*/
|
|
3140
|
+
function isSingleRealm() {
|
|
3141
|
+
return typeof localStorage === "undefined";
|
|
3142
|
+
}
|
|
3143
|
+
function hasWebLocks() {
|
|
3144
|
+
var _a;
|
|
3145
|
+
return typeof navigator !== "undefined"
|
|
3146
|
+
&& typeof ((_a = navigator.locks) === null || _a === void 0 ? void 0 : _a.request) === "function";
|
|
3147
|
+
}
|
|
3148
|
+
/**
|
|
3149
|
+
* Run `fn` with exclusive access to session storage across every tab on this origin.
|
|
3150
|
+
*
|
|
3151
|
+
* Single-realm runtimes execute directly. Browsers serialize through Web Locks.
|
|
3152
|
+
* A browser without Web Locks throws rather than running unserialized.
|
|
3153
|
+
*/
|
|
3154
|
+
async function withSessionLock(fn) {
|
|
3155
|
+
if (isSingleRealm()) {
|
|
3156
|
+
return await fn();
|
|
3157
|
+
}
|
|
3158
|
+
if (!hasWebLocks()) {
|
|
3159
|
+
throw new SessionEnvironmentError("This browser cannot safely coordinate a Bounded session across tabs " +
|
|
3160
|
+
"(the Web Locks API is unavailable). Authenticated operations are disabled; " +
|
|
3161
|
+
"anonymous reads still work.");
|
|
3162
|
+
}
|
|
3163
|
+
return await navigator.locks.request(SESSION_LOCK_NAME, { mode: "exclusive" }, async () => await fn());
|
|
3164
|
+
}
|
|
3165
|
+
/**
|
|
3166
|
+
* Opaque per-login identifier stamped into the stored session.
|
|
3167
|
+
*
|
|
3168
|
+
* Minted on every `storeSession` (login, guest upgrade, wallet switch) and
|
|
3169
|
+
* PRESERVED across refresh commits. That is the whole point: it is what makes
|
|
3170
|
+
* "S1 refreshed into S1'" provable and distinguishable from "someone logged in
|
|
3171
|
+
* again", which token equality and address equality both get wrong — token
|
|
3172
|
+
* equality rejects a valid post-refresh retry, and address equality accepts a
|
|
3173
|
+
* same-wallet re-login as if it were the same session.
|
|
3174
|
+
*
|
|
3175
|
+
* The issuer has a server-side `familyId` that would serve, but `/session/refresh`
|
|
3176
|
+
* does not return it, so this is client-side.
|
|
3177
|
+
*/
|
|
3178
|
+
function mintSessionGeneration() {
|
|
3179
|
+
const c = typeof crypto !== "undefined" ? crypto : undefined;
|
|
3180
|
+
if (typeof (c === null || c === void 0 ? void 0 : c.randomUUID) === "function")
|
|
3181
|
+
return c.randomUUID();
|
|
3182
|
+
if (typeof (c === null || c === void 0 ? void 0 : c.getRandomValues) === "function") {
|
|
3183
|
+
const bytes = c.getRandomValues(new Uint8Array(16));
|
|
3184
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
3185
|
+
}
|
|
3186
|
+
// Last resort only; every runtime the SDK targets provides one of the above.
|
|
3187
|
+
return `g${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
|
|
3188
|
+
}
|
|
3189
|
+
/**
|
|
3190
|
+
* Decide a commit against what storage ACTUALLY holds right now.
|
|
3191
|
+
*
|
|
3192
|
+
* Must be called inside `withSessionLock`, with `current` read inside that same
|
|
3193
|
+
* critical section — comparing against a value read before the network call is
|
|
3194
|
+
* exactly the race this exists to prevent.
|
|
3195
|
+
*/
|
|
3196
|
+
function decideCommit(current, expected, next) {
|
|
3197
|
+
var _a;
|
|
3198
|
+
// Absence is SUPERSEDED, never "free to write". A logout that lands between a
|
|
3199
|
+
// refresh's read and its write must not be undone by that write. This is the
|
|
3200
|
+
// backstop that makes session resurrection impossible.
|
|
3201
|
+
if (!current)
|
|
3202
|
+
return "superseded";
|
|
3203
|
+
// A caller with no captured generation cannot prove lineage, so it must not
|
|
3204
|
+
// write. Reads mint a generation for legacy sessions before returning them, so
|
|
3205
|
+
// in practice this only catches a request that was in flight across that
|
|
3206
|
+
// upgrade. Fail closed: the caller retries and gets a provable session.
|
|
3207
|
+
if (expected.generation === null)
|
|
3208
|
+
return "superseded";
|
|
3209
|
+
// A different generation means a different LOGIN, which the address-based guard
|
|
3210
|
+
// cannot see when the same wallet signs in again.
|
|
3211
|
+
if (((_a = current.sessionGeneration) !== null && _a !== void 0 ? _a : null) !== expected.generation) {
|
|
3212
|
+
return "superseded";
|
|
3213
|
+
}
|
|
3214
|
+
if (current.idToken === next.idToken && current.refreshToken === next.refreshToken) {
|
|
3215
|
+
return "already-equal";
|
|
3216
|
+
}
|
|
3217
|
+
// Storage is exactly what this caller read before it went to the network, so
|
|
3218
|
+
// nothing has moved underneath and this write is the legitimate one.
|
|
3219
|
+
if (expected.idToken === undefined || current.idToken === expected.idToken) {
|
|
3220
|
+
return "applied";
|
|
3221
|
+
}
|
|
3222
|
+
// Same login, but storage has moved on: another caller's refresh of this same
|
|
3223
|
+
// session already landed. Theirs is the current truth — its rotated refresh
|
|
3224
|
+
// token is the live one — so defer rather than overwrite. Reporting a principal
|
|
3225
|
+
// change here would fail ordinary concurrent requests at expiry, which is the
|
|
3226
|
+
// common case, not a rare one.
|
|
3227
|
+
return "same-generation-winner";
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3032
3230
|
class WebSessionManager {
|
|
3231
|
+
/** Raw read of the stored blob. Callers inside the lock use this, never a cached copy. */
|
|
3232
|
+
static readRaw() {
|
|
3233
|
+
if (typeof window === "undefined")
|
|
3234
|
+
return null;
|
|
3235
|
+
const raw = localStorage.getItem(this.BOUNDED_SESSION_STORAGE_KEY);
|
|
3236
|
+
if (!raw)
|
|
3237
|
+
return null;
|
|
3238
|
+
try {
|
|
3239
|
+
return JSON.parse(raw);
|
|
3240
|
+
}
|
|
3241
|
+
catch (_a) {
|
|
3242
|
+
return null;
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
static writeRaw(sessionObj) {
|
|
3246
|
+
localStorage.setItem(this.BOUNDED_SESSION_STORAGE_KEY, JSON.stringify(sessionObj));
|
|
3247
|
+
}
|
|
3248
|
+
/**
|
|
3249
|
+
* Read the session, stamping a `sessionGeneration` if it predates them.
|
|
3250
|
+
*
|
|
3251
|
+
* Sessions written before generations existed have no lineage marker, so a
|
|
3252
|
+
* later refresh could not prove it descended from them. Minting on read
|
|
3253
|
+
* upgrades them in place; a request already in flight across that mint carries
|
|
3254
|
+
* no generation and fails closed at commit rather than writing unprovably.
|
|
3255
|
+
*/
|
|
3256
|
+
static async readSessionWithGeneration() {
|
|
3257
|
+
const existing = this.readRaw();
|
|
3258
|
+
if (!existing)
|
|
3259
|
+
return null;
|
|
3260
|
+
if (existing.sessionGeneration)
|
|
3261
|
+
return existing;
|
|
3262
|
+
return await withSessionLock(() => {
|
|
3263
|
+
// Re-read inside the lock: another tab may have minted one already.
|
|
3264
|
+
const current = this.readRaw();
|
|
3265
|
+
if (!current)
|
|
3266
|
+
return null;
|
|
3267
|
+
if (current.sessionGeneration)
|
|
3268
|
+
return current;
|
|
3269
|
+
const upgraded = Object.assign(Object.assign({}, current), { sessionGeneration: mintSessionGeneration() });
|
|
3270
|
+
this.writeRaw(upgraded);
|
|
3271
|
+
return upgraded;
|
|
3272
|
+
});
|
|
3273
|
+
}
|
|
3033
3274
|
/**
|
|
3034
3275
|
* Decode a base64url-encoded string (used by JWT payloads).
|
|
3035
3276
|
* Normalises base64url → standard base64 before calling browser atob().
|
|
@@ -3078,28 +3319,38 @@ class WebSessionManager {
|
|
|
3078
3319
|
// the RIGHT /session/refresh. Email overrides with humanAuthApiUrl; all
|
|
3079
3320
|
// wallet/guest/signature flows default to authApiUrl (the bounded-auth issuer).
|
|
3080
3321
|
const sessionIssuer = (issuer || config.authApiUrl || "").replace(/\/$/, "");
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3322
|
+
// A login always starts a NEW generation. Refresh commits preserve it, so
|
|
3323
|
+
// "same generation" means "same login", which is what lets a post-refresh
|
|
3324
|
+
// retry be told apart from a re-login by the same wallet.
|
|
3325
|
+
await withSessionLock(() => {
|
|
3326
|
+
this.writeRaw(Object.assign({ address,
|
|
3327
|
+
accessToken,
|
|
3328
|
+
idToken,
|
|
3329
|
+
refreshToken, appId: currentAppId, sessionGeneration: mintSessionGeneration() }, (sessionIssuer ? { issuer: sessionIssuer } : {})));
|
|
3330
|
+
});
|
|
3085
3331
|
}
|
|
3086
3332
|
/* ------------------------------------------------------------------ */
|
|
3087
3333
|
/* GET */
|
|
3088
3334
|
/* ------------------------------------------------------------------ */
|
|
3089
3335
|
static async getSession() {
|
|
3336
|
+
var _a;
|
|
3090
3337
|
if (typeof window === "undefined")
|
|
3091
3338
|
return null;
|
|
3092
|
-
const
|
|
3093
|
-
if (!
|
|
3339
|
+
const sessionObj = await this.readSessionWithGeneration();
|
|
3340
|
+
if (!sessionObj)
|
|
3094
3341
|
return null;
|
|
3095
|
-
const
|
|
3342
|
+
const generation = (_a = sessionObj.sessionGeneration) !== null && _a !== void 0 ? _a : null;
|
|
3096
3343
|
/* ---------- validate app-id ---------- */
|
|
3097
3344
|
const config = await getConfig();
|
|
3098
3345
|
if (sessionObj.appId && sessionObj.appId !== config.appId) {
|
|
3099
|
-
this.
|
|
3346
|
+
await this.clearSessionIfGeneration(generation);
|
|
3100
3347
|
return null;
|
|
3101
3348
|
}
|
|
3102
3349
|
/* ---------- check JWT expiration ---------- */
|
|
3350
|
+
// Deliberately NO skew buffer here, unlike the per-request freshness helper.
|
|
3351
|
+
// This is the restore path: at boot a token already past `exp` is unusable
|
|
3352
|
+
// either way, so clearing it is honest. Preserving it would only make
|
|
3353
|
+
// isAuthenticated() lie about a session that cannot authenticate anything.
|
|
3103
3354
|
try {
|
|
3104
3355
|
// Check expiry on the ID TOKEN — it is ALWAYS a JWT. The OIDC access_token for
|
|
3105
3356
|
// email/social (hosted) logins is an OPAQUE string (not a JWT), so decoding IT
|
|
@@ -3113,29 +3364,39 @@ class WebSessionManager {
|
|
|
3113
3364
|
// (mirrors the refresh-failed branch below) so isAuthenticated()
|
|
3114
3365
|
// does not stay true and every request does not 401 + retry forever.
|
|
3115
3366
|
if (!refreshToken) {
|
|
3116
|
-
this.
|
|
3367
|
+
await this.clearSessionIfGeneration(generation);
|
|
3117
3368
|
return null;
|
|
3118
3369
|
}
|
|
3119
3370
|
const refreshed = await refreshSession(refreshToken, sessionObj.issuer);
|
|
3120
3371
|
if ((refreshed === null || refreshed === void 0 ? void 0 : refreshed.idToken) && (refreshed === null || refreshed === void 0 ? void 0 : refreshed.accessToken)) {
|
|
3121
|
-
await this.
|
|
3372
|
+
const committed = await this.commitRefreshedTokens({ generation, idToken: sessionObj.idToken }, refreshed.idToken, refreshed.accessToken,
|
|
3122
3373
|
// Persist the ROTATED refresh token — the old one is now
|
|
3123
3374
|
// single-use spent; reusing it would revoke the family.
|
|
3124
3375
|
refreshed.refreshToken);
|
|
3125
|
-
|
|
3126
|
-
|
|
3376
|
+
// Superseded means a login or logout landed while we were on the
|
|
3377
|
+
// network. Returning our own stale null here would sign the UI out
|
|
3378
|
+
// of a session that is now perfectly valid, because init() applies
|
|
3379
|
+
// whatever restoreSession() returns. Re-read once and report THAT.
|
|
3380
|
+
if (committed.outcome === "superseded") {
|
|
3381
|
+
const current = this.readRaw();
|
|
3382
|
+
if (!current)
|
|
3383
|
+
return null;
|
|
3384
|
+
return { address: current.address, session: current };
|
|
3385
|
+
}
|
|
3386
|
+
const newObj = committed.session;
|
|
3387
|
+
if (!newObj)
|
|
3127
3388
|
return null;
|
|
3128
|
-
const newObj = JSON.parse(newSession);
|
|
3129
3389
|
return { address: newObj.address, session: newObj };
|
|
3130
3390
|
}
|
|
3131
|
-
// Refresh failed — clear stale session to prevent retry loops
|
|
3132
|
-
|
|
3391
|
+
// Refresh failed — clear stale session to prevent retry loops, but only
|
|
3392
|
+
// if it is still the session we set out to refresh.
|
|
3393
|
+
await this.clearSessionIfGeneration(generation);
|
|
3133
3394
|
return null;
|
|
3134
3395
|
}
|
|
3135
3396
|
}
|
|
3136
3397
|
catch (err) {
|
|
3137
3398
|
// Token decode or refresh failed — clear stale session to prevent retry loops
|
|
3138
|
-
this.
|
|
3399
|
+
await this.clearSessionIfGeneration(generation);
|
|
3139
3400
|
return null;
|
|
3140
3401
|
}
|
|
3141
3402
|
return { address: sessionObj.address, session: sessionObj };
|
|
@@ -3143,10 +3404,55 @@ class WebSessionManager {
|
|
|
3143
3404
|
/* ------------------------------------------------------------------ */
|
|
3144
3405
|
/* CLEAR */
|
|
3145
3406
|
/* ------------------------------------------------------------------ */
|
|
3146
|
-
|
|
3407
|
+
/**
|
|
3408
|
+
* Remove the session unconditionally (logout, app-id mismatch, dead refresh).
|
|
3409
|
+
*
|
|
3410
|
+
* ASYNC AND LOCKED, and callers MUST await it. Two failure modes hang on this:
|
|
3411
|
+
*
|
|
3412
|
+
* - Removing outside the lock lets a refresh that read before the logout write
|
|
3413
|
+
* the session back afterwards, resurrecting a logged-out session.
|
|
3414
|
+
* - Not awaiting lets `logout()` return while the credential is still in
|
|
3415
|
+
* storage, so the very next request authenticates as the user who just left.
|
|
3416
|
+
*/
|
|
3417
|
+
static async clearSession() {
|
|
3418
|
+
if (typeof window === "undefined")
|
|
3419
|
+
return;
|
|
3420
|
+
const removed = await withSessionLock(() => {
|
|
3421
|
+
if (localStorage.getItem(this.BOUNDED_SESSION_STORAGE_KEY) === null)
|
|
3422
|
+
return false;
|
|
3423
|
+
localStorage.removeItem(this.BOUNDED_SESSION_STORAGE_KEY);
|
|
3424
|
+
return true;
|
|
3425
|
+
});
|
|
3426
|
+
// Only announce a clear that actually happened, so a no-op cannot sign out
|
|
3427
|
+
// a UI that was already signed out.
|
|
3428
|
+
if (removed)
|
|
3429
|
+
notifySessionCleared();
|
|
3430
|
+
}
|
|
3431
|
+
/**
|
|
3432
|
+
* Remove the session only if it is still the one identified by `generation`.
|
|
3433
|
+
*
|
|
3434
|
+
* Used by every failure path that wants to drop a dead session without
|
|
3435
|
+
* trampling a newer login that landed while the failure was in flight.
|
|
3436
|
+
*/
|
|
3437
|
+
static async clearSessionIfGeneration(generation) {
|
|
3147
3438
|
if (typeof window === "undefined")
|
|
3148
3439
|
return;
|
|
3149
|
-
|
|
3440
|
+
const removed = await withSessionLock(() => {
|
|
3441
|
+
var _a;
|
|
3442
|
+
const current = this.readRaw();
|
|
3443
|
+
if (!current)
|
|
3444
|
+
return false;
|
|
3445
|
+
// No captured generation means no proof this is the session we meant to
|
|
3446
|
+
// drop. Leave it alone rather than risk removing a replacement.
|
|
3447
|
+
if (generation === null)
|
|
3448
|
+
return false;
|
|
3449
|
+
if (((_a = current.sessionGeneration) !== null && _a !== void 0 ? _a : null) !== generation)
|
|
3450
|
+
return false;
|
|
3451
|
+
localStorage.removeItem(this.BOUNDED_SESSION_STORAGE_KEY);
|
|
3452
|
+
return true;
|
|
3453
|
+
});
|
|
3454
|
+
if (removed)
|
|
3455
|
+
notifySessionCleared();
|
|
3150
3456
|
}
|
|
3151
3457
|
/* ------------------------------------------------------------------ */
|
|
3152
3458
|
/* IS-AUTH */
|
|
@@ -3181,39 +3487,63 @@ class WebSessionManager {
|
|
|
3181
3487
|
/* ------------------------------------------------------------------ */
|
|
3182
3488
|
/* UPDATE TOKENS */
|
|
3183
3489
|
/* ------------------------------------------------------------------ */
|
|
3184
|
-
|
|
3185
|
-
|
|
3490
|
+
/**
|
|
3491
|
+
* Commit refreshed tokens, bound to the session generation they were minted for.
|
|
3492
|
+
*
|
|
3493
|
+
* Returns WHICH of the four outcomes happened, and the session the caller must
|
|
3494
|
+
* actually use. The old version returned void and silently dropped a refused
|
|
3495
|
+
* write, so `getFreshAuthToken` handed back the refreshed token regardless — a
|
|
3496
|
+
* request could then execute as the previous user while storage and the UI had
|
|
3497
|
+
* already moved to a new one.
|
|
3498
|
+
*
|
|
3499
|
+
* Never hand back `idToken` directly: use `result.session`.
|
|
3500
|
+
*/
|
|
3501
|
+
static async commitRefreshedTokens(expected, idToken, accessToken, refreshToken) {
|
|
3186
3502
|
if (typeof window === "undefined")
|
|
3187
|
-
return;
|
|
3188
|
-
// Resolve config
|
|
3189
|
-
//
|
|
3190
|
-
// different session between the read and the write (#221).
|
|
3503
|
+
return { outcome: "superseded", session: null };
|
|
3504
|
+
// Resolve config BEFORE taking the lock: no awaits are allowed inside the
|
|
3505
|
+
// critical section, or another realm could interleave between read and write.
|
|
3191
3506
|
const config = await getConfig();
|
|
3192
|
-
const
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3507
|
+
const result = await withSessionLock(() => {
|
|
3508
|
+
var _a;
|
|
3509
|
+
// Read INSIDE the lock. The snapshot the caller captured before its
|
|
3510
|
+
// network refresh is only a comparison key; what gets written is decided
|
|
3511
|
+
// against what storage holds right now.
|
|
3512
|
+
const current = this.readRaw();
|
|
3513
|
+
/* ---------- app-id guard ---------- */
|
|
3514
|
+
if ((current === null || current === void 0 ? void 0 : current.appId) && current.appId !== config.appId) {
|
|
3515
|
+
localStorage.removeItem(this.BOUNDED_SESSION_STORAGE_KEY);
|
|
3516
|
+
return { outcome: "superseded", session: null, cleared: true };
|
|
3517
|
+
}
|
|
3518
|
+
const next = Object.assign(Object.assign(Object.assign({}, (current !== null && current !== void 0 ? current : {})), { idToken,
|
|
3519
|
+
accessToken }), (typeof refreshToken === "string" && refreshToken.length > 0
|
|
3520
|
+
? { refreshToken }
|
|
3521
|
+
: {}));
|
|
3522
|
+
const outcome = decideCommit(current, expected, next);
|
|
3523
|
+
if (outcome === "superseded")
|
|
3524
|
+
return { outcome, session: null };
|
|
3525
|
+
// Another caller's refresh of this same login already landed. Theirs is
|
|
3526
|
+
// the truth; reporting a principal change here would fail every ordinary
|
|
3527
|
+
// concurrent request at expiry, which is the common case.
|
|
3528
|
+
if (outcome === "same-generation-winner")
|
|
3529
|
+
return { outcome, session: current };
|
|
3530
|
+
if (outcome === "already-equal")
|
|
3531
|
+
return { outcome, session: current };
|
|
3532
|
+
/* ---------- cross-principal guard (#221) ---------- */
|
|
3533
|
+
// Belt-and-braces alongside the generation check: refuse to graft a
|
|
3534
|
+
// refreshed token onto a DIFFERENT wallet's stored session. Generations
|
|
3535
|
+
// catch a same-wallet re-login, which this address check cannot.
|
|
3536
|
+
if (!refreshedTokenMayReplaceStored(idToken, current.address, (s) => this.decodeBase64Url(s))) {
|
|
3537
|
+
console.warn("[WebSessionManager] commitRefreshedTokens: refreshed token wallet does not match the stored session address — discarding stale refresh");
|
|
3538
|
+
return { outcome: "superseded", session: null };
|
|
3539
|
+
}
|
|
3540
|
+
(_a = next.appId) !== null && _a !== void 0 ? _a : (next.appId = config.appId);
|
|
3541
|
+
this.writeRaw(next);
|
|
3542
|
+
return { outcome, session: next };
|
|
3543
|
+
});
|
|
3544
|
+
if (result.cleared)
|
|
3545
|
+
notifySessionCleared();
|
|
3546
|
+
return { outcome: result.outcome, session: result.session };
|
|
3217
3547
|
}
|
|
3218
3548
|
}
|
|
3219
3549
|
WebSessionManager.BOUNDED_SESSION_STORAGE_KEY = "bounded_session_storage";
|
|
@@ -3310,26 +3640,65 @@ class ReactNativeSessionManager {
|
|
|
3310
3640
|
// Issuer base for silent refresh (email overrides w/ humanAuthApiUrl; wallet/
|
|
3311
3641
|
// guest default to authApiUrl). See WebSessionManager for the rationale.
|
|
3312
3642
|
const sessionIssuer = (issuer || config.authApiUrl || "").replace(/\/$/, "");
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3643
|
+
// A login always starts a NEW generation; refresh commits preserve it.
|
|
3644
|
+
// See WebSessionManager for why lineage cannot be inferred from tokens or
|
|
3645
|
+
// address. RN is single-realm, so the lock resolves to direct execution.
|
|
3646
|
+
await withSessionLock(() => {
|
|
3647
|
+
this.writeRaw(Object.assign({ address,
|
|
3648
|
+
accessToken,
|
|
3649
|
+
idToken,
|
|
3650
|
+
refreshToken, appId: currentAppId, sessionGeneration: mintSessionGeneration() }, (sessionIssuer ? { issuer: sessionIssuer } : {})));
|
|
3651
|
+
});
|
|
3652
|
+
}
|
|
3653
|
+
/** Raw read of the stored blob. Callers inside the lock use this, never a cached copy. */
|
|
3654
|
+
static readRaw() {
|
|
3655
|
+
try {
|
|
3656
|
+
const raw = this.getStorage().getItem(this.BOUNDED_SESSION_STORAGE_KEY);
|
|
3657
|
+
return raw ? JSON.parse(raw) : null;
|
|
3658
|
+
}
|
|
3659
|
+
catch (_a) {
|
|
3660
|
+
return null;
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
static writeRaw(sessionObj) {
|
|
3664
|
+
this.getStorage().setItem(this.BOUNDED_SESSION_STORAGE_KEY, JSON.stringify(sessionObj));
|
|
3665
|
+
}
|
|
3666
|
+
/** Mirror of WebSessionManager.readSessionWithGeneration. */
|
|
3667
|
+
static async readSessionWithGeneration() {
|
|
3668
|
+
const existing = this.readRaw();
|
|
3669
|
+
if (!existing)
|
|
3670
|
+
return null;
|
|
3671
|
+
if (existing.sessionGeneration)
|
|
3672
|
+
return existing;
|
|
3673
|
+
return await withSessionLock(() => {
|
|
3674
|
+
const current = this.readRaw();
|
|
3675
|
+
if (!current)
|
|
3676
|
+
return null;
|
|
3677
|
+
if (current.sessionGeneration)
|
|
3678
|
+
return current;
|
|
3679
|
+
const upgraded = Object.assign(Object.assign({}, current), { sessionGeneration: mintSessionGeneration() });
|
|
3680
|
+
this.writeRaw(upgraded);
|
|
3681
|
+
return upgraded;
|
|
3682
|
+
});
|
|
3317
3683
|
}
|
|
3318
3684
|
/* ------------------------------------------------------------------ */
|
|
3319
3685
|
/* GET */
|
|
3320
3686
|
/* ------------------------------------------------------------------ */
|
|
3321
3687
|
static async getSession() {
|
|
3322
|
-
|
|
3323
|
-
|
|
3688
|
+
var _a;
|
|
3689
|
+
const sessionObj = await this.readSessionWithGeneration();
|
|
3690
|
+
if (!sessionObj)
|
|
3324
3691
|
return null;
|
|
3325
|
-
const
|
|
3692
|
+
const generation = (_a = sessionObj.sessionGeneration) !== null && _a !== void 0 ? _a : null;
|
|
3326
3693
|
/* ---------- validate app-id ---------- */
|
|
3327
3694
|
const config = await getConfig();
|
|
3328
3695
|
if (sessionObj.appId && sessionObj.appId !== config.appId) {
|
|
3329
|
-
this.
|
|
3696
|
+
await this.clearSessionIfGeneration(generation);
|
|
3330
3697
|
return null;
|
|
3331
3698
|
}
|
|
3332
3699
|
/* ---------- check JWT expiration ---------- */
|
|
3700
|
+
// No skew buffer, deliberately: this is the restore path, and at boot a token
|
|
3701
|
+
// already past `exp` cannot authenticate anything. See WebSessionManager.
|
|
3333
3702
|
try {
|
|
3334
3703
|
// Expiry on the ID TOKEN (always a JWT). The OIDC access_token for email/social
|
|
3335
3704
|
// logins is opaque, so decoding it threw → the catch wiped a valid session.
|
|
@@ -3341,26 +3710,35 @@ class ReactNativeSessionManager {
|
|
|
3341
3710
|
// (mirrors the refresh-failed branch below) so isAuthenticated()
|
|
3342
3711
|
// does not stay true and every request does not 401 + retry forever.
|
|
3343
3712
|
if (!refreshToken) {
|
|
3344
|
-
this.
|
|
3713
|
+
await this.clearSessionIfGeneration(generation);
|
|
3345
3714
|
return null;
|
|
3346
3715
|
}
|
|
3347
3716
|
const refreshed = await refreshSession(refreshToken, sessionObj.issuer);
|
|
3348
3717
|
if ((refreshed === null || refreshed === void 0 ? void 0 : refreshed.idToken) && (refreshed === null || refreshed === void 0 ? void 0 : refreshed.accessToken)) {
|
|
3349
|
-
await this.
|
|
3350
|
-
|
|
3351
|
-
|
|
3718
|
+
const committed = await this.commitRefreshedTokens({ generation, idToken: sessionObj.idToken }, refreshed.idToken, refreshed.accessToken, refreshed.refreshToken);
|
|
3719
|
+
// Superseded means a login or logout landed mid-refresh. Returning
|
|
3720
|
+
// our stale null would sign the UI out of a session that is now
|
|
3721
|
+
// valid, since init() applies whatever restoreSession() returns.
|
|
3722
|
+
if (committed.outcome === "superseded") {
|
|
3723
|
+
const current = this.readRaw();
|
|
3724
|
+
if (!current)
|
|
3725
|
+
return null;
|
|
3726
|
+
return { address: current.address, session: current };
|
|
3727
|
+
}
|
|
3728
|
+
const newObj = committed.session;
|
|
3729
|
+
if (!newObj)
|
|
3352
3730
|
return null;
|
|
3353
|
-
const newObj = JSON.parse(newSession);
|
|
3354
3731
|
return { address: newObj.address, session: newObj };
|
|
3355
3732
|
}
|
|
3356
|
-
// Refresh failed — clear stale session to prevent retry loops
|
|
3357
|
-
|
|
3733
|
+
// Refresh failed — clear stale session to prevent retry loops, but only
|
|
3734
|
+
// if it is still the session we set out to refresh.
|
|
3735
|
+
await this.clearSessionIfGeneration(generation);
|
|
3358
3736
|
return null;
|
|
3359
3737
|
}
|
|
3360
3738
|
}
|
|
3361
3739
|
catch (err) {
|
|
3362
3740
|
// Token decode or refresh failed — clear stale session to prevent retry loops
|
|
3363
|
-
this.
|
|
3741
|
+
await this.clearSessionIfGeneration(generation);
|
|
3364
3742
|
return null;
|
|
3365
3743
|
}
|
|
3366
3744
|
return { address: sessionObj.address, session: sessionObj };
|
|
@@ -3368,8 +3746,37 @@ class ReactNativeSessionManager {
|
|
|
3368
3746
|
/* ------------------------------------------------------------------ */
|
|
3369
3747
|
/* CLEAR */
|
|
3370
3748
|
/* ------------------------------------------------------------------ */
|
|
3371
|
-
|
|
3372
|
-
|
|
3749
|
+
/**
|
|
3750
|
+
* Remove the session unconditionally. Async and awaited for the same reason as
|
|
3751
|
+
* the web manager: a caller that does not await lets logout() return while the
|
|
3752
|
+
* credential is still readable.
|
|
3753
|
+
*/
|
|
3754
|
+
static async clearSession() {
|
|
3755
|
+
const removed = await withSessionLock(() => {
|
|
3756
|
+
if (this.getStorage().getItem(this.BOUNDED_SESSION_STORAGE_KEY) === null)
|
|
3757
|
+
return false;
|
|
3758
|
+
this.getStorage().removeItem(this.BOUNDED_SESSION_STORAGE_KEY);
|
|
3759
|
+
return true;
|
|
3760
|
+
});
|
|
3761
|
+
if (removed)
|
|
3762
|
+
notifySessionCleared();
|
|
3763
|
+
}
|
|
3764
|
+
/** Remove only if the stored session is still the one identified by `generation`. */
|
|
3765
|
+
static async clearSessionIfGeneration(generation) {
|
|
3766
|
+
const removed = await withSessionLock(() => {
|
|
3767
|
+
var _a;
|
|
3768
|
+
const current = this.readRaw();
|
|
3769
|
+
if (!current)
|
|
3770
|
+
return false;
|
|
3771
|
+
if (generation === null)
|
|
3772
|
+
return false;
|
|
3773
|
+
if (((_a = current.sessionGeneration) !== null && _a !== void 0 ? _a : null) !== generation)
|
|
3774
|
+
return false;
|
|
3775
|
+
this.getStorage().removeItem(this.BOUNDED_SESSION_STORAGE_KEY);
|
|
3776
|
+
return true;
|
|
3777
|
+
});
|
|
3778
|
+
if (removed)
|
|
3779
|
+
notifySessionCleared();
|
|
3373
3780
|
}
|
|
3374
3781
|
/* ------------------------------------------------------------------ */
|
|
3375
3782
|
/* IS-AUTH */
|
|
@@ -3412,36 +3819,44 @@ class ReactNativeSessionManager {
|
|
|
3412
3819
|
/* ------------------------------------------------------------------ */
|
|
3413
3820
|
/* UPDATE TOKENS */
|
|
3414
3821
|
/* ------------------------------------------------------------------ */
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3822
|
+
/**
|
|
3823
|
+
* Commit refreshed tokens bound to the generation they were minted for.
|
|
3824
|
+
* Mirror of WebSessionManager.commitRefreshedTokens; see it for the rationale.
|
|
3825
|
+
*/
|
|
3826
|
+
static async commitRefreshedTokens(expected, idToken, accessToken, refreshToken) {
|
|
3827
|
+
// Resolve config BEFORE the lock: no awaits inside the critical section.
|
|
3420
3828
|
const config = await getConfig();
|
|
3421
|
-
const
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3829
|
+
const result = await withSessionLock(() => {
|
|
3830
|
+
var _a;
|
|
3831
|
+
const current = this.readRaw();
|
|
3832
|
+
/* ---------- app-id guard ---------- */
|
|
3833
|
+
if ((current === null || current === void 0 ? void 0 : current.appId) && current.appId !== config.appId) {
|
|
3834
|
+
this.getStorage().removeItem(this.BOUNDED_SESSION_STORAGE_KEY);
|
|
3835
|
+
return { outcome: "superseded", session: null, cleared: true };
|
|
3836
|
+
}
|
|
3837
|
+
const next = Object.assign(Object.assign(Object.assign({}, (current !== null && current !== void 0 ? current : {})), { idToken,
|
|
3838
|
+
accessToken }), (typeof refreshToken === "string" && refreshToken.length > 0
|
|
3839
|
+
? { refreshToken }
|
|
3840
|
+
: {}));
|
|
3841
|
+
const outcome = decideCommit(current, expected, next);
|
|
3842
|
+
if (outcome === "superseded")
|
|
3843
|
+
return { outcome, session: null };
|
|
3844
|
+
if (outcome === "same-generation-winner")
|
|
3845
|
+
return { outcome, session: current };
|
|
3846
|
+
if (outcome === "already-equal")
|
|
3847
|
+
return { outcome, session: current };
|
|
3848
|
+
/* ---------- cross-principal guard (#220) ---------- */
|
|
3849
|
+
if (!refreshedTokenMayReplaceStored(idToken, current.address, (s) => this.decodeBase64Url(s))) {
|
|
3850
|
+
console.warn("[ReactNativeSessionManager] commitRefreshedTokens: refreshed token wallet does not match the stored session address — discarding stale refresh");
|
|
3851
|
+
return { outcome: "superseded", session: null };
|
|
3852
|
+
}
|
|
3853
|
+
(_a = next.appId) !== null && _a !== void 0 ? _a : (next.appId = config.appId);
|
|
3854
|
+
this.writeRaw(next);
|
|
3855
|
+
return { outcome, session: next };
|
|
3856
|
+
});
|
|
3857
|
+
if (result.cleared)
|
|
3858
|
+
notifySessionCleared();
|
|
3859
|
+
return { outcome: result.outcome, session: result.session };
|
|
3445
3860
|
}
|
|
3446
3861
|
}
|
|
3447
3862
|
ReactNativeSessionManager.BOUNDED_SESSION_STORAGE_KEY = "bounded_session_storage";
|
|
@@ -3474,11 +3889,6 @@ function getActiveSessionManager() {
|
|
|
3474
3889
|
: WebSessionManager;
|
|
3475
3890
|
}
|
|
3476
3891
|
|
|
3477
|
-
var sessionManager = /*#__PURE__*/Object.freeze({
|
|
3478
|
-
__proto__: null,
|
|
3479
|
-
getActiveSessionManager: getActiveSessionManager
|
|
3480
|
-
});
|
|
3481
|
-
|
|
3482
3892
|
const NO_AMBIENT_SERVER_SESSION = "Server sessions are not process-global. Use createWalletClient({ keypair }) and call that wallet client's methods.";
|
|
3483
3893
|
/* ------------------------------------------------------------------ */
|
|
3484
3894
|
/* SESSION MANAGER */
|
|
@@ -3536,7 +3946,7 @@ class ServerSessionManager {
|
|
|
3536
3946
|
const address = kp.publicKey.toBase58();
|
|
3537
3947
|
/* fetch nonce from auth API */
|
|
3538
3948
|
const nonce = await genAuthNonce();
|
|
3539
|
-
const { genSolanaMessage } = await Promise.resolve().then(function () { return require('./solana-message-
|
|
3949
|
+
const { genSolanaMessage } = await Promise.resolve().then(function () { return require('./solana-message-B0NoE4nC.js'); });
|
|
3540
3950
|
const message = await genSolanaMessage(address, nonce);
|
|
3541
3951
|
/* sign the message */
|
|
3542
3952
|
const sigBytes = nacl.sign.detached(new TextEncoder().encode(message), kp.secretKey);
|
|
@@ -3766,16 +4176,262 @@ function normalizeEvmAddress(raw) {
|
|
|
3766
4176
|
return /^0x[0-9a-f]{40}$/.test(v) ? v : null;
|
|
3767
4177
|
}
|
|
3768
4178
|
|
|
3769
|
-
|
|
4179
|
+
/**
|
|
4180
|
+
* The one place a caller turns "I need a credential" into a usable token.
|
|
4181
|
+
*
|
|
4182
|
+
* Both transports go through this. The WebSocket path grew a good version of this
|
|
4183
|
+
* logic first; the HTTP path had none at all and simply sent whatever was in
|
|
4184
|
+
* storage, which is what let an expired token reach the server and come back as a
|
|
4185
|
+
* misleading policy denial.
|
|
4186
|
+
*
|
|
4187
|
+
* THE CONTRACT, which callers depend on:
|
|
4188
|
+
*
|
|
4189
|
+
* returns null -> there is no session. Send the request anonymously; public
|
|
4190
|
+
* reads must keep working for logged-out visitors.
|
|
4191
|
+
* returns token -> use exactly this token.
|
|
4192
|
+
* throws -> there IS a session and it is unusable. NEVER fall back to an
|
|
4193
|
+
* anonymous request, or the caller lands in policy as an
|
|
4194
|
+
* anonymous user and gets the same misleading 403 this whole
|
|
4195
|
+
* change exists to remove.
|
|
4196
|
+
*
|
|
4197
|
+
* Four corrections over the WebSocket original, each of which was a real defect:
|
|
4198
|
+
*
|
|
4199
|
+
* - No sticky module-global failure. The original latched any refresh failure
|
|
4200
|
+
* into `ambientAuthFailure` and rethrew it on every later call, so ONE
|
|
4201
|
+
* transient network blip disabled refresh until an unrelated auth-state
|
|
4202
|
+
* change reset it.
|
|
4203
|
+
* - "Expiring soon" and "already expired" are different. The original collapsed
|
|
4204
|
+
* both behind a 60s buffer, so a transient refresh failure threw away a token
|
|
4205
|
+
* that was still valid for another 59 seconds.
|
|
4206
|
+
* - A missing or non-numeric `exp` is unusable, not fresh. `payload.exp * 1000`
|
|
4207
|
+
* is NaN, and `now > NaN` is false, so such a token read as perfectly fresh.
|
|
4208
|
+
* - Storage is cleared only on a DEFINITIVE rejection, never on a transient one.
|
|
4209
|
+
*/
|
|
4210
|
+
/**
|
|
4211
|
+
* Stable machine code for "there is a session and it cannot be used".
|
|
4212
|
+
*
|
|
4213
|
+
* The VALUE is deliberately unchanged from when this lived in subscription-v2 as
|
|
4214
|
+
* `WS_AUTH_EXPIRED_CODE`: it is public surface that consumers branch on
|
|
4215
|
+
* (`e.code === 'auth_expired'`), so renaming it would break them for no gain. Only
|
|
4216
|
+
* the identifier lost its `WS_` prefix, now that HTTP raises it too.
|
|
4217
|
+
*
|
|
4218
|
+
* The realtime worker's 401 body uses `invalid_or_expired_session` for the same
|
|
4219
|
+
* condition on the wire; this is the client-side counterpart.
|
|
4220
|
+
*/
|
|
4221
|
+
const AUTH_EXPIRED_CODE = 'auth_expired';
|
|
4222
|
+
/** The principal changed under a request that was already in flight. */
|
|
4223
|
+
const AUTH_CHANGED_CODE = 'auth_changed';
|
|
4224
|
+
function makeAuthExpiredError(message, status) {
|
|
4225
|
+
const err = new Error(`${AUTH_EXPIRED_CODE}: ${message}`);
|
|
4226
|
+
err.code = AUTH_EXPIRED_CODE;
|
|
4227
|
+
if (status !== undefined)
|
|
4228
|
+
err.status = status;
|
|
4229
|
+
return err;
|
|
4230
|
+
}
|
|
4231
|
+
function makeAuthChangedError(message) {
|
|
4232
|
+
const err = new Error(`${AUTH_CHANGED_CODE}: ${message}`);
|
|
4233
|
+
err.code = AUTH_CHANGED_CODE;
|
|
4234
|
+
return err;
|
|
4235
|
+
}
|
|
4236
|
+
function isAuthExpiredError(error) {
|
|
4237
|
+
const code = error === null || error === void 0 ? void 0 : error.code;
|
|
4238
|
+
return !!error && typeof error === 'object'
|
|
4239
|
+
&& (code === AUTH_EXPIRED_CODE || code === AUTH_CHANGED_CODE);
|
|
4240
|
+
}
|
|
4241
|
+
function normalizeAuthExpiredError(error, fallbackMessage) {
|
|
4242
|
+
var _a, _b;
|
|
4243
|
+
if (isAuthExpiredError(error))
|
|
4244
|
+
return error;
|
|
4245
|
+
const status = (_b = (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : error === null || error === void 0 ? void 0 : error.code;
|
|
4246
|
+
return makeAuthExpiredError(fallbackMessage, status);
|
|
4247
|
+
}
|
|
4248
|
+
/** Refresh this many ms before `exp`, so a token cannot die mid-flight. */
|
|
4249
|
+
const TOKEN_EXPIRY_SKEW_MS = 60000;
|
|
4250
|
+
function classifyToken(token, nowMs = Date.now()) {
|
|
4251
|
+
let exp;
|
|
4252
|
+
try {
|
|
4253
|
+
const payload = JSON.parse(decodeBase64Url(token.split('.')[1]));
|
|
4254
|
+
exp = payload === null || payload === void 0 ? void 0 : payload.exp;
|
|
4255
|
+
}
|
|
4256
|
+
catch (_a) {
|
|
4257
|
+
return 'unusable';
|
|
4258
|
+
}
|
|
4259
|
+
// A token with no numeric exp used to compare against NaN and read as fresh.
|
|
4260
|
+
if (typeof exp !== 'number' || !Number.isFinite(exp))
|
|
4261
|
+
return 'unusable';
|
|
4262
|
+
const expiresAtMs = exp * 1000;
|
|
4263
|
+
if (nowMs >= expiresAtMs)
|
|
4264
|
+
return 'expired';
|
|
4265
|
+
if (nowMs >= expiresAtMs - TOKEN_EXPIRY_SKEW_MS)
|
|
4266
|
+
return 'expiring-soon';
|
|
4267
|
+
return 'fresh';
|
|
4268
|
+
}
|
|
4269
|
+
/** Short, non-secret fingerprint used only to key the in-flight map. Never logged. */
|
|
4270
|
+
function tokenFingerprint(token) {
|
|
4271
|
+
let h = 5381;
|
|
4272
|
+
for (let i = 0; i < token.length; i++)
|
|
4273
|
+
h = ((h << 5) + h + token.charCodeAt(i)) >>> 0;
|
|
4274
|
+
return h.toString(16);
|
|
4275
|
+
}
|
|
4276
|
+
/**
|
|
4277
|
+
* In-flight refresh TRANSACTIONS, keyed by the session they act on.
|
|
4278
|
+
*
|
|
4279
|
+
* The issuer-level dedupe in `refreshSession` shares only the network response;
|
|
4280
|
+
* each caller then commits independently. With a generation check that means the
|
|
4281
|
+
* first commit wins and every other caller sees storage it does not recognise —
|
|
4282
|
+
* so a burst of ordinary requests at expiry would all fail as "the user changed".
|
|
4283
|
+
* Deduping the WHOLE snapshot -> refresh -> commit makes them share one outcome.
|
|
4284
|
+
*
|
|
4285
|
+
* Keyed by generation as well as app, so a caller that arrives after a re-login
|
|
4286
|
+
* starts its own transaction instead of joining a stale one.
|
|
4287
|
+
*/
|
|
4288
|
+
const refreshTransactions = new Map();
|
|
4289
|
+
function transactionKey(scope, appId, generation, refreshToken) {
|
|
4290
|
+
return `${scope}:${appId}:${generation !== null && generation !== void 0 ? generation : 'none'}:${tokenFingerprint(refreshToken)}`;
|
|
4291
|
+
}
|
|
4292
|
+
function isDefinitiveRejection(error) {
|
|
4293
|
+
var _a;
|
|
4294
|
+
const status = (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status;
|
|
4295
|
+
return status === 401 || status === 403;
|
|
4296
|
+
}
|
|
4297
|
+
async function readSessionParts(isServer) {
|
|
4298
|
+
var _a, _b, _c, _d, _e, _f;
|
|
3770
4299
|
if (isServer) {
|
|
3771
|
-
const
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
4300
|
+
const session = await ServerSessionManager.instance.getSession();
|
|
4301
|
+
return {
|
|
4302
|
+
idToken: (_a = session === null || session === void 0 ? void 0 : session.idToken) !== null && _a !== void 0 ? _a : null,
|
|
4303
|
+
refreshToken: (_b = session === null || session === void 0 ? void 0 : session.refreshToken) !== null && _b !== void 0 ? _b : null,
|
|
4304
|
+
issuer: undefined,
|
|
4305
|
+
generation: null,
|
|
4306
|
+
};
|
|
4307
|
+
}
|
|
4308
|
+
const mgr = getActiveSessionManager();
|
|
4309
|
+
const session = await mgr.readSessionWithGeneration();
|
|
4310
|
+
return {
|
|
4311
|
+
idToken: (_c = session === null || session === void 0 ? void 0 : session.idToken) !== null && _c !== void 0 ? _c : null,
|
|
4312
|
+
refreshToken: (_d = session === null || session === void 0 ? void 0 : session.refreshToken) !== null && _d !== void 0 ? _d : null,
|
|
4313
|
+
issuer: (_e = session === null || session === void 0 ? void 0 : session.issuer) !== null && _e !== void 0 ? _e : undefined,
|
|
4314
|
+
generation: (_f = session === null || session === void 0 ? void 0 : session.sessionGeneration) !== null && _f !== void 0 ? _f : null,
|
|
4315
|
+
};
|
|
4316
|
+
}
|
|
4317
|
+
async function commit(isServer, expected, refreshed) {
|
|
4318
|
+
var _a, _b;
|
|
4319
|
+
if (isServer) {
|
|
4320
|
+
const mgr = ServerSessionManager.instance;
|
|
4321
|
+
const session = await mgr.getSession();
|
|
4322
|
+
if (!session)
|
|
4323
|
+
return { outcome: 'superseded', token: null };
|
|
4324
|
+
const next = Object.assign(Object.assign(Object.assign({}, session), { idToken: refreshed.idToken, accessToken: refreshed.accessToken }), (refreshed.refreshToken ? { refreshToken: refreshed.refreshToken } : {}));
|
|
4325
|
+
mgr.setSession(next);
|
|
4326
|
+
return { outcome: 'applied', token: next.idToken };
|
|
4327
|
+
}
|
|
4328
|
+
const result = await getActiveSessionManager().commitRefreshedTokens(expected, refreshed.idToken, refreshed.accessToken, refreshed.refreshToken);
|
|
4329
|
+
// NEVER return refreshed.idToken directly. When the commit is refused or lost,
|
|
4330
|
+
// that token was not stored, and using it would authenticate the request as
|
|
4331
|
+
// whoever the refresh was for rather than whoever the session now belongs to.
|
|
4332
|
+
return { outcome: result.outcome, token: (_b = (_a = result.session) === null || _a === void 0 ? void 0 : _a.idToken) !== null && _b !== void 0 ? _b : null };
|
|
4333
|
+
}
|
|
4334
|
+
/**
|
|
4335
|
+
* Get a token that is safe to send right now, refreshing if needed.
|
|
4336
|
+
*
|
|
4337
|
+
* `null` means anonymous is fine. A throw means there is a session that cannot be
|
|
4338
|
+
* used; the caller must surface it rather than downgrade to anonymous.
|
|
4339
|
+
*/
|
|
4340
|
+
async function getFreshAuthToken(isServer = false, options = {}) {
|
|
4341
|
+
const { idToken, refreshToken, issuer, generation } = await readSessionParts(isServer);
|
|
4342
|
+
// No session at all: anonymous is legitimate.
|
|
4343
|
+
if (!idToken)
|
|
4344
|
+
return null;
|
|
4345
|
+
const force = options.force === true;
|
|
4346
|
+
const freshness = classifyToken(idToken);
|
|
4347
|
+
if (!force && freshness === 'fresh')
|
|
4348
|
+
return idToken;
|
|
4349
|
+
// Still valid for a little longer, so a failed refresh can fall back to it —
|
|
4350
|
+
// but never when forced, since the server has already refused this exact token.
|
|
4351
|
+
const stillUsable = !force && freshness === 'expiring-soon';
|
|
4352
|
+
if (!refreshToken) {
|
|
4353
|
+
if (stillUsable)
|
|
4354
|
+
return idToken;
|
|
4355
|
+
// Nothing can revive this session. Drop it so isAuthenticated() stops lying
|
|
4356
|
+
// and the UI is told, then fail loudly instead of going anonymous.
|
|
4357
|
+
await clearDeadSession(isServer, generation);
|
|
4358
|
+
throw makeAuthExpiredError('Authentication expired and no refresh token is available');
|
|
4359
|
+
}
|
|
4360
|
+
const config = await getConfig();
|
|
4361
|
+
const key = transactionKey(isServer ? 'server' : 'web', config.appId, generation, refreshToken);
|
|
4362
|
+
const existing = refreshTransactions.get(key);
|
|
4363
|
+
if (existing)
|
|
4364
|
+
return await existing;
|
|
4365
|
+
const transaction = (async () => {
|
|
4366
|
+
var _a, _b, _c;
|
|
4367
|
+
try {
|
|
4368
|
+
const refreshed = await refreshSession(refreshToken, issuer);
|
|
4369
|
+
if (!(refreshed === null || refreshed === void 0 ? void 0 : refreshed.idToken) || !(refreshed === null || refreshed === void 0 ? void 0 : refreshed.accessToken)) {
|
|
4370
|
+
throw makeAuthExpiredError('Authentication refresh returned an incomplete session');
|
|
4371
|
+
}
|
|
4372
|
+
const { outcome, token } = await commit(isServer, { generation, idToken }, refreshed);
|
|
4373
|
+
if (outcome === 'superseded') {
|
|
4374
|
+
// A login or logout landed while we were on the network. Refuse rather
|
|
4375
|
+
// than authenticate this request as the principal that just left.
|
|
4376
|
+
throw makeAuthChangedError('The signed-in account changed while this request was in flight');
|
|
4377
|
+
}
|
|
4378
|
+
// applied / already-equal / same-generation-winner are all successes: the
|
|
4379
|
+
// stored session is valid for this same login either way.
|
|
4380
|
+
if (!token) {
|
|
4381
|
+
throw makeAuthExpiredError('Authentication refresh produced no usable token');
|
|
4382
|
+
}
|
|
4383
|
+
return token;
|
|
4384
|
+
}
|
|
4385
|
+
catch (error) {
|
|
4386
|
+
if (isAuthExpiredError(error))
|
|
4387
|
+
throw error;
|
|
4388
|
+
// Log the STATUS only: the axios error carries config.data, which holds
|
|
4389
|
+
// the refresh token, and must never reach a console or log sink.
|
|
4390
|
+
const status = (_c = (_b = (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : error === null || error === void 0 ? void 0 : error.code) !== null && _c !== void 0 ? _c : 'unknown';
|
|
4391
|
+
if (isDefinitiveRejection(error)) {
|
|
4392
|
+
// The issuer says this refresh token is dead. Drop the session.
|
|
4393
|
+
await clearDeadSession(isServer, generation);
|
|
4394
|
+
throw makeAuthExpiredError('Authentication expired and refresh was rejected', status);
|
|
4395
|
+
}
|
|
4396
|
+
// TRANSIENT: network blip, 5xx, timeout. Keep the session — it may well
|
|
4397
|
+
// work in a moment. If the access token is still inside its skew window
|
|
4398
|
+
// it is valid right now, so use it rather than failing the request.
|
|
4399
|
+
if (stillUsable)
|
|
4400
|
+
return idToken;
|
|
4401
|
+
throw makeAuthExpiredError('Authentication expired and refresh failed', status);
|
|
4402
|
+
}
|
|
4403
|
+
finally {
|
|
4404
|
+
refreshTransactions.delete(key);
|
|
4405
|
+
}
|
|
4406
|
+
})();
|
|
4407
|
+
refreshTransactions.set(key, transaction);
|
|
4408
|
+
return await transaction;
|
|
4409
|
+
}
|
|
4410
|
+
async function clearDeadSession(isServer, generation) {
|
|
4411
|
+
if (isServer) {
|
|
4412
|
+
try {
|
|
4413
|
+
ServerSessionManager.instance.clearSession();
|
|
3775
4414
|
}
|
|
3776
|
-
|
|
4415
|
+
catch ( /* best effort */_a) { /* best effort */ }
|
|
4416
|
+
return;
|
|
4417
|
+
}
|
|
4418
|
+
try {
|
|
4419
|
+
// Conditional: never remove a session that a concurrent login already replaced.
|
|
4420
|
+
await getActiveSessionManager().clearSessionIfGeneration(generation);
|
|
3777
4421
|
}
|
|
3778
|
-
|
|
4422
|
+
catch ( /* a clear failure must not mask the auth error we are about to throw */_b) { /* a clear failure must not mask the auth error we are about to throw */ }
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4425
|
+
async function createBearerToken(isServer) {
|
|
4426
|
+
// Both branches go through the freshness helper now. The web branch used to read
|
|
4427
|
+
// localStorage synchronously and send whatever was there, so an expired token was
|
|
4428
|
+
// handed to the server on every request and only recovered from AFTER a 401 —
|
|
4429
|
+
// and until the worker started returning 401 for a rejected bearer, never.
|
|
4430
|
+
//
|
|
4431
|
+
// A throw here is deliberate and must propagate: it means a session exists but
|
|
4432
|
+
// cannot be used. Swallowing it and sending the request anonymously would put
|
|
4433
|
+
// the caller back in policy as an anonymous user, which is the original bug.
|
|
4434
|
+
const idToken = await getFreshAuthToken(isServer);
|
|
3779
4435
|
if (!idToken) {
|
|
3780
4436
|
return null;
|
|
3781
4437
|
}
|
|
@@ -3859,41 +4515,18 @@ async function getIdToken(isServer) {
|
|
|
3859
4515
|
}
|
|
3860
4516
|
return getActiveSessionManager().getIdToken();
|
|
3861
4517
|
}
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
}
|
|
3870
|
-
/** The auth issuer base that minted the active client session, so a refresh POSTs
|
|
3871
|
-
* to the right /session/refresh. Undefined on the server (keypair, no refresh). */
|
|
3872
|
-
function getSessionIssuer(isServer) {
|
|
4518
|
+
/**
|
|
4519
|
+
* Read the active session's generation, minting one for a pre-generation session.
|
|
4520
|
+
*
|
|
4521
|
+
* The server manager is a single in-memory session per process, so it has no
|
|
4522
|
+
* cross-realm lineage problem and reports null.
|
|
4523
|
+
*/
|
|
4524
|
+
async function getSessionGeneration(isServer = false) {
|
|
3873
4525
|
var _a;
|
|
3874
4526
|
if (isServer)
|
|
3875
|
-
return
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
async function updateIdTokenAndAccessToken(idToken, accessToken, isServer = false, refreshToken) {
|
|
3879
|
-
if (isServer) {
|
|
3880
|
-
const sessionMgr = ServerSessionManager.instance;
|
|
3881
|
-
// Ensure we have a session to update; if missing, let lazy creation happen on next request.
|
|
3882
|
-
let session = null;
|
|
3883
|
-
try {
|
|
3884
|
-
session = await sessionMgr.getSession();
|
|
3885
|
-
}
|
|
3886
|
-
catch (_a) {
|
|
3887
|
-
return;
|
|
3888
|
-
}
|
|
3889
|
-
if (!session) {
|
|
3890
|
-
return;
|
|
3891
|
-
}
|
|
3892
|
-
sessionMgr.setSession(Object.assign(Object.assign(Object.assign({}, session), { idToken,
|
|
3893
|
-
accessToken }), (refreshToken ? { refreshToken } : {})));
|
|
3894
|
-
return;
|
|
3895
|
-
}
|
|
3896
|
-
await getActiveSessionManager().updateIdTokenAndAccessToken(idToken, accessToken, refreshToken);
|
|
4527
|
+
return null;
|
|
4528
|
+
const session = await getActiveSessionManager().readSessionWithGeneration();
|
|
4529
|
+
return (_a = session === null || session === void 0 ? void 0 : session.sessionGeneration) !== null && _a !== void 0 ? _a : null;
|
|
3897
4530
|
}
|
|
3898
4531
|
|
|
3899
4532
|
// ---------------------------------------------------------------------------
|
|
@@ -4115,57 +4748,44 @@ axiosRetry(apiClient, {
|
|
|
4115
4748
|
}
|
|
4116
4749
|
},
|
|
4117
4750
|
});
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4751
|
+
/**
|
|
4752
|
+
* Recover from a 401, bound to the session that authenticated the ORIGINAL attempt.
|
|
4753
|
+
*
|
|
4754
|
+
* The dedupe, rotation and conditional-clear all live in `getFreshAuthToken` now;
|
|
4755
|
+
* this only adds the principal binding a reactive retry needs.
|
|
4756
|
+
*
|
|
4757
|
+
* Why the binding matters: `createAuthHeader` is acquired inside `executeRequest`,
|
|
4758
|
+
* so a retry re-reads whatever session is current at retry time. If B logs in while
|
|
4759
|
+
* A's request is in flight, A's 401 handler used to refresh B's session and replay
|
|
4760
|
+
* A's exact request as B — writing data under the wrong principal. Making a rejected
|
|
4761
|
+
* bearer return 401 turned that from a near-impossible race into the normal recovery
|
|
4762
|
+
* path, so it has to be closed here.
|
|
4763
|
+
*
|
|
4764
|
+
* Returns true only when the retry may proceed under the SAME login.
|
|
4765
|
+
*/
|
|
4766
|
+
async function refreshAuthSessionOnce(capturedGeneration, isServer) {
|
|
4767
|
+
try {
|
|
4768
|
+
// force: the server just rejected the stored token, so a locally-fresh-looking
|
|
4769
|
+
// one is not evidence of anything. Without this, a 401 caused by anything the
|
|
4770
|
+
// client cannot see (revoked family, app mismatch, key rotation) would be
|
|
4771
|
+
// "recovered" by replaying the exact same credential.
|
|
4772
|
+
const token = await getFreshAuthToken(isServer, { force: true });
|
|
4773
|
+
if (!token)
|
|
4774
|
+
return false;
|
|
4775
|
+
// Equal generation covers both "unchanged" and "refreshed descendant": a
|
|
4776
|
+
// refresh preserves the generation, a re-login mints a new one. Comparing
|
|
4777
|
+
// tokens instead would reject a perfectly good retry whenever a concurrent
|
|
4778
|
+
// request refreshed first, which at expiry is the common case.
|
|
4779
|
+
const currentGeneration = await getSessionGeneration(isServer);
|
|
4780
|
+
if (currentGeneration !== capturedGeneration)
|
|
4781
|
+
return false;
|
|
4782
|
+
return true;
|
|
4138
4783
|
}
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
return
|
|
4784
|
+
catch (_a) {
|
|
4785
|
+
// Includes auth_changed and a definitively rejected refresh. Either way the
|
|
4786
|
+
// retry must not proceed; the caller surfaces the original 401.
|
|
4787
|
+
return false;
|
|
4143
4788
|
}
|
|
4144
|
-
const refreshPromise = (async () => {
|
|
4145
|
-
var _a, _b;
|
|
4146
|
-
try {
|
|
4147
|
-
const refreshData = await refreshSession(refreshToken, getSessionIssuer(isServer));
|
|
4148
|
-
if (!(refreshData === null || refreshData === void 0 ? void 0 : refreshData.idToken) || !(refreshData === null || refreshData === void 0 ? void 0 : refreshData.accessToken)) {
|
|
4149
|
-
return false;
|
|
4150
|
-
}
|
|
4151
|
-
await updateIdTokenAndAccessToken(refreshData.idToken, refreshData.accessToken, isServer,
|
|
4152
|
-
// Persist the ROTATED refresh token — the old one is single-use spent.
|
|
4153
|
-
refreshData.refreshToken);
|
|
4154
|
-
return true;
|
|
4155
|
-
}
|
|
4156
|
-
catch (error) {
|
|
4157
|
-
if (!isServer && (((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) === 401 || ((_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.status) === 403)) {
|
|
4158
|
-
const { getActiveSessionManager } = await Promise.resolve().then(function () { return sessionManager; });
|
|
4159
|
-
getActiveSessionManager().clearSession();
|
|
4160
|
-
}
|
|
4161
|
-
return false;
|
|
4162
|
-
}
|
|
4163
|
-
finally {
|
|
4164
|
-
refreshInFlight.delete(key);
|
|
4165
|
-
}
|
|
4166
|
-
})();
|
|
4167
|
-
refreshInFlight.set(key, refreshPromise);
|
|
4168
|
-
return refreshPromise;
|
|
4169
4789
|
}
|
|
4170
4790
|
async function makeApiRequest(method, urlPath, data, _overrides) {
|
|
4171
4791
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
@@ -4173,6 +4793,24 @@ async function makeApiRequest(method, urlPath, data, _overrides) {
|
|
|
4173
4793
|
// fall back to the module-global init() config.
|
|
4174
4794
|
const config = (_a = _overrides === null || _overrides === void 0 ? void 0 : _overrides._config) !== null && _a !== void 0 ? _a : await getConfig();
|
|
4175
4795
|
const readOnly = (_overrides === null || _overrides === void 0 ? void 0 : _overrides._readOnly) === true;
|
|
4796
|
+
// CREDENTIAL PROVENANCE.
|
|
4797
|
+
//
|
|
4798
|
+
// Only a MANAGED credential — one this SDK minted from the global session — may
|
|
4799
|
+
// trigger global-session recovery. A caller that supplies its own Authorization
|
|
4800
|
+
// (via `_overrides.headers` or a wallet client's `_getAuthHeaders`) owns that
|
|
4801
|
+
// token; refreshing or clearing the global session because THAT token got a 401
|
|
4802
|
+
// would sign out an unrelated user. Rare before, because a rejected bearer used
|
|
4803
|
+
// to come back as 403; routine now that it is a 401.
|
|
4804
|
+
const usesOverrideAuthHeader = !!(_overrides === null || _overrides === void 0 ? void 0 : _overrides.headers)
|
|
4805
|
+
&& Object.keys(_overrides.headers).some((name) => name.toLowerCase() === "authorization");
|
|
4806
|
+
const isManagedCredential = !readOnly
|
|
4807
|
+
&& !(_overrides === null || _overrides === void 0 ? void 0 : _overrides._getAuthHeaders)
|
|
4808
|
+
&& !usesOverrideAuthHeader;
|
|
4809
|
+
// The session that authenticated the FIRST attempt. 401 recovery is bound to it
|
|
4810
|
+
// so a retry can never execute under a principal that replaced it mid-flight.
|
|
4811
|
+
const capturedGeneration = isManagedCredential
|
|
4812
|
+
? await getSessionGeneration(config.isServer)
|
|
4813
|
+
: null;
|
|
4176
4814
|
async function executeRequest() {
|
|
4177
4815
|
var _a;
|
|
4178
4816
|
// When _getAuthHeaders is provided (wallet client), use it as the sole auth source.
|
|
@@ -4222,30 +4860,50 @@ async function makeApiRequest(method, urlPath, data, _overrides) {
|
|
|
4222
4860
|
try {
|
|
4223
4861
|
return await executeRequest();
|
|
4224
4862
|
}
|
|
4225
|
-
catch (
|
|
4863
|
+
catch (initialError) {
|
|
4864
|
+
// The error that ultimately gets normalized below. A retry REPLACES it, so a
|
|
4865
|
+
// genuine post-refresh policy denial surfaces as itself.
|
|
4866
|
+
//
|
|
4867
|
+
// Both retry paths used to `return await executeRequest()` from inside this
|
|
4868
|
+
// catch, which meant a retry that failed skipped the whole normalization
|
|
4869
|
+
// block further down. The default path additionally swallowed the retry error
|
|
4870
|
+
// and rethrew the ORIGINAL 401 — so `401 -> refresh succeeds -> retry is
|
|
4871
|
+
// genuinely denied 403` reached the caller as a 401, contradicting the point
|
|
4872
|
+
// of returning 401 for expired credentials in the first place.
|
|
4873
|
+
let error = initialError;
|
|
4226
4874
|
if (((_b = error.response) === null || _b === void 0 ? void 0 : _b.status) === 401) {
|
|
4227
4875
|
// Read-only scoped instance: it holds no session, so there is nothing
|
|
4228
4876
|
// to refresh. Surface the 401 without touching the global session.
|
|
4229
4877
|
if (readOnly) {
|
|
4230
4878
|
throw error;
|
|
4231
4879
|
}
|
|
4232
|
-
// Wallet client path: use client-specific retry, skip global fallback entirely
|
|
4233
4880
|
if (_overrides === null || _overrides === void 0 ? void 0 : _overrides._clearAuth) {
|
|
4881
|
+
// Wallet client path: client-specific retry, never the global session.
|
|
4234
4882
|
await _overrides._clearAuth();
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
if (!refreshed) {
|
|
4242
|
-
throw new Error("Unable to refresh auth session");
|
|
4883
|
+
try {
|
|
4884
|
+
return await executeRequest();
|
|
4885
|
+
}
|
|
4886
|
+
catch (retryError) {
|
|
4887
|
+
// Normalize the RETRY's own failure. Still no global auth touched.
|
|
4888
|
+
error = retryError;
|
|
4243
4889
|
}
|
|
4244
|
-
return await executeRequest();
|
|
4245
4890
|
}
|
|
4246
|
-
|
|
4247
|
-
|
|
4891
|
+
else if (isManagedCredential) {
|
|
4892
|
+
// Default path: recover the global session, bound to the principal
|
|
4893
|
+
// that authenticated the first attempt.
|
|
4894
|
+
const recovered = await refreshAuthSessionOnce(capturedGeneration, config.isServer);
|
|
4895
|
+
if (recovered) {
|
|
4896
|
+
try {
|
|
4897
|
+
return await executeRequest();
|
|
4898
|
+
}
|
|
4899
|
+
catch (retryError) {
|
|
4900
|
+
error = retryError;
|
|
4901
|
+
}
|
|
4902
|
+
}
|
|
4903
|
+
// Not recovered: keep the original 401 for normalization below.
|
|
4248
4904
|
}
|
|
4905
|
+
// Unmanaged credential (caller-supplied Authorization): its 401 is the
|
|
4906
|
+
// caller's to handle. Never refresh or clear the global session for it.
|
|
4249
4907
|
}
|
|
4250
4908
|
if (((_c = error.response) === null || _c === void 0 ? void 0 : _c.status) == 404) {
|
|
4251
4909
|
return { data: null, status: 404 };
|
|
@@ -4334,7 +4992,6 @@ const MIN_RECONNECT_DELAY_JITTER_MS = 1000;
|
|
|
4334
4992
|
const MAX_RECONNECT_DELAY_MS = 300000;
|
|
4335
4993
|
const RECONNECT_DELAY_GROW_FACTOR = 1.8;
|
|
4336
4994
|
const MIN_BROWSER_RECONNECT_INTERVAL_MS = 5000;
|
|
4337
|
-
const WS_AUTH_EXPIRED_CODE = 'auth_expired';
|
|
4338
4995
|
// React runs old-route effect cleanup before new-route effects. Keep an
|
|
4339
4996
|
// otherwise-idle socket for one short handoff window so the new route can
|
|
4340
4997
|
// cancel teardown and reuse it; genuinely idle sockets still close promptly.
|
|
@@ -4470,19 +5127,11 @@ function bearerFromAuthHeaders(headers) {
|
|
|
4470
5127
|
}
|
|
4471
5128
|
// Monotonic counter for unique-ish fallback identity keys (no Date.now in some envs).
|
|
4472
5129
|
let connectionEpoch = 0;
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
const currentTime = Date.now();
|
|
4479
|
-
return currentTime > (expirationTime - 60000); // 60 second buffer
|
|
4480
|
-
}
|
|
4481
|
-
catch (error) {
|
|
4482
|
-
console.error('[WS v2] Error checking token expiration. reason:', (_a = error === null || error === void 0 ? void 0 : error.name) !== null && _a !== void 0 ? _a : 'decode_failed');
|
|
4483
|
-
return true;
|
|
4484
|
-
}
|
|
4485
|
-
}
|
|
5130
|
+
// isTokenExpired / makeAuthExpiredError / isAuthExpiredError /
|
|
5131
|
+
// normalizeAuthExpiredError now live in utils/auth-token-freshness so the HTTP and
|
|
5132
|
+
// WebSocket paths cannot drift apart. `classifyToken` replaces the old boolean,
|
|
5133
|
+
// which collapsed "expiring soon" and "already expired" into one answer and read a
|
|
5134
|
+
// token with no numeric `exp` as fresh.
|
|
4486
5135
|
function getTokenExpirationTime(token) {
|
|
4487
5136
|
try {
|
|
4488
5137
|
const payload = JSON.parse(decodeBase64Url(token.split('.')[1]));
|
|
@@ -4492,27 +5141,34 @@ function getTokenExpirationTime(token) {
|
|
|
4492
5141
|
return null;
|
|
4493
5142
|
}
|
|
4494
5143
|
}
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
}
|
|
4505
|
-
function normalizeAuthExpiredError(error, fallbackMessage) {
|
|
4506
|
-
var _a, _b;
|
|
4507
|
-
if (isAuthExpiredError(error))
|
|
4508
|
-
return error;
|
|
4509
|
-
const status = (_b = (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : error === null || error === void 0 ? void 0 : error.code;
|
|
4510
|
-
return makeAuthExpiredError(fallbackMessage, status);
|
|
4511
|
-
}
|
|
5144
|
+
/**
|
|
5145
|
+
* Latch a failure so a dead session does not re-hammer /session/refresh every tick.
|
|
5146
|
+
*
|
|
5147
|
+
* NOT latched when the failure came with the session being cleared or replaced.
|
|
5148
|
+
* The clear notifies auth-state listeners, which kicks a reconnect that resets this
|
|
5149
|
+
* — and if we re-latched afterwards, `getConnectionAuthToken` would keep throwing
|
|
5150
|
+
* even though storage is now empty, so an ordinary expiry left WebSocket
|
|
5151
|
+
* acquisition permanently broken instead of falling back to an anonymous socket.
|
|
5152
|
+
*/
|
|
4512
5153
|
function rememberAmbientAuthFailure(error) {
|
|
5154
|
+
if ((error === null || error === void 0 ? void 0 : error.code) === AUTH_CHANGED_CODE)
|
|
5155
|
+
return error;
|
|
5156
|
+
// The session is gone: the right next move is an anonymous connection, not a
|
|
5157
|
+
// remembered failure.
|
|
5158
|
+
if (!getIdTokenSync())
|
|
5159
|
+
return error;
|
|
4513
5160
|
ambientAuthFailure = error;
|
|
4514
5161
|
return error;
|
|
4515
5162
|
}
|
|
5163
|
+
/** Best-effort synchronous peek, only for deciding whether a session still exists. */
|
|
5164
|
+
function getIdTokenSync() {
|
|
5165
|
+
try {
|
|
5166
|
+
return getActiveSessionManager().getIdToken();
|
|
5167
|
+
}
|
|
5168
|
+
catch (_a) {
|
|
5169
|
+
return null;
|
|
5170
|
+
}
|
|
5171
|
+
}
|
|
4516
5172
|
function failConnectionAuth(connection, error) {
|
|
4517
5173
|
connection.authFailure = error;
|
|
4518
5174
|
connection.pendingAuthToken = null;
|
|
@@ -4540,7 +5196,7 @@ function scheduleTokenRefresh(connection, isServer) {
|
|
|
4540
5196
|
// This replaces the old single setTimeout approach which was unreliable for long
|
|
4541
5197
|
// delays (browsers throttle/suspend timers in background tabs).
|
|
4542
5198
|
connection.tokenRefreshTimer = setInterval(async () => {
|
|
4543
|
-
var _a, _b, _c, _d, _e
|
|
5199
|
+
var _a, _b, _c, _d, _e;
|
|
4544
5200
|
try {
|
|
4545
5201
|
const currentToken = await getIdToken(isServer);
|
|
4546
5202
|
if (!currentToken)
|
|
@@ -4555,90 +5211,70 @@ function scheduleTokenRefresh(connection, isServer) {
|
|
|
4555
5211
|
// verifies JWT at $connect time, so the existing connection continues
|
|
4556
5212
|
// working. The fresh token is picked up by urlProvider the next time
|
|
4557
5213
|
// a reconnect naturally happens (network drop, keepalive timeout, etc.).
|
|
5214
|
+
//
|
|
5215
|
+
// Routed through the shared transaction. This used to read a refresh
|
|
5216
|
+
// token, call refreshSession, and write the result with nothing binding
|
|
5217
|
+
// it to the session it started from — so a timer that fired for S1 and
|
|
5218
|
+
// completed after a same-wallet re-login could overwrite S2's rotated
|
|
5219
|
+
// token, and the next refresh would trip the issuer's reuse detection
|
|
5220
|
+
// and revoke the whole family.
|
|
4558
5221
|
try {
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
console.warn('[WS v2] No refresh token available for proactive refresh');
|
|
4562
|
-
return;
|
|
4563
|
-
}
|
|
4564
|
-
const refreshData = await refreshSession(currentRefreshToken, getSessionIssuer(isServer));
|
|
4565
|
-
if (refreshData && refreshData.idToken && refreshData.accessToken) {
|
|
4566
|
-
await updateIdTokenAndAccessToken(refreshData.idToken, refreshData.accessToken, isServer, refreshData.refreshToken);
|
|
4567
|
-
console.info('[WS v2] Token refreshed successfully, stored for next connection');
|
|
4568
|
-
}
|
|
4569
|
-
else {
|
|
4570
|
-
console.warn('[WS v2] Proactive token refresh returned incomplete data');
|
|
4571
|
-
}
|
|
5222
|
+
await getFreshAuthToken(isServer);
|
|
5223
|
+
console.info('[WS v2] Token refreshed successfully, stored for next connection');
|
|
4572
5224
|
}
|
|
4573
5225
|
catch (refreshError) {
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
5226
|
+
const definitive = (refreshError === null || refreshError === void 0 ? void 0 : refreshError.code) === AUTH_EXPIRED_CODE
|
|
5227
|
+
|| (refreshError === null || refreshError === void 0 ? void 0 : refreshError.code) === AUTH_CHANGED_CODE;
|
|
5228
|
+
if (definitive) {
|
|
5229
|
+
// The session is dead or belongs to someone else now. The shared
|
|
5230
|
+
// helper has already cleared storage and notified auth-state
|
|
5231
|
+
// listeners, so the UI stops showing a connection that is gone —
|
|
5232
|
+
// this branch used to just stop the timer and leave the UI
|
|
5233
|
+
// claiming to be connected.
|
|
5234
|
+
console.warn('[WS v2] Session no longer usable, stopping periodic refresh');
|
|
4579
5235
|
if (connection.tokenRefreshTimer) {
|
|
4580
5236
|
clearInterval(connection.tokenRefreshTimer);
|
|
4581
5237
|
connection.tokenRefreshTimer = null;
|
|
4582
5238
|
}
|
|
4583
5239
|
return;
|
|
4584
5240
|
}
|
|
4585
|
-
//
|
|
4586
|
-
//
|
|
4587
|
-
|
|
5241
|
+
// Transient: keep the session and try again next interval.
|
|
5242
|
+
// Status only — the raw axios error carries config.data (the refresh
|
|
5243
|
+
// token) and must never hit the console / log sinks.
|
|
5244
|
+
console.warn('[WS v2] Proactive token refresh failed, will retry next interval. status:', (_b = (_a = refreshError === null || refreshError === void 0 ? void 0 : refreshError.status) !== null && _a !== void 0 ? _a : refreshError === null || refreshError === void 0 ? void 0 : refreshError.code) !== null && _b !== void 0 ? _b : 'unknown');
|
|
4588
5245
|
}
|
|
4589
5246
|
}
|
|
4590
5247
|
}
|
|
4591
5248
|
catch (error) {
|
|
4592
|
-
console.error('[WS v2] Error in periodic token check. status:', (
|
|
5249
|
+
console.error('[WS v2] Error in periodic token check. status:', (_e = (_d = (_c = error === null || error === void 0 ? void 0 : error.response) === null || _c === void 0 ? void 0 : _c.status) !== null && _d !== void 0 ? _d : error === null || error === void 0 ? void 0 : error.code) !== null && _e !== void 0 ? _e : 'unknown');
|
|
4593
5250
|
}
|
|
4594
5251
|
}, TOKEN_CHECK_INTERVAL);
|
|
4595
5252
|
}
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
if (ambientAuthFailure) {
|
|
5253
|
+
/**
|
|
5254
|
+
* WS-local wrapper around the shared freshness helper.
|
|
5255
|
+
*
|
|
5256
|
+
* The shared helper owns expiry classification, the deduplicated
|
|
5257
|
+
* snapshot -> refresh -> commit transaction, and conditional clearing. This adds
|
|
5258
|
+
* only the WS-specific storm guard: without it, a dead session would retry the
|
|
5259
|
+
* refresh on every reconnect attempt and every 5-minute tick.
|
|
5260
|
+
*/
|
|
5261
|
+
async function getWsAuthToken(isServer) {
|
|
5262
|
+
var _a, _b;
|
|
5263
|
+
// A remembered failure short-circuits, but only while a session still exists.
|
|
5264
|
+
// See rememberAmbientAuthFailure: once storage is cleared the correct next move
|
|
5265
|
+
// is an anonymous connection, not a replayed error.
|
|
5266
|
+
if (ambientAuthFailure && getIdTokenSync()) {
|
|
4610
5267
|
throw ambientAuthFailure;
|
|
4611
5268
|
}
|
|
4612
|
-
// Token is expired — attempt refresh
|
|
4613
|
-
const refreshToken = await getRefreshToken(isServer);
|
|
4614
|
-
if (!refreshToken) {
|
|
4615
|
-
console.warn('[WS v2] Token expired but no refresh token available');
|
|
4616
|
-
throw rememberAmbientAuthFailure(makeAuthExpiredError('Authentication expired and no refresh token is available'));
|
|
4617
|
-
}
|
|
4618
5269
|
try {
|
|
4619
|
-
const
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
ambientAuthFailure = null;
|
|
4623
|
-
return refreshData.idToken;
|
|
4624
|
-
}
|
|
4625
|
-
throw makeAuthExpiredError('Authentication refresh returned an incomplete session');
|
|
5270
|
+
const token = await getFreshAuthToken(isServer);
|
|
5271
|
+
ambientAuthFailure = null;
|
|
5272
|
+
return token;
|
|
4626
5273
|
}
|
|
4627
5274
|
catch (error) {
|
|
4628
|
-
//
|
|
4629
|
-
//
|
|
4630
|
-
console.error('[WS v2] Error
|
|
4631
|
-
// If the refresh token is permanently invalid (401/403), clear the stale
|
|
4632
|
-
// session from storage so future attempts don't keep retrying with it.
|
|
4633
|
-
if (!isServer && (((_d = error === null || error === void 0 ? void 0 : error.response) === null || _d === void 0 ? void 0 : _d.status) === 401 || ((_e = error === null || error === void 0 ? void 0 : error.response) === null || _e === void 0 ? void 0 : _e.status) === 403)) {
|
|
4634
|
-
try {
|
|
4635
|
-
const { getActiveSessionManager } = await Promise.resolve().then(function () { return sessionManager; });
|
|
4636
|
-
getActiveSessionManager().clearSession();
|
|
4637
|
-
}
|
|
4638
|
-
catch (clearError) {
|
|
4639
|
-
console.warn('[WS v2] Failed to clear stale session:', clearError);
|
|
4640
|
-
}
|
|
4641
|
-
}
|
|
5275
|
+
// Status only — the raw axios error carries config.data (the refresh token)
|
|
5276
|
+
// and must never reach a console or log sink.
|
|
5277
|
+
console.error('[WS v2] Error acquiring auth token. status:', (_b = (_a = error === null || error === void 0 ? void 0 : error.status) !== null && _a !== void 0 ? _a : error === null || error === void 0 ? void 0 : error.code) !== null && _b !== void 0 ? _b : 'unknown');
|
|
4642
5278
|
throw rememberAmbientAuthFailure(normalizeAuthExpiredError(error, 'Authentication expired and refresh failed'));
|
|
4643
5279
|
}
|
|
4644
5280
|
}
|
|
@@ -4650,7 +5286,10 @@ async function getConnectionAuthToken(isServer, authTokenProvider, anonymous) {
|
|
|
4650
5286
|
return null;
|
|
4651
5287
|
}
|
|
4652
5288
|
if (!authTokenProvider) {
|
|
4653
|
-
|
|
5289
|
+
// getWsAuthToken, not the shared helper directly: the wrapper carries the
|
|
5290
|
+
// reconnect-storm guard that keeps a dead session from re-refreshing on
|
|
5291
|
+
// every reconnect attempt.
|
|
5292
|
+
return getWsAuthToken(isServer);
|
|
4654
5293
|
}
|
|
4655
5294
|
try {
|
|
4656
5295
|
const token = await authTokenProvider();
|
|
@@ -6010,7 +6649,7 @@ async function doReconnectWithNewAuth() {
|
|
|
6010
6649
|
console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
|
|
6011
6650
|
}
|
|
6012
6651
|
try {
|
|
6013
|
-
const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-
|
|
6652
|
+
const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-Bz4Ob6K6.js'); });
|
|
6014
6653
|
await reconnectRealtimeStoreWithNewAuth();
|
|
6015
6654
|
}
|
|
6016
6655
|
catch (error) {
|
|
@@ -7717,11 +8356,11 @@ async function setMany(many, options) {
|
|
|
7717
8356
|
transactionResult = await handleEvmTransaction(curTx.evmTransaction, authProvider, options);
|
|
7718
8357
|
}
|
|
7719
8358
|
else if (curTx.serializedTransaction) {
|
|
7720
|
-
const { handlePreBuiltTransaction } = await Promise.resolve().then(function () { return require('./solana-operations-
|
|
8359
|
+
const { handlePreBuiltTransaction } = await Promise.resolve().then(function () { return require('./solana-operations-ngqIkN9W.js'); });
|
|
7721
8360
|
transactionResult = await handlePreBuiltTransaction(curTx, authProvider, options, expectedWritePaths);
|
|
7722
8361
|
}
|
|
7723
8362
|
else {
|
|
7724
|
-
const { handleSolanaTransaction } = await Promise.resolve().then(function () { return require('./solana-operations-
|
|
8363
|
+
const { handleSolanaTransaction } = await Promise.resolve().then(function () { return require('./solana-operations-ngqIkN9W.js'); });
|
|
7725
8364
|
transactionResult = await handleSolanaTransaction(curTx, authProvider, options, expectedWritePaths);
|
|
7726
8365
|
}
|
|
7727
8366
|
// Sync items after all transactions are confirmed
|
|
@@ -8820,7 +9459,7 @@ async function buildSetDocumentsTransaction(connection, idl, programProvider, pa
|
|
|
8820
9459
|
}
|
|
8821
9460
|
/** SIWS message creation is async already, so its onchain module can be lazy. */
|
|
8822
9461
|
async function genSolanaMessage(address, nonce) {
|
|
8823
|
-
const implementation = await Promise.resolve().then(function () { return require('./solana-message-
|
|
9462
|
+
const implementation = await Promise.resolve().then(function () { return require('./solana-message-B0NoE4nC.js'); });
|
|
8824
9463
|
return implementation.genSolanaMessage(address, nonce);
|
|
8825
9464
|
}
|
|
8826
9465
|
const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
@@ -9362,6 +10001,8 @@ function defineLiveModule(mod) {
|
|
|
9362
10001
|
return mod;
|
|
9363
10002
|
}
|
|
9364
10003
|
|
|
10004
|
+
exports.AUTH_CHANGED_CODE = AUTH_CHANGED_CODE;
|
|
10005
|
+
exports.AUTH_EXPIRED_CODE = AUTH_EXPIRED_CODE;
|
|
9365
10006
|
exports.BOUNDED_RUNTIME_SET_DOCUMENTS_ATTESTED_SELECTOR = BOUNDED_RUNTIME_SET_DOCUMENTS_ATTESTED_SELECTOR;
|
|
9366
10007
|
exports.BOUNDED_RUNTIME_SET_DOCUMENTS_SELECTOR = BOUNDED_RUNTIME_SET_DOCUMENTS_SELECTOR;
|
|
9367
10008
|
exports.BoundedDeclineError = BoundedDeclineError;
|
|
@@ -9376,6 +10017,8 @@ exports.RATE_LIMIT_EVENT_NAME = RATE_LIMIT_EVENT_NAME;
|
|
|
9376
10017
|
exports.ReactNativeSessionManager = ReactNativeSessionManager;
|
|
9377
10018
|
exports.ScopedClientReadOnlyError = ScopedClientReadOnlyError;
|
|
9378
10019
|
exports.ServerSessionManager = ServerSessionManager;
|
|
10020
|
+
exports.SessionEnvironmentError = SessionEnvironmentError;
|
|
10021
|
+
exports.UNSUPPORTED_ENVIRONMENT_CODE = UNSUPPORTED_ENVIRONMENT_CODE;
|
|
9379
10022
|
exports.WebSessionManager = WebSessionManager;
|
|
9380
10023
|
exports.aggregate = aggregate;
|
|
9381
10024
|
exports.assertCanonicalBoundedRuntimeWriteCalldata = assertCanonicalBoundedRuntimeWriteCalldata;
|
|
@@ -9415,10 +10058,12 @@ exports.increment = increment;
|
|
|
9415
10058
|
exports.init = init;
|
|
9416
10059
|
exports.intent = intent;
|
|
9417
10060
|
exports.invoke = invoke;
|
|
10061
|
+
exports.isAuthExpiredError = isAuthExpiredError;
|
|
9418
10062
|
exports.isEffectResult = isEffectResult;
|
|
9419
10063
|
exports.live = live;
|
|
9420
10064
|
exports.normalizeEvmAddress = normalizeEvmAddress;
|
|
9421
10065
|
exports.now = now;
|
|
10066
|
+
exports.onSessionCleared = onSessionCleared;
|
|
9422
10067
|
exports.queryAggregate = queryAggregate;
|
|
9423
10068
|
exports.reconnectWithNewAuth = reconnectWithNewAuth;
|
|
9424
10069
|
exports.refreshSession = refreshSession;
|
|
@@ -9444,4 +10089,4 @@ exports.toLowerEvmAddress = toLowerEvmAddress;
|
|
|
9444
10089
|
exports.toMillis = toMillis;
|
|
9445
10090
|
exports.toSeconds = toSeconds;
|
|
9446
10091
|
exports.withEffects = withEffects;
|
|
9447
|
-
//# sourceMappingURL=index-
|
|
10092
|
+
//# sourceMappingURL=index-BIdD2yUB.js.map
|