@syfthub/sdk 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -157,9 +157,6 @@ if (client.isAuthenticated) {
157
157
  | Variable | Description |
158
158
  |----------|-------------|
159
159
  | `SYFTHUB_URL` | SyftHub API base URL |
160
- | `SYFTHUB_ACCOUNTING_URL` | Accounting service URL |
161
- | `SYFTHUB_ACCOUNTING_EMAIL` | Accounting auth email |
162
- | `SYFTHUB_ACCOUNTING_PASSWORD` | Accounting auth password |
163
160
 
164
161
  ## Error Handling
165
162
 
package/dist/index.cjs CHANGED
@@ -606,11 +606,7 @@ var AuthResource = class {
606
606
  if (!tokens) {
607
607
  throw new exports.AuthenticationError("No refresh token available");
608
608
  }
609
- const response = await this.http.post(
610
- "/api/v1/auth/refresh",
611
- { refreshToken: tokens.refreshToken },
612
- { includeAuth: false }
613
- );
609
+ const response = await this.http.post("/api/v1/auth/refresh", { refreshToken: tokens.refreshToken }, { includeAuth: false });
614
610
  this.http.setTokens(response.accessToken, response.refreshToken);
615
611
  }
616
612
  /**
@@ -887,22 +883,6 @@ var MyEndpointsResource = class {
887
883
  }
888
884
  return [parts[0], parts[1]];
889
885
  }
890
- /**
891
- * Resolve an endpoint path to its ID.
892
- *
893
- * @param path - Endpoint path in "owner/slug" format
894
- * @returns The endpoint ID
895
- */
896
- async resolveEndpointId(path) {
897
- const [owner, slug] = this.parsePath(path);
898
- const response = await this.http.get(`/${owner}/${slug}`);
899
- if (response.id === void 0) {
900
- throw new Error(
901
- `Could not resolve endpoint ID for '${path}'. Make sure you own this endpoint.`
902
- );
903
- }
904
- return response.id;
905
- }
906
886
  /**
907
887
  * List the current user's endpoints.
908
888
  *
@@ -943,8 +923,17 @@ var MyEndpointsResource = class {
943
923
  * @throws {AuthorizationError} If not authorized to view
944
924
  */
945
925
  async get(path) {
946
- const [owner, slug] = this.parsePath(path);
947
- return this.http.get(`/${owner}/${slug}`);
926
+ const [, slug] = this.parsePath(path);
927
+ const endpoints = await this.http.get("/api/v1/endpoints", { limit: 100 });
928
+ for (const ep of endpoints) {
929
+ if (ep.slug === slug) {
930
+ return ep;
931
+ }
932
+ }
933
+ const { NotFoundError: NotFoundError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
934
+ throw new NotFoundError2(
935
+ `Endpoint not found: '${path}'. No endpoint found with slug '${slug}' in your endpoints.`
936
+ );
948
937
  }
949
938
  /**
950
939
  * Update an endpoint.
@@ -959,8 +948,8 @@ var MyEndpointsResource = class {
959
948
  * @throws {AuthorizationError} If not owner/admin
960
949
  */
961
950
  async update(path, input) {
962
- const endpointId = await this.resolveEndpointId(path);
963
- return this.http.patch(`/api/v1/endpoints/${endpointId}`, input);
951
+ const [, slug] = this.parsePath(path);
952
+ return this.http.patch(`/api/v1/endpoints/slug/${slug}`, input);
964
953
  }
965
954
  /**
966
955
  * Delete an endpoint.
@@ -971,8 +960,44 @@ var MyEndpointsResource = class {
971
960
  * @throws {AuthorizationError} If not owner/admin
972
961
  */
973
962
  async delete(path) {
974
- const endpointId = await this.resolveEndpointId(path);
975
- await this.http.delete(`/api/v1/endpoints/${endpointId}`);
963
+ const [, slug] = this.parsePath(path);
964
+ await this.http.delete(`/api/v1/endpoints/slug/${slug}`);
965
+ }
966
+ /**
967
+ * Synchronize user's endpoints with provided list.
968
+ *
969
+ * This is a DESTRUCTIVE operation that:
970
+ * 1. Deletes ALL existing endpoints owned by the current user
971
+ * 2. Creates ALL endpoints from the provided list
972
+ * 3. Is ATOMIC: either all endpoints sync successfully, or none do
973
+ *
974
+ * Important Notes:
975
+ * - Organization endpoints are NOT affected
976
+ * - Stars on existing endpoints will be lost (reset to 0)
977
+ * - Endpoint IDs will change (new IDs assigned)
978
+ * - Maximum 100 endpoints per sync request
979
+ *
980
+ * @param endpoints - List of endpoint specifications to sync.
981
+ * Pass an empty array to delete ALL user endpoints.
982
+ * @returns SyncEndpointsResponse with synced count, deleted count, and created endpoints
983
+ * @throws {AuthenticationError} If not authenticated
984
+ * @throws {ValidationError} If any endpoint fails validation (entire batch rejected)
985
+ *
986
+ * @example
987
+ * // Sync with new endpoints
988
+ * const result = await client.myEndpoints.sync([
989
+ * { name: 'Model A', type: 'model', visibility: 'public' },
990
+ * { name: 'Data Source B', type: 'data_source', visibility: 'private' },
991
+ * ]);
992
+ * console.log(`Deleted ${result.deleted}, created ${result.synced} endpoints`);
993
+ *
994
+ * @example
995
+ * // Clear all endpoints
996
+ * const result = await client.myEndpoints.sync([]);
997
+ * console.log(`Deleted ${result.deleted} endpoints`);
998
+ */
999
+ async sync(endpoints = []) {
1000
+ return this.http.post("/api/v1/endpoints/sync", { endpoints });
976
1001
  }
977
1002
  };
978
1003
 
@@ -1047,12 +1072,61 @@ var HubResource = class {
1047
1072
  if (options?.minStars !== void 0) {
1048
1073
  params["minStars"] = options.minStars;
1049
1074
  }
1050
- return this.http.get(
1051
- "/api/v1/endpoints/trending",
1052
- params,
1075
+ return this.http.get("/api/v1/endpoints/trending", params, {
1076
+ includeAuth: false
1077
+ });
1078
+ }, pageSize);
1079
+ }
1080
+ /**
1081
+ * Search for endpoints using semantic search.
1082
+ *
1083
+ * Uses RAG-powered semantic search to find endpoints that match the
1084
+ * natural language query. Returns results sorted by relevance score.
1085
+ *
1086
+ * Note: If RAG is not configured on the server (no OpenAI API key),
1087
+ * this method returns an empty array.
1088
+ *
1089
+ * @param query - Natural language search query (e.g., "machine learning models for image classification")
1090
+ * @param options - Search options (topK, type filter, minScore)
1091
+ * @returns Promise resolving to array of EndpointSearchResult with relevance scores.
1092
+ * Returns empty array if query is too short (<3 chars) or no matches found.
1093
+ *
1094
+ * @example
1095
+ * // Search for machine learning models
1096
+ * const results = await client.hub.search('image classification models');
1097
+ * for (const result of results) {
1098
+ * console.log(`${result.ownerUsername}/${result.slug}: ${result.relevanceScore.toFixed(2)}`);
1099
+ * }
1100
+ *
1101
+ * @example
1102
+ * // Filter by type and minimum score
1103
+ * const dataSources = await client.hub.search('customer data', {
1104
+ * type: EndpointType.DATA_SOURCE,
1105
+ * minScore: 0.5,
1106
+ * });
1107
+ */
1108
+ async search(query, options = {}) {
1109
+ const { topK = 10, type, minScore = 0 } = options;
1110
+ if (!query || query.trim().length < 3) {
1111
+ return [];
1112
+ }
1113
+ const body = {
1114
+ query: query.trim(),
1115
+ top_k: topK
1116
+ };
1117
+ if (type !== void 0) {
1118
+ body["type"] = type;
1119
+ }
1120
+ try {
1121
+ const response = await this.http.post(
1122
+ "/api/v1/endpoints/search",
1123
+ body,
1053
1124
  { includeAuth: false }
1054
1125
  );
1055
- }, pageSize);
1126
+ return (response.results ?? []).filter((result) => result.relevanceScore >= minScore);
1127
+ } catch {
1128
+ return [];
1129
+ }
1056
1130
  }
