@syfthub/sdk 0.2.0 → 0.2.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
@@ -2769,28 +2769,125 @@ var GenerationError = class extends exports.SyftHubError {
2769
2769
  }
2770
2770
  };
2771
2771
  var SyftAIResource = class {
2772
- // No dependencies - uses direct fetch to SyftAI-Space endpoints
2772
+ /**
2773
+ * @param http - Hub HTTP client, used to mint satellite tokens and settle
2774
+ * MPP payments. Endpoint queries themselves use direct `fetch`, since the
2775
+ * SyftAI-Space URL is arbitrary and not the Hub base URL.
2776
+ */
2777
+ constructor(http) {
2778
+ this.http = http;
2779
+ }
2780
+ /**
2781
+ * Mint a satellite token for `audience` (the endpoint owner's username).
2782
+ *
2783
+ * Mirrors the aggregator's token coordination layer: try an authenticated
2784
+ * token first, then fall back to a guest token. Returns `undefined` if both
2785
+ * fail, so the caller can still attempt an unauthenticated request.
2786
+ */
2787
+ async mintSatelliteToken(audience) {
2788
+ if (this.http.hasTokens()) {
2789
+ try {
2790
+ const res = await this.http.get("/api/v1/token", {
2791
+ aud: audience
2792
+ });
2793
+ if (res.targetToken) return res.targetToken;
2794
+ } catch {
2795
+ }
2796
+ }
2797
+ try {
2798
+ const res = await this.http.get(
2799
+ "/api/v1/token/guest",
2800
+ { aud: audience },
2801
+ { includeAuth: false }
2802
+ );
2803
+ return res.targetToken;
2804
+ } catch {
2805
+ return void 0;
2806
+ }
2807
+ }
2808
+ /**
2809
+ * Pay an MPP `402` challenge via the Hub wallet, returning an X-Payment credential.
2810
+ *
2811
+ * Mirrors the aggregator's `handleMppPayment`: the `WWW-Authenticate`
2812
+ * challenge is forwarded verbatim to the Hub's `/api/v1/wallet/pay`, which
2813
+ * parses it and returns an `x_payment` string to attach to a retry.
2814
+ */
2815
+ async payMpp(wwwAuthenticate, slug) {
2816
+ if (!wwwAuthenticate) return void 0;
2817
+ const res = await this.http.post("/api/v1/wallet/pay", {
2818
+ wwwAuthenticate,
2819
+ endpointSlug: slug
2820
+ });
2821
+ return res.xPayment;
2822
+ }
2773
2823
  /**
2774
2824
  * Build headers for SyftAI-Space request.
2775
2825
  */
2776
- buildHeaders(tenantName) {
2826
+ buildHeaders(tenantName, authorizationToken) {
2777
2827
  const headers = {
2778
2828
  "Content-Type": "application/json"
2779
2829
  };
2780
2830
  if (tenantName) {
2781
2831
  headers["X-Tenant-Name"] = tenantName;
2782
2832
  }
2833
+ if (authorizationToken) {
2834
+ headers["Authorization"] = `Bearer ${authorizationToken}`;
2835
+ }
2783
2836
  return headers;
2784
2837
  }
2838
+ /**
2839
+ * Parse documents from a SyftAI-Space query response.
2840
+ *
2841
+ * Mirrors the aggregator's `DataSourceClient._parse_syftai_response`: the
2842
+ * canonical shape nests documents under `references.documents` and names the
2843
+ * score `similarity_score`. A legacy top-level `documents` list (with
2844
+ * `score`) is still honoured for backward compatibility.
2845
+ */
2846
+ parseDocuments(data) {
2847
+ const references = data["references"];
2848
+ let rawDocs;
2849
+ let scoreKey = "score";
2850
+ if (references && typeof references === "object") {
2851
+ rawDocs = references["documents"];
2852
+ scoreKey = "similarity_score";
2853
+ } else {
2854
+ rawDocs = data["documents"];
2855
+ }
2856
+ const documents = [];
2857
+ if (Array.isArray(rawDocs)) {
2858
+ for (const doc of rawDocs) {
2859
+ documents.push({
2860
+ content: String(doc["content"] ?? ""),
2861
+ score: Number(doc[scoreKey] ?? doc["score"] ?? 0),
2862
+ metadata: doc["metadata"] ?? {}
2863
+ });
2864
+ }
2865
+ }
2866
+ return documents;
2867
+ }
2785
2868
  /**
2786
2869
  * Query a data source endpoint directly.
2787
2870
  *
2871
+ * Authentication mirrors the aggregator: SyftAI-Space endpoints expect a
2872
+ * satellite bearer token whose audience is the endpoint owner's username. If
2873
+ * `authorizationToken` is not supplied, one is minted automatically when an
2874
+ * owner is known (`ownerUsername` option or `endpoint.ownerUsername`).
2875
+ *
2788
2876
  * @param options - Query options
2789
2877
  * @returns Array of Document objects
2790
2878
  * @throws {RetrievalError} If the query fails
2791
2879
  */
2792
2880
  async queryDataSource(options) {
2793
- const { endpoint, query, userEmail, topK = 5, similarityThreshold = 0.5 } = options;
2881
+ const {
2882
+ endpoint,
2883
+ query,
2884
+ userEmail,
2885
+ topK = 5,
2886
+ similarityThreshold = 0.5,
2887
+ authorizationToken,
2888
+ ownerUsername,
2889
+ pay = false
2890
+ } = options;
2794
2891
  const url = `${endpoint.url.replace(/\/$/, "")}/api/v1/endpoints/${endpoint.slug}/query`;
2795
2892
  const requestBody = {
2796
2893
  user_email: userEmail,
@@ -2799,19 +2896,44 @@ var SyftAIResource = class {
2799
2896
  limit: topK,
2800
2897
  similarity_threshold: similarityThreshold
2801
2898
  };
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
- );
2899
+ let token = authorizationToken;
2900
+ if (!token) {
2901
+ const audience = ownerUsername ?? endpoint.ownerUsername;
2902
+ if (audience) {
2903
+ token = await this.mintSatelliteToken(audience);
2904
+ }
2905
+ }
2906
+ const headers = this.buildHeaders(endpoint.tenantName, token);
2907
+ const postQuery = async (extraHeaders) => {
2908
+ try {
2909
+ return await fetch(url, {
2910
+ method: "POST",
2911
+ headers: { ...headers, ...extraHeaders },
2912
+ body: JSON.stringify(requestBody)
2913
+ });
2914
+ } catch (error) {
2915
+ throw new RetrievalError(
2916
+ `Failed to connect to data source '${endpoint.slug}': ${error instanceof Error ? error.message : String(error)}`,
2917
+ endpoint.slug,
2918
+ error
2919
+ );
2920
+ }
2921
+ };
2922
+ let response = await postQuery();
2923
+ if (response.status === 402 && pay) {
2924
+ let xPayment;
2925
+ try {
2926
+ xPayment = await this.payMpp(response.headers.get("www-authenticate") ?? "", endpoint.slug);
2927
+ } catch (error) {
2928
+ throw new RetrievalError(
2929
+ `Payment failed for data source '${endpoint.slug}': ${error instanceof Error ? error.message : String(error)}`,
2930
+ endpoint.slug,
2931
+ error
2932
+ );
2933
+ }
2934
+ if (xPayment) {
2935
+ response = await postQuery({ "X-Payment": xPayment });
2936
+ }
2815
2937
  }
2816
2938
  if (!response.ok) {
2817
2939
  let message = `HTTP ${response.status}`;
@@ -2823,17 +2945,7 @@ var SyftAIResource = class {
2823
2945
  throw new RetrievalError(`Data source query failed: ${message}`, endpoint.slug);
2824
2946
  }
2825
2947
  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
- }
2948
+ const documents = this.parseDocuments(data);
2837
2949
  return documents;
2838
2950
  }
2839
2951
  /**
@@ -3186,7 +3298,7 @@ var SyftHubClient = class {
3186
3298
  */
3187
3299
  get syftai() {
3188
3300
  if (!this._syftai) {
3189
- this._syftai = new SyftAIResource();
3301
+ this._syftai = new SyftAIResource(this.http);
3190
3302
  }
3191
3303
  return this._syftai;
3192
3304
  }