solana-traderclaw 1.0.19

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.
@@ -0,0 +1,361 @@
1
+ // src/session-manager.ts
2
+ var TRADERCLAW_SESSION_TROUBLESHOOTING = "https://docs.traderclaw.ai/docs/installation#troubleshooting-session-expired-auth-errors-or-the-agent-logged-out";
3
+ var BS58_CHARS = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
4
+ function b58Decode(str) {
5
+ let num = BigInt(0);
6
+ for (const c of str) {
7
+ const idx = BS58_CHARS.indexOf(c);
8
+ if (idx < 0) throw new Error(`Invalid base58 character: ${c}`);
9
+ num = num * 58n + BigInt(idx);
10
+ }
11
+ const hex = num.toString(16);
12
+ const paddedHex = hex.length % 2 ? "0" + hex : hex;
13
+ const bytes = new Uint8Array(paddedHex.length / 2);
14
+ for (let i = 0; i < bytes.length; i++) {
15
+ bytes[i] = parseInt(paddedHex.substring(i * 2, i * 2 + 2), 16);
16
+ }
17
+ let leadingZeros = 0;
18
+ for (const c of str) {
19
+ if (c === "1") leadingZeros++;
20
+ else break;
21
+ }
22
+ if (leadingZeros > 0) {
23
+ const combined = new Uint8Array(leadingZeros + bytes.length);
24
+ combined.set(bytes, leadingZeros);
25
+ return combined;
26
+ }
27
+ return bytes;
28
+ }
29
+ function b58Encode(bytes) {
30
+ let num = BigInt(0);
31
+ for (const b of bytes) {
32
+ num = num * 256n + BigInt(b);
33
+ }
34
+ let result = "";
35
+ while (num > 0n) {
36
+ result = BS58_CHARS[Number(num % 58n)] + result;
37
+ num = num / 58n;
38
+ }
39
+ for (const b of bytes) {
40
+ if (b === 0) result = "1" + result;
41
+ else break;
42
+ }
43
+ return result || "1";
44
+ }
45
+ function buildEd25519Pkcs8(rawPrivKey) {
46
+ const prefix = new Uint8Array([
47
+ 48,
48
+ 46,
49
+ 2,
50
+ 1,
51
+ 0,
52
+ 48,
53
+ 5,
54
+ 6,
55
+ 3,
56
+ 43,
57
+ 101,
58
+ 112,
59
+ 4,
60
+ 34,
61
+ 4,
62
+ 32
63
+ ]);
64
+ const result = new Uint8Array(prefix.length + 32);
65
+ result.set(prefix);
66
+ result.set(rawPrivKey.slice(0, 32), prefix.length);
67
+ return result;
68
+ }
69
+ async function signChallengeAsync(challengeBytes, privateKeyBase58) {
70
+ const keyBytes = b58Decode(privateKeyBase58);
71
+ const privKeyRaw = keyBytes.slice(0, 32);
72
+ const pkcs8Der = buildEd25519Pkcs8(privKeyRaw);
73
+ try {
74
+ const cryptoKey = await crypto.subtle.importKey(
75
+ "pkcs8",
76
+ pkcs8Der,
77
+ { name: "Ed25519" },
78
+ false,
79
+ ["sign"]
80
+ );
81
+ const sigBytes = new Uint8Array(
82
+ await crypto.subtle.sign("Ed25519", cryptoKey, new TextEncoder().encode(challengeBytes))
83
+ );
84
+ return b58Encode(sigBytes);
85
+ } catch {
86
+ try {
87
+ const nodeCrypto = await import("crypto");
88
+ const keyObj = nodeCrypto.createPrivateKey({
89
+ key: Buffer.from(pkcs8Der),
90
+ format: "der",
91
+ type: "pkcs8"
92
+ });
93
+ const sig = nodeCrypto.sign(null, Buffer.from(challengeBytes, "utf-8"), keyObj);
94
+ return b58Encode(new Uint8Array(sig));
95
+ } catch (innerErr) {
96
+ throw new Error(`Failed to sign challenge: ${innerErr.message}. Ensure walletPrivateKey is a valid base58-encoded Solana private key.`);
97
+ }
98
+ }
99
+ }
100
+ async function rawFetch(url, method, body, bearerToken, timeout = 15e3) {
101
+ const controller = new AbortController();
102
+ const timer = setTimeout(() => controller.abort(), timeout);
103
+ try {
104
+ const headers = { "Content-Type": "application/json" };
105
+ if (bearerToken) {
106
+ headers["Authorization"] = `Bearer ${bearerToken}`;
107
+ }
108
+ const fetchOpts = { method, headers, signal: controller.signal };
109
+ if (body) {
110
+ fetchOpts.body = JSON.stringify(body);
111
+ }
112
+ const res = await fetch(url, fetchOpts);
113
+ const text = await res.text();
114
+ let data;
115
+ try {
116
+ data = JSON.parse(text);
117
+ } catch {
118
+ data = { raw: text };
119
+ }
120
+ return { ok: res.ok, status: res.status, data };
121
+ } catch (err) {
122
+ if (err?.name === "AbortError") {
123
+ throw new Error(`Session request timed out after ${timeout}ms: ${method} ${url}`);
124
+ }
125
+ throw err;
126
+ } finally {
127
+ clearTimeout(timer);
128
+ }
129
+ }
130
+ var SessionManager = class {
131
+ baseUrl;
132
+ apiKey;
133
+ accessToken = null;
134
+ refreshTokenValue = null;
135
+ walletPublicKey = null;
136
+ walletPrivateKeyProvider;
137
+ clientLabel;
138
+ timeout;
139
+ accessTokenExpiresAt = 0;
140
+ sessionId = null;
141
+ tier = null;
142
+ scopes = [];
143
+ onTokensRotated;
144
+ log;
145
+ refreshInFlight = null;
146
+ constructor(config) {
147
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
148
+ this.apiKey = config.apiKey;
149
+ this.refreshTokenValue = config.refreshToken || null;
150
+ this.walletPublicKey = config.walletPublicKey || null;
151
+ this.walletPrivateKeyProvider = config.walletPrivateKeyProvider;
152
+ this.clientLabel = config.clientLabel || "openclaw-plugin-runtime";
153
+ this.timeout = config.timeout || 15e3;
154
+ this.onTokensRotated = config.onTokensRotated;
155
+ this.log = config.logger || { info: console.log, warn: console.warn, error: console.error };
156
+ }
157
+ async requestChallenge() {
158
+ const body = {
159
+ apiKey: this.apiKey,
160
+ clientLabel: this.clientLabel
161
+ };
162
+ if (this.walletPublicKey) {
163
+ body.walletPublicKey = this.walletPublicKey;
164
+ }
165
+ const res = await rawFetch(
166
+ `${this.baseUrl}/api/session/challenge`,
167
+ "POST",
168
+ body,
169
+ void 0,
170
+ this.timeout
171
+ );
172
+ if (!res.ok) {
173
+ throw new Error(`Challenge request failed (HTTP ${res.status}): ${JSON.stringify(res.data)}`);
174
+ }
175
+ return res.data;
176
+ }
177
+ async startSession(challengeId, walletPublicKey, walletSignature) {
178
+ const body = {
179
+ apiKey: this.apiKey,
180
+ challengeId,
181
+ clientLabel: this.clientLabel
182
+ };
183
+ if (walletPublicKey) body.walletPublicKey = walletPublicKey;
184
+ if (walletSignature) body.walletSignature = walletSignature;
185
+ const res = await rawFetch(
186
+ `${this.baseUrl}/api/session/start`,
187
+ "POST",
188
+ body,
189
+ void 0,
190
+ this.timeout
191
+ );
192
+ if (!res.ok) {
193
+ throw new Error(`Session start failed (HTTP ${res.status}): ${JSON.stringify(res.data)}`);
194
+ }
195
+ const tokens = res.data;
196
+ this.applyTokens(tokens);
197
+ return tokens;
198
+ }
199
+ async refresh() {
200
+ if (!this.refreshTokenValue) {
201
+ throw new Error("No refresh token available. Must authenticate via challenge flow.");
202
+ }
203
+ const res = await rawFetch(
204
+ `${this.baseUrl}/api/session/refresh`,
205
+ "POST",
206
+ { refreshToken: this.refreshTokenValue },
207
+ void 0,
208
+ this.timeout
209
+ );
210
+ if (!res.ok) {
211
+ if (res.status === 401 || res.status === 403) {
212
+ this.accessToken = null;
213
+ this.refreshTokenValue = null;
214
+ this.accessTokenExpiresAt = 0;
215
+ throw new Error("Refresh token expired or revoked. Must re-authenticate via challenge flow.");
216
+ }
217
+ throw new Error(`Token refresh failed (HTTP ${res.status}): ${JSON.stringify(res.data)}`);
218
+ }
219
+ const tokens = res.data;
220
+ this.applyTokens(tokens);
221
+ return tokens;
222
+ }
223
+ async logout() {
224
+ if (!this.refreshTokenValue) return;
225
+ try {
226
+ await rawFetch(
227
+ `${this.baseUrl}/api/session/logout`,
228
+ "POST",
229
+ { refreshToken: this.refreshTokenValue },
230
+ void 0,
231
+ this.timeout
232
+ );
233
+ } finally {
234
+ this.accessToken = null;
235
+ this.refreshTokenValue = null;
236
+ this.accessTokenExpiresAt = 0;
237
+ this.sessionId = null;
238
+ }
239
+ }
240
+ async initialize() {
241
+ if (this.refreshTokenValue) {
242
+ try {
243
+ this.log.info("[session] Refreshing existing session...");
244
+ await this.refresh();
245
+ this.log.info(`[session] Session refreshed. Tier: ${this.tier}, Scopes: ${this.scopes.join(", ")}`);
246
+ return;
247
+ } catch (err) {
248
+ this.log.warn(`[session] Refresh failed: ${err.message}. Falling back to challenge flow.`);
249
+ }
250
+ }
251
+ if (!this.apiKey) {
252
+ throw new Error(
253
+ "No apiKey configured. On this machine run: traderclaw setup --signup (or traderclaw signup) for a new account, or add an API key via traderclaw setup. The agent cannot create accounts or change credentials."
254
+ );
255
+ }
256
+ this.log.info("[session] Starting challenge flow...");
257
+ const challenge = await this.requestChallenge();
258
+ let walletPubKey;
259
+ let walletSig;
260
+ if (challenge.walletProofRequired && challenge.challenge) {
261
+ const walletPrivateKey = (await this.walletPrivateKeyProvider?.())?.trim();
262
+ if (!walletPrivateKey) {
263
+ throw new Error(
264
+ `Wallet proof required but no walletPrivateKey configured. This account already has a wallet \u2014 set TRADERCLAW_WALLET_PRIVATE_KEY in the OpenClaw gateway process environment (e.g. systemd), not only in an SSH shell, then restart the gateway. The key is used only for local signing and is never sent to the orchestrator. Do not store private keys in openclaw.json. Troubleshooting: ${TRADERCLAW_SESSION_TROUBLESHOOTING}`
265
+ );
266
+ }
267
+ walletPubKey = challenge.walletPublicKey || this.walletPublicKey || void 0;
268
+ this.log.info("[session] Signing wallet challenge locally...");
269
+ walletSig = await signChallengeAsync(challenge.challenge, walletPrivateKey);
270
+ }
271
+ const tokens = await this.startSession(challenge.challengeId, walletPubKey, walletSig);
272
+ this.log.info(`[session] Session established. ID: ${this.sessionId}, Tier: ${this.tier}`);
273
+ if (challenge.walletPublicKey) {
274
+ this.walletPublicKey = challenge.walletPublicKey;
275
+ }
276
+ }
277
+ async getAccessToken() {
278
+ if (this.accessToken && Date.now() < this.accessTokenExpiresAt - 12e4) {
279
+ return this.accessToken;
280
+ }
281
+ if (!this.refreshInFlight) {
282
+ this.refreshInFlight = this.ensureRefreshed();
283
+ }
284
+ try {
285
+ await this.refreshInFlight;
286
+ } finally {
287
+ this.refreshInFlight = null;
288
+ }
289
+ if (!this.accessToken) {
290
+ throw new Error("Failed to obtain access token after refresh.");
291
+ }
292
+ return this.accessToken;
293
+ }
294
+ async handleUnauthorized() {
295
+ this.accessToken = null;
296
+ this.accessTokenExpiresAt = 0;
297
+ if (!this.refreshInFlight) {
298
+ this.refreshInFlight = this.ensureRefreshed();
299
+ }
300
+ try {
301
+ await this.refreshInFlight;
302
+ } finally {
303
+ this.refreshInFlight = null;
304
+ }
305
+ if (!this.accessToken) {
306
+ throw new Error(
307
+ `Session expired and could not be refreshed. Re-authentication required. Troubleshooting: ${TRADERCLAW_SESSION_TROUBLESHOOTING}`
308
+ );
309
+ }
310
+ return this.accessToken;
311
+ }
312
+ isAuthenticated() {
313
+ return !!this.accessToken;
314
+ }
315
+ getSessionInfo() {
316
+ return {
317
+ sessionId: this.sessionId,
318
+ tier: this.tier,
319
+ scopes: this.scopes,
320
+ apiKey: this.apiKey
321
+ };
322
+ }
323
+ getApiKey() {
324
+ return this.apiKey;
325
+ }
326
+ getRefreshToken() {
327
+ return this.refreshTokenValue;
328
+ }
329
+ getWalletPublicKey() {
330
+ return this.walletPublicKey;
331
+ }
332
+ applyTokens(tokens) {
333
+ this.accessToken = tokens.accessToken;
334
+ this.refreshTokenValue = tokens.refreshToken;
335
+ this.accessTokenExpiresAt = Date.now() + tokens.accessTokenTtlSeconds * 1e3;
336
+ this.sessionId = tokens.session.id;
337
+ this.tier = tokens.session.tier;
338
+ this.scopes = tokens.session.scopes;
339
+ if (this.onTokensRotated) {
340
+ this.onTokensRotated({
341
+ refreshToken: tokens.refreshToken,
342
+ walletPublicKey: this.walletPublicKey || void 0
343
+ });
344
+ }
345
+ }
346
+ async ensureRefreshed() {
347
+ if (this.refreshTokenValue) {
348
+ try {
349
+ await this.refresh();
350
+ return;
351
+ } catch {
352
+ this.log.warn("[session] Refresh failed during token renewal. Attempting challenge flow...");
353
+ }
354
+ }
355
+ await this.initialize();
356
+ }
357
+ };
358
+
359
+ export {
360
+ SessionManager
361
+ };
@@ -0,0 +1,64 @@
1
+ // src/http-client.ts
2
+ async function orchestratorRequest(opts) {
3
+ const result = await doRequest(opts);
4
+ return result;
5
+ }
6
+ async function doRequest(opts, isRetry = false) {
7
+ const url = `${opts.baseUrl.replace(/\/$/, "")}${opts.path}`;
8
+ const controller = new AbortController();
9
+ const timeoutId = setTimeout(
10
+ () => controller.abort(),
11
+ opts.timeout ?? 12e4
12
+ );
13
+ try {
14
+ const headers = {
15
+ "Content-Type": "application/json"
16
+ };
17
+ const bearer = opts.accessToken || opts.apiKey;
18
+ if (bearer) {
19
+ headers["Authorization"] = `Bearer ${bearer}`;
20
+ }
21
+ if (opts.extraHeaders) {
22
+ Object.assign(headers, opts.extraHeaders);
23
+ }
24
+ const fetchOpts = {
25
+ method: opts.method,
26
+ headers,
27
+ signal: controller.signal
28
+ };
29
+ if ((opts.method === "POST" || opts.method === "PUT") && opts.body) {
30
+ fetchOpts.body = JSON.stringify(opts.body);
31
+ }
32
+ const res = await fetch(url, fetchOpts);
33
+ const text = await res.text();
34
+ let data;
35
+ try {
36
+ data = JSON.parse(text);
37
+ } catch {
38
+ data = { raw: text };
39
+ }
40
+ if ((res.status === 401 || res.status === 403) && !isRetry && opts.onUnauthorized) {
41
+ clearTimeout(timeoutId);
42
+ const newToken = await opts.onUnauthorized();
43
+ return doRequest({ ...opts, accessToken: newToken }, true);
44
+ }
45
+ if (!res.ok) {
46
+ const errMsg = data && typeof data === "object" && "error" in data ? data.error : `HTTP ${res.status}: ${text.slice(0, 200)}`;
47
+ throw new Error(errMsg);
48
+ }
49
+ return data;
50
+ } catch (err) {
51
+ if (err instanceof Error && err.name === "AbortError") {
52
+ throw new Error(
53
+ `Orchestrator request timed out after ${opts.timeout ?? 3e4}ms: ${opts.method} ${opts.path}`
54
+ );
55
+ }
56
+ throw err;
57
+ } finally {
58
+ clearTimeout(timeoutId);
59
+ }
60
+ }
61
+
62
+ export {
63
+ orchestratorRequest
64
+ };