@syfthub/sdk 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,2581 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/errors.ts
12
+ var errors_exports = {};
13
+ __export(errors_exports, {
14
+ APIError: () => APIError,
15
+ AccountingAccountExistsError: () => AccountingAccountExistsError,
16
+ AccountingServiceUnavailableError: () => AccountingServiceUnavailableError,
17
+ AuthenticationError: () => AuthenticationError,
18
+ AuthorizationError: () => AuthorizationError,
19
+ ConfigurationError: () => ConfigurationError,
20
+ InvalidAccountingPasswordError: () => InvalidAccountingPasswordError,
21
+ NetworkError: () => NetworkError,
22
+ NotFoundError: () => NotFoundError,
23
+ SyftHubError: () => SyftHubError,
24
+ UserAlreadyExistsError: () => UserAlreadyExistsError,
25
+ ValidationError: () => ValidationError
26
+ });
27
+ var SyftHubError, APIError, AuthenticationError, AuthorizationError, NotFoundError, ValidationError, NetworkError, ConfigurationError, UserAlreadyExistsError, AccountingAccountExistsError, InvalidAccountingPasswordError, AccountingServiceUnavailableError;
28
+ var init_errors = __esm({
29
+ "src/errors.ts"() {
30
+ SyftHubError = class extends Error {
31
+ constructor(message) {
32
+ super(message);
33
+ this.name = "SyftHubError";
34
+ if (Error.captureStackTrace) {
35
+ Error.captureStackTrace(this, this.constructor);
36
+ }
37
+ }
38
+ };
39
+ APIError = class extends SyftHubError {
40
+ constructor(message, status, data) {
41
+ super(message);
42
+ this.status = status;
43
+ this.data = data;
44
+ this.name = "APIError";
45
+ }
46
+ };
47
+ AuthenticationError = class extends SyftHubError {
48
+ constructor(message = "Authentication required") {
49
+ super(message);
50
+ this.name = "AuthenticationError";
51
+ }
52
+ };
53
+ AuthorizationError = class extends SyftHubError {
54
+ constructor(message = "Permission denied") {
55
+ super(message);
56
+ this.name = "AuthorizationError";
57
+ }
58
+ };
59
+ NotFoundError = class extends SyftHubError {
60
+ constructor(message = "Resource not found") {
61
+ super(message);
62
+ this.name = "NotFoundError";
63
+ }
64
+ };
65
+ ValidationError = class extends SyftHubError {
66
+ constructor(message, errors) {
67
+ super(message);
68
+ this.errors = errors;
69
+ this.name = "ValidationError";
70
+ }
71
+ };
72
+ NetworkError = class extends SyftHubError {
73
+ constructor(message = "Network request failed", cause) {
74
+ super(message);
75
+ this.cause = cause;
76
+ this.name = "NetworkError";
77
+ }
78
+ };
79
+ ConfigurationError = class extends SyftHubError {
80
+ constructor(message = "Invalid SDK configuration") {
81
+ super(message);
82
+ this.name = "ConfigurationError";
83
+ }
84
+ };
85
+ UserAlreadyExistsError = class extends SyftHubError {
86
+ constructor(message = "Username or email already exists", detail) {
87
+ super(message);
88
+ this.detail = detail;
89
+ this.name = "UserAlreadyExistsError";
90
+ if (detail && typeof detail === "object" && "field" in detail) {
91
+ this.field = detail.field;
92
+ }
93
+ }
94
+ /** The field that caused the conflict ("username" or "email") */
95
+ field;
96
+ };
97
+ AccountingAccountExistsError = class extends SyftHubError {
98
+ constructor(message = "This email already has an account in the accounting service", detail) {
99
+ super(message);
100
+ this.detail = detail;
101
+ this.name = "AccountingAccountExistsError";
102
+ }
103
+ /** Indicates that the user needs to provide their existing accounting password */
104
+ requiresAccountingPassword = true;
105
+ };
106
+ InvalidAccountingPasswordError = class extends SyftHubError {
107
+ constructor(message = "The provided accounting password is invalid", detail) {
108
+ super(message);
109
+ this.detail = detail;
110
+ this.name = "InvalidAccountingPasswordError";
111
+ }
112
+ };
113
+ AccountingServiceUnavailableError = class extends SyftHubError {
114
+ constructor(message = "Accounting service is unavailable", detail) {
115
+ super(message);
116
+ this.detail = detail;
117
+ this.name = "AccountingServiceUnavailableError";
118
+ }
119
+ };
120
+ }
121
+ });
122
+
123
+ // src/http.ts
124
+ init_errors();
125
+
126
+ // src/utils.ts
127
+ function snakeToCamel(str) {
128
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
129
+ }
130
+ function camelToSnake(str) {
131
+ return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
132
+ }
133
+ var ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
134
+ function isISODateString(value) {
135
+ return typeof value === "string" && ISO_DATE_REGEX.test(value);
136
+ }
137
+ function transformKeys(obj, keyTransformer, parseDates = true) {
138
+ if (obj === null || obj === void 0) {
139
+ return obj;
140
+ }
141
+ if (Array.isArray(obj)) {
142
+ return obj.map((item) => transformKeys(item, keyTransformer, parseDates));
143
+ }
144
+ if (parseDates && isISODateString(obj)) {
145
+ return new Date(obj);
146
+ }
147
+ if (typeof obj === "object") {
148
+ const transformed = {};
149
+ for (const [key, value] of Object.entries(obj)) {
150
+ transformed[keyTransformer(key)] = transformKeys(value, keyTransformer, parseDates);
151
+ }
152
+ return transformed;
153
+ }
154
+ return obj;
155
+ }
156
+ function toSnakeCase(obj) {
157
+ return transformKeys(obj, camelToSnake, false);
158
+ }
159
+ function toCamelCase(obj) {
160
+ return transformKeys(obj, snakeToCamel, true);
161
+ }
162
+ function buildSearchParams(params) {
163
+ const searchParams = new URLSearchParams();
164
+ for (const [key, value] of Object.entries(params)) {
165
+ if (value !== void 0 && value !== null) {
166
+ searchParams.append(camelToSnake(key), String(value));
167
+ }
168
+ }
169
+ return searchParams;
170
+ }
171
+
172
+ // src/http.ts
173
+ init_errors();
174
+ var HTTPClient = class {
175
+ /**
176
+ * Create a new HTTP client.
177
+ *
178
+ * @param baseUrl - Base URL for all API requests (without trailing slash)
179
+ * @param timeout - Default timeout in milliseconds (default: 30000)
180
+ */
181
+ constructor(baseUrl, timeout = 3e4) {
182
+ this.baseUrl = baseUrl;
183
+ this.timeout = timeout;
184
+ }
185
+ accessToken = null;
186
+ refreshToken = null;
187
+ isRefreshing = false;
188
+ refreshPromise = null;
189
+ /**
190
+ * Set authentication tokens.
191
+ */
192
+ setTokens(access, refresh) {
193
+ this.accessToken = access;
194
+ this.refreshToken = refresh;
195
+ }
196
+ /**
197
+ * Get current authentication tokens.
198
+ */
199
+ getTokens() {
200
+ if (!this.accessToken || !this.refreshToken) {
201
+ return null;
202
+ }
203
+ return {
204
+ accessToken: this.accessToken,
205
+ refreshToken: this.refreshToken,
206
+ tokenType: "bearer"
207
+ };
208
+ }
209
+ /**
210
+ * Clear authentication tokens.
211
+ */
212
+ clearTokens() {
213
+ this.accessToken = null;
214
+ this.refreshToken = null;
215
+ }
216
+ /**
217
+ * Check if the client has valid tokens.
218
+ */
219
+ hasTokens() {
220
+ return this.accessToken !== null;
221
+ }
222
+ /**
223
+ * Make a GET request.
224
+ */
225
+ async get(path, params, options) {
226
+ return this.request("GET", path, { ...options, params });
227
+ }
228
+ /**
229
+ * Make a POST request.
230
+ */
231
+ async post(path, body, options) {
232
+ return this.request("POST", path, { ...options, body });
233
+ }
234
+ /**
235
+ * Make a PUT request.
236
+ */
237
+ async put(path, body, options) {
238
+ return this.request("PUT", path, { ...options, body });
239
+ }
240
+ /**
241
+ * Make a PATCH request.
242
+ */
243
+ async patch(path, body, options) {
244
+ return this.request("PATCH", path, { ...options, body });
245
+ }
246
+ /**
247
+ * Make a DELETE request.
248
+ */
249
+ async delete(path, options) {
250
+ return this.request("DELETE", path, options);
251
+ }
252
+ /**
253
+ * Make an HTTP request with automatic retry on 401.
254
+ */
255
+ async request(method, path, options = {}) {
256
+ const { includeAuth = true, isFormData = false, timeout, body, params } = options;
257
+ let url = `${this.baseUrl}${path}`;
258
+ if (params) {
259
+ const searchParams = buildSearchParams(params);
260
+ const queryString = searchParams.toString();
261
+ if (queryString) {
262
+ url += `?${queryString}`;
263
+ }
264
+ }
265
+ const headers = {};
266
+ if (includeAuth && this.accessToken) {
267
+ headers["Authorization"] = `Bearer ${this.accessToken}`;
268
+ }
269
+ let requestBody;
270
+ if (body !== void 0) {
271
+ if (isFormData) {
272
+ headers["Content-Type"] = "application/x-www-form-urlencoded";
273
+ const formData = new URLSearchParams();
274
+ for (const [key, value] of Object.entries(body)) {
275
+ if (value !== void 0 && value !== null) {
276
+ formData.append(key, String(value));
277
+ }
278
+ }
279
+ requestBody = formData.toString();
280
+ } else {
281
+ headers["Content-Type"] = "application/json";
282
+ requestBody = JSON.stringify(toSnakeCase(body));
283
+ }
284
+ }
285
+ const controller = new AbortController();
286
+ const timeoutId = setTimeout(() => controller.abort(), timeout ?? this.timeout);
287
+ try {
288
+ const response = await fetch(url, {
289
+ method,
290
+ headers,
291
+ body: requestBody,
292
+ signal: controller.signal
293
+ });
294
+ clearTimeout(timeoutId);
295
+ if (response.status === 401 && includeAuth && this.refreshToken) {
296
+ await this.attemptTokenRefresh();
297
+ return this.request(method, path, {
298
+ ...options,
299
+ // Mark that we shouldn't retry again to prevent infinite loops
300
+ includeAuth: true
301
+ });
302
+ }
303
+ return await this.handleResponse(response);
304
+ } catch (error) {
305
+ clearTimeout(timeoutId);
306
+ if (error instanceof SyftHubError) {
307
+ throw error;
308
+ }
309
+ if (error instanceof Error) {
310
+ if (error.name === "AbortError") {
311
+ throw new NetworkError("Request timed out", error);
312
+ }
313
+ throw new NetworkError(error.message, error);
314
+ }
315
+ throw new NetworkError("Unknown network error");
316
+ }
317
+ }
318
+ /**
319
+ * Handle the HTTP response and convert to the expected type.
320
+ */
321
+ async handleResponse(response) {
322
+ let data;
323
+ const contentType = response.headers.get("content-type");
324
+ if (contentType?.includes("application/json")) {
325
+ try {
326
+ data = await response.json();
327
+ } catch {
328
+ data = null;
329
+ }
330
+ } else {
331
+ const text = await response.text();
332
+ data = text || null;
333
+ }
334
+ if (!response.ok) {
335
+ this.handleErrorResponse(response.status, data);
336
+ }
337
+ return toCamelCase(data);
338
+ }
339
+ /**
340
+ * Handle error responses by throwing appropriate exceptions.
341
+ */
342
+ handleErrorResponse(status, data) {
343
+ const message = this.extractErrorMessage(data);
344
+ const { code, detail } = this.extractErrorCodeAndDetail(data);
345
+ if (code) {
346
+ switch (code) {
347
+ // User registration errors
348
+ case "USER_ALREADY_EXISTS":
349
+ throw new UserAlreadyExistsError(message, detail);
350
+ // Accounting-related errors
351
+ case "ACCOUNTING_ACCOUNT_EXISTS":
352
+ throw new AccountingAccountExistsError(message, detail);
353
+ case "INVALID_ACCOUNTING_PASSWORD":
354
+ throw new InvalidAccountingPasswordError(message, detail);
355
+ case "ACCOUNTING_SERVICE_UNAVAILABLE":
356
+ throw new AccountingServiceUnavailableError(message, detail);
357
+ }
358
+ }
359
+ switch (status) {
360
+ case 401:
361
+ throw new AuthenticationError(message);
362
+ case 403:
363
+ throw new AuthorizationError(message);
364
+ case 404:
365
+ throw new NotFoundError(message);
366
+ case 422:
367
+ throw new ValidationError(message, this.extractValidationErrors(data));
368
+ default:
369
+ throw new APIError(message, status, data);
370
+ }
371
+ }
372
+ /**
373
+ * Extract error code and detail from API response.
374
+ * Used for accounting-specific error handling.
375
+ */
376
+ extractErrorCodeAndDetail(data) {
377
+ if (!data || typeof data !== "object") {
378
+ return {};
379
+ }
380
+ if ("detail" in data) {
381
+ const detail = data.detail;
382
+ if (detail && typeof detail === "object" && "code" in detail) {
383
+ const innerDetail = detail;
384
+ return {
385
+ code: innerDetail.code,
386
+ detail
387
+ };
388
+ }
389
+ }
390
+ return {};
391
+ }
392
+ /**
393
+ * Extract error message from API response.
394
+ */
395
+ extractErrorMessage(data) {
396
+ if (typeof data === "string") {
397
+ return data;
398
+ }
399
+ if (data && typeof data === "object") {
400
+ if ("detail" in data) {
401
+ const detail = data.detail;
402
+ if (typeof detail === "string") {
403
+ return detail;
404
+ }
405
+ if (Array.isArray(detail) && detail.length > 0) {
406
+ const firstError = detail[0];
407
+ if (firstError?.msg) {
408
+ return firstError.msg;
409
+ }
410
+ }
411
+ }
412
+ if ("message" in data && typeof data.message === "string") {
413
+ return data.message;
414
+ }
415
+ if ("error" in data && typeof data.error === "string") {
416
+ return data.error;
417
+ }
418
+ }
419
+ return "An error occurred";
420
+ }
421
+ /**
422
+ * Extract field-level validation errors from API response.
423
+ */
424
+ extractValidationErrors(data) {
425
+ if (!data || typeof data !== "object" || !("detail" in data)) {
426
+ return void 0;
427
+ }
428
+ const detail = data.detail;
429
+ if (!Array.isArray(detail)) {
430
+ return void 0;
431
+ }
432
+ const errors = {};
433
+ for (const error of detail) {
434
+ if (typeof error === "object" && error !== null && "loc" in error && "msg" in error) {
435
+ const { loc, msg } = error;
436
+ const field = String(loc[loc.length - 1] ?? "unknown");
437
+ if (!errors[field]) {
438
+ errors[field] = [];
439
+ }
440
+ errors[field].push(msg);
441
+ }
442
+ }
443
+ return Object.keys(errors).length > 0 ? errors : void 0;
444
+ }
445
+ /**
446
+ * Attempt to refresh the access token using the refresh token.
447
+ */
448
+ async attemptTokenRefresh() {
449
+ if (this.isRefreshing && this.refreshPromise) {
450
+ await this.refreshPromise;
451
+ return;
452
+ }
453
+ this.isRefreshing = true;
454
+ this.refreshPromise = (async () => {
455
+ try {
456
+ const response = await fetch(`${this.baseUrl}/api/v1/auth/refresh`, {
457
+ method: "POST",
458
+ headers: {
459
+ "Content-Type": "application/json"
460
+ },
461
+ body: JSON.stringify({ refresh_token: this.refreshToken })
462
+ });
463
+ if (!response.ok) {
464
+ this.clearTokens();
465
+ throw new AuthenticationError("Token refresh failed");
466
+ }
467
+ const data = await response.json();
468
+ this.accessToken = data.access_token;
469
+ this.refreshToken = data.refresh_token;
470
+ } finally {
471
+ this.isRefreshing = false;
472
+ this.refreshPromise = null;
473
+ }
474
+ })();
475
+ await this.refreshPromise;
476
+ }
477
+ };
478
+
479
+ // src/client.ts
480
+ init_errors();
481
+
482
+ // src/resources/auth.ts
483
+ init_errors();
484
+ var AuthResource = class {
485
+ constructor(http) {
486
+ this.http = http;
487
+ }
488
+ /**
489
+ * Register a new user account.
490
+ *
491
+ * If an accounting service URL is configured (via `accountingServiceUrl` or server default),
492
+ * the backend will handle accounting integration using a "try-create-first" approach:
493
+ *
494
+ * **Accounting Password Behavior:**
495
+ * - **Not provided**: A secure password is auto-generated and a new accounting account is created.
496
+ * - **Provided (new user)**: The account is created with your chosen password.
497
+ * - **Provided (existing user)**: Your password is validated and accounts are linked.
498
+ *
499
+ * This means you can set your own accounting password during registration even if you're
500
+ * a new user - you don't need an existing accounting account first.
501
+ *
502
+ * @param input - Registration details (username, email, password, fullName)
503
+ * @returns The created User
504
+ * @throws {ValidationError} If input validation fails
505
+ * @throws {UserAlreadyExistsError} If username or email already exists in SyftHub
506
+ * @throws {AccountingAccountExistsError} If email already exists in accounting service
507
+ * and no `accountingPassword` was provided. Retry with the password.
508
+ * @throws {InvalidAccountingPasswordError} If the provided accounting password doesn't
509
+ * match an existing accounting account
510
+ * @throws {AccountingServiceUnavailableError} If the accounting service is unreachable
511
+ *
512
+ * @example
513
+ * // Basic registration (auto-generated accounting password)
514
+ * const user = await client.auth.register({
515
+ * username: 'alice',
516
+ * email: 'alice@example.com',
517
+ * password: 'SecurePass123!',
518
+ * fullName: 'Alice'
519
+ * });
520
+ *
521
+ * @example
522
+ * // Registration with custom accounting password (NEW user)
523
+ * const user = await client.auth.register({
524
+ * username: 'bob',
525
+ * email: 'bob@example.com',
526
+ * password: 'SecurePass123!',
527
+ * fullName: 'Bob',
528
+ * accountingPassword: 'MyChosenAccountingPass!' // Creates account with this password
529
+ * });
530
+ *
531
+ * @example
532
+ * // Handle existing accounting account
533
+ * try {
534
+ * await client.auth.register({ username, email, password, fullName });
535
+ * } catch (error) {
536
+ * if (error instanceof AccountingAccountExistsError) {
537
+ * // Prompt user for their existing accounting password
538
+ * const accountingPassword = await promptUser('Enter your existing accounting password:');
539
+ * await client.auth.register({ username, email, password, fullName, accountingPassword });
540
+ * } else {
541
+ * throw error;
542
+ * }
543
+ * }
544
+ */
545
+ async register(input) {
546
+ const response = await this.http.post("/api/v1/auth/register", input, {
547
+ includeAuth: false
548
+ });
549
+ this.http.setTokens(response.accessToken, response.refreshToken);
550
+ return response.user;
551
+ }
552
+ /**
553
+ * Login with username/email and password.
554
+ *
555
+ * Uses OAuth2 password flow (form-urlencoded body).
556
+ *
557
+ * @param username - Username or email
558
+ * @param password - Password
559
+ * @returns The authenticated User
560
+ * @throws {AuthenticationError} If credentials are invalid
561
+ */
562
+ async login(username, password) {
563
+ const response = await this.http.post(
564
+ "/api/v1/auth/login",
565
+ { username, password },
566
+ {
567
+ includeAuth: false,
568
+ isFormData: true
569
+ }
570
+ );
571
+ this.http.setTokens(response.accessToken, response.refreshToken);
572
+ return response.user;
573
+ }
574
+ /**
575
+ * Logout the current user.
576
+ *
577
+ * Invalidates tokens on the server and clears local token storage.
578
+ */
579
+ async logout() {
580
+ try {
581
+ await this.http.post("/api/v1/auth/logout");
582
+ } finally {
583
+ this.http.clearTokens();
584
+ }
585
+ }
586
+ /**
587
+ * Get the current authenticated user.
588
+ *
589
+ * @returns The current User
590
+ * @throws {AuthenticationError} If not authenticated
591
+ */
592
+ async me() {
593
+ return this.http.get("/api/v1/auth/me");
594
+ }
595
+ /**
596
+ * Manually refresh the access token.
597
+ *
598
+ * This is normally handled automatically when a request returns 401.
599
+ *
600
+ * @throws {AuthenticationError} If refresh token is invalid or expired
601
+ */
602
+ async refresh() {
603
+ const tokens = this.http.getTokens();
604
+ if (!tokens) {
605
+ throw new AuthenticationError("No refresh token available");
606
+ }
607
+ const response = await this.http.post(
608
+ "/api/v1/auth/refresh",
609
+ { refreshToken: tokens.refreshToken },
610
+ { includeAuth: false }
611
+ );
612
+ this.http.setTokens(response.accessToken, response.refreshToken);
613
+ }
614
+ /**
615
+ * Change the current user's password.
616
+ *
617
+ * @param currentPassword - Current password for verification
618
+ * @param newPassword - New password to set
619
+ * @throws {AuthenticationError} If current password is incorrect
620
+ * @throws {ValidationError} If new password doesn't meet requirements
621
+ */
622
+ async changePassword(currentPassword, newPassword) {
623
+ await this.http.put("/api/v1/auth/me/password", {
624
+ currentPassword,
625
+ newPassword
626
+ });
627
+ }
628
+ /**
629
+ * Get a satellite token for a specific audience (target service).
630
+ *
631
+ * Satellite tokens are short-lived, RS256-signed JWTs that allow satellite
632
+ * services (like SyftAI-Space) to verify user identity without calling
633
+ * SyftHub for every request.
634
+ *
635
+ * @param audience - Target service identifier (username of the service owner)
636
+ * @returns Satellite token response with token and expiry
637
+ * @throws {AuthenticationError} If not authenticated
638
+ * @throws {ValidationError} If audience is invalid or inactive
639
+ *
640
+ * @example
641
+ * // Get a token for querying alice's SyftAI-Space endpoints
642
+ * const tokenResponse = await client.auth.getSatelliteToken('alice');
643
+ * console.log(`Token expires in ${tokenResponse.expiresIn} seconds`);
644
+ */
645
+ async getSatelliteToken(audience) {
646
+ return this.http.get("/api/v1/token", { aud: audience });
647
+ }
648
+ /**
649
+ * Get satellite tokens for multiple audiences in parallel.
650
+ *
651
+ * This is useful when making requests to endpoints owned by different users.
652
+ * Tokens are cached and reused where possible.
653
+ *
654
+ * @param audiences - Array of unique audience identifiers (usernames)
655
+ * @returns Map of audience to satellite token
656
+ * @throws {AuthenticationError} If not authenticated
657
+ *
658
+ * @example
659
+ * // Get tokens for multiple endpoint owners
660
+ * const tokens = await client.auth.getSatelliteTokens(['alice', 'bob']);
661
+ * console.log(`Got ${tokens.size} tokens`);
662
+ */
663
+ async getSatelliteTokens(audiences) {
664
+ const uniqueAudiences = [...new Set(audiences)];
665
+ const tokenMap = /* @__PURE__ */ new Map();
666
+ const results = await Promise.allSettled(
667
+ uniqueAudiences.map(async (aud) => {
668
+ const response = await this.getSatelliteToken(aud);
669
+ return { audience: aud, token: response.targetToken };
670
+ })
671
+ );
672
+ for (const result of results) {
673
+ if (result.status === "fulfilled") {
674
+ tokenMap.set(result.value.audience, result.value.token);
675
+ }
676
+ }
677
+ return tokenMap;
678
+ }
679
+ /**
680
+ * Get transaction tokens for multiple endpoint owners.
681
+ *
682
+ * Transaction tokens are short-lived JWTs that pre-authorize the endpoint owner
683
+ * (recipient) to charge the current user (sender) for usage. These tokens are
684
+ * created via the accounting service and passed to the aggregator.
685
+ *
686
+ * This is used by the chat flow to enable billing for endpoint usage.
687
+ *
688
+ * @param ownerUsernames - Array of endpoint owner usernames
689
+ * @returns TransactionTokensResponse with tokens map and any errors
690
+ * @throws {AuthenticationError} If not authenticated
691
+ *
692
+ * @example
693
+ * // Get transaction tokens for endpoint owners
694
+ * const response = await client.auth.getTransactionTokens(['alice', 'bob']);
695
+ * console.log(`Got ${Object.keys(response.tokens).length} tokens`);
696
+ * if (Object.keys(response.errors).length > 0) {
697
+ * console.log('Some tokens failed:', response.errors);
698
+ * }
699
+ */
700
+ async getTransactionTokens(ownerUsernames) {
701
+ const uniqueOwners = [...new Set(ownerUsernames)];
702
+ if (uniqueOwners.length === 0) {
703
+ return { tokens: {}, errors: {} };
704
+ }
705
+ try {
706
+ return await this.http.post(
707
+ "/api/v1/accounting/transaction-tokens",
708
+ { owner_usernames: uniqueOwners }
709
+ );
710
+ } catch (error) {
711
+ console.warn("Failed to get transaction tokens:", error);
712
+ return { tokens: {}, errors: {} };
713
+ }
714
+ }
715
+ };
716
+
717
+ // src/resources/users.ts
718
+ var UsersResource = class {
719
+ constructor(http) {
720
+ this.http = http;
721
+ }
722
+ /**
723
+ * Update the current user's profile.
724
+ *
725
+ * Only provided fields will be updated.
726
+ *
727
+ * @param input - Fields to update
728
+ * @returns The updated User
729
+ * @throws {AuthenticationError} If not authenticated
730
+ * @throws {ValidationError} If input validation fails
731
+ */
732
+ async update(input) {
733
+ return this.http.put("/api/v1/users/me", input);
734
+ }
735
+ /**
736
+ * Check if a username is available.
737
+ *
738
+ * @param username - Username to check
739
+ * @returns True if the username is available
740
+ */
741
+ async checkUsername(username) {
742
+ const response = await this.http.get(
743
+ `/api/v1/users/check-username/${encodeURIComponent(username)}`,
744
+ void 0,
745
+ { includeAuth: false }
746
+ );
747
+ return response.available;
748
+ }
749
+ /**
750
+ * Check if an email is available.
751
+ *
752
+ * @param email - Email to check
753
+ * @returns True if the email is available
754
+ */
755
+ async checkEmail(email) {
756
+ const response = await this.http.get(
757
+ `/api/v1/users/check-email/${encodeURIComponent(email)}`,
758
+ void 0,
759
+ { includeAuth: false }
760
+ );
761
+ return response.available;
762
+ }
763
+ /**
764
+ * Get the current user's accounting service credentials.
765
+ *
766
+ * Returns credentials stored in SyftHub for connecting to an external
767
+ * accounting service. The email is always the same as the user's SyftHub email.
768
+ *
769
+ * @returns Accounting credentials (url and password may be null if not configured)
770
+ * @throws {AuthenticationError} If not authenticated
771
+ *
772
+ * @example
773
+ * const credentials = await client.users.getAccountingCredentials();
774
+ * if (credentials.url && credentials.password) {
775
+ * // Use credentials to connect to accounting service
776
+ * }
777
+ */
778
+ async getAccountingCredentials() {
779
+ return this.http.get("/api/v1/users/me/accounting");
780
+ }
781
+ };
782
+
783
+ // src/pagination.ts
784
+ var PageIterator = class {
785
+ /**
786
+ * Create a new PageIterator.
787
+ *
788
+ * @param fetcher - Function that fetches a page of items given skip and limit
789
+ * @param pageSize - Number of items to fetch per page (default: 20)
790
+ */
791
+ constructor(fetcher, pageSize = 20) {
792
+ this.fetcher = fetcher;
793
+ this.pageSize = pageSize;
794
+ }
795
+ items = [];
796
+ index = 0;
797
+ skip = 0;
798
+ exhausted = false;
799
+ initialized = false;
800
+ /**
801
+ * Async iterator implementation for `for await...of` loops.
802
+ */
803
+ async *[Symbol.asyncIterator]() {
804
+ while (true) {
805
+ if (this.index >= this.items.length) {
806
+ if (this.exhausted) break;
807
+ await this.fetchNextPage();
808
+ if (this.items.length === 0) break;
809
+ }
810
+ const item = this.items[this.index];
811
+ if (item === void 0) break;
812
+ this.index++;
813
+ yield item;
814
+ }
815
+ }
816
+ /**
817
+ * Get just the first page of results.
818
+ *
819
+ * @returns Promise resolving to the first page of items
820
+ */
821
+ async firstPage() {
822
+ if (!this.initialized) {
823
+ await this.fetchNextPage();
824
+ }
825
+ return [...this.items];
826
+ }
827
+ /**
828
+ * Get all items across all pages.
829
+ *
830
+ * Warning: This loads all items into memory. For large datasets,
831
+ * consider iterating with `for await...of` instead.
832
+ *
833
+ * @returns Promise resolving to all items
834
+ */
835
+ async all() {
836
+ const results = [];
837
+ for await (const item of this) {
838
+ results.push(item);
839
+ }
840
+ return results;
841
+ }
842
+ /**
843
+ * Get the first N items.
844
+ *
845
+ * @param n - Maximum number of items to return
846
+ * @returns Promise resolving to up to N items
847
+ */
848
+ async take(n) {
849
+ const results = [];
850
+ for await (const item of this) {
851
+ results.push(item);
852
+ if (results.length >= n) break;
853
+ }
854
+ return results;
855
+ }
856
+ /**
857
+ * Fetch the next page of items from the API.
858
+ */
859
+ async fetchNextPage() {
860
+ const page = await this.fetcher(this.skip, this.pageSize);
861
+ this.items = page;
862
+ this.index = 0;
863
+ this.skip += this.pageSize;
864
+ this.exhausted = page.length < this.pageSize;
865
+ this.initialized = true;
866
+ }
867
+ };
868
+
869
+ // src/resources/my-endpoints.ts
870
+ var MyEndpointsResource = class {
871
+ constructor(http) {
872
+ this.http = http;
873
+ }
874
+ /**
875
+ * Parse an endpoint path into owner and slug.
876
+ *
877
+ * @param path - Path in "owner/slug" format
878
+ * @returns Tuple of [owner, slug]
879
+ * @throws {Error} If path format is invalid
880
+ */
881
+ parsePath(path) {
882
+ const parts = path.replace(/^\/|\/$/g, "").split("/");
883
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
884
+ throw new Error(`Invalid endpoint path: '${path}'. Expected format: 'owner/slug'`);
885
+ }
886
+ return [parts[0], parts[1]];
887
+ }
888
+ /**
889
+ * Resolve an endpoint path to its ID.
890
+ *
891
+ * @param path - Endpoint path in "owner/slug" format
892
+ * @returns The endpoint ID
893
+ */
894
+ async resolveEndpointId(path) {
895
+ const [owner, slug] = this.parsePath(path);
896
+ const response = await this.http.get(`/${owner}/${slug}`);
897
+ if (response.id === void 0) {
898
+ throw new Error(
899
+ `Could not resolve endpoint ID for '${path}'. Make sure you own this endpoint.`
900
+ );
901
+ }
902
+ return response.id;
903
+ }
904
+ /**
905
+ * List the current user's endpoints.
906
+ *
907
+ * @param options - Filtering and pagination options
908
+ * @returns PageIterator that lazily fetches endpoints
909
+ * @throws {AuthenticationError} If not authenticated
910
+ */
911
+ list(options) {
912
+ const pageSize = options?.pageSize ?? 20;
913
+ return new PageIterator(async (skip, limit) => {
914
+ const params = { skip, limit };
915
+ if (options?.visibility) {
916
+ params["visibility"] = options.visibility;
917
+ }
918
+ return this.http.get("/api/v1/endpoints", params);
919
+ }, pageSize);
920
+ }
921
+ /**
922
+ * Create a new endpoint.
923
+ *
924
+ * @param input - Endpoint creation details
925
+ * @param organizationId - Optional organization ID (for org-owned endpoints)
926
+ * @returns The created Endpoint
927
+ * @throws {AuthenticationError} If not authenticated
928
+ * @throws {ValidationError} If input validation fails
929
+ */
930
+ async create(input, organizationId) {
931
+ const body = organizationId !== void 0 ? { ...input, organizationId } : input;
932
+ return this.http.post("/api/v1/endpoints", body);
933
+ }
934
+ /**
935
+ * Get a specific endpoint by path.
936
+ *
937
+ * @param path - Endpoint path in "owner/slug" format (e.g., "alice/my-api")
938
+ * @returns The Endpoint
939
+ * @throws {AuthenticationError} If not authenticated
940
+ * @throws {NotFoundError} If endpoint not found
941
+ * @throws {AuthorizationError} If not authorized to view
942
+ */
943
+ async get(path) {
944
+ const [owner, slug] = this.parsePath(path);
945
+ return this.http.get(`/${owner}/${slug}`);
946
+ }
947
+ /**
948
+ * Update an endpoint.
949
+ *
950
+ * Only provided fields will be updated.
951
+ *
952
+ * @param path - Endpoint path in "owner/slug" format
953
+ * @param input - Fields to update
954
+ * @returns The updated Endpoint
955
+ * @throws {AuthenticationError} If not authenticated
956
+ * @throws {NotFoundError} If endpoint not found
957
+ * @throws {AuthorizationError} If not owner/admin
958
+ */
959
+ async update(path, input) {
960
+ const endpointId = await this.resolveEndpointId(path);
961
+ return this.http.patch(`/api/v1/endpoints/${endpointId}`, input);
962
+ }
963
+ /**
964
+ * Delete an endpoint.
965
+ *
966
+ * @param path - Endpoint path in "owner/slug" format
967
+ * @throws {AuthenticationError} If not authenticated
968
+ * @throws {NotFoundError} If endpoint not found
969
+ * @throws {AuthorizationError} If not owner/admin
970
+ */
971
+ async delete(path) {
972
+ const endpointId = await this.resolveEndpointId(path);
973
+ await this.http.delete(`/api/v1/endpoints/${endpointId}`);
974
+ }
975
+ };
976
+
977
+ // src/resources/hub.ts
978
+ var HubResource = class {
979
+ constructor(http) {
980
+ this.http = http;
981
+ }
982
+ /**
983
+ * Parse an endpoint path into owner and slug.
984
+ *
985
+ * @param path - Path in "owner/slug" format
986
+ * @returns Tuple of [owner, slug]
987
+ */
988
+ parsePath(path) {
989
+ const parts = path.replace(/^\/|\/$/g, "").split("/");
990
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
991
+ throw new Error(`Invalid endpoint path: '${path}'. Expected format: 'owner/slug'`);
992
+ }
993
+ return [parts[0], parts[1]];
994
+ }
995
+ /**
996
+ * Resolve an endpoint path to its ID.
997
+ *
998
+ * This searches the user's own endpoints to find the ID.
999
+ *
1000
+ * @param path - Endpoint path in "owner/slug" format
1001
+ * @returns The endpoint ID
1002
+ */
1003
+ async resolveEndpointId(path) {
1004
+ const [, slug] = this.parsePath(path);
1005
+ const endpoints = await this.http.get(
1006
+ "/api/v1/endpoints",
1007
+ { limit: 100 }
1008
+ );
1009
+ for (const ep of endpoints) {
1010
+ if (ep.slug === slug && ep.id !== void 0) {
1011
+ return ep.id;
1012
+ }
1013
+ }
1014
+ const { NotFoundError: NotFoundError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
1015
+ throw new NotFoundError2(
1016
+ `Could not resolve endpoint ID for '${path}'. Endpoint not found or you don't have access to get its ID.`
1017
+ );
1018
+ }
1019
+ /**
1020
+ * Browse all public endpoints.
1021
+ *
1022
+ * @param options - Pagination options
1023
+ * @returns PageIterator that lazily fetches endpoints
1024
+ */
1025
+ browse(options) {
1026
+ const pageSize = options?.pageSize ?? 20;
1027
+ return new PageIterator(async (skip, limit) => {
1028
+ return this.http.get(
1029
+ "/api/v1/endpoints/public",
1030
+ { skip, limit },
1031
+ { includeAuth: false }
1032
+ );
1033
+ }, pageSize);
1034
+ }
1035
+ /**
1036
+ * Get trending endpoints sorted by stars.
1037
+ *
1038
+ * @param options - Filter and pagination options
1039
+ * @returns PageIterator that lazily fetches endpoints
1040
+ */
1041
+ trending(options) {
1042
+ const pageSize = options?.pageSize ?? 20;
1043
+ return new PageIterator(async (skip, limit) => {
1044
+ const params = { skip, limit };
1045
+ if (options?.minStars !== void 0) {
1046
+ params["minStars"] = options.minStars;
1047
+ }
1048
+ return this.http.get(
1049
+ "/api/v1/endpoints/trending",
1050
+ params,
1051
+ { includeAuth: false }
1052
+ );
1053
+ }, pageSize);
1054
+ }
1055
+ /**
1056
+ * Get an endpoint by its path.
1057
+ *
1058
+ * This method searches the public endpoints API to find the endpoint,
1059
+ * which works reliably across all deployment configurations.
1060
+ *
1061
+ * @param path - Endpoint path in "owner/slug" format (e.g., "alice/cool-api")
1062
+ * @returns The EndpointPublic
1063
+ * @throws {NotFoundError} If endpoint not found
1064
+ */
1065
+ async get(path) {
1066
+ const [owner, slug] = this.parsePath(path);
1067
+ for await (const endpoint of this.browse({ pageSize: 100 })) {
1068
+ if (endpoint.ownerUsername === owner && endpoint.slug === slug) {
1069
+ return endpoint;
1070
+ }
1071
+ }
1072
+ const { NotFoundError: NotFoundError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
1073
+ throw new NotFoundError2(
1074
+ `Endpoint not found: '${path}'. No public endpoint found with owner '${owner}' and slug '${slug}'.`
1075
+ );
1076
+ }
1077
+ /**
1078
+ * Star an endpoint.
1079
+ *
1080
+ * @param path - Endpoint path in "owner/slug" format
1081
+ * @throws {AuthenticationError} If not authenticated
1082
+ * @throws {NotFoundError} If endpoint not found
1083
+ */
1084
+ async star(path) {
1085
+ const endpointId = await this.resolveEndpointId(path);
1086
+ await this.http.patch(`/api/v1/endpoints/${endpointId}/star`);
1087
+ }
1088
+ /**
1089
+ * Unstar an endpoint.
1090
+ *
1091
+ * @param path - Endpoint path in "owner/slug" format
1092
+ * @throws {AuthenticationError} If not authenticated
1093
+ * @throws {NotFoundError} If endpoint not found
1094
+ */
1095
+ async unstar(path) {
1096
+ const endpointId = await this.resolveEndpointId(path);
1097
+ await this.http.patch(`/api/v1/endpoints/${endpointId}/unstar`);
1098
+ }
1099
+ /**
1100
+ * Check if you have starred an endpoint.
1101
+ *
1102
+ * @param path - Endpoint path in "owner/slug" format
1103
+ * @returns True if starred, False otherwise
1104
+ * @throws {AuthenticationError} If not authenticated
1105
+ * @throws {NotFoundError} If endpoint not found
1106
+ */
1107
+ async isStarred(path) {
1108
+ const endpointId = await this.resolveEndpointId(path);
1109
+ const response = await this.http.get(
1110
+ `/api/v1/endpoints/${endpointId}/starred`
1111
+ );
1112
+ return response.starred ?? false;
1113
+ }
1114
+ };
1115
+
1116
+ // src/models/common.ts
1117
+ var Visibility = {
1118
+ /** Visible to everyone, no authentication required */
1119
+ PUBLIC: "public",
1120
+ /** Only visible to the owner and collaborators */
1121
+ PRIVATE: "private",
1122
+ /** Visible to authenticated users within the organization */
1123
+ INTERNAL: "internal"
1124
+ };
1125
+ var EndpointType = {
1126
+ /** Machine learning model endpoint */
1127
+ MODEL: "model",
1128
+ /** Data source endpoint */
1129
+ DATA_SOURCE: "data_source"
1130
+ };
1131
+ var UserRole = {
1132
+ /** Administrator with full access */
1133
+ ADMIN: "admin",
1134
+ /** Regular user */
1135
+ USER: "user",
1136
+ /** Guest user with limited access */
1137
+ GUEST: "guest"
1138
+ };
1139
+ var OrganizationRole = {
1140
+ /** Organization owner with full control */
1141
+ OWNER: "owner",
1142
+ /** Administrator with management privileges */
1143
+ ADMIN: "admin",
1144
+ /** Regular member */
1145
+ MEMBER: "member"
1146
+ };
1147
+
1148
+ // src/models/endpoint.ts
1149
+ function getEndpointOwnerType(endpoint) {
1150
+ return endpoint.organizationId !== null ? "organization" : "user";
1151
+ }
1152
+ function getEndpointPublicPath(endpoint) {
1153
+ return `${endpoint.ownerUsername}/${endpoint.slug}`;
1154
+ }
1155
+
1156
+ // src/models/accounting.ts
1157
+ var TransactionStatus = {
1158
+ /** Transaction created, awaiting confirmation */
1159
+ PENDING: "pending",
1160
+ /** Transaction confirmed, funds transferred */
1161
+ COMPLETED: "completed",
1162
+ /** Transaction cancelled, no funds transferred */
1163
+ CANCELLED: "cancelled"
1164
+ };
1165
+ var CreatorType = {
1166
+ /** System-initiated transaction */
1167
+ SYSTEM: "system",
1168
+ /** Sender-initiated transaction */
1169
+ SENDER: "sender",
1170
+ /** Recipient-initiated transaction (delegated) */
1171
+ RECIPIENT: "recipient"
1172
+ };
1173
+ function parseTransaction(response) {
1174
+ return {
1175
+ ...response,
1176
+ createdAt: new Date(response.createdAt),
1177
+ resolvedAt: response.resolvedAt ? new Date(response.resolvedAt) : null
1178
+ };
1179
+ }
1180
+ function isTransactionPending(tx) {
1181
+ return tx.status === TransactionStatus.PENDING;
1182
+ }
1183
+ function isTransactionCompleted(tx) {
1184
+ return tx.status === TransactionStatus.COMPLETED;
1185
+ }
1186
+ function isTransactionCancelled(tx) {
1187
+ return tx.status === TransactionStatus.CANCELLED;
1188
+ }
1189
+
1190
+ // src/resources/accounting.ts
1191
+ init_errors();
1192
+ async function handleResponseError(response) {
1193
+ if (response.ok) return;
1194
+ let detail;
1195
+ try {
1196
+ const body = await response.json();
1197
+ detail = body.detail ?? body.message ?? JSON.stringify(body);
1198
+ } catch {
1199
+ detail = await response.text() || `HTTP ${response.status}`;
1200
+ }
1201
+ switch (response.status) {
1202
+ case 401:
1203
+ throw new AuthenticationError(`Authentication failed: ${detail}`);
1204
+ case 403:
1205
+ throw new AuthorizationError(`Permission denied: ${detail}`);
1206
+ case 404:
1207
+ throw new NotFoundError(`Not found: ${detail}`);
1208
+ case 422:
1209
+ throw new ValidationError(`Validation error: ${detail}`);
1210
+ default:
1211
+ throw new APIError(`Accounting API error: ${detail}`, response.status);
1212
+ }
1213
+ }
1214
+ function createBasicAuth(email, password) {
1215
+ const credentials = `${email}:${password}`;
1216
+ const encoded = typeof btoa !== "undefined" ? btoa(credentials) : Buffer.from(credentials).toString("base64");
1217
+ return `Basic ${encoded}`;
1218
+ }
1219
+ var AccountingResource = class {
1220
+ baseUrl;
1221
+ email;
1222
+ password;
1223
+ timeout;
1224
+ authHeader;
1225
+ constructor(options) {
1226
+ this.baseUrl = options.url.replace(/\/$/, "");
1227
+ this.email = options.email;
1228
+ this.password = options.password;
1229
+ this.timeout = options.timeout ?? 3e4;
1230
+ this.authHeader = createBasicAuth(this.email, this.password);
1231
+ }
1232
+ // ===========================================================================
1233
+ // Private HTTP Methods
1234
+ // ===========================================================================
1235
+ /**
1236
+ * Make an authenticated request to the accounting service.
1237
+ */
1238
+ async request(method, path, options) {
1239
+ const url = new URL(path, this.baseUrl);
1240
+ if (options?.params) {
1241
+ for (const [key, value] of Object.entries(options.params)) {
1242
+ url.searchParams.set(key, String(value));
1243
+ }
1244
+ }
1245
+ const controller = new AbortController();
1246
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1247
+ try {
1248
+ const response = await fetch(url.toString(), {
1249
+ method,
1250
+ headers: {
1251
+ "Authorization": this.authHeader,
1252
+ "Content-Type": "application/json",
1253
+ "Accept": "application/json"
1254
+ },
1255
+ body: options?.body ? JSON.stringify(options.body) : void 0,
1256
+ signal: controller.signal
1257
+ });
1258
+ await handleResponseError(response);
1259
+ if (response.status === 204) {
1260
+ return {};
1261
+ }
1262
+ return await response.json();
1263
+ } catch (error) {
1264
+ if (error instanceof SyftHubError) {
1265
+ throw error;
1266
+ }
1267
+ if (error instanceof Error && error.name === "AbortError") {
1268
+ throw new APIError("Request timeout", 408);
1269
+ }
1270
+ throw new APIError(`Accounting request failed: ${error instanceof Error ? error.message : "Unknown error"}`, 0);
1271
+ } finally {
1272
+ clearTimeout(timeoutId);
1273
+ }
1274
+ }
1275
+ /**
1276
+ * Make a request using Bearer token auth (for delegated transactions).
1277
+ */
1278
+ async requestWithToken(method, path, token, options) {
1279
+ const url = new URL(path, this.baseUrl);
1280
+ const controller = new AbortController();
1281
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1282
+ try {
1283
+ const response = await fetch(url.toString(), {
1284
+ method,
1285
+ headers: {
1286
+ "Authorization": `Bearer ${token}`,
1287
+ "Content-Type": "application/json",
1288
+ "Accept": "application/json"
1289
+ },
1290
+ body: options?.body ? JSON.stringify(options.body) : void 0,
1291
+ signal: controller.signal
1292
+ });
1293
+ await handleResponseError(response);
1294
+ if (response.status === 204) {
1295
+ return {};
1296
+ }
1297
+ return await response.json();
1298
+ } catch (error) {
1299
+ if (error instanceof SyftHubError) {
1300
+ throw error;
1301
+ }
1302
+ if (error instanceof Error && error.name === "AbortError") {
1303
+ throw new APIError("Request timeout", 408);
1304
+ }
1305
+ throw new APIError(`Accounting request failed: ${error instanceof Error ? error.message : "Unknown error"}`, 0);
1306
+ } finally {
1307
+ clearTimeout(timeoutId);
1308
+ }
1309
+ }
1310
+ // ===========================================================================
1311
+ // User Operations
1312
+ // ===========================================================================
1313
+ /**
1314
+ * Get the current user's account information including balance.
1315
+ *
1316
+ * @returns AccountingUser with id, email, balance, and organization
1317
+ * @throws {AuthenticationError} If authentication fails
1318
+ * @throws {APIError} On other errors
1319
+ *
1320
+ * @example
1321
+ * ```typescript
1322
+ * const user = await accounting.getUser();
1323
+ * console.log(`Balance: ${user.balance}`);
1324
+ * console.log(`Organization: ${user.organization}`);
1325
+ * ```
1326
+ */
1327
+ async getUser() {
1328
+ return this.request("GET", "/user");
1329
+ }
1330
+ /**
1331
+ * Update the user's password.
1332
+ *
1333
+ * @param currentPassword - Current password for verification
1334
+ * @param newPassword - New password to set
1335
+ * @throws {AuthenticationError} If current password is wrong
1336
+ * @throws {ValidationError} If new password doesn't meet requirements
1337
+ *
1338
+ * @example
1339
+ * ```typescript
1340
+ * await accounting.updatePassword('old_secret', 'new_secret');
1341
+ * ```
1342
+ */
1343
+ async updatePassword(currentPassword, newPassword) {
1344
+ await this.request("PUT", "/user/password", {
1345
+ body: {
1346
+ oldPassword: currentPassword,
1347
+ newPassword
1348
+ }
1349
+ });
1350
+ }
1351
+ /**
1352
+ * Update the user's organization.
1353
+ *
1354
+ * @param organization - New organization name
1355
+ * @throws {AuthenticationError} If authentication fails
1356
+ *
1357
+ * @example
1358
+ * ```typescript
1359
+ * await accounting.updateOrganization('OpenMined');
1360
+ * ```
1361
+ */
1362
+ async updateOrganization(organization) {
1363
+ await this.request("PUT", "/user/organization", {
1364
+ body: { organization }
1365
+ });
1366
+ }
1367
+ // ===========================================================================
1368
+ // Transaction Listing
1369
+ // ===========================================================================
1370
+ /**
1371
+ * List account transactions with pagination.
1372
+ *
1373
+ * Returns a lazy iterator that fetches pages on demand.
1374
+ *
1375
+ * @param options - Pagination options
1376
+ * @returns PageIterator that yields Transaction objects
1377
+ *
1378
+ * @example
1379
+ * ```typescript
1380
+ * // Iterate through all transactions
1381
+ * for await (const tx of accounting.getTransactions()) {
1382
+ * console.log(`${tx.createdAt}: ${tx.amount} from ${tx.senderEmail}`);
1383
+ * }
1384
+ *
1385
+ * // Get first page only
1386
+ * const firstPage = await accounting.getTransactions().firstPage();
1387
+ *
1388
+ * // Get all transactions
1389
+ * const allTxs = await accounting.getTransactions().all();
1390
+ * ```
1391
+ */
1392
+ getTransactions(options) {
1393
+ const pageSize = options?.pageSize ?? 20;
1394
+ return new PageIterator(
1395
+ async (skip, limit) => {
1396
+ const response = await this.request("GET", "/transactions", {
1397
+ params: { skip, limit }
1398
+ });
1399
+ return response.map(parseTransaction);
1400
+ },
1401
+ pageSize
1402
+ );
1403
+ }
1404
+ /**
1405
+ * Get a specific transaction by ID.
1406
+ *
1407
+ * @param transactionId - The transaction ID
1408
+ * @returns Transaction object
1409
+ * @throws {NotFoundError} If transaction not found
1410
+ *
1411
+ * @example
1412
+ * ```typescript
1413
+ * const tx = await accounting.getTransaction('tx_123');
1414
+ * console.log(`Status: ${tx.status}`);
1415
+ * ```
1416
+ */
1417
+ async getTransaction(transactionId) {
1418
+ const response = await this.request(
1419
+ "GET",
1420
+ `/transactions/${transactionId}`
1421
+ );
1422
+ return parseTransaction(response);
1423
+ }
1424
+ // ===========================================================================
1425
+ // Direct Transaction Operations
1426
+ // ===========================================================================
1427
+ /**
1428
+ * Create a new transaction (direct transfer).
1429
+ *
1430
+ * Creates a PENDING transaction that must be confirmed or cancelled.
1431
+ * The transaction is created by the sender (current user).
1432
+ *
1433
+ * @param input - Transaction details
1434
+ * @returns Transaction in PENDING status
1435
+ * @throws {ValidationError} If amount <= 0 or insufficient balance
1436
+ *
1437
+ * @example
1438
+ * ```typescript
1439
+ * const tx = await accounting.createTransaction({
1440
+ * recipientEmail: 'bob@example.com',
1441
+ * amount: 10.0,
1442
+ * appName: 'syftai-space',
1443
+ * appEpPath: 'alice/my-model'
1444
+ * });
1445
+ * console.log(`Created transaction ${tx.id}: ${tx.status}`);
1446
+ *
1447
+ * // Later, confirm or cancel
1448
+ * await accounting.confirmTransaction(tx.id);
1449
+ * ```
1450
+ */
1451
+ async createTransaction(input) {
1452
+ if (input.amount <= 0) {
1453
+ throw new ValidationError("Amount must be greater than 0");
1454
+ }
1455
+ const response = await this.request("POST", "/transactions", {
1456
+ body: {
1457
+ recipientEmail: input.recipientEmail,
1458
+ amount: input.amount,
1459
+ ...input.appName && { appName: input.appName },
1460
+ ...input.appEpPath && { appEpPath: input.appEpPath }
1461
+ }
1462
+ });
1463
+ return parseTransaction(response);
1464
+ }
1465
+ /**
1466
+ * Confirm a pending transaction.
1467
+ *
1468
+ * Confirms the transaction, transferring funds from sender to recipient.
1469
+ * Can be called by either the sender or recipient.
1470
+ *
1471
+ * @param transactionId - The transaction ID to confirm
1472
+ * @returns Transaction in COMPLETED status
1473
+ * @throws {NotFoundError} If transaction not found
1474
+ * @throws {ValidationError} If transaction is not in PENDING status
1475
+ *
1476
+ * @example
1477
+ * ```typescript
1478
+ * const tx = await accounting.confirmTransaction('tx_123');
1479
+ * console.log(`Confirmed: ${tx.status}`); // "completed"
1480
+ * ```
1481
+ */
1482
+ async confirmTransaction(transactionId) {
1483
+ const response = await this.request(
1484
+ "POST",
1485
+ `/transactions/${transactionId}/confirm`
1486
+ );
1487
+ return parseTransaction(response);
1488
+ }
1489
+ /**
1490
+ * Cancel a pending transaction.
1491
+ *
1492
+ * Cancels the transaction without transferring funds.
1493
+ * Can be called by either the sender or recipient.
1494
+ *
1495
+ * @param transactionId - The transaction ID to cancel
1496
+ * @returns Transaction in CANCELLED status
1497
+ * @throws {NotFoundError} If transaction not found
1498
+ * @throws {ValidationError} If transaction is not in PENDING status
1499
+ *
1500
+ * @example
1501
+ * ```typescript
1502
+ * const tx = await accounting.cancelTransaction('tx_123');
1503
+ * console.log(`Cancelled: ${tx.status}`); // "cancelled"
1504
+ * ```
1505
+ */
1506
+ async cancelTransaction(transactionId) {
1507
+ const response = await this.request(
1508
+ "POST",
1509
+ `/transactions/${transactionId}/cancel`
1510
+ );
1511
+ return parseTransaction(response);
1512
+ }
1513
+ // ===========================================================================
1514
+ // Delegated Transaction Operations
1515
+ // ===========================================================================
1516
+ /**
1517
+ * Create a transaction token for delegated transfers.
1518
+ *
1519
+ * Creates a JWT token that authorizes the recipient to create a
1520
+ * transaction on behalf of the sender (current user). The token
1521
+ * is short-lived (typically ~5 minutes).
1522
+ *
1523
+ * Use this when you want to pre-authorize a payment that will be
1524
+ * initiated by the recipient (e.g., a service charging for usage).
1525
+ *
1526
+ * @param recipientEmail - Email of the authorized recipient
1527
+ * @returns JWT token string to share with recipient
1528
+ *
1529
+ * @example
1530
+ * ```typescript
1531
+ * // Sender creates token
1532
+ * const token = await accounting.createTransactionToken('service@example.com');
1533
+ *
1534
+ * // Share token with recipient out-of-band
1535
+ * // Recipient uses token to create delegated transaction
1536
+ * ```
1537
+ */
1538
+ async createTransactionToken(recipientEmail) {
1539
+ const response = await this.request("POST", "/token/create", {
1540
+ body: { recipientEmail }
1541
+ });
1542
+ return response.token;
1543
+ }
1544
+ /**
1545
+ * Create a delegated transaction using a pre-authorized token.
1546
+ *
1547
+ * Creates a transaction on behalf of the sender using their token.
1548
+ * This is typically used by services to charge users for usage.
1549
+ *
1550
+ * The token authenticates the request instead of Basic auth.
1551
+ *
1552
+ * @param senderEmail - Email of the sender who created the token
1553
+ * @param amount - Amount to transfer (must be > 0)
1554
+ * @param token - JWT token from sender's createTransactionToken()
1555
+ * @returns Transaction in PENDING status (createdBy=RECIPIENT)
1556
+ * @throws {AuthenticationError} If token is invalid or expired
1557
+ * @throws {ValidationError} If amount <= 0
1558
+ *
1559
+ * @example
1560
+ * ```typescript
1561
+ * // Recipient creates transaction using sender's token
1562
+ * const tx = await accounting.createDelegatedTransaction(
1563
+ * 'alice@example.com',
1564
+ * 5.0,
1565
+ * aliceToken
1566
+ * );
1567
+ *
1568
+ * // Recipient confirms the transaction
1569
+ * await accounting.confirmTransaction(tx.id);
1570
+ * ```
1571
+ */
1572
+ async createDelegatedTransaction(senderEmail, amount, token) {
1573
+ if (amount <= 0) {
1574
+ throw new ValidationError("Amount must be greater than 0");
1575
+ }
1576
+ const response = await this.requestWithToken(
1577
+ "POST",
1578
+ "/transactions",
1579
+ token,
1580
+ {
1581
+ body: {
1582
+ senderEmail,
1583
+ amount
1584
+ }
1585
+ }
1586
+ );
1587
+ return parseTransaction(response);
1588
+ }
1589
+ };
1590
+ function createAccountingResource(options) {
1591
+ return new AccountingResource(options);
1592
+ }
1593
+
1594
+ // src/resources/chat.ts
1595
+ init_errors();
1596
+ var AggregatorError = class extends SyftHubError {
1597
+ constructor(message, status, detail) {
1598
+ super(message);
1599
+ this.status = status;
1600
+ this.detail = detail;
1601
+ this.name = "AggregatorError";
1602
+ }
1603
+ };
1604
+ var EndpointResolutionError = class extends SyftHubError {
1605
+ constructor(message, endpointPath) {
1606
+ super(message);
1607
+ this.endpointPath = endpointPath;
1608
+ this.name = "EndpointResolutionError";
1609
+ }
1610
+ };
1611
+ var ChatResource = class {
1612
+ constructor(hub, auth, aggregatorUrl) {
1613
+ this.hub = hub;
1614
+ this.auth = auth;
1615
+ this.aggregatorUrl = aggregatorUrl;
1616
+ }
1617
+ /**
1618
+ * Convert any endpoint format to EndpointRef with URL and owner info.
1619
+ * The ownerUsername is critical for satellite token authentication.
1620
+ */
1621
+ async resolveEndpointRef(endpoint, expectedType) {
1622
+ if (this.isEndpointRef(endpoint)) {
1623
+ return endpoint;
1624
+ }
1625
+ if (this.isEndpointPublic(endpoint)) {
1626
+ if (expectedType && endpoint.type !== expectedType) {
1627
+ throw new Error(
1628
+ `Expected endpoint type '${expectedType}', got '${endpoint.type}' for '${endpoint.slug}'`
1629
+ );
1630
+ }
1631
+ for (const conn of endpoint.connect) {
1632
+ if (conn.enabled && conn.config["url"]) {
1633
+ return {
1634
+ url: String(conn.config["url"]),
1635
+ slug: endpoint.slug,
1636
+ name: endpoint.name,
1637
+ tenantName: conn.config["tenant_name"],
1638
+ ownerUsername: endpoint.ownerUsername
1639
+ // Capture owner for satellite token
1640
+ };
1641
+ }
1642
+ }
1643
+ throw new EndpointResolutionError(
1644
+ `Endpoint '${endpoint.slug}' has no connection with URL configured`,
1645
+ `${endpoint.ownerUsername}/${endpoint.slug}`
1646
+ );
1647
+ }
1648
+ if (typeof endpoint === "string") {
1649
+ let ep;
1650
+ try {
1651
+ ep = await this.hub.get(endpoint);
1652
+ } catch (error) {
1653
+ throw new EndpointResolutionError(
1654
+ `Failed to fetch endpoint '${endpoint}': ${error instanceof Error ? error.message : String(error)}`,
1655
+ endpoint
1656
+ );
1657
+ }
1658
+ return this.resolveEndpointRef(ep, expectedType);
1659
+ }
1660
+ throw new TypeError(`Cannot resolve endpoint from type: ${typeof endpoint}`);
1661
+ }
1662
+ /**
1663
+ * Collect unique owner usernames from all endpoints.
1664
+ * Used to determine which satellite tokens need to be fetched.
1665
+ */
1666
+ collectUniqueOwners(modelRef, dataSourceRefs) {
1667
+ const owners = /* @__PURE__ */ new Set();
1668
+ if (modelRef.ownerUsername) {
1669
+ owners.add(modelRef.ownerUsername);
1670
+ }
1671
+ for (const ds of dataSourceRefs) {
1672
+ if (ds.ownerUsername) {
1673
+ owners.add(ds.ownerUsername);
1674
+ }
1675
+ }
1676
+ return [...owners];
1677
+ }
1678
+ /**
1679
+ * Get satellite tokens for all unique endpoint owners.
1680
+ * Returns a map of owner username to satellite token.
1681
+ */
1682
+ async getSatelliteTokensForOwners(owners) {
1683
+ if (owners.length === 0) {
1684
+ return {};
1685
+ }
1686
+ const tokenMap = await this.auth.getSatelliteTokens(owners);
1687
+ const result = {};
1688
+ for (const [owner, token] of tokenMap) {
1689
+ result[owner] = token;
1690
+ }
1691
+ return result;
1692
+ }
1693
+ /**
1694
+ * Get transaction tokens for all unique endpoint owners.
1695
+ * Returns a map of owner username to transaction token.
1696
+ *
1697
+ * Transaction tokens are used for billing - they authorize the endpoint
1698
+ * owner to charge the current user for usage.
1699
+ */
1700
+ async getTransactionTokensForOwners(owners) {
1701
+ if (owners.length === 0) {
1702
+ return {};
1703
+ }
1704
+ const response = await this.auth.getTransactionTokens(owners);
1705
+ return response.tokens;
1706
+ }
1707
+ /**
1708
+ * Type guard for EndpointRef.
1709
+ */
1710
+ isEndpointRef(value) {
1711
+ return typeof value === "object" && value !== null && "url" in value && "slug" in value && typeof value.url === "string" && typeof value.slug === "string";
1712
+ }
1713
+ /**
1714
+ * Type guard for EndpointPublic.
1715
+ */
1716
+ isEndpointPublic(value) {
1717
+ return typeof value === "object" && value !== null && "connect" in value && "ownerUsername" in value && Array.isArray(value.connect);
1718
+ }
1719
+ /**
1720
+ * Build the request body for the aggregator.
1721
+ * Includes endpoint_tokens mapping for satellite token authentication.
1722
+ * Includes transaction_tokens mapping for billing authorization.
1723
+ * User identity is derived from satellite tokens, not passed in request body.
1724
+ */
1725
+ buildRequestBody(prompt, modelRef, dataSourceRefs, endpointTokens, transactionTokens, options) {
1726
+ return {
1727
+ prompt,
1728
+ model: {
1729
+ url: modelRef.url,
1730
+ slug: modelRef.slug,
1731
+ name: modelRef.name ?? "",
1732
+ tenant_name: modelRef.tenantName ?? null,
1733
+ owner_username: modelRef.ownerUsername ?? null
1734
+ },
1735
+ data_sources: dataSourceRefs.map((ds) => ({
1736
+ url: ds.url,
1737
+ slug: ds.slug,
1738
+ name: ds.name ?? "",
1739
+ tenant_name: ds.tenantName ?? null,
1740
+ owner_username: ds.ownerUsername ?? null
1741
+ })),
1742
+ endpoint_tokens: endpointTokens,
1743
+ transaction_tokens: transactionTokens,
1744
+ top_k: options.topK ?? 5,
1745
+ max_tokens: options.maxTokens ?? 1024,
1746
+ temperature: options.temperature ?? 0.7,
1747
+ similarity_threshold: options.similarityThreshold ?? 0.5,
1748
+ stream: options.stream ?? false
1749
+ };
1750
+ }
1751
+ /**
1752
+ * Parse a SourceInfo from raw data.
1753
+ */
1754
+ parseSourceInfo(data) {
1755
+ return {
1756
+ path: String(data["path"] ?? ""),
1757
+ documentsRetrieved: Number(data["documents_retrieved"] ?? 0),
1758
+ status: data["status"] ?? "success",
1759
+ errorMessage: data["error_message"]
1760
+ };
1761
+ }
1762
+ /**
1763
+ * Parse ChatMetadata from raw data.
1764
+ */
1765
+ parseMetadata(data) {
1766
+ return {
1767
+ retrievalTimeMs: Number(data["retrieval_time_ms"] ?? 0),
1768
+ generationTimeMs: Number(data["generation_time_ms"] ?? 0),
1769
+ totalTimeMs: Number(data["total_time_ms"] ?? 0)
1770
+ };
1771
+ }
1772
+ /**
1773
+ * Parse TokenUsage from raw data.
1774
+ */
1775
+ parseUsage(data) {
1776
+ return {
1777
+ promptTokens: Number(data["prompt_tokens"] ?? 0),
1778
+ completionTokens: Number(data["completion_tokens"] ?? 0),
1779
+ totalTokens: Number(data["total_tokens"] ?? 0)
1780
+ };
1781
+ }
1782
+ /**
1783
+ * Parse document sources from raw data.
1784
+ * The new format is a dict mapping document title to {slug, content}.
1785
+ */
1786
+ parseDocumentSources(data) {
1787
+ const sources = {};
1788
+ if (!data || typeof data !== "object") {
1789
+ return sources;
1790
+ }
1791
+ for (const [title, value] of Object.entries(data)) {
1792
+ if (value && typeof value === "object") {
1793
+ const source = value;
1794
+ sources[title] = {
1795
+ slug: String(source["slug"] ?? ""),
1796
+ content: String(source["content"] ?? "")
1797
+ };
1798
+ }
1799
+ }
1800
+ return sources;
1801
+ }
1802
+ /**
1803
+ * Parse retrieval info (SourceInfo array) from raw data.
1804
+ */
1805
+ parseRetrievalInfo(data) {
1806
+ const retrievalInfo = [];
1807
+ if (!Array.isArray(data)) {
1808
+ return retrievalInfo;
1809
+ }
1810
+ for (const item of data) {
1811
+ retrievalInfo.push(this.parseSourceInfo(item));
1812
+ }
1813
+ return retrievalInfo;
1814
+ }
1815
+ /**
1816
+ * Send a chat request and get the complete response.
1817
+ *
1818
+ * This method automatically:
1819
+ * 1. Resolves endpoints and extracts owner information
1820
+ * 2. Exchanges Hub tokens for satellite tokens (one per unique owner)
1821
+ * 3. Fetches transaction tokens for billing authorization
1822
+ * 4. Sends tokens to the aggregator for forwarding to SyftAI-Space
1823
+ *
1824
+ * @param options - Chat completion options
1825
+ * @returns ChatResponse with response text, sources, and metadata
1826
+ * @throws {EndpointResolutionError} If endpoint cannot be resolved
1827
+ * @throws {AggregatorError} If aggregator service fails
1828
+ */
1829
+ async complete(options) {
1830
+ const modelRef = await this.resolveEndpointRef(options.model, "model");
1831
+ const dsRefs = [];
1832
+ for (const ds of options.dataSources ?? []) {
1833
+ dsRefs.push(await this.resolveEndpointRef(ds, "data_source"));
1834
+ }
1835
+ const uniqueOwners = this.collectUniqueOwners(modelRef, dsRefs);
1836
+ const endpointTokens = await this.getSatelliteTokensForOwners(uniqueOwners);
1837
+ const transactionTokens = await this.getTransactionTokensForOwners(uniqueOwners);
1838
+ const requestBody = this.buildRequestBody(
1839
+ options.prompt,
1840
+ modelRef,
1841
+ dsRefs,
1842
+ endpointTokens,
1843
+ transactionTokens,
1844
+ {
1845
+ topK: options.topK,
1846
+ maxTokens: options.maxTokens,
1847
+ temperature: options.temperature,
1848
+ similarityThreshold: options.similarityThreshold,
1849
+ stream: false
1850
+ }
1851
+ );
1852
+ const url = `${this.aggregatorUrl}/chat`;
1853
+ const response = await fetch(url, {
1854
+ method: "POST",
1855
+ headers: {
1856
+ "Content-Type": "application/json"
1857
+ },
1858
+ body: JSON.stringify(requestBody)
1859
+ });
1860
+ if (!response.ok) {
1861
+ let message = `HTTP ${response.status}`;
1862
+ try {
1863
+ const data2 = await response.json();
1864
+ message = String(data2["message"] ?? data2["error"] ?? message);
1865
+ } catch {
1866
+ }
1867
+ throw new AggregatorError(`Aggregator error: ${message}`, response.status);
1868
+ }
1869
+ const data = await response.json();
1870
+ const sourcesData = data["sources"];
1871
+ const sources = this.parseDocumentSources(sourcesData);
1872
+ const retrievalInfoData = data["retrieval_info"];
1873
+ const retrievalInfo = this.parseRetrievalInfo(retrievalInfoData);
1874
+ const metadataData = data["metadata"];
1875
+ const metadata = this.parseMetadata(metadataData ?? {});
1876
+ const usageData = data["usage"];
1877
+ const usage = usageData ? this.parseUsage(usageData) : void 0;
1878
+ return {
1879
+ response: String(data["response"] ?? ""),
1880
+ sources,
1881
+ retrievalInfo,
1882
+ metadata,
1883
+ usage
1884
+ };
1885
+ }
1886
+ /**
1887
+ * Send a chat request and stream response events.
1888
+ *
1889
+ * This method automatically:
1890
+ * 1. Resolves endpoints and extracts owner information
1891
+ * 2. Exchanges Hub tokens for satellite tokens (one per unique owner)
1892
+ * 3. Fetches transaction tokens for billing authorization
1893
+ * 4. Sends tokens to the aggregator for forwarding to SyftAI-Space
1894
+ *
1895
+ * @param options - Chat completion options
1896
+ * @yields ChatStreamEvent objects as they arrive
1897
+ */
1898
+ async *stream(options) {
1899
+ const modelRef = await this.resolveEndpointRef(options.model, "model");
1900
+ const dsRefs = [];
1901
+ for (const ds of options.dataSources ?? []) {
1902
+ dsRefs.push(await this.resolveEndpointRef(ds, "data_source"));
1903
+ }
1904
+ const uniqueOwners = this.collectUniqueOwners(modelRef, dsRefs);
1905
+ const endpointTokens = await this.getSatelliteTokensForOwners(uniqueOwners);
1906
+ const transactionTokens = await this.getTransactionTokensForOwners(uniqueOwners);
1907
+ const requestBody = this.buildRequestBody(
1908
+ options.prompt,
1909
+ modelRef,
1910
+ dsRefs,
1911
+ endpointTokens,
1912
+ transactionTokens,
1913
+ {
1914
+ topK: options.topK,
1915
+ maxTokens: options.maxTokens,
1916
+ temperature: options.temperature,
1917
+ similarityThreshold: options.similarityThreshold,
1918
+ stream: true
1919
+ }
1920
+ );
1921
+ const url = `${this.aggregatorUrl}/chat/stream`;
1922
+ const response = await fetch(url, {
1923
+ method: "POST",
1924
+ headers: {
1925
+ "Content-Type": "application/json",
1926
+ Accept: "text/event-stream"
1927
+ },
1928
+ body: JSON.stringify(requestBody),
1929
+ signal: options.signal
1930
+ });
1931
+ if (!response.ok) {
1932
+ let message = `HTTP ${response.status}`;
1933
+ try {
1934
+ const data = await response.json();
1935
+ message = String(data["message"] ?? data["error"] ?? message);
1936
+ } catch {
1937
+ }
1938
+ throw new AggregatorError(`Aggregator error: ${message}`, response.status);
1939
+ }
1940
+ if (!response.body) {
1941
+ throw new AggregatorError("No response body from aggregator");
1942
+ }
1943
+ const reader = response.body.getReader();
1944
+ const decoder = new TextDecoder();
1945
+ let buffer = "";
1946
+ let currentEvent = null;
1947
+ let currentData = "";
1948
+ try {
1949
+ while (true) {
1950
+ const { done, value } = await reader.read();
1951
+ if (done) break;
1952
+ buffer += decoder.decode(value, { stream: true });
1953
+ const lines = buffer.split("\n");
1954
+ buffer = lines.pop() ?? "";
1955
+ for (const line of lines) {
1956
+ const trimmedLine = line.trim();
1957
+ if (!trimmedLine) {
1958
+ if (currentEvent && currentData) {
1959
+ try {
1960
+ const data = JSON.parse(currentData);
1961
+ const event = this.parseSSEEvent(currentEvent, data);
1962
+ if (event) {
1963
+ yield event;
1964
+ }
1965
+ } catch {
1966
+ }
1967
+ }
1968
+ currentEvent = null;
1969
+ currentData = "";
1970
+ continue;
1971
+ }
1972
+ if (trimmedLine.startsWith("event:")) {
1973
+ currentEvent = trimmedLine.slice(6).trim();
1974
+ } else if (trimmedLine.startsWith("data:")) {
1975
+ currentData = trimmedLine.slice(5).trim();
1976
+ }
1977
+ }
1978
+ }
1979
+ } finally {
1980
+ reader.releaseLock();
1981
+ }
1982
+ }
1983
+ /**
1984
+ * Parse an SSE event into a typed event object.
1985
+ */
1986
+ parseSSEEvent(eventType, data) {
1987
+ switch (eventType) {
1988
+ case "retrieval_start":
1989
+ return {
1990
+ type: "retrieval_start",
1991
+ sourceCount: Number(data["sources"] ?? 0)
1992
+ };
1993
+ case "source_complete":
1994
+ return {
1995
+ type: "source_complete",
1996
+ path: String(data["path"] ?? ""),
1997
+ status: String(data["status"] ?? ""),
1998
+ documentsRetrieved: Number(data["documents"] ?? 0)
1999
+ };
2000
+ case "retrieval_complete":
2001
+ return {
2002
+ type: "retrieval_complete",
2003
+ totalDocuments: Number(data["total_documents"] ?? 0),
2004
+ timeMs: Number(data["time_ms"] ?? 0)
2005
+ };
2006
+ case "generation_start":
2007
+ return { type: "generation_start" };
2008
+ case "token":
2009
+ return {
2010
+ type: "token",
2011
+ content: String(data["content"] ?? "")
2012
+ };
2013
+ case "done": {
2014
+ const sourcesData = data["sources"];
2015
+ const sources = this.parseDocumentSources(sourcesData);
2016
+ const retrievalInfoData = data["retrieval_info"];
2017
+ const retrievalInfo = this.parseRetrievalInfo(retrievalInfoData);
2018
+ const metadataData = data["metadata"];
2019
+ const metadata = this.parseMetadata(metadataData ?? {});
2020
+ const usageData = data["usage"];
2021
+ const usage = usageData ? this.parseUsage(usageData) : void 0;
2022
+ return { type: "done", sources, retrievalInfo, metadata, usage };
2023
+ }
2024
+ case "error":
2025
+ return {
2026
+ type: "error",
2027
+ message: String(data["message"] ?? "Unknown error")
2028
+ };
2029
+ default:
2030
+ return {
2031
+ type: "error",
2032
+ message: `Unknown event type: ${eventType}`
2033
+ };
2034
+ }
2035
+ }
2036
+ /**
2037
+ * Get model endpoints that have connection URLs configured.
2038
+ *
2039
+ * @param limit - Maximum number of results (default: 20)
2040
+ * @returns Array of EndpointPublic objects for models with URLs
2041
+ */
2042
+ async getAvailableModels(limit = 20) {
2043
+ const results = [];
2044
+ for await (const endpoint of this.hub.browse()) {
2045
+ if (results.length >= limit) break;
2046
+ if (endpoint.type !== EndpointType.MODEL) continue;
2047
+ const hasUrl = endpoint.connect.some(
2048
+ (conn) => conn.enabled && conn.config["url"]
2049
+ );
2050
+ if (hasUrl) {
2051
+ results.push(endpoint);
2052
+ }
2053
+ }
2054
+ return results;
2055
+ }
2056
+ /**
2057
+ * Get data source endpoints that have connection URLs configured.
2058
+ *
2059
+ * @param limit - Maximum number of results (default: 20)
2060
+ * @returns Array of EndpointPublic objects for data sources with URLs
2061
+ */
2062
+ async getAvailableDataSources(limit = 20) {
2063
+ const results = [];
2064
+ for await (const endpoint of this.hub.browse()) {
2065
+ if (results.length >= limit) break;
2066
+ if (endpoint.type !== EndpointType.DATA_SOURCE) continue;
2067
+ const hasUrl = endpoint.connect.some(
2068
+ (conn) => conn.enabled && conn.config["url"]
2069
+ );
2070
+ if (hasUrl) {
2071
+ results.push(endpoint);
2072
+ }
2073
+ }
2074
+ return results;
2075
+ }
2076
+ };
2077
+
2078
+ // src/resources/syftai.ts
2079
+ init_errors();
2080
+ var RetrievalError = class extends SyftHubError {
2081
+ constructor(message, sourcePath, detail) {
2082
+ super(message);
2083
+ this.sourcePath = sourcePath;
2084
+ this.detail = detail;
2085
+ this.name = "RetrievalError";
2086
+ }
2087
+ };
2088
+ var GenerationError = class extends SyftHubError {
2089
+ constructor(message, modelSlug, detail) {
2090
+ super(message);
2091
+ this.modelSlug = modelSlug;
2092
+ this.detail = detail;
2093
+ this.name = "GenerationError";
2094
+ }
2095
+ };
2096
+ var SyftAIResource = class {
2097
+ // No dependencies - uses direct fetch to SyftAI-Space endpoints
2098
+ /**
2099
+ * Build headers for SyftAI-Space request.
2100
+ */
2101
+ buildHeaders(tenantName) {
2102
+ const headers = {
2103
+ "Content-Type": "application/json"
2104
+ };
2105
+ if (tenantName) {
2106
+ headers["X-Tenant-Name"] = tenantName;
2107
+ }
2108
+ return headers;
2109
+ }
2110
+ /**
2111
+ * Query a data source endpoint directly.
2112
+ *
2113
+ * @param options - Query options
2114
+ * @returns Array of Document objects
2115
+ * @throws {RetrievalError} If the query fails
2116
+ */
2117
+ async queryDataSource(options) {
2118
+ const { endpoint, query, userEmail, topK = 5, similarityThreshold = 0.5 } = options;
2119
+ const url = `${endpoint.url.replace(/\/$/, "")}/api/v1/endpoints/${endpoint.slug}/query`;
2120
+ const requestBody = {
2121
+ user_email: userEmail,
2122
+ messages: query,
2123
+ // SyftAI-Space expects "messages" for query text
2124
+ limit: topK,
2125
+ similarity_threshold: similarityThreshold
2126
+ };
2127
+ let response;
2128
+ try {
2129
+ response = await fetch(url, {
2130
+ method: "POST",
2131
+ headers: this.buildHeaders(endpoint.tenantName),
2132
+ body: JSON.stringify(requestBody)
2133
+ });
2134
+ } catch (error) {
2135
+ throw new RetrievalError(
2136
+ `Failed to connect to data source '${endpoint.slug}': ${error instanceof Error ? error.message : String(error)}`,
2137
+ endpoint.slug,
2138
+ error
2139
+ );
2140
+ }
2141
+ if (!response.ok) {
2142
+ let message = `HTTP ${response.status}`;
2143
+ try {
2144
+ const data2 = await response.json();
2145
+ message = String(data2["detail"] ?? data2["message"] ?? message);
2146
+ } catch {
2147
+ }
2148
+ throw new RetrievalError(`Data source query failed: ${message}`, endpoint.slug);
2149
+ }
2150
+ const data = await response.json();
2151
+ const documents = [];
2152
+ const docsData = data["documents"];
2153
+ if (Array.isArray(docsData)) {
2154
+ for (const doc of docsData) {
2155
+ documents.push({
2156
+ content: String(doc["content"] ?? ""),
2157
+ score: Number(doc["score"] ?? 0),
2158
+ metadata: doc["metadata"] ?? {}
2159
+ });
2160
+ }
2161
+ }
2162
+ return documents;
2163
+ }
2164
+ /**
2165
+ * Query a model endpoint directly.
2166
+ *
2167
+ * @param options - Query options
2168
+ * @returns Generated response text
2169
+ * @throws {GenerationError} If generation fails
2170
+ */
2171
+ async queryModel(options) {
2172
+ const { endpoint, messages, userEmail, maxTokens = 1024, temperature = 0.7 } = options;
2173
+ const url = `${endpoint.url.replace(/\/$/, "")}/api/v1/endpoints/${endpoint.slug}/query`;
2174
+ const requestBody = {
2175
+ user_email: userEmail,
2176
+ messages: messages.map((msg) => ({
2177
+ role: msg.role,
2178
+ content: msg.content
2179
+ })),
2180
+ max_tokens: maxTokens,
2181
+ temperature,
2182
+ stream: false
2183
+ };
2184
+ let response;
2185
+ try {
2186
+ response = await fetch(url, {
2187
+ method: "POST",
2188
+ headers: this.buildHeaders(endpoint.tenantName),
2189
+ body: JSON.stringify(requestBody)
2190
+ });
2191
+ } catch (error) {
2192
+ throw new GenerationError(
2193
+ `Failed to connect to model '${endpoint.slug}': ${error instanceof Error ? error.message : String(error)}`,
2194
+ endpoint.slug,
2195
+ error
2196
+ );
2197
+ }
2198
+ if (!response.ok) {
2199
+ let message = `HTTP ${response.status}`;
2200
+ try {
2201
+ const data2 = await response.json();
2202
+ message = String(data2["detail"] ?? data2["message"] ?? message);
2203
+ } catch {
2204
+ }
2205
+ throw new GenerationError(`Model query failed: ${message}`, endpoint.slug);
2206
+ }
2207
+ const data = await response.json();
2208
+ const messageData = data["message"];
2209
+ return String(messageData?.["content"] ?? "");
2210
+ }
2211
+ /**
2212
+ * Stream a model response directly.
2213
+ *
2214
+ * @param options - Query options
2215
+ * @yields Response text chunks as they arrive
2216
+ * @throws {GenerationError} If generation fails
2217
+ */
2218
+ async *queryModelStream(options) {
2219
+ const { endpoint, messages, userEmail, maxTokens = 1024, temperature = 0.7 } = options;
2220
+ const url = `${endpoint.url.replace(/\/$/, "")}/api/v1/endpoints/${endpoint.slug}/query`;
2221
+ const requestBody = {
2222
+ user_email: userEmail,
2223
+ messages: messages.map((msg) => ({
2224
+ role: msg.role,
2225
+ content: msg.content
2226
+ })),
2227
+ max_tokens: maxTokens,
2228
+ temperature,
2229
+ stream: true
2230
+ };
2231
+ let response;
2232
+ try {
2233
+ response = await fetch(url, {
2234
+ method: "POST",
2235
+ headers: {
2236
+ ...this.buildHeaders(endpoint.tenantName),
2237
+ Accept: "text/event-stream"
2238
+ },
2239
+ body: JSON.stringify(requestBody)
2240
+ });
2241
+ } catch (error) {
2242
+ throw new GenerationError(
2243
+ `Failed to connect to model '${endpoint.slug}': ${error instanceof Error ? error.message : String(error)}`,
2244
+ endpoint.slug,
2245
+ error
2246
+ );
2247
+ }
2248
+ if (!response.ok) {
2249
+ let message = `HTTP ${response.status}`;
2250
+ try {
2251
+ const data = await response.json();
2252
+ message = String(data["detail"] ?? data["message"] ?? message);
2253
+ } catch {
2254
+ }
2255
+ throw new GenerationError(`Model stream failed: ${message}`, endpoint.slug);
2256
+ }
2257
+ if (!response.body) {
2258
+ throw new GenerationError("No response body from model", endpoint.slug);
2259
+ }
2260
+ const reader = response.body.getReader();
2261
+ const decoder = new TextDecoder();
2262
+ let buffer = "";
2263
+ try {
2264
+ while (true) {
2265
+ const { done, value } = await reader.read();
2266
+ if (done) break;
2267
+ buffer += decoder.decode(value, { stream: true });
2268
+ const lines = buffer.split("\n");
2269
+ buffer = lines.pop() ?? "";
2270
+ for (const line of lines) {
2271
+ const trimmedLine = line.trim();
2272
+ if (!trimmedLine || trimmedLine.startsWith("event:")) {
2273
+ continue;
2274
+ }
2275
+ if (trimmedLine.startsWith("data:")) {
2276
+ const dataStr = trimmedLine.slice(5).trim();
2277
+ if (dataStr === "[DONE]") {
2278
+ return;
2279
+ }
2280
+ try {
2281
+ const data = JSON.parse(dataStr);
2282
+ if (typeof data["content"] === "string") {
2283
+ yield data["content"];
2284
+ } else if (Array.isArray(data["choices"])) {
2285
+ for (const choice of data["choices"]) {
2286
+ const delta = choice["delta"];
2287
+ if (delta && typeof delta["content"] === "string") {
2288
+ yield delta["content"];
2289
+ }
2290
+ }
2291
+ }
2292
+ } catch {
2293
+ }
2294
+ }
2295
+ }
2296
+ }
2297
+ } finally {
2298
+ reader.releaseLock();
2299
+ }
2300
+ }
2301
+ };
2302
+
2303
+ // src/client.ts
2304
+ function getEnv(key) {
2305
+ if (typeof process !== "undefined" && process.env) {
2306
+ return process.env[key];
2307
+ }
2308
+ return void 0;
2309
+ }
2310
+ function isBrowser() {
2311
+ return typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined" && typeof globalThis.document !== "undefined";
2312
+ }
2313
+ var SyftHubClient = class {
2314
+ http;
2315
+ options;
2316
+ aggregatorUrl;
2317
+ // Lazy-initialized resources
2318
+ _auth;
2319
+ _users;
2320
+ _myEndpoints;
2321
+ _hub;
2322
+ _accounting;
2323
+ _chat;
2324
+ _syftai;
2325
+ /**
2326
+ * Create a new SyftHub client.
2327
+ *
2328
+ * @param options - Configuration options
2329
+ * @throws {SyftHubError} If baseUrl is not provided and SYFTHUB_URL is not set (in non-browser environments)
2330
+ */
2331
+ constructor(options = {}) {
2332
+ this.options = options;
2333
+ let baseUrl = options.baseUrl ?? getEnv("SYFTHUB_URL");
2334
+ if (!baseUrl && !isBrowser()) {
2335
+ throw new SyftHubError(
2336
+ "baseUrl is required. Provide it in options or set the SYFTHUB_URL environment variable."
2337
+ );
2338
+ }
2339
+ baseUrl = baseUrl ?? "";
2340
+ const normalizedUrl = baseUrl ? baseUrl.replace(/\/+$/, "") : "";
2341
+ this.http = new HTTPClient(normalizedUrl, options.timeout ?? 3e4);
2342
+ this.aggregatorUrl = options.aggregatorUrl ?? getEnv("SYFTHUB_AGGREGATOR_URL") ?? `${normalizedUrl}/aggregator/api/v1`;
2343
+ }
2344
+ /**
2345
+ * Authentication resource for login, register, and session management.
2346
+ *
2347
+ * @example
2348
+ * const user = await client.auth.login('alice', 'password');
2349
+ * await client.auth.logout();
2350
+ */
2351
+ get auth() {
2352
+ if (!this._auth) {
2353
+ this._auth = new AuthResource(this.http);
2354
+ }
2355
+ return this._auth;
2356
+ }
2357
+ /**
2358
+ * Users resource for profile management.
2359
+ *
2360
+ * @example
2361
+ * const user = await client.users.update({ fullName: 'Alice Smith' });
2362
+ * const available = await client.users.checkUsername('newname');
2363
+ */
2364
+ get users() {
2365
+ if (!this._users) {
2366
+ this._users = new UsersResource(this.http);
2367
+ }
2368
+ return this._users;
2369
+ }
2370
+ /**
2371
+ * My Endpoints resource for managing your own endpoints.
2372
+ *
2373
+ * @example
2374
+ * const endpoints = await client.myEndpoints.list().all();
2375
+ * const endpoint = await client.myEndpoints.create({ name: 'My API', type: 'model' });
2376
+ */
2377
+ get myEndpoints() {
2378
+ if (!this._myEndpoints) {
2379
+ this._myEndpoints = new MyEndpointsResource(this.http);
2380
+ }
2381
+ return this._myEndpoints;
2382
+ }
2383
+ /**
2384
+ * Hub resource for browsing public endpoints.
2385
+ *
2386
+ * @example
2387
+ * for await (const endpoint of client.hub.browse()) {
2388
+ * console.log(endpoint.name);
2389
+ * }
2390
+ */
2391
+ get hub() {
2392
+ if (!this._hub) {
2393
+ this._hub = new HubResource(this.http);
2394
+ }
2395
+ return this._hub;
2396
+ }
2397
+ /**
2398
+ * Accounting resource for billing and transactions.
2399
+ *
2400
+ * The accounting service is external and uses separate credentials
2401
+ * (email/password Basic auth) from SyftHub's JWT authentication.
2402
+ *
2403
+ * Credentials can be provided via:
2404
+ * - Constructor options: accountingUrl, accountingEmail, accountingPassword
2405
+ * - Environment variables: SYFTHUB_ACCOUNTING_URL, SYFTHUB_ACCOUNTING_EMAIL, SYFTHUB_ACCOUNTING_PASSWORD
2406
+ *
2407
+ * @throws {SyftHubError} If accounting credentials are not configured
2408
+ *
2409
+ * @example
2410
+ * const user = await client.accounting.getUser();
2411
+ * console.log(`Balance: ${user.balance}`);
2412
+ *
2413
+ * // Create a transaction
2414
+ * const tx = await client.accounting.createTransaction({
2415
+ * recipientEmail: 'bob@example.com',
2416
+ * amount: 10.0
2417
+ * });
2418
+ */
2419
+ get accounting() {
2420
+ if (!this._accounting) {
2421
+ const url = this.options.accountingUrl ?? getEnv("SYFTHUB_ACCOUNTING_URL");
2422
+ const email = this.options.accountingEmail ?? getEnv("SYFTHUB_ACCOUNTING_EMAIL");
2423
+ const password = this.options.accountingPassword ?? getEnv("SYFTHUB_ACCOUNTING_PASSWORD");
2424
+ if (!url || !email || !password) {
2425
+ const missing = [];
2426
+ if (!url) missing.push("SYFTHUB_ACCOUNTING_URL");
2427
+ if (!email) missing.push("SYFTHUB_ACCOUNTING_EMAIL");
2428
+ if (!password) missing.push("SYFTHUB_ACCOUNTING_PASSWORD");
2429
+ throw new ConfigurationError(
2430
+ `Accounting not configured. Missing: ${missing.join(", ")}. Set environment variables or pass credentials to SyftHubClient.`
2431
+ );
2432
+ }
2433
+ this._accounting = new AccountingResource({
2434
+ url,
2435
+ email,
2436
+ password,
2437
+ timeout: this.options.timeout
2438
+ });
2439
+ }
2440
+ return this._accounting;
2441
+ }
2442
+ /**
2443
+ * Chat resource for RAG-augmented conversations via the Aggregator.
2444
+ *
2445
+ * This resource provides high-level chat functionality that integrates
2446
+ * with the SyftHub Aggregator service for RAG workflows.
2447
+ *
2448
+ * @example
2449
+ * // Simple chat completion
2450
+ * const response = await client.chat.complete({
2451
+ * prompt: 'What is machine learning?',
2452
+ * model: 'alice/gpt-model',
2453
+ * dataSources: ['bob/ml-docs'],
2454
+ * });
2455
+ * console.log(response.response);
2456
+ *
2457
+ * // Streaming chat
2458
+ * for await (const event of client.chat.stream(options)) {
2459
+ * if (event.type === 'token') {
2460
+ * process.stdout.write(event.content);
2461
+ * }
2462
+ * }
2463
+ *
2464
+ * // Get available endpoints
2465
+ * const models = await client.chat.getAvailableModels();
2466
+ * const sources = await client.chat.getAvailableDataSources();
2467
+ */
2468
+ get chat() {
2469
+ if (!this._chat) {
2470
+ this._chat = new ChatResource(
2471
+ this.hub,
2472
+ this.auth,
2473
+ this.aggregatorUrl
2474
+ );
2475
+ }
2476
+ return this._chat;
2477
+ }
2478
+ /**
2479
+ * SyftAI-Space resource for direct endpoint queries (low-level API).
2480
+ *
2481
+ * This resource provides direct access to SyftAI-Space endpoints without
2482
+ * going through the aggregator. Use this when you need custom RAG pipelines
2483
+ * or fine-grained control over queries.
2484
+ *
2485
+ * For most use cases, prefer the higher-level `client.chat` API instead.
2486
+ *
2487
+ * @example
2488
+ * // Query a data source directly
2489
+ * const docs = await client.syftai.queryDataSource({
2490
+ * endpoint: { url: 'http://syftai:8080', slug: 'docs' },
2491
+ * query: 'What is Python?',
2492
+ * userEmail: 'alice@example.com',
2493
+ * });
2494
+ *
2495
+ * // Query a model directly
2496
+ * const response = await client.syftai.queryModel({
2497
+ * endpoint: { url: 'http://syftai:8080', slug: 'gpt-model' },
2498
+ * messages: [{ role: 'user', content: 'Hello!' }],
2499
+ * userEmail: 'alice@example.com',
2500
+ * });
2501
+ */
2502
+ get syftai() {
2503
+ if (!this._syftai) {
2504
+ this._syftai = new SyftAIResource();
2505
+ }
2506
+ return this._syftai;
2507
+ }
2508
+ /**
2509
+ * Get current authentication tokens.
2510
+ *
2511
+ * Use this to persist tokens for later sessions.
2512
+ *
2513
+ * @returns Current tokens or null if not authenticated
2514
+ *
2515
+ * @example
2516
+ * const tokens = client.getTokens();
2517
+ * if (tokens) {
2518
+ * localStorage.setItem('tokens', JSON.stringify(tokens));
2519
+ * }
2520
+ */
2521
+ getTokens() {
2522
+ return this.http.getTokens();
2523
+ }
2524
+ /**
2525
+ * Set authentication tokens.
2526
+ *
2527
+ * Use this to restore a session from previously saved tokens.
2528
+ *
2529
+ * @param tokens - Tokens to set
2530
+ *
2531
+ * @example
2532
+ * const saved = JSON.parse(localStorage.getItem('tokens'));
2533
+ * if (saved) {
2534
+ * client.setTokens(saved);
2535
+ * }
2536
+ */
2537
+ setTokens(tokens) {
2538
+ this.http.setTokens(tokens.accessToken, tokens.refreshToken);
2539
+ }
2540
+ /**
2541
+ * Check if the client is currently authenticated.
2542
+ *
2543
+ * @returns True if tokens are present
2544
+ */
2545
+ get isAuthenticated() {
2546
+ return this.http.hasTokens();
2547
+ }
2548
+ /**
2549
+ * Check if accounting service is configured.
2550
+ *
2551
+ * Use this to check if accounting credentials are available before
2552
+ * accessing the `accounting` property, which will throw if not configured.
2553
+ *
2554
+ * @returns True if accounting url, email, and password are all configured
2555
+ *
2556
+ * @example
2557
+ * if (client.isAccountingConfigured) {
2558
+ * const user = await client.accounting.getUser();
2559
+ * }
2560
+ */
2561
+ get isAccountingConfigured() {
2562
+ const url = this.options.accountingUrl ?? getEnv("SYFTHUB_ACCOUNTING_URL");
2563
+ const email = this.options.accountingEmail ?? getEnv("SYFTHUB_ACCOUNTING_EMAIL");
2564
+ const password = this.options.accountingPassword ?? getEnv("SYFTHUB_ACCOUNTING_PASSWORD");
2565
+ return Boolean(url && email && password);
2566
+ }
2567
+ /**
2568
+ * Close the client and clean up resources.
2569
+ *
2570
+ * Currently a no-op, but may be used in future for connection pooling.
2571
+ */
2572
+ close() {
2573
+ }
2574
+ };
2575
+
2576
+ // src/index.ts
2577
+ init_errors();
2578
+
2579
+ export { APIError, AccountingAccountExistsError, AccountingResource, AccountingServiceUnavailableError, AggregatorError, AuthenticationError, AuthorizationError, ChatResource, ConfigurationError, CreatorType, EndpointResolutionError, EndpointType, GenerationError, InvalidAccountingPasswordError, NetworkError, NotFoundError, OrganizationRole, PageIterator, RetrievalError, SyftAIResource, SyftHubClient, SyftHubError, TransactionStatus, UserAlreadyExistsError, UserRole, ValidationError, Visibility, createAccountingResource, getEndpointOwnerType, getEndpointPublicPath, isTransactionCancelled, isTransactionCompleted, isTransactionPending, parseTransaction };
2580
+ //# sourceMappingURL=index.js.map
2581
+ //# sourceMappingURL=index.js.map