@ontrails/config 1.0.0-beta.15 → 1.0.0-beta.17

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 (139) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +2 -2
  3. package/package.json +10 -2
  4. package/src/define-config.ts +6 -3
  5. package/src/extensions.ts +1 -1
  6. package/src/index.ts +0 -1
  7. package/src/registry.ts +1 -1
  8. package/src/resolve.ts +4 -2
  9. package/.turbo/turbo-build.log +0 -1
  10. package/.turbo/turbo-lint.log +0 -3
  11. package/.turbo/turbo-typecheck.log +0 -1
  12. package/dist/app-config.d.ts +0 -65
  13. package/dist/app-config.d.ts.map +0 -1
  14. package/dist/app-config.js +0 -177
  15. package/dist/app-config.js.map +0 -1
  16. package/dist/collect.d.ts +0 -11
  17. package/dist/collect.d.ts.map +0 -1
  18. package/dist/collect.js +0 -81
  19. package/dist/collect.js.map +0 -1
  20. package/dist/compose.d.ts +0 -26
  21. package/dist/compose.d.ts.map +0 -1
  22. package/dist/compose.js +0 -19
  23. package/dist/compose.js.map +0 -1
  24. package/dist/config-layer.d.ts +0 -11
  25. package/dist/config-layer.d.ts.map +0 -1
  26. package/dist/config-layer.js +0 -6
  27. package/dist/config-layer.js.map +0 -1
  28. package/dist/config-resource.d.ts +0 -3
  29. package/dist/config-resource.d.ts.map +0 -1
  30. package/dist/config-resource.js +0 -26
  31. package/dist/config-resource.js.map +0 -1
  32. package/dist/define-config.d.ts +0 -61
  33. package/dist/define-config.d.ts.map +0 -1
  34. package/dist/define-config.js +0 -87
  35. package/dist/define-config.js.map +0 -1
  36. package/dist/derive/env.d.ts +0 -15
  37. package/dist/derive/env.d.ts.map +0 -1
  38. package/dist/derive/env.js +0 -65
  39. package/dist/derive/env.js.map +0 -1
  40. package/dist/derive/example.d.ts +0 -16
  41. package/dist/derive/example.d.ts.map +0 -1
  42. package/dist/derive/example.js +0 -138
  43. package/dist/derive/example.js.map +0 -1
  44. package/dist/derive/helpers.d.ts +0 -35
  45. package/dist/derive/helpers.d.ts.map +0 -1
  46. package/dist/derive/helpers.js +0 -116
  47. package/dist/derive/helpers.js.map +0 -1
  48. package/dist/derive/index.d.ts +0 -4
  49. package/dist/derive/index.d.ts.map +0 -1
  50. package/dist/derive/index.js +0 -4
  51. package/dist/derive/index.js.map +0 -1
  52. package/dist/derive/json-schema.d.ts +0 -18
  53. package/dist/derive/json-schema.d.ts.map +0 -1
  54. package/dist/derive/json-schema.js +0 -97
  55. package/dist/derive/json-schema.js.map +0 -1
  56. package/dist/derive-fields.d.ts +0 -25
  57. package/dist/derive-fields.d.ts.map +0 -1
  58. package/dist/derive-fields.js +0 -147
  59. package/dist/derive-fields.js.map +0 -1
  60. package/dist/derive-provenance.d.ts +0 -30
  61. package/dist/derive-provenance.d.ts.map +0 -1
  62. package/dist/derive-provenance.js +0 -101
  63. package/dist/derive-provenance.js.map +0 -1
  64. package/dist/doctor.d.ts +0 -27
  65. package/dist/doctor.d.ts.map +0 -1
  66. package/dist/doctor.js +0 -131
  67. package/dist/doctor.js.map +0 -1
  68. package/dist/extensions.d.ts +0 -38
  69. package/dist/extensions.d.ts.map +0 -1
  70. package/dist/extensions.js +0 -35
  71. package/dist/extensions.js.map +0 -1
  72. package/dist/index.d.ts +0 -20
  73. package/dist/index.d.ts.map +0 -1
  74. package/dist/index.js +0 -20
  75. package/dist/index.js.map +0 -1
  76. package/dist/merge.d.ts +0 -16
  77. package/dist/merge.d.ts.map +0 -1
  78. package/dist/merge.js +0 -34
  79. package/dist/merge.js.map +0 -1
  80. package/dist/ref.d.ts +0 -29
  81. package/dist/ref.d.ts.map +0 -1
  82. package/dist/ref.js +0 -30
  83. package/dist/ref.js.map +0 -1
  84. package/dist/registry.d.ts +0 -24
  85. package/dist/registry.d.ts.map +0 -1
  86. package/dist/registry.js +0 -12
  87. package/dist/registry.js.map +0 -1
  88. package/dist/resolve.d.ts +0 -21
  89. package/dist/resolve.d.ts.map +0 -1
  90. package/dist/resolve.js +0 -177
  91. package/dist/resolve.js.map +0 -1
  92. package/dist/secret-heuristics.d.ts +0 -10
  93. package/dist/secret-heuristics.d.ts.map +0 -1
  94. package/dist/secret-heuristics.js +0 -11
  95. package/dist/secret-heuristics.js.map +0 -1
  96. package/dist/trails/config-check.d.ts +0 -11
  97. package/dist/trails/config-check.d.ts.map +0 -1
  98. package/dist/trails/config-check.js +0 -53
  99. package/dist/trails/config-check.js.map +0 -1
  100. package/dist/trails/config-describe.d.ts +0 -12
  101. package/dist/trails/config-describe.d.ts.map +0 -1
  102. package/dist/trails/config-describe.js +0 -41
  103. package/dist/trails/config-describe.js.map +0 -1
  104. package/dist/trails/config-explain.d.ts +0 -8
  105. package/dist/trails/config-explain.d.ts.map +0 -1
  106. package/dist/trails/config-explain.js +0 -74
  107. package/dist/trails/config-explain.js.map +0 -1
  108. package/dist/trails/config-init.d.ts +0 -9
  109. package/dist/trails/config-init.d.ts.map +0 -1
  110. package/dist/trails/config-init.js +0 -78
  111. package/dist/trails/config-init.js.map +0 -1
  112. package/dist/workspace.d.ts +0 -9
  113. package/dist/workspace.d.ts.map +0 -1
  114. package/dist/workspace.js +0 -49
  115. package/dist/workspace.js.map +0 -1
  116. package/dist/zod-utils.d.ts +0 -14
  117. package/dist/zod-utils.d.ts.map +0 -1
  118. package/dist/zod-utils.js +0 -41
  119. package/dist/zod-utils.js.map +0 -1
  120. package/src/__tests__/app-config.test.ts +0 -342
  121. package/src/__tests__/compose.test.ts +0 -59
  122. package/src/__tests__/config-check.test.ts +0 -171
  123. package/src/__tests__/config-describe.test.ts +0 -154
  124. package/src/__tests__/config-explain.test.ts +0 -167
  125. package/src/__tests__/config-init.test.ts +0 -210
  126. package/src/__tests__/config-resource.test.ts +0 -87
  127. package/src/__tests__/define-config.test.ts +0 -269
  128. package/src/__tests__/derive-fields.test.ts +0 -158
  129. package/src/__tests__/derive-provenance.test.ts +0 -139
  130. package/src/__tests__/derive.test.ts +0 -269
  131. package/src/__tests__/doctor.test.ts +0 -172
  132. package/src/__tests__/extensions.test.ts +0 -134
  133. package/src/__tests__/ref.test.ts +0 -35
  134. package/src/__tests__/resolve.test.ts +0 -246
  135. package/src/__tests__/workspace.test.ts +0 -64
  136. package/src/workspace.ts +0 -56
  137. package/tsconfig.json +0 -9
  138. package/tsconfig.tests.json +0 -10
  139. package/tsconfig.tsbuildinfo +0 -1
