@zodal/dials-core 0.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.
package/dist/index.cjs ADDED
@@ -0,0 +1,739 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ UNSET: () => UNSET,
24
+ applyDependentDefaults: () => applyDependentDefaults,
25
+ applyJsonPatch: () => applyJsonPatch,
26
+ applyMergePatch: () => applyMergePatch,
27
+ baseType: () => baseType,
28
+ classifySensitivity: () => classifySensitivity,
29
+ defineDials: () => defineDials,
30
+ deserializeLayer: () => deserializeLayer,
31
+ diffJsonPatch: () => diffJsonPatch,
32
+ evaluateConstraints: () => evaluateConstraints,
33
+ extractDefaults: () => extractDefaults,
34
+ getObjectShape: () => getObjectShape,
35
+ invertJsonPatch: () => invertJsonPatch,
36
+ isSecretRef: () => isSecretRef,
37
+ isUnset: () => isUnset,
38
+ keyMergeStrategy: () => keyMergeStrategy,
39
+ layerToMergePatch: () => layerToMergePatch,
40
+ makeSecretRef: () => makeSecretRef,
41
+ maskEffectiveResult: () => maskEffectiveResult,
42
+ maskSecrets: () => maskSecrets,
43
+ mergeValues: () => mergeValues,
44
+ readMeta: () => readMeta,
45
+ redactSecretsFromLayer: () => redactSecretsFromLayer,
46
+ resolve: () => resolve,
47
+ serializeLayer: () => serializeLayer,
48
+ splitBySensitivity: () => splitBySensitivity
49
+ });
50
+ module.exports = __toCommonJS(index_exports);
51
+
52
+ // src/model.ts
53
+ var UNSET = /* @__PURE__ */ Symbol.for("@zodal/dials.UNSET");
54
+ function isUnset(v) {
55
+ return v === UNSET;
56
+ }
57
+ function isSecretRef(v) {
58
+ return typeof v === "object" && v !== null && v._tag === "SecretRef";
59
+ }
60
+
61
+ // src/util.ts
62
+ var PROTO_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
63
+ function setOwn(obj, key, value) {
64
+ if (PROTO_KEYS.has(key)) {
65
+ Object.defineProperty(obj, key, { value, writable: true, enumerable: true, configurable: true });
66
+ } else {
67
+ obj[key] = value;
68
+ }
69
+ }
70
+ function isPlainObject(v) {
71
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
72
+ const proto = Object.getPrototypeOf(v);
73
+ return proto === Object.prototype || proto === null;
74
+ }
75
+ function deepClone(v) {
76
+ if (Array.isArray(v)) return v.map((x) => deepClone(x));
77
+ if (isPlainObject(v)) {
78
+ const out = {};
79
+ for (const [k, val] of Object.entries(v)) setOwn(out, k, deepClone(val));
80
+ return out;
81
+ }
82
+ return v;
83
+ }
84
+ function deepEqual(a, b) {
85
+ if (a === b) return true;
86
+ if (Array.isArray(a) && Array.isArray(b)) {
87
+ return a.length === b.length && a.every((x, i) => deepEqual(x, b[i]));
88
+ }
89
+ if (isPlainObject(a) && isPlainObject(b)) {
90
+ const ak = Object.keys(a);
91
+ const bk = Object.keys(b);
92
+ return ak.length === bk.length && ak.every((k) => Object.prototype.hasOwnProperty.call(b, k) && deepEqual(a[k], b[k]));
93
+ }
94
+ return false;
95
+ }
96
+ function deepMerge(a, b) {
97
+ if (isPlainObject(a) && isPlainObject(b)) {
98
+ const out = deepClone(a);
99
+ for (const [k, vb] of Object.entries(b)) {
100
+ setOwn(out, k, Object.prototype.hasOwnProperty.call(out, k) ? deepMerge(out[k], vb) : deepClone(vb));
101
+ }
102
+ return out;
103
+ }
104
+ return deepClone(b);
105
+ }
106
+
107
+ // src/merge.ts
108
+ function mergeValues(values, strategy) {
109
+ if (values.length === 0) return void 0;
110
+ switch (strategy) {
111
+ case "replace":
112
+ return deepClone(values[values.length - 1]);
113
+ case "deep-merge":
114
+ return deepClone(
115
+ values.reduce((acc, v) => isPlainObject(acc) && isPlainObject(v) ? deepMerge(acc, v) : deepClone(v))
116
+ );
117
+ case "append": {
118
+ const out = [];
119
+ let sawArray = false;
120
+ for (const v of values) {
121
+ if (Array.isArray(v)) {
122
+ sawArray = true;
123
+ out.push(...deepClone(v));
124
+ }
125
+ }
126
+ return sawArray ? out : deepClone(values[values.length - 1]);
127
+ }
128
+ }
129
+ }
130
+
131
+ // src/cascade.ts
132
+ function resolve(stack, options = {}) {
133
+ const strategyFor = options.strategyFor ?? (() => "replace");
134
+ const band = stack.length + 1;
135
+ const byKey = /* @__PURE__ */ new Map();
136
+ stack.forEach((sl, index) => {
137
+ const managed = sl.managed === true;
138
+ const precedence = (managed ? band : 0) + index;
139
+ for (const [key, raw] of Object.entries(sl.layer)) {
140
+ const entry = { scope: sl.scope, value: isUnset(raw) ? void 0 : raw, isUnset: isUnset(raw), managed, precedence };
141
+ const list = byKey.get(key);
142
+ if (list) list.push(entry);
143
+ else byKey.set(key, [entry]);
144
+ }
145
+ });
146
+ const effective = {};
147
+ const provenance = {};
148
+ const conflicts = [];
149
+ for (const [key, rawEntries] of byKey) {
150
+ const entries = [...rawEntries].sort((a, b) => b.precedence - a.precedence);
151
+ const contributors = entries.filter((e) => !e.isUnset);
152
+ const shadowed = entries.map((e) => ({
153
+ scope: e.scope,
154
+ value: e.isUnset ? "UNSET" : e.value,
155
+ managed: e.managed
156
+ }));
157
+ if (contributors.length === 0) {
158
+ continue;
159
+ }
160
+ const strategy = strategyFor(key);
161
+ const winner = contributors[0];
162
+ let value;
163
+ let mergedFrom;
164
+ if (strategy === "deep-merge") {
165
+ const lowToHigh = [...contributors].reverse();
166
+ value = mergeValues(lowToHigh.map((e) => e.value), "deep-merge");
167
+ const contributing = deepMergeContributors(contributors, value);
168
+ mergedFrom = contributing.length > 1 ? contributing : void 0;
169
+ } else if (strategy === "append") {
170
+ const lowToHigh = [...contributors].reverse();
171
+ value = mergeValues(lowToHigh.map((e) => e.value), "append");
172
+ const contributing = contributingScopes(lowToHigh, "append", value);
173
+ mergedFrom = contributing.length > 1 ? contributing : void 0;
174
+ } else {
175
+ value = deepClone(winner.value);
176
+ }
177
+ effective[key] = value;
178
+ provenance[key] = {
179
+ key,
180
+ winningScope: winner.scope,
181
+ value,
182
+ managed: winner.managed,
183
+ mergeStrategy: strategy,
184
+ shadowed: shadowed.filter((s, i) => !(entries[i].scope === winner.scope && entries[i].precedence === winner.precedence)),
185
+ mergedFrom
186
+ };
187
+ const distinct = [];
188
+ for (const c of contributors) if (!distinct.some((d) => deepEqual(d, c.value))) distinct.push(c.value);
189
+ if (distinct.length > 1) {
190
+ const overriddenByPolicy = winner.managed && contributors.some((c) => !c.managed && !deepEqual(c.value, winner.value));
191
+ conflicts.push({
192
+ key,
193
+ contributors: contributors.map((c) => ({ scope: c.scope, value: c.value, managed: c.managed })),
194
+ overriddenByPolicy
195
+ });
196
+ }
197
+ }
198
+ return { effective, provenance, conflicts };
199
+ }
200
+ function flattenLeaves(value, prefix, out) {
201
+ if (isPlainObject(value)) {
202
+ for (const [k, v] of Object.entries(value)) flattenLeaves(v, `${prefix}/${k}`, out);
203
+ } else {
204
+ out.set(prefix, value);
205
+ }
206
+ }
207
+ function deepMergeContributors(contributorsHighToLow, merged) {
208
+ const mergedLeaves = /* @__PURE__ */ new Map();
209
+ flattenLeaves(merged, "", mergedLeaves);
210
+ const perEntry = contributorsHighToLow.map((e) => {
211
+ const m = /* @__PURE__ */ new Map();
212
+ flattenLeaves(e.value, "", m);
213
+ return m;
214
+ });
215
+ const owners = /* @__PURE__ */ new Set();
216
+ for (const [path, val] of mergedLeaves) {
217
+ for (let i = 0; i < contributorsHighToLow.length; i += 1) {
218
+ const leaf = perEntry[i].get(path);
219
+ if (perEntry[i].has(path) && deepEqual(leaf, val)) {
220
+ owners.add(contributorsHighToLow[i].scope);
221
+ break;
222
+ }
223
+ }
224
+ }
225
+ return [...contributorsHighToLow].reverse().filter((e) => owners.has(e.scope)).map((e) => e.scope);
226
+ }
227
+ function contributingScopes(lowToHigh, strategy, full) {
228
+ const values = lowToHigh.map((e) => e.value);
229
+ const out = [];
230
+ for (let i = 0; i < lowToHigh.length; i += 1) {
231
+ const without = mergeValues(
232
+ values.filter((_, j) => j !== i),
233
+ strategy
234
+ );
235
+ if (!deepEqual(without, full)) out.push(lowToHigh[i].scope);
236
+ }
237
+ return out;
238
+ }
239
+
240
+ // src/patch.ts
241
+ var PROTO_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
242
+ function applyMergePatch(target, patch) {
243
+ if (!isPlainObject(patch)) return deepClone(patch);
244
+ const base = isPlainObject(target) ? deepClone(target) : {};
245
+ for (const [key, val] of Object.entries(patch)) {
246
+ if (PROTO_KEYS2.has(key)) continue;
247
+ if (val === null) delete base[key];
248
+ else base[key] = applyMergePatch(base[key], val);
249
+ }
250
+ return base;
251
+ }
252
+ function serializeLayer(layer) {
253
+ const values = {};
254
+ const unset = [];
255
+ for (const [k, v] of Object.entries(layer)) {
256
+ if (isUnset(v)) unset.push(k);
257
+ else values[k] = deepClone(v);
258
+ }
259
+ return { values, unset };
260
+ }
261
+ function deserializeLayer(s) {
262
+ const layer = {};
263
+ for (const [k, v] of Object.entries(s.values ?? {})) layer[k] = deepClone(v);
264
+ for (const k of s.unset ?? []) layer[k] = UNSET;
265
+ return layer;
266
+ }
267
+ function layerToMergePatch(layer) {
268
+ const patch = {};
269
+ for (const [k, v] of Object.entries(layer)) patch[k] = isUnset(v) ? null : deepClone(v);
270
+ return patch;
271
+ }
272
+ var ARRAY_INDEX = /^(?:0|[1-9][0-9]*)$/;
273
+ function parsePointer(pointer) {
274
+ if (pointer === "") return [];
275
+ if (pointer[0] !== "/") throw new Error(`Invalid JSON Pointer (must start with "/"): ${pointer}`);
276
+ return pointer.slice(1).split("/").map((tok) => tok.replace(/~1/g, "/").replace(/~0/g, "~"));
277
+ }
278
+ function arrayIndex(token, length, mode) {
279
+ if (mode === "add" && token === "-") return length;
280
+ if (!ARRAY_INDEX.test(token)) throw new Error(`Invalid array index "${token}"`);
281
+ const idx = Number(token);
282
+ if (mode === "add" ? idx > length : idx >= length) {
283
+ throw new Error(`Array index out of bounds: ${token} (length ${length})`);
284
+ }
285
+ return idx;
286
+ }
287
+ function getAtPointer(doc, tokens) {
288
+ let cur = doc;
289
+ for (const tok of tokens) {
290
+ if (Array.isArray(cur)) {
291
+ if (tok === "-" || !ARRAY_INDEX.test(tok)) return void 0;
292
+ cur = cur[Number(tok)];
293
+ } else if (isPlainObject(cur)) {
294
+ if (PROTO_KEYS2.has(tok)) return void 0;
295
+ cur = cur[tok];
296
+ } else {
297
+ return void 0;
298
+ }
299
+ }
300
+ return cur;
301
+ }
302
+ function setAtPointer(doc, tokens, value, insert) {
303
+ if (tokens.length === 0) return value;
304
+ const last = tokens[tokens.length - 1];
305
+ const parent = getAtPointer(doc, tokens.slice(0, -1));
306
+ if (Array.isArray(parent)) {
307
+ const idx = arrayIndex(last, parent.length, insert ? "add" : "access");
308
+ if (insert) parent.splice(idx, 0, value);
309
+ else parent[idx] = value;
310
+ } else if (isPlainObject(parent)) {
311
+ if (PROTO_KEYS2.has(last)) throw new Error(`Refusing to set prototype-polluting key: ${last}`);
312
+ parent[last] = value;
313
+ } else {
314
+ throw new Error(`Cannot set at pointer; parent is not a container: /${tokens.slice(0, -1).join("/")}`);
315
+ }
316
+ return doc;
317
+ }
318
+ function removeAtPointer(doc, tokens) {
319
+ if (tokens.length === 0) throw new Error("Cannot remove the document root");
320
+ const last = tokens[tokens.length - 1];
321
+ const parent = getAtPointer(doc, tokens.slice(0, -1));
322
+ if (Array.isArray(parent)) {
323
+ const idx = arrayIndex(last, parent.length, "access");
324
+ parent.splice(idx, 1);
325
+ } else if (isPlainObject(parent)) {
326
+ if (PROTO_KEYS2.has(last) || !Object.prototype.hasOwnProperty.call(parent, last)) {
327
+ throw new Error(`Cannot remove; member does not exist: /${tokens.join("/")}`);
328
+ }
329
+ delete parent[last];
330
+ } else {
331
+ throw new Error(`Cannot remove at pointer; parent is not a container: /${tokens.slice(0, -1).join("/")}`);
332
+ }
333
+ return doc;
334
+ }
335
+ function pointerExists(doc, tokens) {
336
+ if (tokens.length === 0) return true;
337
+ const parent = getAtPointer(doc, tokens.slice(0, -1));
338
+ const last = tokens[tokens.length - 1];
339
+ if (Array.isArray(parent)) return ARRAY_INDEX.test(last) && Number(last) < parent.length;
340
+ if (isPlainObject(parent)) return !PROTO_KEYS2.has(last) && Object.prototype.hasOwnProperty.call(parent, last);
341
+ return false;
342
+ }
343
+ function applyJsonPatch(doc, ops) {
344
+ let result = deepClone(doc);
345
+ for (const op of ops) {
346
+ switch (op.op) {
347
+ case "add": {
348
+ result = setAtPointer(result, parsePointer(op.path), deepClone(op.value), true);
349
+ break;
350
+ }
351
+ case "remove": {
352
+ result = removeAtPointer(result, parsePointer(op.path));
353
+ break;
354
+ }
355
+ case "replace": {
356
+ const tokens = parsePointer(op.path);
357
+ if (!pointerExists(result, tokens)) throw new Error(`replace target does not exist: ${op.path}`);
358
+ result = setAtPointer(result, tokens, deepClone(op.value), false);
359
+ break;
360
+ }
361
+ case "move": {
362
+ const fromTokens = parsePointer(op.from);
363
+ if (!pointerExists(result, fromTokens)) throw new Error(`move source does not exist: ${op.from}`);
364
+ const val = deepClone(getAtPointer(result, fromTokens));
365
+ result = removeAtPointer(result, fromTokens);
366
+ result = setAtPointer(result, parsePointer(op.path), val, true);
367
+ break;
368
+ }
369
+ case "copy": {
370
+ const fromTokens = parsePointer(op.from);
371
+ if (!pointerExists(result, fromTokens)) throw new Error(`copy source does not exist: ${op.from}`);
372
+ const val = deepClone(getAtPointer(result, fromTokens));
373
+ result = setAtPointer(result, parsePointer(op.path), val, true);
374
+ break;
375
+ }
376
+ case "test": {
377
+ const tokens = parsePointer(op.path);
378
+ if (!pointerExists(result, tokens) || !deepEqual(getAtPointer(result, tokens), op.value)) {
379
+ throw new Error(`test failed at ${op.path}`);
380
+ }
381
+ break;
382
+ }
383
+ default: {
384
+ throw new Error(`Unknown JSON Patch op: ${op.op}`);
385
+ }
386
+ }
387
+ }
388
+ return result;
389
+ }
390
+ function diffJsonPatch(before, after, basePath = "") {
391
+ if (deepEqual(before, after)) return [];
392
+ if (!isPlainObject(before) || !isPlainObject(after)) {
393
+ return [{ op: "replace", path: basePath, value: deepClone(after) }];
394
+ }
395
+ const ops = [];
396
+ const enc = (k) => k.replace(/~/g, "~0").replace(/\//g, "~1");
397
+ for (const k of Object.keys(before)) {
398
+ const path = `${basePath}/${enc(k)}`;
399
+ if (!Object.prototype.hasOwnProperty.call(after, k)) ops.push({ op: "remove", path });
400
+ else ops.push(...diffJsonPatch(before[k], after[k], path));
401
+ }
402
+ for (const k of Object.keys(after)) {
403
+ if (!Object.prototype.hasOwnProperty.call(before, k)) {
404
+ ops.push({ op: "add", path: `${basePath}/${enc(k)}`, value: deepClone(after[k]) });
405
+ }
406
+ }
407
+ return ops;
408
+ }
409
+ function invertJsonPatch(ops, before) {
410
+ const inverse = [];
411
+ let state = deepClone(before);
412
+ for (const op of ops) {
413
+ const tokens = "path" in op ? parsePointer(op.path) : [];
414
+ switch (op.op) {
415
+ case "add": {
416
+ const parent = getAtPointer(state, tokens.slice(0, -1));
417
+ if (Array.isArray(parent)) {
418
+ inverse.unshift({ op: "remove", path: op.path });
419
+ } else {
420
+ const existed = pointerExists(state, tokens);
421
+ inverse.unshift(
422
+ existed ? { op: "replace", path: op.path, value: deepClone(getAtPointer(state, tokens)) } : { op: "remove", path: op.path }
423
+ );
424
+ }
425
+ break;
426
+ }
427
+ case "remove": {
428
+ inverse.unshift({ op: "add", path: op.path, value: deepClone(getAtPointer(state, tokens)) });
429
+ break;
430
+ }
431
+ case "replace": {
432
+ inverse.unshift({ op: "replace", path: op.path, value: deepClone(getAtPointer(state, tokens)) });
433
+ break;
434
+ }
435
+ case "move": {
436
+ inverse.unshift({ op: "move", from: op.path, path: op.from });
437
+ break;
438
+ }
439
+ case "copy": {
440
+ const existed = pointerExists(state, tokens);
441
+ inverse.unshift(
442
+ existed ? { op: "replace", path: op.path, value: deepClone(getAtPointer(state, tokens)) } : { op: "remove", path: op.path }
443
+ );
444
+ break;
445
+ }
446
+ case "test": {
447
+ inverse.unshift(op);
448
+ break;
449
+ }
450
+ }
451
+ state = applyJsonPatch(state, [op]);
452
+ }
453
+ return inverse;
454
+ }
455
+
456
+ // src/schema.ts
457
+ var import_zod = require("zod");
458
+ var import_core = require("@zodal/core");
459
+ function getObjectShape(schema) {
460
+ const anySchema = schema;
461
+ return anySchema._zod?.def?.shape ?? anySchema.shape ?? {};
462
+ }
463
+ function readMeta(schema) {
464
+ const tryMeta = (s) => {
465
+ const m = s.meta?.();
466
+ return m && typeof m === "object" ? m : void 0;
467
+ };
468
+ return tryMeta(schema) ?? tryMeta((0, import_core.unwrapZodSchema)(schema)) ?? {};
469
+ }
470
+ function baseType(field) {
471
+ const s = (0, import_core.unwrapZodSchema)(field);
472
+ if (s instanceof import_zod.z.ZodObject) return "object";
473
+ if (s instanceof import_zod.z.ZodRecord) return "object";
474
+ if (s instanceof import_zod.z.ZodArray) return "array";
475
+ if (s instanceof import_zod.z.ZodNumber) return "number";
476
+ if (s instanceof import_zod.z.ZodBoolean) return "boolean";
477
+ if (s instanceof import_zod.z.ZodString) return "string";
478
+ if (s instanceof import_zod.z.ZodEnum) return "enum";
479
+ return "unknown";
480
+ }
481
+ function extractDefaults(schema) {
482
+ const defaults = {};
483
+ for (const [key, field] of Object.entries(getObjectShape(schema))) {
484
+ const r = field.safeParse(void 0);
485
+ if (r.success && r.data !== void 0) defaults[key] = r.data;
486
+ }
487
+ return defaults;
488
+ }
489
+ function keyMergeStrategy(field) {
490
+ const override = readMeta(field).mergeStrategy;
491
+ if (override === "replace" || override === "deep-merge" || override === "append") return override;
492
+ return baseType(field) === "object" ? "deep-merge" : "replace";
493
+ }
494
+ function normalizeKey(key) {
495
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/[._\-/]+/g, " ").toLowerCase();
496
+ }
497
+ var SECRET_NAME = /(?:^| )(secret|secrets|secret ?id|passwords?|passwd|credentials?|tokens?|api ?keys?|access ?keys?|private ?keys?|client ?secret|refresh ?token|access ?token|auth ?token)(?: |$)/;
498
+ function secretByName(key) {
499
+ return SECRET_NAME.test(normalizeKey(key));
500
+ }
501
+ var MAX_SECRET_DEPTH = 8;
502
+ function childSchemas(inner) {
503
+ const a = inner;
504
+ const def = a._zod?.def ?? {};
505
+ const kids = [];
506
+ const push = (c) => {
507
+ if (c && typeof c === "object" && "_zod" in c) kids.push(c);
508
+ };
509
+ push(a.element);
510
+ push(def.element);
511
+ push(a.valueType);
512
+ push(def.valueType);
513
+ for (const arr of [a.items, def.items, a.options, def.options]) {
514
+ if (Array.isArray(arr)) for (const c of arr) push(c);
515
+ }
516
+ return kids;
517
+ }
518
+ function schemaContainsSecret(field, depth) {
519
+ if (depth > MAX_SECRET_DEPTH) return false;
520
+ const meta = readMeta(field);
521
+ if (meta.secret === true || meta.sensitivity === "secret") return true;
522
+ const inner = (0, import_core.unwrapZodSchema)(field);
523
+ if (inner instanceof import_zod.z.ZodObject) {
524
+ for (const [subKey, subField] of Object.entries(getObjectShape(inner))) {
525
+ if (secretByName(subKey)) return true;
526
+ if (schemaContainsSecret(subField, depth + 1)) return true;
527
+ }
528
+ return false;
529
+ }
530
+ for (const kid of childSchemas(inner)) {
531
+ if (schemaContainsSecret(kid, depth + 1)) return true;
532
+ }
533
+ return false;
534
+ }
535
+ function classifySensitivity(key, field) {
536
+ if (field) {
537
+ const meta = readMeta(field);
538
+ if (meta.secret === true || meta.sensitivity === "secret") return "secret";
539
+ if (meta.sensitivity === "sensitive") return "sensitive";
540
+ if (meta.sensitivity === "public") return "public";
541
+ }
542
+ if (secretByName(key)) return "secret";
543
+ if (field && schemaContainsSecret(field, 0)) return "secret";
544
+ return "public";
545
+ }
546
+
547
+ // src/secrets.ts
548
+ var SECRET_MASK = "\u2022\u2022\u2022\u2022";
549
+ function makeSecretRef(key, isSet) {
550
+ return { _tag: "SecretRef", key, isSet, masked: isSet ? "\u2022\u2022\u2022\u2022 (set)" : "not set" };
551
+ }
552
+ function isMeaningfullySet(v) {
553
+ return !isUnset(v) && v !== void 0 && v !== null && v !== "";
554
+ }
555
+ function maskSecrets(effective, sensitivityFor) {
556
+ const out = {};
557
+ for (const [k, v] of Object.entries(effective)) {
558
+ out[k] = sensitivityFor(k) === "secret" ? makeSecretRef(k, isMeaningfullySet(v)) : v;
559
+ }
560
+ return out;
561
+ }
562
+ function maskEffectiveResult(result, sensitivityFor) {
563
+ const isSecret = (key) => sensitivityFor(key) === "secret";
564
+ const maskShadowValue = (v) => v === "UNSET" ? "UNSET" : SECRET_MASK;
565
+ const effective = {};
566
+ for (const [k, v] of Object.entries(result.effective)) {
567
+ effective[k] = isSecret(k) ? makeSecretRef(k, isMeaningfullySet(v)) : v;
568
+ }
569
+ const provenance = {};
570
+ for (const [k, p] of Object.entries(result.provenance)) {
571
+ provenance[k] = isSecret(k) ? {
572
+ ...p,
573
+ value: makeSecretRef(k, isMeaningfullySet(p.value)),
574
+ shadowed: p.shadowed.map((s) => ({ ...s, value: maskShadowValue(s.value) }))
575
+ } : p;
576
+ }
577
+ const conflicts = result.conflicts.map(
578
+ (c) => isSecret(c.key) ? { ...c, contributors: c.contributors.map((ct) => ({ ...ct, value: maskShadowValue(ct.value) })) } : c
579
+ );
580
+ return { effective, provenance, conflicts };
581
+ }
582
+ function splitBySensitivity(layer, sensitivityFor) {
583
+ const config = {};
584
+ const secrets = {};
585
+ for (const [k, v] of Object.entries(layer)) {
586
+ if (sensitivityFor(k) === "secret") secrets[k] = v;
587
+ else config[k] = v;
588
+ }
589
+ return { config, secrets };
590
+ }
591
+ function redactSecretsFromLayer(serialized, sensitivityFor) {
592
+ const values = {};
593
+ for (const [k, v] of Object.entries(serialized.values ?? {})) {
594
+ if (sensitivityFor(k) !== "secret") values[k] = v;
595
+ }
596
+ const unset = (serialized.unset ?? []).filter((k) => sensitivityFor(k) !== "secret");
597
+ return { values, unset };
598
+ }
599
+
600
+ // src/constraints.ts
601
+ function evaluateConstraints(values, config = {}) {
602
+ const errors = [];
603
+ const warnings = [];
604
+ if (config.schema) {
605
+ const r = config.schema.safeParse(values);
606
+ if (!r.success) {
607
+ for (const issue of r.error.issues) {
608
+ errors.push({ message: issue.message, keys: issue.path.map((p) => String(p)) });
609
+ }
610
+ }
611
+ }
612
+ for (const a of config.assertions ?? []) {
613
+ let satisfied = false;
614
+ try {
615
+ satisfied = a.check(values);
616
+ } catch {
617
+ satisfied = false;
618
+ }
619
+ if (!satisfied) errors.push({ message: a.message, keys: a.keys ?? [] });
620
+ }
621
+ for (const w of config.warnings ?? []) {
622
+ let hit = false;
623
+ try {
624
+ hit = w.when(values);
625
+ } catch {
626
+ hit = false;
627
+ }
628
+ if (hit) warnings.push(w.message);
629
+ }
630
+ return { ok: errors.length === 0, errors, warnings };
631
+ }
632
+
633
+ // src/derive.ts
634
+ function applyDependentDefaults(values, defaults, options = {}) {
635
+ const dirty = new Set(options.dirtyKeys ?? []);
636
+ const out = { ...values };
637
+ const applied = [];
638
+ for (const d of defaults) {
639
+ if (dirty.has(d.key)) continue;
640
+ let suggestion;
641
+ try {
642
+ suggestion = d.derive(out);
643
+ } catch {
644
+ continue;
645
+ }
646
+ if (suggestion !== void 0) {
647
+ out[d.key] = suggestion;
648
+ applied.push(d.key);
649
+ }
650
+ }
651
+ return { values: out, applied };
652
+ }
653
+
654
+ // src/define-dials.ts
655
+ var import_core2 = require("@zodal/core");
656
+ function defineDials(schema, config = {}) {
657
+ const shape = getObjectShape(schema);
658
+ const keys = Object.keys(shape);
659
+ const defaults = extractDefaults(schema);
660
+ let collection;
661
+ try {
662
+ collection = (0, import_core2.defineCollection)(schema, config.collection);
663
+ } catch {
664
+ collection = void 0;
665
+ }
666
+ const mergeCache = /* @__PURE__ */ new Map();
667
+ const sensCache = /* @__PURE__ */ new Map();
668
+ const mergeStrategyFor = (key) => {
669
+ if (mergeCache.has(key)) return mergeCache.get(key);
670
+ const value = shape[key] ? keyMergeStrategy(shape[key]) : "replace";
671
+ mergeCache.set(key, value);
672
+ return value;
673
+ };
674
+ const sensitivityFor = (key) => {
675
+ if (sensCache.has(key)) return sensCache.get(key);
676
+ const value = classifySensitivity(key, shape[key]);
677
+ sensCache.set(key, value);
678
+ return value;
679
+ };
680
+ const buildStack = (stack, options) => options.includeDefaults === false ? stack : [{ scope: "default", layer: defaults }, ...stack];
681
+ const doResolve = (stack, options = {}) => {
682
+ const resolveOptions = { strategyFor: mergeStrategyFor };
683
+ const result = resolve(buildStack(stack, options), resolveOptions);
684
+ return options.maskSecrets ? maskEffectiveResult(result, sensitivityFor) : result;
685
+ };
686
+ return {
687
+ schema,
688
+ collection,
689
+ defaults,
690
+ keys,
691
+ mergeStrategyFor,
692
+ sensitivityFor,
693
+ resolve: doResolve,
694
+ explain: (key, stack, options) => doResolve(stack, options).provenance[key],
695
+ validate: (values) => evaluateConstraints(values, config.constraints),
696
+ withDependentDefaults: (values, dirtyKeys) => applyDependentDefaults(values, config.dependentDefaults ?? [], { dirtyKeys }).values,
697
+ getCapabilities: () => {
698
+ const mergeStrategies = {};
699
+ for (const k of keys) mergeStrategies[k] = mergeStrategyFor(k);
700
+ return {
701
+ keyCount: keys.length,
702
+ hasSecrets: keys.some((k) => sensitivityFor(k) === "secret"),
703
+ hasConstraints: Boolean(config.constraints?.schema || config.constraints?.assertions?.length),
704
+ hasDependentDefaults: (config.dependentDefaults?.length ?? 0) > 0,
705
+ mergeStrategies
706
+ };
707
+ }
708
+ };
709
+ }
710
+ // Annotate the CommonJS export names for ESM import in node:
711
+ 0 && (module.exports = {
712
+ UNSET,
713
+ applyDependentDefaults,
714
+ applyJsonPatch,
715
+ applyMergePatch,
716
+ baseType,
717
+ classifySensitivity,
718
+ defineDials,
719
+ deserializeLayer,
720
+ diffJsonPatch,
721
+ evaluateConstraints,
722
+ extractDefaults,
723
+ getObjectShape,
724
+ invertJsonPatch,
725
+ isSecretRef,
726
+ isUnset,
727
+ keyMergeStrategy,
728
+ layerToMergePatch,
729
+ makeSecretRef,
730
+ maskEffectiveResult,
731
+ maskSecrets,
732
+ mergeValues,
733
+ readMeta,
734
+ redactSecretsFromLayer,
735
+ resolve,
736
+ serializeLayer,
737
+ splitBySensitivity
738
+ });
739
+ //# sourceMappingURL=index.cjs.map