@vertesia/client 0.79.1 → 0.80.0-dev-20251118

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertesia/client",
3
- "version": "0.79.1",
3
+ "version": "0.80.0-dev-20251118",
4
4
  "type": "module",
5
5
  "types": "./lib/types/index.d.ts",
6
6
  "files": [
@@ -47,6 +47,11 @@
47
47
  "require": "./lib/cjs/nodejs/index.js"
48
48
  }
49
49
  },
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "https://github.com/vertesia/composableai.git",
53
+ "directory": "packages/client"
54
+ },
50
55
  "typesVersions": {
51
56
  "*": {
52
57
  "node": [
@@ -0,0 +1,72 @@
1
+ import { ApiTopic, ClientBase } from "@vertesia/api-fetch-client";
2
+ import {
3
+ CatalogInteractionRef,
4
+ InCodeInteraction,
5
+ InteractionStatus
6
+ } from "@vertesia/common";
7
+
8
+
9
+ export class InteractionCatalogApi extends ApiTopic {
10
+ constructor(parent: ClientBase) {
11
+ super(parent, "/api/v1/interactions/catalog");
12
+ }
13
+
14
+ /**
15
+ * List all project interactions
16
+ */
17
+ list(query: { status?: InteractionStatus, tag?: string } = {}): Promise<CatalogInteractionRef[]> {
18
+ return this.get("/", {
19
+ query
20
+ });
21
+ }
22
+
23
+ /**
24
+ * List all stored interactions
25
+ */
26
+ listStoredInteractions(query: { status?: InteractionStatus, tag?: string } = {}): Promise<CatalogInteractionRef[]> {
27
+ return this.get("/stored", {
28
+ query
29
+ });
30
+ }
31
+
32
+ /**
33
+ * List sys interactions
34
+ */
35
+ listSysInteractions(tag?: string): Promise<CatalogInteractionRef[]> {
36
+ return this.get(`/sys`, {
37
+ query: {
38
+ tag
39
+ }
40
+ });
41
+ }
42
+
43
+ /**
44
+ * List sys interactions
45
+ */
46
+ listAppInteractions(appName: string, tag?: string): Promise<CatalogInteractionRef[]> {
47
+ return this.get(`/apps/${appName}`, {
48
+ query: {
49
+ tag
50
+ }
51
+ });
52
+ }
53
+
54
+ /**
55
+ * List all app interactions
56
+ */
57
+ listAllAppInteractions(tag?: string): Promise<CatalogInteractionRef[]> {
58
+ return this.get(`/apps`, {
59
+ query: {
60
+ tag
61
+ }
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Resolve an interaction given its id to a InCodeInteraction
67
+ * @param id Interaction id
68
+ */
69
+ resolve(id: string): Promise<InCodeInteraction> {
70
+ return this.get(`/resolve/${id}`);
71
+ }
72
+ }
@@ -1,12 +1,16 @@
1
1
  import { ApiTopic, ClientBase, ServerError } from "@vertesia/api-fetch-client";
2
2
  import {
3
3
  AsyncExecutionPayload, ComputeInteractionFacetPayload, GenerateInteractionPayload, GenerateTestDataPayload, ImprovePromptPayload,
4
- Interaction, InteractionCreatePayload, InteractionEndpoint, InteractionEndpointQuery, InteractionExecutionPayload, InteractionForkPayload,
4
+ ImprovePromptPayloadConfig,
5
+ Interaction, InteractionCreatePayload, InteractionEndpoint, InteractionEndpointQuery,
6
+ InteractionExecutionPayload, InteractionForkPayload,
5
7
  InteractionPublishPayload, InteractionRef, InteractionRefWithSchema, InteractionSearchPayload, InteractionSearchQuery,
6
- InteractionsExportPayload, InteractionUpdatePayload, RateLimitRequestPayload, RateLimitRequestResponse
8
+ InteractionsExportPayload, InteractionUpdatePayload,
9
+ RateLimitRequestPayload, RateLimitRequestResponse
7
10
  } from "@vertesia/common";
8
11
  import { VertesiaClient } from "./client.js";
9
12
  import { checkRateLimit, executeInteraction, executeInteractionAsync, executeInteractionByName } from "./execute.js";
13
+ import { InteractionCatalogApi } from "./InteractionCatalogApi.js";
10
14
  import { EnhancedExecutionRun, EnhancedInteractionExecutionResult, enhanceExecutionRun, enhanceInteractionExecutionResult } from "./InteractionOutput.js";
11
15
 
12
16
  export interface ComputeInteractionFacetsResponse {
@@ -20,8 +24,11 @@ export interface AsyncExecutionResult {
20
24
  }
21
25
 
22
26
  export default class InteractionsApi extends ApiTopic {
27
+ catalog: InteractionCatalogApi;
28
+
23
29
  constructor(parent: ClientBase) {
24
30
  super(parent, "/api/v1/interactions");
31
+ this.catalog = new InteractionCatalogApi(parent);
25
32
  }
26
33
 
27
34
  /**
@@ -221,14 +228,23 @@ export default class InteractionsApi extends ApiTopic {
221
228
 
222
229
  /**
223
230
  * Suggest Improvement for a prompt
231
+ * @deprecated use suggestPromptImprovements instead
224
232
  */
225
- async suggestImprovements<ResultT = any, ParamsT = any>(id: string, payload: ImprovePromptPayload): Promise<EnhancedExecutionRun<ResultT, ParamsT>> {
233
+ async suggestImprovements<ResultT = any, ParamsT = any>(id: string, payload: ImprovePromptPayloadConfig): Promise<EnhancedExecutionRun<ResultT, ParamsT>> {
226
234
  const r = await this.post(`${id}/suggest-prompt-improvements`, {
227
235
  payload
228
236
  });
229
237
  return enhanceExecutionRun<ResultT, ParamsT>(r);
230
238
  }
231
239
 
240
+ async suggestPromptImprovements<ResultT = any, ParamsT = any>(payload: ImprovePromptPayload): Promise<EnhancedInteractionExecutionResult<ResultT, ParamsT>> {
241
+ const r = await this.post(`/improve`, {
242
+ payload
243
+ });
244
+ return enhanceInteractionExecutionResult<ResultT, ParamsT>(r);
245
+ }
246
+
247
+
232
248
  /**
233
249
  * List the versions of the interaction. Returns an empty array if no versions are found
234
250
  * @param id
@@ -1,5 +1,5 @@
1
1
  import { ApiTopic, ClientBase } from "@vertesia/api-fetch-client";
2
- import { AwsConfiguration, GithubConfiguration, GladiaConfiguration, ICreateProjectPayload, MagicPdfConfiguration, Project, ProjectIntegrationListEntry, ProjectRef, SupportedIntegrations } from "@vertesia/common";
2
+ import { AwsConfiguration, GithubConfiguration, GladiaConfiguration, ICreateProjectPayload, MagicPdfConfiguration, Project, ProjectConfiguration, ProjectIntegrationListEntry, ProjectRef, SupportedIntegrations } from "@vertesia/common";
3
3
 
4
4
  export default class ProjectsApi extends ApiTopic {
5
5
  constructor(parent: ClientBase) {
@@ -26,6 +26,12 @@ export default class ProjectsApi extends ApiTopic {
26
26
  });
27
27
  }
28
28
 
29
+ updateConfiguration(projectId: string, payload: Partial<ProjectConfiguration>): Promise<ProjectConfiguration> {
30
+ return this.put(`/${projectId}/configuration`, {
31
+ payload
32
+ });
33
+ }
34
+
29
35
  integrations: IntegrationsConfigurationApi = new IntegrationsConfigurationApi(this);
30
36
 
31
37
  }
package/src/RunsApi.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ExecutionResponse } from "@llumiverse/common";
1
2
  import { ApiTopic, ClientBase } from "@vertesia/api-fetch-client";
2
3
  import {
3
4
  CheckpointConversationPayload,
@@ -5,6 +6,7 @@ import {
5
6
  ExecutionRun,
6
7
  ExecutionRunRef,
7
8
  FindPayload,
9
+ PopulatedExecutionRun,
8
10
  RunCreatePayload,
9
11
  RunListingFilters,
10
12
  RunListingQueryOptions,
@@ -13,7 +15,6 @@ import {
13
15
  UserMessagePayload,
14
16
  } from "@vertesia/common";
15
17
  import { VertesiaClient } from "./client.js";
16
- import type { ExecutionResponse } from "@llumiverse/common";
17
18
  import { EnhancedExecutionRun, enhanceExecutionRun } from "./InteractionOutput.js";
18
19
 
19
20
  export interface FilterOption {
@@ -69,6 +70,12 @@ export class RunsApi extends ApiTopic {
69
70
  return enhanceExecutionRun<ResultT, ParamsT>(r);
70
71
  }
71
72
 
73
+ retrievePopulated<P = any>(id: string): Promise<PopulatedExecutionRun<P>> {
74
+ return this.get("/" + id, {
75
+ query: { populate: "true" },
76
+ });
77
+ }
78
+
72
79
  /**
73
80
  * Get filter options for a field
74
81
  * return FilterOption[]
@@ -6,6 +6,7 @@ describe('Test Vertesia Client', () => {
6
6
  const client = new VertesiaClient({
7
7
  serverUrl: 'https://api.vertesia.io',
8
8
  storeUrl: 'https://api.vertesia.io',
9
+ tokenServerUrl: 'https://sts.vertesia.io',
9
10
  apikey: '1234',
10
11
  });
11
12
  expect(client).toBeDefined();
@@ -73,6 +74,7 @@ describe('Test Vertesia Client', () => {
73
74
  const client = new VertesiaClient({
74
75
  serverUrl: 'http://localhost:8091',
75
76
  storeUrl: 'http://localhost:8092',
77
+ tokenServerUrl: 'http://localhost:8093',
76
78
  });
77
79
 
78
80
  expect(client).toBeDefined();
package/src/client.ts CHANGED
@@ -37,18 +37,21 @@ export type VertesiaClientProps = {
37
37
  * @default api.vertesia.io
38
38
  * @since 0.52.0
39
39
  */
40
- site?: 'api.vertesia.io' | 'api-preview.vertesia.io' | 'api-staging.vertesia.io';
40
+ site?:
41
+ | "api.vertesia.io"
42
+ | "api-preview.vertesia.io"
43
+ | "api-staging.vertesia.io";
41
44
  serverUrl?: string;
42
45
  storeUrl?: string;
46
+ tokenServerUrl?: string;
43
47
  apikey?: string;
44
48
  projectId?: string;
45
49
  sessionTags?: string | string[];
46
50
  onRequest?: (request: Request) => void;
47
51
  onResponse?: (response: Response) => void;
48
- }
52
+ };
49
53
 
50
54
  export class VertesiaClient extends AbstractFetchClient<VertesiaClient> {
51
-
52
55
  /**
53
56
  * The JWT token linked to the API KEY (sk or pk)
54
57
  */
@@ -64,11 +67,15 @@ export class VertesiaClient extends AbstractFetchClient<VertesiaClient> {
64
67
  */
65
68
  sessionTags?: string | string[];
66
69
 
70
+ /**
71
+ * tokenServerUrl
72
+ */
73
+ tokenServerUrl: string;
67
74
 
68
75
  /**
69
- * Create a client from the given token.
76
+ * Create a client from the given token.
70
77
  * If you already have the decoded token you can pass it as the second argument to avoid decodinf it again.
71
- *
78
+ *
72
79
  * @param token the raw JWT token
73
80
  * @param payload the decoded JWT token as an AuthTokenPayload - optional
74
81
  */
@@ -76,21 +83,21 @@ export class VertesiaClient extends AbstractFetchClient<VertesiaClient> {
76
83
  if (!payload) {
77
84
  payload = decodeJWT(token);
78
85
  }
79
- const endpoints = decodeEndpoints(payload!.endpoints);
86
+
87
+ const endpoints = decodeEndpoints(payload.endpoints);
80
88
  return await new VertesiaClient({
81
89
  serverUrl: endpoints.studio,
82
- storeUrl: endpoints.store
90
+ storeUrl: endpoints.store,
91
+ tokenServerUrl: payload.iss,
83
92
  }).withApiKey(token);
84
93
  }
85
94
 
86
- static decodeEndpoints() {
87
-
88
- }
95
+ static decodeEndpoints() {}
89
96
 
90
97
  constructor(
91
98
  opts: VertesiaClientProps = {
92
- site: 'api.vertesia.io',
93
- }
99
+ site: "api.vertesia.io",
100
+ },
94
101
  ) {
95
102
  let studioServerUrl: string;
96
103
  let zenoServerUrl: string;
@@ -100,7 +107,9 @@ export class VertesiaClient extends AbstractFetchClient<VertesiaClient> {
100
107
  } else if (opts.site) {
101
108
  studioServerUrl = `https://${opts.site}`;
102
109
  } else {
103
- throw new Error("Parameter 'site' or 'serverUrl' is required for VertesiaClient");
110
+ throw new Error(
111
+ "Parameter 'site' or 'serverUrl' is required for VertesiaClient",
112
+ );
104
113
  }
105
114
 
106
115
  if (opts.storeUrl) {
@@ -108,25 +117,67 @@ export class VertesiaClient extends AbstractFetchClient<VertesiaClient> {
108
117
  } else if (opts.site) {
109
118
  zenoServerUrl = `https://${opts.site}`;
110
119
  } else {
111
- throw new Error("Parameter 'site' or 'storeUrl' is required for VertesiaClient");
120
+ throw new Error(
121
+ "Parameter 'site' or 'storeUrl' is required for VertesiaClient",
122
+ );
112
123
  }
113
124
 
114
125
  super(studioServerUrl);
115
126
 
127
+ if (opts.tokenServerUrl) {
128
+ this.tokenServerUrl = opts.tokenServerUrl;
129
+ } else if (opts.site) {
130
+ this.tokenServerUrl = `https://${opts.site.replace(/^api/, "sts")}`;
131
+ } else if (opts.serverUrl || opts.storeUrl) {
132
+ // Determine STS URL based on environment in serverUrl or storeUrl
133
+ const urlToCheck = opts.serverUrl || opts.storeUrl || "";
134
+ try {
135
+ const url = new URL(urlToCheck);
136
+ // Check for environment patterns
137
+ if (url.hostname.includes("-production.")) {
138
+ // zeno-server-production.api.vertesia.io -> sts.vertesia.io
139
+ this.tokenServerUrl = "https://sts.vertesia.io";
140
+ } else if (url.hostname.includes("-preview.")) {
141
+ // zeno-server-preview.api.vertesia.io -> sts-preview.vertesia.io
142
+ this.tokenServerUrl = "https://sts-preview.vertesia.io";
143
+ } else if (url.hostname === "api.vertesia.io") {
144
+ // api.vertesia.io -> sts.vertesia.io
145
+ this.tokenServerUrl = "https://sts.vertesia.io";
146
+ } else if (url.hostname === "api-preview.vertesia.io") {
147
+ // api-preview.vertesia.io -> sts-preview.vertesia.io
148
+ this.tokenServerUrl = "https://sts-preview.vertesia.io";
149
+ } else if (url.hostname === "api-staging.vertesia.io") {
150
+ // api-staging.vertesia.io -> sts-staging.vertesia.io
151
+ this.tokenServerUrl = "https://sts-staging.vertesia.io";
152
+ } else if (url.hostname.startsWith("api")) {
153
+ // Generic api.* pattern replacement
154
+ url.hostname = url.hostname.replace(/^api/, "sts");
155
+ this.tokenServerUrl = url.toString();
156
+ } else {
157
+ // Default to staging for everything else
158
+ this.tokenServerUrl = "https://sts-staging.vertesia.io";
159
+ }
160
+ } catch (e) {
161
+ // Default to staging if URL parsing fails
162
+ this.tokenServerUrl = "https://sts-staging.vertesia.io";
163
+ }
164
+ } else {
165
+ // Default to staging if no URL provided
166
+ this.tokenServerUrl = "https://sts-staging.vertesia.io";
167
+ }
168
+
116
169
  this.store = new ZenoClient({
117
170
  serverUrl: zenoServerUrl,
171
+ tokenServerUrl: this.tokenServerUrl,
118
172
  apikey: opts.apikey,
119
173
  onRequest: opts.onRequest,
120
- onResponse: opts.onResponse
174
+ onResponse: opts.onResponse,
121
175
  });
122
176
 
123
177
  if (opts.apikey) {
124
178
  this.withApiKey(opts.apikey);
125
179
  }
126
- //TODO: this is no more used, remove in next major version
127
- if (opts.projectId) {
128
- this.headers["x-project-id"] = opts.projectId;
129
- }
180
+
130
181
  this.onRequest = opts.onRequest;
131
182
  this.onResponse = opts.onResponse;
132
183
  this.sessionTags = opts.sessionTags;
@@ -153,25 +204,28 @@ export class VertesiaClient extends AbstractFetchClient<VertesiaClient> {
153
204
 
154
205
  async withApiKey(apiKey: string | null) {
155
206
  return this.withAuthCallback(
156
- apiKey ? async () => {
157
- if (!isApiKey(apiKey)) {
158
- return `Bearer ${apiKey}`
159
- }
160
-
161
- if (isTokenExpired(this._jwt)) {
162
- const jwt = await this.getAuthToken(apiKey);
163
- this._jwt = jwt.token;
164
- }
165
- return `Bearer ${this._jwt}`
166
- } : undefined
207
+ apiKey
208
+ ? async () => {
209
+ if (!isApiKey(apiKey)) {
210
+ return `Bearer ${apiKey}`;
211
+ }
212
+
213
+ if (isTokenExpired(this._jwt)) {
214
+ const jwt = await this.getAuthToken(apiKey);
215
+ this._jwt = jwt.token;
216
+ }
217
+ return `Bearer ${this._jwt}`;
218
+ }
219
+ : undefined,
167
220
  );
168
221
  }
169
222
 
170
223
  async getRawJWT() {
171
224
  if (!this._jwt && this._auth) {
172
225
  const auth = await this._auth();
173
- if (!this._jwt) { // the _jwt may be set by the auth callback
174
- this._jwt = auth.trim().split(' ')[1]; // remove Bearer prefix
226
+ if (!this._jwt) {
227
+ // the _jwt may be set by the auth callback
228
+ this._jwt = auth.trim().split(" ")[1]; // remove Bearer prefix
175
229
  }
176
230
  }
177
231
  return this._jwt || null;
@@ -221,21 +275,29 @@ export class VertesiaClient extends AbstractFetchClient<VertesiaClient> {
221
275
  return this.store.baseUrl;
222
276
  }
223
277
 
224
-
225
278
  /**
226
279
  *
227
- * Generate a token for use with other Composable's services
280
+ * Generate a token for use with other Vertesia's services
228
281
  *
229
- * @param accountId: selected account to generate the token for
230
282
  * @returns AuthTokenResponse
231
283
  */
232
- async getAuthToken(token?: string, accountId?: string): Promise<AuthTokenResponse> {
233
- const query = {
234
- accountId,
235
- token
236
- };
237
-
238
- return this.get('/auth/token', { query: query, headers: { "authorization": undefined } as any });
284
+ async getAuthToken(token?: string): Promise<AuthTokenResponse> {
285
+ return fetch(`${this.tokenServerUrl}/token/issue`, {
286
+ method: "POST",
287
+ headers: {
288
+ "Content-Type": "application/json",
289
+ Authorization: `Bearer ${token}`,
290
+ },
291
+ })
292
+ .then((response) => response.json())
293
+ .then((data) => data as AuthTokenResponse)
294
+ .catch((error) => {
295
+ console.error(
296
+ `Error fetching token from ${this.tokenServerUrl}:`,
297
+ { error },
298
+ );
299
+ throw error;
300
+ });
239
301
  }
240
302
 
241
303
  get initialHeaders() {
@@ -263,7 +325,7 @@ export class VertesiaClient extends AbstractFetchClient<VertesiaClient> {
263
325
  }
264
326
 
265
327
  function isApiKey(apiKey: string) {
266
- return (apiKey.startsWith('pk-') || apiKey.startsWith('sk-'));
328
+ return apiKey.startsWith("pk-") || apiKey.startsWith("sk-");
267
329
  }
268
330
 
269
331
  function isTokenExpired(token: string | null) {
@@ -274,38 +336,45 @@ function isTokenExpired(token: string | null) {
274
336
  const decoded = decodeJWT(token);
275
337
  const exp = decoded.exp;
276
338
  const currentTime = Date.now();
277
- return (currentTime <= exp * 1000 - EXPIRATION_THRESHOLD);
339
+ return currentTime <= exp * 1000 - EXPIRATION_THRESHOLD;
278
340
  }
279
341
 
280
342
  export function decodeJWT(jwt: string): AuthTokenPayload {
281
- const payloadBase64 = jwt.split('.')[1];
343
+ const payloadBase64 = jwt.split(".")[1];
282
344
  const decodedJson = base64UrlDecode(payloadBase64);
283
- return JSON.parse(decodedJson)
345
+ return JSON.parse(decodedJson);
284
346
  }
285
347
 
286
348
  function base64UrlDecode(input: string): string {
287
349
  // Convert base64url to base64
288
- const base64 = input.replace(/-/g, '+').replace(/_/g, '/')
350
+ const base64 = input
351
+ .replace(/-/g, "+")
352
+ .replace(/_/g, "/")
289
353
  // Pad with '=' to make length a multiple of 4
290
- .padEnd(Math.ceil(input.length / 4) * 4, '=');
354
+ .padEnd(Math.ceil(input.length / 4) * 4, "=");
291
355
 
292
- if (typeof Buffer !== 'undefined') {
356
+ if (typeof Buffer !== "undefined") {
293
357
  // Node.js
294
- return Buffer.from(base64, 'base64').toString('utf-8');
295
- } else if (typeof atob !== 'undefined' && typeof TextDecoder !== 'undefined') {
358
+ return Buffer.from(base64, "base64").toString("utf-8");
359
+ } else if (
360
+ typeof atob !== "undefined" &&
361
+ typeof TextDecoder !== "undefined"
362
+ ) {
296
363
  // Browser
297
364
  const binary = atob(base64);
298
- const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
365
+ const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
299
366
  // decode to utf8
300
367
  return new TextDecoder().decode(bytes);
301
368
  } else {
302
- throw new Error('No base64 decoder available');
369
+ throw new Error("No base64 decoder available");
303
370
  }
304
371
  }
305
372
 
306
- export function decodeEndpoints(endpoints: string | Record<string, string> | undefined): Record<string, string> {
373
+ export function decodeEndpoints(
374
+ endpoints: string | Record<string, string> | undefined,
375
+ ): Record<string, string> {
307
376
  if (!endpoints) {
308
- return getEndpointsFromDomain("api.vertesia.io")
377
+ return getEndpointsFromDomain("api.vertesia.io");
309
378
  }
310
379
  if (typeof endpoints === "string") {
311
380
  return getEndpointsFromDomain(endpoints);
@@ -319,12 +388,14 @@ function getEndpointsFromDomain(domain: string) {
319
388
  return {
320
389
  studio: `http://localhost:8091`,
321
390
  store: `http://localhost:8092`,
322
- }
391
+ token: process.env.STS_URL ?? "https://sts-staging.vertesia.io",
392
+ };
323
393
  } else {
324
394
  const url = `https://${domain}`;
325
395
  return {
326
396
  studio: url,
327
397
  store: url,
328
- }
398
+ token: url.replace("api", "sts"),
399
+ };
329
400
  }
330
401
  }
package/src/index.ts CHANGED
@@ -7,3 +7,4 @@ export type { ComputeRunFacetsResponse, FilterOption } from './RunsApi.js';
7
7
  export type { GroupsQueryOptions } from './GroupsApi.js';
8
8
  export * from "./store/index.js";
9
9
  export * from "./StreamSource.js";
10
+ export type { OrphanedAppInstallation } from "./AppsApi.js";
@@ -128,7 +128,6 @@ export class CollectionsApi extends ApiTopic {
128
128
  updatePermissions(collectionId: string, permissions: Record<string, string[]>): Promise<{
129
129
  id: string;
130
130
  security: Record<string, string[]>;
131
- objectsUpdated: number;
132
131
  }> {
133
132
  return this.put(`/${collectionId}/permissions`, {
134
133
  payload: permissions
@@ -145,9 +144,23 @@ export class CollectionsApi extends ApiTopic {
145
144
  id: string;
146
145
  message: string;
147
146
  security?: Record<string, string[]>;
148
- objectsUpdated: number;
149
147
  }> {
150
148
  return this.post(`/${collectionId}/propagate-permissions`);
151
149
  }
152
150
 
151
+ /**
152
+ * Manually trigger shared properties propagation from collection to member objects
153
+ * Useful for debugging and fixing shared properties issues
154
+ * @param collectionId - The collection ID
155
+ * @returns Object with collection id, message, and number of objects updated
156
+ */
157
+ propagateSharedProperties(collectionId: string): Promise<{
158
+ id: string;
159
+ message: string;
160
+ shared_properties: string[]
161
+ }> {
162
+ return this.post(`/${collectionId}/propagate-shared-props`);
163
+ }
164
+
165
+
153
166
  }
@@ -59,12 +59,14 @@ export class FilesApi extends ApiTopic {
59
59
  });
60
60
  }
61
61
 
62
- getDownloadUrl(file: string): Promise<GetFileUrlResponse> {
63
- return this.post("/download-url", {
64
- payload: {
65
- file,
66
- } satisfies GetFileUrlPayload,
67
- });
62
+ // Strictly typed: provide either simple args or a full payload via a separate method
63
+ getDownloadUrl(file: string, name?: string, disposition?: "inline" | "attachment"): Promise<GetFileUrlResponse> {
64
+ const payload: GetFileUrlPayload = { file, name, disposition };
65
+ return this.post("/download-url", { payload });
66
+ }
67
+
68
+ getDownloadUrlWithOptions(payload: GetFileUrlPayload): Promise<GetFileUrlResponse> {
69
+ return this.post("/download-url", { payload });
68
70
  }
69
71
 
70
72
  /**
@@ -120,9 +122,9 @@ export class FilesApi extends ApiTopic {
120
122
  if (res.ok) {
121
123
  return res;
122
124
  } else if (res.status === 404) {
123
- throw new Error(`File ${location} not found`); //TODO: type fetch error better with a fetch error class
125
+ throw new Error(`File at ${url} not found`); //TODO: type fetch error better with a fetch error class
124
126
  } else if (res.status === 403) {
125
- throw new Error(`File ${location} is forbidden`);
127
+ throw new Error(`File at ${url} is forbidden`);
126
128
  } else {
127
129
  console.log(res);
128
130
  throw new Error(
@@ -27,24 +27,6 @@ import { StreamSource } from "../StreamSource.js";
27
27
  import { AnalyzeDocApi } from "./AnalyzeDocApi.js";
28
28
  import { ZenoClient } from "./client.js";
29
29
 
30
- export interface UploadContentObjectPayload
31
- extends Omit<CreateContentObjectPayload, "content"> {
32
- content?:
33
- | StreamSource
34
- | File
35
- | {
36
- // the source URI
37
- source: string;
38
- // the original name of the input file if any
39
- name?: string;
40
- // the mime type of the content source.
41
- type?: string;
42
-
43
- // the target id in the content store
44
- id?: string;
45
- };
46
- }
47
-
48
30
  export interface ComputeFacetsResponse {
49
31
  type?: { _id: string; count: number }[];
50
32
  location?: { _id: string; count: number }[];
@@ -72,14 +54,16 @@ export class ObjectsApi extends ApiTopic {
72
54
  });
73
55
  }
74
56
 
75
- getDownloadUrl(fileUri: string): Promise<{ url: string }> {
57
+ getDownloadUrl(fileUri: string, name?: string, disposition?: "inline" | "attachment"): Promise<{ url: string }> {
76
58
  return this.post("/download-url", {
77
- payload: {
78
- file: fileUri,
79
- } satisfies GetFileUrlPayload,
59
+ payload: { file: fileUri, name, disposition } satisfies GetFileUrlPayload,
80
60
  });
81
61
  }
82
62
 
63
+ getDownloadUrlWithOptions(payload: GetFileUrlPayload): Promise<{ url: string }> {
64
+ return this.post("/download-url", { payload });
65
+ }
66
+
83
67
  getContentSource(objectId: string): Promise<ContentSource> {
84
68
  return this.get(`/${objectId}/content-source`);
85
69
  }
@@ -217,7 +201,7 @@ export class ObjectsApi extends ApiTopic {
217
201
  }
218
202
 
219
203
  async create(
220
- payload: UploadContentObjectPayload,
204
+ payload: CreateContentObjectPayload,
221
205
  options?: {
222
206
  collection_id?: string;
223
207
  processing_priority?: ContentObjectProcessingPriority;
@@ -10,6 +10,8 @@ import {
10
10
  ListWorkflowInteractionsResponse,
11
11
  ListWorkflowRunsPayload,
12
12
  ListWorkflowRunsResponse,
13
+ WebSocketClientMessage,
14
+ WebSocketServerMessage,
13
15
  WorkflowActionPayload,
14
16
  WorkflowDefinitionRef,
15
17
  WorkflowRule,
@@ -251,6 +253,170 @@ export class WorkflowsApi extends ApiTopic {
251
253
  });
252
254
  }
253
255
 
256
+ /**
257
+ * Stream workflow messages via WebSocket (for mobile/React Native clients)
258
+ * @param workflowId The workflow ID
259
+ * @param runId The run ID
260
+ * @param onMessage Callback for incoming messages
261
+ * @param since Optional timestamp to resume from
262
+ * @returns Promise that resolves with cleanup function and sendSignal helper
263
+ */
264
+ async streamMessagesWS(
265
+ workflowId: string,
266
+ runId: string,
267
+ onMessage?: (message: AgentMessage) => void,
268
+ since?: number
269
+ ): Promise<{ cleanup: () => void; sendSignal: (signalName: string, data: any) => void }> {
270
+ return new Promise((resolve, reject) => {
271
+ let reconnectAttempts = 0;
272
+ const maxReconnectAttempts = 10;
273
+ const baseDelay = 1000;
274
+ const maxDelay = 30000;
275
+ let ws: WebSocket | null = null;
276
+ let lastMessageTimestamp = since || 0;
277
+ let isClosed = false;
278
+
279
+ const calculateBackoffDelay = (attempts: number): number => {
280
+ const exponentialDelay = Math.min(baseDelay * Math.pow(2, attempts), maxDelay);
281
+ const jitter = Math.random() * 0.1 * exponentialDelay;
282
+ return exponentialDelay + jitter;
283
+ };
284
+
285
+ const connect = async () => {
286
+ if (isClosed) return;
287
+
288
+ try {
289
+ const client = this.client as VertesiaClient;
290
+ const wsUrl = new URL(client.workflows.baseUrl + `/runs/${workflowId}/${runId}/ws`);
291
+
292
+ // Replace http/https with ws/wss
293
+ wsUrl.protocol = wsUrl.protocol.replace('http', 'ws');
294
+
295
+ // Add query parameters
296
+ if (lastMessageTimestamp > 0) {
297
+ wsUrl.searchParams.set('since', lastMessageTimestamp.toString());
298
+ }
299
+
300
+ const bearerToken = client._auth ? await client._auth() : undefined;
301
+ if (!bearerToken) {
302
+ reject(new Error('No auth token available'));
303
+ return;
304
+ }
305
+
306
+ const token = bearerToken.split(' ')[1];
307
+ wsUrl.searchParams.set('access_token', token);
308
+
309
+ if (reconnectAttempts > 0) {
310
+ console.log(`Reconnecting to WebSocket for run ${runId} (attempt ${reconnectAttempts + 1}/${maxReconnectAttempts})`);
311
+ }
312
+
313
+ ws = new WebSocket(wsUrl.href);
314
+
315
+ ws.onopen = () => {
316
+ if (reconnectAttempts > 0) {
317
+ console.log(`Successfully reconnected to WebSocket for run ${runId}`);
318
+ }
319
+ reconnectAttempts = 0;
320
+
321
+ // Resolve with helpers on first successful connection
322
+ if (!isClosed) {
323
+ resolve({
324
+ cleanup: () => {
325
+ isClosed = true;
326
+ if (ws) {
327
+ ws.close();
328
+ ws = null;
329
+ }
330
+ },
331
+ sendSignal: (signalName: string, data: any) => {
332
+ if (ws?.readyState === WebSocket.OPEN) {
333
+ const message: WebSocketClientMessage = {
334
+ type: 'signal',
335
+ signalName,
336
+ data,
337
+ requestId: Date.now()
338
+ };
339
+ ws.send(JSON.stringify(message));
340
+ } else {
341
+ console.warn('WebSocket not open, cannot send signal');
342
+ }
343
+ }
344
+ });
345
+ }
346
+ };
347
+
348
+ ws.onmessage = (event: MessageEvent) => {
349
+ try {
350
+ const message = JSON.parse(event.data) as WebSocketServerMessage;
351
+
352
+ // Handle different message types
353
+ if ('workflow_run_id' in message) {
354
+ // This is an AgentMessage
355
+ const agentMessage = message as AgentMessage;
356
+
357
+ if (agentMessage.timestamp) {
358
+ lastMessageTimestamp = Math.max(lastMessageTimestamp, agentMessage.timestamp);
359
+ }
360
+
361
+ if (onMessage) onMessage(agentMessage);
362
+
363
+ // Check for stream completion
364
+ const streamIsOver =
365
+ agentMessage.type === AgentMessageType.TERMINATED ||
366
+ (agentMessage.type === AgentMessageType.COMPLETE &&
367
+ (!agentMessage.workstream_id || agentMessage.workstream_id === 'main'));
368
+
369
+ if (streamIsOver) {
370
+ console.log('Closing WebSocket due to workflow completion');
371
+ isClosed = true;
372
+ if (ws) {
373
+ ws.close();
374
+ ws = null;
375
+ }
376
+ }
377
+ } else if (message.type === 'pong') {
378
+ // Heartbeat response
379
+ console.debug('Received pong');
380
+ } else if (message.type === 'ack') {
381
+ console.debug('Signal acknowledged', message);
382
+ } else if (message.type === 'error') {
383
+ console.error('WebSocket error message', message);
384
+ }
385
+ } catch (err) {
386
+ console.error('Failed to parse WebSocket message', err);
387
+ }
388
+ };
389
+
390
+ ws.onerror = (err) => {
391
+ console.error('WebSocket error', err);
392
+ };
393
+
394
+ ws.onclose = () => {
395
+ if (!isClosed && reconnectAttempts < maxReconnectAttempts) {
396
+ const delay = calculateBackoffDelay(reconnectAttempts);
397
+ console.log(`WebSocket closed, reconnecting in ${delay}ms (attempt ${reconnectAttempts + 1}/${maxReconnectAttempts})`);
398
+ reconnectAttempts++;
399
+ setTimeout(connect, delay);
400
+ } else if (reconnectAttempts >= maxReconnectAttempts) {
401
+ reject(new Error(`WebSocket connection failed after ${maxReconnectAttempts} attempts`));
402
+ }
403
+ };
404
+ } catch (err) {
405
+ console.error('Error setting up WebSocket', err);
406
+ if (reconnectAttempts < maxReconnectAttempts) {
407
+ const delay = calculateBackoffDelay(reconnectAttempts);
408
+ reconnectAttempts++;
409
+ setTimeout(connect, delay);
410
+ } else {
411
+ reject(err);
412
+ }
413
+ }
414
+ };
415
+
416
+ connect();
417
+ });
418
+ }
419
+
254
420
  rules = new WorkflowsRulesApi(this);
255
421
  definitions = new WorkflowsDefinitionApi(this);
256
422
  }
@@ -13,6 +13,7 @@ import { WorkflowsApi } from "./WorkflowsApi.js";
13
13
 
14
14
  export interface ZenoClientProps {
15
15
  serverUrl?: string;
16
+ tokenServerUrl?: string;
16
17
  apikey?: string;
17
18
  onRequest?: (request: Request) => void;
18
19
  onResponse?: (response: Response) => void;