solana-traderclaw 1.0.37 → 1.0.39
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/bin/openclaw-trader.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { createInterface } from "readline";
|
|
4
|
-
import { readFileSync, writeFileSync, mkdirSync, appendFileSync } from "fs";
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync, appendFileSync, existsSync } from "fs";
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import { homedir } from "os";
|
|
7
7
|
import { randomUUID, createPrivateKey, sign as cryptoSign } from "crypto";
|
|
@@ -1015,11 +1015,6 @@ async function cmdSetup(args) {
|
|
|
1015
1015
|
printWarn(
|
|
1016
1016
|
` For the OpenClaw gateway (Telegram/agent tools), the same env must be set on the gateway service — not only in this shell. See: ${TRADERCLAW_SESSION_TROUBLESHOOTING_URL}`,
|
|
1017
1017
|
);
|
|
1018
|
-
print("Next steps:");
|
|
1019
|
-
print(" 1. Install the plugin: openclaw plugins install solana-traderclaw (or: npm install -g solana-traderclaw)");
|
|
1020
|
-
print(" 2. Restart the gateway: openclaw gateway --restart");
|
|
1021
|
-
print(" 3. Start trading: Ask OpenClaw to scan for opportunities");
|
|
1022
|
-
print("");
|
|
1023
1018
|
print("Session commands:");
|
|
1024
1019
|
print(" traderclaw status Check connection health (auto-refreshes session)");
|
|
1025
1020
|
print(" traderclaw login Re-authenticate (challenge flow)");
|
|
@@ -2596,6 +2591,242 @@ async function cmdInstall(args) {
|
|
|
2596
2591
|
printInfo("Press Ctrl+C to stop the wizard server.");
|
|
2597
2592
|
}
|
|
2598
2593
|
|
|
2594
|
+
async function cmdTestSession(args) {
|
|
2595
|
+
const config = readConfig();
|
|
2596
|
+
const pluginConfig = getPluginConfig(config);
|
|
2597
|
+
|
|
2598
|
+
if (!pluginConfig) {
|
|
2599
|
+
printError("No plugin configuration found. Run 'traderclaw setup' first.");
|
|
2600
|
+
process.exit(1);
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
const orchestratorUrl = pluginConfig.orchestratorUrl;
|
|
2604
|
+
if (!orchestratorUrl) {
|
|
2605
|
+
printError("orchestratorUrl not set in config. Run 'traderclaw setup' to fix.");
|
|
2606
|
+
process.exit(1);
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
const dataDir = pluginConfig.dataDir || join(process.cwd(), ".traderclaw-v1-data");
|
|
2610
|
+
const sessionTokensPath = join(dataDir, "session-tokens.json");
|
|
2611
|
+
|
|
2612
|
+
let sidecar = null;
|
|
2613
|
+
try {
|
|
2614
|
+
if (existsSync(sessionTokensPath)) {
|
|
2615
|
+
sidecar = JSON.parse(readFileSync(sessionTokensPath, "utf-8"));
|
|
2616
|
+
}
|
|
2617
|
+
} catch { /* ignore */ }
|
|
2618
|
+
|
|
2619
|
+
const effectiveRefreshToken =
|
|
2620
|
+
(sidecar?.refreshToken && sidecar.refreshToken.length > 0)
|
|
2621
|
+
? sidecar.refreshToken
|
|
2622
|
+
: pluginConfig.refreshToken;
|
|
2623
|
+
|
|
2624
|
+
const walletPrivateKeyInput = args.includes("--wallet-private-key")
|
|
2625
|
+
? args[args.indexOf("--wallet-private-key") + 1] || ""
|
|
2626
|
+
: "";
|
|
2627
|
+
|
|
2628
|
+
print("\nTraderClaw V1 — Session Auth Test\n");
|
|
2629
|
+
print("=".repeat(50));
|
|
2630
|
+
printInfo(` Orchestrator: ${orchestratorUrl}`);
|
|
2631
|
+
printInfo(` API key: ${pluginConfig.apiKey ? maskKey(pluginConfig.apiKey) : "MISSING"}`);
|
|
2632
|
+
printInfo(` Refresh token: ${effectiveRefreshToken ? maskKey(effectiveRefreshToken) : "MISSING"}`);
|
|
2633
|
+
printInfo(` Sidecar file: ${sidecar ? sessionTokensPath : "not found"}`);
|
|
2634
|
+
printInfo(` Wallet pub key: ${pluginConfig.walletPublicKey || "not set"}`);
|
|
2635
|
+
printInfo(` Wallet priv key: ${getRuntimeWalletPrivateKey(walletPrivateKeyInput) ? "available" : "NOT AVAILABLE"}`);
|
|
2636
|
+
print("");
|
|
2637
|
+
|
|
2638
|
+
const results = [];
|
|
2639
|
+
let currentAccessToken = null;
|
|
2640
|
+
let currentRefreshToken = effectiveRefreshToken;
|
|
2641
|
+
|
|
2642
|
+
// --- Test 1: Initial refresh ---
|
|
2643
|
+
print(" [1/5] Token refresh...");
|
|
2644
|
+
const t1Start = Date.now();
|
|
2645
|
+
try {
|
|
2646
|
+
if (!currentRefreshToken) {
|
|
2647
|
+
throw new Error("No refresh token available — skip to challenge flow test");
|
|
2648
|
+
}
|
|
2649
|
+
const tokens = await doRefresh(orchestratorUrl, currentRefreshToken);
|
|
2650
|
+
if (!tokens) {
|
|
2651
|
+
printWarn(" Refresh returned null (token revoked/expired) — will test challenge flow");
|
|
2652
|
+
results.push({ test: "initial_refresh", status: "expired", ms: Date.now() - t1Start });
|
|
2653
|
+
currentRefreshToken = null;
|
|
2654
|
+
} else {
|
|
2655
|
+
const ms = Date.now() - t1Start;
|
|
2656
|
+
currentAccessToken = tokens.accessToken;
|
|
2657
|
+
currentRefreshToken = tokens.refreshToken;
|
|
2658
|
+
printSuccess(` OK (${ms}ms) — accessTokenTtl: ${tokens.accessTokenTtlSeconds}s, refreshTokenTtl: ${tokens.refreshTokenTtlSeconds}s`);
|
|
2659
|
+
results.push({
|
|
2660
|
+
test: "initial_refresh",
|
|
2661
|
+
status: "ok",
|
|
2662
|
+
ms,
|
|
2663
|
+
accessTokenTtl: tokens.accessTokenTtlSeconds,
|
|
2664
|
+
refreshTokenTtl: tokens.refreshTokenTtlSeconds,
|
|
2665
|
+
tier: tokens.session?.tier,
|
|
2666
|
+
});
|
|
2667
|
+
}
|
|
2668
|
+
} catch (err) {
|
|
2669
|
+
printError(` FAIL: ${err.message}`);
|
|
2670
|
+
results.push({ test: "initial_refresh", status: "fail", ms: Date.now() - t1Start, error: err.message });
|
|
2671
|
+
currentRefreshToken = null;
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
// --- Test 2: Second refresh (verifies rotation worked) ---
|
|
2675
|
+
print(" [2/5] Second refresh (token rotation check)...");
|
|
2676
|
+
const t2Start = Date.now();
|
|
2677
|
+
try {
|
|
2678
|
+
if (!currentRefreshToken) {
|
|
2679
|
+
throw new Error("No refresh token — skipped (previous test failed)");
|
|
2680
|
+
}
|
|
2681
|
+
const tokens2 = await doRefresh(orchestratorUrl, currentRefreshToken);
|
|
2682
|
+
if (!tokens2) {
|
|
2683
|
+
printError(" FAIL: second refresh returned null — rotation may be broken");
|
|
2684
|
+
results.push({ test: "rotation_check", status: "fail", ms: Date.now() - t2Start, error: "null response" });
|
|
2685
|
+
} else {
|
|
2686
|
+
const ms = Date.now() - t2Start;
|
|
2687
|
+
const rotated = tokens2.refreshToken !== currentRefreshToken;
|
|
2688
|
+
currentAccessToken = tokens2.accessToken;
|
|
2689
|
+
currentRefreshToken = tokens2.refreshToken;
|
|
2690
|
+
if (rotated) {
|
|
2691
|
+
printSuccess(` OK (${ms}ms) — refresh token rotated correctly`);
|
|
2692
|
+
} else {
|
|
2693
|
+
printWarn(` OK (${ms}ms) — refresh token NOT rotated (server may use static tokens)`);
|
|
2694
|
+
}
|
|
2695
|
+
results.push({ test: "rotation_check", status: "ok", ms, rotated });
|
|
2696
|
+
}
|
|
2697
|
+
} catch (err) {
|
|
2698
|
+
printError(` FAIL: ${err.message}`);
|
|
2699
|
+
results.push({ test: "rotation_check", status: "fail", ms: Date.now() - t2Start, error: err.message });
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
// --- Test 3: API call with access token ---
|
|
2703
|
+
print(" [3/5] Authenticated API call (/healthz)...");
|
|
2704
|
+
const t3Start = Date.now();
|
|
2705
|
+
try {
|
|
2706
|
+
if (!currentAccessToken) {
|
|
2707
|
+
throw new Error("No access token — skipped");
|
|
2708
|
+
}
|
|
2709
|
+
const health = await httpRequest(`${orchestratorUrl}/healthz`, {
|
|
2710
|
+
accessToken: currentAccessToken,
|
|
2711
|
+
timeout: 8000,
|
|
2712
|
+
});
|
|
2713
|
+
const ms = Date.now() - t3Start;
|
|
2714
|
+
if (health.ok) {
|
|
2715
|
+
printSuccess(` OK (${ms}ms) — orchestrator healthy`);
|
|
2716
|
+
results.push({ test: "api_call", status: "ok", ms });
|
|
2717
|
+
} else {
|
|
2718
|
+
printError(` FAIL: HTTP ${health.status}`);
|
|
2719
|
+
results.push({ test: "api_call", status: "fail", ms, error: `HTTP ${health.status}` });
|
|
2720
|
+
}
|
|
2721
|
+
} catch (err) {
|
|
2722
|
+
printError(` FAIL: ${err.message}`);
|
|
2723
|
+
results.push({ test: "api_call", status: "fail", ms: Date.now() - t3Start, error: err.message });
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
// --- Test 4: Challenge flow (re-auth from scratch) ---
|
|
2727
|
+
print(" [4/5] Challenge flow (full re-authentication)...");
|
|
2728
|
+
const t4Start = Date.now();
|
|
2729
|
+
try {
|
|
2730
|
+
if (!pluginConfig.apiKey) {
|
|
2731
|
+
throw new Error("No API key — cannot test challenge flow");
|
|
2732
|
+
}
|
|
2733
|
+
const challenge = await doChallenge(orchestratorUrl, pluginConfig.apiKey, pluginConfig.walletPublicKey);
|
|
2734
|
+
const ms = Date.now() - t4Start;
|
|
2735
|
+
if (challenge.walletProofRequired) {
|
|
2736
|
+
const wpk = getRuntimeWalletPrivateKey(walletPrivateKeyInput);
|
|
2737
|
+
if (wpk) {
|
|
2738
|
+
try {
|
|
2739
|
+
const walletSig = signChallengeLocally(challenge.challenge, wpk);
|
|
2740
|
+
const tokens = await doSessionStart(
|
|
2741
|
+
orchestratorUrl,
|
|
2742
|
+
pluginConfig.apiKey,
|
|
2743
|
+
challenge.challengeId,
|
|
2744
|
+
challenge.walletPublicKey || pluginConfig.walletPublicKey,
|
|
2745
|
+
walletSig,
|
|
2746
|
+
);
|
|
2747
|
+
const totalMs = Date.now() - t4Start;
|
|
2748
|
+
currentAccessToken = tokens.accessToken;
|
|
2749
|
+
currentRefreshToken = tokens.refreshToken;
|
|
2750
|
+
printSuccess(` OK (${totalMs}ms) — challenge + wallet proof succeeded`);
|
|
2751
|
+
results.push({ test: "challenge_flow", status: "ok", ms: totalMs, walletProof: true });
|
|
2752
|
+
} catch (sigErr) {
|
|
2753
|
+
printError(` FAIL (wallet signing): ${sigErr.message}`);
|
|
2754
|
+
results.push({ test: "challenge_flow", status: "fail", ms: Date.now() - t4Start, error: sigErr.message });
|
|
2755
|
+
}
|
|
2756
|
+
} else {
|
|
2757
|
+
printWarn(` PARTIAL (${ms}ms) — challenge OK but wallet proof needed and TRADERCLAW_WALLET_PRIVATE_KEY not available`);
|
|
2758
|
+
printWarn(" Set TRADERCLAW_WALLET_PRIVATE_KEY env var or pass --wallet-private-key to test fully");
|
|
2759
|
+
results.push({ test: "challenge_flow", status: "partial", ms, walletProofRequired: true, keyAvailable: false });
|
|
2760
|
+
}
|
|
2761
|
+
} else {
|
|
2762
|
+
const tokens = await doSessionStart(orchestratorUrl, pluginConfig.apiKey, challenge.challengeId);
|
|
2763
|
+
const totalMs = Date.now() - t4Start;
|
|
2764
|
+
currentAccessToken = tokens.accessToken;
|
|
2765
|
+
currentRefreshToken = tokens.refreshToken;
|
|
2766
|
+
printSuccess(` OK (${totalMs}ms) — challenge flow succeeded (no wallet proof needed)`);
|
|
2767
|
+
results.push({ test: "challenge_flow", status: "ok", ms: totalMs, walletProof: false });
|
|
2768
|
+
}
|
|
2769
|
+
} catch (err) {
|
|
2770
|
+
printError(` FAIL: ${err.message}`);
|
|
2771
|
+
results.push({ test: "challenge_flow", status: "fail", ms: Date.now() - t4Start, error: err.message });
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
// --- Test 5: Persist rotated tokens back to sidecar ---
|
|
2775
|
+
print(" [5/5] Persist tokens to sidecar...");
|
|
2776
|
+
try {
|
|
2777
|
+
if (currentRefreshToken && currentAccessToken) {
|
|
2778
|
+
mkdirSync(dataDir, { recursive: true });
|
|
2779
|
+
const payload = {
|
|
2780
|
+
refreshToken: currentRefreshToken,
|
|
2781
|
+
accessToken: currentAccessToken,
|
|
2782
|
+
accessTokenExpiresAt: Date.now() + 900_000,
|
|
2783
|
+
walletPublicKey: pluginConfig.walletPublicKey || undefined,
|
|
2784
|
+
};
|
|
2785
|
+
const tmp = `${sessionTokensPath}.${process.pid}.${Date.now()}.tmp`;
|
|
2786
|
+
writeFileSync(tmp, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
2787
|
+
const { renameSync } = await import("fs");
|
|
2788
|
+
renameSync(tmp, sessionTokensPath);
|
|
2789
|
+
printSuccess(` OK — written to ${sessionTokensPath}`);
|
|
2790
|
+
results.push({ test: "persist_sidecar", status: "ok" });
|
|
2791
|
+
|
|
2792
|
+
pluginConfig.refreshToken = currentRefreshToken;
|
|
2793
|
+
removeLegacyWalletPrivateKey(pluginConfig);
|
|
2794
|
+
setPluginConfig(config, pluginConfig);
|
|
2795
|
+
writeConfig(config);
|
|
2796
|
+
printSuccess(" Config updated with latest refresh token");
|
|
2797
|
+
} else {
|
|
2798
|
+
printWarn(" SKIP — no valid tokens to persist");
|
|
2799
|
+
results.push({ test: "persist_sidecar", status: "skip" });
|
|
2800
|
+
}
|
|
2801
|
+
} catch (err) {
|
|
2802
|
+
printError(` FAIL: ${err.message}`);
|
|
2803
|
+
results.push({ test: "persist_sidecar", status: "fail", error: err.message });
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
// --- Summary ---
|
|
2807
|
+
print("\n" + "=".repeat(50));
|
|
2808
|
+
const passed = results.filter((r) => r.status === "ok").length;
|
|
2809
|
+
const failed = results.filter((r) => r.status === "fail").length;
|
|
2810
|
+
const partial = results.filter((r) => r.status === "partial" || r.status === "expired" || r.status === "skip").length;
|
|
2811
|
+
|
|
2812
|
+
if (failed === 0 && passed > 0) {
|
|
2813
|
+
printSuccess(`\n ALL TESTS PASSED (${passed}/${results.length})`);
|
|
2814
|
+
} else if (failed > 0) {
|
|
2815
|
+
printError(`\n ${failed} FAILED, ${passed} passed, ${partial} skipped/partial`);
|
|
2816
|
+
} else {
|
|
2817
|
+
printWarn(`\n ${passed} passed, ${partial} skipped/partial`);
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
print("\n Build-and-test workflow (no reinstall):");
|
|
2821
|
+
printInfo(" cd /path/to/plugin && npm run build");
|
|
2822
|
+
printInfo(" npm link");
|
|
2823
|
+
printInfo(" sudo systemctl restart openclaw");
|
|
2824
|
+
printInfo(" traderclaw test-session");
|
|
2825
|
+
print("");
|
|
2826
|
+
|
|
2827
|
+
if (failed > 0) process.exit(1);
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2599
2830
|
function printHelp() {
|
|
2600
2831
|
print(`
|
|
2601
2832
|
TraderClaw V1 CLI v${VERSION}
|
|
@@ -2612,6 +2843,7 @@ Commands:
|
|
|
2612
2843
|
logout Revoke current session and clear tokens
|
|
2613
2844
|
status Check connection health and wallet status
|
|
2614
2845
|
config View and manage configuration
|
|
2846
|
+
test-session Test session auth flow (refresh, rotation, challenge) without reinstalling
|
|
2615
2847
|
|
|
2616
2848
|
Setup options:
|
|
2617
2849
|
--api-key, -k API key (skip interactive prompt)
|
|
@@ -2658,6 +2890,8 @@ Examples:
|
|
|
2658
2890
|
traderclaw status
|
|
2659
2891
|
traderclaw config show
|
|
2660
2892
|
traderclaw config set apiTimeout 60000
|
|
2893
|
+
traderclaw test-session
|
|
2894
|
+
traderclaw test-session --wallet-private-key <base58_key>
|
|
2661
2895
|
`);
|
|
2662
2896
|
}
|
|
2663
2897
|
|
|
@@ -2703,6 +2937,9 @@ async function main() {
|
|
|
2703
2937
|
case "config":
|
|
2704
2938
|
await cmdConfig(args.slice(1));
|
|
2705
2939
|
break;
|
|
2940
|
+
case "test-session":
|
|
2941
|
+
await cmdTestSession(args.slice(1));
|
|
2942
|
+
break;
|
|
2706
2943
|
default:
|
|
2707
2944
|
printError(`Unknown command: ${command}`);
|
|
2708
2945
|
printHelp();
|
package/bin/traderclaw.cjs
CHANGED
|
File without changes
|
|
@@ -144,6 +144,8 @@ var SessionManager = class {
|
|
|
144
144
|
log;
|
|
145
145
|
refreshInFlight = null;
|
|
146
146
|
refreshTokenTtlMs = 0;
|
|
147
|
+
accessTokenTtlMs = 0;
|
|
148
|
+
tokenGeneration = 0;
|
|
147
149
|
proactiveRefreshTimer = null;
|
|
148
150
|
proactiveRefreshRunning = false;
|
|
149
151
|
constructor(config) {
|
|
@@ -224,6 +226,7 @@ var SessionManager = class {
|
|
|
224
226
|
if (!this.refreshTokenValue) {
|
|
225
227
|
throw new Error("No refresh token available. Must authenticate via challenge flow.");
|
|
226
228
|
}
|
|
229
|
+
const genBefore = this.tokenGeneration;
|
|
227
230
|
const res = await rawFetch(
|
|
228
231
|
`${this.baseUrl}/api/session/refresh`,
|
|
229
232
|
"POST",
|
|
@@ -233,9 +236,13 @@ var SessionManager = class {
|
|
|
233
236
|
);
|
|
234
237
|
if (!res.ok) {
|
|
235
238
|
if (res.status === 401 || res.status === 403) {
|
|
236
|
-
this.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
+
if (this.tokenGeneration === genBefore) {
|
|
240
|
+
this.accessToken = null;
|
|
241
|
+
this.refreshTokenValue = null;
|
|
242
|
+
this.accessTokenExpiresAt = 0;
|
|
243
|
+
} else {
|
|
244
|
+
this.log.info("[session] Stale 401/403 ignored \u2014 tokens already rotated by concurrent call.");
|
|
245
|
+
}
|
|
239
246
|
throw new Error("Refresh token expired or revoked. Must re-authenticate via challenge flow.");
|
|
240
247
|
}
|
|
241
248
|
throw new Error(`Token refresh failed (HTTP ${res.status}): ${JSON.stringify(res.data)}`);
|
|
@@ -303,14 +310,7 @@ var SessionManager = class {
|
|
|
303
310
|
if (this.accessToken && Date.now() < this.accessTokenExpiresAt - 12e4) {
|
|
304
311
|
return this.accessToken;
|
|
305
312
|
}
|
|
306
|
-
|
|
307
|
-
this.refreshInFlight = this.ensureRefreshed();
|
|
308
|
-
}
|
|
309
|
-
try {
|
|
310
|
-
await this.refreshInFlight;
|
|
311
|
-
} finally {
|
|
312
|
-
this.refreshInFlight = null;
|
|
313
|
-
}
|
|
313
|
+
await this.unifiedRefresh();
|
|
314
314
|
if (!this.accessToken) {
|
|
315
315
|
throw new Error(
|
|
316
316
|
`Session expired and could not be refreshed. Re-authentication required. Troubleshooting: ${TRADERCLAW_SESSION_TROUBLESHOOTING}`
|
|
@@ -321,14 +321,7 @@ var SessionManager = class {
|
|
|
321
321
|
async handleUnauthorized() {
|
|
322
322
|
this.accessToken = null;
|
|
323
323
|
this.accessTokenExpiresAt = 0;
|
|
324
|
-
|
|
325
|
-
this.refreshInFlight = this.ensureRefreshed();
|
|
326
|
-
}
|
|
327
|
-
try {
|
|
328
|
-
await this.refreshInFlight;
|
|
329
|
-
} finally {
|
|
330
|
-
this.refreshInFlight = null;
|
|
331
|
-
}
|
|
324
|
+
await this.unifiedRefresh();
|
|
332
325
|
if (!this.accessToken) {
|
|
333
326
|
throw new Error(
|
|
334
327
|
`Session expired and could not be refreshed. Re-authentication required. Troubleshooting: ${TRADERCLAW_SESSION_TROUBLESHOOTING}`
|
|
@@ -336,6 +329,20 @@ var SessionManager = class {
|
|
|
336
329
|
}
|
|
337
330
|
return this.accessToken;
|
|
338
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* Single-mutex refresh: all paths (proactive timer, on-demand getAccessToken,
|
|
334
|
+
* handleUnauthorized) funnel through here so only one refresh HTTP call is
|
|
335
|
+
* ever in-flight. Prevents the race where two concurrent refresh() calls
|
|
336
|
+
* with the same rotating refresh token kill the session.
|
|
337
|
+
*/
|
|
338
|
+
async unifiedRefresh() {
|
|
339
|
+
if (!this.refreshInFlight) {
|
|
340
|
+
this.refreshInFlight = this.ensureRefreshed().finally(() => {
|
|
341
|
+
this.refreshInFlight = null;
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
await this.refreshInFlight;
|
|
345
|
+
}
|
|
339
346
|
isAuthenticated() {
|
|
340
347
|
return !!this.accessToken;
|
|
341
348
|
}
|
|
@@ -357,9 +364,11 @@ var SessionManager = class {
|
|
|
357
364
|
return this.walletPublicKey;
|
|
358
365
|
}
|
|
359
366
|
applyTokens(tokens) {
|
|
367
|
+
this.tokenGeneration++;
|
|
360
368
|
this.accessToken = tokens.accessToken;
|
|
361
369
|
this.refreshTokenValue = tokens.refreshToken;
|
|
362
370
|
this.accessTokenExpiresAt = Date.now() + tokens.accessTokenTtlSeconds * 1e3;
|
|
371
|
+
this.accessTokenTtlMs = tokens.accessTokenTtlSeconds * 1e3;
|
|
363
372
|
this.refreshTokenTtlMs = (tokens.refreshTokenTtlSeconds || 0) * 1e3;
|
|
364
373
|
this.sessionId = tokens.session.id;
|
|
365
374
|
this.tier = tokens.session.tier;
|
|
@@ -375,14 +384,19 @@ var SessionManager = class {
|
|
|
375
384
|
this.scheduleProactiveRefresh();
|
|
376
385
|
}
|
|
377
386
|
/**
|
|
378
|
-
* Schedule a background token refresh
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
387
|
+
* Schedule a repeating background token refresh. Uses setInterval so the
|
|
388
|
+
* chain cannot silently break if a single cycle fails to re-schedule.
|
|
389
|
+
*
|
|
390
|
+
* Interval = min(50% refresh-token TTL, accessTokenTtl - 2.5 min buffer),
|
|
391
|
+
* clamped between 2 min and 20 min. Falls back to 10 min when TTLs unknown.
|
|
392
|
+
*
|
|
393
|
+
* Goes through unifiedRefresh() so it shares the same mutex as on-demand
|
|
394
|
+
* callers and can fall back to the full challenge flow when refresh tokens
|
|
395
|
+
* are permanently revoked.
|
|
382
396
|
*/
|
|
383
397
|
scheduleProactiveRefresh() {
|
|
384
398
|
if (this.proactiveRefreshTimer) {
|
|
385
|
-
|
|
399
|
+
clearInterval(this.proactiveRefreshTimer);
|
|
386
400
|
this.proactiveRefreshTimer = null;
|
|
387
401
|
}
|
|
388
402
|
const MIN_INTERVAL_MS = 2 * 60 * 1e3;
|
|
@@ -394,17 +408,20 @@ var SessionManager = class {
|
|
|
394
408
|
} else {
|
|
395
409
|
intervalMs = DEFAULT_INTERVAL_MS;
|
|
396
410
|
}
|
|
397
|
-
this.
|
|
411
|
+
if (this.accessTokenTtlMs > 0) {
|
|
412
|
+
const accessBasedMs = Math.max(MIN_INTERVAL_MS, this.accessTokenTtlMs - 15e4);
|
|
413
|
+
intervalMs = Math.min(intervalMs, accessBasedMs);
|
|
414
|
+
}
|
|
415
|
+
this.log.info(`[session] Proactive refresh scheduled every ${Math.round(intervalMs / 1e3)}s`);
|
|
416
|
+
this.proactiveRefreshTimer = setInterval(async () => {
|
|
398
417
|
if (this.proactiveRefreshRunning) return;
|
|
399
418
|
this.proactiveRefreshRunning = true;
|
|
400
419
|
try {
|
|
401
|
-
if (!this.refreshTokenValue) return;
|
|
402
420
|
this.log.info(`[session] Proactive token refresh (interval: ${Math.round(intervalMs / 1e3)}s)...`);
|
|
403
|
-
await this.
|
|
421
|
+
await this.unifiedRefresh();
|
|
404
422
|
this.log.info("[session] Proactive refresh succeeded \u2014 token chain extended.");
|
|
405
423
|
} catch (err) {
|
|
406
|
-
this.log.warn(`[session] Proactive refresh failed: ${err.message}. Will retry next
|
|
407
|
-
this.scheduleProactiveRefresh();
|
|
424
|
+
this.log.warn(`[session] Proactive refresh failed: ${err.message}. Will retry next interval or on-demand.`);
|
|
408
425
|
} finally {
|
|
409
426
|
this.proactiveRefreshRunning = false;
|
|
410
427
|
}
|
|
@@ -415,7 +432,7 @@ var SessionManager = class {
|
|
|
415
432
|
}
|
|
416
433
|
destroy() {
|
|
417
434
|
if (this.proactiveRefreshTimer) {
|
|
418
|
-
|
|
435
|
+
clearInterval(this.proactiveRefreshTimer);
|
|
419
436
|
this.proactiveRefreshTimer = null;
|
|
420
437
|
}
|
|
421
438
|
}
|
package/dist/index.js
CHANGED
package/package.json
CHANGED