1057
1131
  /**
1058
1132
  * Get an endpoint by its path.
@@ -1085,7 +1159,7 @@ var HubResource = class {
1085
1159
  */
1086
1160
  async star(path) {
1087
1161
  const endpointId = await this.resolveEndpointId(path);
1088
- await this.http.patch(`/api/v1/endpoints/${endpointId}/star`);
1162
+ await this.http.post(`/api/v1/endpoints/${endpointId}/star`);
1089
1163
  }
1090
1164
  /**
1091
1165
  * Unstar an endpoint.
@@ -1096,7 +1170,7 @@ var HubResource = class {
1096
1170
  */
1097
1171
  async unstar(path) {
1098
1172
  const endpointId = await this.resolveEndpointId(path);
1099
- await this.http.patch(`/api/v1/endpoints/${endpointId}/unstar`);
1173
+ await this.http.delete(`/api/v1/endpoints/${endpointId}/star`);
1100
1174
  }
1101
1175
  /**
1102
1176
  * Check if you have starred an endpoint.
@@ -1250,9 +1324,9 @@ var AccountingResource = class {
1250
1324
  const response = await fetch(url.toString(), {
1251
1325
  method,
1252
1326
  headers: {
1253
- "Authorization": this.authHeader,
1327
+ Authorization: this.authHeader,
1254
1328
  "Content-Type": "application/json",
1255
- "Accept": "application/json"
1329
+ Accept: "application/json"
1256
1330
  },
1257
1331
  body: options?.body ? JSON.stringify(options.body) : void 0,
1258
1332
  signal: controller.signal
@@ -1269,7 +1343,10 @@ var AccountingResource = class {
1269
1343
  if (error instanceof Error && error.name === "AbortError") {
1270
1344
  throw new exports.APIError("Request timeout", 408);
1271
1345
  }
1272
- throw new exports.APIError(`Accounting request failed: ${error instanceof Error ? error.message : "Unknown error"}`, 0);
1346
+ throw new exports.APIError(
1347
+ `Accounting request failed: ${error instanceof Error ? error.message : "Unknown error"}`,
1348
+ 0
1349
+ );
1273
1350
  } finally {
1274
1351
  clearTimeout(timeoutId);
1275
1352
  }
@@ -1285,9 +1362,9 @@ var AccountingResource = class {
1285
1362
  const response = await fetch(url.toString(), {
1286
1363
  method,
1287
1364
  headers: {
1288
- "Authorization": `Bearer ${token}`,
1365
+ Authorization: `Bearer ${token}`,
1289
1366
  "Content-Type": "application/json",
1290
- "Accept": "application/json"
1367
+ Accept: "application/json"
1291
1368
  },
1292
1369
  body: options?.body ? JSON.stringify(options.body) : void 0,
1293
1370
  signal: controller.signal
@@ -1304,7 +1381,10 @@ var AccountingResource = class {
1304
1381
  if (error instanceof Error && error.name === "AbortError") {
1305
1382
  throw new exports.APIError("Request timeout", 408);
1306
1383
  }
1307
- throw new exports.APIError(`Accounting request failed: ${error instanceof Error ? error.message : "Unknown error"}`, 0);
1384
+ throw new exports.APIError(
1385
+ `Accounting request failed: ${error instanceof Error ? error.message : "Unknown error"}`,
1386
+ 0
1387
+ );
1308
1388
  } finally {
1309
1389
  clearTimeout(timeoutId);
1310
1390
  }
@@ -1393,15 +1473,12 @@ var AccountingResource = class {
1393
1473
  */
