@robosystems/client 1.12.2 → 1.13.0

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.
@@ -1,3 +1,4 @@
1
+ import type { TokenProvider } from './graphql/client';
1
2
  export interface OperationProgress {
2
3
  message: string;
3
4
  progressPercent?: number;
@@ -25,6 +26,7 @@ export declare class OperationClient {
25
26
  credentials?: 'include' | 'same-origin' | 'omit';
26
27
  headers?: Record<string, string>;
27
28
  token?: string;
29
+ tokenProvider?: TokenProvider;
28
30
  maxRetries?: number;
29
31
  retryDelay?: number;
30
32
  });
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { cancelOperation as cancelOperationSDK, getOperationStatus } from '../sdk.gen'
9
+ import type { TokenProvider } from './graphql/client'
9
10
  import { EventType, SSEClient } from './SSEClient'
10
11
 
11
12
  export interface OperationProgress {
@@ -35,6 +36,7 @@ export class OperationClient {
35
36
  credentials?: 'include' | 'same-origin' | 'omit'
36
37
  headers?: Record<string, string>
37
38
  token?: string // JWT token for authentication
39
+ tokenProvider?: TokenProvider // consulted on every SSE connect; wins over `token`
38
40
  maxRetries?: number
39
41
  retryDelay?: number
40
42
  }
@@ -46,6 +48,7 @@ export class OperationClient {
46
48
  credentials?: 'include' | 'same-origin' | 'omit'
47
49
  headers?: Record<string, string>
48
50
  token?: string // JWT token for authentication
51
+ tokenProvider?: TokenProvider // consulted on every SSE connect; wins over `token`
49
52
  maxRetries?: number
50
53
  retryDelay?: number
51
54
  }) {
@@ -1,3 +1,4 @@
1
+ import type { TokenProvider } from './graphql/client';
1
2
  export interface OperatorQueryRequest {
2
3
  message: string;
3
4
  history?: Array<{
@@ -13,6 +14,12 @@ export interface OperatorOptions {
13
14
  mode?: 'auto' | 'sync' | 'async';
14
15
  maxWait?: number;
15
16
  onProgress?: (message: string, percentage?: number) => void;
17
+ /**
18
+ * Interval between `/v1/operations/{id}/status` polls while the client
19
+ * follows a queued run it could not open an SSE stream for. Defaults to
20
+ * 2000 ms; only the fallback path uses it.
21
+ */
22
+ pollIntervalMs?: number;
16
23
  }
17
24
  export interface OperatorResult {
18
25
  content: string;
@@ -27,6 +34,12 @@ export interface OperatorResult {
27
34
  confidence_score?: number;
28
35
  execution_time?: number;
29
36
  timestamp?: string;
37
+ /**
38
+ * Present when the API reports a failed run inside an otherwise successful
39
+ * response (credit pre-flight, operator timeouts, cancelled runs). `content`
40
+ * then carries the explanation rather than an answer.
41
+ */
42
+ error_details?: Record<string, any>;
30
43
  }
31
44
  export interface QueuedOperatorResponse {
32
45
  status: 'queued';
@@ -34,15 +47,25 @@ export interface QueuedOperatorResponse {
34
47
  message: string;
35
48
  sse_endpoint?: string;
36
49
  }
50
+ export interface OperatorClientConfig {
51
+ baseUrl: string;
52
+ credentials?: 'include' | 'same-origin' | 'omit';
53
+ headers?: Record<string, string>;
54
+ /**
55
+ * Static JWT captured at construction. Prefer `tokenProvider` in the
56
+ * browser — see `SSEConfig` for why a captured token goes stale.
57
+ */
58
+ token?: string;
59
+ /**
60
+ * Consulted on every SSE connect so a rotated JWT keeps the progress
61
+ * stream authenticated. Wins over `token` when both are set.
62
+ */
63
+ tokenProvider?: TokenProvider;
64
+ }
37
65
  export declare class OperatorClient {
38
66
  private sseClient?;
39
67
  private config;
40
- constructor(config: {
41
- baseUrl: string;
42
- credentials?: 'include' | 'same-origin' | 'omit';
43
- headers?: Record<string, string>;
44
- token?: string;
45
- });
68
+ constructor(config: OperatorClientConfig);
46
69
  /**
47
70
  * Execute operator query with automatic operator selection
48
71
  */
@@ -51,7 +74,18 @@ export declare class OperatorClient {
51
74
  * Execute specific operator type
52
75
  */
53
76
  executeOperator(graphId: string, operatorType: string, request: OperatorQueryRequest, options?: OperatorOptions): Promise<OperatorResult>;
77
+ /**
78
+ * Resolve an operator endpoint response: a sync 200 body is the result, a
79
+ * 202 with an `operation_id` is followed to completion.
80
+ */
81
+ private settle;
54
82
  private waitForOperatorCompletion;
83
+ /**
84
+ * Follow a queued run over `/v1/operations/{id}/status` until it settles.
85
+ * Used when the SSE stream could not be opened; `streamError` is folded
86
+ * into the failure message if polling cannot reach a verdict either.
87
+ */
88
+ private pollForCompletion;
55
89
  /**
56
90
  * Convenience method for simple agent queries with auto-selection
57
91
  */
@@ -8,6 +8,50 @@ exports.QueuedOperatorError = exports.OperatorClient = void 0;
8
8
  */
9
9
  const sdk_gen_1 = require("../sdk.gen");
10
10
  const SSEClient_1 = require("./SSEClient");
11
+ const DEFAULT_POLL_INTERVAL_MS = 2000;
12
+ // Consecutive `/status` failures tolerated before the polling fallback gives
13
+ // up: one transient network error must not lose a run that is still going.
14
+ const MAX_CONSECUTIVE_POLL_FAILURES = 3;
15
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
16
+ const describeError = (err) => (err instanceof Error ? err.message : String(err));
17
+ /**
18
+ * Text for the `error` slot of a generated-client envelope: a thrown `Error`
19
+ * (network failure — the client returns those rather than throwing), an API
20
+ * body with `detail`, or whatever else came back.
21
+ */
22
+ const describeEnvelopeError = (err) => {
23
+ if (err instanceof Error)
24
+ return err.message;
25
+ if (err && typeof err === 'object' && 'detail' in err)
26
+ return String(err.detail);
27
+ if (err === undefined || err === null)
28
+ return 'empty response';
29
+ return typeof err === 'string' ? err : JSON.stringify(err);
30
+ };
31
+ /**
32
+ * Shape an operator payload — the sync 200 body, the `operation_completed`
33
+ * event, or the `/status` `result` — into the public result type. All three
34
+ * carry the same fields, so one mapper keeps them consistent.
35
+ */
36
+ function toOperatorResult(data) {
37
+ const result = {
38
+ content: data.content ?? '',
39
+ operator_used: data.operator_used || 'unknown',
40
+ mode_used: data.mode_used || 'standard',
41
+ metadata: data.metadata,
42
+ tokens_used: data.tokens_used,
43
+ confidence_score: data.confidence_score,
44
+ execution_time: data.execution_time,
45
+ timestamp: data.timestamp || new Date().toISOString(),
46
+ };
47
+ if (data.error_details) {
48
+ result.error_details = data.error_details;
49
+ }
50
+ return result;
51
+ }
52
+ /** Internal: a `/status` verdict that must not be retried. */
53
+ class PollAbort extends Error {
54
+ }
11
55
  class OperatorClient {
12
56
  constructor(config) {
13
57
  this.config = config;
@@ -29,32 +73,7 @@ class OperatorClient {
29
73
  },
30
74
  };
31
75
  const response = await (0, sdk_gen_1.autoSelectOperator)(data);
32
- const responseData = response.data;
33
- // Check if this is an immediate response (sync execution)
34
- if (responseData?.content !== undefined && responseData?.operator_used) {
35
- return {
36
- content: responseData.content,
37
- operator_used: responseData.operator_used,
38
- mode_used: responseData.mode_used,
39
- metadata: responseData.metadata,
40
- tokens_used: responseData.tokens_used,
41
- confidence_score: responseData.confidence_score,
42
- execution_time: responseData.execution_time,
43
- timestamp: new Date().toISOString(),
44
- };
45
- }
46
- // Check if this is a queued response (async background task execution)
47
- if (responseData?.operation_id) {
48
- const queuedResponse = responseData;
49
- // If user doesn't want to wait, throw with queue info
50
- if (options.maxWait === 0) {
51
- throw new QueuedOperatorError(queuedResponse);
52
- }
53
- // Use SSE to monitor the operation
54
- return this.waitForOperatorCompletion(queuedResponse.operation_id, options);
55
- }
56
- // Unexpected response format
57
- throw new Error('Unexpected response format from operator endpoint');
76
+ return this.settle(response.data, options);
58
77
  }
59
78
  /**
60
79
  * Execute specific operator type
@@ -73,103 +92,150 @@ class OperatorClient {
73
92
  },
74
93
  };
75
94
  const response = await (0, sdk_gen_1.executeSpecificOperator)(data);
76
- const responseData = response.data;
77
- // Check if this is an immediate response (sync execution)
95
+ return this.settle(response.data, options);
96
+ }
97
+ /**
98
+ * Resolve an operator endpoint response: a sync 200 body is the result, a
99
+ * 202 with an `operation_id` is followed to completion.
100
+ */
101
+ async settle(responseData, options) {
102
+ // Immediate response (sync execution)
78
103
  if (responseData?.content !== undefined && responseData?.operator_used) {
79
- return {
80
- content: responseData.content,
81
- operator_used: responseData.operator_used,
82
- mode_used: responseData.mode_used,
83
- metadata: responseData.metadata,
84
- tokens_used: responseData.tokens_used,
85
- confidence_score: responseData.confidence_score,
86
- execution_time: responseData.execution_time,
87
- timestamp: new Date().toISOString(),
88
- };
104
+ return toOperatorResult(responseData);
89
105
  }
90
- // Check if this is a queued response (async background task execution)
106
+ // Queued response (async background task execution)
91
107
  if (responseData?.operation_id) {
92
108
  const queuedResponse = responseData;
93
109
  // If user doesn't want to wait, throw with queue info
94
110
  if (options.maxWait === 0) {
95
111
  throw new QueuedOperatorError(queuedResponse);
96
112
  }
97
- // Use SSE to monitor the operation
98
113
  return this.waitForOperatorCompletion(queuedResponse.operation_id, options);
99
114
  }
100
115
  // Unexpected response format
101
116
  throw new Error('Unexpected response format from operator endpoint');
102
117
  }
103
118
  async waitForOperatorCompletion(operationId, options) {
119
+ const sseClient = new SSEClient_1.SSEClient(this.config);
120
+ this.sseClient = sseClient;
121
+ try {
122
+ await sseClient.connect(operationId);
123
+ }
124
+ catch (streamError) {
125
+ // The run is already queued and finishes whether or not anyone is
126
+ // watching, so a stream that cannot open — a revoked token, a proxy
127
+ // that will not hold the connection, the per-user connection cap —
128
+ // must not lose the result. Follow the operation over `/status`,
129
+ // which rides the regular REST auth path, instead.
130
+ this.sseClient = undefined;
131
+ return this.pollForCompletion(operationId, options, streamError);
132
+ }
104
133
  return new Promise((resolve, reject) => {
105
- const sseClient = new SSEClient_1.SSEClient(this.config);
106
- sseClient
107
- .connect(operationId)
108
- .then(() => {
109
- let result = null;
110
- // Listen for progress events
111
- sseClient.on(SSEClient_1.EventType.OPERATION_PROGRESS, (data) => {
112
- options.onProgress?.(data.message, data.progress_percentage);
113
- });
114
- // Listen for agent-specific events
115
- sseClient.on('operator_started', (data) => {
116
- options.onProgress?.(`Operator ${data.operator_type} started`, 0);
117
- });
118
- sseClient.on('operator_initialized', (data) => {
119
- options.onProgress?.(`${data.agent_name} initialized`, 10);
120
- });
121
- sseClient.on('progress', (data) => {
122
- options.onProgress?.(data.message, data.percentage);
123
- });
124
- sseClient.on('operator_completed', (data) => {
125
- result = {
126
- content: data.content,
127
- operator_used: data.operator_used,
128
- mode_used: data.mode_used,
129
- metadata: data.metadata,
130
- tokens_used: data.tokens_used,
131
- confidence_score: data.confidence_score,
132
- execution_time: data.execution_time,
133
- timestamp: data.timestamp || new Date().toISOString(),
134
- };
135
- sseClient.close();
134
+ let result = null;
135
+ const finish = () => {
136
+ sseClient.close();
137
+ if (this.sseClient === sseClient) {
138
+ this.sseClient = undefined;
139
+ }
140
+ };
141
+ // Listen for progress events
142
+ sseClient.on(SSEClient_1.EventType.OPERATION_PROGRESS, (data) => {
143
+ options.onProgress?.(data.message, data.progress_percentage);
144
+ });
145
+ // Listen for agent-specific events
146
+ sseClient.on('operator_started', (data) => {
147
+ options.onProgress?.(`Operator ${data.operator_type} started`, 0);
148
+ });
149
+ sseClient.on('operator_initialized', (data) => {
150
+ options.onProgress?.(`${data.agent_name} initialized`, 10);
151
+ });
152
+ sseClient.on('progress', (data) => {
153
+ options.onProgress?.(data.message, data.percentage);
154
+ });
155
+ sseClient.on('operator_completed', (data) => {
156
+ result = toOperatorResult(data);
157
+ finish();
158
+ resolve(result);
159
+ });
160
+ // Fallback to generic completion event
161
+ sseClient.on(SSEClient_1.EventType.OPERATION_COMPLETED, (data) => {
162
+ if (!result) {
163
+ result = toOperatorResult(data.result || data);
164
+ finish();
136
165
  resolve(result);
137
- });
138
- // Fallback to generic completion event
139
- sseClient.on(SSEClient_1.EventType.OPERATION_COMPLETED, (data) => {
140
- if (!result) {
141
- const operatorResult = data.result || data;
142
- result = {
143
- content: operatorResult.content || '',
144
- operator_used: operatorResult.operator_used || 'unknown',
145
- mode_used: operatorResult.mode_used || 'standard',
146
- metadata: operatorResult.metadata,
147
- tokens_used: operatorResult.tokens_used,
148
- confidence_score: operatorResult.confidence_score,
149
- execution_time: operatorResult.execution_time,
150
- timestamp: operatorResult.timestamp || new Date().toISOString(),
151
- };
152
- sseClient.close();
153
- resolve(result);
154
- }
155
- });
156
- sseClient.on(SSEClient_1.EventType.OPERATION_ERROR, (error) => {
157
- sseClient.close();
158
- reject(new Error(error.message || error.error));
159
- });
160
- sseClient.on(SSEClient_1.EventType.OPERATION_CANCELLED, () => {
161
- sseClient.close();
162
- reject(new Error('Agent execution cancelled'));
163
- });
164
- // Handle generic error event
165
- sseClient.on('error', (error) => {
166
- sseClient.close();
167
- reject(new Error(error.error || error.message || 'Agent execution failed'));
168
- });
169
- })
170
- .catch(reject);
166
+ }
167
+ });
168
+ sseClient.on(SSEClient_1.EventType.OPERATION_ERROR, (error) => {
169
+ finish();
170
+ reject(new Error(error.message || error.error));
171
+ });
172
+ sseClient.on(SSEClient_1.EventType.OPERATION_CANCELLED, () => {
173
+ finish();
174
+ reject(new Error('Agent execution cancelled'));
175
+ });
176
+ // Handle generic error event
177
+ sseClient.on('error', (error) => {
178
+ finish();
179
+ reject(new Error(error.error || error.message || 'Agent execution failed'));
180
+ });
171
181
  });
172
182
  }
183
+ /**
184
+ * Follow a queued run over `/v1/operations/{id}/status` until it settles.
185
+ * Used when the SSE stream could not be opened; `streamError` is folded
186
+ * into the failure message if polling cannot reach a verdict either.
187
+ */
188
+ async pollForCompletion(operationId, options, streamError) {
189
+ const interval = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
190
+ const streamDetail = describeError(streamError);
191
+ let consecutiveFailures = 0;
192
+ options.onProgress?.('Live progress unavailable — waiting for the result');
193
+ for (;;) {
194
+ let status;
195
+ try {
196
+ const response = await (0, sdk_gen_1.getOperationStatus)({ path: { operation_id: operationId } });
197
+ if (response.error || !response.data) {
198
+ const httpStatus = response.response?.status;
199
+ const detail = describeEnvelopeError(response.error);
200
+ // A definitive 4xx (expired, not ours, unauthenticated) ends the
201
+ // wait; anything else is treated as transient and retried below.
202
+ if (httpStatus !== undefined &&
203
+ httpStatus >= 400 &&
204
+ httpStatus < 500 &&
205
+ httpStatus !== 429) {
206
+ throw new PollAbort(`Operator stream failed (${streamDetail}); status check failed (${httpStatus}: ${detail})`);
207
+ }
208
+ throw new Error(detail);
209
+ }
210
+ status = response.data;
211
+ consecutiveFailures = 0;
212
+ }
213
+ catch (pollError) {
214
+ if (pollError instanceof PollAbort) {
215
+ throw new Error(pollError.message);
216
+ }
217
+ consecutiveFailures += 1;
218
+ if (consecutiveFailures >= MAX_CONSECUTIVE_POLL_FAILURES) {
219
+ throw new Error(`Operator stream failed (${streamDetail}); status polling failed (${describeError(pollError)})`);
220
+ }
221
+ await sleep(interval);
222
+ continue;
223
+ }
224
+ switch (status.status) {
225
+ case 'completed':
226
+ return toOperatorResult(status.result || {});
227
+ case 'failed':
228
+ throw new Error(status.error || status.message || 'Operator run failed');
229
+ case 'cancelled':
230
+ throw new Error('Agent execution cancelled');
231
+ default:
232
+ if (status.message) {
233
+ options.onProgress?.(status.message);
234
+ }
235
+ await sleep(interval);
236
+ }
237
+ }
238
+ }
173
239
  /**
174
240
  * Convenience method for simple agent queries with auto-selection
175
241
  */
@@ -5,8 +5,9 @@
5
5
  * Provides intelligent operator execution with automatic strategy selection
6
6
  */
7
7
 
8
- import { autoSelectOperator, executeSpecificOperator } from '../sdk.gen'
8
+ import { autoSelectOperator, executeSpecificOperator, getOperationStatus } from '../sdk.gen'
9
9
  import type { AutoSelectOperatorData, ExecuteSpecificOperatorData } from '../types.gen'
10
+ import type { TokenProvider } from './graphql/client'
10
11
  import { EventType, SSEClient } from './SSEClient'
11
12
 
12
13
  export interface OperatorQueryRequest {
@@ -22,6 +23,12 @@ export interface OperatorOptions {
22
23
  mode?: 'auto' | 'sync' | 'async'
23
24
  maxWait?: number
24
25
  onProgress?: (message: string, percentage?: number) => void
26
+ /**
27
+ * Interval between `/v1/operations/{id}/status` polls while the client
28
+ * follows a queued run it could not open an SSE stream for. Defaults to
29
+ * 2000 ms; only the fallback path uses it.
30
+ */
31
+ pollIntervalMs?: number
25
32
  }
26
33
 
27
34
  export interface OperatorResult {
@@ -37,6 +44,12 @@ export interface OperatorResult {
37
44
  confidence_score?: number
38
45
  execution_time?: number
39
46
  timestamp?: string
47
+ /**
48
+ * Present when the API reports a failed run inside an otherwise successful
49
+ * response (credit pre-flight, operator timeouts, cancelled runs). `content`
50
+ * then carries the explanation rather than an answer.
51
+ */
52
+ error_details?: Record<string, any>
40
53
  }
41
54
 
42
55
  export interface QueuedOperatorResponse {
@@ -46,21 +59,74 @@ export interface QueuedOperatorResponse {
46
59
  sse_endpoint?: string
47
60
  }
48
61
 
62
+ export interface OperatorClientConfig {
63
+ baseUrl: string
64
+ credentials?: 'include' | 'same-origin' | 'omit'
65
+ headers?: Record<string, string>
66
+ /**
67
+ * Static JWT captured at construction. Prefer `tokenProvider` in the
68
+ * browser — see `SSEConfig` for why a captured token goes stale.
69
+ */
70
+ token?: string
71
+ /**
72
+ * Consulted on every SSE connect so a rotated JWT keeps the progress
73
+ * stream authenticated. Wins over `token` when both are set.
74
+ */
75
+ tokenProvider?: TokenProvider
76
+ }
77
+
78
+ const DEFAULT_POLL_INTERVAL_MS = 2000
79
+
80
+ // Consecutive `/status` failures tolerated before the polling fallback gives
81
+ // up: one transient network error must not lose a run that is still going.
82
+ const MAX_CONSECUTIVE_POLL_FAILURES = 3
83
+
84
+ const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
85
+
86
+ const describeError = (err: unknown): string => (err instanceof Error ? err.message : String(err))
87
+
88
+ /**
89
+ * Text for the `error` slot of a generated-client envelope: a thrown `Error`
90
+ * (network failure — the client returns those rather than throwing), an API
91
+ * body with `detail`, or whatever else came back.
92
+ */
93
+ const describeEnvelopeError = (err: unknown): string => {
94
+ if (err instanceof Error) return err.message
95
+ if (err && typeof err === 'object' && 'detail' in err) return String((err as any).detail)
96
+ if (err === undefined || err === null) return 'empty response'
97
+ return typeof err === 'string' ? err : JSON.stringify(err)
98
+ }
99
+
100
+ /**
101
+ * Shape an operator payload — the sync 200 body, the `operation_completed`
102
+ * event, or the `/status` `result` — into the public result type. All three
103
+ * carry the same fields, so one mapper keeps them consistent.
104
+ */
105
+ function toOperatorResult(data: Record<string, any>): OperatorResult {
106
+ const result: OperatorResult = {
107
+ content: data.content ?? '',
108
+ operator_used: data.operator_used || 'unknown',
109
+ mode_used: data.mode_used || 'standard',
110
+ metadata: data.metadata,
111
+ tokens_used: data.tokens_used,
112
+ confidence_score: data.confidence_score,
113
+ execution_time: data.execution_time,
114
+ timestamp: data.timestamp || new Date().toISOString(),
115
+ }
116
+ if (data.error_details) {
117
+ result.error_details = data.error_details
118
+ }
119
+ return result
120
+ }
121
+
122
+ /** Internal: a `/status` verdict that must not be retried. */
123
+ class PollAbort extends Error {}
124
+
49
125
  export class OperatorClient {
50
126
  private sseClient?: SSEClient
51
- private config: {
52
- baseUrl: string
53
- credentials?: 'include' | 'same-origin' | 'omit'
54
- headers?: Record<string, string>
55
- token?: string
56
- }
127
+ private config: OperatorClientConfig
57
128
 
58
- constructor(config: {
59
- baseUrl: string
60
- credentials?: 'include' | 'same-origin' | 'omit'
61
- headers?: Record<string, string>
62
- token?: string
63
- }) {
129
+ constructor(config: OperatorClientConfig) {
64
130
  this.config = config
65
131
  }
66
132
 
@@ -86,37 +152,7 @@ export class OperatorClient {
86
152
  }
87
153
 
88
154
  const response = await autoSelectOperator(data)
89
- const responseData = response.data as any
90
-
91
- // Check if this is an immediate response (sync execution)
92
- if (responseData?.content !== undefined && responseData?.operator_used) {
93
- return {
94
- content: responseData.content,
95
- operator_used: responseData.operator_used,
96
- mode_used: responseData.mode_used,
97
- metadata: responseData.metadata,
98
- tokens_used: responseData.tokens_used,
99
- confidence_score: responseData.confidence_score,
100
- execution_time: responseData.execution_time,
101
- timestamp: new Date().toISOString(),
102
- }
103
- }
104
-
105
- // Check if this is a queued response (async background task execution)
106
- if (responseData?.operation_id) {
107
- const queuedResponse = responseData as QueuedOperatorResponse
108
-
109
- // If user doesn't want to wait, throw with queue info
110
- if (options.maxWait === 0) {
111
- throw new QueuedOperatorError(queuedResponse)
112
- }
113
-
114
- // Use SSE to monitor the operation
115
- return this.waitForOperatorCompletion(queuedResponse.operation_id, options)
116
- }
117
-
118
- // Unexpected response format
119
- throw new Error('Unexpected response format from operator endpoint')
155
+ return this.settle(response.data, options)
120
156
  }
121
157
 
122
158
  /**
@@ -142,23 +178,20 @@ export class OperatorClient {
142
178
  }
143
179
 
144
180
  const response = await executeSpecificOperator(data)
145
- const responseData = response.data as any
181
+ return this.settle(response.data, options)
182
+ }
146
183
 
147
- // Check if this is an immediate response (sync execution)
184
+ /**
185
+ * Resolve an operator endpoint response: a sync 200 body is the result, a
186
+ * 202 with an `operation_id` is followed to completion.
187
+ */
188
+ private async settle(responseData: any, options: OperatorOptions): Promise<OperatorResult> {
189
+ // Immediate response (sync execution)
148
190
  if (responseData?.content !== undefined && responseData?.operator_used) {
149
- return {
150
- content: responseData.content,
151
- operator_used: responseData.operator_used,
152
- mode_used: responseData.mode_used,
153
- metadata: responseData.metadata,
154
- tokens_used: responseData.tokens_used,
155
- confidence_score: responseData.confidence_score,
156
- execution_time: responseData.execution_time,
157
- timestamp: new Date().toISOString(),
158
- }
191
+ return toOperatorResult(responseData)
159
192
  }
160
193
 
161
- // Check if this is a queued response (async background task execution)
194
+ // Queued response (async background task execution)
162
195
  if (responseData?.operation_id) {
163
196
  const queuedResponse = responseData as QueuedOperatorResponse
164
197
 
@@ -167,7 +200,6 @@ export class OperatorClient {
167
200
  throw new QueuedOperatorError(queuedResponse)
168
201
  }
169
202
 
170
- // Use SSE to monitor the operation
171
203
  return this.waitForOperatorCompletion(queuedResponse.operation_id, options)
172
204
  }
173
205
 
@@ -179,86 +211,151 @@ export class OperatorClient {
179
211
  operationId: string,
180
212
  options: OperatorOptions
181
213
  ): Promise<OperatorResult> {
214
+ const sseClient = new SSEClient(this.config)
215
+ this.sseClient = sseClient
216
+
217
+ try {
218
+ await sseClient.connect(operationId)
219
+ } catch (streamError) {
220
+ // The run is already queued and finishes whether or not anyone is
221
+ // watching, so a stream that cannot open — a revoked token, a proxy
222
+ // that will not hold the connection, the per-user connection cap —
223
+ // must not lose the result. Follow the operation over `/status`,
224
+ // which rides the regular REST auth path, instead.
225
+ this.sseClient = undefined
226
+ return this.pollForCompletion(operationId, options, streamError)
227
+ }
228
+
182
229
  return new Promise((resolve, reject) => {
183
- const sseClient = new SSEClient(this.config)
184
-
185
- sseClient
186
- .connect(operationId)
187
- .then(() => {
188
- let result: OperatorResult | null = null
189
-
190
- // Listen for progress events
191
- sseClient.on(EventType.OPERATION_PROGRESS, (data) => {
192
- options.onProgress?.(data.message, data.progress_percentage)
193
- })
194
-
195
- // Listen for agent-specific events
196
- sseClient.on('operator_started' as EventType, (data) => {
197
- options.onProgress?.(`Operator ${data.operator_type} started`, 0)
198
- })
199
-
200
- sseClient.on('operator_initialized' as EventType, (data) => {
201
- options.onProgress?.(`${data.agent_name} initialized`, 10)
202
- })
203
-
204
- sseClient.on('progress' as EventType, (data) => {
205
- options.onProgress?.(data.message, data.percentage)
206
- })
207
-
208
- sseClient.on('operator_completed' as EventType, (data) => {
209
- result = {
210
- content: data.content,
211
- operator_used: data.operator_used,
212
- mode_used: data.mode_used,
213
- metadata: data.metadata,
214
- tokens_used: data.tokens_used,
215
- confidence_score: data.confidence_score,
216
- execution_time: data.execution_time,
217
- timestamp: data.timestamp || new Date().toISOString(),
218
- }
219
- sseClient.close()
220
- resolve(result)
221
- })
222
-
223
- // Fallback to generic completion event
224
- sseClient.on(EventType.OPERATION_COMPLETED, (data) => {
225
- if (!result) {
226
- const operatorResult = data.result || data
227
- result = {
228
- content: operatorResult.content || '',
229
- operator_used: operatorResult.operator_used || 'unknown',
230
- mode_used: operatorResult.mode_used || 'standard',
231
- metadata: operatorResult.metadata,
232
- tokens_used: operatorResult.tokens_used,
233
- confidence_score: operatorResult.confidence_score,
234
- execution_time: operatorResult.execution_time,
235
- timestamp: operatorResult.timestamp || new Date().toISOString(),
236
- }
237
- sseClient.close()
238
- resolve(result)
239
- }
240
- })
241
-
242
- sseClient.on(EventType.OPERATION_ERROR, (error) => {
243
- sseClient.close()
244
- reject(new Error(error.message || error.error))
245
- })
246
-
247
- sseClient.on(EventType.OPERATION_CANCELLED, () => {
248
- sseClient.close()
249
- reject(new Error('Agent execution cancelled'))
250
- })
251
-
252
- // Handle generic error event
253
- sseClient.on('error' as EventType, (error) => {
254
- sseClient.close()
255
- reject(new Error(error.error || error.message || 'Agent execution failed'))
256
- })
257
- })
258
- .catch(reject)
230
+ let result: OperatorResult | null = null
231
+
232
+ const finish = () => {
233
+ sseClient.close()
234
+ if (this.sseClient === sseClient) {
235
+ this.sseClient = undefined
236
+ }
237
+ }
238
+
239
+ // Listen for progress events
240
+ sseClient.on(EventType.OPERATION_PROGRESS, (data) => {
241
+ options.onProgress?.(data.message, data.progress_percentage)
242
+ })
243
+
244
+ // Listen for agent-specific events
245
+ sseClient.on('operator_started' as EventType, (data) => {
246
+ options.onProgress?.(`Operator ${data.operator_type} started`, 0)
247
+ })
248
+
249
+ sseClient.on('operator_initialized' as EventType, (data) => {
250
+ options.onProgress?.(`${data.agent_name} initialized`, 10)
251
+ })
252
+
253
+ sseClient.on('progress' as EventType, (data) => {
254
+ options.onProgress?.(data.message, data.percentage)
255
+ })
256
+
257
+ sseClient.on('operator_completed' as EventType, (data) => {
258
+ result = toOperatorResult(data)
259
+ finish()
260
+ resolve(result)
261
+ })
262
+
263
+ // Fallback to generic completion event
264
+ sseClient.on(EventType.OPERATION_COMPLETED, (data) => {
265
+ if (!result) {
266
+ result = toOperatorResult(data.result || data)
267
+ finish()
268
+ resolve(result)
269
+ }
270
+ })
271
+
272
+ sseClient.on(EventType.OPERATION_ERROR, (error) => {
273
+ finish()
274
+ reject(new Error(error.message || error.error))
275
+ })
276
+
277
+ sseClient.on(EventType.OPERATION_CANCELLED, () => {
278
+ finish()
279
+ reject(new Error('Agent execution cancelled'))
280
+ })
281
+
282
+ // Handle generic error event
283
+ sseClient.on('error' as EventType, (error) => {
284
+ finish()
285
+ reject(new Error(error.error || error.message || 'Agent execution failed'))
286
+ })
259
287
  })
260
288
  }
261
289
 
290
+ /**
291
+ * Follow a queued run over `/v1/operations/{id}/status` until it settles.
292
+ * Used when the SSE stream could not be opened; `streamError` is folded
293
+ * into the failure message if polling cannot reach a verdict either.
294
+ */
295
+ private async pollForCompletion(
296
+ operationId: string,
297
+ options: OperatorOptions,
298
+ streamError: unknown
299
+ ): Promise<OperatorResult> {
300
+ const interval = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS
301
+ const streamDetail = describeError(streamError)
302
+ let consecutiveFailures = 0
303
+
304
+ options.onProgress?.('Live progress unavailable — waiting for the result')
305
+
306
+ for (;;) {
307
+ let status: any
308
+ try {
309
+ const response = await getOperationStatus({ path: { operation_id: operationId } })
310
+ if (response.error || !response.data) {
311
+ const httpStatus = response.response?.status
312
+ const detail = describeEnvelopeError(response.error)
313
+ // A definitive 4xx (expired, not ours, unauthenticated) ends the
314
+ // wait; anything else is treated as transient and retried below.
315
+ if (
316
+ httpStatus !== undefined &&
317
+ httpStatus >= 400 &&
318
+ httpStatus < 500 &&
319
+ httpStatus !== 429
320
+ ) {
321
+ throw new PollAbort(
322
+ `Operator stream failed (${streamDetail}); status check failed (${httpStatus}: ${detail})`
323
+ )
324
+ }
325
+ throw new Error(detail)
326
+ }
327
+ status = response.data
328
+ consecutiveFailures = 0
329
+ } catch (pollError) {
330
+ if (pollError instanceof PollAbort) {
331
+ throw new Error(pollError.message)
332
+ }
333
+ consecutiveFailures += 1
334
+ if (consecutiveFailures >= MAX_CONSECUTIVE_POLL_FAILURES) {
335
+ throw new Error(
336
+ `Operator stream failed (${streamDetail}); status polling failed (${describeError(pollError)})`
337
+ )
338
+ }
339
+ await sleep(interval)
340
+ continue
341
+ }
342
+
343
+ switch (status.status) {
344
+ case 'completed':
345
+ return toOperatorResult(status.result || {})
346
+ case 'failed':
347
+ throw new Error(status.error || status.message || 'Operator run failed')
348
+ case 'cancelled':
349
+ throw new Error('Agent execution cancelled')
350
+ default:
351
+ if (status.message) {
352
+ options.onProgress?.(status.message)
353
+ }
354
+ await sleep(interval)
355
+ }
356
+ }
357
+ }
358
+
262
359
  /**
263
360
  * Convenience method for simple agent queries with auto-selection
264
361
  */
@@ -1,3 +1,4 @@
1
+ import type { TokenProvider } from './graphql/client';
1
2
  export interface QueryRequest {
2
3
  query: string;
3
4
  parameters?: Record<string, any>;
@@ -34,6 +35,7 @@ export declare class QueryClient {
34
35
  credentials?: 'include' | 'same-origin' | 'omit';
35
36
  headers?: Record<string, string>;
36
37
  token?: string;
38
+ tokenProvider?: TokenProvider;
37
39
  });
38
40
  executeQuery(graphId: string, request: QueryRequest, options?: QueryOptions): Promise<QueryResult | AsyncIterableIterator<any>>;
39
41
  private parseNDJSONResponse;
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { executeCypher } from '../sdk.gen'
9
9
  import type { ExecuteCypherData } from '../types.gen'
10
+ import type { TokenProvider } from './graphql/client'
10
11
  import { EventType, SSEClient } from './SSEClient'
11
12
 
12
13
  export interface QueryRequest {
@@ -48,6 +49,7 @@ export class QueryClient {
48
49
  credentials?: 'include' | 'same-origin' | 'omit'
49
50
  headers?: Record<string, string>
50
51
  token?: string // JWT token for authentication
52
+ tokenProvider?: TokenProvider // consulted on every SSE connect; wins over `token`
51
53
  }
52
54
 
53
55
  constructor(config: {
@@ -55,6 +57,7 @@ export class QueryClient {
55
57
  credentials?: 'include' | 'same-origin' | 'omit'
56
58
  headers?: Record<string, string>
57
59
  token?: string // JWT token for authentication
60
+ tokenProvider?: TokenProvider // consulted on every SSE connect; wins over `token`
58
61
  }) {
59
62
  this.config = config
60
63
  }
@@ -14,12 +14,30 @@
14
14
  * - Implementing a WebSocket-based alternative
15
15
  * - Using short-lived tokens that expire quickly
16
16
  * - Ensuring all connections use HTTPS
17
+ *
18
+ * CREDENTIAL ROTATION: the stream endpoint authenticates the JWT it finds in
19
+ * the URL, and the backend revokes the previous JWT on every session refresh.
20
+ * A `token` captured once therefore goes dead the moment the session rotates
21
+ * (about every 25 minutes in the browser apps); pass a `tokenProvider` and it
22
+ * is consulted on every connect — including automatic reconnects — instead.
17
23
  */
24
+ import type { TokenProvider } from './graphql/client';
18
25
  export interface SSEConfig {
19
26
  baseUrl: string;
20
27
  credentials?: 'include' | 'same-origin' | 'omit';
21
28
  headers?: Record<string, string>;
29
+ /**
30
+ * Static JWT captured at construction. Fine for long-lived API keys and
31
+ * server-side flows; browser sessions should use `tokenProvider`.
32
+ */
22
33
  token?: string;
34
+ /**
35
+ * Dynamic credential callback, consulted on every `connect()` and
36
+ * preferred over `token` when both are set. Return `null` to connect
37
+ * without a token. See the class documentation for why a static token
38
+ * is not enough in the browser.
39
+ */
40
+ tokenProvider?: TokenProvider;
23
41
  maxRetries?: number;
24
42
  retryDelay?: number;
25
43
  heartbeatInterval?: number;
@@ -51,6 +69,14 @@ export declare class SSEClient {
51
69
  private listeners;
52
70
  constructor(config: SSEConfig);
53
71
  connect(operationId: string, fromSequence?: number): Promise<void>;
72
+ /**
73
+ * Credential for one connect attempt: the `tokenProvider` result when one
74
+ * is configured (`null`/`undefined` connects unauthenticated), otherwise
75
+ * the static `token`. A throwing provider fails the connect — matching the
76
+ * GraphQL client — because the caller intended to authenticate, and a
77
+ * silent unauthenticated attempt would surface as an unrelated-looking 401.
78
+ */
79
+ private resolveToken;
54
80
  private handleMessage;
55
81
  private handleTypedEvent;
56
82
  private handleError;
@@ -27,13 +27,16 @@ class SSEClient {
27
27
  };
28
28
  }
29
29
  async connect(operationId, fromSequence = 0) {
30
+ // Resolved per attempt so a rotated JWT is picked up by every connect,
31
+ // not just the first one after construction.
32
+ const token = await this.resolveToken();
30
33
  return new Promise((resolve, reject) => {
31
34
  let url = `${this.config.baseUrl}/v1/operations/${operationId}/stream?from_sequence=${fromSequence}`;
32
35
  // Add JWT token as query parameter if provided
33
36
  // WARNING: EventSource API doesn't support custom headers, so tokens are passed via query param
34
37
  // This has security implications - see class documentation
35
- if (this.config.token) {
36
- url += `&token=${encodeURIComponent(this.config.token)}`;
38
+ if (token) {
39
+ url += `&token=${encodeURIComponent(token)}`;
37
40
  }
38
41
  this.eventSource = new EventSource(url, {
39
42
  withCredentials: this.config.credentials === 'include',
@@ -83,6 +86,29 @@ class SSEClient {
83
86
  });
84
87
  });
85
88
  }
89
+ /**
90
+ * Credential for one connect attempt: the `tokenProvider` result when one
91
+ * is configured (`null`/`undefined` connects unauthenticated), otherwise
92
+ * the static `token`. A throwing provider fails the connect — matching the
93
+ * GraphQL client — because the caller intended to authenticate, and a
94
+ * silent unauthenticated attempt would surface as an unrelated-looking 401.
95
+ */
96
+ async resolveToken() {
97
+ if (!this.config.tokenProvider) {
98
+ return this.config.token;
99
+ }
100
+ let token;
101
+ try {
102
+ token = await this.config.tokenProvider();
103
+ }
104
+ catch (err) {
105
+ const detail = err instanceof Error ? err.message : String(err);
106
+ throw new Error(`RoboSystems SDK: tokenProvider threw while resolving the SSE credential (${detail}). ` +
107
+ 'Fix the tokenProvider passed in the client config (or via setSDKClientConfig) so it ' +
108
+ 'returns the current token, or null to connect without one.');
109
+ }
110
+ return token || undefined;
111
+ }
86
112
  handleMessage(event) {
87
113
  try {
88
114
  const data = JSON.parse(event.data);
@@ -16,13 +16,32 @@
16
16
  * - Implementing a WebSocket-based alternative
17
17
  * - Using short-lived tokens that expire quickly
18
18
  * - Ensuring all connections use HTTPS
19
+ *
20
+ * CREDENTIAL ROTATION: the stream endpoint authenticates the JWT it finds in
21
+ * the URL, and the backend revokes the previous JWT on every session refresh.
22
+ * A `token` captured once therefore goes dead the moment the session rotates
23
+ * (about every 25 minutes in the browser apps); pass a `tokenProvider` and it
24
+ * is consulted on every connect — including automatic reconnects — instead.
19
25
  */
20
26
 
27
+ import type { TokenProvider } from './graphql/client'
28
+
21
29
  export interface SSEConfig {
22
30
  baseUrl: string
23
31
  credentials?: 'include' | 'same-origin' | 'omit'
24
32
  headers?: Record<string, string>
25
- token?: string // JWT token for authentication
33
+ /**
34
+ * Static JWT captured at construction. Fine for long-lived API keys and
35
+ * server-side flows; browser sessions should use `tokenProvider`.
36
+ */
37
+ token?: string
38
+ /**
39
+ * Dynamic credential callback, consulted on every `connect()` and
40
+ * preferred over `token` when both are set. Return `null` to connect
41
+ * without a token. See the class documentation for why a static token
42
+ * is not enough in the browser.
43
+ */
44
+ tokenProvider?: TokenProvider
26
45
  maxRetries?: number
27
46
  retryDelay?: number
28
47
  heartbeatInterval?: number
@@ -66,14 +85,18 @@ export class SSEClient {
66
85
  }
67
86
 
68
87
  async connect(operationId: string, fromSequence: number = 0): Promise<void> {
88
+ // Resolved per attempt so a rotated JWT is picked up by every connect,
89
+ // not just the first one after construction.
90
+ const token = await this.resolveToken()
91
+
69
92
  return new Promise((resolve, reject) => {
70
93
  let url = `${this.config.baseUrl}/v1/operations/${operationId}/stream?from_sequence=${fromSequence}`
71
94
 
72
95
  // Add JWT token as query parameter if provided
73
96
  // WARNING: EventSource API doesn't support custom headers, so tokens are passed via query param
74
97
  // This has security implications - see class documentation
75
- if (this.config.token) {
76
- url += `&token=${encodeURIComponent(this.config.token)}`
98
+ if (token) {
99
+ url += `&token=${encodeURIComponent(token)}`
77
100
  }
78
101
 
79
102
  this.eventSource = new EventSource(url, {
@@ -135,6 +158,31 @@ export class SSEClient {
135
158
  })
136
159
  }
137
160
 
161
+ /**
162
+ * Credential for one connect attempt: the `tokenProvider` result when one
163
+ * is configured (`null`/`undefined` connects unauthenticated), otherwise
164
+ * the static `token`. A throwing provider fails the connect — matching the
165
+ * GraphQL client — because the caller intended to authenticate, and a
166
+ * silent unauthenticated attempt would surface as an unrelated-looking 401.
167
+ */
168
+ private async resolveToken(): Promise<string | undefined> {
169
+ if (!this.config.tokenProvider) {
170
+ return this.config.token
171
+ }
172
+ let token: string | null | undefined
173
+ try {
174
+ token = await this.config.tokenProvider()
175
+ } catch (err) {
176
+ const detail = err instanceof Error ? err.message : String(err)
177
+ throw new Error(
178
+ `RoboSystems SDK: tokenProvider threw while resolving the SSE credential (${detail}). ` +
179
+ 'Fix the tokenProvider passed in the client config (or via setSDKClientConfig) so it ' +
180
+ 'returns the current token, or null to connect without one.'
181
+ )
182
+ }
183
+ return token || undefined
184
+ }
185
+
138
186
  private handleMessage(event: MessageEvent): void {
139
187
  try {
140
188
  const data = JSON.parse(event.data)
@@ -45,6 +45,7 @@ function useQuery(graphId) {
45
45
  credentials: sdkConfig.credentials,
46
46
  headers: sdkConfig.headers,
47
47
  token,
48
+ tokenProvider: sdkConfig.tokenProvider,
48
49
  });
49
50
  return () => {
50
51
  clientRef.current?.close();
@@ -122,6 +123,7 @@ function useStreamingQuery(graphId) {
122
123
  credentials: sdkConfig.credentials,
123
124
  headers: sdkConfig.headers,
124
125
  token: sdkConfig.token,
126
+ tokenProvider: sdkConfig.tokenProvider,
125
127
  });
126
128
  return () => {
127
129
  clientRef.current?.close();
@@ -201,6 +203,7 @@ function useOperation(operationId) {
201
203
  baseUrl: sdkConfig.baseUrl || clientConfig.baseUrl || 'http://localhost:8000',
202
204
  credentials: sdkConfig.credentials,
203
205
  token,
206
+ tokenProvider: sdkConfig.tokenProvider,
204
207
  maxRetries: sdkConfig.maxRetries,
205
208
  retryDelay: sdkConfig.retryDelay,
206
209
  });
@@ -300,6 +303,7 @@ function useMultipleOperations() {
300
303
  baseUrl: sdkConfig.baseUrl || clientConfig.baseUrl || 'http://localhost:8000',
301
304
  credentials: sdkConfig.credentials,
302
305
  token,
306
+ tokenProvider: sdkConfig.tokenProvider,
303
307
  maxRetries: sdkConfig.maxRetries,
304
308
  retryDelay: sdkConfig.retryDelay,
305
309
  });
@@ -368,6 +372,7 @@ function useSDKClients() {
368
372
  credentials: sdkConfig.credentials,
369
373
  headers: sdkConfig.headers,
370
374
  token,
375
+ tokenProvider: sdkConfig.tokenProvider,
371
376
  };
372
377
  const queryClient = new QueryClient_1.QueryClient(baseConfig);
373
378
  const operationsClient = new OperationClient_1.OperationClient(baseConfig);
@@ -46,6 +46,7 @@ export function useQuery(graphId: string) {
46
46
  credentials: sdkConfig.credentials,
47
47
  headers: sdkConfig.headers,
48
48
  token,
49
+ tokenProvider: sdkConfig.tokenProvider,
49
50
  })
50
51
 
51
52
  return () => {
@@ -143,6 +144,7 @@ export function useStreamingQuery(graphId: string) {
143
144
  credentials: sdkConfig.credentials,
144
145
  headers: sdkConfig.headers,
145
146
  token: sdkConfig.token,
147
+ tokenProvider: sdkConfig.tokenProvider,
146
148
  })
147
149
 
148
150
  return () => {
@@ -240,6 +242,7 @@ export function useOperation<T = any>(operationId?: string) {
240
242
  baseUrl: sdkConfig.baseUrl || clientConfig.baseUrl || 'http://localhost:8000',
241
243
  credentials: sdkConfig.credentials,
242
244
  token,
245
+ tokenProvider: sdkConfig.tokenProvider,
243
246
  maxRetries: sdkConfig.maxRetries,
244
247
  retryDelay: sdkConfig.retryDelay,
245
248
  })
@@ -353,6 +356,7 @@ export function useMultipleOperations<T = any>() {
353
356
  baseUrl: sdkConfig.baseUrl || clientConfig.baseUrl || 'http://localhost:8000',
354
357
  credentials: sdkConfig.credentials,
355
358
  token,
359
+ tokenProvider: sdkConfig.tokenProvider,
356
360
  maxRetries: sdkConfig.maxRetries,
357
361
  retryDelay: sdkConfig.retryDelay,
358
362
  })
@@ -439,6 +443,7 @@ export function useSDKClients() {
439
443
  credentials: sdkConfig.credentials,
440
444
  headers: sdkConfig.headers,
441
445
  token,
446
+ tokenProvider: sdkConfig.tokenProvider,
442
447
  }
443
448
 
444
449
  const queryClient = new QueryClient(baseConfig)
@@ -22,9 +22,12 @@ export interface RoboSystemsClientConfig {
22
22
  */
23
23
  token?: string;
24
24
  /**
25
- * Dynamic credential callback invoked on every GraphQL request.
26
- * When set, JWT refreshes are picked up automatically — no need
27
- * to rebuild or clear cached clients after a refresh.
25
+ * Dynamic credential callback invoked on every GraphQL request and
26
+ * every SSE connect. When set, JWT refreshes are picked up
27
+ * automatically — no need to rebuild or clear cached clients after a
28
+ * refresh. Browser sessions need this: the backend revokes the previous
29
+ * JWT on each refresh, so a static `token` captured at construction
30
+ * stops opening progress streams the moment the session rotates.
28
31
  */
29
32
  tokenProvider?: TokenProvider;
30
33
  headers?: Record<string, string>;
@@ -60,27 +60,33 @@ class RoboSystemsClients {
60
60
  retryDelay: config.retryDelay || 1000,
61
61
  timeout: config.timeout,
62
62
  };
63
+ // Every client that opens an SSE stream gets the tokenProvider too:
64
+ // the stream endpoint authenticates the JWT from the URL, and a
65
+ // captured static token is dead after the first session refresh.
63
66
  this.query = new QueryClient_1.QueryClient({
64
67
  baseUrl: this.config.baseUrl,
65
68
  credentials: this.config.credentials,
66
69
  token: this.config.token,
70
+ tokenProvider: this.config.tokenProvider,
67
71
  headers: this.config.headers,
68
72
  });
69
73
  this.operator = new OperatorClient_1.OperatorClient({
70
74
  baseUrl: this.config.baseUrl,
71
75
  credentials: this.config.credentials,
72
76
  token: this.config.token,
77
+ tokenProvider: this.config.tokenProvider,
73
78
  headers: this.config.headers,
74
79
  });
75
80
  this.operations = new OperationClient_1.OperationClient({
76
81
  baseUrl: this.config.baseUrl,
77
82
  credentials: this.config.credentials,
78
83
  token: this.config.token,
84
+ tokenProvider: this.config.tokenProvider,
79
85
  maxRetries: this.config.maxRetries,
80
86
  retryDelay: this.config.retryDelay,
81
87
  });
82
- // LedgerClient / InvestorClient use GraphQL internally, so they
83
- // get the tokenProvider — REST-only clients do not.
88
+ // LedgerClient / InvestorClient / LibraryClient use GraphQL internally
89
+ // and consult the tokenProvider on every request.
84
90
  this.ledger = new LedgerClient_1.LedgerClient({
85
91
  baseUrl: this.config.baseUrl,
86
92
  credentials: this.config.credentials,
@@ -125,6 +131,7 @@ class RoboSystemsClients {
125
131
  baseUrl: this.config.baseUrl,
126
132
  credentials: this.config.credentials,
127
133
  token: this.config.token,
134
+ tokenProvider: this.config.tokenProvider,
128
135
  headers: this.config.headers,
129
136
  maxRetries: this.config.maxRetries,
130
137
  retryDelay: this.config.retryDelay,
@@ -34,9 +34,12 @@ export interface RoboSystemsClientConfig {
34
34
  */
35
35
  token?: string
36
36
  /**
37
- * Dynamic credential callback invoked on every GraphQL request.
38
- * When set, JWT refreshes are picked up automatically — no need
39
- * to rebuild or clear cached clients after a refresh.
37
+ * Dynamic credential callback invoked on every GraphQL request and
38
+ * every SSE connect. When set, JWT refreshes are picked up
39
+ * automatically — no need to rebuild or clear cached clients after a
40
+ * refresh. Browser sessions need this: the backend revokes the previous
41
+ * JWT on each refresh, so a static `token` captured at construction
42
+ * stops opening progress streams the moment the session rotates.
40
43
  */
41
44
  tokenProvider?: TokenProvider
42
45
  headers?: Record<string, string>
@@ -98,10 +101,14 @@ export class RoboSystemsClients {
98
101
  timeout: config.timeout,
99
102
  }
100
103
 
104
+ // Every client that opens an SSE stream gets the tokenProvider too:
105
+ // the stream endpoint authenticates the JWT from the URL, and a
106
+ // captured static token is dead after the first session refresh.
101
107
  this.query = new QueryClient({
102
108
  baseUrl: this.config.baseUrl,
103
109
  credentials: this.config.credentials,
104
110
  token: this.config.token,
111
+ tokenProvider: this.config.tokenProvider,
105
112
  headers: this.config.headers,
106
113
  })
107
114
 
@@ -109,6 +116,7 @@ export class RoboSystemsClients {
109
116
  baseUrl: this.config.baseUrl,
110
117
  credentials: this.config.credentials,
111
118
  token: this.config.token,
119
+ tokenProvider: this.config.tokenProvider,
112
120
  headers: this.config.headers,
113
121
  })
114
122
 
@@ -116,12 +124,13 @@ export class RoboSystemsClients {
116
124
  baseUrl: this.config.baseUrl,
117
125
  credentials: this.config.credentials,
118
126
  token: this.config.token,
127
+ tokenProvider: this.config.tokenProvider,
119
128
  maxRetries: this.config.maxRetries,
120
129
  retryDelay: this.config.retryDelay,
121
130
  })
122
131
 
123
- // LedgerClient / InvestorClient use GraphQL internally, so they
124
- // get the tokenProvider — REST-only clients do not.
132
+ // LedgerClient / InvestorClient / LibraryClient use GraphQL internally
133
+ // and consult the tokenProvider on every request.
125
134
  this.ledger = new LedgerClient({
126
135
  baseUrl: this.config.baseUrl,
127
136
  credentials: this.config.credentials,
@@ -171,6 +180,7 @@ export class RoboSystemsClients {
171
180
  baseUrl: this.config.baseUrl,
172
181
  credentials: this.config.credentials,
173
182
  token: this.config.token,
183
+ tokenProvider: this.config.tokenProvider,
174
184
  headers: this.config.headers,
175
185
  maxRetries: this.config.maxRetries,
176
186
  retryDelay: this.config.retryDelay,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robosystems/client",
3
- "version": "1.12.2",
3
+ "version": "1.13.0",
4
4
  "description": "TypeScript client library for the RoboSystems financial intelligence platform",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",