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.
Files changed (84) hide show
  1. package/README.md +118 -7
  2. package/dist/packages/types/src/index.d.ts +5 -0
  3. package/dist/packages/types/src/index.js +5 -0
  4. package/dist/src/cli/coerce.d.ts +1 -3
  5. package/dist/src/cli/coerce.js +1 -17
  6. package/dist/src/cli/commands/app.js +709 -14
  7. package/dist/src/cli/commands/code-env.js +2 -2
  8. package/dist/src/cli/commands/dashboard.js +1 -1
  9. package/dist/src/cli/commands/dataset.js +2 -2
  10. package/dist/src/cli/commands/folder.js +1 -1
  11. package/dist/src/cli/commands/insight.js +2 -2
  12. package/dist/src/cli/commands/notebook.js +2 -2
  13. package/dist/src/cli/commands/recipe.js +3 -3
  14. package/dist/src/cli/commands/scenario.js +1 -1
  15. package/dist/src/cli/commands/sql.js +1 -1
  16. package/dist/src/cli/commands/variable.js +1 -1
  17. package/dist/src/cli/commands/wiki.js +1 -1
  18. package/dist/src/cli/contract.d.ts +12 -3
  19. package/dist/src/cli/contract.js +805 -49
  20. package/dist/src/cli/flags.js +8 -0
  21. package/dist/src/cli/helpers/app-permissions.d.ts +63 -0
  22. package/dist/src/cli/helpers/app-permissions.js +239 -0
  23. package/dist/src/cli/helpers/app-successor.d.ts +106 -0
  24. package/dist/src/cli/helpers/app-successor.js +625 -0
  25. package/dist/src/cli/helpers/cleanup.js +46 -12
  26. package/dist/src/cli/output.d.ts +12 -1
  27. package/dist/src/cli/output.js +53 -6
  28. package/dist/src/cli/usage.d.ts +1 -1
  29. package/dist/src/cli/usage.js +5 -2
  30. package/dist/src/cli.js +329 -32
  31. package/dist/src/client.d.ts +6 -0
  32. package/dist/src/client.js +28 -1
  33. package/dist/src/errors.d.ts +1 -1
  34. package/dist/src/resources/applications.d.ts +191 -1
  35. package/dist/src/resources/applications.js +549 -10
  36. package/dist/src/resources/datasets.js +8 -0
  37. package/dist/src/resources/futures.js +7 -2
  38. package/dist/src/skill.js +76 -18
  39. package/dist/src/utils/cleanup-ledger.d.ts +34 -1
  40. package/dist/src/utils/cleanup-ledger.js +118 -7
  41. package/dist/src/utils/dss-url.d.ts +6 -0
  42. package/dist/src/utils/dss-url.js +17 -0
  43. package/dist/src/utils/project-incarnation.d.ts +6 -0
  44. package/dist/src/utils/project-incarnation.js +27 -0
  45. package/dist/src/utils/stable-hash.d.ts +11 -0
  46. package/dist/src/utils/stable-hash.js +27 -0
  47. package/node_modules/@sinclair/typebox/build/cjs/compiler/compiler.js +26 -46
  48. package/node_modules/@sinclair/typebox/build/cjs/parser/runtime/guard.d.ts +1 -1
  49. package/node_modules/@sinclair/typebox/build/cjs/parser/runtime/token.d.ts +2 -2
  50. package/node_modules/@sinclair/typebox/build/cjs/syntax/mapping.d.ts +1 -1
  51. package/node_modules/@sinclair/typebox/build/cjs/system/evaluate.d.ts +5 -0
  52. package/node_modules/@sinclair/typebox/build/cjs/system/evaluate.js +11 -0
  53. package/node_modules/@sinclair/typebox/build/cjs/system/index.d.ts +1 -0
  54. package/node_modules/@sinclair/typebox/build/cjs/system/index.js +1 -0
  55. package/node_modules/@sinclair/typebox/build/cjs/type/object/object.d.ts +2 -2
  56. package/node_modules/@sinclair/typebox/build/cjs/type/object/object.js +18 -2
  57. package/node_modules/@sinclair/typebox/build/cjs/value/parse/parse.d.ts +1 -1
  58. package/node_modules/@sinclair/typebox/build/esm/compiler/compiler.mjs +27 -47
  59. package/node_modules/@sinclair/typebox/build/esm/parser/runtime/guard.d.mts +1 -1
  60. package/node_modules/@sinclair/typebox/build/esm/parser/runtime/token.d.mts +2 -2
  61. package/node_modules/@sinclair/typebox/build/esm/syntax/mapping.d.mts +1 -1
  62. package/node_modules/@sinclair/typebox/build/esm/system/evaluate.d.mts +5 -0
  63. package/node_modules/@sinclair/typebox/build/esm/system/evaluate.mjs +7 -0
  64. package/node_modules/@sinclair/typebox/build/esm/system/index.d.mts +1 -0
  65. package/node_modules/@sinclair/typebox/build/esm/system/index.mjs +1 -0
  66. package/node_modules/@sinclair/typebox/build/esm/type/composite/composite.mjs +2 -2
  67. package/node_modules/@sinclair/typebox/build/esm/type/const/const.mjs +3 -3
  68. package/node_modules/@sinclair/typebox/build/esm/type/instantiate/instantiate.mjs +2 -2
  69. package/node_modules/@sinclair/typebox/build/esm/type/mapped/mapped.mjs +3 -3
  70. package/node_modules/@sinclair/typebox/build/esm/type/module/compute.mjs +2 -2
  71. package/node_modules/@sinclair/typebox/build/esm/type/object/object.d.mts +2 -2
  72. package/node_modules/@sinclair/typebox/build/esm/type/object/object.mjs +18 -2
  73. package/node_modules/@sinclair/typebox/build/esm/type/omit/omit.mjs +3 -3
  74. package/node_modules/@sinclair/typebox/build/esm/type/partial/partial.mjs +3 -3
  75. package/node_modules/@sinclair/typebox/build/esm/type/pick/pick.mjs +3 -3
  76. package/node_modules/@sinclair/typebox/build/esm/type/record/record.mjs +3 -3
  77. package/node_modules/@sinclair/typebox/build/esm/type/required/required.mjs +3 -3
  78. package/node_modules/@sinclair/typebox/build/esm/type/type/json.mjs +2 -2
  79. package/node_modules/@sinclair/typebox/build/esm/value/delta/delta.mjs +4 -4
  80. package/node_modules/@sinclair/typebox/build/esm/value/parse/parse.d.mts +1 -1
  81. package/node_modules/@sinclair/typebox/package.json +1 -1
  82. package/package.json +7 -5
  83. package/packages/types/dist/index.d.ts +5 -0
  84. package/packages/types/dist/index.js +5 -0
