@syfthub/sdk 0.2.0 → 0.3.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/dist/index.js CHANGED
@@ -677,6 +677,116 @@ var APITokensResource = class {
677
677
  }
678
678
  };
679
679
 
680
+ // src/resources/aggregators.ts
681
+ var AggregatorsResource = class {
682
+ constructor(http) {
683
+ this.http = http;
684
+ }
685
+ /**
686
+ * List all aggregator configurations for the current user.
687
+ *
688
+ * @returns Array of UserAggregator objects
689
+ * @throws {AuthenticationError} If not authenticated
690
+ *
691
+ * @example
692
+ * const aggregators = await client.users.aggregators.list();
693
+ * for (const agg of aggregators) {
694
+ * if (agg.isDefault) {
695
+ * console.log(`Default: ${agg.name}`);
696
+ * }
697
+ * }
698
+ */
699
+ async list() {
700
+ return this.http.get("/api/v1/users/me/aggregators");
701
+ }
702
+ /**
703
+ * Get a specific aggregator configuration by ID.
704
+ *
705
+ * @param aggregatorId - The aggregator ID
706
+ * @returns The UserAggregator object
707
+ * @throws {AuthenticationError} If not authenticated
708
+ * @throws {NotFoundError} If aggregator not found
709
+ *
710
+ * @example
711
+ * const agg = await client.users.aggregators.get(1);
712
+ * console.log(`${agg.name}: ${agg.url}`);
713
+ */
714
+ async get(aggregatorId) {
715
+ return this.http.get(`/api/v1/users/me/aggregators/${aggregatorId}`);
716
+ }
717
+ /**
718
+ * Create a new aggregator configuration.
719
+ *
720
+ * The first aggregator created is automatically set as the default.
721
+ *
722
+ * @param input - Aggregator creation input
723
+ * @returns The created UserAggregator object
724
+ * @throws {AuthenticationError} If not authenticated
725
+ * @throws {ValidationError} If input is invalid
726
+ *
727
+ * @example
728
+ * const agg = await client.users.aggregators.create({
729
+ * name: 'My Custom Aggregator',
730
+ * url: 'https://my-aggregator.example.com'
731
+ * });
732
+ * console.log(`Created: ${agg.id}`);
733
+ */
734
+ async create(input) {
735
+ return this.http.post("/api/v1/users/me/aggregators", input);
736
+ }
737
+ /**
738
+ * Update an aggregator configuration.
739
+ *
740
+ * Only provided fields will be updated.
741
+ *
742
+ * @param aggregatorId - The aggregator ID to update
743
+ * @param input - Fields to update
744
+ * @returns The updated UserAggregator object
745
+ * @throws {AuthenticationError} If not authenticated
746
+ * @throws {NotFoundError} If aggregator not found
747
+ * @throws {ValidationError} If input is invalid
748
+ *
749
+ * @example
750
+ * const agg = await client.users.aggregators.update(1, {
751
+ * name: 'Updated Name'
752
+ * });
753
+ */
754
+ async update(aggregatorId, input) {
755
+ return this.http.put(`/api/v1/users/me/aggregators/${aggregatorId}`, input);
756
+ }
757
+ /**
758
+ * Delete an aggregator configuration.
759
+ *
760
+ * @param aggregatorId - The aggregator ID to delete
761
+ * @throws {AuthenticationError} If not authenticated
762
+ * @throws {NotFoundError} If aggregator not found
763
+ *
764
+ * @example
765
+ * await client.users.aggregators.delete(1);
766
+ */
767
+ async delete(aggregatorId) {
768
+ await this.http.delete(`/api/v1/users/me/aggregators/${aggregatorId}`);
769
+ }
770
+ /**
771
+ * Set an aggregator as the default.
772
+ *
773
+ * Only one aggregator can be the default at a time. Setting a new default
774
+ * automatically unsets the previous one.
775
+ *
776
+ * @param aggregatorId - The aggregator ID to set as default
777
+ * @returns The updated UserAggregator object with isDefault=true
778
+ * @throws {AuthenticationError} If not authenticated
779
+ * @throws {NotFoundError} If aggregator not found
780
+ *
781
+ * @example
782
+ * const agg = await client.users.aggregators.setDefault(2);
783
+ * console.log(`${agg.name} is now the default`);
784
+ */
785
+ async setDefault(aggregatorId) {
786
+ return this.http.patch(`/api/v1/users/me/aggregators/${aggregatorId}/default`);
787
+ }
788
+ };
789
+
680
790
  // src/resources/auth.ts
681
791
  init_errors();
682
792
  var AuthResource = class {
@@ -1064,116 +1174,6 @@ var AuthResource = class {
1064
1174
  }
1065
1175
  };
