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