@spooled/sdk 1.0.27 → 1.0.29
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 +13 -12
- package/dist/{index-BKzL6R4h.d.cts → index-CZorNMoD.d.cts} +47 -6
- package/dist/{index-BKzL6R4h.d.ts → index-CZorNMoD.d.ts} +47 -6
- package/dist/index.cjs +123 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +123 -22
- package/dist/index.js.map +1 -1
- package/dist/worker/index.cjs +3 -1
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +1 -1
- package/dist/worker/index.d.ts +1 -1
- package/dist/worker/index.js +3 -1
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ npm install @spooled/sdk
|
|
|
37
37
|
import { SpooledClient } from '@spooled/sdk';
|
|
38
38
|
|
|
39
39
|
const client = new SpooledClient({
|
|
40
|
-
apiKey: '
|
|
40
|
+
apiKey: 'sp_live_your_api_key',
|
|
41
41
|
// For self-hosted: baseUrl: 'https://your-spooled-server.com'
|
|
42
42
|
});
|
|
43
43
|
|
|
@@ -116,7 +116,7 @@ Process jobs with the built-in worker runtime:
|
|
|
116
116
|
```typescript
|
|
117
117
|
import { SpooledClient, SpooledWorker } from '@spooled/sdk';
|
|
118
118
|
|
|
119
|
-
const client = new SpooledClient({ apiKey: '
|
|
119
|
+
const client = new SpooledClient({ apiKey: 'sp_live_...' });
|
|
120
120
|
|
|
121
121
|
const worker = new SpooledWorker(client, {
|
|
122
122
|
queueName: 'my-queue',
|
|
@@ -293,7 +293,7 @@ const { exists } = await client.auth.checkEmail('user@example.com');
|
|
|
293
293
|
|
|
294
294
|
// Exchange API key for JWT
|
|
295
295
|
const { accessToken, refreshToken } = await client.auth.login({
|
|
296
|
-
apiKey: '
|
|
296
|
+
apiKey: 'sp_live_...'
|
|
297
297
|
});
|
|
298
298
|
|
|
299
299
|
// Use JWT for subsequent requests
|
|
@@ -323,13 +323,15 @@ All operations automatically enforce tier-based limits:
|
|
|
323
323
|
When limits are exceeded, you'll receive a `403 Forbidden` response with details:
|
|
324
324
|
|
|
325
325
|
```typescript
|
|
326
|
+
import { AuthorizationError } from '@spooled/sdk';
|
|
327
|
+
|
|
326
328
|
try {
|
|
327
329
|
await client.jobs.create({ /* ... */ });
|
|
328
330
|
} catch (error) {
|
|
329
|
-
if (error
|
|
330
|
-
|
|
331
|
-
console.log(`
|
|
332
|
-
console.log(
|
|
331
|
+
if (error instanceof AuthorizationError) {
|
|
332
|
+
// Plan limit reached (HTTP 403); the message describes the limit
|
|
333
|
+
console.log(`Plan limit: ${error.message}`);
|
|
334
|
+
console.log('Details:', error.details);
|
|
333
335
|
}
|
|
334
336
|
}
|
|
335
337
|
```
|
|
@@ -343,7 +345,7 @@ import {
|
|
|
343
345
|
NotFoundError,
|
|
344
346
|
RateLimitError,
|
|
345
347
|
ValidationError,
|
|
346
|
-
|
|
348
|
+
AuthorizationError,
|
|
347
349
|
isSpooledError,
|
|
348
350
|
} from '@spooled/sdk';
|
|
349
351
|
|
|
@@ -352,9 +354,8 @@ try {
|
|
|
352
354
|
} catch (error) {
|
|
353
355
|
if (error instanceof NotFoundError) {
|
|
354
356
|
console.log('Job not found');
|
|
355
|
-
} else if (error instanceof
|
|
356
|
-
console.log(`
|
|
357
|
-
console.log(`Upgrade to ${error.upgradeTo} for more capacity`);
|
|
357
|
+
} else if (error instanceof AuthorizationError) {
|
|
358
|
+
console.log(`Forbidden (e.g. plan limit): ${error.message}`);
|
|
358
359
|
} else if (error instanceof RateLimitError) {
|
|
359
360
|
console.log(`Retry after ${error.getRetryAfter()} seconds`);
|
|
360
361
|
} else if (isSpooledError(error)) {
|
|
@@ -375,7 +376,7 @@ import { SpooledGrpcClient } from '@spooled/sdk';
|
|
|
375
376
|
// useTls: false if connecting to localhost or direct backend
|
|
376
377
|
const grpcClient = new SpooledGrpcClient({
|
|
377
378
|
address: 'grpc.spooled.cloud:443',
|
|
378
|
-
apiKey: '
|
|
379
|
+
apiKey: 'sp_live_your_key',
|
|
379
380
|
useTls: true
|
|
380
381
|
});
|
|
381
382
|
|
|
@@ -285,15 +285,22 @@ declare class HttpClient {
|
|
|
285
285
|
private readonly debug;
|
|
286
286
|
/** Current authentication token (API key or JWT) */
|
|
287
287
|
private authToken;
|
|
288
|
+
/** Optional function to mint a fresh access token (set when auto-refresh is enabled) */
|
|
289
|
+
private refreshTokenFn;
|
|
290
|
+
/** Guard to prevent a refresh request from recursively triggering another refresh */
|
|
291
|
+
private isRefreshing;
|
|
288
292
|
constructor(config: ResolvedConfig, circuitBreaker: CircuitBreaker);
|
|
289
293
|
/**
|
|
290
294
|
* Set the authentication token
|
|
291
295
|
*/
|
|
292
296
|
setAuthToken(token: string): void;
|
|
293
297
|
/**
|
|
294
|
-
* Set the token refresh function
|
|
298
|
+
* Set the token refresh function.
|
|
299
|
+
*
|
|
300
|
+
* When set, an expired-token 401 on the data plane triggers a single
|
|
301
|
+
* refresh + retry of the original request with the new token.
|
|
295
302
|
*/
|
|
296
|
-
setRefreshTokenFn(
|
|
303
|
+
setRefreshTokenFn(fn: () => Promise<string>): void;
|
|
297
304
|
/**
|
|
298
305
|
* Build full URL with query parameters
|
|
299
306
|
*/
|
|
@@ -314,6 +321,22 @@ declare class HttpClient {
|
|
|
314
321
|
* Make an HTTP request with retry and circuit breaker
|
|
315
322
|
*/
|
|
316
323
|
request<T>(path: string, options?: HttpRequestOptions): Promise<HttpResponse<T>>;
|
|
324
|
+
/**
|
|
325
|
+
* Determine whether a request carries an idempotency key (making a
|
|
326
|
+
* non-idempotent method safe to retry).
|
|
327
|
+
*/
|
|
328
|
+
private hasIdempotencyKey;
|
|
329
|
+
/**
|
|
330
|
+
* Resolve the retry configuration for a request.
|
|
331
|
+
*
|
|
332
|
+
* Idempotent methods (GET/PUT/DELETE) and any request carrying an
|
|
333
|
+
* idempotency key retry normally. Non-idempotent methods (POST/PATCH)
|
|
334
|
+
* without an idempotency key must NOT be retried on ambiguous failures
|
|
335
|
+
* (network/timeout/5xx) — doing so risks duplicate jobs, double claims,
|
|
336
|
+
* etc. They are still retried on 429, which is rejected before the
|
|
337
|
+
* server processes the request and is therefore side-effect-free.
|
|
338
|
+
*/
|
|
339
|
+
private resolveRetryConfig;
|
|
317
340
|
/**
|
|
318
341
|
* Make a GET request
|
|
319
342
|
*/
|
|
@@ -401,7 +424,11 @@ interface ValidateTokenResponse {
|
|
|
401
424
|
|
|
402
425
|
declare class AuthResource {
|
|
403
426
|
private readonly http;
|
|
404
|
-
|
|
427
|
+
/** Optional accessor for the client's current refresh token (used by logout) */
|
|
428
|
+
private readonly getRefreshToken?;
|
|
429
|
+
constructor(http: HttpClient,
|
|
430
|
+
/** Optional accessor for the client's current refresh token (used by logout) */
|
|
431
|
+
getRefreshToken?: (() => string | undefined) | undefined);
|
|
405
432
|
/**
|
|
406
433
|
* Exchange API key for JWT tokens
|
|
407
434
|
*/
|
|
@@ -411,9 +438,13 @@ declare class AuthResource {
|
|
|
411
438
|
*/
|
|
412
439
|
refresh(params: RefreshTokenParams): Promise<RefreshTokenResponse>;
|
|
413
440
|
/**
|
|
414
|
-
* Logout and
|
|
441
|
+
* Logout and revoke the refresh token server-side.
|
|
442
|
+
*
|
|
443
|
+
* The backend requires the refresh token in the body to invalidate the
|
|
444
|
+
* session. When omitted, the client's current refresh token (if any) is
|
|
445
|
+
* used automatically.
|
|
415
446
|
*/
|
|
416
|
-
logout(): Promise<void>;
|
|
447
|
+
logout(refreshToken?: string): Promise<void>;
|
|
417
448
|
/**
|
|
418
449
|
* Get current user/session info
|
|
419
450
|
*/
|
|
@@ -2343,8 +2374,16 @@ interface RealtimeConnectionOptions {
|
|
|
2343
2374
|
baseUrl: string;
|
|
2344
2375
|
/** WebSocket URL for realtime connection (e.g., wss://api.spooled.cloud) - used for WebSocket */
|
|
2345
2376
|
wsUrl: string;
|
|
2346
|
-
/** JWT token for authentication */
|
|
2377
|
+
/** JWT token for authentication (used as a fallback when tokenProvider is not supplied) */
|
|
2347
2378
|
token: string;
|
|
2379
|
+
/**
|
|
2380
|
+
* Async provider that mints a fresh JWT for each (re)connect.
|
|
2381
|
+
*
|
|
2382
|
+
* JWTs are short-lived, so a static token captured once will be stale by
|
|
2383
|
+
* the time a long-running connection needs to reconnect. When provided,
|
|
2384
|
+
* this is invoked before every connect/reconnect to obtain a valid token.
|
|
2385
|
+
*/
|
|
2386
|
+
tokenProvider?: () => Promise<string>;
|
|
2348
2387
|
/** Auto-reconnect on disconnect (default: true) */
|
|
2349
2388
|
autoReconnect?: boolean;
|
|
2350
2389
|
/** Maximum reconnect attempts (default: 10) */
|
|
@@ -2387,6 +2426,8 @@ interface FullRealtimeOptions extends SpooledRealtimeOptions {
|
|
|
2387
2426
|
baseUrl: string;
|
|
2388
2427
|
wsUrl: string;
|
|
2389
2428
|
token: string;
|
|
2429
|
+
/** Async provider that mints a fresh JWT for each (re)connect */
|
|
2430
|
+
tokenProvider?: () => Promise<string>;
|
|
2390
2431
|
debug?: (message: string, meta?: unknown) => void;
|
|
2391
2432
|
}
|
|
2392
2433
|
/** Event handler type - simplified for internal use */
|
|
@@ -285,15 +285,22 @@ declare class HttpClient {
|
|
|
285
285
|
private readonly debug;
|
|
286
286
|
/** Current authentication token (API key or JWT) */
|
|
287
287
|
private authToken;
|
|
288
|
+
/** Optional function to mint a fresh access token (set when auto-refresh is enabled) */
|
|
289
|
+
private refreshTokenFn;
|
|
290
|
+
/** Guard to prevent a refresh request from recursively triggering another refresh */
|
|
291
|
+
private isRefreshing;
|
|
288
292
|
constructor(config: ResolvedConfig, circuitBreaker: CircuitBreaker);
|
|
289
293
|
/**
|
|
290
294
|
* Set the authentication token
|
|
291
295
|
*/
|
|
292
296
|
setAuthToken(token: string): void;
|
|
293
297
|
/**
|
|
294
|
-
* Set the token refresh function
|
|
298
|
+
* Set the token refresh function.
|
|
299
|
+
*
|
|
300
|
+
* When set, an expired-token 401 on the data plane triggers a single
|
|
301
|
+
* refresh + retry of the original request with the new token.
|
|
295
302
|
*/
|
|
296
|
-
setRefreshTokenFn(
|
|
303
|
+
setRefreshTokenFn(fn: () => Promise<string>): void;
|
|
297
304
|
/**
|
|
298
305
|
* Build full URL with query parameters
|
|
299
306
|
*/
|
|
@@ -314,6 +321,22 @@ declare class HttpClient {
|
|
|
314
321
|
* Make an HTTP request with retry and circuit breaker
|
|
315
322
|
*/
|
|
316
323
|
request<T>(path: string, options?: HttpRequestOptions): Promise<HttpResponse<T>>;
|
|
324
|
+
/**
|
|
325
|
+
* Determine whether a request carries an idempotency key (making a
|
|
326
|
+
* non-idempotent method safe to retry).
|
|
327
|
+
*/
|
|
328
|
+
private hasIdempotencyKey;
|
|
329
|
+
/**
|
|
330
|
+
* Resolve the retry configuration for a request.
|
|
331
|
+
*
|
|
332
|
+
* Idempotent methods (GET/PUT/DELETE) and any request carrying an
|
|
333
|
+
* idempotency key retry normally. Non-idempotent methods (POST/PATCH)
|
|
334
|
+
* without an idempotency key must NOT be retried on ambiguous failures
|
|
335
|
+
* (network/timeout/5xx) — doing so risks duplicate jobs, double claims,
|
|
336
|
+
* etc. They are still retried on 429, which is rejected before the
|
|
337
|
+
* server processes the request and is therefore side-effect-free.
|
|
338
|
+
*/
|
|
339
|
+
private resolveRetryConfig;
|
|
317
340
|
/**
|
|
318
341
|
* Make a GET request
|
|
319
342
|
*/
|
|
@@ -401,7 +424,11 @@ interface ValidateTokenResponse {
|
|
|
401
424
|
|
|
402
425
|
declare class AuthResource {
|
|
403
426
|
private readonly http;
|
|
404
|
-
|
|
427
|
+
/** Optional accessor for the client's current refresh token (used by logout) */
|
|
428
|
+
private readonly getRefreshToken?;
|
|
429
|
+
constructor(http: HttpClient,
|
|
430
|
+
/** Optional accessor for the client's current refresh token (used by logout) */
|
|
431
|
+
getRefreshToken?: (() => string | undefined) | undefined);
|
|
405
432
|
/**
|
|
406
433
|
* Exchange API key for JWT tokens
|
|
407
434
|
*/
|
|
@@ -411,9 +438,13 @@ declare class AuthResource {
|
|
|
411
438
|
*/
|
|
412
439
|
refresh(params: RefreshTokenParams): Promise<RefreshTokenResponse>;
|
|
413
440
|
/**
|
|
414
|
-
* Logout and
|
|
441
|
+
* Logout and revoke the refresh token server-side.
|
|
442
|
+
*
|
|
443
|
+
* The backend requires the refresh token in the body to invalidate the
|
|
444
|
+
* session. When omitted, the client's current refresh token (if any) is
|
|
445
|
+
* used automatically.
|
|
415
446
|
*/
|
|
416
|
-
logout(): Promise<void>;
|
|
447
|
+
logout(refreshToken?: string): Promise<void>;
|
|
417
448
|
/**
|
|
418
449
|
* Get current user/session info
|
|
419
450
|
*/
|
|
@@ -2343,8 +2374,16 @@ interface RealtimeConnectionOptions {
|
|
|
2343
2374
|
baseUrl: string;
|
|
2344
2375
|
/** WebSocket URL for realtime connection (e.g., wss://api.spooled.cloud) - used for WebSocket */
|
|
2345
2376
|
wsUrl: string;
|
|
2346
|
-
/** JWT token for authentication */
|
|
2377
|
+
/** JWT token for authentication (used as a fallback when tokenProvider is not supplied) */
|
|
2347
2378
|
token: string;
|
|
2379
|
+
/**
|
|
2380
|
+
* Async provider that mints a fresh JWT for each (re)connect.
|
|
2381
|
+
*
|
|
2382
|
+
* JWTs are short-lived, so a static token captured once will be stale by
|
|
2383
|
+
* the time a long-running connection needs to reconnect. When provided,
|
|
2384
|
+
* this is invoked before every connect/reconnect to obtain a valid token.
|
|
2385
|
+
*/
|
|
2386
|
+
tokenProvider?: () => Promise<string>;
|
|
2348
2387
|
/** Auto-reconnect on disconnect (default: true) */
|
|
2349
2388
|
autoReconnect?: boolean;
|
|
2350
2389
|
/** Maximum reconnect attempts (default: 10) */
|
|
@@ -2387,6 +2426,8 @@ interface FullRealtimeOptions extends SpooledRealtimeOptions {
|
|
|
2387
2426
|
baseUrl: string;
|
|
2388
2427
|
wsUrl: string;
|
|
2389
2428
|
token: string;
|
|
2429
|
+
/** Async provider that mints a fresh JWT for each (re)connect */
|
|
2430
|
+
tokenProvider?: () => Promise<string>;
|
|
2390
2431
|
debug?: (message: string, meta?: unknown) => void;
|
|
2391
2432
|
}
|
|
2392
2433
|
/** Event handler type - simplified for internal use */
|
package/dist/index.cjs
CHANGED
|
@@ -328,9 +328,10 @@ async function parseErrorBody(response) {
|
|
|
328
328
|
try {
|
|
329
329
|
const body = await response.json();
|
|
330
330
|
if (typeof body === "object" && body !== null) {
|
|
331
|
+
const message = (body.message ?? body.error) || response.statusText;
|
|
331
332
|
return {
|
|
332
333
|
code: body.code || "UNKNOWN_ERROR",
|
|
333
|
-
message
|
|
334
|
+
message,
|
|
334
335
|
details: body.details
|
|
335
336
|
};
|
|
336
337
|
}
|
|
@@ -548,12 +549,17 @@ function createRetryWrapper(config) {
|
|
|
548
549
|
}
|
|
549
550
|
|
|
550
551
|
// src/utils/http.ts
|
|
552
|
+
var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "PUT", "DELETE"]);
|
|
551
553
|
var HttpClient = class {
|
|
552
554
|
config;
|
|
553
555
|
circuitBreaker;
|
|
554
556
|
debug;
|
|
555
557
|
/** Current authentication token (API key or JWT) */
|
|
556
558
|
authToken;
|
|
559
|
+
/** Optional function to mint a fresh access token (set when auto-refresh is enabled) */
|
|
560
|
+
refreshTokenFn = null;
|
|
561
|
+
/** Guard to prevent a refresh request from recursively triggering another refresh */
|
|
562
|
+
isRefreshing = false;
|
|
557
563
|
constructor(config, circuitBreaker) {
|
|
558
564
|
this.config = config;
|
|
559
565
|
this.circuitBreaker = circuitBreaker;
|
|
@@ -567,9 +573,13 @@ var HttpClient = class {
|
|
|
567
573
|
this.authToken = token;
|
|
568
574
|
}
|
|
569
575
|
/**
|
|
570
|
-
* Set the token refresh function
|
|
576
|
+
* Set the token refresh function.
|
|
577
|
+
*
|
|
578
|
+
* When set, an expired-token 401 on the data plane triggers a single
|
|
579
|
+
* refresh + retry of the original request with the new token.
|
|
571
580
|
*/
|
|
572
|
-
setRefreshTokenFn(
|
|
581
|
+
setRefreshTokenFn(fn) {
|
|
582
|
+
this.refreshTokenFn = fn;
|
|
573
583
|
}
|
|
574
584
|
/**
|
|
575
585
|
* Build full URL with query parameters
|
|
@@ -639,7 +649,7 @@ var HttpClient = class {
|
|
|
639
649
|
/**
|
|
640
650
|
* Execute a single HTTP request (no retry)
|
|
641
651
|
*/
|
|
642
|
-
async executeRequest(url, options) {
|
|
652
|
+
async executeRequest(url, options, hasRefreshed = false) {
|
|
643
653
|
const method = options.method ?? "GET";
|
|
644
654
|
const headers = this.buildHeaders(options.headers);
|
|
645
655
|
const timeout = options.timeout ?? this.config.timeout;
|
|
@@ -668,6 +678,23 @@ var HttpClient = class {
|
|
|
668
678
|
}
|
|
669
679
|
this.debug?.(`Response: ${response.status}`, { url });
|
|
670
680
|
if (!response.ok) {
|
|
681
|
+
if (response.status === 401 && this.refreshTokenFn && !hasRefreshed && !this.isRefreshing) {
|
|
682
|
+
let refreshed = false;
|
|
683
|
+
this.isRefreshing = true;
|
|
684
|
+
try {
|
|
685
|
+
const newToken = await this.refreshTokenFn();
|
|
686
|
+
this.setAuthToken(newToken);
|
|
687
|
+
refreshed = true;
|
|
688
|
+
} catch (refreshError) {
|
|
689
|
+
const message = refreshError instanceof Error ? refreshError.message : "unknown";
|
|
690
|
+
this.debug?.("Token refresh failed", { error: message });
|
|
691
|
+
} finally {
|
|
692
|
+
this.isRefreshing = false;
|
|
693
|
+
}
|
|
694
|
+
if (refreshed) {
|
|
695
|
+
return this.executeRequest(url, options, true);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
671
698
|
const apiError = await createErrorFromResponse(response);
|
|
672
699
|
throw apiError;
|
|
673
700
|
}
|
|
@@ -692,6 +719,7 @@ var HttpClient = class {
|
|
|
692
719
|
*/
|
|
693
720
|
async request(path, options = {}) {
|
|
694
721
|
const url = this.buildUrl(path, options.params, options.skipApiPrefix);
|
|
722
|
+
const method = options.method ?? "GET";
|
|
695
723
|
const execute = async () => {
|
|
696
724
|
return this.circuitBreaker.execute(() => this.executeRequest(url, options));
|
|
697
725
|
};
|
|
@@ -699,13 +727,46 @@ var HttpClient = class {
|
|
|
699
727
|
return execute();
|
|
700
728
|
}
|
|
701
729
|
return withRetry(execute, {
|
|
702
|
-
config: this.
|
|
730
|
+
config: this.resolveRetryConfig(method, options),
|
|
703
731
|
signal: options.signal,
|
|
704
732
|
onRetry: (attempt, error, delayMs) => {
|
|
705
733
|
this.debug?.(`Retry attempt ${attempt} after ${delayMs}ms`, { error: error.message });
|
|
706
734
|
}
|
|
707
735
|
});
|
|
708
736
|
}
|
|
737
|
+
/**
|
|
738
|
+
* Determine whether a request carries an idempotency key (making a
|
|
739
|
+
* non-idempotent method safe to retry).
|
|
740
|
+
*/
|
|
741
|
+
hasIdempotencyKey(options) {
|
|
742
|
+
if (!options.headers) {
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
return Object.keys(options.headers).some((key) => key.toLowerCase() === "idempotency-key");
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Resolve the retry configuration for a request.
|
|
749
|
+
*
|
|
750
|
+
* Idempotent methods (GET/PUT/DELETE) and any request carrying an
|
|
751
|
+
* idempotency key retry normally. Non-idempotent methods (POST/PATCH)
|
|
752
|
+
* without an idempotency key must NOT be retried on ambiguous failures
|
|
753
|
+
* (network/timeout/5xx) — doing so risks duplicate jobs, double claims,
|
|
754
|
+
* etc. They are still retried on 429, which is rejected before the
|
|
755
|
+
* server processes the request and is therefore side-effect-free.
|
|
756
|
+
*/
|
|
757
|
+
resolveRetryConfig(method, options) {
|
|
758
|
+
const base = this.config.retry;
|
|
759
|
+
if (base.retryOn) {
|
|
760
|
+
return base;
|
|
761
|
+
}
|
|
762
|
+
if (IDEMPOTENT_METHODS.has(method) || this.hasIdempotencyKey(options)) {
|
|
763
|
+
return base;
|
|
764
|
+
}
|
|
765
|
+
return {
|
|
766
|
+
...base,
|
|
767
|
+
retryOn: (error) => error instanceof RateLimitError
|
|
768
|
+
};
|
|
769
|
+
}
|
|
709
770
|
/**
|
|
710
771
|
* Make a GET request
|
|
711
772
|
*/
|
|
@@ -907,8 +968,9 @@ function createCircuitBreaker(config) {
|
|
|
907
968
|
|
|
908
969
|
// src/resources/auth.ts
|
|
909
970
|
var AuthResource = class {
|
|
910
|
-
constructor(http) {
|
|
971
|
+
constructor(http, getRefreshToken) {
|
|
911
972
|
this.http = http;
|
|
973
|
+
this.getRefreshToken = getRefreshToken;
|
|
912
974
|
}
|
|
913
975
|
/**
|
|
914
976
|
* Exchange API key for JWT tokens
|
|
@@ -923,10 +985,15 @@ var AuthResource = class {
|
|
|
923
985
|
return this.http.post("/auth/refresh", params);
|
|
924
986
|
}
|
|
925
987
|
/**
|
|
926
|
-
* Logout and
|
|
988
|
+
* Logout and revoke the refresh token server-side.
|
|
989
|
+
*
|
|
990
|
+
* The backend requires the refresh token in the body to invalidate the
|
|
991
|
+
* session. When omitted, the client's current refresh token (if any) is
|
|
992
|
+
* used automatically.
|
|
927
993
|
*/
|
|
928
|
-
async logout() {
|
|
929
|
-
|
|
994
|
+
async logout(refreshToken) {
|
|
995
|
+
const token = refreshToken ?? this.getRefreshToken?.();
|
|
996
|
+
await this.http.post("/auth/logout", token ? { refreshToken: token } : void 0);
|
|
930
997
|
}
|
|
931
998
|
/**
|
|
932
999
|
* Get current user/session info
|
|
@@ -1796,7 +1863,10 @@ var WebSocketRealtimeClient = class {
|
|
|
1796
1863
|
maxReconnectDelay: 3e4,
|
|
1797
1864
|
debug: () => {
|
|
1798
1865
|
},
|
|
1799
|
-
...options
|
|
1866
|
+
...options,
|
|
1867
|
+
// Always resolve to a provider so reconnects can mint a fresh JWT.
|
|
1868
|
+
// Falls back to the static token when no provider is supplied.
|
|
1869
|
+
tokenProvider: options.tokenProvider ?? (async () => options.token)
|
|
1800
1870
|
};
|
|
1801
1871
|
}
|
|
1802
1872
|
/**
|
|
@@ -1823,8 +1893,14 @@ var WebSocketRealtimeClient = class {
|
|
|
1823
1893
|
return;
|
|
1824
1894
|
}
|
|
1825
1895
|
this.setState("connecting");
|
|
1826
|
-
|
|
1827
|
-
|
|
1896
|
+
let wsUrl;
|
|
1897
|
+
try {
|
|
1898
|
+
wsUrl = await this.buildWsUrl();
|
|
1899
|
+
} catch (error) {
|
|
1900
|
+
this.setState("disconnected");
|
|
1901
|
+
throw new Error(`Failed to acquire realtime token: ${error}`);
|
|
1902
|
+
}
|
|
1903
|
+
this.options.debug(`Connecting to ${redactToken(wsUrl)}`);
|
|
1828
1904
|
let WebSocketImpl;
|
|
1829
1905
|
try {
|
|
1830
1906
|
WebSocketImpl = await this.getWebSocketImpl();
|
|
@@ -1938,9 +2014,10 @@ var WebSocketRealtimeClient = class {
|
|
|
1938
2014
|
return () => this.stateChangeHandlers.delete(handler);
|
|
1939
2015
|
}
|
|
1940
2016
|
// Private methods
|
|
1941
|
-
buildWsUrl() {
|
|
2017
|
+
async buildWsUrl() {
|
|
1942
2018
|
const wsBase = this.options.wsUrl;
|
|
1943
|
-
|
|
2019
|
+
const token = await this.options.tokenProvider();
|
|
2020
|
+
return `${wsBase}/api/v1/ws?token=${encodeURIComponent(token)}`;
|
|
1944
2021
|
}
|
|
1945
2022
|
setState(state) {
|
|
1946
2023
|
if (this.state !== state) {
|
|
@@ -1950,7 +2027,7 @@ var WebSocketRealtimeClient = class {
|
|
|
1950
2027
|
}
|
|
1951
2028
|
handleMessage(data) {
|
|
1952
2029
|
try {
|
|
1953
|
-
const message = JSON.parse(data);
|
|
2030
|
+
const message = convertResponse(JSON.parse(data));
|
|
1954
2031
|
if (message.type === "subscribed" || message.type === "unsubscribed" || message.type === "error") {
|
|
1955
2032
|
this.handleCommandResponse(message);
|
|
1956
2033
|
return;
|
|
@@ -2007,10 +2084,11 @@ var WebSocketRealtimeClient = class {
|
|
|
2007
2084
|
scheduleReconnect() {
|
|
2008
2085
|
this.setState("reconnecting");
|
|
2009
2086
|
this.reconnectAttempts++;
|
|
2010
|
-
const
|
|
2087
|
+
const baseDelay = Math.min(
|
|
2011
2088
|
this.options.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
|
|
2012
2089
|
this.options.maxReconnectDelay
|
|
2013
2090
|
);
|
|
2091
|
+
const delay = Math.floor(baseDelay + Math.random() * baseDelay * 0.25);
|
|
2014
2092
|
this.options.debug(`Scheduling reconnect attempt ${this.reconnectAttempts} in ${delay}ms`);
|
|
2015
2093
|
this.reconnectTimer = setTimeout(async () => {
|
|
2016
2094
|
try {
|
|
@@ -2058,6 +2136,9 @@ var WebSocketRealtimeClient = class {
|
|
|
2058
2136
|
return `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
2059
2137
|
}
|
|
2060
2138
|
};
|
|
2139
|
+
function redactToken(url) {
|
|
2140
|
+
return url.replace(/([?&](?:token|api_key)=)[^&]*/gi, "$1***");
|
|
2141
|
+
}
|
|
2061
2142
|
|
|
2062
2143
|
// src/realtime/sse.ts
|
|
2063
2144
|
var SseRealtimeClient = class {
|
|
@@ -2080,7 +2161,10 @@ var SseRealtimeClient = class {
|
|
|
2080
2161
|
maxReconnectDelay: 3e4,
|
|
2081
2162
|
debug: () => {
|
|
2082
2163
|
},
|
|
2083
|
-
...options
|
|
2164
|
+
...options,
|
|
2165
|
+
// Always resolve to a provider so reconnects can mint a fresh token.
|
|
2166
|
+
// Falls back to the static token when no provider is supplied.
|
|
2167
|
+
tokenProvider: options.tokenProvider ?? (async () => options.token)
|
|
2084
2168
|
};
|
|
2085
2169
|
}
|
|
2086
2170
|
/**
|
|
@@ -2112,9 +2196,15 @@ var SseRealtimeClient = class {
|
|
|
2112
2196
|
throw new Error("eventsource package is required for SSE in Node.js. Install it with: npm install eventsource");
|
|
2113
2197
|
}
|
|
2114
2198
|
}
|
|
2199
|
+
let token;
|
|
2200
|
+
try {
|
|
2201
|
+
token = await this.options.tokenProvider();
|
|
2202
|
+
} catch (error) {
|
|
2203
|
+
this.setState("disconnected");
|
|
2204
|
+
throw new Error(`Failed to acquire realtime token: ${error}`);
|
|
2205
|
+
}
|
|
2115
2206
|
return new Promise((resolve, reject) => {
|
|
2116
2207
|
try {
|
|
2117
|
-
const token = this.options.token;
|
|
2118
2208
|
this.eventSource = new EventSourceImpl(sseUrl, {
|
|
2119
2209
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2120
2210
|
fetch: (input, init) => fetch(input, {
|
|
@@ -2138,6 +2228,10 @@ var SseRealtimeClient = class {
|
|
|
2138
2228
|
this.options.debug("SSE error");
|
|
2139
2229
|
if (this.state === "connecting") {
|
|
2140
2230
|
this.setState("disconnected");
|
|
2231
|
+
if (this.eventSource) {
|
|
2232
|
+
this.eventSource.close();
|
|
2233
|
+
this.eventSource = null;
|
|
2234
|
+
}
|
|
2141
2235
|
reject(new Error("SSE connection failed"));
|
|
2142
2236
|
return;
|
|
2143
2237
|
}
|
|
@@ -2222,7 +2316,7 @@ var SseRealtimeClient = class {
|
|
|
2222
2316
|
}
|
|
2223
2317
|
handleMessage(data) {
|
|
2224
2318
|
try {
|
|
2225
|
-
const event = JSON.parse(data);
|
|
2319
|
+
const event = convertResponse(JSON.parse(data));
|
|
2226
2320
|
this.options.debug(`Received SSE event: ${event.type}`, event);
|
|
2227
2321
|
const handlers = this.eventHandlers.get(event.type);
|
|
2228
2322
|
if (handlers) {
|
|
@@ -2260,10 +2354,11 @@ var SseRealtimeClient = class {
|
|
|
2260
2354
|
scheduleReconnect() {
|
|
2261
2355
|
this.setState("reconnecting");
|
|
2262
2356
|
this.reconnectAttempts++;
|
|
2263
|
-
const
|
|
2357
|
+
const baseDelay = Math.min(
|
|
2264
2358
|
this.options.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
|
|
2265
2359
|
this.options.maxReconnectDelay
|
|
2266
2360
|
);
|
|
2361
|
+
const delay = Math.floor(baseDelay + Math.random() * baseDelay * 0.25);
|
|
2267
2362
|
this.options.debug(`Scheduling SSE reconnect attempt ${this.reconnectAttempts} in ${delay}ms`);
|
|
2268
2363
|
this.reconnectTimer = setTimeout(async () => {
|
|
2269
2364
|
try {
|
|
@@ -2284,6 +2379,7 @@ var SpooledRealtime = class {
|
|
|
2284
2379
|
baseUrl: options.baseUrl,
|
|
2285
2380
|
wsUrl: options.wsUrl,
|
|
2286
2381
|
token: options.token,
|
|
2382
|
+
tokenProvider: options.tokenProvider,
|
|
2287
2383
|
autoReconnect: options.autoReconnect,
|
|
2288
2384
|
maxReconnectAttempts: options.maxReconnectAttempts,
|
|
2289
2385
|
reconnectDelay: options.reconnectDelay,
|
|
@@ -2783,7 +2879,7 @@ var SpooledClient = class _SpooledClient {
|
|
|
2783
2879
|
if (this.config.accessToken && this.config.refreshToken && this.config.autoRefreshToken) {
|
|
2784
2880
|
this.http.setRefreshTokenFn(this.refreshAccessToken.bind(this));
|
|
2785
2881
|
}
|
|
2786
|
-
this.auth = new AuthResource(this.http);
|
|
2882
|
+
this.auth = new AuthResource(this.http, () => this.config.refreshToken);
|
|
2787
2883
|
this.jobs = new JobsResource(this.http);
|
|
2788
2884
|
this.queues = new QueuesResource(this.http);
|
|
2789
2885
|
this.workers = new WorkersResource(this.http);
|
|
@@ -2862,6 +2958,9 @@ var SpooledClient = class _SpooledClient {
|
|
|
2862
2958
|
baseUrl: this.config.baseUrl,
|
|
2863
2959
|
wsUrl: this.config.wsUrl,
|
|
2864
2960
|
token,
|
|
2961
|
+
// Provider so each (re)connect mints a fresh JWT via login/refresh,
|
|
2962
|
+
// rather than reusing the short-lived token captured above.
|
|
2963
|
+
tokenProvider: () => this.getJwtToken(),
|
|
2865
2964
|
...options,
|
|
2866
2965
|
debug: this.config.debug ?? void 0
|
|
2867
2966
|
});
|
|
@@ -3169,9 +3268,11 @@ var SpooledWorker = class {
|
|
|
3169
3268
|
this.waitForActiveJobs(),
|
|
3170
3269
|
this.sleep(this.options.shutdownTimeout)
|
|
3171
3270
|
]);
|
|
3172
|
-
|
|
3271
|
+
const remaining = Array.from(this.activeJobs.entries());
|
|
3272
|
+
for (const [jobId, active] of remaining) {
|
|
3173
3273
|
this.debug(`Force-failing job ${jobId} due to shutdown timeout`);
|
|
3174
3274
|
await this.failJob(active.job, "Worker shutdown timeout");
|
|
3275
|
+
this.cleanupJob(jobId);
|
|
3175
3276
|
}
|
|
3176
3277
|
}
|
|
3177
3278
|
if (this.workerId) {
|