1066
1176
 
1067
- // src/resources/aggregators.ts
1068
- var AggregatorsResource = class {
1069
- constructor(http) {
1070
- this.http = http;
1071
- }
1072
- /**
1073
- * List all aggregator configurations for the current user.
1074
- *
1075
- * @returns Array of UserAggregator objects
1076
- * @throws {AuthenticationError} If not authenticated
1077
- *
1078
- * @example
1079
- * const aggregators = await client.users.aggregators.list();
1080
- * for (const agg of aggregators) {
1081
- * if (agg.isDefault) {
1082
- * console.log(`Default: ${agg.name}`);
1083
- * }
1084
- * }
1085
- */
1086
- async list() {
1087
- return this.http.get("/api/v1/users/me/aggregators");
1088
- }
1089
- /**
1090
- * Get a specific aggregator configuration by ID.
1091
- *
1092
- * @param aggregatorId - The aggregator ID
1093
- * @returns The UserAggregator object
1094
- * @throws {AuthenticationError} If not authenticated
1095
- * @throws {NotFoundError} If aggregator not found
1096
- *
1097
- * @example
1098
- * const agg = await client.users.aggregators.get(1);
1099
- * console.log(`${agg.name}: ${agg.url}`);
1100
- */
1101
- async get(aggregatorId) {
1102
- return this.http.get(`/api/v1/users/me/aggregators/${aggregatorId}`);
1103
- }
1104
- /**
1105
- * Create a new aggregator configuration.
1106
- *
1107
- * The first aggregator created is automatically set as the default.
1108
- *
1109
- * @param input - Aggregator creation input
1110
- * @returns The created UserAggregator object
1111
- * @throws {AuthenticationError} If not authenticated
1112
- * @throws {ValidationError} If input is invalid
1113
- *
1114
- * @example
1115
- * const agg = await client.users.aggregators.create({
1116
- * name: 'My Custom Aggregator',
1117
- * url: 'https://my-aggregator.example.com'
1118
- * });
1119
- * console.log(`Created: ${agg.id}`);
1120
- */
1121
- async create(input) {
1122
- return this.http.post("/api/v1/users/me/aggregators", input);
1123
- }
1124
- /**
1125
- * Update an aggregator configuration.
1126
- *
1127
- * Only provided fields will be updated.
1128
- *
1129
- * @param aggregatorId - The aggregator ID to update
1130
- * @param input - Fields to update
1131
- * @returns The updated UserAggregator object
1132
- * @throws {AuthenticationError} If not authenticated
1133
- * @throws {NotFoundError} If aggregator not found
1134
- * @throws {ValidationError} If input is invalid
1135
- *
1136
- * @example
1137
- * const agg = await client.users.aggregators.update(1, {
1138
- * name: 'Updated Name'
1139
- * });
1140
- */
1141
- async update(aggregatorId, input) {
1142
- return this.http.put(`/api/v1/users/me/aggregators/${aggregatorId}`, input);
1143
- }
1144
- /**
1145
- * Delete an aggregator configuration.
1146
- *
1147
- * @param aggregatorId - The aggregator ID to delete
1148
- * @throws {AuthenticationError} If not authenticated
1149
- * @throws {NotFoundError} If aggregator not found
1150
- *
1151
- * @example
1152
- * await client.users.aggregators.delete(1);
1153
- */
1154
- async delete(aggregatorId) {
1155
- await this.http.delete(`/api/v1/users/me/aggregators/${aggregatorId}`);
1156
- }
1157
- /**
1158
- * Set an aggregator as the default.
1159
- *
1160
- * Only one aggregator can be the default at a time. Setting a new default
1161
- * automatically unsets the previous one.
1162
- *
1163
- * @param aggregatorId - The aggregator ID to set as default
1164
- * @returns The updated UserAggregator object with isDefault=true
1165
- * @throws {AuthenticationError} If not authenticated
1166
- * @throws {NotFoundError} If aggregator not found
1167
- *
1168
- * @example
1169
- * const agg = await client.users.aggregators.setDefault(2);
1170
- * console.log(`${agg.name} is now the default`);
1171
- */
1172
- async setDefault(aggregatorId) {
1173
- return this.http.patch(`/api/v1/users/me/aggregators/${aggregatorId}/default`);
1174
- }
1175
- };
1176
-
1177
1177
  // src/resources/users.ts