1394
1474
  getTransactions(options) {
1395
1475
  const pageSize = options?.pageSize ?? 20;
1396
- return new PageIterator(
1397
- async (skip, limit) => {
1398
- const response = await this.request("GET", "/transactions", {
1399
- params: { skip, limit }
1400
- });
1401
- return response.map(parseTransaction);
1402
- },
1403
- pageSize
1404
- );
1476
+ return new PageIterator(async (skip, limit) => {
1477
+ const response = await this.request("GET", "/transactions", {
1478
+ params: { skip, limit }
1479
+ });
1480
+ return response.map(parseTransaction);
1481
+ }, pageSize);
1405
1482
  }
1406
1483
  /**
1407
1484
  * Get a specific transaction by ID.
@@ -1851,7 +1928,11 @@ var ChatResource = class {
1851
1928
  stream: false
1852
1929
  }
1853
1930
  );
1854
- const url = `${this.aggregatorUrl}/chat`;
1931
+ const effectiveAggregatorUrl = (options.aggregatorUrl ?? this.aggregatorUrl).replace(
1932
+ /\/+$/,
1933
+ ""
1934
+ );
1935
+ const url = `${effectiveAggregatorUrl}/chat`;
1855
1936
  const response = await fetch(url, {
1856
1937
  method: "POST",
1857
1938
  headers: {
@@ -1920,7 +2001,11 @@ var ChatResource = class {
1920
2001
  stream: true
1921
2002
  }
1922
2003
  );
1923
- const url = `${this.aggregatorUrl}/chat/stream`;
2004
+ const effectiveAggregatorUrl = (options.aggregatorUrl ?? this.aggregatorUrl).replace(
2005
+ /\/+$/,
2006
+ ""
2007
+ );
2008
+ const url = `${effectiveAggregatorUrl}/chat/stream`;
1924
2009
  const response = await fetch(url, {
1925
2010
  method: "POST",
1926
2011
  headers: {
@@ -2046,9 +2131,7 @@ var ChatResource = class {
2046
2131
  for await (const endpoint of this.hub.browse()) {
2047
2132
  if (results.length >= limit) break;
2048
2133
  if (endpoint.type !== EndpointType.MODEL) continue;
2049
- const hasUrl = endpoint.connect.some(
2050
- (conn) => conn.enabled && conn.config["url"]
2051
- );
2134
+ const hasUrl = endpoint.connect.some((conn) => conn.enabled && conn.config["url"]);
2052
2135
  if (hasUrl) {
2053
2136
  results.push(endpoint);
2054
2137
  }
@@ -2066,9 +2149,7 @@ var ChatResource = class {
2066
2149
  for await (const endpoint of this.hub.browse()) {
2067
2150
  if (results.length >= limit) break;
2068
2151
  if (endpoint.type !== EndpointType.DATA_SOURCE) continue;
2069
- const hasUrl = endpoint.connect.some(
2070
- (conn) => conn.enabled && conn.config["url"]
2071
- );
2152
+ const hasUrl = endpoint.connect.some((conn) => conn.enabled && conn.config["url"]);
2072
2153
  if (hasUrl) {
2073
2154
  results.push(endpoint);
2074
2155
  }
@@ -2322,6 +2403,7 @@ var SyftHubClient = class {
2322
2403
  _myEndpoints;
2323
2404
  _hub;
2324
2405
  _accounting;
2406
+ _accountingInitPromise = null;
2325
2407
  _chat;
2326
2408
  _syftai;
2327
2409
  /**
@@ -2402,44 +2484,86 @@ var SyftHubClient = class {
2402
2484
  * The accounting service is external and uses separate credentials
2403
2485
  * (email/password Basic auth) from SyftHub's JWT authentication.
2404
2486
  *
2405
- * Credentials can be provided via:
2406
- * - Constructor options: accountingUrl, accountingEmail, accountingPassword
2407
- * - Environment variables: SYFTHUB_ACCOUNTING_URL, SYFTHUB_ACCOUNTING_EMAIL, SYFTHUB_ACCOUNTING_PASSWORD
2487
+ * Credentials are automatically retrieved from the backend after login.
2488
+ * You must call `initAccounting()` after login to initialize this resource.
2408
2489
  *
2409
- * @throws {SyftHubError} If accounting credentials are not configured
2490
+ * @throws {AuthenticationError} If not initialized
2410
2491
  *
2411
2492
  * @example
2412
- * const user = await client.accounting.getUser();
2413
- * console.log(`Balance: ${user.balance}`);
2493
+ * // Login first, then initialize accounting
2494
+ * await client.auth.login('alice', 'password');
2495
+ * await client.initAccounting();
2414
2496
  *
2415
- * // Create a transaction
2416
- * const tx = await client.accounting.createTransaction({
2417
- * recipientEmail: 'bob@example.com',
2418
- * amount: 10.0
2419
- * });
2497
+ * // Now accounting is available
2498
+ * const user = await client.accounting.getUser();
2420
2499
  */
