dataiku-sdk 3.8.0 → 3.9.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.
@@ -80,7 +80,8 @@ export declare class JobsResource extends BaseResource {
80
80
  /**
81
81
  * Retrieve job log text.
82
82
  * Returns the last `maxLogLines` lines (default 500) from the tail.
83
- * Use `0` or `-1` to return the log without line truncation.
83
+ * Use `0` or `-1` to return the log without line truncation. Credential-like
84
+ * JSON pairs (`"jobTicketSecret":"..."`) are redacted.
84
85
  *
85
86
  * The download is byte-bounded and deadline-covered: at most `maxLogBytes`
86
87
  * (default 10 MiB) of the *most recent* log output is retained, so a
@@ -2,6 +2,7 @@ import { ClientValidationError, DataikuError, } from "../errors.js";
2
2
  import { JobSummaryArraySchema, } from "../schemas.js";
3
3
  import { computeNextPollDelayMs, DEFAULT_POLL_INTERVAL_MS, DEFAULT_TIMEOUT_MS, isRequestDeadlineError, } from "../utils/polling.js";
4
4
  import { asRecord, } from "../utils/records.js";
5
+ import { redactCredentialPairs, } from "../utils/secret-sanitize.js";
5
6
  import { BaseResource, } from "./base.js";
6
7
  const DEFAULT_MAX_LOG_LINES = 500;
7
8
  /**
@@ -104,6 +105,41 @@ function dssRecordMessage(line) {
104
105
  function isErrorContent(line) {
105
106
  return ERROR_KEYWORDS.test(line) || ERROR_CLASS_NAME.test(line) || PY_FILE_FRAME.test(line);
106
107
  }
108
+ /**
109
+ * Remove embedded JSON object payloads (`{"...": ...}`) from a DSS record
110
+ * line. Structured payloads such as `SQL status {"statusWarnLevel":"ERROR",...}`
111
+ * carry level/class words as data, not as a failure of the record itself.
112
+ * An unbalanced payload (truncated line) is dropped to the end of the line.
113
+ */
114
+ function stripJsonPayloads(line) {
115
+ let out = "";
116
+ let from = 0;
117
+ for (let start = line.indexOf('{"'); start !== -1; start = line.indexOf('{"', from)) {
118
+ out += line.slice(from, start);
119
+ let depth = 0;
120
+ let inString = false;
121
+ let end = line.length;
122
+ for (let i = start; i < line.length; i++) {
123
+ const ch = line[i];
124
+ if (inString) {
125
+ if (ch === "\\")
126
+ i++;
127
+ else if (ch === '"')
128
+ inString = false;
129
+ }
130
+ else if (ch === '"')
131
+ inString = true;
132
+ else if (ch === "{")
133
+ depth++;
134
+ else if (ch === "}" && --depth === 0) {
135
+ end = i + 1;
136
+ break;
137
+ }
138
+ }
139
+ from = end;
140
+ }
141
+ return out + line.slice(from);
142
+ }
107
143
  function lineMatchesLogFilter(line, filter) {
108
144
  const normalized = line.toLowerCase();
109
145
  switch (filter) {
@@ -160,7 +196,7 @@ function filterJobLogToErrors(lines) {
160
196
  pyTraceback = false;
161
197
  }
162
198
  if (isHeader) {
163
- recordMatches = isErrorContent(line) || PY_FILE_FRAME.test(message);
199
+ recordMatches = isErrorContent(stripJsonPayloads(line)) || PY_FILE_FRAME.test(message);
164
200
  if (recordMatches && (PY_TRACEBACK_HEAD.test(message) || PY_FILE_FRAME.test(message))) {
165
201
  pyTraceback = true;
166
202
  }
@@ -176,7 +212,7 @@ function filterJobLogToErrors(lines) {
176
212
  if (recordMatches
177
213
  && !JVM_STACK_FRAME.test(line)
178
214
  && !JVM_MORE_FRAMES.test(line)
179
- && isErrorContent(line))
215
+ && isErrorContent(stripJsonPayloads(line)))
180
216
  kept.push(line);
181
217
  }
182
218
  return kept.join("\n");
@@ -283,7 +319,8 @@ export class JobsResource extends BaseResource {
283
319
  /**
284
320
  * Retrieve job log text.
285
321
  * Returns the last `maxLogLines` lines (default 500) from the tail.
286
- * Use `0` or `-1` to return the log without line truncation.
322
+ * Use `0` or `-1` to return the log without line truncation. Credential-like
323
+ * JSON pairs (`"jobTicketSecret":"..."`) are redacted.
287
324
  *
288
325
  * The download is byte-bounded and deadline-covered: at most `maxLogBytes`
289
326
  * (default 10 MiB) of the *most recent* log output is retained, so a
@@ -298,7 +335,9 @@ export class JobsResource extends BaseResource {
298
335
  const path = `/public/api/projects/${this.enc(opts?.projectKey)}/jobs/${jobEnc}/log/${query}`;
299
336
  const maxLogBytes = Math.max(1, opts?.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES);
300
337
  const { text, } = await this.client.getTextTailLimited(path, maxLogBytes);
301
- return limitJobLog(filterJobLog(text, opts?.logFilter), opts?.maxLogLines);
338
+ // Single download choke point (logFromUrl and wait/summary go through here):
339
+ // DSS logs embed the job ticket secret in start_session JSON.
340
+ return limitJobLog(filterJobLog(redactCredentialPairs(text), opts?.logFilter), opts?.maxLogLines);
302
341
  }
303
342
  async logFromUrl(logUrl, opts) {
304
343
  const parsed = new URL(logUrl, "http://dss.local");
@@ -7,8 +7,10 @@
7
7
  * - optionally redacts userinfo embedded in HTTP(S) URL authorities
8
8
  * (`https://user:pass@host/...` -> `https://[redacted]@host/...`).
9
9
  *
10
- * The SDK resource layer forwards values verbatim to DSS over TLS; sanitization
11
- * is a CLI presentation concern and must never leak secret material instead.
10
+ * The SDK resource layer forwards values verbatim to DSS over TLS; sanitizing
11
+ * structured output is a CLI presentation concern and must never leak secret
12
+ * material instead. DSS-produced read text that consumers never need secrets
13
+ * from (job logs) is scrubbed at the SDK download via `redactCredentialPairs`.
12
14
  */
13
15
  /** Marker replacing every redacted value or secret occurrence. */
14
16
  export declare const SECRET_REDACTED = "[redacted]";
@@ -46,6 +48,13 @@ export declare function redactUrlUserinfo(text: string): string;
46
48
  * recurse, arrays recurse. Non-string scalars pass through unchanged.
47
49
  */
48
50
  export declare function sanitizeSecrets<T>(value: T, options: SecretSanitizeOptions): T;
51
+ /**
52
+ * Redact the string value of every JSON-style `"key":"value"` pair in free
53
+ * text whose key is credential-like (e.g. the `jobTicketSecret` DSS writes
54
+ * into job logs). Bare `key` fields are not redacted here: in log text they
55
+ * name ordinary identifiers far more often than secrets.
56
+ */
57
+ export declare function redactCredentialPairs(text: string): string;
49
58
  /**
50
59
  * Every string value under a credential-like key (any depth), plus bare
51
60
  * `key` fields such as `authRealm.key`. Collected from a command's own JSON
@@ -7,8 +7,10 @@
7
7
  * - optionally redacts userinfo embedded in HTTP(S) URL authorities
8
8
  * (`https://user:pass@host/...` -> `https://[redacted]@host/...`).
9
9
  *
10
- * The SDK resource layer forwards values verbatim to DSS over TLS; sanitization
11
- * is a CLI presentation concern and must never leak secret material instead.
10
+ * The SDK resource layer forwards values verbatim to DSS over TLS; sanitizing
11
+ * structured output is a CLI presentation concern and must never leak secret
12
+ * material instead. DSS-produced read text that consumers never need secrets
13
+ * from (job logs) is scrubbed at the SDK download via `redactCredentialPairs`.
12
14
  */
13
15
  /** Marker replacing every redacted value or secret occurrence. */
14
16
  export const SECRET_REDACTED = "[redacted]";
@@ -92,6 +94,24 @@ const CREDENTIAL_KEY_FRAGMENTS = [
92
94
  ];
93
95
  /** Keys naming how credentials work (`credentialsMode`, `passwordType`), not secrets. */
94
96
  const CREDENTIAL_SETTING_SUFFIXES = ["mode", "type", "policy",];
97
+ /** Credential-like key (normalized fragment match, minus credential settings). */
98
+ function isCredentialKey(key) {
99
+ const normalized = normalizeSecretKey(key);
100
+ if (CREDENTIAL_SETTING_SUFFIXES.some((suffix) => normalized.endsWith(suffix)))
101
+ return false;
102
+ return CREDENTIAL_KEY_FRAGMENTS.some((fragment) => normalized.includes(fragment));
103
+ }
104
+ /** JSON-style `"key": "value"` pair with escape-aware string bodies. */
105
+ const JSON_STRING_PAIR = /"((?:[^"\\\n]|\\.)*)"(\s*:\s*)"((?:[^"\\\n]|\\.)*)"/g;
106
+ /**
107
+ * Redact the string value of every JSON-style `"key":"value"` pair in free
108
+ * text whose key is credential-like (e.g. the `jobTicketSecret` DSS writes
109
+ * into job logs). Bare `key` fields are not redacted here: in log text they
110
+ * name ordinary identifiers far more often than secrets.
111
+ */
112
+ export function redactCredentialPairs(text) {
113
+ return text.replace(JSON_STRING_PAIR, (pair, key, separator, value) => value.length > 0 && isCredentialKey(key) ? `"${key}"${separator}"${SECRET_REDACTED}"` : pair);
114
+ }
95
115
  /**
96
116
  * Every string value under a credential-like key (any depth), plus bare
97
117
  * `key` fields such as `authRealm.key`. Collected from a command's own JSON
@@ -114,10 +134,7 @@ export function credentialValues(value) {
114
134
  if (node === null || typeof node !== "object")
115
135
  return;
116
136
  for (const [key, item,] of Object.entries(node)) {
117
- const normalized = normalizeSecretKey(key);
118
- const setting = CREDENTIAL_SETTING_SUFFIXES.some((suffix) => normalized.endsWith(suffix));
119
- visit(item, sensitive || (!setting && (normalized === "key"
120
- || CREDENTIAL_KEY_FRAGMENTS.some((fragment) => normalized.includes(fragment)))));
137
+ visit(item, sensitive || isCredentialKey(key) || normalizeSecretKey(key) === "key");
121
138
  }
122
139
  };
123
140
  visit(value, false);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dataiku-sdk",
3
- "version": "3.8.0",
3
+ "version": "3.9.0",
4
4
  "description": "Dataiku DSS SDK and CLI for programmatic access to DSS REST APIs",
5
5
  "repository": {
6
6
  "type": "git",
@@ -48,12 +48,12 @@
48
48
  "node-stream-zip": "^1.16.0"
49
49
  },
50
50
  "devDependencies": {
51
- "@oxlint/plugins": "1.86.0",
51
+ "@oxlint/plugins": "1.87.0",
52
52
  "@types/bun": "^1.4.2",
53
53
  "cross-env": "^10.1.0",
54
- "dprint": "^0.60.1",
54
+ "dprint": "^0.61.1",
55
55
  "js-tiktoken": "1.0.21",
56
- "oxlint": "1.86.0",
56
+ "oxlint": "1.87.0",
57
57
  "typescript": "^7.0.2"
58
58
  },
59
59
  "bundleDependencies": [
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "dataiku-sdk",
4
- "version": "3.8.0",
4
+ "version": "3.9.0",
5
5
  "description": "Dataiku DSS SDK and agent-oriented CLI for programmatic access to DSS REST APIs.",
6
6
  "homepage": "https://github.com/clssck/dataiku-sdk",
7
7
  "repository": "https://github.com/clssck/dataiku-sdk",
@@ -3,12 +3,12 @@
3
3
  ## Contract
4
4
 
5
5
  - Stdout: exactly one compact JSON value; void success: `{ok:true}`. No prompts, help screens, tables, banners, or prose.
6
- - Dispatch/runtime failure: stdout error object (`type:"error"`, `ok:false`, `error`, `code`, `category`, `exitCode`), nonzero exit. `doctor`/`batch`/`cleanup` failures instead return direct result objects; inspect exit code and fields.
6
+ - Dispatch/runtime failure: stdout error object (`type:"error"`, `error`, `code`, `category`, `exitCode`; `projectKey` when resolved), nonzero exit. `doctor`/`batch`/`cleanup` failures instead return direct result objects; inspect exit code and fields.
7
7
  - Stderr: JSONL diagnostics only; `type:"warning"` / `type:"trace"` for warnings / `--verbose` HTTP traces, flushed before success or failure output.
8
8
  - Exits: 0 success, 1 usage/configuration, 2 DSS/internal/permission-or-environment, 3 transient/retryable DSS error, 4 failed long-running result/assertion.
9
9
  - Recipe payload stdout: JSON string. With `--output PATH`: exact bytes to file, JSON string equal to `PATH` on stdout.
10
10
  - General `--fields a,b,c`: object projection, element-wise for object arrays. Dotted paths traverse nested objects; missing fields become `null`; strings/scalars pass through.
11
- - `*.list`: compact items (ids, kind, next-step fields such as recipe `inputs`/`outputs`); `--full`/`--fields` use DSS objects. `--contains TEXT` filters ids; `--limit N` caps (`list_truncated` warning).
11
+ - Compact by default: `*.list` (ids, kind, next-step fields such as recipe `inputs`/`outputs`), `job get` (no log tail), `dataset get`/`info`, `project get`; `--full`/`--fields` use DSS objects. Lists: `--contains TEXT` filters ids; `--limit N` caps (`list_truncated` warning).
12
12
 
13
13
  ## Discovery
14
14
 
@@ -3,7 +3,7 @@
3
3
  ## Platform, builds, data
4
4
 
5
5
  - Code/SQL: use `--file`/`--sql-file`; shells, especially PowerShell, mangle quotes, `$`, and multiline text. Non-UTF-8 consoles (e.g. Windows cp1252): write non-ASCII results to UTF-8 files or `--output PATH`, not the console.
6
- - Build errors: `dss job log <id> --errors-only` selects errors/tracebacks. For full logs, use `--output PATH` without `--errors-only`. Logs may arrive as one JVM-noisy line; `Error in Python process: At line <N>` identifies the payload source line.
6
+ - Build errors: `dss job log <id> --errors-only` selects errors/tracebacks. Plain `job log` prints the last 100 lines (`log_truncated`); for full logs, use `--output PATH` without `--errors-only`. Logs may arrive as one JVM-noisy line; `Error in Python process: At line <N>` identifies the payload source line.
7
7
  - Schemas: code recipes set output schemas at run time. Visual/SQL query recipes: `recipe create` fills created or empty outputs (`outputSchemaUpdated`); `recipe set-payload` updates empty or DSS-derived ones, warns `recipe_output_schema_outdated` for hand-edited ones. Apply with `dss recipe update-schema NAME` (one recipe), `dss flow propagate-schema DATASET --wait` (every downstream recipe), or build `--auto-update-schema`. `built_dataset_has_no_columns`: the DONE build wrote nothing. `nothing_to_build` (exit 4): no recipe writes the managed target. Manual columns: `dss dataset refresh-schema NAME --data-file columns.json`. `dss dataset validate-build` catches file-backed misconfiguration.
8
8
  - `Inline` (editable) datasets: the public API cannot write rows, so `dataset create` rejects them; use `UploadedFiles` + `dataset upload-file`, or a python recipe output. `code run` is a scenario step; some DSS versions refuse dataset writes there.
9
9
  - `dss dataset download`: default 100k rows; result `{ path, rows, truncated, limit }`. If truncated, raise `--limit`. Specify `--output` or read the `dataset_download_default_location` path warning. CSV is spreadsheet-safe; `--raw-data` retains formula prefixes. For large tables, aggregate in SQL or use a recipe.
@@ -22,7 +22,7 @@
22
22
 
23
23
  Dispatch/runtime failure: one compact stdout error:
24
24
  ```json
25
- {"type":"error","ok":false,"error":"Missing API key.","code":"missing_required_flag","category":"usage","exitCode":1,"resource":"dataset","action":"list"}
25
+ {"type":"error","error":"Missing API key.","code":"missing_required_flag","category":"usage","exitCode":1}
26
26
  ```
27
27
 
28
28
  `doctor`/`batch`/`cleanup` command failures return direct stdout result objects; check nonzero exit before interpreting. Recover from structured `code`, `category`, `exitCode`, `retryable`, `status`, `details`, never message scraping.