@spooled/sdk 1.0.29 → 1.0.31
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-CZorNMoD.d.cts → index-CfLL1w6Y.d.cts} +49 -12
- package/dist/{index-CZorNMoD.d.ts → index-CfLL1w6Y.d.ts} +49 -12
- package/dist/index.cjs +134 -25
- 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 +134 -25
- 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
|
@@ -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
|
/**
|
|
@@ -1086,7 +1086,11 @@ interface CreateScheduleParams {
|
|
|
1086
1086
|
timezone?: string;
|
|
1087
1087
|
/** Target queue name */
|
|
1088
1088
|
queueName: string;
|
|
1089
|
-
/**
|
|
1089
|
+
/**
|
|
1090
|
+
* Job payload template. Required: the API rejects a create without it
|
|
1091
|
+
* (HTTP 422), so this is typed as non-optional to surface the omission at
|
|
1092
|
+
* compile time rather than as a runtime 422.
|
|
1093
|
+
*/
|
|
1090
1094
|
payloadTemplate: JsonObject;
|
|
1091
1095
|
/** Job priority (-100 to 100) */
|
|
1092
1096
|
priority?: number;
|
|
@@ -2377,13 +2381,17 @@ interface RealtimeConnectionOptions {
|
|
|
2377
2381
|
/** JWT token for authentication (used as a fallback when tokenProvider is not supplied) */
|
|
2378
2382
|
token: string;
|
|
2379
2383
|
/**
|
|
2380
|
-
* Async provider that
|
|
2384
|
+
* Async provider that supplies a valid JWT for each (re)connect.
|
|
2381
2385
|
*
|
|
2382
|
-
* JWTs are short-lived, so a static token captured once will be stale by
|
|
2383
|
-
*
|
|
2384
|
-
*
|
|
2385
|
-
|
|
2386
|
-
|
|
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>;
|
|
2387
2395
|
/** Auto-reconnect on disconnect (default: true) */
|
|
2388
2396
|
autoReconnect?: boolean;
|
|
2389
2397
|
/** Maximum reconnect attempts (default: 10) */
|
|
@@ -2426,8 +2434,8 @@ interface FullRealtimeOptions extends SpooledRealtimeOptions {
|
|
|
2426
2434
|
baseUrl: string;
|
|
2427
2435
|
wsUrl: string;
|
|
2428
2436
|
token: string;
|
|
2429
|
-
/** Async provider that
|
|
2430
|
-
tokenProvider?: () => Promise<string>;
|
|
2437
|
+
/** Async provider that supplies a valid JWT for each (re)connect */
|
|
2438
|
+
tokenProvider?: (forceRefresh?: boolean) => Promise<string>;
|
|
2431
2439
|
debug?: (message: string, meta?: unknown) => void;
|
|
2432
2440
|
}
|
|
2433
2441
|
/** Event handler type - simplified for internal use */
|
|
@@ -3086,6 +3094,18 @@ declare class SpooledClient {
|
|
|
3086
3094
|
/** Token refresh state */
|
|
3087
3095
|
private refreshPromise;
|
|
3088
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;
|
|
3089
3109
|
constructor(options: SpooledClientConfig);
|
|
3090
3110
|
/**
|
|
3091
3111
|
* Real gRPC client for high-performance operations (HTTP/2 + Protobuf)
|
|
@@ -3128,9 +3148,26 @@ declare class SpooledClient {
|
|
|
3128
3148
|
*/
|
|
3129
3149
|
realtime(options?: SpooledRealtimeOptions): Promise<SpooledRealtime>;
|
|
3130
3150
|
/**
|
|
3131
|
-
* 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.
|
|
3132
3162
|
*/
|
|
3133
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;
|
|
3134
3171
|
/**
|
|
3135
3172
|
* Check if token should be refreshed
|
|
3136
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
|
/**
|
|
@@ -1086,7 +1086,11 @@ interface CreateScheduleParams {
|
|
|
1086
1086
|
timezone?: string;
|
|
1087
1087
|
/** Target queue name */
|
|
1088
1088
|
queueName: string;
|
|
1089
|
-
/**
|
|
1089
|
+
/**
|
|
1090
|
+
* Job payload template. Required: the API rejects a create without it
|
|
1091
|
+
* (HTTP 422), so this is typed as non-optional to surface the omission at
|
|
1092
|
+
* compile time rather than as a runtime 422.
|
|
1093
|
+
*/
|
|
1090
1094
|
payloadTemplate: JsonObject;
|
|
1091
1095
|
/** Job priority (-100 to 100) */
|
|
1092
1096
|
priority?: number;
|
|
@@ -2377,13 +2381,17 @@ interface RealtimeConnectionOptions {
|
|
|
2377
2381
|
/** JWT token for authentication (used as a fallback when tokenProvider is not supplied) */
|
|
2378
2382
|
token: string;
|
|
2379
2383
|
/**
|
|
2380
|
-
* Async provider that
|
|
2384
|
+
* Async provider that supplies a valid JWT for each (re)connect.
|
|
2381
2385
|
*
|
|
2382
|
-
* JWTs are short-lived, so a static token captured once will be stale by
|
|
2383
|
-
*
|
|
2384
|
-
*
|
|
2385
|
-
|
|
2386
|
-
|
|
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>;
|
|
2387
2395
|
/** Auto-reconnect on disconnect (default: true) */
|
|
2388
2396
|
autoReconnect?: boolean;
|
|
2389
2397
|
/** Maximum reconnect attempts (default: 10) */
|
|
@@ -2426,8 +2434,8 @@ interface FullRealtimeOptions extends SpooledRealtimeOptions {
|
|
|
2426
2434
|
baseUrl: string;
|
|
2427
2435
|
wsUrl: string;
|
|
2428
2436
|
token: string;
|
|
2429
|
-
/** Async provider that
|
|
2430
|
-
tokenProvider?: () => Promise<string>;
|
|
2437
|
+
/** Async provider that supplies a valid JWT for each (re)connect */
|
|
2438
|
+
tokenProvider?: (forceRefresh?: boolean) => Promise<string>;
|
|
2431
2439
|
debug?: (message: string, meta?: unknown) => void;
|
|
2432
2440
|
}
|
|
2433
2441
|
/** Event handler type - simplified for internal use */
|
|
@@ -3086,6 +3094,18 @@ declare class SpooledClient {
|
|
|
3086
3094
|
/** Token refresh state */
|
|
3087
3095
|
private refreshPromise;
|
|
3088
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;
|
|
3089
3109
|
constructor(options: SpooledClientConfig);
|
|
3090
3110
|
/**
|
|
3091
3111
|
* Real gRPC client for high-performance operations (HTTP/2 + Protobuf)
|
|
@@ -3128,9 +3148,26 @@ declare class SpooledClient {
|
|
|
3128
3148
|
*/
|
|
3129
3149
|
realtime(options?: SpooledRealtimeOptions): Promise<SpooledRealtime>;
|
|
3130
3150
|
/**
|
|
3131
|
-
* 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.
|
|
3132
3162
|
*/
|
|
3133
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;
|
|
3134
3171
|
/**
|
|
3135
3172
|
* Check if token should be refreshed
|
|
3136
3173
|
*/
|
package/dist/index.cjs
CHANGED
|
@@ -49,10 +49,10 @@ 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
58
|
function resolveConfig(options) {
|
|
@@ -1849,6 +1849,12 @@ var WebSocketRealtimeClient = class {
|
|
|
1849
1849
|
state = "disconnected";
|
|
1850
1850
|
reconnectAttempts = 0;
|
|
1851
1851
|
reconnectTimer = null;
|
|
1852
|
+
/**
|
|
1853
|
+
* Set when the server rejected our token on the upgrade (e.g. HTTP 401/403).
|
|
1854
|
+
* The next (re)connect uses it to force the token provider to mint a fresh
|
|
1855
|
+
* JWT instead of replaying the rejected one (which would loop forever).
|
|
1856
|
+
*/
|
|
1857
|
+
authFailure = false;
|
|
1852
1858
|
subscriptions = /* @__PURE__ */ new Map();
|
|
1853
1859
|
pendingCommands = /* @__PURE__ */ new Map();
|
|
1854
1860
|
// Event handlers
|
|
@@ -1857,16 +1863,16 @@ var WebSocketRealtimeClient = class {
|
|
|
1857
1863
|
stateChangeHandlers = /* @__PURE__ */ new Set();
|
|
1858
1864
|
constructor(options) {
|
|
1859
1865
|
this.options = {
|
|
1860
|
-
autoReconnect: true,
|
|
1861
|
-
maxReconnectAttempts: 10,
|
|
1862
|
-
reconnectDelay: 1e3,
|
|
1863
|
-
maxReconnectDelay: 3e4,
|
|
1864
|
-
debug: () => {
|
|
1865
|
-
},
|
|
1866
1866
|
...options,
|
|
1867
|
+
autoReconnect: options.autoReconnect ?? true,
|
|
1868
|
+
maxReconnectAttempts: options.maxReconnectAttempts ?? 10,
|
|
1869
|
+
reconnectDelay: options.reconnectDelay ?? 1e3,
|
|
1870
|
+
maxReconnectDelay: options.maxReconnectDelay ?? 3e4,
|
|
1867
1871
|
// Always resolve to a provider so reconnects can mint a fresh JWT.
|
|
1868
1872
|
// Falls back to the static token when no provider is supplied.
|
|
1869
|
-
tokenProvider: options.tokenProvider ?? (async () => options.token)
|
|
1873
|
+
tokenProvider: options.tokenProvider ?? (async () => options.token),
|
|
1874
|
+
debug: options.debug ?? (() => {
|
|
1875
|
+
})
|
|
1870
1876
|
};
|
|
1871
1877
|
}
|
|
1872
1878
|
/**
|
|
@@ -1930,6 +1936,9 @@ var WebSocketRealtimeClient = class {
|
|
|
1930
1936
|
};
|
|
1931
1937
|
this.ws.onerror = (event) => {
|
|
1932
1938
|
this.options.debug("WebSocket error", event);
|
|
1939
|
+
if (isAuthFailureEvent(event)) {
|
|
1940
|
+
this.authFailure = true;
|
|
1941
|
+
}
|
|
1933
1942
|
};
|
|
1934
1943
|
} catch (error) {
|
|
1935
1944
|
this.setState("disconnected");
|
|
@@ -2016,7 +2025,9 @@ var WebSocketRealtimeClient = class {
|
|
|
2016
2025
|
// Private methods
|
|
2017
2026
|
async buildWsUrl() {
|
|
2018
2027
|
const wsBase = this.options.wsUrl;
|
|
2019
|
-
const
|
|
2028
|
+
const forceRefresh = this.authFailure;
|
|
2029
|
+
this.authFailure = false;
|
|
2030
|
+
const token = await this.options.tokenProvider(forceRefresh);
|
|
2020
2031
|
return `${wsBase}/api/v1/ws?token=${encodeURIComponent(token)}`;
|
|
2021
2032
|
}
|
|
2022
2033
|
setState(state) {
|
|
@@ -2139,6 +2150,17 @@ var WebSocketRealtimeClient = class {
|
|
|
2139
2150
|
function redactToken(url) {
|
|
2140
2151
|
return url.replace(/([?&](?:token|api_key)=)[^&]*/gi, "$1***");
|
|
2141
2152
|
}
|
|
2153
|
+
function isAuthFailureEvent(event) {
|
|
2154
|
+
if (!event) {
|
|
2155
|
+
return false;
|
|
2156
|
+
}
|
|
2157
|
+
const status = event.status ?? event.statusCode ?? event.error?.status;
|
|
2158
|
+
if (status === 401 || status === 403) {
|
|
2159
|
+
return true;
|
|
2160
|
+
}
|
|
2161
|
+
const text = String(event.message ?? event.error?.message ?? event.reason ?? "");
|
|
2162
|
+
return /\b(401|403)\b/.test(text);
|
|
2163
|
+
}
|
|
2142
2164
|
|
|
2143
2165
|
// src/realtime/sse.ts
|
|
2144
2166
|
var SseRealtimeClient = class {
|
|
@@ -2155,16 +2177,22 @@ var SseRealtimeClient = class {
|
|
|
2155
2177
|
stateChangeHandlers = /* @__PURE__ */ new Set();
|
|
2156
2178
|
constructor(options) {
|
|
2157
2179
|
this.options = {
|
|
2158
|
-
autoReconnect: true,
|
|
2159
|
-
maxReconnectAttempts: 10,
|
|
2160
|
-
reconnectDelay: 1e3,
|
|
2161
|
-
maxReconnectDelay: 3e4,
|
|
2162
|
-
debug: () => {
|
|
2163
|
-
},
|
|
2164
2180
|
...options,
|
|
2181
|
+
autoReconnect: options.autoReconnect ?? true,
|
|
2182
|
+
maxReconnectAttempts: options.maxReconnectAttempts ?? 10,
|
|
2183
|
+
reconnectDelay: options.reconnectDelay ?? 1e3,
|
|
2184
|
+
maxReconnectDelay: options.maxReconnectDelay ?? 3e4,
|
|
2165
2185
|
// Always resolve to a provider so reconnects can mint a fresh token.
|
|
2166
2186
|
// Falls back to the static token when no provider is supplied.
|
|
2167
|
-
tokenProvider: options.tokenProvider ?? (async () => options.token)
|
|
2187
|
+
tokenProvider: options.tokenProvider ?? (async () => options.token),
|
|
2188
|
+
// Coalesce debug to a no-op AFTER the spread. A caller that omits debug
|
|
2189
|
+
// still passes `debug: undefined` explicitly (e.g. via SpooledRealtime),
|
|
2190
|
+
// and spreading that would overwrite a pre-spread default with undefined,
|
|
2191
|
+
// making connect() call `this.options.debug(...)` on undefined and throw
|
|
2192
|
+
// "this.options.debug is not a function". Guarding here keeps it callable
|
|
2193
|
+
// regardless of what the caller passes.
|
|
2194
|
+
debug: options.debug ?? (() => {
|
|
2195
|
+
})
|
|
2168
2196
|
};
|
|
2169
2197
|
}
|
|
2170
2198
|
/**
|
|
@@ -2871,6 +2899,18 @@ var SpooledClient = class _SpooledClient {
|
|
|
2871
2899
|
/** Token refresh state */
|
|
2872
2900
|
refreshPromise = null;
|
|
2873
2901
|
tokenExpiresAt = null;
|
|
2902
|
+
/**
|
|
2903
|
+
* Cached JWT for realtime connections (API-key auth path).
|
|
2904
|
+
*
|
|
2905
|
+
* The realtime layer requests a token on every (re)connect. Minting a brand
|
|
2906
|
+
* new JWT via POST /auth/login each time trips the login rate limit (429)
|
|
2907
|
+
* under a reconnect storm, after which realtime can never recover. We cache
|
|
2908
|
+
* the token here — at the client level, shared across reconnects — and reuse
|
|
2909
|
+
* it until it nears expiry.
|
|
2910
|
+
*/
|
|
2911
|
+
cachedJwt = null;
|
|
2912
|
+
/** Deduplicates concurrent logins (e.g. WS + SSE reconnecting together). */
|
|
2913
|
+
jwtLoginPromise = null;
|
|
2874
2914
|
constructor(options) {
|
|
2875
2915
|
this.config = resolveConfig(options);
|
|
2876
2916
|
validateConfig(this.config);
|
|
@@ -2958,31 +2998,72 @@ var SpooledClient = class _SpooledClient {
|
|
|
2958
2998
|
baseUrl: this.config.baseUrl,
|
|
2959
2999
|
wsUrl: this.config.wsUrl,
|
|
2960
3000
|
token,
|
|
2961
|
-
// Provider
|
|
2962
|
-
//
|
|
2963
|
-
|
|
3001
|
+
// Provider the realtime layer calls on every (re)connect. It returns a
|
|
3002
|
+
// cached JWT and only re-logs-in when the token is absent, near expiry,
|
|
3003
|
+
// or the transport asks to force a refresh (e.g. after a 401 upgrade).
|
|
3004
|
+
tokenProvider: (forceRefresh) => this.getJwtToken(forceRefresh),
|
|
2964
3005
|
...options,
|
|
2965
|
-
|
|
3006
|
+
// Only forward debug when the client actually has a logger, so we never
|
|
3007
|
+
// hand the realtime layer an explicit `debug: undefined`. The WS/SSE
|
|
3008
|
+
// constructors also guard against this, but keeping the undefined out of
|
|
3009
|
+
// the option object makes the intent clear at the call site.
|
|
3010
|
+
...this.config.debug ? { debug: this.config.debug } : {}
|
|
2966
3011
|
});
|
|
2967
3012
|
}
|
|
2968
3013
|
/**
|
|
2969
|
-
* Get or acquire a JWT token for realtime connections
|
|
3014
|
+
* Get or acquire a JWT token for realtime connections.
|
|
3015
|
+
*
|
|
3016
|
+
* The realtime layer calls this on every (re)connect. We cache the JWT and
|
|
3017
|
+
* reuse it until it nears expiry so a reconnect storm doesn't hammer
|
|
3018
|
+
* /auth/login and trip its rate limit. A fresh login happens only when the
|
|
3019
|
+
* cached token is absent, within ~60s of its `exp`, or a refresh is forced.
|
|
3020
|
+
*
|
|
3021
|
+
* @param forceRefresh Bypass the cache and mint a new token. Set by the
|
|
3022
|
+
* transport when the server rejected the current token (e.g. a 401 on the
|
|
3023
|
+
* WebSocket upgrade), so a stale/revoked token is replaced without waiting
|
|
3024
|
+
* for it to expire.
|
|
2970
3025
|
*/
|
|
2971
|
-
async getJwtToken() {
|
|
3026
|
+
async getJwtToken(forceRefresh = false) {
|
|
2972
3027
|
if (this.config.accessToken) {
|
|
2973
|
-
if (this.shouldRefreshToken()) {
|
|
3028
|
+
if (forceRefresh || this.shouldRefreshToken()) {
|
|
2974
3029
|
return this.refreshAccessToken();
|
|
2975
3030
|
}
|
|
2976
3031
|
return this.config.accessToken;
|
|
2977
3032
|
}
|
|
2978
3033
|
if (this.config.apiKey) {
|
|
3034
|
+
if (forceRefresh) {
|
|
3035
|
+
this.cachedJwt = null;
|
|
3036
|
+
}
|
|
3037
|
+
if (this.cachedJwt && !isJwtNearExpiry(this.cachedJwt)) {
|
|
3038
|
+
return this.cachedJwt;
|
|
3039
|
+
}
|
|
3040
|
+
return this.loginForJwt();
|
|
3041
|
+
}
|
|
3042
|
+
throw new AuthenticationError("No authentication method available for realtime connection");
|
|
3043
|
+
}
|
|
3044
|
+
/**
|
|
3045
|
+
* Exchange the API key for a JWT via /auth/login and cache the result.
|
|
3046
|
+
*
|
|
3047
|
+
* Concurrent callers share a single in-flight login so a burst of reconnects
|
|
3048
|
+
* (e.g. WebSocket and SSE at once) issues just one request.
|
|
3049
|
+
*/
|
|
3050
|
+
async loginForJwt() {
|
|
3051
|
+
if (this.jwtLoginPromise) {
|
|
3052
|
+
return this.jwtLoginPromise;
|
|
3053
|
+
}
|
|
3054
|
+
this.jwtLoginPromise = (async () => {
|
|
2979
3055
|
const response = await this.auth.login({ apiKey: this.config.apiKey });
|
|
2980
3056
|
this.http.setAuthToken(response.accessToken);
|
|
2981
3057
|
this.tokenExpiresAt = Date.now() + response.expiresIn * 1e3;
|
|
2982
3058
|
this.config.refreshToken = response.refreshToken;
|
|
3059
|
+
this.cachedJwt = response.accessToken;
|
|
2983
3060
|
return response.accessToken;
|
|
3061
|
+
})();
|
|
3062
|
+
try {
|
|
3063
|
+
return await this.jwtLoginPromise;
|
|
3064
|
+
} finally {
|
|
3065
|
+
this.jwtLoginPromise = null;
|
|
2984
3066
|
}
|
|
2985
|
-
throw new AuthenticationError("No authentication method available for realtime connection");
|
|
2986
3067
|
}
|
|
2987
3068
|
/**
|
|
2988
3069
|
* Check if token should be refreshed
|
|
@@ -3083,6 +3164,34 @@ var SpooledClient = class _SpooledClient {
|
|
|
3083
3164
|
function createClient(options) {
|
|
3084
3165
|
return new SpooledClient(options);
|
|
3085
3166
|
}
|
|
3167
|
+
function decodeJwtExp(token) {
|
|
3168
|
+
const parts = token.split(".");
|
|
3169
|
+
if (parts.length < 2) {
|
|
3170
|
+
return null;
|
|
3171
|
+
}
|
|
3172
|
+
try {
|
|
3173
|
+
const payload = JSON.parse(base64UrlDecode(parts[1]));
|
|
3174
|
+
return typeof payload.exp === "number" ? payload.exp : null;
|
|
3175
|
+
} catch {
|
|
3176
|
+
return null;
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
function isJwtNearExpiry(token, skewMs = 6e4) {
|
|
3180
|
+
const exp = decodeJwtExp(token);
|
|
3181
|
+
if (exp === null) {
|
|
3182
|
+
return true;
|
|
3183
|
+
}
|
|
3184
|
+
return Date.now() >= exp * 1e3 - skewMs;
|
|
3185
|
+
}
|
|
3186
|
+
function base64UrlDecode(segment) {
|
|
3187
|
+
const b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
3188
|
+
if (typeof globalThis.Buffer !== "undefined") {
|
|
3189
|
+
return globalThis.Buffer.from(b64, "base64").toString("utf8");
|
|
3190
|
+
}
|
|
3191
|
+
const binary = globalThis.atob(b64);
|
|
3192
|
+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
3193
|
+
return new globalThis.TextDecoder().decode(bytes);
|
|
3194
|
+
}
|
|
3086
3195
|
|
|
3087
3196
|
// src/types/common.ts
|
|
3088
3197
|
function createPaginatedResponse(items, limit, offset = 0, total) {
|