2421
2500
  get accounting() {
2422
- if (!this._accounting) {
2423
- const url = this.options.accountingUrl ?? getEnv("SYFTHUB_ACCOUNTING_URL");
2424
- const email = this.options.accountingEmail ?? getEnv("SYFTHUB_ACCOUNTING_EMAIL");
2425
- const password = this.options.accountingPassword ?? getEnv("SYFTHUB_ACCOUNTING_PASSWORD");
2426
- if (!url || !email || !password) {
2427
- const missing = [];
2428
- if (!url) missing.push("SYFTHUB_ACCOUNTING_URL");
2429
- if (!email) missing.push("SYFTHUB_ACCOUNTING_EMAIL");
2430
- if (!password) missing.push("SYFTHUB_ACCOUNTING_PASSWORD");
2431
- throw new exports.ConfigurationError(
2432
- `Accounting not configured. Missing: ${missing.join(", ")}. Set environment variables or pass credentials to SyftHubClient.`
2433
- );
2434
- }
2435
- this._accounting = new AccountingResource({
2436
- url,
2437
- email,
2438
- password,
2439
- timeout: this.options.timeout
2440
- });
2501
+ if (this._accounting) {
2502
+ return this._accounting;
2503
+ }
2504
+ throw new exports.AuthenticationError(
2505
+ "Accounting not initialized. Call `await client.initAccounting()` after login."
2506
+ );
2507
+ }
2508
+ /**
2509
+ * Initialize accounting resource by fetching credentials from the backend.
2510
+ *
2511
+ * This method retrieves accounting credentials from the SyftHub backend
2512
+ * and initializes the accounting resource. Requires authentication.
2513
+ *
2514
+ * @returns The initialized AccountingResource
2515
+ * @throws {AuthenticationError} If not authenticated
2516
+ * @throws {ConfigurationError} If user has no accounting service configured
2517
+ *
2518
+ * @example
2519
+ * // Login first, then initialize accounting
2520
+ * await client.auth.login('alice', 'password');
2521
+ * await client.initAccounting();
2522
+ *
2523
+ * // Now accounting is available
2524
+ * const user = await client.accounting.getUser();
2525
+ */
2526
+ async initAccounting() {
2527
+ if (this._accounting) {
2528
+ return this._accounting;
2529
+ }
2530
+ if (this._accountingInitPromise) {
2531
+ return this._accountingInitPromise;
2532
+ }
2533
+ this._accountingInitPromise = this._doInitAccounting();
2534
+ try {
2535
+ this._accounting = await this._accountingInitPromise;
2536
+ return this._accounting;
2537
+ } finally {
2538
+ this._accountingInitPromise = null;
2441
2539
  }
2442
- return this._accounting;
2540
+ }
2541
+ /**
2542
+ * Internal method to perform accounting initialization.
2543
+ */
2544
+ async _doInitAccounting() {
2545
+ if (!this.isAuthenticated) {
2546
+ throw new exports.AuthenticationError(
2547
+ "Must be logged in to use accounting. Call client.auth.login() first."
2548
+ );
2549
+ }
2550
+ const creds = await this.users.getAccountingCredentials();
2551
+ if (!creds.url) {
2552
+ throw new exports.ConfigurationError(
2553
+ "No accounting service configured for this user. Contact your administrator to set up accounting."
2554
+ );
2555
+ }
2556
+ if (!creds.password) {
2557
+ throw new exports.ConfigurationError(
2558
+ "Accounting password not available. This may indicate an issue with your account setup."
2559
+ );
2560
+ }
2561
+ return new AccountingResource({
2562
+ url: creds.url,
2563
+ email: creds.email,
2564
+ password: creds.password,
2565
+ timeout: this.options.timeout
2566
+ });
2443
2567
  }
2444
2568
  /**
2445
2569
  * Chat resource for RAG-augmented conversations via the Aggregator.
@@ -2469,11 +2593,7 @@ var SyftHubClient = class {
2469
2593
  */
2470
2594
  get chat() {
2471
2595
  if (!this._chat) {
2472
- this._chat = new ChatResource(
2473
- this.hub,
2474
- this.auth,
2475
- this.aggregatorUrl
2476
- );
2596
+ this._chat = new ChatResource(this.hub, this.auth, this.aggregatorUrl);
2477
2597
  }
2478
2598
  return this._chat;
2479
2599
  }
@@ -2548,23 +2668,20 @@ var SyftHubClient = class {
2548
2668
  return this.http.hasTokens();
2549
2669
  }
2550
2670
  /**
2551
- * Check if accounting service is configured.
2671
+ * Check if accounting has been initialized.
2552
2672
  *
2553
- * Use this to check if accounting credentials are available before
2554
- * accessing the `accounting` property, which will throw if not configured.
2673
+ * Use this to check if accounting is available before accessing
2674
+ * the `accounting` property, which will throw if not initialized.
2555
2675
  *
2556
- * @returns True if accounting url, email, and password are all configured
2676
+ * @returns True if accounting has been initialized via `initAccounting()`
2557
2677
  *
2558
2678
  * @example
2559
- * if (client.isAccountingConfigured) {
2679
+ * if (client.isAccountingInitialized) {
2560
2680
  * const user = await client.accounting.getUser();
2561
2681
  * }
2562
2682
  */
2563
- get isAccountingConfigured() {
2564
- const url = this.options.accountingUrl ?? getEnv("SYFTHUB_ACCOUNTING_URL");
2565
- const email = this.options.accountingEmail ?? getEnv("SYFTHUB_ACCOUNTING_EMAIL");
2566
- const password = this.options.accountingPassword ?? getEnv("SYFTHUB_ACCOUNTING_PASSWORD");
2567
- return Boolean(url && email && password);
2683
+ get isAccountingInitialized() {
2684
+ return this._accounting !== void 0 && this._accounting !== null;
2568
2685
  }
2569
2686
  /**
2570
2687
  * Close the client and clean up resources.