@proteos/sdk 0.18.1

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.
Files changed (94) hide show
  1. package/LICENSE +40 -0
  2. package/dist/chunk-7RGN4E22.cjs +1185 -0
  3. package/dist/chunk-7RGN4E22.cjs.map +1 -0
  4. package/dist/chunk-XJP5WCRZ.js +1125 -0
  5. package/dist/chunk-XJP5WCRZ.js.map +1 -0
  6. package/dist/index.cjs +2384 -0
  7. package/dist/index.cjs.map +1 -0
  8. package/dist/index.d.cts +5225 -0
  9. package/dist/index.d.ts +5225 -0
  10. package/dist/index.js +2146 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/meta/index.cjs +204 -0
  13. package/dist/meta/index.cjs.map +1 -0
  14. package/dist/meta/index.d.cts +2 -0
  15. package/dist/meta/index.d.ts +2 -0
  16. package/dist/meta/index.js +3 -0
  17. package/dist/meta/index.js.map +1 -0
  18. package/dist/types-BNsjfU8N.d.cts +3299 -0
  19. package/dist/types-BNsjfU8N.d.ts +3299 -0
  20. package/package.json +86 -0
  21. package/src/agent/agents.ts +53 -0
  22. package/src/agent/index.ts +134 -0
  23. package/src/agent/mcp-servers.ts +102 -0
  24. package/src/agent/prompts.ts +80 -0
  25. package/src/agent/session-types.ts +397 -0
  26. package/src/agent/sessions.ts +197 -0
  27. package/src/agent/skills.ts +89 -0
  28. package/src/agent/tools.ts +53 -0
  29. package/src/agent/types.ts +362 -0
  30. package/src/auth/index.ts +111 -0
  31. package/src/auth/me.ts +46 -0
  32. package/src/auth/organizations.ts +128 -0
  33. package/src/auth/platform-entities.ts +78 -0
  34. package/src/auth/roles.ts +213 -0
  35. package/src/auth/types.ts +294 -0
  36. package/src/auth/users.ts +226 -0
  37. package/src/client.ts +441 -0
  38. package/src/connector/index.ts +120 -0
  39. package/src/connector/types.ts +150 -0
  40. package/src/conversation/index.ts +297 -0
  41. package/src/conversation/types.ts +590 -0
  42. package/src/conversation/voice.ts +123 -0
  43. package/src/data/index.ts +53 -0
  44. package/src/data/queries.ts +66 -0
  45. package/src/data/records.ts +122 -0
  46. package/src/data/types.ts +89 -0
  47. package/src/errors.ts +148 -0
  48. package/src/events/index.ts +172 -0
  49. package/src/events/types.ts +77 -0
  50. package/src/functions/actions.ts +95 -0
  51. package/src/functions/index.ts +32 -0
  52. package/src/functions/types.ts +71 -0
  53. package/src/http/index.ts +2 -0
  54. package/src/http/query-params.ts +106 -0
  55. package/src/index.ts +598 -0
  56. package/src/iterator.ts +183 -0
  57. package/src/knowledge/graph.ts +35 -0
  58. package/src/knowledge/index.ts +104 -0
  59. package/src/knowledge/labels.ts +70 -0
  60. package/src/knowledge/links.ts +65 -0
  61. package/src/knowledge/nodes.ts +198 -0
  62. package/src/knowledge/record-links.ts +66 -0
  63. package/src/knowledge/types.ts +569 -0
  64. package/src/meta/apps.ts +107 -0
  65. package/src/meta/components.ts +124 -0
  66. package/src/meta/currency/index.ts +202 -0
  67. package/src/meta/entities.ts +193 -0
  68. package/src/meta/filters.ts +76 -0
  69. package/src/meta/index.ts +227 -0
  70. package/src/meta/layout/common-props.ts +93 -0
  71. package/src/meta/layout/control-registry.json +70 -0
  72. package/src/meta/layout/control-registry.ts +92 -0
  73. package/src/meta/layout/elements.ts +203 -0
  74. package/src/meta/layout/index.ts +41 -0
  75. package/src/meta/layout/page-layout.ts +35 -0
  76. package/src/meta/layout/size-value.ts +27 -0
  77. package/src/meta/list-views.ts +109 -0
  78. package/src/meta/lists.ts +104 -0
  79. package/src/meta/menu-configurations.ts +128 -0
  80. package/src/meta/modules.ts +159 -0
  81. package/src/meta/pages.ts +106 -0
  82. package/src/meta/types.ts +1115 -0
  83. package/src/meta/variables.ts +98 -0
  84. package/src/storage/files.ts +183 -0
  85. package/src/storage/index.ts +33 -0
  86. package/src/storage/types.ts +70 -0
  87. package/src/types/common.ts +143 -0
  88. package/src/types/index.ts +28 -0
  89. package/src/types/options.ts +95 -0
  90. package/src/workflow/executions.ts +99 -0
  91. package/src/workflow/index.ts +109 -0
  92. package/src/workflow/node-types.ts +50 -0
  93. package/src/workflow/types.ts +658 -0
  94. package/src/workflow/workflows.ts +152 -0
