@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/README.md +279 -0
- package/dist/index.cjs +2604 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2212 -0
- package/dist/index.d.ts +2212 -0
- package/dist/index.js +2581 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -0
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
|