dataiku-sdk 3.9.0 → 4.1.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.
Files changed (74) hide show
  1. package/README.md +4 -3
  2. package/dist/build-metadata.json +1 -1
  3. package/dist/packages/types/src/index.d.ts +6 -0
  4. package/dist/packages/types/src/index.js +6 -0
  5. package/dist/src/cli/coerce.d.ts +6 -0
  6. package/dist/src/cli/coerce.js +9 -0
  7. package/dist/src/cli/command-syntax.json +216 -0
  8. package/dist/src/cli/commands/app.js +2 -2
  9. package/dist/src/cli/commands/code-env.js +10 -9
  10. package/dist/src/cli/commands/code.js +16 -2
  11. package/dist/src/cli/commands/dashboard.js +1 -2
  12. package/dist/src/cli/commands/dataset.js +6 -4
  13. package/dist/src/cli/commands/flow-zone.js +1 -1
  14. package/dist/src/cli/commands/job.js +70 -16
  15. package/dist/src/cli/commands/plugin.js +7 -7
  16. package/dist/src/cli/commands/project-library.js +2 -2
  17. package/dist/src/cli/commands/project.js +21 -4
  18. package/dist/src/cli/commands/recipe.js +34 -21
  19. package/dist/src/cli/commands/scenario.js +78 -12
  20. package/dist/src/cli/contract.js +30 -7
  21. package/dist/src/cli/env.d.ts +6 -0
  22. package/dist/src/cli/env.js +10 -3
  23. package/dist/src/cli/helpers/app-permissions.d.ts +1 -1
  24. package/dist/src/cli/helpers/app-permissions.js +2 -2
  25. package/dist/src/cli/helpers/cleanup.d.ts +2 -0
  26. package/dist/src/cli/helpers/cleanup.js +122 -8
  27. package/dist/src/cli/helpers/flow-zone.d.ts +1 -1
  28. package/dist/src/cli/helpers/flow-zone.js +3 -4
  29. package/dist/src/cli/list-output.d.ts +5 -0
  30. package/dist/src/cli/list-output.js +11 -2
  31. package/dist/src/cli/list-shapes.d.ts +6 -0
  32. package/dist/src/cli/list-shapes.js +7 -1
  33. package/dist/src/cli/output-schemas.d.ts +1 -1
  34. package/dist/src/cli/output-schemas.js +1 -1
  35. package/dist/src/cli/output.d.ts +2 -0
  36. package/dist/src/cli/output.js +31 -4
  37. package/dist/src/cli/read-shapes.js +90 -0
  38. package/dist/src/cli/result-shapes.d.ts +19 -1
  39. package/dist/src/cli/result-shapes.js +227 -4
  40. package/dist/src/cli/selector-suggestions.d.ts +14 -0
  41. package/dist/src/cli/selector-suggestions.js +56 -0
  42. package/dist/src/cli/usage.d.ts +1 -1
  43. package/dist/src/cli/usage.js +29 -4
  44. package/dist/src/cli/version.d.ts +2 -2
  45. package/dist/src/cli/version.js +4 -5
  46. package/dist/src/cli.js +128 -30
  47. package/dist/src/client.d.ts +15 -2
  48. package/dist/src/client.js +36 -11
  49. package/dist/src/errors.d.ts +2 -0
  50. package/dist/src/errors.js +12 -2
  51. package/dist/src/generated/action-output-schemas.json +232 -0
  52. package/dist/src/index.d.ts +1 -1
  53. package/dist/src/resources/datasets.d.ts +11 -1
  54. package/dist/src/resources/datasets.js +91 -30
  55. package/dist/src/resources/jobs.d.ts +35 -5
  56. package/dist/src/resources/jobs.js +68 -41
  57. package/dist/src/resources/projects.js +2 -2
  58. package/dist/src/resources/recipe-create.d.ts +4 -1
  59. package/dist/src/resources/recipe-create.js +11 -2
  60. package/dist/src/resources/recipes.d.ts +12 -1
  61. package/dist/src/resources/recipes.js +39 -12
  62. package/dist/src/resources/scenarios.d.ts +8 -1
  63. package/dist/src/resources/scenarios.js +51 -12
  64. package/dist/src/resources/sql.js +3 -3
  65. package/dist/src/resources/wiki.js +3 -7
  66. package/dist/src/skill.js +1 -2
  67. package/dist/src/utils/project-archive.d.ts +25 -0
  68. package/dist/src/utils/project-archive.js +216 -5
  69. package/dist/src/utils/response-file.js +15 -8
  70. package/package.json +12 -12
  71. package/plugin.json +1 -1
  72. package/skills/dataiku-dss/SKILL.md +15 -17
  73. package/skills/dataiku-dss/references/discovery.md +2 -2
  74. package/skills/dataiku-dss/references/troubleshooting.md +1 -0