@@ -0,0 +1,226 @@
1
+ import type { ProteosClient } from '../client.js'
2
+ import { PageIterator } from '../iterator.js'
3
+ import type { ListResult } from '../types/common.js'
4
+ import type {
5
+ ApiKey,
6
+ AssignRoleRequest,
7
+ CreateApiKeyRequest,
8
+ CreatedApiKey,
9
+ CreateUserRequest,
10
+ ListUserRoleAssignmentsOptions,
11
+ ListUsersOptions,
12
+ UpdateUserRequest,
13
+ User,
14
+ UserRoleAssignment,
15
+ } from './types.js'
16
+
17
+ const USERS_BASE_PATH = '/accounts/v1/users'
18
+
19
+ /**
20
+ * Service for managing users.
21
+ * Handles user CRUD operations and role assignments.
22
+ */
23
+ export interface UserService {
24
+ /**
25
+ * Lists users with optional filtering.
26
+ * Returns an async iterator that automatically handles pagination.
27
+ *
28
+ * @param options - Filter and pagination options
29
+ * @returns Async iterator over users
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * // Iterate over all users
34
+ * for await (const user of service.list()) {
35
+ * console.log(user.email);
36
+ * }
37
+ *
38
+ * // With filtering
39
+ * for await (const user of service.list({ email: 'john@example.com' })) {
40
+ * console.log(user.given_name);
41
+ * }
42
+ * ```
43
+ */
44
+ list(options?: ListUsersOptions): PageIterator<User, ListUsersOptions>
45
+
46
+ /**
47
+ * Gets a single user by ID.
48
+ *
49
+ * @param id - User ID
50
+ * @returns The user
51
+ * @throws {ProteosError} If user not found (404)
52
+ */
53
+ get(id: string): Promise<User>
54
+
55
+ /**
56
+ * Creates a new user.
57
+ *
58
+ * @param request - User creation request
59
+ * @returns The created user
60
+ * @throws {ProteosError} If validation fails (400) or conflict (409)
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * const user = await service.create({
65
+ * given_name: 'John',
66
+ * family_name: 'Doe',
67
+ * email: 'john.doe@example.com',
68
+ * });
69
+ * ```
70
+ */
71
+ create(request: CreateUserRequest): Promise<User>
72
+
73
+ /**
74
+ * Updates an existing user.
75
+ *
76
+ * @param id - User ID
77
+ * @param request - Fields to update
78
+ * @returns The updated user
79
+ * @throws {ProteosError} If user not found (404) or validation fails (400)
80
+ */
81
+ update(id: string, request: UpdateUserRequest): Promise<User>
82
+
83
+ /**
84
+ * Fetches a single page of users.
85
+ *
86
+ * @param options - Filter and pagination options (including `page`)
87
+ * @returns A single page of users with pagination metadata
88
+ */
89
+ listPage(options?: ListUsersOptions): Promise<ListResult<User>>
90
+
91
+ /**
92
+ * Gets roles assigned to a user.
93
+ * Returns an async iterator that automatically handles pagination.
94
+ *
95
+ * @param userId - User ID
96
+ * @param options - Filter and pagination options
97
+ * @returns Async iterator over user role assignments
98
+ */
99
+ getRoles(
100
+ userId: string,
101
+ options?: ListUserRoleAssignmentsOptions,
102
+ ): PageIterator<UserRoleAssignment, ListUserRoleAssignmentsOptions>
103
+
104
+ /**
105
+ * Assigns a role to a user.
106
+ *
107
+ * @param userId - User ID
108
+ * @param request - Role assignment request
109
+ * @returns The created user role assignment
110
+ * @throws {ProteosError} If user or role not found (404)
111
+ */
112
+ assignRole(userId: string, request: AssignRoleRequest): Promise<UserRoleAssignment>
113
+
114
+ /**
115
+ * Removes a role from a user.
116
+ *
117
+ * @param userId - User ID
118
+ * @param roleSlug - Role slug to unassign
119
+ * @throws {ProteosError} If assignment not found (404)
120
+ */
121
+ unassignRole(userId: string, roleSlug: string): Promise<void>
122
+
123
+ /**
124
+ * Lists a user's API keys (display hints only — never tokens).
125
+ *
126
+ * @param userId - User ID, or `'me'` for the calling user
127
+ */
128
+ listApiKeys(userId: string): Promise<ApiKey[]>
129
+
130
+ /**
131
+ * Creates an API key for the user. The response carries the full token
132
+ * exactly once; it cannot be read again. Requires a JWT session — requests
133
+ * authenticated with an API key are rejected (403).
134
+ *
135
+ * @param userId - User ID, or `'me'` for the calling user
136
+ * @param request - Key name, optional org binding and expiry
137
+ */
138
+ createApiKey(userId: string, request: CreateApiKeyRequest): Promise<CreatedApiKey>
139
+
140
+ /**
141
+ * Deletes (revokes) an API key. Other services may keep accepting the key
142
+ * for up to their verification-cache TTL (~60s).
143
+ *
144
+ * @param userId - User ID, or `'me'` for the calling user
145
+ * @param keyId - API key ID
146
+ * @throws {ProteosError} If the key is not found under this user (404)
147
+ */
148
+ deleteApiKey(userId: string, keyId: string): Promise<void>
149
+ }
150
+
151
+ /**
152
+ * Implementation of UserService.
153
+ */
154
+ export class UserServiceImpl implements UserService {
155
+ constructor(private readonly client: ProteosClient) {}
156
+
157
+ list(options: ListUsersOptions = {}): PageIterator<User, ListUsersOptions> {
158
+ return new PageIterator((opts) => this.listPage(opts), options)
159
+ }
160
+
161
+ async listPage(options: ListUsersOptions = {}): Promise<ListResult<User>> {
162
+ return this.client.requestWithQuery<ListResult<User>>('GET', USERS_BASE_PATH, options)
163
+ }
164
+
165
+ async get(id: string): Promise<User> {
166
+ return this.client.request<User>('GET', `${USERS_BASE_PATH}/${id}`)
167
+ }
168
+
169
+ async create(request: CreateUserRequest): Promise<User> {
170
+ return this.client.request<User>('POST', USERS_BASE_PATH, request)
171
+ }
172
+
173
+ async update(id: string, request: UpdateUserRequest): Promise<User> {
174
+ return this.client.request<User>('PATCH', `${USERS_BASE_PATH}/${id}`, request)
175
+ }
176
+
177
+ getRoles(
178
+ userId: string,
179
+ options: ListUserRoleAssignmentsOptions = {},
180
+ ): PageIterator<UserRoleAssignment, ListUserRoleAssignmentsOptions> {
181
+ return new PageIterator((opts) => this.fetchRolesPage(userId, opts), options)
182
+ }
183
+
184
+ async assignRole(userId: string, request: AssignRoleRequest): Promise<UserRoleAssignment> {
185
+ return this.client.request<UserRoleAssignment>(
186
+ 'POST',
187
+ `${USERS_BASE_PATH}/${userId}/roles`,
188
+ request,
189
+ )
190
+ }
191
+
192
+ async unassignRole(userId: string, roleSlug: string): Promise<void> {
193
+ await this.client.request<void>('DELETE', `${USERS_BASE_PATH}/${userId}/roles/${roleSlug}`)
194
+ }
195
+
196
+ async listApiKeys(userId: string): Promise<ApiKey[]> {
197
+ const response = await this.client.request<{ data: ApiKey[] }>(
198
+ 'GET',
199
+ `${USERS_BASE_PATH}/${userId}/api-keys`,
200
+ )
201
+ return response.data
202
+ }
203
+
204
+ async createApiKey(userId: string, request: CreateApiKeyRequest): Promise<CreatedApiKey> {
205
+ return this.client.request<CreatedApiKey>(
206
+ 'POST',
207
+ `${USERS_BASE_PATH}/${userId}/api-keys`,
208
+ request,
209
+ )
210
+ }
211
+
212
+ async deleteApiKey(userId: string, keyId: string): Promise<void> {
213
+ await this.client.request<void>('DELETE', `${USERS_BASE_PATH}/${userId}/api-keys/${keyId}`)
214
+ }
215
+
216
+ private async fetchRolesPage(
217
+ userId: string,
218
+ options: ListUserRoleAssignmentsOptions,
219
+ ): Promise<ListResult<UserRoleAssignment>> {
220
+ return this.client.requestWithQuery<ListResult<UserRoleAssignment>>(
221
+ 'GET',
222
+ `${USERS_BASE_PATH}/${userId}/roles`,
223
+ options,
224
+ )
225
+ }
226
+ }
package/src/client.ts ADDED
@@ -0,0 +1,441 @@
1
+ import { type EventSourceMessage, fetchEventSource } from '@microsoft/fetch-event-source'
2
+ import { ProteosError, parseErrorResponse } from './errors.js'
3
+ import { toQueryParams } from './http/query-params.js'
4
+ import {
5
+ type ClientOptions,
6
+ type RequestOptions,
7
+ type ResolvedClientOptions,
8
+ resolveOptions,
9
+ } from './types/options.js'
10
+
11
+ /** One parsed Server-Sent-Events frame yielded by {@link ProteosClient.streamEvents}. */
12
+ export interface SseFrame {
13
+ /** The frame `id:` line — Proteos services use the per-stream monotonic seq. */
14
+ id?: string
15
+ /** The frame `event:` line — the named event type. */
16
+ event?: string
17
+ /** The frame `data:` line — the raw payload (typically JSON). */
18
+ data: string
19
+ }
20
+
21
+ /** Options for opening an SSE stream. */
22
+ export interface StreamOptions {
23
+ /** Abort to tear the stream down (the normal stop path). */
24
+ signal?: AbortSignal
25
+ }
26
+
27
+ /** True when an error is an AbortError (a stream's normal teardown). */
28
+ function isAbortError(error: unknown): boolean {
29
+ return error instanceof DOMException && error.name === 'AbortError'
30
+ }
31
+
32
+ /**
33
+ * Base HTTP client for all Proteos services.
34
+ * Handles authentication, request/response formatting, and error handling.
35
+ */
36
+ export class ProteosClient {
37
+ private readonly options: ResolvedClientOptions
38
+
39
+ /**
40
+ * Creates a new ProteosClient instance.
41
+ *
42
+ * @param options - Client configuration options
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * // With static token
47
+ * const client = new ProteosClient({
48
+ * baseUrl: 'https://api.proteos.ai',
49
+ * token: 'your-api-token',
50
+ * });
51
+ *
52
+ * // With dynamic token provider
53
+ * const client = new ProteosClient({
54
+ * baseUrl: 'https://api.proteos.ai',
55
+ * tokenProvider: async () => {
56
+ * return await authService.getAccessToken();
57
+ * },
58
+ * });
59
+ * ```
60
+ */
61
+ constructor(options: ClientOptions) {
62
+ this.options = resolveOptions(options)
63
+ }
64
+
65
+ /**
66
+ * Returns the base URL configured for this client.
67
+ */
68
+ get baseUrl(): string {
69
+ return this.options.baseUrl
70
+ }
71
+
72
+ /**
73
+ * Gets the current authentication token.
74
+ * Calls tokenProvider if available, otherwise returns static token.
75
+ */
76
+ private async getToken(): Promise<string | undefined> {
77
+ if (this.options.tokenProvider) {
78
+ return await this.options.tokenProvider()
79
+ }
80
+ return this.options.token
81
+ }
82
+
83
+ /**
84
+ * Builds the headers for a request.
85
+ */
86
+ private async buildHeaders(requestOptions?: RequestOptions): Promise<Headers> {
87
+ const headers = new Headers({
88
+ 'Content-Type': 'application/json',
89
+ Accept: 'application/json',
90
+ ...this.options.headers,
91
+ ...requestOptions?.headers,
92
+ })
93
+
94
+ const token = await this.getToken()
95
+ if (token) {
96
+ headers.set('Authorization', `Bearer ${token}`)
97
+ }
98
+
99
+ return headers
100
+ }
101
+
102
+ /**
103
+ * Creates an AbortSignal with timeout.
104
+ */
105
+ private createTimeoutSignal(requestOptions?: RequestOptions): AbortSignal {
106
+ const timeout = requestOptions?.timeout ?? this.options.timeout
107
+
108
+ // Combine user signal with timeout signal if both exist
109
+ if (requestOptions?.signal) {
110
+ const controller = new AbortController()
111
+
112
+ // Abort when user signal aborts
113
+ requestOptions.signal.addEventListener('abort', () => {
114
+ controller.abort(requestOptions.signal?.reason)
115
+ })
116
+
117
+ // Abort on timeout
118
+ const timeoutId = setTimeout(() => {
119
+ controller.abort(new Error(`Request timeout after ${timeout}ms`))
120
+ }, timeout)
121
+
122
+ // Clean up timeout if aborted early
123
+ controller.signal.addEventListener('abort', () => {
124
+ clearTimeout(timeoutId)
125
+ })
126
+
127
+ return controller.signal
128
+ }
129
+
130
+ return AbortSignal.timeout(timeout)
131
+ }
132
+
133
+ /**
134
+ * Performs an HTTP request and decodes the JSON response.
135
+ *
136
+ * @param method - HTTP method (GET, POST, PATCH, DELETE, etc.)
137
+ * @param path - Request path (appended to baseUrl)
138
+ * @param body - Request body (will be JSON-serialized)
139
+ * @param requestOptions - Optional request configuration
140
+ * @returns Decoded response body
141
+ * @throws {ProteosError} If the response status is >= 400
142
+ */
143
+ async request<T>(
144
+ method: string,
145
+ path: string,
146
+ body?: unknown,
147
+ requestOptions?: RequestOptions,
148
+ ): Promise<T> {
149
+ return this.requestWithQuery(method, path, undefined, body, requestOptions)
150
+ }
151
+
152
+ /**
153
+ * Performs an HTTP request with query parameters.
154
+ *
155
+ * @param method - HTTP method
156
+ * @param path - Request path
157
+ * @param query - Query parameters object
158
+ * @param body - Request body
159
+ * @param requestOptions - Optional request configuration
160
+ * @returns Decoded response body
161
+ * @throws {ProteosError} If the response status is >= 400
162
+ */
163
+ async requestWithQuery<T, Q extends object = object>(
164
+ method: string,
165
+ path: string,
166
+ query?: Q,
167
+ body?: unknown,
168
+ requestOptions?: RequestOptions,
169
+ ): Promise<T> {
170
+ let url = `${this.options.baseUrl}${path}`
171
+
172
+ if (query) {
173
+ const params = toQueryParams(query)
174
+ const queryString = params.toString()
175
+ if (queryString) {
176
+ url += `?${queryString}`
177
+ }
178
+ }
179
+
180
+ const headers = await this.buildHeaders(requestOptions)
181
+ const signal = this.createTimeoutSignal(requestOptions)
182
+
183
+ const response = await this.options.fetch(url, {
184
+ method,
185
+ headers,
186
+ body: body !== undefined ? JSON.stringify(body) : null,
187
+ signal,
188
+ })
189
+
190
+ if (response.status >= 400) {
191
+ throw await parseErrorResponse(response)
192
+ }
193
+
194
+ // No content response
195
+ if (response.status === 204) {
196
+ return undefined as T
197
+ }
198
+
199
+ // Parse JSON response
200
+ const text = await response.text()
201
+ if (!text) {
202
+ return undefined as T
203
+ }
204
+
205
+ try {
206
+ return JSON.parse(text) as T
207
+ } catch {
208
+ throw new ProteosError(
209
+ `Failed to parse response as JSON: ${text.slice(0, 100)}`,
210
+ response.status,
211
+ 'parse_error',
212
+ )
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Performs a multipart form request (for file uploads).
218
+ *
219
+ * @param method - HTTP method
220
+ * @param path - Request path
221
+ * @param formData - FormData instance with files and fields
222
+ * @param requestOptions - Optional request configuration
223
+ * @returns Decoded response body
224
+ * @throws {ProteosError} If the response status is >= 400
225
+ */
226
+ async requestMultipart<T>(
227
+ method: string,
228
+ path: string,
229
+ formData: FormData,
230
+ requestOptions?: RequestOptions,
231
+ ): Promise<T> {
232
+ const url = `${this.options.baseUrl}${path}`
233
+
234
+ // Don't set Content-Type - let fetch set it with boundary
235
+ const headers = new Headers({
236
+ Accept: 'application/json',
237
+ ...this.options.headers,
238
+ ...requestOptions?.headers,
239
+ })
240
+
241
+ const token = await this.getToken()
242
+ if (token) {
243
+ headers.set('Authorization', `Bearer ${token}`)
244
+ }
245
+
246
+ const signal = this.createTimeoutSignal(requestOptions)
247
+
248
+ const response = await this.options.fetch(url, {
249
+ method,
250
+ headers,
251
+ body: formData,
252
+ signal,
253
+ })
254
+
255
+ if (response.status >= 400) {
256
+ throw await parseErrorResponse(response)
257
+ }
258
+
259
+ if (response.status === 204) {
260
+ return undefined as T
261
+ }
262
+
263
+ return (await response.json()) as T
264
+ }
265
+
266
+ /**
267
+ * Performs an HTTP request and returns the raw response.
268
+ * Used for downloading files or streaming responses.
269
+ *
270
+ * @param method - HTTP method
271
+ * @param path - Request path
272
+ * @param body - Request body
273
+ * @param requestOptions - Optional request configuration
274
+ * @returns Object with response body stream and full Response object
275
+ * @throws {ProteosError} If the response status is >= 400
276
+ */
277
+ async requestRaw(
278
+ method: string,
279
+ path: string,
280
+ body?: unknown,
281
+ requestOptions?: RequestOptions,
282
+ ): Promise<{ body: ReadableStream<Uint8Array> | null; response: Response }> {
283
+ const url = `${this.options.baseUrl}${path}`
284
+
285
+ const headers = await this.buildHeaders(requestOptions)
286
+ const signal = this.createTimeoutSignal(requestOptions)
287
+
288
+ const response = await this.options.fetch(url, {
289
+ method,
290
+ headers,
291
+ body: body !== undefined ? JSON.stringify(body) : null,
292
+ signal,
293
+ })
294
+
295
+ if (response.status >= 400) {
296
+ throw await parseErrorResponse(response)
297
+ }
298
+
299
+ return {
300
+ body: response.body,
301
+ response,
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Opens a Server-Sent-Events stream and yields each frame as it arrives.
307
+ *
308
+ * Built on `@microsoft/fetch-event-source` (not the browser-native
309
+ * `EventSource`, which can't set an `Authorization` header) so the bearer token
310
+ * is attached like any other request. The library tracks `Last-Event-ID` and
311
+ * reconnects with exponential backoff on transient errors; a 4xx is fatal and
312
+ * rejects. The stream runs until the caller's `AbortSignal` fires.
313
+ *
314
+ * The token is resolved once at stream start; teardown is the caller's signal.
315
+ *
316
+ * @param path - Request path (appended to baseUrl)
317
+ * @param requestOptions - Optional request configuration (signal)
318
+ * @returns Async generator of parsed {@link SseFrame}s
319
+ * @throws {ProteosError} On a non-retryable (4xx) response
320
+ */
321
+ async *streamEvents(
322
+ path: string,
323
+ requestOptions?: StreamOptions,
324
+ ): AsyncGenerator<SseFrame, void, unknown> {
325
+ const url = `${this.options.baseUrl}${path}`
326
+
327
+ const headers: Record<string, string> = { ...this.options.headers }
328
+ const token = await this.getToken()
329
+ if (token) {
330
+ headers.Authorization = `Bearer ${token}`
331
+ }
332
+
333
+ // Bridge fetch-event-source's callbacks into an async generator: producer
334
+ // pushes frames onto a queue, the consumer (this generator) drains it,
335
+ // parking on a promise whenever the queue is empty.
336
+ const queue: SseFrame[] = []
337
+ let wake: (() => void) | null = null
338
+ let finished = false
339
+ let failure: unknown
340
+
341
+ const notify = () => {
342
+ wake?.()
343
+ wake = null
344
+ }
345
+ const push = (frame: SseFrame) => {
346
+ queue.push(frame)
347
+ notify()
348
+ }
349
+ const finish = (error?: unknown) => {
350
+ if (error !== undefined) failure = error
351
+ finished = true
352
+ notify()
353
+ }
354
+
355
+ const controller = new AbortController()
356
+ const onExternalAbort = () => controller.abort()
357
+ if (requestOptions?.signal) {
358
+ if (requestOptions.signal.aborted) controller.abort()
359
+ else requestOptions.signal.addEventListener('abort', onExternalAbort, { once: true })
360
+ }
361
+
362
+ const streaming = fetchEventSource(url, {
363
+ method: 'GET',
364
+ headers,
365
+ signal: controller.signal,
366
+ fetch: this.options.fetch,
367
+ // Keep streaming when the tab is backgrounded (default would disconnect).
368
+ openWhenHidden: true,
369
+ onopen: async (response: Response) => {
370
+ if (response.status >= 400) {
371
+ throw await parseErrorResponse(response)
372
+ }
373
+ },
374
+ onmessage: (message: EventSourceMessage) => {
375
+ push({
376
+ id: message.id || undefined,
377
+ event: message.event || undefined,
378
+ data: message.data,
379
+ })
380
+ },
381
+ onerror: (error: unknown) => {
382
+ // Abort + 4xx are fatal (rethrow to stop). Everything else is transient
383
+ // — return a backoff so the library reconnects (Last-Event-ID resumes).
384
+ if (isAbortError(error)) throw error
385
+ if (error instanceof ProteosError && error.httpStatus >= 400 && error.httpStatus < 500) {
386
+ throw error
387
+ }
388
+ return 2000
389
+ },
390
+ })
391
+ .then(() => finish())
392
+ .catch((error: unknown) => finish(error))
393
+
394
+ try {
395
+ while (true) {
396
+ if (queue.length > 0) {
397
+ yield queue.shift() as SseFrame
398
+ continue
399
+ }
400
+ if (finished) break
401
+ await new Promise<void>((resolve) => {
402
+ wake = resolve
403
+ })
404
+ }
405
+ if (failure !== undefined && !isAbortError(failure)) throw failure
406
+ } finally {
407
+ controller.abort()
408
+ requestOptions?.signal?.removeEventListener('abort', onExternalAbort)
409
+ await streaming.catch(() => {})
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Opens an authenticated WebSocket to a service path and resolves once it is
415
+ * open.
416
+ *
417
+ * The browser-native `WebSocket` can't set an `Authorization` header, so the
418
+ * access token is passed via the `bearer` subprotocol (`["bearer", token]`),
419
+ * which the gateway lifts back into the auth header — the same token resolved
420
+ * for every other request. The `http(s)` base URL is rewritten to `ws(s)`.
421
+ *
422
+ * @param path - Request path (appended to baseUrl), including any query string
423
+ * @returns The open WebSocket
424
+ */
425
+ async openWebSocket(path: string): Promise<WebSocket> {
426
+ const wsBaseUrl = this.options.baseUrl.replace(/^http/, 'ws')
427
+ const url = `${wsBaseUrl}${path}`
428
+
429
+ const token = await this.getToken()
430
+ const socket = token ? new WebSocket(url, ['bearer', token]) : new WebSocket(url)
431
+
432
+ await new Promise<void>((resolve, reject) => {
433
+ socket.onopen = () => resolve()
434
+ socket.onerror = () => reject(new ProteosError('WebSocket connection failed', 0, 'ws_error'))
435
+ })
436
+ // Clear the bootstrap handlers so the caller owns the socket cleanly.
437
+ socket.onopen = null
438
+ socket.onerror = null
439
+ return socket
440
+ }
441
+ }