@nexusm/sdk 5.0.0 → 5.2.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/dist/index.mjs CHANGED
@@ -163,6 +163,12 @@ var ValidationError = class extends ApiError {
163
163
  this.details = details;
164
164
  }
165
165
  };
166
+ var UpstreamInterceptError = class extends ApiError {
167
+ constructor(message, statusCode, response) {
168
+ super(message, statusCode, response, "NEXUS_UPSTREAM_INTERCEPT");
169
+ this.name = "UpstreamInterceptError";
170
+ }
171
+ };
166
172
  var NotFoundError = class extends ApiError {
167
173
  constructor(message, response) {
168
174
  super(message, 404, response, "NEXUS_NOT_FOUND_ERROR");
@@ -574,6 +580,35 @@ var OfflineQueue = class {
574
580
  };
575
581
 
576
582
  // src/http/client.ts
583
+ function redirectHost(location) {
584
+ if (typeof location !== "string" || location === "") return "an unknown host";
585
+ try {
586
+ return new URL(location).host;
587
+ } catch {
588
+ return "an unparseable location";
589
+ }
590
+ }
591
+ function upstreamRedirectError(response, url) {
592
+ const headers = response.headers ?? {};
593
+ const host = redirectHost(headers.location ?? headers.Location);
594
+ return new UpstreamInterceptError(
595
+ `Request to ${url ?? "the API"} was redirected (HTTP ${response.status}) to ${host} instead of being answered by Nexus. This is typically an expired or missing edge credential (e.g. a Cloudflare Access service token) \u2014 the API itself was never reached.`,
596
+ response.status,
597
+ response.data
598
+ );
599
+ }
600
+ function assertNotIntercepted(response) {
601
+ if (response.config?.responseType === "text") return;
602
+ const headers = response.headers ?? {};
603
+ const raw = headers["content-type"] ?? headers["Content-Type"];
604
+ if (typeof raw !== "string" || raw === "") return;
605
+ if (raw.toLowerCase().includes("json")) return;
606
+ throw new UpstreamInterceptError(
607
+ `Request to ${response.config?.url ?? "the API"} returned HTTP ${response.status} with content-type "${raw}" where JSON was expected. Something between this client and Nexus answered the request (auth edge, proxy, or captive portal); treating it as data would look like an empty result.`,
608
+ response.status,
609
+ response.data
610
+ );
611
+ }
577
612
  var HttpClient = class {
578
613
  /**
579
614
  * Create a new HTTP client.
@@ -591,7 +626,14 @@ var HttpClient = class {
591
626
  }
592
627
  this.axios = axios.create({
593
628
  baseURL: config.baseUrl,
594
- timeout: config.timeout
629
+ timeout: config.timeout,
630
+ // Never follow redirects (Kairos#66 / Aether#372). An auth edge such as
631
+ // Cloudflare Access answers an unauthenticated call with 302 → its own
632
+ // login page; following it yields a 200 with HTML, which is
633
+ // indistinguishable from an empty result to the caller. With
634
+ // maxRedirects: 0 the 3xx fails axios' status validation and reaches the
635
+ // error interceptor, which turns it into an UpstreamInterceptError.
636
+ maxRedirects: 0
595
637
  });
596
638
  this.setupRequestInterceptor();
597
639
  this.setupResponseInterceptor();
@@ -727,6 +769,30 @@ var HttpClient = class {
727
769
  this.cache.invalidate(path.split("/").filter(Boolean)[0] ?? path);
728
770
  return result;
729
771
  }
772
+ /**
773
+ * Send a GET request and return the raw response body as a string.
774
+ *
775
+ * Intended for file-download endpoints (e.g. `GET /dashboard/export`) that
776
+ * return `text/csv` or `application/json` as a raw file stream rather than a
777
+ * JSON-parsed object. The retry and auth interceptors still apply; the
778
+ * response cache is intentionally bypassed (export payloads are not cacheable
779
+ * at the SDK layer).
780
+ *
781
+ * @param path - URL path relative to the base URL (e.g. `/dashboard/export`).
782
+ * @param params - Optional query parameters.
783
+ * @param signal - Optional {@link AbortSignal} to cancel the request.
784
+ * @returns The raw response body as a string.
785
+ */
786
+ async getText(path, params, signal) {
787
+ return this.retry.execute(async () => {
788
+ const response = await this.axios.get(path, {
789
+ params,
790
+ signal,
791
+ responseType: "text"
792
+ });
793
+ return response.data;
794
+ });
795
+ }
730
796
  /**
731
797
  * Send a DELETE request.
732
798
  *
@@ -782,8 +848,11 @@ var HttpClient = class {
782
848
  */
783
849
  setupResponseInterceptor() {
784
850
  this.axios.interceptors.response.use(
785
- // Success handler -- pass through
786
- (response) => response,
851
+ // Success handler -- pass through, except for a 2xx that is not JSON.
852
+ (response) => {
853
+ assertNotIntercepted(response);
854
+ return response;
855
+ },
787
856
  // Error handler -- normalise into NexusError hierarchy
788
857
  (error) => {
789
858
  if (axios.isCancel(error)) {
@@ -797,6 +866,11 @@ var HttpClient = class {
797
866
  )
798
867
  );
799
868
  }
869
+ if (error.response && error.response.status >= 300 && error.response.status < 400) {
870
+ return Promise.reject(
871
+ upstreamRedirectError(error.response, error.config?.url)
872
+ );
873
+ }
800
874
  if (error.response) {
801
875
  const apiError = ApiError.fromResponse(error.response);
802
876
  const reqUrl = error.config?.url ?? "";
@@ -1343,6 +1417,35 @@ var ErrorService = class extends BaseService {
1343
1417
  }
1344
1418
  };
1345
1419
 
1420
+ // src/services/dashboard.ts
1421
+ var DashboardService = class extends BaseService {
1422
+ /**
1423
+ * Export a dashboard dataset as raw file content.
1424
+ *
1425
+ * Sends `GET /dashboard/export` with the given query parameters and returns
1426
+ * the raw response body as a string. The string is exactly the file the
1427
+ * server would send for a browser download:
1428
+ * - `format: 'csv'` (default) → comma-separated text
1429
+ * - `format: 'json'` → JSON text (not parsed into an object)
1430
+ *
1431
+ * @param params - Dataset selection and format options.
1432
+ * @param options - Optional request options (e.g. AbortSignal).
1433
+ * @returns Raw file body string.
1434
+ *
1435
+ * @throws {ApiError} HTTP 401 — missing or invalid API key.
1436
+ * @throws {ApiError} HTTP 403 — `target_tenant_id` requires admin scope.
1437
+ * @throws {ApiError} HTTP 422 — `dataset` is not one of the six whitelisted values.
1438
+ */
1439
+ async export(params, options) {
1440
+ const query = { dataset: params.dataset };
1441
+ if (params.format !== void 0) query["format"] = params.format;
1442
+ if (params.target_tenant_id !== void 0) {
1443
+ query["target_tenant_id"] = params.target_tenant_id;
1444
+ }
1445
+ return this.http.getText("/dashboard/export", query, options?.signal);
1446
+ }
1447
+ };
1448
+
1346
1449
  // src/client.ts
1347
1450
  var NexusClient = class {
1348
1451
  /**
@@ -1364,6 +1467,7 @@ var NexusClient = class {
1364
1467
  this.tenants = new TenantService(this.http);
1365
1468
  this.feedback = new FeedbackService(this.http);
1366
1469
  this.errors = new ErrorService(this.http);
1470
+ this.dashboard = new DashboardService(this.http);
1367
1471
  if (resolved.autoErrorReport) {
1368
1472
  this.http.onApiError = (statusCode, method, url, detail) => {
1369
1473
  this.errors.submit({
@@ -1422,6 +1526,7 @@ export {
1422
1526
  RateLimitError,
1423
1527
  TenantService,
1424
1528
  TimeoutError,
1529
+ UpstreamInterceptError,
1425
1530
  ValidationError,
1426
1531
  apiKeyCreateSchema,
1427
1532
  contextRequestSchema,