package/dist/src/cli.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import * as fs from "node:fs/promises";
3
2
  import { homedir, } from "node:os";
4
3
  import { resolve, } from "node:path";
5
- import { num, seenJsonInputCredentials, unknownJsonInput, } from "./cli/coerce.js";
4
+ import { num, resultRecord, seenJsonInputCredentials, stringField, unknownJsonInput, } from "./cli/coerce.js";
6
5
  import { commands, } from "./cli/commands/index.js";
7
6
  import { ambientProjectKey, loadEnvFile, } from "./cli/env.js";
8
7
  import { executionMode, FLAG_ALIASES, isFlagValueToken, parseArgs, rawPositionals, } from "./cli/flags.js";
9
- import { cleanupLedgerEntry, } from "./cli/helpers/cleanup.js";
10
- import { commandFailureExitCode, CommandResultFailure, enqueueCliWarning, flushCliWarnings, isFailedWaitResult, planResult, projectResultFields, setOutputFieldProjection, writeCommandResult, } from "./cli/output.js";
8
+ import { cleanupInputIdentifierField, cleanupLedgerEntry, } from "./cli/helpers/cleanup.js";
9
+ import { commandFailureExitCode, CommandResultFailure, enqueueCliWarning, flushCliWarnings, isFailedWaitResult, planResult, projectResultFields, setOutputFieldProjection, stdoutJson, writeCommandResult, } from "./cli/output.js";
11
10
  import { shapeCommandResult, } from "./cli/result-shapes.js";
12
11
  import { currentCommandContext, resolveCredentials, resolveLoginCredentials, } from "./cli/runtime.js";
12
+ import { suggestSelectors, } from "./cli/selector-suggestions.js";
13
13
  import { commandSyntaxTree, commandUsage, } from "./cli/syntax.js";
14
14
  import { COMMANDS_RUN_HINT, inferRequiresProject, missingActionError, noCommandError, requireArgs, RESOURCE_NAMES, unknownActionError, unknownFlagError, unknownResourceError, UsageError, } from "./cli/usage.js";
15
15
  import { cliVersionResult, } from "./cli/version.js";
