core-services-sdk 1.3.90 → 1.3.91
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/package.json
CHANGED
|
@@ -4,6 +4,10 @@ import { mask as defaultMask } from '../util/mask-sensitive.js'
|
|
|
4
4
|
/**
|
|
5
5
|
* Converts a single field definition into a Zod schema.
|
|
6
6
|
*
|
|
7
|
+
* NOTE: This function does NOT apply .default() — defaults are handled
|
|
8
|
+
* manually in validateEnv so that they work correctly even when other
|
|
9
|
+
* fields fail validation.
|
|
10
|
+
*
|
|
7
11
|
* @param {Object} def
|
|
8
12
|
* @param {'string'|'number'|'boolean'} def.type
|
|
9
13
|
*
|
|
@@ -130,10 +134,6 @@ export function defToZod(def) {
|
|
|
130
134
|
}
|
|
131
135
|
}
|
|
132
136
|
|
|
133
|
-
if (def.default !== undefined) {
|
|
134
|
-
schema = schema.default(def.default)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
137
|
if (!def.required) {
|
|
138
138
|
schema = schema.optional()
|
|
139
139
|
}
|
|
@@ -163,39 +163,56 @@ export function createZodSchema(definition) {
|
|
|
163
163
|
/**
|
|
164
164
|
* Validates values using a JSON definition and Zod.
|
|
165
165
|
*
|
|
166
|
+
* Defaults are applied manually (not via Zod .default()) so that:
|
|
167
|
+
* - defaults fill in when value is undefined
|
|
168
|
+
* - existing values are always validated (never replaced by defaults)
|
|
169
|
+
* - invalid values are kept in data (with errors reported)
|
|
170
|
+
* - data is ALWAYS returned, even on failure
|
|
171
|
+
*
|
|
166
172
|
* @param {Record<string, Object>} definition
|
|
167
173
|
* @param {Record<string, any>} values
|
|
168
174
|
*
|
|
169
175
|
* @returns {{
|
|
170
|
-
* success:
|
|
171
|
-
* data: Record<string, any
|
|
172
|
-
*
|
|
173
|
-
* success: false,
|
|
174
|
-
* summary: Record<string, string[]>
|
|
176
|
+
* success: boolean,
|
|
177
|
+
* data: Record<string, any>,
|
|
178
|
+
* summary?: Record<string, string[]>
|
|
175
179
|
* }}
|
|
176
180
|
*/
|
|
177
181
|
export function validateEnv(definition, values) {
|
|
178
|
-
const
|
|
179
|
-
const
|
|
182
|
+
const data = {}
|
|
183
|
+
const summary = {}
|
|
180
184
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
+
for (const [key, def] of Object.entries(definition)) {
|
|
186
|
+
const raw = values[key]
|
|
187
|
+
|
|
188
|
+
// Validate the field individually using its Zod schema
|
|
189
|
+
const fieldSchema = defToZod(def)
|
|
190
|
+
|
|
191
|
+
// Apply default when value is undefined
|
|
192
|
+
if (raw === undefined && def.default !== undefined) {
|
|
193
|
+
const defaultResult = fieldSchema.safeParse(def.default)
|
|
194
|
+
data[key] = defaultResult.success ? defaultResult.data : def.default
|
|
195
|
+
continue
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const result = fieldSchema.safeParse(raw)
|
|
199
|
+
|
|
200
|
+
if (result.success) {
|
|
201
|
+
data[key] = result.data
|
|
202
|
+
} else {
|
|
203
|
+
// Keep original value in data, record errors
|
|
204
|
+
data[key] = raw
|
|
205
|
+
summary[key] = result.error.issues.map((issue) => issue.message)
|
|
185
206
|
}
|
|
186
207
|
}
|
|
187
208
|
|
|
188
|
-
const
|
|
189
|
-
const key = issue.path[0] || 'root'
|
|
190
|
-
acc[key] = acc[key] || []
|
|
191
|
-
acc[key].push(issue.message)
|
|
192
|
-
return acc
|
|
193
|
-
}, {})
|
|
209
|
+
const success = Object.keys(summary).length === 0
|
|
194
210
|
|
|
195
|
-
|
|
196
|
-
success:
|
|
197
|
-
summary,
|
|
211
|
+
if (success) {
|
|
212
|
+
return { success: true, data }
|
|
198
213
|
}
|
|
214
|
+
|
|
215
|
+
return { success: false, data, summary }
|
|
199
216
|
}
|
|
200
217
|
|
|
201
218
|
/**
|
|
@@ -232,13 +249,11 @@ export function buildEnvReport(definition, values, validationResult, mask) {
|
|
|
232
249
|
const def = definition[key]
|
|
233
250
|
const isSecret = Boolean(def.secret)
|
|
234
251
|
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
const rawValue = values[key]
|
|
239
|
-
const value = validationResult.success
|
|
252
|
+
// Always prefer validated data (includes defaults and parsed values)
|
|
253
|
+
// Fall back to raw values only for backward compat when data is missing
|
|
254
|
+
const value = validationResult.data
|
|
240
255
|
? validationResult.data[key]
|
|
241
|
-
:
|
|
256
|
+
: values[key]
|
|
242
257
|
|
|
243
258
|
const displayValue = isSecret ? maskValue(value, '.', 3) : String(value)
|
|
244
259
|
|
|
@@ -330,7 +345,7 @@ export function formatEnvReport(report) {
|
|
|
330
345
|
* success: boolean,
|
|
331
346
|
* validation: {
|
|
332
347
|
* success: boolean,
|
|
333
|
-
* data
|
|
348
|
+
* data: Record<string, any>,
|
|
334
349
|
* summary?: Record<string, string[]>
|
|
335
350
|
* },
|
|
336
351
|
* report: {
|
|
@@ -379,7 +394,7 @@ export function validateAndReportEnv(definition, values, options = {}) {
|
|
|
379
394
|
* table: string,
|
|
380
395
|
* validation: {
|
|
381
396
|
* success: boolean,
|
|
382
|
-
* data
|
|
397
|
+
* data: Record<string, any>,
|
|
383
398
|
* summary?: Record<string, string[]>
|
|
384
399
|
* },
|
|
385
400
|
* report: {
|
|
@@ -392,7 +407,8 @@ export function validateAndReportEnv(definition, values, options = {}) {
|
|
|
392
407
|
* valid: boolean,
|
|
393
408
|
* errors?: string[]
|
|
394
409
|
* }>
|
|
395
|
-
* }
|
|
410
|
+
* },
|
|
411
|
+
* env: Record<string, any>
|
|
396
412
|
* }}
|
|
397
413
|
*/
|
|
398
414
|
export function prepareEnvValidation(definition, values, options = {}) {
|
|
@@ -407,6 +423,7 @@ export function prepareEnvValidation(definition, values, options = {}) {
|
|
|
407
423
|
table,
|
|
408
424
|
validation,
|
|
409
425
|
report,
|
|
426
|
+
env: validation.data,
|
|
410
427
|
}
|
|
411
428
|
}
|
|
412
429
|
|
|
@@ -18,18 +18,19 @@ describe('createZodSchema', () => {
|
|
|
18
18
|
it('parses valid values successfully', () => {
|
|
19
19
|
const definition = {
|
|
20
20
|
PORT: { type: 'number', required: true, min: 1 },
|
|
21
|
-
DEBUG: { type: 'boolean'
|
|
21
|
+
DEBUG: { type: 'boolean' },
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const schema = createZodSchema(definition)
|
|
25
25
|
|
|
26
26
|
const result = schema.parse({
|
|
27
27
|
PORT: '3000',
|
|
28
|
+
DEBUG: true,
|
|
28
29
|
})
|
|
29
30
|
|
|
30
31
|
expect(result).toEqual({
|
|
31
32
|
PORT: 3000,
|
|
32
|
-
DEBUG:
|
|
33
|
+
DEBUG: true,
|
|
33
34
|
})
|
|
34
35
|
})
|
|
35
36
|
|
|
@@ -68,6 +68,43 @@ describe('prepareEnvValidation', () => {
|
|
|
68
68
|
expect(result.validation.data.PORT).toBe(8080)
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
+
it('returns env with parsed values and defaults applied', () => {
|
|
72
|
+
const def = {
|
|
73
|
+
PORT: { type: 'number', default: 3000 },
|
|
74
|
+
DEBUG: { type: 'boolean', default: 'false' },
|
|
75
|
+
MODE: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
enum: ['development', 'production'],
|
|
78
|
+
required: true,
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const values = { MODE: 'production' }
|
|
83
|
+
|
|
84
|
+
const result = prepareEnvValidation(def, values)
|
|
85
|
+
|
|
86
|
+
expect(result.success).toBe(true)
|
|
87
|
+
expect(result.env).toBeDefined()
|
|
88
|
+
expect(result.env.PORT).toBe(3000)
|
|
89
|
+
expect(result.env.DEBUG).toBe(false)
|
|
90
|
+
expect(result.env.MODE).toBe('production')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('returns env with defaults and invalid originals on failure', () => {
|
|
94
|
+
const def = {
|
|
95
|
+
PORT: { type: 'number', default: 3000 },
|
|
96
|
+
HOST: { type: 'string', format: 'url', required: true },
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const values = { HOST: 'not-a-url' }
|
|
100
|
+
|
|
101
|
+
const result = prepareEnvValidation(def, values)
|
|
102
|
+
|
|
103
|
+
expect(result.success).toBe(false)
|
|
104
|
+
expect(result.env.PORT).toBe(3000)
|
|
105
|
+
expect(result.env.HOST).toBe('not-a-url')
|
|
106
|
+
})
|
|
107
|
+
|
|
71
108
|
it('returns a table string even on failure', () => {
|
|
72
109
|
const values = {
|
|
73
110
|
PORT: 0,
|
|
@@ -90,6 +90,121 @@ describe('validateEnv', () => {
|
|
|
90
90
|
expect(result.summary).toHaveProperty('NODE_ENV')
|
|
91
91
|
})
|
|
92
92
|
|
|
93
|
+
it('always returns data even when validation fails', () => {
|
|
94
|
+
const definition = {
|
|
95
|
+
PORT: { type: 'number', default: 3000 },
|
|
96
|
+
HOST: { type: 'string', format: 'url' },
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const values = {
|
|
100
|
+
HOST: 'invalid-url',
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const result = validateEnv(definition, values)
|
|
104
|
+
|
|
105
|
+
expect(result.success).toBe(false)
|
|
106
|
+
expect(result.data).toBeDefined()
|
|
107
|
+
expect(result.data.PORT).toBe(3000)
|
|
108
|
+
expect(result.data.HOST).toBe('invalid-url')
|
|
109
|
+
expect(result.summary).toHaveProperty('HOST')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('applies default when value is undefined', () => {
|
|
113
|
+
const definition = {
|
|
114
|
+
PORT: { type: 'number', default: 3000 },
|
|
115
|
+
DEBUG: { type: 'boolean', default: false },
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const values = {}
|
|
119
|
+
|
|
120
|
+
const result = validateEnv(definition, values)
|
|
121
|
+
|
|
122
|
+
expect(result.success).toBe(true)
|
|
123
|
+
expect(result.data.PORT).toBe(3000)
|
|
124
|
+
expect(result.data.DEBUG).toBe(false)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('does not replace existing value with default', () => {
|
|
128
|
+
const definition = {
|
|
129
|
+
PORT: { type: 'number', default: 3000 },
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const values = { PORT: '8080' }
|
|
133
|
+
|
|
134
|
+
const result = validateEnv(definition, values)
|
|
135
|
+
|
|
136
|
+
expect(result.success).toBe(true)
|
|
137
|
+
expect(result.data.PORT).toBe(8080)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('does not replace invalid value with default', () => {
|
|
141
|
+
const definition = {
|
|
142
|
+
PORT: { type: 'number', min: 1, default: 3000 },
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const values = { PORT: 'not-a-number' }
|
|
146
|
+
|
|
147
|
+
const result = validateEnv(definition, values)
|
|
148
|
+
|
|
149
|
+
expect(result.success).toBe(false)
|
|
150
|
+
expect(result.data.PORT).toBe('not-a-number')
|
|
151
|
+
expect(result.summary).toHaveProperty('PORT')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('parses boolean string "false" to false', () => {
|
|
155
|
+
const definition = {
|
|
156
|
+
ENABLED: { type: 'boolean' },
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const values = { ENABLED: 'false' }
|
|
160
|
+
|
|
161
|
+
const result = validateEnv(definition, values)
|
|
162
|
+
|
|
163
|
+
expect(result.success).toBe(true)
|
|
164
|
+
expect(result.data.ENABLED).toBe(false)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('parses boolean string "true" to true', () => {
|
|
168
|
+
const definition = {
|
|
169
|
+
ENABLED: { type: 'boolean' },
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const values = { ENABLED: 'true' }
|
|
173
|
+
|
|
174
|
+
const result = validateEnv(definition, values)
|
|
175
|
+
|
|
176
|
+
expect(result.success).toBe(true)
|
|
177
|
+
expect(result.data.ENABLED).toBe(true)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('parses boolean string "1" to true and "0" to false', () => {
|
|
181
|
+
const definition = {
|
|
182
|
+
A: { type: 'boolean' },
|
|
183
|
+
B: { type: 'boolean' },
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const values = { A: '1', B: '0' }
|
|
187
|
+
|
|
188
|
+
const result = validateEnv(definition, values)
|
|
189
|
+
|
|
190
|
+
expect(result.success).toBe(true)
|
|
191
|
+
expect(result.data.A).toBe(true)
|
|
192
|
+
expect(result.data.B).toBe(false)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('parses boolean default string "false" to false', () => {
|
|
196
|
+
const definition = {
|
|
197
|
+
DIGITAL_SIGNING_ENABLED: { type: 'boolean', default: 'false' },
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const values = {}
|
|
201
|
+
|
|
202
|
+
const result = validateEnv(definition, values)
|
|
203
|
+
|
|
204
|
+
expect(result.success).toBe(true)
|
|
205
|
+
expect(result.data.DIGITAL_SIGNING_ENABLED).toBe(false)
|
|
206
|
+
})
|
|
207
|
+
|
|
93
208
|
it('does not include summary when validation succeeds', () => {
|
|
94
209
|
const definition = {
|
|
95
210
|
PORT: { type: 'number', required: true },
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Converts a single field definition into a Zod schema.
|
|
3
3
|
*
|
|
4
|
+
* NOTE: This function does NOT apply .default() — defaults are handled
|
|
5
|
+
* manually in validateEnv so that they work correctly even when other
|
|
6
|
+
* fields fail validation.
|
|
7
|
+
*
|
|
4
8
|
* @param {Object} def
|
|
5
9
|
* @param {'string'|'number'|'boolean'} def.type
|
|
6
10
|
*
|
|
@@ -59,29 +63,29 @@ export function createZodSchema(
|
|
|
59
63
|
/**
|
|
60
64
|
* Validates values using a JSON definition and Zod.
|
|
61
65
|
*
|
|
66
|
+
* Defaults are applied manually (not via Zod .default()) so that:
|
|
67
|
+
* - defaults fill in when value is undefined
|
|
68
|
+
* - existing values are always validated (never replaced by defaults)
|
|
69
|
+
* - invalid values are kept in data (with errors reported)
|
|
70
|
+
* - data is ALWAYS returned, even on failure
|
|
71
|
+
*
|
|
62
72
|
* @param {Record<string, Object>} definition
|
|
63
73
|
* @param {Record<string, any>} values
|
|
64
74
|
*
|
|
65
75
|
* @returns {{
|
|
66
|
-
* success:
|
|
67
|
-
* data: Record<string, any
|
|
68
|
-
*
|
|
69
|
-
* success: false,
|
|
70
|
-
* summary: Record<string, string[]>
|
|
76
|
+
* success: boolean,
|
|
77
|
+
* data: Record<string, any>,
|
|
78
|
+
* summary?: Record<string, string[]>
|
|
71
79
|
* }}
|
|
72
80
|
*/
|
|
73
81
|
export function validateEnv(
|
|
74
82
|
definition: Record<string, any>,
|
|
75
83
|
values: Record<string, any>,
|
|
76
|
-
):
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
| {
|
|
82
|
-
success: false
|
|
83
|
-
summary: Record<string, string[]>
|
|
84
|
-
}
|
|
84
|
+
): {
|
|
85
|
+
success: boolean
|
|
86
|
+
data: Record<string, any>
|
|
87
|
+
summary?: Record<string, string[]>
|
|
88
|
+
}
|
|
85
89
|
/**
|
|
86
90
|
* Builds a structured environment validation report.
|
|
87
91
|
*
|
|
@@ -177,7 +181,7 @@ export function formatEnvReport(report: {
|
|
|
177
181
|
* success: boolean,
|
|
178
182
|
* validation: {
|
|
179
183
|
* success: boolean,
|
|
180
|
-
* data
|
|
184
|
+
* data: Record<string, any>,
|
|
181
185
|
* summary?: Record<string, string[]>
|
|
182
186
|
* },
|
|
183
187
|
* report: {
|
|
@@ -204,7 +208,7 @@ export function validateAndReportEnv(
|
|
|
204
208
|
success: boolean
|
|
205
209
|
validation: {
|
|
206
210
|
success: boolean
|
|
207
|
-
data
|
|
211
|
+
data: Record<string, any>
|
|
208
212
|
summary?: Record<string, string[]>
|
|
209
213
|
}
|
|
210
214
|
report: {
|
|
@@ -237,7 +241,7 @@ export function validateAndReportEnv(
|
|
|
237
241
|
* table: string,
|
|
238
242
|
* validation: {
|
|
239
243
|
* success: boolean,
|
|
240
|
-
* data
|
|
244
|
+
* data: Record<string, any>,
|
|
241
245
|
* summary?: Record<string, string[]>
|
|
242
246
|
* },
|
|
243
247
|
* report: {
|
|
@@ -250,7 +254,8 @@ export function validateAndReportEnv(
|
|
|
250
254
|
* valid: boolean,
|
|
251
255
|
* errors?: string[]
|
|
252
256
|
* }>
|
|
253
|
-
* }
|
|
257
|
+
* },
|
|
258
|
+
* env: Record<string, any>
|
|
254
259
|
* }}
|
|
255
260
|
*/
|
|
256
261
|
export function prepareEnvValidation(
|
|
@@ -264,7 +269,7 @@ export function prepareEnvValidation(
|
|
|
264
269
|
table: string
|
|
265
270
|
validation: {
|
|
266
271
|
success: boolean
|
|
267
|
-
data
|
|
272
|
+
data: Record<string, any>
|
|
268
273
|
summary?: Record<string, string[]>
|
|
269
274
|
}
|
|
270
275
|
report: {
|
|
@@ -278,6 +283,7 @@ export function prepareEnvValidation(
|
|
|
278
283
|
errors?: string[]
|
|
279
284
|
}>
|
|
280
285
|
}
|
|
286
|
+
env: Record<string, any>
|
|
281
287
|
}
|
|
282
288
|
/**
|
|
283
289
|
* Builds a console-ready table output for environment validation.
|