dataiku-sdk 0.12.4 → 0.12.5
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/README.md +118 -7
- package/dist/packages/types/src/index.d.ts +5 -0
- package/dist/packages/types/src/index.js +5 -0
- package/dist/src/cli/coerce.d.ts +1 -3
- package/dist/src/cli/coerce.js +1 -17
- package/dist/src/cli/commands/app.js +709 -14
- package/dist/src/cli/commands/code-env.js +2 -2
- package/dist/src/cli/commands/dashboard.js +1 -1
- package/dist/src/cli/commands/dataset.js +2 -2
- package/dist/src/cli/commands/folder.js +1 -1
- package/dist/src/cli/commands/insight.js +2 -2
- package/dist/src/cli/commands/notebook.js +2 -2
- package/dist/src/cli/commands/recipe.js +3 -3
- package/dist/src/cli/commands/scenario.js +1 -1
- package/dist/src/cli/commands/sql.js +1 -1
- package/dist/src/cli/commands/variable.js +1 -1
- package/dist/src/cli/commands/wiki.js +1 -1
- package/dist/src/cli/contract.d.ts +12 -3
- package/dist/src/cli/contract.js +805 -49
- package/dist/src/cli/flags.js +8 -0
- package/dist/src/cli/helpers/app-permissions.d.ts +63 -0
- package/dist/src/cli/helpers/app-permissions.js +239 -0
- package/dist/src/cli/helpers/app-successor.d.ts +106 -0
- package/dist/src/cli/helpers/app-successor.js +625 -0
- package/dist/src/cli/helpers/cleanup.js +46 -12
- package/dist/src/cli/output.d.ts +12 -1
- package/dist/src/cli/output.js +53 -6
- package/dist/src/cli/usage.d.ts +1 -1
- package/dist/src/cli/usage.js +5 -2
- package/dist/src/cli.js +329 -32
- package/dist/src/client.d.ts +6 -0
- package/dist/src/client.js +28 -1
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/resources/applications.d.ts +191 -1
- package/dist/src/resources/applications.js +549 -10
- package/dist/src/resources/datasets.js +8 -0
- package/dist/src/resources/futures.js +7 -2
- package/dist/src/skill.js +76 -18
- package/dist/src/utils/cleanup-ledger.d.ts +34 -1
- package/dist/src/utils/cleanup-ledger.js +118 -7
- package/dist/src/utils/dss-url.d.ts +6 -0
- package/dist/src/utils/dss-url.js +17 -0
- package/dist/src/utils/project-incarnation.d.ts +6 -0
- package/dist/src/utils/project-incarnation.js +27 -0
- package/dist/src/utils/stable-hash.d.ts +11 -0
- package/dist/src/utils/stable-hash.js +27 -0
- package/node_modules/@sinclair/typebox/build/cjs/compiler/compiler.js +26 -46
- package/node_modules/@sinclair/typebox/build/cjs/parser/runtime/guard.d.ts +1 -1
- package/node_modules/@sinclair/typebox/build/cjs/parser/runtime/token.d.ts +2 -2
- package/node_modules/@sinclair/typebox/build/cjs/syntax/mapping.d.ts +1 -1
- package/node_modules/@sinclair/typebox/build/cjs/system/evaluate.d.ts +5 -0
- package/node_modules/@sinclair/typebox/build/cjs/system/evaluate.js +11 -0
- package/node_modules/@sinclair/typebox/build/cjs/system/index.d.ts +1 -0
- package/node_modules/@sinclair/typebox/build/cjs/system/index.js +1 -0
- package/node_modules/@sinclair/typebox/build/cjs/type/object/object.d.ts +2 -2
- package/node_modules/@sinclair/typebox/build/cjs/type/object/object.js +18 -2
- package/node_modules/@sinclair/typebox/build/cjs/value/parse/parse.d.ts +1 -1
- package/node_modules/@sinclair/typebox/build/esm/compiler/compiler.mjs +27 -47
- package/node_modules/@sinclair/typebox/build/esm/parser/runtime/guard.d.mts +1 -1
- package/node_modules/@sinclair/typebox/build/esm/parser/runtime/token.d.mts +2 -2
- package/node_modules/@sinclair/typebox/build/esm/syntax/mapping.d.mts +1 -1
- package/node_modules/@sinclair/typebox/build/esm/system/evaluate.d.mts +5 -0
- package/node_modules/@sinclair/typebox/build/esm/system/evaluate.mjs +7 -0
- package/node_modules/@sinclair/typebox/build/esm/system/index.d.mts +1 -0
- package/node_modules/@sinclair/typebox/build/esm/system/index.mjs +1 -0
- package/node_modules/@sinclair/typebox/build/esm/type/composite/composite.mjs +2 -2
- package/node_modules/@sinclair/typebox/build/esm/type/const/const.mjs +3 -3
- package/node_modules/@sinclair/typebox/build/esm/type/instantiate/instantiate.mjs +2 -2
- package/node_modules/@sinclair/typebox/build/esm/type/mapped/mapped.mjs +3 -3
- package/node_modules/@sinclair/typebox/build/esm/type/module/compute.mjs +2 -2
- package/node_modules/@sinclair/typebox/build/esm/type/object/object.d.mts +2 -2
- package/node_modules/@sinclair/typebox/build/esm/type/object/object.mjs +18 -2
- package/node_modules/@sinclair/typebox/build/esm/type/omit/omit.mjs +3 -3
- package/node_modules/@sinclair/typebox/build/esm/type/partial/partial.mjs +3 -3
- package/node_modules/@sinclair/typebox/build/esm/type/pick/pick.mjs +3 -3
- package/node_modules/@sinclair/typebox/build/esm/type/record/record.mjs +3 -3
- package/node_modules/@sinclair/typebox/build/esm/type/required/required.mjs +3 -3
- package/node_modules/@sinclair/typebox/build/esm/type/type/json.mjs +2 -2
- package/node_modules/@sinclair/typebox/build/esm/value/delta/delta.mjs +4 -4
- package/node_modules/@sinclair/typebox/build/esm/value/parse/parse.d.mts +1 -1
- package/node_modules/@sinclair/typebox/package.json +1 -1
- package/package.json +7 -5
- package/packages/types/dist/index.d.ts +5 -0
- package/packages/types/dist/index.js +5 -0
package/dist/src/cli/flags.js
CHANGED
|
@@ -27,6 +27,7 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
27
27
|
"force-rebuild",
|
|
28
28
|
"latest",
|
|
29
29
|
"copy-output-settings",
|
|
30
|
+
"copy-permissions",
|
|
30
31
|
"continue-on-error",
|
|
31
32
|
"no-backup",
|
|
32
33
|
"payload-only",
|
|
@@ -38,6 +39,7 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
38
39
|
"full-log",
|
|
39
40
|
"drop-data",
|
|
40
41
|
"normalize",
|
|
42
|
+
"unconfirmed-creation",
|
|
41
43
|
]);
|
|
42
44
|
export const SHORT_FLAGS = {
|
|
43
45
|
v: "verbose",
|
|
@@ -82,6 +84,9 @@ export const VALUE_FLAGS = new Set([
|
|
|
82
84
|
"active",
|
|
83
85
|
"deployment-mode",
|
|
84
86
|
"env-version",
|
|
87
|
+
"expect-hash",
|
|
88
|
+
"expect-project-incarnation",
|
|
89
|
+
"expect-version",
|
|
85
90
|
"data-file",
|
|
86
91
|
"database",
|
|
87
92
|
"dataset",
|
|
@@ -104,6 +109,7 @@ export const VALUE_FLAGS = new Set([
|
|
|
104
109
|
"package",
|
|
105
110
|
"packages",
|
|
106
111
|
"local",
|
|
112
|
+
"manifest-version",
|
|
107
113
|
"max-edges",
|
|
108
114
|
"max-lines",
|
|
109
115
|
"max-log-lines",
|
|
@@ -142,6 +148,7 @@ export const VALUE_FLAGS = new Set([
|
|
|
142
148
|
"rule-id",
|
|
143
149
|
"role",
|
|
144
150
|
"retries",
|
|
151
|
+
"future-id",
|
|
145
152
|
"poll-interval",
|
|
146
153
|
"python-interpreter",
|
|
147
154
|
"replace-input",
|
|
@@ -166,6 +173,7 @@ export const VALUE_FLAGS = new Set([
|
|
|
166
173
|
"type",
|
|
167
174
|
"url",
|
|
168
175
|
"until",
|
|
176
|
+
"version-notes",
|
|
169
177
|
"to",
|
|
170
178
|
"zone",
|
|
171
179
|
"zone-id",
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ProjectPermissions } from "../../resources/projects.js";
|
|
2
|
+
import { UsageError } from "../usage.js";
|
|
3
|
+
/**
|
|
4
|
+
* Version 3 binds every snapshot to both its DSS server and the concrete project
|
|
5
|
+
* incarnation present at capture time. Earlier versions are refused.
|
|
6
|
+
*/
|
|
7
|
+
export declare const APP_PERMISSIONS_SNAPSHOT_VERSION = 3;
|
|
8
|
+
export interface AppPermissionsSnapshot {
|
|
9
|
+
version: number;
|
|
10
|
+
projectKey: string;
|
|
11
|
+
/** Canonical base URL of the DSS server these rules were read from. */
|
|
12
|
+
dssUrl: string;
|
|
13
|
+
/** Hash of the concrete project incarnation these rules were read from. */
|
|
14
|
+
projectIncarnationHash: string;
|
|
15
|
+
capturedAt: string;
|
|
16
|
+
hash: string;
|
|
17
|
+
permissionsHash: string;
|
|
18
|
+
permissions: ProjectPermissions;
|
|
19
|
+
}
|
|
20
|
+
export type AppPermissionsDifferenceStatus = "added" | "removed" | "changed";
|
|
21
|
+
export interface AppPermissionsDifference {
|
|
22
|
+
path: string;
|
|
23
|
+
status: AppPermissionsDifferenceStatus;
|
|
24
|
+
backup?: unknown;
|
|
25
|
+
current?: unknown;
|
|
26
|
+
}
|
|
27
|
+
export interface AppPermissionsDiff {
|
|
28
|
+
changed: boolean;
|
|
29
|
+
differences: AppPermissionsDifference[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Canonical hash of a permissions payload. Key order and unknown DSS fields are
|
|
33
|
+
* normalized by `stableJson`, so re-serializing a snapshot never changes the hash.
|
|
34
|
+
*/
|
|
35
|
+
export declare function appPermissionsHash(permissions: unknown): string;
|
|
36
|
+
export declare function buildAppPermissionsSnapshot(projectKey: string, dssUrl: string, projectIncarnationHash: string, permissions: ProjectPermissions, capturedAt?: string): AppPermissionsSnapshot;
|
|
37
|
+
/**
|
|
38
|
+
* Write a snapshot as owner-only JSON. Access-control rules must never sit on disk
|
|
39
|
+
* under wider permissions: `writeFileSync`'s `mode` is ignored for an existing file
|
|
40
|
+
* and a trailing `chmod` would publish the payload first, so the descriptor is
|
|
41
|
+
* clamped to 0600 before the file is truncated or written.
|
|
42
|
+
*/
|
|
43
|
+
export declare function writeAppPermissionsSnapshot(filePath: string, snapshot: AppPermissionsSnapshot): void;
|
|
44
|
+
/** Validate envelope shape, version, and hash integrity. Never echoes permission payloads. */
|
|
45
|
+
export declare function parseAppPermissionsSnapshot(text: string, source: string): AppPermissionsSnapshot;
|
|
46
|
+
export declare function readAppPermissionsSnapshot(filePath: string): AppPermissionsSnapshot;
|
|
47
|
+
/**
|
|
48
|
+
* Refuse to use a snapshot outside the exact project *and* DSS server it was captured
|
|
49
|
+
* from. Access-control rules are server-scoped: the same project key, users, and groups
|
|
50
|
+
* exist on other instances with different meaning, so replaying a snapshot elsewhere can
|
|
51
|
+
* silently grant or revoke access. Runs before any live permission read or write.
|
|
52
|
+
*/
|
|
53
|
+
export declare function assertAppPermissionsSnapshotBinding(snapshot: AppPermissionsSnapshot, targetProjectKey: string, currentDssUrl: string, currentProjectIncarnationHash?: string): void;
|
|
54
|
+
export declare function appPermissionsVerificationError(projectKey: string, hashes: {
|
|
55
|
+
beforeHash: string;
|
|
56
|
+
desiredHash: string;
|
|
57
|
+
verifiedHash: string;
|
|
58
|
+
}): UsageError;
|
|
59
|
+
/**
|
|
60
|
+
* Deterministic, path-addressed differences between a snapshot payload and live
|
|
61
|
+
* permissions. Paths are sorted so repeated runs emit byte-identical output.
|
|
62
|
+
*/
|
|
63
|
+
export declare function diffAppPermissions(backup: unknown, current: unknown): AppPermissionsDiff;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { canonicalDssUrl, } from "../../utils/dss-url.js";
|
|
3
|
+
import { compareStrings, } from "../../utils/stable-hash.js";
|
|
4
|
+
import { parseJsonObject, plainRecord, stableHash, stableJson, } from "../coerce.js";
|
|
5
|
+
import { UsageError, } from "../usage.js";
|
|
6
|
+
/**
|
|
7
|
+
* Version 3 binds every snapshot to both its DSS server and the concrete project
|
|
8
|
+
* incarnation present at capture time. Earlier versions are refused.
|
|
9
|
+
*/
|
|
10
|
+
export const APP_PERMISSIONS_SNAPSHOT_VERSION = 3;
|
|
11
|
+
/** Snapshots hold access-control rules: keep them owner-only on disk. */
|
|
12
|
+
const SNAPSHOT_FILE_MODE = 0o600;
|
|
13
|
+
/**
|
|
14
|
+
* Open for writing *without* `O_TRUNC`: truncation must wait until the descriptor
|
|
15
|
+
* has been narrowed to owner-only. `O_NOFOLLOW`, where the platform defines it,
|
|
16
|
+
* refuses a symlinked destination so a hostile link cannot redirect the snapshot —
|
|
17
|
+
* or the permission change itself — onto another file.
|
|
18
|
+
*/
|
|
19
|
+
const SNAPSHOT_OPEN_FLAGS = fs.constants.O_WRONLY | fs.constants.O_CREAT
|
|
20
|
+
| (typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0);
|
|
21
|
+
/**
|
|
22
|
+
* Canonical hash of a permissions payload. Key order and unknown DSS fields are
|
|
23
|
+
* normalized by `stableJson`, so re-serializing a snapshot never changes the hash.
|
|
24
|
+
*/
|
|
25
|
+
export function appPermissionsHash(permissions) {
|
|
26
|
+
return stableHash(permissions);
|
|
27
|
+
}
|
|
28
|
+
export function buildAppPermissionsSnapshot(projectKey, dssUrl, projectIncarnationHash, permissions, capturedAt = new Date().toISOString()) {
|
|
29
|
+
const version = APP_PERMISSIONS_SNAPSHOT_VERSION;
|
|
30
|
+
const boundUrl = canonicalDssUrl(dssUrl);
|
|
31
|
+
if (boundUrl.length === 0) {
|
|
32
|
+
throw new UsageError("Cannot capture an app permissions snapshot without the DSS server URL it was read from.", "validation_failed");
|
|
33
|
+
}
|
|
34
|
+
if (!/^[0-9a-f]{64}$/.test(projectIncarnationHash)) {
|
|
35
|
+
throw new UsageError("Cannot capture an app permissions snapshot without a valid project-incarnation hash.", "validation_failed");
|
|
36
|
+
}
|
|
37
|
+
const permissionsHash = appPermissionsHash(permissions);
|
|
38
|
+
const hash = stableHash({
|
|
39
|
+
version,
|
|
40
|
+
projectKey,
|
|
41
|
+
dssUrl: boundUrl,
|
|
42
|
+
projectIncarnationHash,
|
|
43
|
+
capturedAt,
|
|
44
|
+
permissionsHash,
|
|
45
|
+
permissions,
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
version,
|
|
49
|
+
projectKey,
|
|
50
|
+
dssUrl: boundUrl,
|
|
51
|
+
projectIncarnationHash,
|
|
52
|
+
capturedAt,
|
|
53
|
+
hash,
|
|
54
|
+
permissionsHash,
|
|
55
|
+
permissions,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Drain the payload: one `writeSync` may cover only part of the buffer. */
|
|
59
|
+
function writeAllSync(fd, payload) {
|
|
60
|
+
let offset = 0;
|
|
61
|
+
while (offset < payload.length) {
|
|
62
|
+
const written = fs.writeSync(fd, payload, offset, payload.length - offset, offset);
|
|
63
|
+
if (written <= 0) {
|
|
64
|
+
throw new Error(`Stalled writing app permissions snapshot at byte ${offset}.`);
|
|
65
|
+
}
|
|
66
|
+
offset += written;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Write a snapshot as owner-only JSON. Access-control rules must never sit on disk
|
|
71
|
+
* under wider permissions: `writeFileSync`'s `mode` is ignored for an existing file
|
|
72
|
+
* and a trailing `chmod` would publish the payload first, so the descriptor is
|
|
73
|
+
* clamped to 0600 before the file is truncated or written.
|
|
74
|
+
*/
|
|
75
|
+
export function writeAppPermissionsSnapshot(filePath, snapshot) {
|
|
76
|
+
// Serialize before touching the filesystem: a serialization fault must not
|
|
77
|
+
// truncate or half-overwrite an existing snapshot.
|
|
78
|
+
const payload = Buffer.from(`${JSON.stringify(snapshot, null, 2)}\n`, "utf-8");
|
|
79
|
+
const fd = fs.openSync(filePath, SNAPSHOT_OPEN_FLAGS, SNAPSHOT_FILE_MODE);
|
|
80
|
+
let wrote = false;
|
|
81
|
+
try {
|
|
82
|
+
fs.fchmodSync(fd, SNAPSHOT_FILE_MODE);
|
|
83
|
+
fs.ftruncateSync(fd, 0);
|
|
84
|
+
writeAllSync(fd, payload);
|
|
85
|
+
wrote = true;
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
if (wrote) {
|
|
89
|
+
fs.closeSync(fd);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
try {
|
|
93
|
+
fs.closeSync(fd);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Keep the original failure: a close fault must not mask it.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function requiredSnapshotString(record, field, source) {
|
|
102
|
+
const value = record[field];
|
|
103
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
104
|
+
throw new UsageError(`${source} is not an app permissions snapshot: "${field}" must be a non-empty string.`, "validation_failed");
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
/** Validate envelope shape, version, and hash integrity. Never echoes permission payloads. */
|
|
109
|
+
export function parseAppPermissionsSnapshot(text, source) {
|
|
110
|
+
const record = parseJsonObject(text, source);
|
|
111
|
+
const version = record["version"];
|
|
112
|
+
if (typeof version !== "number" || !Number.isInteger(version)) {
|
|
113
|
+
throw new UsageError(`${source} is not an app permissions snapshot: "version" must be an integer.`, "validation_failed");
|
|
114
|
+
}
|
|
115
|
+
if (version !== APP_PERMISSIONS_SNAPSHOT_VERSION) {
|
|
116
|
+
throw new UsageError(`${source} has unsupported snapshot version ${String(version)} (expected ${String(APP_PERMISSIONS_SNAPSHOT_VERSION)}).`, "validation_failed", version < APP_PERMISSIONS_SNAPSHOT_VERSION
|
|
117
|
+
? "Older snapshots are not bound to the concrete project incarnation they came from, so they cannot be applied safely after project-key reuse. Re-run `dss app permissions-snapshot` against that project to capture a current snapshot."
|
|
118
|
+
: undefined);
|
|
119
|
+
}
|
|
120
|
+
const projectKey = requiredSnapshotString(record, "projectKey", source);
|
|
121
|
+
const dssUrl = requiredSnapshotString(record, "dssUrl", source);
|
|
122
|
+
const projectIncarnationHash = requiredSnapshotString(record, "projectIncarnationHash", source);
|
|
123
|
+
const capturedAt = requiredSnapshotString(record, "capturedAt", source);
|
|
124
|
+
const hash = requiredSnapshotString(record, "hash", source);
|
|
125
|
+
const permissionsHash = requiredSnapshotString(record, "permissionsHash", source);
|
|
126
|
+
const permissions = plainRecord(record["permissions"]);
|
|
127
|
+
if (!permissions) {
|
|
128
|
+
throw new UsageError(`${source} is not an app permissions snapshot: "permissions" must be a JSON object.`, "validation_failed");
|
|
129
|
+
}
|
|
130
|
+
const actualPermissionsHash = appPermissionsHash(permissions);
|
|
131
|
+
const actualHash = stableHash({
|
|
132
|
+
version,
|
|
133
|
+
projectKey,
|
|
134
|
+
dssUrl,
|
|
135
|
+
projectIncarnationHash,
|
|
136
|
+
capturedAt,
|
|
137
|
+
permissionsHash,
|
|
138
|
+
permissions,
|
|
139
|
+
});
|
|
140
|
+
if (actualPermissionsHash !== permissionsHash || actualHash !== hash) {
|
|
141
|
+
throw new UsageError(`${source} failed its integrity check. Refusing to use a corrupted or hand-edited snapshot.`, "validation_failed", "Re-run `dss app permissions-snapshot` to capture a fresh snapshot.");
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
version,
|
|
145
|
+
projectKey,
|
|
146
|
+
dssUrl,
|
|
147
|
+
projectIncarnationHash,
|
|
148
|
+
capturedAt,
|
|
149
|
+
hash,
|
|
150
|
+
permissionsHash,
|
|
151
|
+
permissions,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
export function readAppPermissionsSnapshot(filePath) {
|
|
155
|
+
let text;
|
|
156
|
+
try {
|
|
157
|
+
text = fs.readFileSync(filePath, "utf-8");
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
161
|
+
throw new UsageError(`Cannot read app permissions snapshot ${filePath}: ${message}`, "validation_failed");
|
|
162
|
+
}
|
|
163
|
+
return parseAppPermissionsSnapshot(text, filePath);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Refuse to use a snapshot outside the exact project *and* DSS server it was captured
|
|
167
|
+
* from. Access-control rules are server-scoped: the same project key, users, and groups
|
|
168
|
+
* exist on other instances with different meaning, so replaying a snapshot elsewhere can
|
|
169
|
+
* silently grant or revoke access. Runs before any live permission read or write.
|
|
170
|
+
*/
|
|
171
|
+
export function assertAppPermissionsSnapshotBinding(snapshot, targetProjectKey, currentDssUrl, currentProjectIncarnationHash) {
|
|
172
|
+
if (snapshot.projectKey !== targetProjectKey) {
|
|
173
|
+
throw new UsageError(`Snapshot was captured from project ${snapshot.projectKey} but the target project is ${targetProjectKey}. Refusing to restore permissions across projects.`, "validation_failed", `Pass --project-key ${snapshot.projectKey} to restore the project the snapshot came from.`);
|
|
174
|
+
}
|
|
175
|
+
const bound = canonicalDssUrl(snapshot.dssUrl);
|
|
176
|
+
const current = canonicalDssUrl(currentDssUrl);
|
|
177
|
+
if (bound !== current) {
|
|
178
|
+
throw new UsageError(`Snapshot was captured from DSS server ${bound} but the configured server is ${current}. Refusing to use permissions across DSS servers.`, "validation_failed", `Point the CLI at ${bound} to use this snapshot, or capture a fresh snapshot from ${current}.`, { projectKey: snapshot.projectKey, snapshotDssUrl: bound, currentDssUrl: current, });
|
|
179
|
+
}
|
|
180
|
+
if (currentProjectIncarnationHash === undefined)
|
|
181
|
+
return;
|
|
182
|
+
if (snapshot.projectIncarnationHash === currentProjectIncarnationHash)
|
|
183
|
+
return;
|
|
184
|
+
throw new UsageError(`Snapshot was captured from a different incarnation of project ${targetProjectKey}. Refusing to use permissions after project-key reuse.`, "validation_failed", "Capture a fresh permissions snapshot from the current project. Do not restore access-control data from the replaced project.", {
|
|
185
|
+
projectKey: targetProjectKey,
|
|
186
|
+
snapshotProjectIncarnationHash: snapshot.projectIncarnationHash,
|
|
187
|
+
currentProjectIncarnationHash,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
export function appPermissionsVerificationError(projectKey, hashes) {
|
|
191
|
+
return new UsageError(`Permission restore verification failed for project ${projectKey}: expected hash ${hashes.desiredHash} after the update but DSS returned ${hashes.verifiedHash}.`, "validation_failed", "Re-read the live permissions with `dss app permissions-diff` before retrying; DSS may have rejected or rewritten part of the payload.", { projectKey, ...hashes, });
|
|
192
|
+
}
|
|
193
|
+
function differenceStatus(backup, current) {
|
|
194
|
+
if (backup === undefined)
|
|
195
|
+
return "added";
|
|
196
|
+
if (current === undefined)
|
|
197
|
+
return "removed";
|
|
198
|
+
return "changed";
|
|
199
|
+
}
|
|
200
|
+
function pushDifference(out, path, backup, current) {
|
|
201
|
+
out.push({
|
|
202
|
+
path,
|
|
203
|
+
status: differenceStatus(backup, current),
|
|
204
|
+
...(backup === undefined ? {} : { backup, }),
|
|
205
|
+
...(current === undefined ? {} : { current, }),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function collectDifferences(path, backup, current, out) {
|
|
209
|
+
if (stableJson(backup) === stableJson(current))
|
|
210
|
+
return;
|
|
211
|
+
const backupRecord = plainRecord(backup);
|
|
212
|
+
const currentRecord = plainRecord(current);
|
|
213
|
+
if (backupRecord && currentRecord) {
|
|
214
|
+
const keys = [...new Set([...Object.keys(backupRecord), ...Object.keys(currentRecord),]),]
|
|
215
|
+
.sort(compareStrings);
|
|
216
|
+
for (const key of keys) {
|
|
217
|
+
collectDifferences(path ? `${path}.${key}` : key, backupRecord[key], currentRecord[key], out);
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (Array.isArray(backup) && Array.isArray(current)) {
|
|
222
|
+
const length = Math.max(backup.length, current.length);
|
|
223
|
+
for (let index = 0; index < length; index += 1) {
|
|
224
|
+
collectDifferences(`${path}[${String(index)}]`, backup[index], current[index], out);
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
pushDifference(out, path || "(root)", backup, current);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Deterministic, path-addressed differences between a snapshot payload and live
|
|
232
|
+
* permissions. Paths are sorted so repeated runs emit byte-identical output.
|
|
233
|
+
*/
|
|
234
|
+
export function diffAppPermissions(backup, current) {
|
|
235
|
+
const differences = [];
|
|
236
|
+
collectDifferences("", backup, current, differences);
|
|
237
|
+
differences.sort((a, b) => compareStrings(a.path, b.path));
|
|
238
|
+
return { changed: differences.length > 0, differences, };
|
|
239
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { DataikuClient } from "../../client.js";
|
|
2
|
+
import type { AppManifestValidationResult } from "../../resources/applications.js";
|
|
3
|
+
/**
|
|
4
|
+
* A DSS API key authenticates the public REST API only: the Application UI sits
|
|
5
|
+
* behind the interactive (often SSO) session, so no command in this SDK can
|
|
6
|
+
* observe it. Every app-release result therefore carries this gate explicitly
|
|
7
|
+
* instead of implying that a green API check means the UI works.
|
|
8
|
+
*/
|
|
9
|
+
export declare const VISUAL_UI_GATE: {
|
|
10
|
+
readonly gate: "visual-ui";
|
|
11
|
+
readonly status: "required";
|
|
12
|
+
readonly reason: "API key authenticates public REST only; use a pre-authenticated SSO browser session or dedicated UI test identity.";
|
|
13
|
+
readonly requiredAuthentication: "pre-authenticated-sso-or-dedicated-ui-test-identity";
|
|
14
|
+
readonly evidenceRequired: "Open the instance in the Dataiku Apps UI and exercise the affected tiles, forms, and actions to confirm the release behaves as intended.";
|
|
15
|
+
};
|
|
16
|
+
export type AppInstanceCheckName = "project-type" | "app-registration" | "manifest-version" | "manifest-references";
|
|
17
|
+
export interface AppInstanceCheck {
|
|
18
|
+
check: AppInstanceCheckName;
|
|
19
|
+
status: "ok" | "failed";
|
|
20
|
+
expected: unknown;
|
|
21
|
+
actual: unknown;
|
|
22
|
+
/** Machine-readable cause when the compared values alone do not explain a failure. */
|
|
23
|
+
reason?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface AppManifestReferenceSummary {
|
|
26
|
+
valid: boolean;
|
|
27
|
+
manifestHash: string;
|
|
28
|
+
checks: AppManifestValidationResult["checks"];
|
|
29
|
+
errors: AppManifestValidationResult["errors"];
|
|
30
|
+
}
|
|
31
|
+
export interface AppInstanceVerification {
|
|
32
|
+
appId: string;
|
|
33
|
+
projectKey: string;
|
|
34
|
+
projectAppType: string | null;
|
|
35
|
+
registered: boolean;
|
|
36
|
+
templateVersion: string | null;
|
|
37
|
+
expectedVersion: string | null;
|
|
38
|
+
expectedVersionSource: "expect-version" | "template-manifest";
|
|
39
|
+
instanceVersion: string | null;
|
|
40
|
+
instanceManifestHash: string | null;
|
|
41
|
+
templateManifestHash: string;
|
|
42
|
+
referenceValidation: AppManifestReferenceSummary | null;
|
|
43
|
+
checks: AppInstanceCheck[];
|
|
44
|
+
valid: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface SuccessorInstanceRequest {
|
|
47
|
+
appId: string;
|
|
48
|
+
from: string;
|
|
49
|
+
to: string;
|
|
50
|
+
name?: string;
|
|
51
|
+
copyPermissions: boolean;
|
|
52
|
+
dryRun: boolean;
|
|
53
|
+
timeoutMs?: number;
|
|
54
|
+
pollIntervalMs?: number;
|
|
55
|
+
}
|
|
56
|
+
/** Never echoes response bodies: only the taxonomy DataikuError already deems safe. */
|
|
57
|
+
export declare function safeErrorSummary(error: unknown): Record<string, unknown>;
|
|
58
|
+
/**
|
|
59
|
+
* Read-only API readiness of one app instance. Only checks that the public REST
|
|
60
|
+
* API can answer authoritatively are performed; the UI gate stays external.
|
|
61
|
+
*
|
|
62
|
+
* The instance manifest is read exactly once: its version, hash and reference
|
|
63
|
+
* validation all describe that one snapshot, so a concurrent manifest edit can
|
|
64
|
+
* never produce a report whose fields contradict each other.
|
|
65
|
+
*/
|
|
66
|
+
export declare function verifyAppInstance(client: DataikuClient, appId: string, projectKey: string, expectVersion?: string): Promise<AppInstanceVerification>;
|
|
67
|
+
/** Stage a permission copy reached. Only `copied`/`unchanged` mean the successor holds them. */
|
|
68
|
+
export type AppPermissionCopyState = "not-requested" | "unchanged" | "copied" | "not-attempted" | "rejected" | "unknown" | "completed-unverified";
|
|
69
|
+
/**
|
|
70
|
+
* True only when DSS definitively refused a write: a non-retryable 4xx with a
|
|
71
|
+
* classified client-side taxonomy. Transport failures (status 0), 5xx, retryable
|
|
72
|
+
* 4xx (408/425/429) and unclassified statuses leave the outcome unknown, so the
|
|
73
|
+
* effect of the request must be assumed possible.
|
|
74
|
+
*/
|
|
75
|
+
export declare function isDefinitiveRejection(error: unknown): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Successor identity as reported by the creation future's own result.
|
|
78
|
+
*
|
|
79
|
+
* DSS names the project it created in `targetProjectKey`, and live instances of
|
|
80
|
+
* this future are also observed carrying `projectKey` for the *source template*,
|
|
81
|
+
* so the target-specific field always wins and `projectKey` speaks for the
|
|
82
|
+
* target only when no usable `targetProjectKey` is present. A missing, blank or
|
|
83
|
+
* non-string value is no identity claim at all, exactly as a blank manifest
|
|
84
|
+
* version names no release.
|
|
85
|
+
*/
|
|
86
|
+
export declare function futureTargetIdentity(result: unknown): {
|
|
87
|
+
projectKey: string;
|
|
88
|
+
field: "targetProjectKey" | "projectKey";
|
|
89
|
+
} | null;
|
|
90
|
+
/**
|
|
91
|
+
* Creation cleanup is safe only when the target was proven absent before POST.
|
|
92
|
+
* A hidden 403 is not absence: project listing can prove a collision, but it
|
|
93
|
+
* cannot prove that an inaccessible project does not exist.
|
|
94
|
+
*/
|
|
95
|
+
export declare function requireAbsentProjectTarget(client: DataikuClient, targetProjectKey: string, targetFlag: "--to" | "targetProjectKey"): Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Create a new app instance from the current template version alongside an
|
|
98
|
+
* existing instance. Classic app instances have no public upgrade endpoint, so
|
|
99
|
+
* the predecessor is never touched: it stays available until it is separately
|
|
100
|
+
* and deliberately retired.
|
|
101
|
+
*
|
|
102
|
+
* After the creation POST this function never throws. A post-POST failure
|
|
103
|
+
* returns a failed-wait result carrying the target project key, so the CLI still
|
|
104
|
+
* exits 4 and the cleanup ledger still records an addressable successor.
|
|
105
|
+
*/
|
|
106
|
+
export declare function createSuccessorInstance(client: DataikuClient, request: SuccessorInstanceRequest, usage: string): Promise<Record<string, unknown>>;
|