@@ -480,7 +480,7 @@ async function validateBatchStep(argv) {
480
480
  return { positional, flags, resource, action: metaAction, };
481
481
  const resourceActions = commands[resource];
482
482
  if (!resourceActions)
483
- throw unknownResourceError(resource);
483
+ throw unknownResourceError(resource, action);
484
484
  if (!action) {
485
485
  throw missingActionError(resource, Object.keys(resourceActions), `dss ${resource} <action>`);
486
486
  }
@@ -767,6 +767,8 @@ async function commandsRunResult(flags) {
767
767
  return contract.commandActionSummary();
768
768
  }
769
769
  const registry = selectors.length === 0 ? contract.buildCommandRegistry() : {};
770
+ const knownSelectors = [];
771
+ const unknownSelectors = {};
770
772
  for (const selector of selectors) {
771
773
  const selectorParts = selector.split(".");
772
774
  if (selectorParts.some((part) => part.length === 0)) {
@@ -774,19 +776,65 @@ async function commandsRunResult(flags) {
774
776
  }
775
777
  const [selectedResource, selectedAction,] = selectorParts;
776
778
  const resourceActions = cachedCommandRegistry(selectedResource);
777
- if (!resourceActions)
778
- throw unknownResourceError(selectedResource);
779
- registry[selectedResource] = resourceActions;
780
- if (selectedAction && !resourceActions[selectedAction]) {
781
- throw unknownActionError(selectedResource, selectedAction, Object.keys(resourceActions));
779
+ const error = !resourceActions
780
+ ? unknownResourceError(selectedResource)
781
+ : selectedAction && !resourceActions[selectedAction]
782
+ ? unknownActionError(selectedResource, selectedAction, Object.keys(resourceActions))
783
+ : undefined;
784
+ if (resourceActions && !error) {
785
+ registry[selectedResource] = resourceActions;
786
+ knownSelectors.push(selector);
787
+ continue;
782
788
  }
789
+ unknownSelectors[selector] = {
790
+ error: error,
791
+ suggestions: suggestSelectors(selector, contract.commandActionSummary()),
792
+ };
793
+ }
794
+ const unknownNames = Object.keys(unknownSelectors);
795
+ if (unknownNames.length > 0) {
796
+ if (knownSelectors.length === 0 && selectors.length > 0) {
797
+ const { error, suggestions, } = unknownSelectors[unknownNames[0]];
798
+ // The shared error suggests `dss resource action` commands; here the fix is a selector.
799
+ const hint = suggestions.length === 0
800
+ ? COMMANDS_RUN_HINT
801
+ : `Did you mean ${suggestions.map((name) => `\`--fields ${name}\``).join(" or ")}? ${COMMANDS_RUN_HINT}`;
802
+ const { suggestions: _commandSuggestions, ...details } = error.details ?? {};
803
+ throw new UsageError(error.message, error.code, hint, {
804
+ ...details,
805
+ ...(suggestions.length > 0 ? { suggestions, } : {}),
806
+ });
807
+ }
808
+ enqueueCliWarning({
809
+ code: "unknown_fields",
810
+ fields: unknownNames,
811
+ suggestions: Object.fromEntries(unknownNames.flatMap((name) => unknownSelectors[name].suggestions.length > 0
812
+ ? [[name, unknownSelectors[name].suggestions,],]
813
+ : [])),
814
+ hint: "Unknown selectors were skipped; the result holds the known ones. Run `dss commands run` for resource/action names.",
815
+ });
816
+ }
817
+ // On stdout a bare RESOURCE selector lists its actions (usage, description, side effect):
818
+ // the full entries (flags, schemas, examples) run 30-55 KB per resource, more than an agent
819
+ // can read in one result. RESOURCE.ACTION and --output keep the full entries.
820
+ const fullSelectors = typeof outputPath === "string"
821
+ ? knownSelectors
822
+ : knownSelectors.filter((selector) => selector.includes("."));
823
+ const selectedRegistry = selectors.length === 0 ? registry : {};
824
+ if (fullSelectors.length > 0) {
825
+ Object.assign(selectedRegistry, projectResultFields(registry, fullSelectors, () => Object.keys(contract.commandActionSummary()).sort()));
826
+ }
827
+ for (const resource of knownSelectors) {
828
+ if (fullSelectors.includes(resource))
829
+ continue;
830
+ selectedRegistry[resource] = Object.fromEntries(Object.entries(registry[resource]).map(([action, entry,]) => [
831
+ action,
832
+ { usage: entry.usage, description: entry.description, sideEffect: entry.sideEffect, },
833
+ ]));
783
834
  }
784
- const selectedRegistry = selectors.length === 0
785
- ? registry
786
- : projectResultFields(registry, selectors, () => Object.keys(contract.commandActionSummary()).sort());
787
835
  if (typeof outputPath === "string") {
788
836
  const exported = selectedRegistry;
789
- await fs.writeFile(outputPath, `${JSON.stringify(exported)}\n`, "utf-8");
837
+ await Bun.write(outputPath, `${JSON.stringify(exported)}\n`);
790
838
  return { path: outputPath, };
791
839
  }
792
840
  return selectedRegistry;
@@ -802,8 +850,9 @@ function assertCleanupLedgerSupported(resource, action, flags) {
802
850
  if (!contract.supportsCleanupLedger(resource, action)) {
803
851
  throw new UsageError(`--record-cleanup is not supported for ${resource} ${action}.`);
804
852
  }
805
- if (resource === "workspace" && action === "create" && flags["stdin"] === true) {
806
- throw new UsageError("workspace create cannot combine --record-cleanup with --stdin because the cleanup identifier cannot be replayed from a consumed stream.", "conflicting_input_sources", "Use --data or --data-file so the workspaceKey remains available for the cleanup ledger.");
853
+ const pinnedField = cleanupInputIdentifierField(resource, action);
854
+ if (pinnedField !== undefined && flags["stdin"] === true) {
855
+ throw new UsageError(`${resource} ${action} cannot combine --record-cleanup with --stdin because the cleanup identifier cannot be replayed from a consumed stream.`, "conflicting_input_sources", `Use --data or --data-file so the ${pinnedField} remains available for the cleanup ledger.`);
807
856
  }
808
857
  }
809
858
  /**
@@ -856,11 +905,36 @@ async function preflightCleanupLedgerForFlags(client, flags) {
856
905
  }
857
906
  }
858
907
  /**
859
- * Append the cleanup entry for a completed handler result. Callers must pass the
860
- * raw, unprojected result and must call this before any failure exit code is
861
- * raised: an indeterminate post-POST outcome stays addressable only when the
862
- * ledger records it first. Eligibility (dry runs, skipped work, an explicit
863
- * `cleanupEligible:false`) is decided by `cleanupLedgerEntry`, never here.
908
+ * What the command result already says about the created object, so a failed
909
+ * ledger write never hides it: the `created`/`saved` receipt, the object `id`
910
+ * and `name`, and the project key DSS landed it in. Fields stay distinct (a
911
+ * name is not an id) and absent ones are omitted rather than guessed.
912
+ */
913
+ function createdObjectFromResult(result, fallbackProjectKey) {
914
+ const record = resultRecord(result);
915
+ const created = stringField(record, ["created", "saved", "usedProjectKey",]);
916
+ const id = stringField(record, ["id",]);
917
+ const name = stringField(record, ["name",]);
918
+ const projectKey = stringField(record, ["projectKey", "usedProjectKey", "targetProjectKey",])
919
+ ?? fallbackProjectKey;
920
+ return {
921
+ ...(created ? { created, } : {}),
922
+ ...(id ? { id, } : {}),
923
+ ...(name ? { name, } : {}),
924
+ ...(projectKey ? { projectKey, } : {}),
925
+ };
926
+ }
927
+ /**
928
+ * Append the cleanup entry for a completed handler result. Callers must call
929
+ * this before any failure exit code is raised: an indeterminate post-POST
930
+ * outcome stays addressable only when the ledger records it first. Eligibility
931
+ * (dry runs, skipped work, an explicit `cleanupEligible:false`) is decided by
932
+ * `cleanupLedgerEntry`, never here.
933
+ *
934
+ * The mutation has already happened when this runs, so every failure here is
935
+ * an `ambiguous_outcome` that says so (`mutationSucceeded: true`,
936
+ * `cleanupRecorded: false`) and carries the created identifier/project key:
937
+ * callers must never retry the create, only clean up or record it themselves.
864
938
  */
865
939
  async function recordCleanupLedgerEntry(client, resource, action, args, flags, result, projectKey) {
866
940
  const ledgerPath = flags["record-cleanup"];
@@ -868,15 +942,33 @@ async function recordCleanupLedgerEntry(client, resource, action, args, flags, r
868
942
  return;
869
943
  const entry = cleanupLedgerEntry(resource, action, args, flags, result, projectKey);
870
944
  if (entry) {
871
- await appendCleanupLedgerEntry(ledgerPath, entry, client.getBaseUrl());
945
+ try {
946
+ await appendCleanupLedgerEntry(ledgerPath, entry, client.getBaseUrl());
947
+ }
948
+ catch (error) {
949
+ throw new ClientValidationError(`Mutation completed but the cleanup entry for ${resource} ${action} could not be recorded: ${error instanceof Error ? error.message : String(error)}`, "ambiguous_outcome", "The mutation already succeeded: do not retry it. Remove the created object manually or append this cleanup entry to the ledger yourself.", {
950
+ resource,
951
+ action,
952
+ ledgerPath,
953
+ mutationSucceeded: true,
954
+ cleanupRecorded: false,
955
+ ...createdObjectFromResult(result, projectKey),
956
+ cleanup: entry.cleanup,
957
+ });
958
+ }
872
959
  return;
873
960
  }
874
- const record = result !== null && typeof result === "object" && !Array.isArray(result)
875
- ? result
876
- : {};
961
+ const record = resultRecord(result);
877
962
  if (record.skipped !== undefined || record.cleanupEligible === false)
878
963
  return;
879
- throw new ClientValidationError(`Mutation completed but no cleanup entry could be derived for ${resource} ${action}.`, "ambiguous_outcome", "Inspect the created object and remove it manually; do not assume the requested cleanup ledger contains it.", { resource, action, ledgerPath, });
964
+ throw new ClientValidationError(`Mutation completed but no cleanup entry could be derived for ${resource} ${action}.`, "ambiguous_outcome", "The mutation already succeeded: do not retry it. Inspect the created object and remove it manually; the requested cleanup ledger does not contain it.", {
965
+ resource,
966
+ action,
967
+ ledgerPath,
968
+ mutationSucceeded: true,
969
+ cleanupRecorded: false,
970
+ ...createdObjectFromResult(result, projectKey),
971
+ });
880
972
  }
881
973
  async function runBatch(flags) {
882
974
  const payload = unknownJsonInput(flags);
@@ -1003,7 +1095,7 @@ async function runBatch(flags) {
1003
1095
  else {
1004
1096
  const resourceActions = commands[resource];
1005
1097
  if (!resourceActions)
1006
- throw unknownResourceError(resource);
1098
+ throw unknownResourceError(resource, action);
1007
1099
  if (!action) {
1008
1100
  throw missingActionError(resource, Object.keys(resourceActions), `dss ${resource} <action>`);
1009
1101
  }
@@ -1306,15 +1398,21 @@ function buildErrorReport(err, contextOverride) {
1306
1398
  if (err instanceof ClientValidationError
1307
1399
  && (err.code === "ambiguous_outcome" || err.code === "assertion_failed"
1308
1400
  || err.code === "long_running_failure")) {
1401
+ // The DSS response behind an indeterminate outcome stays visible (status,
1402
+ // sanitized message), e.g. a 500 that names the conflicting project key.
1403
+ const cause = err.cause instanceof DataikuError ? err.cause : undefined;
1404
+ const diagnostics = cause ? dssDiagnosticDetails(cause) : {};
1405
+ const details = { ...diagnostics, ...err.details, };
1309
1406
  return {
1310
1407
  type: "error",
1311
1408
  error: err.message,
1312
1409
  code: err.code,
1313
1410
  category: "dss",
1314
1411
  exitCode,
1412
+ ...(cause ? { status: cause.status, } : {}),
1315
1413
  retryable: false,
1316
1414
  ...(err.hint ? { hint: err.hint, } : {}),
1317
- ...(err.details ? { details: err.details, } : {}),
1415
+ ...(Object.keys(details).length > 0 ? { details, } : {}),
1318
1416
  ...scope,
1319
1417
  };
1320
1418
  }
@@ -1412,7 +1510,7 @@ function buildErrorReport(err, contextOverride) {
1412
1510
  }
1413
1511
  function writeErrorReport(err) {
1414
1512
  flushCliWarnings();
1415
- process.stdout.write(`${JSON.stringify(buildErrorReport(err))}\n`);
1513
+ process.stdout.write(stdoutJson(buildErrorReport(err)));
1416
1514
  }
1417
1515
  /** Only ambiguous mutation failures require full contract metadata. */
1418
1516
  function errorReportNeedsCommandRuntime(err) {
@@ -1460,7 +1558,7 @@ async function main() {
1460
1558
  }
1461
1559
  // Resource and action recovery require only this resource's definitions.
1462
1560
  if (!RESOURCE_NAMES.includes(resource))
1463
- throw unknownResourceError(resource);
1561
+ throw unknownResourceError(resource, positional[1]);
1464
1562
  const resourceActions = commands[resource];
1465
1563
  if (positional.length === 1) {
1466
1564
  throw missingActionError(resource, Object.keys(resourceActions), `dss ${resource} <action> [args...]`);
@@ -153,7 +153,13 @@ export declare class DataikuClient {
153
153
  private readonly maxResponseBodyBytes;
154
154
  private readonly retryMaxAttempts;
155
155
  private readonly verbose;
156
- private readonly tlsOptions;
156
+ /**
157
+ * Connection pool carrying this client's TLS settings (custom CA, disabled
158
+ * verification). A session never shares connections with plain `fetch` or
159
+ * another client, so a connection opened with relaxed TLS cannot serve a
160
+ * client that expects strict verification. Default-TLS clients use plain fetch.
161
+ */
162
+ private readonly session;
157
163
  private readonly onTrace;
158
164
  private readonly onValidationWarning;
159
165
  private projectsResource?;
@@ -294,7 +300,8 @@ export declare class DataikuClient {
294
300
  * When the response exceeds the cap, earlier bytes are discarded, only
295
301
  * the tail is kept and `truncated` is true — so callers that need the end
296
302
  * of a large document (e.g. job logs) stay bounded without losing the most
297
- * recent content. The read runs under the request deadline.
303
+ * recent content. The request timeout applies per received chunk (a stall
304
+ * deadline); the whole transfer is capped at 10x the request timeout.
298
305
  */
299
306
  getTextTailLimited(path: string, maxBytes: number): Promise<{
300
307
  text: string;
@@ -385,6 +392,12 @@ export declare class DataikuClient {
385
392
  * Returns `truncated: true` when bytes had to be dropped. Bytes that fall
386
393
  * in the middle of a UTF-8 character are dropped from both ends so the
387
394
  * decoded text never contains a replacement character.
395
+ *
396
+ * The request timeout applies per chunk (a stall deadline), like the
397
+ * streaming path: a body that stops sending for longer than the timeout
398
+ * fails with the body-read timeout error. A total budget of
399
+ * {@link TAIL_READ_TOTAL_BUDGET_FACTOR} x the request timeout bounds a body
400
+ * that keeps trickling, with an error naming that limit.
388
401
  */
389
402
  private readBodyTextTail;
390
403
  private buildResponseTooLargeError;
@@ -2,7 +2,7 @@ import { readFileSync, } from "node:fs";
2
2
  import { basename, } from "node:path";
3
3
  import { Value, } from "@sinclair/typebox/value";
4
4
  import { safeParseSchema, } from "./schemas.js";
5
- import { classifyDataikuError, ClientValidationError, DataikuError, nonJsonResponseBody, } from "./errors.js";
5
+ import { classifyDataikuError, ClientValidationError, DataikuError, nonJsonResponseBody, RESPONSE_TOO_LARGE_STATUS_TEXT, } from "./errors.js";
6
6
  /* ------------------------------------------------------------------ */
7
7
  /* Constants */
8
8
  /* ------------------------------------------------------------------ */
@@ -18,7 +18,6 @@ const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
18
18
  * streaming methods (stream, postStream) are unaffected.
19
19
  */
20
20
  const DEFAULT_MAX_BUFFERED_BODY_BYTES = 50 * 1024 * 1024;
21
- const RESPONSE_TOO_LARGE_STATUS_TEXT = "Response Too Large";
22
21
  /* ------------------------------------------------------------------ */
23
22
  /* Helpers */
24
23
  /* ------------------------------------------------------------------ */
@@ -115,6 +114,8 @@ function trimFromUtf8BoundaryStart(bytes) {
115
114
  function buildBodyReadTimeoutError(timeoutMs) {
116
115
  return new DataikuError(0, "Request Timeout", `Request timed out after ${timeoutMs}ms while reading the response body.`);
117
116
  }
117
+ /** Total budget of a tail read (e.g. `job log`) as a multiple of the per-chunk stall timeout. */
118
+ const TAIL_READ_TOTAL_BUDGET_FACTOR = 10;
118
119
  /**
119
120
  * Read the next chunk of a response body under a hard deadline.
120
121
  * When `remainingMs` elapses, the stream is cancelled and the caller's
@@ -211,7 +212,13 @@ export class DataikuClient {
211
212
  maxResponseBodyBytes;
212
213
  retryMaxAttempts;
213
214
  verbose;
214
- tlsOptions;
215
+ /**
216
+ * Connection pool carrying this client's TLS settings (custom CA, disabled
217
+ * verification). A session never shares connections with plain `fetch` or
218
+ * another client, so a connection opened with relaxed TLS cannot serve a
219
+ * client that expects strict verification. Default-TLS clients use plain fetch.
220
+ */
221
+ session;
215
222
  onTrace;
216
223
  onValidationWarning;
217
224
  /* Resource namespaces — lazily initialized to break circular imports */
@@ -606,7 +613,8 @@ export class DataikuClient {
606
613
  const rawMax = config?.retryMaxAttempts ?? DEFAULT_RETRY_MAX_ATTEMPTS;
607
614
  this.retryMaxAttempts = Math.min(Math.max(1, rawMax), MAX_RETRY_ATTEMPTS_CAP);
608
615
  this.verbose = config?.verbose === true;
609
- this.tlsOptions = config ? buildFetchTlsOptions(config) : undefined;
616
+ const tls = config ? buildFetchTlsOptions(config) : undefined;
617
+ this.session = tls ? new Bun.FetchSession({ tls, }) : undefined;
610
618
  this.onTrace = config?.onTrace ?? defaultTrace;
611
619
  this.onValidationWarning = config?.onValidationWarning ?? defaultValidationWarning;
612
620
  }
@@ -691,7 +699,8 @@ export class DataikuClient {
691
699
  * When the response exceeds the cap, earlier bytes are discarded, only
692
700
  * the tail is kept and `truncated` is true — so callers that need the end
693
701
  * of a large document (e.g. job logs) stay bounded without losing the most
694
- * recent content. The read runs under the request deadline.
702
+ * recent content. The request timeout applies per received chunk (a stall
703
+ * deadline); the whole transfer is capped at 10x the request timeout.
695
704
  */
696
705
  async getTextTailLimited(path, maxBytes) {
697
706
  const limit = Math.max(0, Math.floor(maxBytes));
@@ -1080,6 +1089,12 @@ export class DataikuClient {
1080
1089
  * Returns `truncated: true` when bytes had to be dropped. Bytes that fall
1081
1090
  * in the middle of a UTF-8 character are dropped from both ends so the
1082
1091
  * decoded text never contains a replacement character.
1092
+ *
1093
+ * The request timeout applies per chunk (a stall deadline), like the
1094
+ * streaming path: a body that stops sending for longer than the timeout
1095
+ * fails with the body-read timeout error. A total budget of
1096
+ * {@link TAIL_READ_TOTAL_BUDGET_FACTOR} x the request timeout bounds a body
1097
+ * that keeps trickling, with an error naming that limit.
1083
1098
  */
1084
1099
  async readBodyTextTail(res, maxBytes) {
1085
1100
  const limit = Math.max(0, Math.floor(maxBytes));
@@ -1090,13 +1105,24 @@ export class DataikuClient {
1090
1105
  let writeOffset = 0;
1091
1106
  let retainedBytes = 0;
1092
1107
  let truncated = false;
1108
+ // Stall deadline per chunk, plus an overall cap so a body that trickles
1109
+ // forever still ends; the error names which limit fired.
1110
+ const totalBudgetMs = this.requestTimeoutMs * TAIL_READ_TOTAL_BUDGET_FACTOR;
1093
1111
  const startedAt = Date.now();
1094
1112
  try {
1095
1113
  while (true) {
1096
- const remainingMs = this.requestTimeoutMs - (Date.now() - startedAt);
1097
- if (remainingMs <= 0)
1098
- throw buildBodyReadTimeoutError(this.requestTimeoutMs);
1099
- const { done, value, } = await readChunkWithDeadline(reader, remainingMs, this.requestTimeoutMs);
1114
+ const totalRemainingMs = totalBudgetMs - (Date.now() - startedAt);
1115
+ const totalBound = totalRemainingMs < this.requestTimeoutMs;
1116
+ let chunk;
1117
+ try {
1118
+ chunk = await readChunkWithDeadline(reader, Math.max(0, Math.min(this.requestTimeoutMs, totalRemainingMs)), this.requestTimeoutMs);
1119
+ }
1120
+ catch (error) {
1121
+ if (!totalBound || !(error instanceof DataikuError) || error.status !== 0)
1122
+ throw error;
1123
+ throw new DataikuError(0, "Request Timeout", `Response body was still arriving after ${totalBudgetMs}ms (the total read limit is ${TAIL_READ_TOTAL_BUDGET_FACTOR}x --request-timeout); raise --request-timeout to allow more time.`);
1124
+ }
1125
+ const { done, value, } = chunk;
1100
1126
  if (done)
1101
1127
  break;
1102
1128
  if (limit === 0) {
@@ -1187,9 +1213,8 @@ export class DataikuClient {
1187
1213
  ...init,
1188
1214
  method,
1189
1215
  signal: controller.signal,
1216
+ ...(this.session ? { session: this.session, } : {}),
1190
1217
  };
1191
- if (this.tlsOptions)
1192
- requestInit.tls = this.tlsOptions;
1193
1218
  const res = await fetch(url, requestInit);
1194
1219
  this.logTrace({
1195
1220
  phase: "response",
@@ -51,6 +51,8 @@ export declare const UNEXPECTED_RESPONSE_MARKER = "Unexpected DSS response";
51
51
  export declare function unexpectedResponseError(detail: string, status?: number): DataikuError;
52
52
  /** Prefix for a SQL query failure reported by DSS on a 2xx finish-streaming response. */
53
53
  export declare const SQL_QUERY_FAILED_MARKER = "SQL query failed in DSS";
54
+ /** statusText of a DataikuError raised when a response exceeds a client-side byte budget. */
55
+ export declare const RESPONSE_TOO_LARGE_STATUS_TEXT = "Response Too Large";
54
56
  /** Generic permission hint; the CLI names the project when the request was project-scoped. */
55
57
  export declare const FORBIDDEN_HINT = "Check API key validity and project permissions for the requested action.";
56
58
  /** Generic 404 hint; the CLI names the resource and project when it knows them. */
@@ -78,6 +78,9 @@ export function unexpectedResponseError(detail, status = 200) {
78
78
  const DEFINITE_FAILURE_HINT = "DSS reported that the request failed (for SQL: syntax error, missing object, or missing privileges of the connection's role). Fix the request or grants; do not retry unchanged.";
79
79
  /** Prefix for a SQL query failure reported by DSS on a 2xx finish-streaming response. */
80
80
  export const SQL_QUERY_FAILED_MARKER = "SQL query failed in DSS";
81
+ /** statusText of a DataikuError raised when a response exceeds a client-side byte budget. */
82
+ export const RESPONSE_TOO_LARGE_STATUS_TEXT = "Response Too Large";
83
+ const RESPONSE_TOO_LARGE_HINT = "The response exceeded a client-side size limit; retrying returns the same body. Narrow the request (fewer rows or columns, --preview, --output PATH) instead.";
81
84
  const TLS_CERTIFICATE_HINT = "TLS certificate verification failed. Trust the DSS/corporate CA with --ca-cert PATH or NODE_EXTRA_CA_CERTS, or save it for every process and directory with `dss auth login --ca-cert PATH` (a project .env applies only when dss runs in that directory); use --insecure only for temporary troubleshooting.";
82
85
  /** Generic permission hint; the CLI names the project when the request was project-scoped. */
83
86
  export const FORBIDDEN_HINT = "Check API key validity and project permissions for the requested action.";
@@ -388,14 +391,21 @@ export class DataikuError extends Error {
388
391
  this.body = body;
389
392
  this.name = "DataikuError";
390
393
  const definite = trustedMetadata?.definiteFailure === true;
394
+ // A client-side byte budget tripped: the same request returns the same
395
+ // oversized body, so retrying cannot help (it is not a transport fault).
396
+ const overBudget = statusText === RESPONSE_TOO_LARGE_STATUS_TEXT;
391
397
  // A definite DSS refusal is not a transport/server fault: replaying it
392
398
  // unchanged cannot succeed. Keep a more specific forbidden/not_found class.
393
- const reclassify = definite
399
+ const reclassify = overBudget || definite
394
400
  && (details.category === "transient" || details.category === "unknown"
395
401
  || details.category === "unexpected_response");
396
402
  this.category = reclassify ? "validation" : details.category;
397
403
  this.retryable = reclassify ? false : details.retryable;
398
- this.retryHint = reclassify ? DEFINITE_FAILURE_HINT : details.retryHint;
404
+ this.retryHint = overBudget
405
+ ? RESPONSE_TOO_LARGE_HINT
406
+ : reclassify
407
+ ? DEFINITE_FAILURE_HINT
408
+ : details.retryHint;
399
409
  this.retry = retry;
400
410
  this.requestId = requestId;
401
411
  this.trustedTarget = trustedMetadata?.target;