package/dist/src/skill.js CHANGED
@@ -10,22 +10,30 @@ If the installed \`dss\` binary is unavailable but the repository checkout is th
10
10
 
11
11
  ## Contract
12
12
 
13
- - Success writes exactly one JSON result to stdout.
14
- - Failure writes exactly one JSONL error event to stderr with \`type:"error"\`, \`ok:false\`, \`error\`, \`code\`, and \`exitCode\`.
13
+ - Command results write exactly one JSON value to stdout, including \`doctor\`, \`batch\`, and \`cleanup\` failure reports; use the exit code and result fields.
14
+ - Dispatch/runtime failures write one JSONL error event to stderr with \`type:"error"\`, \`ok:false\`, \`error\`, \`code\`, and \`exitCode\`, while stdout stays empty.
15
15
  - Non-fatal warnings and \`--verbose\` HTTP traces are JSONL stderr events (\`type:"warning"\` / \`type:"trace"\`), never prose.
16
16
  - No prompts, help screens, tables, banners, or prose output are part of the contract.
17
- - Exit codes: 0 success, 1 usage/configuration error, 2 DSS or internal error, 3 transient/retryable DSS error, 4 completed command with failed long-running DSS work.
17
+ - Exit codes: 0 success, 1 usage/configuration error, 2 DSS or internal error, 3 transient/retryable DSS error, 4 a failed long-running result or synchronous assertion.
18
+ - Pass \`--json\` for compact single-line JSON on stdout; use it for agent discovery and other JSON results to minimize tokens. Without it, success JSON is pretty-printed.
18
19
  - \`--raw\` is the only stdout escape hatch: recipe payload commands emit raw bytes to stdout unless \`--output PATH\` is also set; with \`--output\`, stdout is the JSON string equal to \`PATH\` and the file receives exact raw bytes.
19
20
  - \`--fields a,b,c\` projects those fields from object or array-of-objects results; dotted paths (\`a.b.c\`) drill into nested objects, and missing fields become \`null\`; string and scalar results pass through unchanged.
20
21
 
21
22
  ## Discover commands
22
23
 
23
24
  \`\`\`text
24
- dss agent contract
25
- dss commands run
25
+ dss agent contract --fields protocol,agentContractVersion,cli,stdio,planning,compatibility --json
26
+ dss commands run --fields dataset.create --json
27
+ dss commands run --fields dataset.create.usage,dataset.create.description,dataset.create.flags,dataset.create.examples --json
28
+ dss commands run --fields dataset --json
29
+ dss agent contract --fields commands.actions --json
26
30
  \`\`\`
27
31
 
28
- Use \`dss agent contract\` once to check \`agentContractVersion\`, stderr event schemas, non-JSON escape hatches, and compatibility rules. The command registry from \`dss commands run\` is the canonical schema for resources, actions, flags, positional arguments, side effects, auth requirements, output shape, idempotency, dry-run support, structured examples, payload schemas, unsafe outputs, cleanup hints, and exit codes. Use it before choosing command syntax.
32
+ Bootstrap with the scoped \`agent contract\` call above: \`protocol,agentContractVersion,cli,stdio,planning,compatibility\` cover protocol/schema compatibility, stderr event semantics, preferred discovery commands, non-JSON escape hatches, planning rules, and compatibility guarantees in ~250 tokens. Request \`schemas\` (the JSON schemas) or \`commands\` (the contract's discovery section) only when you actually need them.
33
+
34
+ Before choosing command syntax, look the action up in the command registry: \`--fields RESOURCE.ACTION\` returns one complete entry, and appended \`.FIELD\` paths return only the metadata you need. Prefer the four-field projection \`usage,description,flags,examples\` by default — it is the smallest self-sufficient starting point for constructing an invocation. The registry entry is the canonical schema for flags, positional arguments, side effects, auth requirements, output shape, idempotency, dry-run support, structured examples, payload schemas, unsafe outputs, cleanup hints, and exit codes.
35
+
36
+ \`--fields\` takes a comma-separated list, so request several actions in one call. Each key is echoed exactly as requested (\`--fields dataset.create\` returns \`{"dataset.create": {...}}\`). Enumerate every resource and action with \`dss agent contract --fields commands.actions --json\` (~1k tokens); the hundreds-of-thousands-token unscoped registry is compatibility-only. An unknown resource or action exits with code 1 and a JSON error envelope on stderr containing the valid options.
29
37
  Credential lookup order is flags first, then \`DATAIKU_*\` environment variables, then saved credentials.
30
38
  Set \`DATAIKU_DISABLE_ENV=1\` when a test must ignore both \`.env\` files and \`DATAIKU_*\` environment variables.
31
39
  When \`.env\` loading is enabled, the CLI reads \`.env\` from the command's current working directory first and then the CLI build/root directory; the invocation directory wins on conflicting keys. Put test-specific \`.env\` files in the directory where you invoke \`dss\`.
@@ -84,25 +92,74 @@ TLS flags: \`--insecure\` disables certificate verification; \`--ca-cert PATH\`
84
92
  \`\`\`text
85
93
  dss version
86
94
  dss project list
87
- dss doctor --fast
88
95
  dss dataset list --project-key MYPROJ
89
- dss dataset list --project-key MYPROJ --fields name,type
90
- dss dataset preview orders --max-rows 10 --project-key MYPROJ
91
- dss recipe get-payload compute_orders --project-key MYPROJ
92
- dss recipe get-payload compute_orders --raw --project-key MYPROJ
93
96
  dss recipe get-payload compute_orders --raw --output code.py --project-key MYPROJ
94
97
  dss recipe diff compute_orders --file code.py --project-key MYPROJ
95
98
  dss recipe set-payload compute_orders --file code.py --project-key MYPROJ
96
99
  dss job build-and-wait orders --include-logs --project-key MYPROJ
97
- dss scenario run daily_build --project-key MYPROJ
98
100
  dss sql query --connection analytics --sql "select 1" --project-key MYPROJ
99
- dss sql query --dataset MYPROJ.orders --sql-file query.sql
100
- dss notebook clear-jupyter-outputs exploration --dry-run --project-key MYPROJ
101
- dss api-service list-packages churn-service --project-key MYPROJ
102
101
  dss batch --data-file steps.json
103
102
  \`\`\`
104
103
  For fake-DSS smoke tests, return project lists as JSON arrays such as \`[{"projectKey":"MYPROJ","name":"My Project"}]\` from \`/public/api/projects/\`; recipe payload commands read \`/public/api/projects/<PROJECT>/recipes/<NAME>?includePayload=true\` and expect a JSON object shaped like \`{"recipe":{"name":"<NAME>","type":"python"},"payload":"..."}\`.
105
104
 
105
+ ## Application release safety
106
+
107
+ Treat app release as explicit validate, compare, version, successor, verify, and permission gates.
108
+ The public app-manifest \`version\` field is raw metadata: writing it is NOT a publish transaction.
109
+ New instances inherit the template's raw \`version\`; existing instances are never upgraded in
110
+ place — create an additive successor (the old instance is preserved), verify it, then retire the
111
+ old one as a separate guarded step. Never infer private publish/recreate/rename,
112
+ recipient-sharing, or UI-click operations that the public DSS API does not expose.
113
+
114
+ \`\`\`text
115
+ dss app validate-manifest --project-key APP_TEMPLATE
116
+ dss app compare-manifest APP_ID --project-key RELEASE_INSTANCE
117
+ dss app manifest-version --project-key APP_TEMPLATE
118
+ dss app set-manifest-version --manifest-version 1.4.0 --project-key APP_TEMPLATE
119
+ # targetProjectKey in instance.json must be confirmed absent
120
+ dss app create-instance APP_ID --data-file instance.json --wait --record-cleanup cleanup.jsonl
121
+ dss app create-successor-instance APP_ID --from RELEASE_INSTANCE --to RELEASE_INSTANCE_V2 --copy-permissions --record-cleanup cleanup.jsonl
122
+ dss app verify-instance APP_ID --project-key RELEASE_INSTANCE_V2 --expect-version 1.4.0
123
+ dss cleanup --file cleanup.jsonl
124
+ dss app permissions-snapshot --project-key RELEASE_INSTANCE --output permissions.json
125
+ dss app permissions-diff --project-key RELEASE_INSTANCE --file permissions.json
126
+ dss app permissions-restore --project-key RELEASE_INSTANCE --file permissions.json --dry-run
127
+ \`\`\`
128
+
129
+ An invalid manifest exits non-zero with the validation result in the structured error details.
130
+ Review the cleanup preview before \`dss cleanup --file cleanup.jsonl --apply\`. Every new entry is
131
+ bound to the canonical DSS URL. App cleanup records a \`creationTag\` hash observed after the DSS
132
+ future identifies its target key; unconfirmed creation stops unresolved. The future target and
133
+ later \`creationTag\` are independent, non-atomic observations because the public API exposes
134
+ neither an immutable project ID joined to the future nor a conditional DELETE. Cleanup rechecks
135
+ type and \`creationTag\` immediately before deletion, rejecting detected key reuse but unable to
136
+ eliminate replacement in the remaining check-to-DELETE gap. Apply validates the full ledger before
137
+ any request and rejects legacy, mixed-server, mismatched-server, or unbound app cleanup entries.
138
+ The successor's cleanup ledger entry targets only the new project key;
139
+ the predecessor is never targeted. Some DSS deployments return 403 for an unknown target project:
140
+ instance creation requires confirmed target absence before POST, so an inaccessible target absent
141
+ from the visible project list is still rejected as unconfirmed rather than risking cleanup against
142
+ a pre-existing project. A definitive create rejection never produces a cleanup entry.
143
+
144
+ \`app set-manifest-version\` reports
145
+ \`concurrencyControl:"client-side-non-atomic-stale-read-check"\`. \`--expect-hash SHA256\` refuses the
146
+ write when the manifest already changed, but the PUT itself stays unconditional: this command can
147
+ overwrite a writer that commits inside the read-then-write window, and the post-write read cannot
148
+ detect that lost update when this command's payload wins. Never treat the hash as a serializing lock.
149
+ An ambiguous manifest PUT or verification read exits non-zero with \`persisted:null\`,
150
+ \`after:null\`, and \`outcome:"indeterminate"\`; it never claims success or rejection.
151
+
152
+ The API key authenticates public REST only. \`app verify-instance\` reports \`apiReady:true\`,
153
+ \`status:"API_VERIFIED_UI_PENDING"\`, and \`uiPublicationVerified:false\`. Its \`visual-ui\` gate
154
+ requires a pre-authenticated SSO browser session or dedicated UI test identity and evidence from
155
+ exercising the affected tiles, forms, and actions; the CLI never marks that external gate complete.
156
+ Permission snapshots have an integrity hash bound to the canonical DSS URL, project key, and
157
+ observed concrete project incarnation from \`creationTag\`; diff and restore reject detected server,
158
+ key, or incarnation mismatches. DSS exposes no conditional permission PUT, so these client-side checks
159
+ narrow and detect key-reuse races but cannot serialize the final check with the write. Snapshots
160
+ contain access-control identities; keep them mode \`0600\` and commit them only when repository
161
+ policy permits.
162
+
106
163
  ## Confirming mutations
107
164
 
108
165
  Mutations print a small JSON ack to stdout and exit 0 on success (e.g. \`{"updated":"NAME","resource":"recipe"}\`); on failure they print the error envelope to stderr and exit non-zero. The exit code is the source of truth.
@@ -125,7 +182,7 @@ Mutations print a small JSON ack to stdout and exit 0 on success (e.g. \`{"updat
125
182
 
126
183
  ## Error envelope
127
184
 
128
- Parse each stderr line as JSON; on non-zero exit the final line is an error event:
185
+ For dispatch/runtime failures, parse stderr as JSONL; the final line is an error event:
129
186
 
130
187
  \`\`\`json
131
188
  {
@@ -140,6 +197,7 @@ Parse each stderr line as JSON; on non-zero exit the final line is an error even
140
197
  }
141
198
  \`\`\`
142
199
 
200
+ \`doctor\`, \`batch\`, and \`cleanup\` report command-level failures on stdout and leave stderr empty. Use the exit code before choosing a stream.
143
201
  Use \`code\`, \`category\`, \`exitCode\`, \`retryable\`, \`status\`, and \`details\` for recovery logic. Do not scrape message text when a structured field is available.
144
202
  Treat \`details.body\` as sanitized metadata only: it contains at most \`requestId\`/\`request_id\`/\`errorId\`/\`elapsedMs\` plus locally trusted target/timing fields, not the arbitrary DSS response body. \`details.statusText\` is canonical text derived from the numeric status, not a remote reason phrase. Base recovery on top-level \`code\`, \`category\`, \`status\`, \`retryable\`, \`requestId\`, \`hint\`, and \`details.dssCategory\`; never parse or expose assumed server-body fields.
145
203
  `;