@@ -1,269 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
2
- import { mkdir, mkdtemp, rm } from 'node:fs/promises';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import { z } from 'zod';
6
-
7
- import { defineConfig } from '../define-config.js';
8
-
9
- const schema = z.object({
10
- debug: z.boolean().default(false),
11
- host: z.string().default('localhost'),
12
- port: z.number().default(3000),
13
- });
14
-
15
- type EnvKey = 'NODE_ENV' | 'TRAILS_ENV';
16
-
17
- interface EnvSnapshot {
18
- readonly NODE_ENV: string | undefined;
19
- readonly TRAILS_ENV: string | undefined;
20
- }
21
-
22
- const readEnvSnapshot = (): EnvSnapshot => ({
23
- NODE_ENV: process.env.NODE_ENV,
24
- TRAILS_ENV: process.env.TRAILS_ENV,
25
- });
26
-
27
- const setEnvVar = (key: EnvKey, value: string | undefined): void => {
28
- if (value === undefined) {
29
- if (key === 'NODE_ENV') {
30
- delete process.env.NODE_ENV;
31
- } else {
32
- delete process.env.TRAILS_ENV;
33
- }
34
- return;
35
- }
36
- if (key === 'NODE_ENV') {
37
- process.env.NODE_ENV = value;
38
- } else {
39
- process.env.TRAILS_ENV = value;
40
- }
41
- };
42
-
43
- const restoreEnv = (snapshot: EnvSnapshot): void => {
44
- setEnvVar('NODE_ENV', snapshot.NODE_ENV);
45
- setEnvVar('TRAILS_ENV', snapshot.TRAILS_ENV);
46
- };
47
-
48
- describe('defineConfig', () => {
49
- test('returns an object with schema, base, and profiles', () => {
50
- const base = { host: 'example.com' };
51
- const profiles = { production: { port: 443 } };
52
-
53
- const config = defineConfig({ base, profiles, schema });
54
-
55
- expect(config.schema).toBe(schema);
56
- expect(config.base).toBe(base);
57
- expect(config.profiles).toBe(profiles);
58
- });
59
-
60
- test('resolve() uses TRAILS_ENV to select profile', async () => {
61
- const config = defineConfig({
62
- base: { host: 'example.com' },
63
- profiles: {
64
- production: { host: 'prod.example.com', port: 443 },
65
- test: { host: 'test.example.com', port: 9999 },
66
- },
67
- schema,
68
- });
69
-
70
- const result = await config.resolve({
71
- env: { TRAILS_ENV: 'production' },
72
- });
73
-
74
- expect(result.isOk()).toBe(true);
75
- expect(result.unwrap().host).toBe('prod.example.com');
76
- expect(result.unwrap().port).toBe(443);
77
- });
78
-
79
- test('resolve() with explicit profile option overrides TRAILS_ENV', async () => {
80
- const config = defineConfig({
81
- base: { host: 'example.com' },
82
- profiles: {
83
- production: { host: 'prod.example.com' },
84
- test: { host: 'test.example.com' },
85
- },
86
- schema,
87
- });
88
-
89
- const result = await config.resolve({
90
- env: { TRAILS_ENV: 'production' },
91
- profile: 'test',
92
- });
93
-
94
- expect(result.isOk()).toBe(true);
95
- expect(result.unwrap().host).toBe('test.example.com');
96
- });
97
-
98
- test('envFromNodeEnv maps NODE_ENV to TRAILS_ENV when unset', async () => {
99
- const config = defineConfig({
100
- base: { host: 'example.com' },
101
- envFromNodeEnv: true,
102
- profiles: {
103
- production: { host: 'prod.example.com', port: 443 },
104
- },
105
- schema,
106
- });
107
-
108
- const result = await config.resolve({
109
- env: { NODE_ENV: 'production' },
110
- });
111
-
112
- expect(result.isOk()).toBe(true);
113
- expect(result.unwrap().host).toBe('prod.example.com');
114
- expect(result.unwrap().port).toBe(443);
115
- });
116
-
117
- test('envFromNodeEnv does not mutate process.env', async () => {
118
- const snapshot = readEnvSnapshot();
119
- setEnvVar('TRAILS_ENV', undefined);
120
- setEnvVar('NODE_ENV', 'production');
121
-
122
- try {
123
- const config = defineConfig({
124
- base: { host: 'example.com' },
125
- envFromNodeEnv: true,
126
- profiles: {
127
- production: { host: 'prod.example.com', port: 443 },
128
- },
129
- schema,
130
- });
131
-
132
- const result = await config.resolve();
133
-
134
- expect(result.isOk()).toBe(true);
135
- expect(result.unwrap().host).toBe('prod.example.com');
136
- expect(process.env.TRAILS_ENV).toBeUndefined();
137
- } finally {
138
- restoreEnv(snapshot);
139
- }
140
- });
141
-
142
- test('envFromNodeEnv does not override explicit TRAILS_ENV', async () => {
143
- const config = defineConfig({
144
- base: { host: 'example.com' },
145
- envFromNodeEnv: true,
146
- profiles: {
147
- production: { host: 'prod.example.com' },
148
- test: { host: 'test.example.com' },
149
- },
150
- schema,
151
- });
152
-
153
- const result = await config.resolve({
154
- env: { NODE_ENV: 'production', TRAILS_ENV: 'test' },
155
- });
156
-
157
- expect(result.isOk()).toBe(true);
158
- expect(result.unwrap().host).toBe('test.example.com');
159
- });
160
-
161
- test('test profile works when TRAILS_ENV=test', async () => {
162
- const config = defineConfig({
163
- base: { port: 8080 },
164
- profiles: {
165
- test: { debug: true, port: 0 },
166
- },
167
- schema,
168
- });
169
-
170
- const result = await config.resolve({
171
- env: { TRAILS_ENV: 'test' },
172
- });
173
-
174
- expect(result.isOk()).toBe(true);
175
- const value = result.unwrap();
176
- expect(value.debug).toBe(true);
177
- expect(value.port).toBe(0);
178
- });
179
-
180
- test('inherits the canonical appConfig format list, including jsonc', () => {
181
- const config = defineConfig({ schema });
182
-
183
- expect(config.formats).toEqual(['toml', 'json', 'jsonc', 'yaml']);
184
- });
185
-
186
- describe('local overrides', () => {
187
- let tempDir: string;
188
-
189
- beforeEach(async () => {
190
- tempDir = await mkdtemp(join(tmpdir(), 'trails-define-config-'));
191
- });
192
-
193
- afterEach(async () => {
194
- await rm(tempDir, { force: true, recursive: true });
195
- });
196
-
197
- test('applies local overrides from .trails/config/local.ts', async () => {
198
- const configDir = join(tempDir, '.trails', 'config');
199
- await mkdir(configDir, { recursive: true });
200
- await Bun.write(
201
- join(configDir, 'local.ts'),
202
- 'export default { port: 4444 };'
203
- );
204
-
205
- const config = defineConfig({
206
- base: { host: 'example.com' },
207
- schema,
208
- });
209
-
210
- const result = await config.resolve({
211
- cwd: tempDir,
212
- env: {},
213
- });
214
-
215
- expect(result.isOk()).toBe(true);
216
- expect(result.unwrap().port).toBe(4444);
217
- });
218
-
219
- test('local overrides applied between profile and env', async () => {
220
- const configDir = join(tempDir, '.trails', 'config');
221
- await mkdir(configDir, { recursive: true });
222
- await Bun.write(
223
- join(configDir, 'local.js'),
224
- 'export default { port: 5555 };'
225
- );
226
-
227
- const config = defineConfig({
228
- base: { port: 8080 },
229
- profiles: {
230
- dev: { port: 9090 },
231
- },
232
- schema,
233
- });
234
-
235
- // Local overrides should win over profile (9090)
236
- const result = await config.resolve({
237
- cwd: tempDir,
238
- env: { TRAILS_ENV: 'dev' },
239
- profile: 'dev',
240
- });
241
-
242
- expect(result.isOk()).toBe(true);
243
- expect(result.unwrap().port).toBe(5555);
244
- });
245
-
246
- test('skips local overrides when TRAILS_ENV=test', async () => {
247
- const configDir = join(tempDir, '.trails', 'config');
248
- await mkdir(configDir, { recursive: true });
249
- await Bun.write(
250
- join(configDir, 'local.ts'),
251
- 'export default { port: 4444 };'
252
- );
253
-
254
- const config = defineConfig({
255
- base: { port: 8080 },
256
- schema,
257
- });
258
-
259
- const result = await config.resolve({
260
- cwd: tempDir,
261
- env: { TRAILS_ENV: 'test' },
262
- });
263
-
264
- expect(result.isOk()).toBe(true);
265
- // Should NOT apply local overrides, so port stays at base 8080
266
- expect(result.unwrap().port).toBe(8080);
267
- });
268
- });
269
- });
@@ -1,158 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { z } from 'zod';
3
-
4
- import { deprecated, env, secret } from '../extensions.js';
5
- import { deriveConfigFields } from '../derive-fields.js';
6
-
7
- describe('deriveConfigFields', () => {
8
- describe('basic field descriptions', () => {
9
- test('returns path, type, and required for each field', () => {
10
- const schema = z.object({
11
- debug: z.boolean(),
12
- host: z.string(),
13
- port: z.number(),
14
- });
15
-
16
- const fields = deriveConfigFields(schema);
17
-
18
- expect(fields).toHaveLength(3);
19
- const host = fields.find((f) => f.path === 'host');
20
- expect(host?.type).toBe('string');
21
- expect(host?.required).toBe(true);
22
-
23
- const port = fields.find((f) => f.path === 'port');
24
- expect(port?.type).toBe('number');
25
- expect(port?.required).toBe(true);
26
- });
27
- });
28
-
29
- describe('Zod describe() metadata', () => {
30
- test('includes description from .describe()', () => {
31
- const schema = z.object({
32
- host: z.string().describe('The server hostname'),
33
- });
34
-
35
- const fields = deriveConfigFields(schema);
36
- const host = fields.find((f) => f.path === 'host');
37
- expect(host?.description).toBe('The server hostname');
38
- });
39
- });
40
-
41
- describe('config metadata', () => {
42
- test('includes env, secret, deprecated from metadata', () => {
43
- const schema = z.object({
44
- apiKey: secret(env(z.string(), 'API_KEY')),
45
- legacyMode: deprecated(z.boolean(), 'Use "mode" instead').default(
46
- false
47
- ),
48
- });
49
-
50
- const fields = deriveConfigFields(schema);
51
-
52
- const apiKey = fields.find((f) => f.path === 'apiKey');
53
- expect(apiKey?.env).toBe('API_KEY');
54
- expect(apiKey?.secret).toBe(true);
55
-
56
- const legacy = fields.find((f) => f.path === 'legacyMode');
57
- expect(legacy?.deprecated).toBe('Use "mode" instead');
58
- });
59
- });
60
-
61
- describe('defaults', () => {
62
- test('detects default values', () => {
63
- const schema = z.object({
64
- debug: z.boolean().default(false),
65
- port: z.number().default(3000),
66
- });
67
-
68
- const fields = deriveConfigFields(schema);
69
-
70
- const port = fields.find((f) => f.path === 'port');
71
- expect(port?.default).toBe(3000);
72
- expect(port?.required).toBe(false);
73
-
74
- const debug = fields.find((f) => f.path === 'debug');
75
- expect(debug?.default).toBe(false);
76
- });
77
- });
78
-
79
- describe('nested objects', () => {
80
- test('handles nested object schemas with dot paths', () => {
81
- const schema = z.object({
82
- db: z.object({
83
- host: z.string(),
84
- port: z.number().default(5432),
85
- }),
86
- });
87
-
88
- const fields = deriveConfigFields(schema);
89
-
90
- const dbHost = fields.find((f) => f.path === 'db.host');
91
- expect(dbHost?.type).toBe('string');
92
- expect(dbHost?.required).toBe(true);
93
-
94
- const dbPort = fields.find((f) => f.path === 'db.port');
95
- expect(dbPort?.type).toBe('number');
96
- expect(dbPort?.default).toBe(5432);
97
- });
98
-
99
- test('unwraps optional nested object schemas before walking', () => {
100
- const schema = z.object({
101
- db: z
102
- .object({
103
- host: z.string(),
104
- port: z.number().default(5432),
105
- })
106
- .optional(),
107
- });
108
-
109
- const fields = deriveConfigFields(schema);
110
-
111
- expect(fields).toEqual([
112
- expect.objectContaining({ path: 'db.host', required: true }),
113
- expect.objectContaining({ default: 5432, path: 'db.port' }),
114
- ]);
115
- });
116
- });
117
-
118
- describe('constraints', () => {
119
- test('detects enum values', () => {
120
- const schema = z.object({
121
- env: z.enum(['development', 'production', 'test']),
122
- });
123
-
124
- const fields = deriveConfigFields(schema);
125
- const envField = fields.find((f) => f.path === 'env');
126
- expect(envField?.type).toBe('enum');
127
- expect(envField?.constraints?.values).toEqual([
128
- 'development',
129
- 'production',
130
- 'test',
131
- ]);
132
- });
133
-
134
- test('detects number min/max constraints', () => {
135
- const schema = z.object({
136
- port: z.number().min(1).max(65_535),
137
- });
138
-
139
- const fields = deriveConfigFields(schema);
140
- const port = fields.find((f) => f.path === 'port');
141
- expect(port?.constraints?.min).toBe(1);
142
- expect(port?.constraints?.max).toBe(65_535);
143
- });
144
- });
145
-
146
- describe('optional fields', () => {
147
- test('marks optional fields as not required', () => {
148
- const schema = z.object({
149
- host: z.string(),
150
- nickname: z.string().optional(),
151
- });
152
-
153
- const fields = deriveConfigFields(schema);
154
- const nickname = fields.find((f) => f.path === 'nickname');
155
- expect(nickname?.required).toBe(false);
156
- });
157
- });
158
- });
@@ -1,139 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { z } from 'zod';
3
-
4
- import { env, secret } from '../extensions.js';
5
- import { deriveConfigProvenance } from '../derive-provenance.js';
6
-
7
- const schema = z.object({
8
- debug: z.boolean().default(false),
9
- host: z.string().default('localhost'),
10
- port: z.number().default(3000),
11
- });
12
-
13
- describe('deriveConfigProvenance', () => {
14
- describe('default source', () => {
15
- test('reports "default" when no other source provides value', () => {
16
- const entries = deriveConfigProvenance({
17
- resolved: { debug: false, host: 'localhost', port: 3000 },
18
- schema,
19
- });
20
-
21
- const host = entries.find((e) => e.path === 'host');
22
- expect(host?.source).toBe('default');
23
- expect(host?.value).toBe('localhost');
24
- });
25
- });
26
-
27
- describe('base overrides default', () => {
28
- test('reports "base" when base provides value', () => {
29
- const entries = deriveConfigProvenance({
30
- base: { host: 'base.example.com' },
31
- resolved: { debug: false, host: 'base.example.com', port: 3000 },
32
- schema,
33
- });
34
-
35
- const host = entries.find((e) => e.path === 'host');
36
- expect(host?.source).toBe('base');
37
- expect(host?.value).toBe('base.example.com');
38
- });
39
- });
40
-
41
- describe('profile overrides base', () => {
42
- test('reports "profile" when profile provides winning value', () => {
43
- const entries = deriveConfigProvenance({
44
- base: { host: 'base.example.com' },
45
- profile: { host: 'profile.example.com' },
46
- resolved: { debug: false, host: 'profile.example.com', port: 3000 },
47
- schema,
48
- });
49
-
50
- const host = entries.find((e) => e.path === 'host');
51
- expect(host?.source).toBe('profile');
52
- expect(host?.value).toBe('profile.example.com');
53
- });
54
- });
55
-
56
- describe('local overrides profile', () => {
57
- test('reports "local" when local provides winning value', () => {
58
- const entries = deriveConfigProvenance({
59
- base: { host: 'base.example.com' },
60
- local: { host: 'local.example.com' },
61
- profile: { host: 'profile.example.com' },
62
- resolved: { debug: false, host: 'local.example.com', port: 3000 },
63
- schema,
64
- });
65
-
66
- const host = entries.find((e) => e.path === 'host');
67
- expect(host?.source).toBe('local');
68
- });
69
- });
70
-
71
- describe('env overrides everything', () => {
72
- test('reports "env" when env provides winning value', () => {
73
- const envSchema = z.object({
74
- host: env(z.string(), 'APP_HOST').default('localhost'),
75
- port: z.number().default(3000),
76
- });
77
-
78
- const entries = deriveConfigProvenance({
79
- base: { host: 'base.example.com' },
80
- env: { APP_HOST: 'env.example.com' },
81
- resolved: { host: 'env.example.com', port: 3000 },
82
- schema: envSchema,
83
- });
84
-
85
- const host = entries.find((e) => e.path === 'host');
86
- expect(host?.source).toBe('env');
87
- expect(host?.value).toBe('env.example.com');
88
- });
89
-
90
- test('walks optional nested object schemas for env-backed entries', () => {
91
- const envSchema = z.object({
92
- db: z
93
- .object({
94
- host: env(z.string(), 'DB_HOST').default('localhost'),
95
- })
96
- .optional(),
97
- });
98
-
99
- const entries = deriveConfigProvenance({
100
- env: { DB_HOST: 'env.example.com' },
101
- resolved: { db: { host: 'env.example.com' } },
102
- schema: envSchema,
103
- });
104
-
105
- const host = entries.find((entry) => entry.path === 'db.host');
106
- expect(host?.source).toBe('env');
107
- expect(host?.value).toBe('env.example.com');
108
- });
109
- });
110
-
111
- describe('secret redaction', () => {
112
- test('redacts secret fields', () => {
113
- const secretSchema = z.object({
114
- apiKey: secret(env(z.string(), 'API_KEY')),
115
- host: z.string().default('localhost'),
116
- });
117
-
118
- const entries = deriveConfigProvenance({
119
- env: { API_KEY: 'super-secret-key' },
120
- resolved: { apiKey: 'super-secret-key', host: 'localhost' },
121
- schema: secretSchema,
122
- });
123
-
124
- const apiKey = entries.find((e) => e.path === 'apiKey');
125
- expect(apiKey?.redacted).toBe(true);
126
- expect(apiKey?.value).toBe('[REDACTED]');
127
- });
128
-
129
- test('does not redact non-secret fields', () => {
130
- const entries = deriveConfigProvenance({
131
- resolved: { debug: false, host: 'localhost', port: 3000 },
132
- schema,
133
- });
134
-
135
- const host = entries.find((e) => e.path === 'host');
136
- expect(host?.redacted).toBe(false);
137
- });
138
- });
139
- });