@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.
- package/LICENSE +40 -0
- package/dist/chunk-7RGN4E22.cjs +1185 -0
- package/dist/chunk-7RGN4E22.cjs.map +1 -0
- package/dist/chunk-XJP5WCRZ.js +1125 -0
- package/dist/chunk-XJP5WCRZ.js.map +1 -0
- package/dist/index.cjs +2384 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +5225 -0
- package/dist/index.d.ts +5225 -0
- package/dist/index.js +2146 -0
- package/dist/index.js.map +1 -0
- package/dist/meta/index.cjs +204 -0
- package/dist/meta/index.cjs.map +1 -0
- package/dist/meta/index.d.cts +2 -0
- package/dist/meta/index.d.ts +2 -0
- package/dist/meta/index.js +3 -0
- package/dist/meta/index.js.map +1 -0
- package/dist/types-BNsjfU8N.d.cts +3299 -0
- package/dist/types-BNsjfU8N.d.ts +3299 -0
- package/package.json +86 -0
- package/src/agent/agents.ts +53 -0
- package/src/agent/index.ts +134 -0
- package/src/agent/mcp-servers.ts +102 -0
- package/src/agent/prompts.ts +80 -0
- package/src/agent/session-types.ts +397 -0
- package/src/agent/sessions.ts +197 -0
- package/src/agent/skills.ts +89 -0
- package/src/agent/tools.ts +53 -0
- package/src/agent/types.ts +362 -0
- package/src/auth/index.ts +111 -0
- package/src/auth/me.ts +46 -0
- package/src/auth/organizations.ts +128 -0
- package/src/auth/platform-entities.ts +78 -0
- package/src/auth/roles.ts +213 -0
- package/src/auth/types.ts +294 -0
- package/src/auth/users.ts +226 -0
- package/src/client.ts +441 -0
- package/src/connector/index.ts +120 -0
- package/src/connector/types.ts +150 -0
- package/src/conversation/index.ts +297 -0
- package/src/conversation/types.ts +590 -0
- package/src/conversation/voice.ts +123 -0
- package/src/data/index.ts +53 -0
- package/src/data/queries.ts +66 -0
- package/src/data/records.ts +122 -0
- package/src/data/types.ts +89 -0
- package/src/errors.ts +148 -0
- package/src/events/index.ts +172 -0
- package/src/events/types.ts +77 -0
- package/src/functions/actions.ts +95 -0
- package/src/functions/index.ts +32 -0
- package/src/functions/types.ts +71 -0
- package/src/http/index.ts +2 -0
- package/src/http/query-params.ts +106 -0
- package/src/index.ts +598 -0
- package/src/iterator.ts +183 -0
- package/src/knowledge/graph.ts +35 -0
- package/src/knowledge/index.ts +104 -0
- package/src/knowledge/labels.ts +70 -0
- package/src/knowledge/links.ts +65 -0
- package/src/knowledge/nodes.ts +198 -0
- package/src/knowledge/record-links.ts +66 -0
- package/src/knowledge/types.ts +569 -0
- package/src/meta/apps.ts +107 -0
- package/src/meta/components.ts +124 -0
- package/src/meta/currency/index.ts +202 -0
- package/src/meta/entities.ts +193 -0
- package/src/meta/filters.ts +76 -0
- package/src/meta/index.ts +227 -0
- package/src/meta/layout/common-props.ts +93 -0
- package/src/meta/layout/control-registry.json +70 -0
- package/src/meta/layout/control-registry.ts +92 -0
- package/src/meta/layout/elements.ts +203 -0
- package/src/meta/layout/index.ts +41 -0
- package/src/meta/layout/page-layout.ts +35 -0
- package/src/meta/layout/size-value.ts +27 -0
- package/src/meta/list-views.ts +109 -0
- package/src/meta/lists.ts +104 -0
- package/src/meta/menu-configurations.ts +128 -0
- package/src/meta/modules.ts +159 -0
- package/src/meta/pages.ts +106 -0
- package/src/meta/types.ts +1115 -0
- package/src/meta/variables.ts +98 -0
- package/src/storage/files.ts +183 -0
- package/src/storage/index.ts +33 -0
- package/src/storage/types.ts +70 -0
- package/src/types/common.ts +143 -0
- package/src/types/index.ts +28 -0
- package/src/types/options.ts +95 -0
- package/src/workflow/executions.ts +99 -0
- package/src/workflow/index.ts +109 -0
- package/src/workflow/node-types.ts +50 -0
- package/src/workflow/types.ts +658 -0
- 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
|
+
}
|