1178
1178
  var UsersResource = class {
1179
1179
  constructor(http) {
@@ -2187,10 +2187,11 @@ function getEndpointPublicPath(endpoint) {
2187
2187
 
2188
2188
  // src/resources/chat.ts
2189
2189
  var AggregatorError = class extends SyftHubError {
2190
- constructor(message, status, detail) {
2190
+ constructor(message, status, detail, billing) {
2191
2191
  super(message);
2192
2192
  this.status = status;
2193
2193
  this.detail = detail;
2194
+ this.billing = billing;
2194
2195
  this.name = "AggregatorError";
2195
2196
  }
2196
2197
  };
@@ -2396,7 +2397,7 @@ var ChatResource = class _ChatResource {
2396
2397
  * Resolves endpoints, fetches tokens, and builds the aggregator request body.
2397
2398
  * Returns the request body and the resolved aggregator URL.
2398
2399
  */
2399
- async prepareRequest(options, stream) {
2400
+ async prepareRequest(options, stream, retrievalOnly = false) {
2400
2401
  const modelRef = await this.resolveEndpointRef(options.model, "model");
2401
2402
  const expandedDataSources = await this.expandCollectivePaths(options.dataSources ?? []);
2402
2403
  const dsRefs = [];
@@ -2431,7 +2432,8 @@ var ChatResource = class _ChatResource {
2431
2432
  stream,
2432
2433
  messages: options.messages,
2433
2434
  peerToken,
2434
- peerChannel
2435
+ peerChannel,
2436
+ retrievalOnly
2435
2437
  }
2436
2438
  );
2437
2439
  const effectiveAggregatorUrl = (options.aggregatorUrl ?? this.aggregatorUrl).replace(
@@ -2445,12 +2447,14 @@ var ChatResource = class _ChatResource {
2445
2447
  */
2446
2448
  async handleAggregatorErrorResponse(response) {
2447
2449
  let message = `HTTP ${response.status}`;
2450
+ let billing;
2448
2451
  try {
2449
2452
  const data = await response.json();
2450
- message = String(data["message"] ?? data["error"] ?? message);
2453
+ message = String(data["message"] ?? data["error"] ?? data["detail"] ?? message);
2454
+ billing = this.parseBilling(data);
2451
2455
  } catch {
2452
2456
  }
2453
- throw new AggregatorError(`Aggregator error: ${message}`, response.status);
2457
+ throw new AggregatorError(`Aggregator error: ${message}`, response.status, void 0, billing);
2454
2458
  }
2455
2459
  /**
2456
2460
  * Build the request body for the aggregator.
@@ -2494,6 +2498,9 @@ var ChatResource = class _ChatResource {
2494
2498
  if (options.peerChannel) {
2495
2499
  body["peer_channel"] = options.peerChannel;
2496
2500
  }
2501
+ if (options.retrievalOnly) {
2502
+ body["retrieval_only"] = true;
2503
+ }
2497
2504
  return body;
2498
2505
  }
2499
2506
  /**
@@ -2527,6 +2534,60 @@ var ChatResource = class _ChatResource {
2527
2534
  totalTokens: Number(data["total_tokens"] ?? 0)
2528
2535
  };
2529
2536
  }
2537
+ /**
2538
+ * Parse a single billing/policy-metadata entry from a raw (snake_case) dict.
2539
+ *
2540
+ * Shared by {@link parseBilling} (aggregated, carries `source`) and the
2541
+ * direct-path `policy_metadata` parsing in {@link SyftAIResource}.
2542
+ */
2543
+ static parseBillingEntry(raw) {
2544
+ const recipientRaw = raw["recipient"];
2545
+ const recipient = recipientRaw && typeof recipientRaw === "object" ? {
2546
+ username: recipientRaw["username"],
2547
+ email: recipientRaw["email"],
2548
+ walletAddress: recipientRaw["wallet_address"]
2549
+ } : void 0;
2550
+ const transactionRaw = raw["transaction"];
2551
+ const transaction = transactionRaw && typeof transactionRaw === "object" ? {
2552
+ rail: String(transactionRaw["rail"] ?? ""),
2553
+ id: String(transactionRaw["id"] ?? ""),
2554
+ reference: transactionRaw["reference"]
2555
+ } : void 0;
2556
+ return {
2557
+ source: raw["source"],
2558
+ policyType: String(raw["policy_type"] ?? ""),
2559
+ kind: String(raw["kind"] ?? ""),
2560
+ status: String(raw["status"] ?? ""),
2561
+ amount: raw["amount"] == null ? void 0 : Number(raw["amount"]),
2562
+ currency: raw["currency"],
2563
+ recipient,
2564
+ transaction,
2565
+ reasonCode: raw["reason_code"],
2566
+ reason: raw["reason"],
2567
+ details: raw["details"] ?? {}
2568
+ };
2569
+ }
2570
+ /**
2571
+ * Parse the aggregated `billing` block from a raw aggregator response.
2572
+ *
2573
+ * Returns undefined when no `billing` object is present (e.g. an error body
2574
+ * with no policy metadata). The wire keys are snake_case.
2575
+ */
2576
+ parseBilling(data) {
2577
+ const b = data["billing"];
2578
+ if (!b || typeof b !== "object") {
2579
+ return void 0;
2580
+ }
2581
+ const billing = b;
2582
+ const entriesRaw = Array.isArray(billing["entries"]) ? billing["entries"] : [];
2583
+ return {
2584
+ totalCost: billing["total_cost"] == null ? null : Number(billing["total_cost"]),
2585
+ currency: billing["currency"] ?? null,
2586
+ entries: entriesRaw.map(
2587
+ (e) => _ChatResource.parseBillingEntry(e)
2588
+ )
2589
+ };
2590
+ }
2530
2591
  /**
2531
2592
  * Parse document sources from raw data.
2532
2593
  * The new format is a dict mapping document title to {slug, content}.
@@ -2594,15 +2655,82 @@ var ChatResource = class _ChatResource {
2594
2655
  const usageData = data["usage"];
2595
2656
  const usage = usageData ? this.parseUsage(usageData) : void 0;
2596
2657
  const profitShare = data["profit_share"];
2658
+ const billing = this.parseBilling(data);
2597
2659
  return {
2598
2660
  response: String(data["response"] ?? ""),
2599
2661
  sources,
2600
2662
  retrievalInfo,
2601
2663
  metadata,
2602
2664
  usage,
2603
- profitShare
2665
+ profitShare,
2666
+ billing
2604
2667
  };
2605
2668
  }
2669
+ /**
2670
+ * Placeholder model for retrieval-only requests. The aggregator requires a
2671
+ * `model` field on every request, but short-circuits before dereferencing it
2672
+ * when `retrieval_only` is set, so an empty ref is never contacted.
2673
+ */
2674
+ static RETRIEVAL_ONLY_MODEL = {
2675
+ url: "",
2676
+ slug: "",
2677
+ name: "retrieval-only"
2678
+ };
2679
+ /**
2680
+ * Retrieve documents from data sources without model generation.
2681
+ *
2682
+ * Drives the aggregator's retrieval-only path: data sources are queried in
2683
+ * parallel (with satellite-token auth and MPP payment handled server-side,
2684
+ * exactly like {@link complete}), but no model is invoked.
2685
+ *
2686
+ * Prefer the symmetric `client.search.query(...)` facade; this is the
2687
+ * underlying primitive.
2688
+ *
2689
+ * @param options - Search options
2690
+ * @returns SearchResponse with retrieved documents and per-source metadata
2691
+ * @throws {EndpointResolutionError} If a data source cannot be resolved
2692
+ * @throws {AggregatorError} If the aggregator service fails
2693
+ */
2694
+ async retrieve(options) {
2695
+ const chatOptions = {
2696
+ prompt: options.prompt,
2697
+ model: _ChatResource.RETRIEVAL_ONLY_MODEL,
2698
+ dataSources: options.dataSources,
2699
+ topK: options.topK,
2700
+ similarityThreshold: options.similarityThreshold,
2701
+ aggregatorUrl: options.aggregatorUrl,
2702
+ guestMode: options.guestMode
2703
+ };
2704
+ const { requestBody, effectiveAggregatorUrl } = await this.prepareRequest(
2705
+ chatOptions,
2706
+ false,
2707
+ true
2708
+ );
2709
+ const response = await fetch(`${effectiveAggregatorUrl}/chat`, {
2710
+ method: "POST",
2711
+ headers: { "Content-Type": "application/json" },
2712
+ body: JSON.stringify(requestBody),
2713
+ signal: options.signal
2714
+ });
2715
+ if (!response.ok) {
2716
+ return this.handleAggregatorErrorResponse(response);
2717
+ }
2718
+ const data = await response.json();
2719
+ const sources = this.parseDocumentSources(
2720
+ data["sources"]
2721
+ );
2722
+ const documents = Object.entries(sources).map(([title, source]) => ({
2723
+ title,
2724
+ slug: source.slug,
2725
+ content: source.content
2726
+ }));
2727
+ const retrievalInfo = this.parseRetrievalInfo(
2728
+ data["retrieval_info"]
2729
+ );
2730
+ const metadata = this.parseMetadata(data["metadata"] ?? {});
2731
+ const billing = this.parseBilling(data);
2732
+ return { documents, retrievalInfo, metadata, billing };
2733
+ }
2606
2734
  /**
2607
2735
  * Send a chat request and stream response events.
2608
2736
  *
@@ -2701,13 +2829,24 @@ var ChatResource = class _ChatResource {
2701
2829
  const usageData = data["usage"];
2702
2830
  const usage = usageData ? this.parseUsage(usageData) : void 0;
2703
2831
  const profitShare = data["profit_share"];
2832
+ const billing = this.parseBilling(data);
2704
2833
  const response = data["response"];
2705
- return { type: "done", sources, retrievalInfo, metadata, usage, profitShare, response };
2834
+ return {
2835
+ type: "done",
2836
+ sources,
2837
+ retrievalInfo,
2838
+ metadata,
2839
+ usage,
2840
+ profitShare,
2841
+ billing,
2842
+ response
2843
+ };
2706
2844
  }
2707
2845
  case "error":
2708
2846
  return {
2709
2847
  type: "error",
2710
- message: String(data["message"] ?? "Unknown error")
2848
+ message: String(data["message"] ?? "Unknown error"),
2849
+ billing: this.parseBilling(data)
2711
2850
  };
2712
2851
  default:
2713
2852
  console.warn(`[SyftHub] Unknown SSE event type received from aggregator: ${eventType}`);
@@ -2748,6 +2887,34 @@ var ChatResource = class _ChatResource {
2748
2887
  }
2749
2888
  };
2750
2889
 
2890
+ // src/resources/search.ts
2891
+ var SearchResource = class {
2892
+ /**
2893
+ * @param chat - The chat resource that owns aggregator communication and
2894
+ * request preparation (satellite tokens, MPP, collective expansion). Search
2895
+ * reuses it rather than duplicating that logic.
2896
+ */
2897
+ constructor(chat) {
2898
+ this.chat = chat;
2899
+ }
2900
+ /**
2901
+ * Retrieve documents from data sources without model generation.
2902
+ *
2903
+ * @param options - Search options (prompt, data sources, top-k, etc.)
2904
+ * @returns SearchResponse with retrieved documents and per-source metadata
2905
+ *
2906
+ * @example
2907
+ * const result = await client.search.query({
2908
+ * prompt: 'Hello, world!',
2909
+ * dataSources: ['epfl-news/epfl-news'],
2910
+ * });
2911
+ * console.log(result.documents.length, 'documents');
2912
+ */
2913
+ async query(options) {
2914
+ return this.chat.retrieve(options);
2915
+ }
2916
+ };
2917
+
2751
2918
  // src/resources/syftai.ts
2752
2919
  init_errors();
2753
2920
  var RetrievalError = class extends SyftHubError {
@@ -2767,28 +2934,149 @@ var GenerationError = class extends SyftHubError {
2767
2934
  }
2768
2935
  };
2769
2936
  var SyftAIResource = class {
2770
- // No dependencies - uses direct fetch to SyftAI-Space endpoints
2937
+ /**
2938
+ * @param http - Hub HTTP client, used to mint satellite tokens and settle
2939
+ * MPP payments. Endpoint queries themselves use direct `fetch`, since the
2940
+ * SyftAI-Space URL is arbitrary and not the Hub base URL.
2941
+ */
2942
+ constructor(http) {
2943
+ this.http = http;
2944
+ }
2945
+ /**
2946
+ * Mint a satellite token for `audience` (the endpoint owner's username).
2947
+ *
2948
+ * Mirrors the aggregator's token coordination layer: try an authenticated
2949
+ * token first, then fall back to a guest token. Returns `undefined` if both
2950
+ * fail, so the caller can still attempt an unauthenticated request.
2951
+ */
2952
+ async mintSatelliteToken(audience) {
2953
+ if (this.http.hasTokens()) {
2954
+ try {
2955
+ const res = await this.http.get("/api/v1/token", {
2956
+ aud: audience
2957
+ });
2958
+ if (res.targetToken) return res.targetToken;
2959
+ } catch {
2960
+ }
2961
+ }
2962
+ try {
2963
+ const res = await this.http.get(
2964
+ "/api/v1/token/guest",
2965
+ { aud: audience },
2966
+ { includeAuth: false }
2967
+ );
2968
+ return res.targetToken;
2969
+ } catch {
2970
+ return void 0;
2971
+ }
2972
+ }
2973
+ /**
2974
+ * Pay an MPP `402` challenge via the Hub wallet, returning an X-Payment credential.
2975
+ *
2976
+ * Mirrors the aggregator's `handleMppPayment`: the `WWW-Authenticate`
2977
+ * challenge is forwarded verbatim to the Hub's `/api/v1/wallet/pay`, which
2978
+ * parses it and returns an `x_payment` string to attach to a retry.
2979
+ */
2980
+ async payMpp(wwwAuthenticate, slug) {
2981
+ if (!wwwAuthenticate) return void 0;
2982
+ const res = await this.http.post("/api/v1/wallet/pay", {
2983
+ wwwAuthenticate,
2984
+ endpointSlug: slug
2985
+ });
2986
+ return res.xPayment;
2987
+ }
2771
2988
  /**
2772
2989
  * Build headers for SyftAI-Space request.
2773
2990
  */
2774
- buildHeaders(tenantName) {
2991
+ buildHeaders(tenantName, authorizationToken) {
2775
2992
  const headers = {
2776
2993
  "Content-Type": "application/json"
2777
2994
  };
2778
2995
  if (tenantName) {
2779
2996
  headers["X-Tenant-Name"] = tenantName;
2780
2997
  }
2998
+ if (authorizationToken) {
2999
+ headers["Authorization"] = `Bearer ${authorizationToken}`;
3000
+ }
2781
3001
  return headers;
2782
3002
  }
3003
+ /**
3004
+ * Parse documents from a SyftAI-Space query response.
3005
+ *
3006
+ * Mirrors the aggregator's `DataSourceClient._parse_syftai_response`: the
3007
+ * canonical shape nests documents under `references.documents` and names the
3008
+ * score `similarity_score`. A legacy top-level `documents` list (with
3009
+ * `score`) is still honoured for backward compatibility.
3010
+ */
3011
+ parseDocuments(data) {
3012
+ const references = data["references"];
3013
+ let rawDocs;
3014
+ let scoreKey = "score";
3015
+ if (references && typeof references === "object") {
3016
+ rawDocs = references["documents"];
3017
+ scoreKey = "similarity_score";
3018
+ } else {
3019
+ rawDocs = data["documents"];
3020
+ }
3021
+ const documents = [];
3022
+ if (Array.isArray(rawDocs)) {
3023
+ for (const doc of rawDocs) {
3024
+ documents.push({
3025
+ content: String(doc["content"] ?? ""),
3026
+ score: Number(doc[scoreKey] ?? doc["score"] ?? 0),
3027
+ metadata: doc["metadata"] ?? {}
3028
+ });
3029
+ }
3030
+ }
3031
+ return documents;
3032
+ }
3033
+ /**
3034
+ * Parse the raw `policy_metadata` block from a syft-space response.
3035
+ *
3036
+ * The direct path (Boundary A) carries a top-level `policy_metadata` object
3037
+ * shaped `{ outcome, entries: [...] }`. Entries reuse the {@link BillingEntry}
3038
+ * shape (with `source` absent), so this delegates entry parsing to
3039
+ * {@link ChatResource.parseBillingEntry}. The wire keys are snake_case.
3040
+ */
3041
+ parsePolicyMetadata(data) {
3042
+ const pm = data["policy_metadata"];
3043
+ if (!pm || typeof pm !== "object") {
3044
+ return void 0;
3045
+ }
3046
+ const meta = pm;
3047
+ const entriesRaw = Array.isArray(meta["entries"]) ? meta["entries"] : [];
3048
+ return {
3049
+ outcome: String(meta["outcome"] ?? ""),
3050
+ entries: entriesRaw.map(
3051
+ (e) => ChatResource.parseBillingEntry(e)
3052
+ )
3053
+ };
3054
+ }
2783
3055
  /**
2784
3056
  * Query a data source endpoint directly.
2785
3057
  *
3058
+ * Authentication mirrors the aggregator: SyftAI-Space endpoints expect a
3059
+ * satellite bearer token whose audience is the endpoint owner's username. If
3060
+ * `authorizationToken` is not supplied, one is minted automatically when an
3061
+ * owner is known (`ownerUsername` option or `endpoint.ownerUsername`).
3062
+ *
2786
3063
  * @param options - Query options
2787
- * @returns Array of Document objects
3064
+ * @returns DataSourceQueryResult the retrieved documents plus the raw
3065
+ * `policyMetadata` block from the syft-space response (price, recipient,
3066
+ * transaction, status).
2788
3067
  * @throws {RetrievalError} If the query fails
2789
3068
  */
2790
3069
  async queryDataSource(options) {
2791
- const { endpoint, query, userEmail, topK = 5, similarityThreshold = 0.5 } = options;
3070
+ const {
3071
+ endpoint,
3072
+ query,
3073
+ userEmail,
3074
+ topK = 5,
3075
+ similarityThreshold = 0.5,
3076
+ authorizationToken,
3077
+ ownerUsername,
3078
+ pay = false
3079
+ } = options;
2792
3080
  const url = `${endpoint.url.replace(/\/$/, "")}/api/v1/endpoints/${endpoint.slug}/query`;
2793
3081
  const requestBody = {
2794
3082
  user_email: userEmail,
@@ -2797,19 +3085,44 @@ var SyftAIResource = class {
2797
3085
  limit: topK,
2798
3086
  similarity_threshold: similarityThreshold
2799
3087
  };
2800
- let response;
2801
- try {
2802
- response = await fetch(url, {
2803
- method: "POST",
2804
- headers: this.buildHeaders(endpoint.tenantName),
2805
- body: JSON.stringify(requestBody)
2806
- });
2807
- } catch (error) {
2808
- throw new RetrievalError(
2809
- `Failed to connect to data source '${endpoint.slug}': ${error instanceof Error ? error.message : String(error)}`,
2810
- endpoint.slug,
2811
- error
2812
- );
3088
+ let token = authorizationToken;
3089
+ if (!token) {
3090
+ const audience = ownerUsername ?? endpoint.ownerUsername;
3091
+ if (audience) {
3092
+ token = await this.mintSatelliteToken(audience);
3093
+ }
3094
+ }
3095
+ const headers = this.buildHeaders(endpoint.tenantName, token);
3096
+ const postQuery = async (extraHeaders) => {
3097
+ try {
3098
+ return await fetch(url, {
3099
+ method: "POST",
3100
+ headers: { ...headers, ...extraHeaders },
3101
+ body: JSON.stringify(requestBody)
3102
+ });
3103
+ } catch (error) {
3104
+ throw new RetrievalError(
3105
+ `Failed to connect to data source '${endpoint.slug}': ${error instanceof Error ? error.message : String(error)}`,
3106
+ endpoint.slug,
3107
+ error
3108
+ );
3109
+ }
3110
+ };
3111
+ let response = await postQuery();
3112
+ if (response.status === 402 && pay) {
3113
+ let xPayment;
3114
+ try {
3115
+ xPayment = await this.payMpp(response.headers.get("www-authenticate") ?? "", endpoint.slug);
3116
+ } catch (error) {
3117
+ throw new RetrievalError(
3118
+ `Payment failed for data source '${endpoint.slug}': ${error instanceof Error ? error.message : String(error)}`,
3119
+ endpoint.slug,
3120
+ error
3121
+ );
3122
+ }
3123
+ if (xPayment) {
3124
+ response = await postQuery({ "X-Payment": xPayment });
3125
+ }
2813
3126
  }
2814
3127
  if (!response.ok) {
2815
3128
  let message = `HTTP ${response.status}`;
@@ -2821,18 +3134,10 @@ var SyftAIResource = class {
2821
3134
  throw new RetrievalError(`Data source query failed: ${message}`, endpoint.slug);
2822
3135
  }
2823
3136
  const data = await response.json();
2824
- const documents = [];
2825
- const docsData = data["documents"];
2826
- if (Array.isArray(docsData)) {
2827
- for (const doc of docsData) {
2828
- documents.push({
2829
- content: String(doc["content"] ?? ""),
2830
- score: Number(doc["score"] ?? 0),
2831
- metadata: doc["metadata"] ?? {}
2832
- });
2833
- }
2834
- }
2835
- return documents;
3137
+ return {
3138
+ documents: this.parseDocuments(data),
3139
+ policyMetadata: this.parsePolicyMetadata(data)
3140
+ };
2836
3141
  }
2837
3142
  /**
2838
3143
  * Query a model endpoint directly.
@@ -2969,8 +3274,10 @@ var SyftHubClient = class {
2969
3274
  _myEndpoints;
2970
3275
  _hub;
2971
3276
  _accounting;
3277
+ _aggregators;
2972
3278
  _agent;
2973
3279
  _chat;
3280
+ _search;
2974
3281
  _syftai;
2975
3282
  _apiTokens;
2976
3283
  /**
@@ -3089,43 +3396,23 @@ var SyftHubClient = class {
3089
3396
  * const balance = await client.accounting.getBalance();
3090
3397
  */
3091
3398
  get accounting() {
3092
- if (this._accounting) {
3093
- return this._accounting;
3094
- }
3095
- throw new AuthenticationError(
3096
- "Accounting not initialized. Call `await client.initAccounting()` after login."
3097
- );
3098
- }
3099
- /**
3100
- * Initialize the accounting (wallet) resource.
3101
- *
3102
- * The wallet API uses the same SyftHub authentication as other resources.
3103
- * This method simply verifies authentication and creates the resource.
3104
- *
3105
- * @returns The initialized AccountingResource
3106
- * @throws {AuthenticationError} If not authenticated
3107
- *
3108
- * @example
3109
- * // Login first, then initialize accounting
3110
- * await client.auth.login('alice', 'password');
3111
- * await client.initAccounting();
3112
- *
3113
- * // Now accounting is available
3114
- * const wallet = await client.accounting.getWallet();
3115
- * const balance = await client.accounting.getBalance();
3116
- */
3117
- async initAccounting() {
3118
- if (this._accounting) {
3119
- return this._accounting;
3120
- }
3121
3399
  if (!this.isAuthenticated) {
3122
3400
  throw new AuthenticationError(
3123
3401
  "Must be logged in to use accounting. Call client.auth.login() first."
3124
3402
  );
3125
3403
  }
3126
- this._accounting = new AccountingResource(this.http);
3404
+ if (!this._accounting) {
3405
+ this._accounting = new AccountingResource(this.http);
3406
+ }
3127
3407
  return this._accounting;
3128
3408
  }
3409
+ /**
3410
+ * @deprecated Accounting is now initialized automatically on first access.
3411
+ * This method is kept for backward compatibility and is a no-op.
3412
+ */
3413
+ async initAccounting() {
3414
+ return this.accounting;
3415
+ }
3129
3416
  /**
3130
3417
  * Chat resource for RAG-augmented conversations via the Aggregator.
3131
3418
  *
@@ -3158,6 +3445,28 @@ var SyftHubClient = class {
3158
3445
  }
3159
3446
  return this._chat;
3160
3447
  }
3448
+ /**
3449
+ * Retrieval-only search via the Aggregator (no model generation).
3450
+ *
3451
+ * Symmetric counterpart to {@link chat}: queries data sources for relevant
3452
+ * documents without invoking a model. Satellite-token auth and MPP payment
3453
+ * are handled by the aggregator exactly as for chat.
3454
+ *
3455
+ * @example
3456
+ * const result = await client.search.query({
3457
+ * prompt: 'Hello, world!',
3458
+ * dataSources: ['epfl-news/epfl-news'],
3459
+ * });
3460
+ * for (const doc of result.documents) {
3461
+ * console.log(doc.title, doc.content.slice(0, 80));
3462
+ * }
3463
+ */
3464
+ get search() {
3465
+ if (!this._search) {
3466
+ this._search = new SearchResource(this.chat);
3467
+ }
3468
+ return this._search;
3469
+ }
3161
3470
  /**
3162
3471
  * SyftAI-Space resource for direct endpoint queries (low-level API).
3163
3472
  *
@@ -3184,7 +3493,7 @@ var SyftHubClient = class {
3184
3493
  */
3185
3494
  get syftai() {
3186
3495
  if (!this._syftai) {
3187
- this._syftai = new SyftAIResource();
3496
+ this._syftai = new SyftAIResource(this.http);
3188
3497
  }
3189
3498
  return this._syftai;
3190
3499
  }
@@ -3208,6 +3517,12 @@ var SyftHubClient = class {
3208
3517
  * // Revoke a token
3209
3518
  * await client.apiTokens.revoke(tokenId);
3210
3519
  */
3520
+ get aggregators() {
3521
+ if (!this._aggregators) {
3522
+ this._aggregators = new AggregatorsResource(this.http);
3523
+ }
3524
+ return this._aggregators;
3525
+ }
3211
3526
  get apiTokens() {
3212
3527
  if (!this._apiTokens) {
3213
3528
  this._apiTokens = new APITokensResource(this.http);
@@ -3290,6 +3605,6 @@ var SyftHubClient = class {
3290
3605
  // src/index.ts
3291
3606
  init_errors();
3292
3607
 
3293
- export { APIError, APITokensResource, AccountingAccountExistsError, AccountingResource, AccountingServiceUnavailableError, AgentResource, AgentSessionClient, AgentSessionError, AggregatorError, AggregatorsResource, AuthenticationError, AuthorizationError, ChatResource, ConfigurationError, EndpointResolutionError, EndpointType, GenerationError, InvalidAccountingPasswordError, NetworkError, NotFoundError, PageIterator, RetrievalError, SyftAIResource, SyftHubClient, SyftHubError, UserAlreadyExistsError, UserRole, ValidationError, Visibility, createAccountingResource, getEndpointPublicPath };
3608
+ export { APIError, APITokensResource, AccountingAccountExistsError, AccountingResource, AccountingServiceUnavailableError, AgentResource, AgentSessionClient, AgentSessionError, AggregatorError, AggregatorsResource, AuthenticationError, AuthorizationError, ChatResource, ConfigurationError, EndpointResolutionError, EndpointType, GenerationError, InvalidAccountingPasswordError, NetworkError, NotFoundError, PageIterator, RetrievalError, SearchResource, SyftAIResource, SyftHubClient, SyftHubError, UserAlreadyExistsError, UserRole, ValidationError, Visibility, createAccountingResource, getEndpointPublicPath };
3294
3609
  //# sourceMappingURL=index.js.map
3295
3610
  //# sourceMappingURL=index.js.map