@contrast/assess 1.78.0 → 1.79.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.
@@ -0,0 +1,149 @@
1
+ /*
2
+ * Copyright: 2026 Contrast Security, Inc
3
+ * Contact: support@contrastsecurity.com
4
+ * License: Commercial
5
+
6
+ * NOTICE: This Software and the patented inventions embodied within may only be
7
+ * used as part of Contrast Security’s commercial offerings. Even though it is
8
+ * made available through public repositories, use of this Software is subject to
9
+ * the applicable End User Licensing Agreement found at
10
+ * https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
11
+ * between Contrast Security and the End User. The Software may not be reverse
12
+ * engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
+ * way not consistent with the End User License Agreement.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const {
19
+ DataflowTag: { STRING_TYPE_CHECKED, ALPHANUM_SPACE_HYPHEN, LIMITED_CHARS, CUSTOM_VALIDATED },
20
+ } = require('@contrast/common');
21
+
22
+ // check-kind names that zod v3 (a flat `check.kind` string) and zod v4 (a
23
+ // `check._zod.def.format` string, when `check._zod.def.check ===
24
+ // 'string_format'`) happen to spell identically, with the same tag - shared
25
+ // so neither version's table has to repeat these entries. Anything either
26
+ // version names or tags differently lives in that version's own table below
27
+ // instead of being forced into this shared shape.
28
+ const COMMON_CHECK_MATRIX = {
29
+ includes: STRING_TYPE_CHECKED,
30
+ url: STRING_TYPE_CHECKED,
31
+ emoji: STRING_TYPE_CHECKED,
32
+
33
+ uuid: ALPHANUM_SPACE_HYPHEN,
34
+ cuid: ALPHANUM_SPACE_HYPHEN,
35
+ cuid2: ALPHANUM_SPACE_HYPHEN,
36
+ ulid: ALPHANUM_SPACE_HYPHEN,
37
+ nanoid: ALPHANUM_SPACE_HYPHEN,
38
+ jwt: ALPHANUM_SPACE_HYPHEN,
39
+ base64: ALPHANUM_SPACE_HYPHEN,
40
+ base64url: ALPHANUM_SPACE_HYPHEN,
41
+
42
+ email: LIMITED_CHARS,
43
+ date: LIMITED_CHARS,
44
+ datetime: LIMITED_CHARS,
45
+ time: LIMITED_CHARS,
46
+ duration: LIMITED_CHARS,
47
+
48
+ regex: CUSTOM_VALIDATED,
49
+ };
50
+
51
+ const v3 = {
52
+ // zod v3's checks are a flat array on ZodString's own def (`_def.checks`),
53
+ // each a plain `{kind: '...', ...}` object - no nested format/check split
54
+ // like v4's `_zod.def.check`/`format`, so one flat table keyed by
55
+ // `check.kind` covers every string check. `nonempty` is deliberately
56
+ // absent - `ZodString.nonempty()` compiles to `.min(1)`, there's no
57
+ // distinct 'nonempty' check kind to key off of.
58
+ CHECK_KIND_MATRIX: {
59
+ ...COMMON_CHECK_MATRIX,
60
+ min: STRING_TYPE_CHECKED,
61
+ max: STRING_TYPE_CHECKED,
62
+ length: STRING_TYPE_CHECKED,
63
+ startsWith: STRING_TYPE_CHECKED,
64
+ endsWith: STRING_TYPE_CHECKED,
65
+
66
+ ip: LIMITED_CHARS,
67
+ cidr: LIMITED_CHARS,
68
+ },
69
+
70
+ // check.kind values that mutate the string in place (produce a NEW string
71
+ // identity inside zod) rather than just validating it - handled by the
72
+ // retrack path in process-result-v3.js, not tagged via the matrix above.
73
+ // v3 has no validating `lowercase`/`uppercase` checks like v4 - only these
74
+ // mutating ones.
75
+ OVERWRITE_CHECK_KINDS: new Set(['trim', 'toLowerCase', 'toUpperCase']),
76
+
77
+ // schema._def.typeName values whose successful validation should untrack
78
+ // the value entirely, rather than tag it - it's now constrained to a fixed
79
+ // allow-list of values. Unlike v4 (which collapses nativeEnum into 'enum'),
80
+ // v3 keeps ZodNativeEnum as a distinct typeName.
81
+ UNTRACKING_SCHEMA_TYPES: new Set(['ZodEnum', 'ZodNativeEnum', 'ZodLiteral']),
82
+ };
83
+
84
+ const v4 = {
85
+ // `.email()`, `.uuid()`, `.url()`, `.ipv4()`, etc. all share the same underlying
86
+ // check class ($ZodCheckStringFormat) and are only discriminated by
87
+ // `check._zod.def.format`, not by `check._zod.def.check` (which is the fixed
88
+ // string 'string_format' for all of them). `.includes()`/`.startsWith()`/
89
+ // `.endsWith()`/`.regex()`/`.lowercase()`/`.uppercase()` are format checks too.
90
+ //
91
+ // keyed by check._zod.def.format, when check._zod.def.check === 'string_format'
92
+ FORMAT_MATRIX: {
93
+ ...COMMON_CHECK_MATRIX,
94
+ ends_with: STRING_TYPE_CHECKED,
95
+ lowercase: STRING_TYPE_CHECKED,
96
+ starts_with: STRING_TYPE_CHECKED,
97
+ uppercase: STRING_TYPE_CHECKED,
98
+
99
+ guid: ALPHANUM_SPACE_HYPHEN,
100
+ hex: ALPHANUM_SPACE_HYPHEN,
101
+ ksuid: ALPHANUM_SPACE_HYPHEN,
102
+ xid: ALPHANUM_SPACE_HYPHEN,
103
+
104
+ // hash("<algorithm>")
105
+ md5_hex: ALPHANUM_SPACE_HYPHEN,
106
+ md5_base64: ALPHANUM_SPACE_HYPHEN,
107
+ md5_base64url: ALPHANUM_SPACE_HYPHEN,
108
+ sha1_hex: ALPHANUM_SPACE_HYPHEN,
109
+ sha1_base64: ALPHANUM_SPACE_HYPHEN,
110
+ sha1_base64url: ALPHANUM_SPACE_HYPHEN,
111
+ sha256_hex: ALPHANUM_SPACE_HYPHEN,
112
+ sha256_base64: ALPHANUM_SPACE_HYPHEN,
113
+ sha256_base64url: ALPHANUM_SPACE_HYPHEN,
114
+ sha384_hex: ALPHANUM_SPACE_HYPHEN,
115
+ sha384_base64: ALPHANUM_SPACE_HYPHEN,
116
+ sha384_base64url: ALPHANUM_SPACE_HYPHEN,
117
+ sha512_hex: ALPHANUM_SPACE_HYPHEN,
118
+ sha512_base64: ALPHANUM_SPACE_HYPHEN,
119
+ sha512_base64url: ALPHANUM_SPACE_HYPHEN,
120
+
121
+ cidrv4: LIMITED_CHARS,
122
+ cidrv6: LIMITED_CHARS,
123
+ e164: LIMITED_CHARS,
124
+ ipv4: LIMITED_CHARS,
125
+ ipv6: LIMITED_CHARS,
126
+ mac: LIMITED_CHARS,
127
+ },
128
+
129
+ // keyed by check._zod.def.check, when check !== 'string_format'. Plain
130
+ // .min()/.max()/.length() get their own distinct check identifiers.
131
+ // 'overwrite' (.trim()/.toLowerCase()/.toUpperCase()/.normalize()/.slugify())
132
+ // and 'custom' (refine/superRefine/check(fn)) are deliberately absent here -
133
+ // 'overwrite' is handled by the retrack path and 'custom' by the
134
+ // trust_custom_validators-gated path in process-result-v4.js, neither is
135
+ // tagged via this ungated matrix.
136
+ CHECK_TYPE_MATRIX: {
137
+ min_length: STRING_TYPE_CHECKED,
138
+ max_length: STRING_TYPE_CHECKED,
139
+ length_equals: STRING_TYPE_CHECKED,
140
+ },
141
+
142
+ // schema._zod.def.type values whose successful validation should untrack the
143
+ // value entirely, rather than tag it - it's now constrained to a fixed
144
+ // allow-list of values. zod v4 has no separate 'nativeEnum' schema type;
145
+ // z.nativeEnum() compiles to the same 'enum' type.
146
+ UNTRACKING_SCHEMA_TYPES: new Set(['enum', 'literal']),
147
+ };
148
+
149
+ module.exports = { v3, v4 };
@@ -0,0 +1,228 @@
1
+ /*
2
+ * Copyright: 2026 Contrast Security, Inc
3
+ * Contact: support@contrastsecurity.com
4
+ * License: Commercial
5
+
6
+ * NOTICE: This Software and the patented inventions embodied within may only be
7
+ * used as part of Contrast Security’s commercial offerings. Even though it is
8
+ * made available through public repositories, use of this Software is subject to
9
+ * the applicable End User Licensing Agreement found at
10
+ * https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
11
+ * between Contrast Security and the End User. The Software may not be reverse
12
+ * engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
+ * way not consistent with the End User License Agreement.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const {
19
+ DataflowTag: { STRING_TYPE_CHECKED },
20
+ isNonEmptyObject,
21
+ } = require('@contrast/common');
22
+ const { CHECK_KIND_MATRIX, OVERWRITE_CHECK_KINDS, UNTRACKING_SCHEMA_TYPES } = require('./matrix').v3;
23
+
24
+ // schema._def.typeName values that pass the value through to `_def.innerType`
25
+ // completely unchanged - safe to unwrap transparently when looking for the
26
+ // concrete leaf schema that actually carries checks. ZodBranded is NOT here -
27
+ // it uses `_def.type` instead of `_def.innerType` for its wrapped schema.
28
+ const INNER_TYPE_WRAPPERS = new Set(['ZodOptional', 'ZodNullable', 'ZodDefault', 'ZodCatch', 'ZodReadonly']);
29
+ const TYPE_KEY_WRAPPERS = new Set(['ZodBranded']);
30
+ // backstop against a self-referential `z.lazy(() => schema)` spinning forever
31
+ const MAX_UNWRAP_DEPTH = 100;
32
+
33
+ module.exports = function(core) {
34
+ const { logger } = core;
35
+ const {
36
+ applyTags,
37
+ untrackValue,
38
+ retrackOverwrite,
39
+ tagCustomValidated,
40
+ handleTransformLike,
41
+ hasAnyTrackedValue,
42
+ getPropagatorContext,
43
+ } = require('./utils')(core);
44
+
45
+ /**
46
+ * Unwraps wrapper schema types (optional/nullable/default/branded/lazy/...)
47
+ * to find the schema that actually carries checks. `.catch()` is included
48
+ * here: on the SUCCESS path (the only path this instrumentation ever sees,
49
+ * since the parse-family hooks only fire on success), a `.catch()`
50
+ * schema's result is either the inner schema's own validated value, or a
51
+ * fallback substituted by user code - in the fallback case
52
+ * `tracker.getData(result)` naturally finds nothing tracked (a fresh
53
+ * literal), so no special-casing is needed.
54
+ */
55
+ function resolveConcreteSchema(schema) {
56
+ let current = schema;
57
+ for (let depth = 0; depth < MAX_UNWRAP_DEPTH && current?._def; depth++) {
58
+ const { typeName } = current._def;
59
+ if (INNER_TYPE_WRAPPERS.has(typeName)) {
60
+ current = current._def.innerType;
61
+ continue;
62
+ }
63
+ if (TYPE_KEY_WRAPPERS.has(typeName)) {
64
+ current = current._def.type;
65
+ continue;
66
+ }
67
+ if (typeName === 'ZodLazy') {
68
+ current = current._def.getter();
69
+ continue;
70
+ }
71
+ return current;
72
+ }
73
+ return current;
74
+ }
75
+
76
+ /**
77
+ * Collects every DataflowTag that applies to a concrete (already-unwrapped)
78
+ * schema node. String-family schemas always get a baseline
79
+ * STRING_TYPE_CHECKED (mirroring joi's string.validate() tagging every
80
+ * successfully validated string, regardless of additional rules), on top
81
+ * of whichever more specific tags its chained checks add. Unlike v4, v3
82
+ * has no per-node format schema (no top-level `z.email()`), so every check
83
+ * is an entry in `def.checks`, never the node's own def.
84
+ */
85
+ function checksToTags(def) {
86
+ const tags = new Set();
87
+ if (def.typeName === 'ZodString') tags.add(STRING_TYPE_CHECKED);
88
+
89
+ for (const check of def.checks ?? []) {
90
+ const tag = CHECK_KIND_MATRIX[check.kind];
91
+ if (tag) tags.add(tag);
92
+ }
93
+
94
+ return tags;
95
+ }
96
+
97
+ function hasOverwriteCheck(def) {
98
+ return (def.checks ?? []).some((check) => OVERWRITE_CHECK_KINDS.has(check.kind));
99
+ }
100
+
101
+ /**
102
+ * Writes a walked value back into its container, guarding against
103
+ * `.readonly()` schemas: zod freezes the object/array on the success path
104
+ * (`Object.freeze`), so writing back into it throws in strict mode. Skip
105
+ * the write when nothing changed (the common case) or when the container
106
+ * is frozen - in the frozen case, retracking (e.g. a nested `.trim()`)
107
+ * can't be reflected in the output, which is an accepted degradation.
108
+ */
109
+ function assignWalked(container, key, walked) {
110
+ if (container[key] === walked) return;
111
+ if (Object.isFrozen(container)) return;
112
+ container[key] = walked;
113
+ }
114
+
115
+ /**
116
+ * Handles a ZodEffects node - the schema shape `.refine()`, `.superRefine()`,
117
+ * `.transform()`, and `.preprocess()` all compile to.
118
+ *
119
+ * Unlike v4 (where `.refine()`/`.superRefine()` append a 'custom' check
120
+ * onto the SAME schema node), v3's `.refine()`/`.superRefine()` return a
121
+ * NEW ZodEffects WRAPPING the original schema (`def.schema`). A naive port
122
+ * that only gated CUSTOM_VALIDATED on this node would silently drop
123
+ * STRING_TYPE_CHECKED/format tags and any retracking for every
124
+ * `.refine()`/`.superRefine()` call - so the inner schema is walked FIRST,
125
+ * and CUSTOM_VALIDATED is layered on top of whatever that walk produced.
126
+ * The refinement function's return value is ignored by zod itself, so
127
+ * `result` here is exactly whatever the inner schema's own parse produced.
128
+ *
129
+ * `.preprocess()` is out of v1 scope: the pre-preprocess value is a
130
+ * different, unobservable input than what we receive here.
131
+ */
132
+ function handleEffects(concrete, def, input, result, path) {
133
+ if (def.effect.type === 'refinement') {
134
+ const walked = walk(def.schema, input, result, path);
135
+ return tagCustomValidated({
136
+ sourceValue: walked,
137
+ targetValue: walked,
138
+ schema: concrete,
139
+ methodName: 'refine',
140
+ retrack: false,
141
+ });
142
+ }
143
+
144
+ if (def.effect.type === 'transform') {
145
+ return handleTransformLike(def.schema, input, result);
146
+ }
147
+
148
+ return result;
149
+ }
150
+
151
+ /**
152
+ * Recursively walks a schema alongside its pre-parse input and post-parse
153
+ * result, tagging/untracking/retracking tracked strings it finds. Returns
154
+ * the (possibly retracked) result value - the caller must use this return
155
+ * value, since retracking (trim/toLowerCase/transform) produces a NEW
156
+ * string object that the original caller of `.parse()` needs to actually
157
+ * receive in order for the tag to travel with it.
158
+ */
159
+ function walk(schema, input, result, path) {
160
+ const concrete = resolveConcreteSchema(schema);
161
+ if (!concrete?._def) return result;
162
+ const def = concrete._def;
163
+
164
+ if (UNTRACKING_SCHEMA_TYPES.has(def.typeName)) {
165
+ untrackValue(result);
166
+ return result;
167
+ }
168
+
169
+ if (def.typeName === 'ZodEffects') {
170
+ return handleEffects(concrete, def, input, result, path);
171
+ }
172
+
173
+ if (def.typeName === 'ZodPipeline') {
174
+ return handleTransformLike(def.in, input, result);
175
+ }
176
+
177
+ if (typeof result === 'string') {
178
+ let taggedResult = result;
179
+ if (hasOverwriteCheck(def)) {
180
+ taggedResult = retrackOverwrite(input, result, concrete);
181
+ }
182
+ applyTags(taggedResult, checksToTags(def), concrete, 'string');
183
+ return taggedResult;
184
+ }
185
+
186
+ switch (def.typeName) {
187
+ case 'ZodObject':
188
+ if (isNonEmptyObject(input) && isNonEmptyObject(result)) {
189
+ for (const key of Object.keys(concrete.shape)) {
190
+ assignWalked(result, key, walk(concrete.shape[key], input[key], result[key], [...path, key]));
191
+ }
192
+ }
193
+ return result;
194
+ case 'ZodArray':
195
+ if (Array.isArray(result)) {
196
+ result.forEach((value, i) => {
197
+ assignWalked(result, i, walk(concrete.element, input?.[i], value, [...path, i]));
198
+ });
199
+ }
200
+ return result;
201
+ default:
202
+ // unions/discriminatedUnion/intersection/tuples/records/maps/sets/
203
+ // promises/functions are deliberately out of v1 scope, mirroring v4.
204
+ return result;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Entry point called from the safeParse-family post-hook. Bails out
210
+ * cheaply unless something in `input` or `result` is actually tracked,
211
+ * mirroring joi's early-return checks. Returns the (possibly retracked)
212
+ * result - callers must use the return value as the definitive result
213
+ * going forward.
214
+ */
215
+ function handleParseSuccess(schema, input, result) {
216
+ try {
217
+ if (!getPropagatorContext()) return result;
218
+ if (!hasAnyTrackedValue(input) && !hasAnyTrackedValue(result)) return result;
219
+
220
+ return walk(schema, input, result, []);
221
+ } catch (err) {
222
+ logger.error({ err }, 'zod v3 parse propagation walk failed');
223
+ return result;
224
+ }
225
+ }
226
+
227
+ return { handleParseSuccess };
228
+ };
@@ -0,0 +1,191 @@
1
+ /*
2
+ * Copyright: 2026 Contrast Security, Inc
3
+ * Contact: support@contrastsecurity.com
4
+ * License: Commercial
5
+
6
+ * NOTICE: This Software and the patented inventions embodied within may only be
7
+ * used as part of Contrast Security’s commercial offerings. Even though it is
8
+ * made available through public repositories, use of this Software is subject to
9
+ * the applicable End User Licensing Agreement found at
10
+ * https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
11
+ * between Contrast Security and the End User. The Software may not be reverse
12
+ * engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
+ * way not consistent with the End User License Agreement.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const {
19
+ DataflowTag: { STRING_TYPE_CHECKED },
20
+ isNonEmptyObject,
21
+ } = require('@contrast/common');
22
+ const { CHECK_TYPE_MATRIX, FORMAT_MATRIX, UNTRACKING_SCHEMA_TYPES } = require('./matrix').v4;
23
+
24
+ // schema._zod.def.type values that pass the value through to `innerType`
25
+ // completely unchanged - safe to unwrap transparently when looking for the
26
+ // concrete leaf schema that actually carries checks.
27
+ const TRANSPARENT_WRAPPER_TYPES = new Set([
28
+ 'optional', 'exactOptional', 'nullable', 'default', 'prefault', 'nonoptional', 'success', 'readonly', 'catch',
29
+ ]);
30
+
31
+ module.exports = function(core) {
32
+ const { logger } = core;
33
+ const {
34
+ applyTags,
35
+ untrackValue,
36
+ retrackOverwrite,
37
+ tagCustomValidated,
38
+ handleTransformLike,
39
+ hasAnyTrackedValue,
40
+ getPropagatorContext,
41
+ } = require('./utils')(core);
42
+
43
+ /**
44
+ * Unwraps wrapper schema types (optional/nullable/default/.../lazy) to find
45
+ * the schema that actually carries checks. `.catch()` is included here: on
46
+ * the SUCCESS path (the only path this instrumentation ever sees, since the
47
+ * parse-family hooks only fire on success), a `.catch()` schema's result is
48
+ * either the inner schema's own validated value, or a fallback substituted
49
+ * by user code - in the fallback case `tracker.getData(result)` naturally
50
+ * finds nothing tracked (a fresh literal), so no special-casing is needed.
51
+ */
52
+ function resolveConcreteSchema(schema) {
53
+ let current = schema;
54
+ while (current?._zod?.def) {
55
+ const { type, innerType } = current._zod.def;
56
+ if (TRANSPARENT_WRAPPER_TYPES.has(type)) {
57
+ current = innerType;
58
+ continue;
59
+ }
60
+ if (type === 'lazy') {
61
+ current = current._zod.innerType;
62
+ continue;
63
+ }
64
+ return current;
65
+ }
66
+ return current;
67
+ }
68
+
69
+ /**
70
+ * Resolves the DataflowTag for a single check-shaped def (either a schema's
71
+ * own def, when the schema itself is a format/check node e.g. z.email(), or
72
+ * an entry in some other schema's def.checks array, e.g. z.string().email()
73
+ * - both shapes carry the same {check, format} discriminator). Deliberately
74
+ * excludes 'overwrite' and 'custom' checks - both matrices omit those keys,
75
+ * since they're handled by dedicated, gated code paths below rather than
76
+ * this blanket lookup.
77
+ */
78
+ function tagForCheckDef(def) {
79
+ if (!def?.check) return null;
80
+ return def.check === 'string_format' ? FORMAT_MATRIX[def.format] : CHECK_TYPE_MATRIX[def.check];
81
+ }
82
+
83
+ /**
84
+ * Collects every DataflowTag that applies to a concrete (already-unwrapped)
85
+ * schema node. String-family schemas always get a baseline
86
+ * STRING_TYPE_CHECKED (mirroring joi's string.validate() tagging every
87
+ * successfully validated string, regardless of additional rules), on top
88
+ * of whichever more specific tags its own def or chained checks add.
89
+ */
90
+ function checksToTags(def) {
91
+ const tags = new Set();
92
+ if (def.type === 'string') tags.add(STRING_TYPE_CHECKED);
93
+
94
+ const ownTag = tagForCheckDef(def);
95
+ if (ownTag) tags.add(ownTag);
96
+
97
+ for (const check of def.checks ?? []) {
98
+ const tag = tagForCheckDef(check._zod?.def);
99
+ if (tag) tags.add(tag);
100
+ }
101
+
102
+ return tags;
103
+ }
104
+
105
+ function hasCheck(def, checkName) {
106
+ return (def.checks ?? []).some((check) => check._zod?.def?.check === checkName);
107
+ }
108
+
109
+ /**
110
+ * Recursively walks a schema alongside its pre-parse input and post-parse
111
+ * result, tagging/untracking/retracking tracked strings it finds. Returns
112
+ * the (possibly retracked) result value - the caller must use this return
113
+ * value, since retracking (trim/toLowerCase/transform) produces a NEW
114
+ * string object that the original caller of `.parse()` needs to actually
115
+ * receive in order for the tag to travel with it.
116
+ */
117
+ function walk(schema, input, result, path) {
118
+ const concrete = resolveConcreteSchema(schema);
119
+ if (!concrete?._zod?.def) return result;
120
+ const { def } = concrete._zod;
121
+
122
+ if (UNTRACKING_SCHEMA_TYPES.has(def.type)) {
123
+ untrackValue(result);
124
+ return result;
125
+ }
126
+
127
+ if (def.type === 'pipe') {
128
+ return handleTransformLike(def.in, input, result);
129
+ }
130
+
131
+ if (typeof result === 'string') {
132
+ let taggedResult = result;
133
+ if (hasCheck(def, 'overwrite')) {
134
+ taggedResult = retrackOverwrite(input, result, concrete);
135
+ }
136
+ applyTags(taggedResult, checksToTags(def), concrete, 'string');
137
+ if (hasCheck(def, 'custom')) {
138
+ taggedResult = tagCustomValidated({
139
+ sourceValue: input,
140
+ targetValue: taggedResult,
141
+ schema: concrete,
142
+ methodName: 'refine',
143
+ retrack: false,
144
+ });
145
+ }
146
+ return taggedResult;
147
+ }
148
+
149
+ switch (def.type) {
150
+ case 'object':
151
+ if (isNonEmptyObject(input) && isNonEmptyObject(result)) {
152
+ for (const key of Object.keys(def.shape)) {
153
+ result[key] = walk(def.shape[key], input[key], result[key], [...path, key]);
154
+ }
155
+ }
156
+ return result;
157
+ case 'array':
158
+ if (Array.isArray(result)) {
159
+ result.forEach((value, i) => {
160
+ result[i] = walk(def.element, input?.[i], value, [...path, i]);
161
+ });
162
+ }
163
+ return result;
164
+ default:
165
+ // unions/discriminatedUnion/xor/records/maps/sets/tuples/intersections
166
+ // are deliberately out of v1 scope - see plan's Open Questions.
167
+ return result;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Entry point called from the parse-family post-hook. Bails out cheaply
173
+ * unless something in `input` or `result` is actually tracked, mirroring
174
+ * joi's early-return checks. Returns the (possibly retracked) result -
175
+ * callers must use the return value as the definitive result going
176
+ * forward.
177
+ */
178
+ function handleParseSuccess(schema, input, result) {
179
+ try {
180
+ if (!getPropagatorContext()) return result;
181
+ if (!hasAnyTrackedValue(input) && !hasAnyTrackedValue(result)) return result;
182
+
183
+ return walk(schema, input, result, []);
184
+ } catch (err) {
185
+ logger.error({ err }, 'zod parse propagation walk failed');
186
+ return result;
187
+ }
188
+ }
189
+
190
+ return { handleParseSuccess };
191
+ };