@@ -148,8 +206,8 @@ name: dataiku-dss
148
206
  description: >-
149
207
  Agent-only JSON CLI for Dataiku DSS. Use to inspect or mutate DSS projects,
150
208
  datasets, recipes, jobs, scenarios, folders, notebooks, SQL, variables,
151
- code envs, and connections. Discover the full machine-readable surface with
152
- dss commands run.
209
+ code envs, and connections. Discover scoped command metadata with
210
+ dss commands run --fields RESOURCE.ACTION --json.
153
211
  ---
154
212
 
155
213
  `;
@@ -7,9 +7,42 @@ export interface CleanupLedgerEntry {
7
7
  name?: string;
8
8
  path?: string;
9
9
  projectKey?: string;
10
+ /** Canonical DSS base URL the entry was recorded against. Absent only on legacy ledgers. */
11
+ dssUrl?: string;
10
12
  cleanup: {
11
13
  argv: string[];
12
14
  };
13
15
  }
14
- export declare function appendCleanupLedgerEntry(filePath: string, entry: CleanupLedgerEntry): Promise<void>;
16
+ /**
17
+ * Prove that a cleanup ledger path can be written before any remote mutation.
18
+ * A parent directory is created when missing (matching append behavior); an
19
+ * existing ledger file is opened for append; a not-yet-existing ledger file is
20
+ * probed by creating and removing a sibling file, so nothing is left behind.
21
+ * Throws a plain Error with a non-secret message on any failure.
22
+ */
23
+ export declare function preflightCleanupLedgerPath(filePath: string): Promise<void>;
24
+ /**
25
+ * Atomically bind a cleanup ledger path to one DSS server before a mutation.
26
+ * The adjacent owner-only reservation closes the empty-ledger race: processes
27
+ * targeting different servers cannot both pass preflight and mutate before
28
+ * either cleanup entry is appended. The reservation intentionally persists
29
+ * even when a mutation fails, because another same-server process may already
30
+ * rely on it.
31
+ */
32
+ export declare function reserveCleanupLedgerDssUrl(filePath: string, dssUrl: string): Promise<void>;
33
+ export interface CleanupLedgerBindingViolation {
34
+ index: number;
35
+ resource: string;
36
+ action: string;
37
+ reason: "missing" | "mismatch";
38
+ /** Canonical URL the entry was bound to, for mismatch reasons. */
39
+ found?: string;
40
+ }
41
+ /**
42
+ * First entry not exactly bound to `expectedDssUrl` (both canonicalized), in
43
+ * ledger order. Legacy entries without a `dssUrl` count as missing. Intentionally
44
+ * never exposes argv/ids: the violation is identifiable by index alone.
45
+ */
46
+ export declare function findCleanupLedgerBindingViolation(entries: CleanupLedgerEntry[], expectedDssUrl: string): CleanupLedgerBindingViolation | undefined;
47
+ export declare function appendCleanupLedgerEntry(filePath: string, entry: CleanupLedgerEntry, dssUrl: string): Promise<void>;
15
48
  export declare function readCleanupLedger(filePath: string): Promise<CleanupLedgerEntry[]>;
@@ -1,5 +1,6 @@
1
- import { appendFile, mkdir, readFile, } from "node:fs/promises";
2
- import { dirname, resolve, } from "node:path";
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { canonicalDssUrl, } from "./dss-url.js";
3
4
  function isCleanupLedgerEntry(value) {
4
5
  if (!value || typeof value !== "object")
5
6
  return false;
@@ -12,16 +13,126 @@ function isCleanupLedgerEntry(value) {
12
13
  return false;
13
14
  if (!candidate.cleanup || typeof candidate.cleanup !== "object")
14
15
  return false;
16
+ if (candidate.dssUrl !== undefined && typeof candidate.dssUrl !== "string")
17
+ return false;
15
18
  const cleanup = candidate.cleanup;
16
19
  return Array.isArray(cleanup.argv) && cleanup.argv.every((arg) => typeof arg === "string");
17
20
  }
18
- export async function appendCleanupLedgerEntry(filePath, entry) {
19
- const resolved = resolve(filePath);
20
- await mkdir(dirname(resolved), { recursive: true, });
21
- await appendFile(resolved, `${JSON.stringify(entry)}\n`, "utf-8");
21
+ /**
22
+ * Prove that a cleanup ledger path can be written before any remote mutation.
23
+ * A parent directory is created when missing (matching append behavior); an
24
+ * existing ledger file is opened for append; a not-yet-existing ledger file is
25
+ * probed by creating and removing a sibling file, so nothing is left behind.
26
+ * Throws a plain Error with a non-secret message on any failure.
27
+ */
28
+ export async function preflightCleanupLedgerPath(filePath) {
29
+ const resolved = path.resolve(filePath);
30
+ const parent = path.dirname(resolved);
31
+ await fs.mkdir(parent, { recursive: true, });
32
+ let stats;
33
+ try {
34
+ stats = await fs.stat(resolved);
35
+ }
36
+ catch (error) {
37
+ if (error.code !== "ENOENT") {
38
+ throw new Error(`Could not preflight cleanup ledger ${resolved}: ${error instanceof Error ? error.message : String(error)}`, { cause: error, });
39
+ }
40
+ }
41
+ if (stats?.isDirectory()) {
42
+ throw new Error(`Cleanup ledger path is a directory, not a file: ${resolved}`);
43
+ }
44
+ if (stats !== undefined) {
45
+ try {
46
+ const handle = await fs.open(resolved, "a");
47
+ await handle.close();
48
+ }
49
+ catch (error) {
50
+ const code = error.code;
51
+ throw new Error(code === "EACCES" || code === "EPERM"
52
+ ? `Cleanup ledger file is not writable: ${resolved}`
53
+ : `Could not preflight cleanup ledger ${resolved}: ${error instanceof Error ? error.message : String(error)}`, { cause: error, });
54
+ }
55
+ return;
56
+ }
57
+ const probePrefix = `${resolved}.preflight-`;
58
+ try {
59
+ const probe = await fs.mkdtemp(probePrefix);
60
+ await fs.rm(probe, { recursive: true, force: true, });
61
+ }
62
+ catch (error) {
63
+ throw new Error(`Cleanup ledger directory is not writable: ${parent}${error instanceof Error ? ` (${error.message})` : ""}`, { cause: error, });
64
+ }
65
+ }
66
+ /**
67
+ * Atomically bind a cleanup ledger path to one DSS server before a mutation.
68
+ * The adjacent owner-only reservation closes the empty-ledger race: processes
69
+ * targeting different servers cannot both pass preflight and mutate before
70
+ * either cleanup entry is appended. The reservation intentionally persists
71
+ * even when a mutation fails, because another same-server process may already
72
+ * rely on it.
73
+ */
74
+ export async function reserveCleanupLedgerDssUrl(filePath, dssUrl) {
75
+ const resolved = path.resolve(filePath);
76
+ const bindingPath = `${resolved}.dss-url`;
77
+ const expected = canonicalDssUrl(dssUrl);
78
+ await fs.mkdir(path.dirname(resolved), { recursive: true, });
79
+ try {
80
+ await fs.writeFile(bindingPath, `${expected}\n`, {
81
+ encoding: "utf-8",
82
+ flag: "wx",
83
+ mode: 0o600,
84
+ });
85
+ return;
86
+ }
87
+ catch (error) {
88
+ if (error.code !== "EEXIST")
89
+ throw error;
90
+ }
91
+ const raw = await fs.readFile(bindingPath, "utf-8");
92
+ let found;
93
+ try {
94
+ found = canonicalDssUrl(raw.trim());
95
+ }
96
+ catch (error) {
97
+ throw new Error("Cleanup ledger DSS binding is invalid.", { cause: error, });
98
+ }
99
+ if (found !== expected) {
100
+ throw new Error("Cleanup ledger is reserved for a different DSS server.");
101
+ }
102
+ }
103
+ /**
104
+ * First entry not exactly bound to `expectedDssUrl` (both canonicalized), in
105
+ * ledger order. Legacy entries without a `dssUrl` count as missing. Intentionally
106
+ * never exposes argv/ids: the violation is identifiable by index alone.
107
+ */
108
+ export function findCleanupLedgerBindingViolation(entries, expectedDssUrl) {
109
+ const expected = canonicalDssUrl(expectedDssUrl);
110
+ for (const [index, entry,] of entries.entries()) {
111
+ const bound = typeof entry.dssUrl === "string" ? canonicalDssUrl(entry.dssUrl) : undefined;
112
+ if (!bound) {
113
+ return { index, resource: entry.resource, action: entry.action, reason: "missing", };
114
+ }
115
+ if (bound !== expected) {
116
+ return {
117
+ index,
118
+ resource: entry.resource,
119
+ action: entry.action,
120
+ reason: "mismatch",
121
+ found: bound,
122
+ };
123
+ }
124
+ }
125
+ return undefined;
126
+ }
127
+ export async function appendCleanupLedgerEntry(filePath, entry, dssUrl) {
128
+ const resolved = path.resolve(filePath);
129
+ await fs.mkdir(path.dirname(resolved), { recursive: true, });
130
+ await reserveCleanupLedgerDssUrl(resolved, dssUrl);
131
+ const bound = { ...entry, dssUrl: canonicalDssUrl(dssUrl), };
132
+ await fs.appendFile(resolved, `${JSON.stringify(bound)}\n`, "utf-8");
22
133
  }
23
134
  export async function readCleanupLedger(filePath) {
24
- const content = await readFile(resolve(filePath), "utf-8");
135
+ const content = await fs.readFile(path.resolve(filePath), "utf-8");
25
136
  return content
26
137
  .split(/\r?\n/)
27
138
  .map((line) => line.trim())
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Canonical identity for a DSS base URL stored in local safety artifacts.
3
+ * Formatting differences do not break binding, and embedded URL credentials
4
+ * are never serialized or echoed.
5
+ */
6
+ export declare function canonicalDssUrl(raw: string): string;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Canonical identity for a DSS base URL stored in local safety artifacts.
3
+ * Formatting differences do not break binding, and embedded URL credentials
4
+ * are never serialized or echoed.
5
+ */
6
+ export function canonicalDssUrl(raw) {
7
+ const trimmed = raw.trim().replace(/\/+$/, "");
8
+ try {
9
+ const parsed = new URL(trimmed);
10
+ parsed.username = "";
11
+ parsed.password = "";
12
+ return parsed.toString().replace(/\/+$/, "");
13
+ }
14
+ catch {
15
+ return trimmed.replace(/^([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^/@]*@/, "$1");
16
+ }
17
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Fingerprint one concrete DSS project incarnation using only the immutable
3
+ * creationTag. Mutable project metadata, including projectAppType and
4
+ * versionTag, is intentionally excluded.
5
+ */
6
+ export declare function projectIncarnationHash(projectKey: string, projectDetails: unknown): string | undefined;
@@ -0,0 +1,27 @@
1
+ import { stableHash, } from "./stable-hash.js";
2
+ function plainRecord(value) {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value)
4
+ ? value
5
+ : undefined;
6
+ }
7
+ /**
8
+ * Fingerprint one concrete DSS project incarnation using only the immutable
9
+ * creationTag. Mutable project metadata, including projectAppType and
10
+ * versionTag, is intentionally excluded.
11
+ */
12
+ export function projectIncarnationHash(projectKey, projectDetails) {
13
+ const details = plainRecord(projectDetails);
14
+ if (!details)
15
+ return undefined;
16
+ const returnedKey = details["projectKey"];
17
+ if (typeof returnedKey !== "string" || returnedKey.trim() !== projectKey)
18
+ return undefined;
19
+ const creationTag = plainRecord(details["creationTag"]);
20
+ if (!creationTag)
21
+ return undefined;
22
+ const lastModifiedOn = creationTag["lastModifiedOn"];
23
+ if (!(typeof lastModifiedOn === "number" && Number.isFinite(lastModifiedOn))
24
+ && !(typeof lastModifiedOn === "string" && lastModifiedOn.trim().length > 0))
25
+ return undefined;
26
+ return stableHash({ projectKey, creationTag, });
27
+ }
@@ -0,0 +1,11 @@
1
+ export declare function sha256Hex(value: string): string;
2
+ /** Locale-independent UTF-16 code-unit order for deterministic machine output. */
3
+ export declare function compareStrings(a: string, b: string): number;
4
+ /**
5
+ * Deterministic JSON rendering: object keys are emitted in sorted order so two
6
+ * structurally identical values always produce the same string (and therefore
7
+ * the same hash) regardless of key insertion order. Array order is preserved
8
+ * because it is semantic.
9
+ */
10
+ export declare function stableJson(value: unknown): string;
11
+ export declare function stableHash(value: unknown): string;
@@ -0,0 +1,27 @@
1
+ import { createHash, } from "node:crypto";
2
+ export function sha256Hex(value) {
3
+ return createHash("sha256").update(value).digest("hex");
4
+ }
5
+ /** Locale-independent UTF-16 code-unit order for deterministic machine output. */
6
+ export function compareStrings(a, b) {
7
+ return a < b ? -1 : a > b ? 1 : 0;
8
+ }
9
+ /**
10
+ * Deterministic JSON rendering: object keys are emitted in sorted order so two
11
+ * structurally identical values always produce the same string (and therefore
12
+ * the same hash) regardless of key insertion order. Array order is preserved
13
+ * because it is semantic.
14
+ */
15
+ export function stableJson(value) {
16
+ if (value === undefined)
17
+ return "undefined";
18
+ if (value === null || typeof value !== "object")
19
+ return JSON.stringify(value);
20
+ if (Array.isArray(value))
21
+ return `[${value.map((item) => stableJson(item)).join(",")}]`;
22
+ const entries = Object.entries(value).sort(([a,], [b,]) => compareStrings(a, b));
23
+ return `{${entries.map(([key, item,]) => `${JSON.stringify(key)}:${stableJson(item)}`).join(",")}}`;
24
+ }
25
+ export function stableHash(value) {
26
+ return sha256Hex(stableJson(value));
27
+ }
@@ -91,35 +91,6 @@ var Character;
91
91
  Character.IsNumeric = IsNumeric;
92
92
  })(Character || (Character = {}));
93
93
  // ------------------------------------------------------------------
94
- // MemberExpression
95
- // ------------------------------------------------------------------
96
- var MemberExpression;
97
- (function (MemberExpression) {
98
- function IsFirstCharacterNumeric(value) {
99
- if (value.length === 0)
100
- return false;
101
- return Character.IsNumeric(value.charCodeAt(0));
102
- }
103
- function IsAccessor(value) {
104
- if (IsFirstCharacterNumeric(value))
105
- return false;
106
- for (let i = 0; i < value.length; i++) {
107
- const code = value.charCodeAt(i);
108
- const check = Character.IsAlpha(code) || Character.IsNumeric(code) || Character.DollarSign(code) || Character.IsUnderscore(code);
109
- if (!check)
110
- return false;
111
- }
112
- return true;
113
- }
114
- function EscapeHyphen(key) {
115
- return key.replace(/'/g, "\\'");
116
- }
117
- function Encode(object, key) {
118
- return IsAccessor(key) ? `${object}.${key}` : `${object}['${EscapeHyphen(key)}']`;
119
- }
120
- MemberExpression.Encode = Encode;
121
- })(MemberExpression || (MemberExpression = {}));
122
- // ------------------------------------------------------------------
123
94
  // Identifier
124
95
  // ------------------------------------------------------------------
125
96
  var Identifier;
@@ -140,15 +111,21 @@ var Identifier;
140
111
  Identifier.Encode = Encode;
141
112
  })(Identifier || (Identifier = {}));
142
113
  // ------------------------------------------------------------------
143
- // LiteralString
114
+ // StringConstant
144
115
  // ------------------------------------------------------------------
145
- var LiteralString;
146
- (function (LiteralString) {
147
- function Escape(content) {
148
- return content.replace(/'/g, "\\'");
149
- }
150
- LiteralString.Escape = Escape;
151
- })(LiteralString || (LiteralString = {}));
116
+ function StringConstant(value) {
117
+ if (!(0, index_12.IsString)(value))
118
+ throw Error('ConstantString: Not a String');
119
+ const canonical = JSON.stringify(value).slice(1, -1);
120
+ const escaped = canonical.replace(/'/g, "\\'");
121
+ return `'${escaped}'`;
122
+ }
123
+ // ------------------------------------------------------------------
124
+ // MemberExpression
125
+ // ------------------------------------------------------------------
126
+ function MemberExpression(value, key) {
127
+ return `${value}[${StringConstant(key)}]`;
128
+ }
152
129
  // ------------------------------------------------------------------
153
130
  // Errors
154
131
  // ------------------------------------------------------------------
@@ -172,7 +149,7 @@ exports.TypeCompilerTypeGuardError = TypeCompilerTypeGuardError;
172
149
  var Policy;
173
150
  (function (Policy) {
174
151
  function IsExactOptionalProperty(value, key, expression) {
175
- return index_3.TypeSystemPolicy.ExactOptionalPropertyTypes ? `('${key}' in ${value} ? ${expression} : true)` : `(${MemberExpression.Encode(value, key)} !== undefined ? ${expression} : true)`;
152
+ return index_3.TypeSystemPolicy.ExactOptionalPropertyTypes ? `(${StringConstant(key)} in ${value} ? ${expression} : true)` : `(${MemberExpression(value, key)} !== undefined ? ${expression} : true)`;
176
153
  }
177
154
  Policy.IsExactOptionalProperty = IsExactOptionalProperty;
178
155
  function IsObjectLike(value) {
@@ -317,8 +294,11 @@ var TypeCompiler;
317
294
  if (typeof schema.const === 'number' || typeof schema.const === 'boolean') {
318
295
  yield `(${value} === ${schema.const})`;
319
296
  }
297
+ else if (typeof schema.const === 'string') {
298
+ yield `(${value} === ${StringConstant(schema.const)})`;
299
+ }
320
300
  else {
321
- yield `(${value} === '${LiteralString.Escape(schema.const)}')`;
301
+ throw Error('Invalid Literal Value');
322
302
  }
323
303
  }
324
304
  function* FromNever(schema, references, value) {
@@ -352,12 +332,12 @@ var TypeCompiler;
352
332
  yield `Object.getOwnPropertyNames(${value}).length <= ${schema.maxProperties}`;
353
333
  const knownKeys = Object.getOwnPropertyNames(schema.properties);
354
334
  for (const knownKey of knownKeys) {
355
- const memberExpression = MemberExpression.Encode(value, knownKey);
335
+ const memberExpression = MemberExpression(value, knownKey);
356
336
  const property = schema.properties[knownKey];
357
337
  if (schema.required && schema.required.includes(knownKey)) {
358
338
  yield* Visit(property, references, memberExpression);
359
339
  if ((0, extends_undefined_1.ExtendsUndefinedCheck)(property) || IsAnyOrUnknown(property))
360
- yield `('${knownKey}' in ${value})`;
340
+ yield `(${StringConstant(knownKey)} in ${value})`;
361
341
  }
362
342
  else {
363
343
  const expression = CreateExpression(property, references, memberExpression);
@@ -369,13 +349,13 @@ var TypeCompiler;
369
349
  yield `Object.getOwnPropertyNames(${value}).length === ${knownKeys.length}`;
370
350
  }
371
351
  else {
372
- const keys = `[${knownKeys.map((key) => `'${key}'`).join(', ')}]`;
352
+ const keys = `[${knownKeys.map((key) => `${StringConstant(key)}`).join(', ')}]`;
373
353
  yield `Object.getOwnPropertyNames(${value}).every(key => ${keys}.includes(key))`;
374
354
  }
375
355
  }
376
356
  if (typeof schema.additionalProperties === 'object') {
377
357
  const expression = CreateExpression(schema.additionalProperties, references, `${value}[key]`);
378
- const keys = `[${knownKeys.map((key) => `'${key}'`).join(', ')}]`;
358
+ const keys = `[${knownKeys.map((key) => `${StringConstant(key)}`).join(', ')}]`;
379
359
  yield `(Object.getOwnPropertyNames(${value}).every(key => ${keys}.includes(key) || ${expression}))`;
380
360
  }
381
361
  }
@@ -423,7 +403,7 @@ var TypeCompiler;
423
403
  yield `${variable}.test(${value})`;
424
404
  }
425
405
  if (schema.format !== undefined) {
426
- yield `format('${schema.format}', ${value})`;
406
+ yield `format(${StringConstant(schema.format)}, ${value})`;
427
407
  }
428
408
  }
429
409
  function* FromSymbol(schema, references, value) {
@@ -471,7 +451,7 @@ var TypeCompiler;
471
451
  function* FromKind(schema, references, value) {
472
452
  const instance = state.instances.size;
473
453
  state.instances.set(instance, schema);
474
- yield `kind('${schema[index_7.Kind]}', ${instance}, ${value})`;
454
+ yield `kind(${StringConstant(schema[index_7.Kind])}, ${instance}, ${value})`;
475
455
  }
476
456
  function* Visit(schema, references, value, useHoisting = true) {
477
457
  const references_ = (0, index_12.IsString)(schema.$id) ? [...references, schema] : references;
@@ -645,7 +625,7 @@ var TypeCompiler;
645
625
  /** Compiles a TypeBox type for optimal runtime type checking. Types must be valid TypeBox types of TSchema */
646
626
  function Compile(schema, references = []) {
647
627
  const generatedCode = Code(schema, references, { language: 'javascript' });
648
- const compiledFunction = globalThis.Function('kind', 'format', 'hash', generatedCode);
628
+ const compiledFunction = (0, index_3.Evaluate)('kind', 'format', 'hash', generatedCode);
649
629
  const instances = new Map(state.instances);
650
630
  function typeRegistryFunction(kind, instance, value) {
651
631
  if (!index_8.TypeRegistry.Has(kind) || !instances.has(instance))
@@ -20,4 +20,4 @@ export declare function IsTuple(value: unknown): value is ITuple;
20
20
  /** Returns true if the value is a Union Parser */
21
21
  export declare function IsUnion(value: unknown): value is IUnion;
22
22
  /** Returns true if the value is a Parser */
23
- export declare function IsParser(value: unknown): value is IContext<unknown> | IUnion<unknown> | IArray<unknown> | IConst<unknown> | IIdent<unknown> | INumber<unknown> | IOptional<unknown> | IRef<unknown> | IString<unknown> | ITuple<unknown>;
23
+ export declare function IsParser(value: unknown): value is IArray<unknown> | IConst<unknown> | IContext<unknown> | IIdent<unknown> | INumber<unknown> | IOptional<unknown> | IRef<unknown> | IString<unknown> | ITuple<unknown> | IUnion<unknown>;
@@ -3,6 +3,6 @@ export declare function Const(value: string, code: string): [] | [string, string
3
3
  /** Scans for the next Ident token */
4
4
  export declare function Ident(code: string): [] | [string, string];
5
5
  /** Scans for the next number token */
6
- export declare function Number(code: string): [string, string] | [];
6
+ export declare function Number(code: string): [] | [string, string];
7
7
  /** Scans the next Literal String value */
8
- export declare function String(options: string[], code: string): [string, string] | [];
8
+ export declare function String(options: string[], code: string): [] | [string, string];
@@ -121,7 +121,7 @@ export declare function AsyncIteratorMapping(input: [unknown, unknown, unknown,
121
121
  export type TIteratorMapping<Input extends [unknown, unknown, unknown, unknown], Context extends T.TProperties> = Input extends ['Iterator', '<', infer Type extends T.TSchema, '>'] ? T.TIterator<Type> : never;
122
122
  export declare function IteratorMapping(input: [unknown, unknown, unknown, unknown], context: unknown): T.TIterator<T.TSchema>;
123
123
  export type TArgumentMapping<Input extends [unknown, unknown, unknown, unknown], Context extends T.TProperties> = Input extends ['Argument', '<', infer Type extends T.TSchema, '>'] ? Type extends T.TLiteral<infer Index extends number> ? T.TArgument<Index> : T.TNever : never;
124
- export declare function ArgumentMapping(input: [unknown, unknown, unknown, unknown], context: unknown): T.TNever | T.TArgument<number>;
124
+ export declare function ArgumentMapping(input: [unknown, unknown, unknown, unknown], context: unknown): T.TArgument<number> | T.TNever;
125
125
  export type TAwaitedMapping<Input extends [unknown, unknown, unknown, unknown], Context extends T.TProperties> = Input extends ['Awaited', '<', infer Type extends T.TSchema, '>'] ? T.TAwaited<Type> : never;
126
126
  export declare function AwaitedMapping(input: [unknown, unknown, unknown, unknown], context: unknown): T.TSchema;
127
127
  export type TArrayMapping<Input extends [unknown, unknown, unknown, unknown], Context extends T.TProperties> = Input extends ['Array', '<', infer Type extends T.TSchema, '>'] ? T.TArray<Type> : never;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Evaluates code in the current environment. This function matches centralized
3
+ * evaluation as implemented in TypeBox 1.x.
4
+ */
5
+ export declare function Evaluate(...args: string[]): globalThis.Function;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.Evaluate = Evaluate;
5
+ /**
6
+ * Evaluates code in the current environment. This function matches centralized
7
+ * evaluation as implemented in TypeBox 1.x.
8
+ */
9
+ function Evaluate(...args) {
10
+ return new globalThis.Function(...args);
11
+ }
@@ -1,2 +1,3 @@
1
+ export * from './evaluate';
1
2
  export * from './policy';
2
3
  export * from './system';