@spooled/sdk 1.0.30 → 1.0.32
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/README.md +10 -6
- package/dist/{index-Dv2jBkfc.d.cts → index-CfLL1w6Y.d.cts} +44 -11
- package/dist/{index-Dv2jBkfc.d.ts → index-CfLL1w6Y.d.ts} +44 -11
- package/dist/index.cjs +120 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -2
- package/dist/index.d.ts +8 -2
- package/dist/index.js +120 -14
- package/dist/index.js.map +1 -1
- package/dist/worker/index.d.cts +1 -1
- package/dist/worker/index.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -157,7 +157,7 @@ Run jobs on a cron schedule:
|
|
|
157
157
|
```typescript
|
|
158
158
|
const schedule = await client.schedules.create({
|
|
159
159
|
name: 'Daily Report',
|
|
160
|
-
cronExpression: '0
|
|
160
|
+
cronExpression: '0 9 * * *', // 9 AM daily; 5-field (min hour dom mon dow) or 6-field (leading seconds) both work
|
|
161
161
|
timezone: 'America/New_York',
|
|
162
162
|
queueName: 'reports',
|
|
163
163
|
payloadTemplate: { type: 'daily' },
|
|
@@ -320,22 +320,25 @@ All operations automatically enforce tier-based limits:
|
|
|
320
320
|
- ✅ Queue creation
|
|
321
321
|
- ✅ Webhook creation
|
|
322
322
|
|
|
323
|
-
When
|
|
323
|
+
When a plan quota or limit is exceeded, you'll receive an `HTTP 429` response with
|
|
324
|
+
`code: "QUOTA_EXCEEDED"` and a body describing the `resource`, `current`, `limit`, and `plan`:
|
|
324
325
|
|
|
325
326
|
```typescript
|
|
326
|
-
import {
|
|
327
|
+
import { RateLimitError } from '@spooled/sdk';
|
|
327
328
|
|
|
328
329
|
try {
|
|
329
330
|
await client.jobs.create({ /* ... */ });
|
|
330
331
|
} catch (error) {
|
|
331
|
-
if (error instanceof
|
|
332
|
-
// Plan
|
|
332
|
+
if (error instanceof RateLimitError && error.code === 'QUOTA_EXCEEDED') {
|
|
333
|
+
// Plan quota reached (HTTP 429); details carry resource/current/limit/plan
|
|
333
334
|
console.log(`Plan limit: ${error.message}`);
|
|
334
335
|
console.log('Details:', error.details);
|
|
335
336
|
}
|
|
336
337
|
}
|
|
337
338
|
```
|
|
338
339
|
|
|
340
|
+
(Per-second rate limiting also returns `429`, as a `RateLimitError` with the default `RATE_LIMIT_EXCEEDED` code.)
|
|
341
|
+
|
|
339
342
|
## Error Handling
|
|
340
343
|
|
|
341
344
|
All errors extend `SpooledError` with specific subclasses:
|
|
@@ -355,8 +358,9 @@ try {
|
|
|
355
358
|
if (error instanceof NotFoundError) {
|
|
356
359
|
console.log('Job not found');
|
|
357
360
|
} else if (error instanceof AuthorizationError) {
|
|
358
|
-
console.log(`Forbidden
|
|
361
|
+
console.log(`Forbidden: ${error.message}`);
|
|
359
362
|
} else if (error instanceof RateLimitError) {
|
|
363
|
+
// 429: either a plan quota (code "QUOTA_EXCEEDED") or per-second rate limiting
|
|
360
364
|
console.log(`Retry after ${error.getRetryAfter()} seconds`);
|
|
361
365
|
} else if (isSpooledError(error)) {
|
|
362
366
|
console.log(`API error: ${error.code} - ${error.message}`);
|
|
@@ -107,11 +107,11 @@ declare const DEFAULT_CONFIG: {
|
|
|
107
107
|
successThreshold: number;
|
|
108
108
|
timeout: number;
|
|
109
109
|
};
|
|
110
|
-
readonly userAgent: "@spooled/sdk-nodejs/1.0.
|
|
110
|
+
readonly userAgent: "@spooled/sdk-nodejs/1.0.31";
|
|
111
111
|
readonly autoRefreshToken: true;
|
|
112
112
|
};
|
|
113
113
|
/** SDK version */
|
|
114
|
-
declare const SDK_VERSION = "1.0.
|
|
114
|
+
declare const SDK_VERSION = "1.0.31";
|
|
115
115
|
/** API version prefix */
|
|
116
116
|
declare const API_VERSION = "v1";
|
|
117
117
|
/**
|
|
@@ -2381,13 +2381,17 @@ interface RealtimeConnectionOptions {
|
|
|
2381
2381
|
/** JWT token for authentication (used as a fallback when tokenProvider is not supplied) */
|
|
2382
2382
|
token: string;
|
|
2383
2383
|
/**
|
|
2384
|
-
* Async provider that
|
|
2384
|
+
* Async provider that supplies a valid JWT for each (re)connect.
|
|
2385
2385
|
*
|
|
2386
|
-
* JWTs are short-lived, so a static token captured once will be stale by
|
|
2387
|
-
*
|
|
2388
|
-
*
|
|
2389
|
-
|
|
2390
|
-
|
|
2386
|
+
* JWTs are short-lived, so a static token captured once will be stale by the
|
|
2387
|
+
* time a long-running connection needs to reconnect. When provided, this is
|
|
2388
|
+
* invoked before every connect/reconnect to obtain a valid token. The
|
|
2389
|
+
* provider is expected to cache and reuse the token itself (re-minting only
|
|
2390
|
+
* near expiry), so calling it on every reconnect does not hammer the login
|
|
2391
|
+
* endpoint. `forceRefresh` asks it to bypass that cache — the transport sets
|
|
2392
|
+
* this after the server rejects a token (e.g. a 401 on the WS upgrade).
|
|
2393
|
+
*/
|
|
2394
|
+
tokenProvider?: (forceRefresh?: boolean) => Promise<string>;
|
|
2391
2395
|
/** Auto-reconnect on disconnect (default: true) */
|
|
2392
2396
|
autoReconnect?: boolean;
|
|
2393
2397
|
/** Maximum reconnect attempts (default: 10) */
|
|
@@ -2430,8 +2434,8 @@ interface FullRealtimeOptions extends SpooledRealtimeOptions {
|
|
|
2430
2434
|
baseUrl: string;
|
|
2431
2435
|
wsUrl: string;
|
|
2432
2436
|
token: string;
|
|
2433
|
-
/** Async provider that
|
|
2434
|
-
tokenProvider?: () => Promise<string>;
|
|
2437
|
+
/** Async provider that supplies a valid JWT for each (re)connect */
|
|
2438
|
+
tokenProvider?: (forceRefresh?: boolean) => Promise<string>;
|
|
2435
2439
|
debug?: (message: string, meta?: unknown) => void;
|
|
2436
2440
|
}
|
|
2437
2441
|
/** Event handler type - simplified for internal use */
|
|
@@ -3090,6 +3094,18 @@ declare class SpooledClient {
|
|
|
3090
3094
|
/** Token refresh state */
|
|
3091
3095
|
private refreshPromise;
|
|
3092
3096
|
private tokenExpiresAt;
|
|
3097
|
+
/**
|
|
3098
|
+
* Cached JWT for realtime connections (API-key auth path).
|
|
3099
|
+
*
|
|
3100
|
+
* The realtime layer requests a token on every (re)connect. Minting a brand
|
|
3101
|
+
* new JWT via POST /auth/login each time trips the login rate limit (429)
|
|
3102
|
+
* under a reconnect storm, after which realtime can never recover. We cache
|
|
3103
|
+
* the token here — at the client level, shared across reconnects — and reuse
|
|
3104
|
+
* it until it nears expiry.
|
|
3105
|
+
*/
|
|
3106
|
+
private cachedJwt;
|
|
3107
|
+
/** Deduplicates concurrent logins (e.g. WS + SSE reconnecting together). */
|
|
3108
|
+
private jwtLoginPromise;
|
|
3093
3109
|
constructor(options: SpooledClientConfig);
|
|
3094
3110
|
/**
|
|
3095
3111
|
* Real gRPC client for high-performance operations (HTTP/2 + Protobuf)
|
|
@@ -3132,9 +3148,26 @@ declare class SpooledClient {
|
|
|
3132
3148
|
*/
|
|
3133
3149
|
realtime(options?: SpooledRealtimeOptions): Promise<SpooledRealtime>;
|
|
3134
3150
|
/**
|
|
3135
|
-
* Get or acquire a JWT token for realtime connections
|
|
3151
|
+
* Get or acquire a JWT token for realtime connections.
|
|
3152
|
+
*
|
|
3153
|
+
* The realtime layer calls this on every (re)connect. We cache the JWT and
|
|
3154
|
+
* reuse it until it nears expiry so a reconnect storm doesn't hammer
|
|
3155
|
+
* /auth/login and trip its rate limit. A fresh login happens only when the
|
|
3156
|
+
* cached token is absent, within ~60s of its `exp`, or a refresh is forced.
|
|
3157
|
+
*
|
|
3158
|
+
* @param forceRefresh Bypass the cache and mint a new token. Set by the
|
|
3159
|
+
* transport when the server rejected the current token (e.g. a 401 on the
|
|
3160
|
+
* WebSocket upgrade), so a stale/revoked token is replaced without waiting
|
|
3161
|
+
* for it to expire.
|
|
3136
3162
|
*/
|
|
3137
3163
|
private getJwtToken;
|
|
3164
|
+
/**
|
|
3165
|
+
* Exchange the API key for a JWT via /auth/login and cache the result.
|
|
3166
|
+
*
|
|
3167
|
+
* Concurrent callers share a single in-flight login so a burst of reconnects
|
|
3168
|
+
* (e.g. WebSocket and SSE at once) issues just one request.
|
|
3169
|
+
*/
|
|
3170
|
+
private loginForJwt;
|
|
3138
3171
|
/**
|
|
3139
3172
|
* Check if token should be refreshed
|
|
3140
3173
|
*/
|
|
@@ -107,11 +107,11 @@ declare const DEFAULT_CONFIG: {
|
|
|
107
107
|
successThreshold: number;
|
|
108
108
|
timeout: number;
|
|
109
109
|
};
|
|
110
|
-
readonly userAgent: "@spooled/sdk-nodejs/1.0.
|
|
110
|
+
readonly userAgent: "@spooled/sdk-nodejs/1.0.31";
|
|
111
111
|
readonly autoRefreshToken: true;
|
|
112
112
|
};
|
|
113
113
|
/** SDK version */
|
|
114
|
-
declare const SDK_VERSION = "1.0.
|
|
114
|
+
declare const SDK_VERSION = "1.0.31";
|
|
115
115
|
/** API version prefix */
|
|
116
116
|
declare const API_VERSION = "v1";
|
|
117
117
|
/**
|
|
@@ -2381,13 +2381,17 @@ interface RealtimeConnectionOptions {
|
|
|
2381
2381
|
/** JWT token for authentication (used as a fallback when tokenProvider is not supplied) */
|
|
2382
2382
|
token: string;
|
|
2383
2383
|
/**
|
|
2384
|
-
* Async provider that
|
|
2384
|
+
* Async provider that supplies a valid JWT for each (re)connect.
|
|
2385
2385
|
*
|
|
2386
|
-
* JWTs are short-lived, so a static token captured once will be stale by
|
|
2387
|
-
*
|
|
2388
|
-
*
|
|
2389
|
-
|
|
2390
|
-
|
|
2386
|
+
* JWTs are short-lived, so a static token captured once will be stale by the
|
|
2387
|
+
* time a long-running connection needs to reconnect. When provided, this is
|
|
2388
|
+
* invoked before every connect/reconnect to obtain a valid token. The
|
|
2389
|
+
* provider is expected to cache and reuse the token itself (re-minting only
|
|
2390
|
+
* near expiry), so calling it on every reconnect does not hammer the login
|
|
2391
|
+
* endpoint. `forceRefresh` asks it to bypass that cache — the transport sets
|
|
2392
|
+
* this after the server rejects a token (e.g. a 401 on the WS upgrade).
|
|
2393
|
+
*/
|
|
2394
|
+
tokenProvider?: (forceRefresh?: boolean) => Promise<string>;
|
|
2391
2395
|
/** Auto-reconnect on disconnect (default: true) */
|
|
2392
2396
|
autoReconnect?: boolean;
|
|
2393
2397
|
/** Maximum reconnect attempts (default: 10) */
|
|
@@ -2430,8 +2434,8 @@ interface FullRealtimeOptions extends SpooledRealtimeOptions {
|
|
|
2430
2434
|
baseUrl: string;
|
|
2431
2435
|
wsUrl: string;
|
|
2432
2436
|
token: string;
|
|
2433
|
-
/** Async provider that
|
|
2434
|
-
tokenProvider?: () => Promise<string>;
|
|
2437
|
+
/** Async provider that supplies a valid JWT for each (re)connect */
|
|
2438
|
+
tokenProvider?: (forceRefresh?: boolean) => Promise<string>;
|
|
2435
2439
|
debug?: (message: string, meta?: unknown) => void;
|
|
2436
2440
|
}
|
|
2437
2441
|
/** Event handler type - simplified for internal use */
|
|
@@ -3090,6 +3094,18 @@ declare class SpooledClient {
|
|
|
3090
3094
|
/** Token refresh state */
|
|
3091
3095
|
private refreshPromise;
|
|
3092
3096
|
private tokenExpiresAt;
|
|
3097
|
+
/**
|
|
3098
|
+
* Cached JWT for realtime connections (API-key auth path).
|
|
3099
|
+
*
|
|
3100
|
+
* The realtime layer requests a token on every (re)connect. Minting a brand
|
|
3101
|
+
* new JWT via POST /auth/login each time trips the login rate limit (429)
|
|
3102
|
+
* under a reconnect storm, after which realtime can never recover. We cache
|
|
3103
|
+
* the token here — at the client level, shared across reconnects — and reuse
|
|
3104
|
+
* it until it nears expiry.
|
|
3105
|
+
*/
|
|
3106
|
+
private cachedJwt;
|
|
3107
|
+
/** Deduplicates concurrent logins (e.g. WS + SSE reconnecting together). */
|
|
3108
|
+
private jwtLoginPromise;
|
|
3093
3109
|
constructor(options: SpooledClientConfig);
|
|
3094
3110
|
/**
|
|
3095
3111
|
* Real gRPC client for high-performance operations (HTTP/2 + Protobuf)
|
|
@@ -3132,9 +3148,26 @@ declare class SpooledClient {
|
|
|
3132
3148
|
*/
|
|
3133
3149
|
realtime(options?: SpooledRealtimeOptions): Promise<SpooledRealtime>;
|
|
3134
3150
|
/**
|
|
3135
|
-
* Get or acquire a JWT token for realtime connections
|
|
3151
|
+
* Get or acquire a JWT token for realtime connections.
|
|
3152
|
+
*
|
|
3153
|
+
* The realtime layer calls this on every (re)connect. We cache the JWT and
|
|
3154
|
+
* reuse it until it nears expiry so a reconnect storm doesn't hammer
|
|
3155
|
+
* /auth/login and trip its rate limit. A fresh login happens only when the
|
|
3156
|
+
* cached token is absent, within ~60s of its `exp`, or a refresh is forced.
|
|
3157
|
+
*
|
|
3158
|
+
* @param forceRefresh Bypass the cache and mint a new token. Set by the
|
|
3159
|
+
* transport when the server rejected the current token (e.g. a 401 on the
|
|
3160
|
+
* WebSocket upgrade), so a stale/revoked token is replaced without waiting
|
|
3161
|
+
* for it to expire.
|
|
3136
3162
|
*/
|
|
3137
3163
|
private getJwtToken;
|
|
3164
|
+
/**
|
|
3165
|
+
* Exchange the API key for a JWT via /auth/login and cache the result.
|
|
3166
|
+
*
|
|
3167
|
+
* Concurrent callers share a single in-flight login so a burst of reconnects
|
|
3168
|
+
* (e.g. WebSocket and SSE at once) issues just one request.
|
|
3169
|
+
*/
|
|
3170
|
+
private loginForJwt;
|
|
3138
3171
|
/**
|
|
3139
3172
|
* Check if token should be refreshed
|
|
3140
3173
|
*/
|
package/dist/index.cjs
CHANGED
|
@@ -49,12 +49,19 @@ var DEFAULT_CONFIG = {
|
|
|
49
49
|
successThreshold: 3,
|
|
50
50
|
timeout: 3e4
|
|
51
51
|
},
|
|
52
|
-
userAgent: "@spooled/sdk-nodejs/1.0.
|
|
52
|
+
userAgent: "@spooled/sdk-nodejs/1.0.31",
|
|
53
53
|
autoRefreshToken: true
|
|
54
54
|
};
|
|
55
|
-
var SDK_VERSION = "1.0.
|
|
55
|
+
var SDK_VERSION = "1.0.31";
|
|
56
56
|
var API_VERSION = "v1";
|
|
57
57
|
var API_BASE_PATH = `/api/${API_VERSION}`;
|
|
58
|
+
function normalizeCredential(value) {
|
|
59
|
+
if (value === void 0) {
|
|
60
|
+
return void 0;
|
|
61
|
+
}
|
|
62
|
+
const trimmed = value.trim();
|
|
63
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
64
|
+
}
|
|
58
65
|
function resolveConfig(options) {
|
|
59
66
|
let debugFn = null;
|
|
60
67
|
if (options.debug === true) {
|
|
@@ -84,10 +91,10 @@ function resolveConfig(options) {
|
|
|
84
91
|
const derivedWsUrl = baseUrl.replace(/^http/, "ws");
|
|
85
92
|
const wsUrl = options.wsUrl ?? derivedWsUrl;
|
|
86
93
|
return {
|
|
87
|
-
apiKey: options.apiKey,
|
|
88
|
-
accessToken: options.accessToken,
|
|
89
|
-
refreshToken: options.refreshToken,
|
|
90
|
-
adminKey: options.adminKey,
|
|
94
|
+
apiKey: normalizeCredential(options.apiKey),
|
|
95
|
+
accessToken: normalizeCredential(options.accessToken),
|
|
96
|
+
refreshToken: normalizeCredential(options.refreshToken),
|
|
97
|
+
adminKey: normalizeCredential(options.adminKey),
|
|
91
98
|
baseUrl,
|
|
92
99
|
wsUrl,
|
|
93
100
|
grpcAddress: options.grpcAddress ?? DEFAULT_CONFIG.grpcAddress,
|
|
@@ -1849,6 +1856,12 @@ var WebSocketRealtimeClient = class {
|
|
|
1849
1856
|
state = "disconnected";
|
|
1850
1857
|
reconnectAttempts = 0;
|
|
1851
1858
|
reconnectTimer = null;
|
|
1859
|
+
/**
|
|
1860
|
+
* Set when the server rejected our token on the upgrade (e.g. HTTP 401/403).
|
|
1861
|
+
* The next (re)connect uses it to force the token provider to mint a fresh
|
|
1862
|
+
* JWT instead of replaying the rejected one (which would loop forever).
|
|
1863
|
+
*/
|
|
1864
|
+
authFailure = false;
|
|
1852
1865
|
subscriptions = /* @__PURE__ */ new Map();
|
|
1853
1866
|
pendingCommands = /* @__PURE__ */ new Map();
|
|
1854
1867
|
// Event handlers
|
|
@@ -1930,6 +1943,9 @@ var WebSocketRealtimeClient = class {
|
|
|
1930
1943
|
};
|
|
1931
1944
|
this.ws.onerror = (event) => {
|
|
1932
1945
|
this.options.debug("WebSocket error", event);
|
|
1946
|
+
if (isAuthFailureEvent(event)) {
|
|
1947
|
+
this.authFailure = true;
|
|
1948
|
+
}
|
|
1933
1949
|
};
|
|
1934
1950
|
} catch (error) {
|
|
1935
1951
|
this.setState("disconnected");
|
|
@@ -2016,7 +2032,9 @@ var WebSocketRealtimeClient = class {
|
|
|
2016
2032
|
// Private methods
|
|
2017
2033
|
async buildWsUrl() {
|
|
2018
2034
|
const wsBase = this.options.wsUrl;
|
|
2019
|
-
const
|
|
2035
|
+
const forceRefresh = this.authFailure;
|
|
2036
|
+
this.authFailure = false;
|
|
2037
|
+
const token = await this.options.tokenProvider(forceRefresh);
|
|
2020
2038
|
return `${wsBase}/api/v1/ws?token=${encodeURIComponent(token)}`;
|
|
2021
2039
|
}
|
|
2022
2040
|
setState(state) {
|
|
@@ -2139,6 +2157,17 @@ var WebSocketRealtimeClient = class {
|
|
|
2139
2157
|
function redactToken(url) {
|
|
2140
2158
|
return url.replace(/([?&](?:token|api_key)=)[^&]*/gi, "$1***");
|
|
2141
2159
|
}
|
|
2160
|
+
function isAuthFailureEvent(event) {
|
|
2161
|
+
if (!event) {
|
|
2162
|
+
return false;
|
|
2163
|
+
}
|
|
2164
|
+
const status = event.status ?? event.statusCode ?? event.error?.status;
|
|
2165
|
+
if (status === 401 || status === 403) {
|
|
2166
|
+
return true;
|
|
2167
|
+
}
|
|
2168
|
+
const text = String(event.message ?? event.error?.message ?? event.reason ?? "");
|
|
2169
|
+
return /\b(401|403)\b/.test(text);
|
|
2170
|
+
}
|
|
2142
2171
|
|
|
2143
2172
|
// src/realtime/sse.ts
|
|
2144
2173
|
var SseRealtimeClient = class {
|
|
@@ -2877,6 +2906,18 @@ var SpooledClient = class _SpooledClient {
|
|
|
2877
2906
|
/** Token refresh state */
|
|
2878
2907
|
refreshPromise = null;
|
|
2879
2908
|
tokenExpiresAt = null;
|
|
2909
|
+
/**
|
|
2910
|
+
* Cached JWT for realtime connections (API-key auth path).
|
|
2911
|
+
*
|
|
2912
|
+
* The realtime layer requests a token on every (re)connect. Minting a brand
|
|
2913
|
+
* new JWT via POST /auth/login each time trips the login rate limit (429)
|
|
2914
|
+
* under a reconnect storm, after which realtime can never recover. We cache
|
|
2915
|
+
* the token here — at the client level, shared across reconnects — and reuse
|
|
2916
|
+
* it until it nears expiry.
|
|
2917
|
+
*/
|
|
2918
|
+
cachedJwt = null;
|
|
2919
|
+
/** Deduplicates concurrent logins (e.g. WS + SSE reconnecting together). */
|
|
2920
|
+
jwtLoginPromise = null;
|
|
2880
2921
|
constructor(options) {
|
|
2881
2922
|
this.config = resolveConfig(options);
|
|
2882
2923
|
validateConfig(this.config);
|
|
@@ -2964,9 +3005,10 @@ var SpooledClient = class _SpooledClient {
|
|
|
2964
3005
|
baseUrl: this.config.baseUrl,
|
|
2965
3006
|
wsUrl: this.config.wsUrl,
|
|
2966
3007
|
token,
|
|
2967
|
-
// Provider
|
|
2968
|
-
//
|
|
2969
|
-
|
|
3008
|
+
// Provider the realtime layer calls on every (re)connect. It returns a
|
|
3009
|
+
// cached JWT and only re-logs-in when the token is absent, near expiry,
|
|
3010
|
+
// or the transport asks to force a refresh (e.g. after a 401 upgrade).
|
|
3011
|
+
tokenProvider: (forceRefresh) => this.getJwtToken(forceRefresh),
|
|
2970
3012
|
...options,
|
|
2971
3013
|
// Only forward debug when the client actually has a logger, so we never
|
|
2972
3014
|
// hand the realtime layer an explicit `debug: undefined`. The WS/SSE
|
|
@@ -2976,23 +3018,59 @@ var SpooledClient = class _SpooledClient {
|
|
|
2976
3018
|
});
|
|
2977
3019
|
}
|
|
2978
3020
|
/**
|
|
2979
|
-
* Get or acquire a JWT token for realtime connections
|
|
3021
|
+
* Get or acquire a JWT token for realtime connections.
|
|
3022
|
+
*
|
|
3023
|
+
* The realtime layer calls this on every (re)connect. We cache the JWT and
|
|
3024
|
+
* reuse it until it nears expiry so a reconnect storm doesn't hammer
|
|
3025
|
+
* /auth/login and trip its rate limit. A fresh login happens only when the
|
|
3026
|
+
* cached token is absent, within ~60s of its `exp`, or a refresh is forced.
|
|
3027
|
+
*
|
|
3028
|
+
* @param forceRefresh Bypass the cache and mint a new token. Set by the
|
|
3029
|
+
* transport when the server rejected the current token (e.g. a 401 on the
|
|
3030
|
+
* WebSocket upgrade), so a stale/revoked token is replaced without waiting
|
|
3031
|
+
* for it to expire.
|
|
2980
3032
|
*/
|
|
2981
|
-
async getJwtToken() {
|
|
3033
|
+
async getJwtToken(forceRefresh = false) {
|
|
2982
3034
|
if (this.config.accessToken) {
|
|
2983
|
-
if (this.shouldRefreshToken()) {
|
|
3035
|
+
if (forceRefresh || this.shouldRefreshToken()) {
|
|
2984
3036
|
return this.refreshAccessToken();
|
|
2985
3037
|
}
|
|
2986
3038
|
return this.config.accessToken;
|
|
2987
3039
|
}
|
|
2988
3040
|
if (this.config.apiKey) {
|
|
3041
|
+
if (forceRefresh) {
|
|
3042
|
+
this.cachedJwt = null;
|
|
3043
|
+
}
|
|
3044
|
+
if (this.cachedJwt && !isJwtNearExpiry(this.cachedJwt)) {
|
|
3045
|
+
return this.cachedJwt;
|
|
3046
|
+
}
|
|
3047
|
+
return this.loginForJwt();
|
|
3048
|
+
}
|
|
3049
|
+
throw new AuthenticationError("No authentication method available for realtime connection");
|
|
3050
|
+
}
|
|
3051
|
+
/**
|
|
3052
|
+
* Exchange the API key for a JWT via /auth/login and cache the result.
|
|
3053
|
+
*
|
|
3054
|
+
* Concurrent callers share a single in-flight login so a burst of reconnects
|
|
3055
|
+
* (e.g. WebSocket and SSE at once) issues just one request.
|
|
3056
|
+
*/
|
|
3057
|
+
async loginForJwt() {
|
|
3058
|
+
if (this.jwtLoginPromise) {
|
|
3059
|
+
return this.jwtLoginPromise;
|
|
3060
|
+
}
|
|
3061
|
+
this.jwtLoginPromise = (async () => {
|
|
2989
3062
|
const response = await this.auth.login({ apiKey: this.config.apiKey });
|
|
2990
3063
|
this.http.setAuthToken(response.accessToken);
|
|
2991
3064
|
this.tokenExpiresAt = Date.now() + response.expiresIn * 1e3;
|
|
2992
3065
|
this.config.refreshToken = response.refreshToken;
|
|
3066
|
+
this.cachedJwt = response.accessToken;
|
|
2993
3067
|
return response.accessToken;
|
|
3068
|
+
})();
|
|
3069
|
+
try {
|
|
3070
|
+
return await this.jwtLoginPromise;
|
|
3071
|
+
} finally {
|
|
3072
|
+
this.jwtLoginPromise = null;
|
|
2994
3073
|
}
|
|
2995
|
-
throw new AuthenticationError("No authentication method available for realtime connection");
|
|
2996
3074
|
}
|
|
2997
3075
|
/**
|
|
2998
3076
|
* Check if token should be refreshed
|
|
@@ -3093,6 +3171,34 @@ var SpooledClient = class _SpooledClient {
|
|
|
3093
3171
|
function createClient(options) {
|
|
3094
3172
|
return new SpooledClient(options);
|
|
3095
3173
|
}
|
|
3174
|
+
function decodeJwtExp(token) {
|
|
3175
|
+
const parts = token.split(".");
|
|
3176
|
+
if (parts.length < 2) {
|
|
3177
|
+
return null;
|
|
3178
|
+
}
|
|
3179
|
+
try {
|
|
3180
|
+
const payload = JSON.parse(base64UrlDecode(parts[1]));
|
|
3181
|
+
return typeof payload.exp === "number" ? payload.exp : null;
|
|
3182
|
+
} catch {
|
|
3183
|
+
return null;
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
function isJwtNearExpiry(token, skewMs = 6e4) {
|
|
3187
|
+
const exp = decodeJwtExp(token);
|
|
3188
|
+
if (exp === null) {
|
|
3189
|
+
return true;
|
|
3190
|
+
}
|
|
3191
|
+
return Date.now() >= exp * 1e3 - skewMs;
|
|
3192
|
+
}
|
|
3193
|
+
function base64UrlDecode(segment) {
|
|
3194
|
+
const b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
3195
|
+
if (typeof globalThis.Buffer !== "undefined") {
|
|
3196
|
+
return globalThis.Buffer.from(b64, "base64").toString("utf8");
|
|
3197
|
+
}
|
|
3198
|
+
const binary = globalThis.atob(b64);
|
|
3199
|
+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
3200
|
+
return new globalThis.TextDecoder().decode(bytes);
|
|
3201
|
+
}
|
|
3096
3202
|
|
|
3097
3203
|
// src/types/common.ts
|
|
3098
3204
|
function createPaginatedResponse(items, limit, offset = 0, total) {
|