@uniglot/wont-let-you-see 0.1.1 → 0.3.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.
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
2
2
  import { mask, unmask } from "../masker";
3
3
  import { createMapping } from "../mapping";
4
4
  import { resetConfig } from "../config";
5
+ import { resetPatternCache } from "../patterns";
5
6
 
6
7
  describe("masker", () => {
7
8
  const sessionId = "test-session-masker";
@@ -10,12 +11,14 @@ describe("masker", () => {
10
11
  beforeEach(() => {
11
12
  process.env.WONT_LET_YOU_SEE_REVEALED_PATTERNS = "";
12
13
  resetConfig();
14
+ resetPatternCache();
13
15
  createMapping(sessionId);
14
16
  });
15
17
 
16
18
  afterEach(() => {
17
19
  process.env = { ...originalEnv };
18
20
  resetConfig();
21
+ resetPatternCache();
19
22
  });
20
23
 
21
24
  describe("mask()", () => {
@@ -182,6 +185,67 @@ describe("masker", () => {
182
185
  });
183
186
  });
184
187
 
188
+ describe("customPatterns with regex support", () => {
189
+ it("should mask exact string patterns", () => {
190
+ process.env.WONT_LET_YOU_SEE_CUSTOM_PATTERNS = "my-secret-value";
191
+ resetConfig();
192
+
193
+ const input = "Secret: my-secret-value";
194
+ const result = mask(sessionId, input);
195
+
196
+ expect(result).toMatch(/Secret: #\(custom-\d+\)/);
197
+ expect(result).not.toContain("my-secret-value");
198
+ });
199
+
200
+ it("should mask regex patterns with regex: prefix", () => {
201
+ process.env.WONT_LET_YOU_SEE_CUSTOM_PATTERNS =
202
+ "regex:secret-[a-z]{3}-\\d{4}";
203
+ resetConfig();
204
+
205
+ const input = "Keys: secret-abc-1234, secret-xyz-5678";
206
+ const result = mask(sessionId, input);
207
+
208
+ expect(result).toMatch(/#\(custom-\d+\)/);
209
+ expect(result).not.toContain("secret-abc-1234");
210
+ expect(result).not.toContain("secret-xyz-5678");
211
+ });
212
+
213
+ it("should treat patterns without regex: prefix as literal strings", () => {
214
+ process.env.WONT_LET_YOU_SEE_CUSTOM_PATTERNS = "literal-value";
215
+ resetConfig();
216
+
217
+ const input = "Value: literal-value, Other: literal-values";
218
+ const result = mask(sessionId, input);
219
+
220
+ expect(result).toMatch(/Value: #\(custom-\d+\)/);
221
+ expect(result).toContain("literal-values");
222
+ });
223
+
224
+ it("should round-trip custom regex patterns", () => {
225
+ process.env.WONT_LET_YOU_SEE_CUSTOM_PATTERNS = "regex:token-[A-Z]{8}";
226
+ resetConfig();
227
+
228
+ const original = "Auth: token-ABCDEFGH";
229
+ const masked = mask(sessionId, original);
230
+ const unmasked = unmask(sessionId, masked);
231
+
232
+ expect(unmasked).toBe(original);
233
+ });
234
+
235
+ it("should support multiple custom patterns including regex", () => {
236
+ process.env.WONT_LET_YOU_SEE_CUSTOM_PATTERNS =
237
+ "exact-secret,regex:pattern-\\d+";
238
+ resetConfig();
239
+
240
+ const input = "Secrets: exact-secret, pattern-123, pattern-456";
241
+ const result = mask(sessionId, input);
242
+
243
+ expect(result).not.toContain("exact-secret");
244
+ expect(result).not.toContain("pattern-123");
245
+ expect(result).not.toContain("pattern-456");
246
+ });
247
+ });
248
+
185
249
  describe("round-trip integrity", () => {
186
250
  it("should maintain round-trip integrity for VPC", () => {
187
251
  const original = "vpc-1234567890abcdef0";
@@ -1,26 +1,31 @@
1
- import { describe, it, expect } from "vitest";
2
- import {
3
- AWS_PATTERNS,
4
- K8S_PATTERNS,
5
- COMMON_PATTERNS,
6
- PATTERNS,
7
- } from "../patterns";
8
-
9
- describe("AWS_PATTERNS", () => {
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { loadPatterns, resetPatternCache, getPatternByName } from "../patterns";
3
+
4
+ function getPattern(name: string) {
5
+ const p = getPatternByName(name);
6
+ if (!p) throw new Error(`Pattern '${name}' not found`);
7
+ return p.pattern;
8
+ }
9
+
10
+ describe("AWS Patterns", () => {
11
+ beforeEach(() => {
12
+ resetPatternCache();
13
+ });
14
+
10
15
  describe("ARN", () => {
11
16
  it("should match aws partition", () => {
12
17
  expect(
13
- AWS_PATTERNS.arn.test("arn:aws:iam::123456789012:user/admin"),
18
+ getPattern("arn").test("arn:aws:iam::123456789012:user/admin"),
14
19
  ).toBe(true);
15
20
  });
16
21
 
17
22
  it("should match aws-cn partition", () => {
18
- expect(AWS_PATTERNS.arn.test("arn:aws-cn:s3:::my-bucket")).toBe(true);
23
+ expect(getPattern("arn").test("arn:aws-cn:s3:::my-bucket")).toBe(true);
19
24
  });
20
25
 
21
26
  it("should match aws-us-gov partition", () => {
22
27
  expect(
23
- AWS_PATTERNS.arn.test(
28
+ getPattern("arn").test(
24
29
  "arn:aws-us-gov:ec2:us-gov-west-1:123456789012:instance/i-123",
25
30
  ),
26
31
  ).toBe(true);
@@ -29,94 +34,194 @@ describe("AWS_PATTERNS", () => {
29
34
 
30
35
  describe("VPC resources", () => {
31
36
  it("should match vpc", () => {
32
- expect(AWS_PATTERNS.vpc.test("vpc-0123456789abcdef0")).toBe(true);
37
+ expect(getPattern("vpc").test("vpc-0123456789abcdef0")).toBe(true);
33
38
  });
34
39
 
35
40
  it("should match subnet", () => {
36
- expect(AWS_PATTERNS.subnet.test("subnet-0123456789abcdef0")).toBe(true);
41
+ expect(getPattern("subnet").test("subnet-0123456789abcdef0")).toBe(true);
37
42
  });
38
43
 
39
44
  it("should match security group", () => {
40
- expect(AWS_PATTERNS.securityGroup.test("sg-0123456789abcdef0")).toBe(
45
+ expect(getPattern("security-group").test("sg-0123456789abcdef0")).toBe(
41
46
  true,
42
47
  );
43
48
  });
44
49
 
45
50
  it("should match nat gateway", () => {
46
- expect(AWS_PATTERNS.natGateway.test("nat-0123456789abcdef0")).toBe(true);
51
+ expect(getPattern("nat-gateway").test("nat-0123456789abcdef0")).toBe(
52
+ true,
53
+ );
47
54
  });
48
55
 
49
56
  it("should match network acl", () => {
50
- expect(AWS_PATTERNS.networkAcl.test("acl-0123456789abcdef0")).toBe(true);
57
+ expect(getPattern("network-acl").test("acl-0123456789abcdef0")).toBe(
58
+ true,
59
+ );
51
60
  });
52
61
 
53
62
  it("should match eni", () => {
54
- expect(AWS_PATTERNS.eni.test("eni-0123456789abcdef0")).toBe(true);
63
+ expect(getPattern("eni").test("eni-0123456789abcdef0")).toBe(true);
55
64
  });
56
65
  });
57
66
 
58
67
  describe("EC2 resources", () => {
59
68
  it("should match ebs volume", () => {
60
- expect(AWS_PATTERNS.ebs.test("vol-0123456789abcdef0")).toBe(true);
69
+ expect(getPattern("ebs").test("vol-0123456789abcdef0")).toBe(true);
61
70
  });
62
71
 
63
72
  it("should match snapshot", () => {
64
- expect(AWS_PATTERNS.snapshot.test("snap-0123456789abcdef0")).toBe(true);
73
+ expect(getPattern("snapshot").test("snap-0123456789abcdef0")).toBe(true);
74
+ });
75
+ });
76
+
77
+ describe("VPC networking resources", () => {
78
+ it("should match vpc endpoint", () => {
79
+ expect(getPattern("vpc-endpoint").test("vpce-0123456789abcdef0")).toBe(
80
+ true,
81
+ );
82
+ });
83
+
84
+ it("should match transit gateway", () => {
85
+ expect(getPattern("transit-gateway").test("tgw-0123456789abcdef0")).toBe(
86
+ true,
87
+ );
88
+ });
89
+
90
+ it("should match customer gateway", () => {
91
+ expect(getPattern("customer-gateway").test("cgw-0123456789abcdef0")).toBe(
92
+ true,
93
+ );
94
+ });
95
+
96
+ it("should match vpn gateway", () => {
97
+ expect(getPattern("vpn-gateway").test("vgw-0123456789abcdef0")).toBe(
98
+ true,
99
+ );
100
+ });
101
+
102
+ it("should match vpn connection", () => {
103
+ expect(getPattern("vpn-connection").test("vpn-0123456789abcdef0")).toBe(
104
+ true,
105
+ );
106
+ });
107
+
108
+ it("should NOT match invalid vpc endpoint", () => {
109
+ expect(getPattern("vpc-endpoint").test("vpce-invalid")).toBe(false);
110
+ });
111
+ });
112
+
113
+ describe("ECR resources", () => {
114
+ it("should match ECR repo URI", () => {
115
+ expect(
116
+ getPattern("ecr-repo").test(
117
+ "123456789012.dkr.ecr.us-west-2.amazonaws.com/my-repo",
118
+ ),
119
+ ).toBe(true);
120
+ });
121
+
122
+ it("should match ECR repo URI with nested path", () => {
123
+ expect(
124
+ getPattern("ecr-repo").test(
125
+ "123456789012.dkr.ecr.eu-central-1.amazonaws.com/org/app/service",
126
+ ),
127
+ ).toBe(true);
128
+ });
129
+
130
+ it("should match ECR repo URI with dots and underscores", () => {
131
+ expect(
132
+ getPattern("ecr-repo").test(
133
+ "123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/my_repo.name",
134
+ ),
135
+ ).toBe(true);
136
+ });
137
+
138
+ it("should NOT match invalid ECR URI (wrong account ID length)", () => {
139
+ expect(
140
+ getPattern("ecr-repo").test(
141
+ "12345.dkr.ecr.us-west-2.amazonaws.com/my-repo",
142
+ ),
143
+ ).toBe(false);
144
+ });
145
+
146
+ it("should NOT match non-ECR docker registry", () => {
147
+ expect(getPattern("ecr-repo").test("docker.io/library/nginx")).toBe(
148
+ false,
149
+ );
150
+ });
151
+
152
+ it("should match ECR repo URI with masked account ID", () => {
153
+ expect(
154
+ getPattern("ecr-repo").test(
155
+ "#(custom-1).dkr.ecr.us-west-2.amazonaws.com/my-repo",
156
+ ),
157
+ ).toBe(true);
158
+ });
159
+
160
+ it("should match ECR repo URI with masked account ID (higher number)", () => {
161
+ expect(
162
+ getPattern("ecr-repo").test(
163
+ "#(custom-123).dkr.ecr.us-east-1.amazonaws.com/app/service",
164
+ ),
165
+ ).toBe(true);
65
166
  });
66
167
  });
67
168
 
68
169
  describe("Account ID (contextual)", () => {
69
170
  it("should match OwnerId field", () => {
70
- expect(AWS_PATTERNS.accountId.test('"OwnerId": "123456789012"')).toBe(
171
+ expect(getPattern("account-id").test('"OwnerId": "123456789012"')).toBe(
71
172
  true,
72
173
  );
73
174
  });
74
175
 
75
176
  it("should match account_id field (terraform style)", () => {
76
- expect(AWS_PATTERNS.accountId.test('"account_id": "123456789012"')).toBe(
77
- true,
78
- );
177
+ expect(
178
+ getPattern("account-id").test('"account_id": "123456789012"'),
179
+ ).toBe(true);
79
180
  });
80
181
 
81
182
  it("should NOT match bare number", () => {
82
- expect(AWS_PATTERNS.accountId.test("123456789012")).toBe(false);
183
+ expect(getPattern("account-id").test("123456789012")).toBe(false);
83
184
  });
84
185
  });
85
186
 
86
187
  describe("Access Key ID", () => {
87
188
  it("should match AKIA prefix", () => {
88
- expect(AWS_PATTERNS.accessKeyId.test(" AKIAIOSFODNN7EXAMPLE ")).toBe(
189
+ expect(getPattern("access-key-id").test(" AKIAIOSFODNN7EXAMPLE ")).toBe(
89
190
  true,
90
191
  );
91
192
  });
92
193
 
93
194
  it("should match ASIA prefix (temporary)", () => {
94
- expect(AWS_PATTERNS.accessKeyId.test(" ASIAISAMPLEKEYID1234 ")).toBe(
195
+ expect(getPattern("access-key-id").test(" ASIAISAMPLEKEYID1234 ")).toBe(
95
196
  true,
96
197
  );
97
198
  });
98
199
 
99
200
  it("should NOT match random string", () => {
100
- expect(AWS_PATTERNS.accessKeyId.test("RANDOMSTRING12345678")).toBe(false);
201
+ expect(getPattern("access-key-id").test("RANDOMSTRING12345678")).toBe(
202
+ false,
203
+ );
101
204
  });
102
205
  });
103
206
  });
104
207
 
105
- describe("K8S_PATTERNS", () => {
208
+ describe("Kubernetes Patterns", () => {
209
+ beforeEach(() => {
210
+ resetPatternCache();
211
+ });
212
+
106
213
  describe("Service Account Token", () => {
107
214
  it("should match JWT format", () => {
108
215
  const jwt =
109
216
  "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlZmF1bHQtdG9rZW4tYWJjZGUiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVmYXVsdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjEyMzQ1Njc4LTEyMzQtMTIzNC0xMjM0LTEyMzQ1Njc4OTAxMiIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.signature";
110
- expect(K8S_PATTERNS.serviceAccountToken.test(jwt)).toBe(true);
217
+ expect(getPattern("k8s-token").test(jwt)).toBe(true);
111
218
  });
112
219
  });
113
220
 
114
221
  describe("Node Names", () => {
115
222
  it("should match AWS node name", () => {
116
223
  expect(
117
- K8S_PATTERNS.nodeNameAws.test(
118
- "ip-10-0-1-123.us-west-2.compute.internal",
119
- ),
224
+ getPattern("k8s-node").test("ip-10-0-1-123.us-west-2.compute.internal"),
120
225
  ).toBe(true);
121
226
  });
122
227
  });
@@ -124,7 +229,7 @@ describe("K8S_PATTERNS", () => {
124
229
  describe("Cluster Endpoints", () => {
125
230
  it("should match EKS endpoint", () => {
126
231
  expect(
127
- K8S_PATTERNS.clusterEndpoint.test(
232
+ getPattern("k8s-endpoint").test(
128
233
  "https://ABCDEF1234567890ABCD.us-west-2.eks.amazonaws.com",
129
234
  ),
130
235
  ).toBe(true);
@@ -132,14 +237,18 @@ describe("K8S_PATTERNS", () => {
132
237
  });
133
238
  });
134
239
 
135
- describe("COMMON_PATTERNS", () => {
240
+ describe("Common Patterns", () => {
241
+ beforeEach(() => {
242
+ resetPatternCache();
243
+ });
244
+
136
245
  describe("IPv4", () => {
137
246
  it("should match IPv4", () => {
138
- expect(COMMON_PATTERNS.ipv4.test("192.168.1.1")).toBe(true);
247
+ expect(getPattern("ipv4").test("192.168.1.1")).toBe(true);
139
248
  });
140
249
 
141
250
  it("should not match CIDR notation", () => {
142
- expect(COMMON_PATTERNS.ipv4.test("10.0.0.0/8")).toBe(false);
251
+ expect(getPattern("ipv4").test("10.0.0.0/8")).toBe(false);
143
252
  });
144
253
  });
145
254
 
@@ -153,16 +262,16 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC
153
262
  -----END PRIVATE KEY-----`;
154
263
 
155
264
  it("should match entire RSA private key block", () => {
156
- expect(COMMON_PATTERNS.privateKey.test(rsaKey)).toBe(true);
265
+ expect(getPattern("private-key").test(rsaKey)).toBe(true);
157
266
  });
158
267
 
159
268
  it("should match entire generic private key block", () => {
160
- expect(COMMON_PATTERNS.privateKey.test(genericKey)).toBe(true);
269
+ expect(getPattern("private-key").test(genericKey)).toBe(true);
161
270
  });
162
271
 
163
272
  it("should not match header only", () => {
164
273
  expect(
165
- COMMON_PATTERNS.privateKey.test("-----BEGIN RSA PRIVATE KEY-----"),
274
+ getPattern("private-key").test("-----BEGIN RSA PRIVATE KEY-----"),
166
275
  ).toBe(false);
167
276
  });
168
277
  });
@@ -170,49 +279,89 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC
170
279
  describe("API Key Field (contextual)", () => {
171
280
  it("should match api_key field", () => {
172
281
  expect(
173
- COMMON_PATTERNS.apiKeyField.test('"api_key": "sk-1234567890abcdef"'),
282
+ getPattern("api-key").test('"api_key": "sk-1234567890abcdef"'),
174
283
  ).toBe(true);
175
284
  });
176
285
 
177
286
  it("should match password field", () => {
178
- expect(
179
- COMMON_PATTERNS.apiKeyField.test('"password": "supersecret123"'),
180
- ).toBe(true);
287
+ expect(getPattern("api-key").test('"password": "supersecret123"')).toBe(
288
+ true,
289
+ );
181
290
  });
182
291
 
183
292
  it("should match token field", () => {
184
- expect(
185
- COMMON_PATTERNS.apiKeyField.test('"token": "ghp_xxxxxxxxxxxx"'),
186
- ).toBe(true);
293
+ expect(getPattern("api-key").test('"token": "ghp_xxxxxxxxxxxx"')).toBe(
294
+ true,
295
+ );
187
296
  });
188
297
  });
189
298
  });
190
299
 
191
- describe("PATTERNS (combined)", () => {
192
- it("should include all AWS patterns", () => {
193
- expect(PATTERNS.vpc).toBe(AWS_PATTERNS.vpc);
194
- expect(PATTERNS.arn).toBe(AWS_PATTERNS.arn);
195
- });
196
-
197
- it("should include all K8S patterns", () => {
198
- expect(PATTERNS.serviceAccountToken).toBe(K8S_PATTERNS.serviceAccountToken);
300
+ describe("ReDoS safety", () => {
301
+ beforeEach(() => {
302
+ resetPatternCache();
199
303
  });
200
304
 
201
- it("should include all common patterns", () => {
202
- expect(PATTERNS.ipv4).toBe(COMMON_PATTERNS.ipv4);
203
- expect(PATTERNS.privateKey).toBe(COMMON_PATTERNS.privateKey);
204
- });
205
- });
206
-
207
- describe("ReDoS safety", () => {
208
305
  it("should handle pathological input quickly", () => {
209
306
  const pathological = "a".repeat(1000) + "b";
210
307
  const start = performance.now();
211
308
 
212
- for (const pattern of Object.values(PATTERNS)) {
309
+ const allPatterns = loadPatterns();
310
+ for (const { pattern } of allPatterns) {
213
311
  pattern.test(pathological);
214
312
  }
215
313
 
216
314
  expect(performance.now() - start).toBeLessThan(100);
217
315
  });
218
316
  });
317
+
318
+ describe("loadPatterns", () => {
319
+ beforeEach(() => {
320
+ resetPatternCache();
321
+ });
322
+
323
+ it("should load patterns from JSON files", () => {
324
+ const patterns = loadPatterns();
325
+ expect(patterns.length).toBeGreaterThan(0);
326
+ });
327
+
328
+ it("should load AWS patterns", () => {
329
+ const patterns = loadPatterns();
330
+ const vpcPattern = patterns.find((p) => p.name === "vpc");
331
+ expect(vpcPattern).toBeDefined();
332
+ expect(vpcPattern!.pattern.test("vpc-1234567890abcdef0")).toBe(true);
333
+ });
334
+
335
+ it("should load Kubernetes patterns", () => {
336
+ const patterns = loadPatterns();
337
+ const k8sTokenPattern = patterns.find((p) => p.name === "k8s-token");
338
+ expect(k8sTokenPattern).toBeDefined();
339
+ });
340
+
341
+ it("should load common patterns", () => {
342
+ const patterns = loadPatterns();
343
+ const ipv4Pattern = patterns.find((p) => p.name === "ipv4");
344
+ expect(ipv4Pattern).toBeDefined();
345
+ expect(ipv4Pattern!.pattern.test("192.168.1.1")).toBe(true);
346
+ });
347
+
348
+ it("should mark contextual patterns correctly", () => {
349
+ const patterns = loadPatterns();
350
+ const accountIdPattern = patterns.find((p) => p.name === "account-id");
351
+ expect(accountIdPattern).toBeDefined();
352
+ expect(accountIdPattern!.isContextual).toBe(true);
353
+ });
354
+
355
+ it("should cache patterns after first load", () => {
356
+ const patterns1 = loadPatterns();
357
+ const patterns2 = loadPatterns();
358
+ expect(patterns1).toBe(patterns2);
359
+ });
360
+
361
+ it("should reset cache on resetPatternCache()", () => {
362
+ const patterns1 = loadPatterns();
363
+ resetPatternCache();
364
+ const patterns2 = loadPatterns();
365
+ expect(patterns1).not.toBe(patterns2);
366
+ });
367
+ });
package/src/config.ts CHANGED
@@ -1,5 +1,8 @@
1
- import { existsSync, readFileSync } from "fs";
2
- import { join, dirname, parse } from "path";
1
+ import {
2
+ existsSync as nodeExistsSync,
3
+ readFileSync as nodeReadFileSync,
4
+ } from "fs";
5
+ import { join, dirname } from "path";
3
6
  import { homedir } from "os";
4
7
 
5
8
  export interface Config {
@@ -8,6 +11,24 @@ export interface Config {
8
11
  customPatterns: string[];
9
12
  }
10
13
 
14
+ interface FsAdapter {
15
+ existsSync: (path: string) => boolean;
16
+ readFileSync: (path: string, encoding: "utf-8") => string;
17
+ }
18
+
19
+ let fsAdapter: FsAdapter = {
20
+ existsSync: nodeExistsSync,
21
+ readFileSync: nodeReadFileSync,
22
+ };
23
+
24
+ export function setFsAdapter(adapter: FsAdapter): void {
25
+ fsAdapter = adapter;
26
+ }
27
+
28
+ export function resetFsAdapter(): void {
29
+ fsAdapter = { existsSync: nodeExistsSync, readFileSync: nodeReadFileSync };
30
+ }
31
+
11
32
  const DEFAULT_CONFIG: Config = {
12
33
  enabled: true,
13
34
  revealedPatterns: [],
@@ -22,7 +43,7 @@ function findConfigInAncestors(startDir: string): string | null {
22
43
 
23
44
  while (true) {
24
45
  const configPath = join(currentDir, CONFIG_FILENAME);
25
- if (existsSync(configPath)) {
46
+ if (fsAdapter.existsSync(configPath)) {
26
47
  return configPath;
27
48
  }
28
49
 
@@ -41,20 +62,18 @@ function findConfigInAncestors(startDir: string): string | null {
41
62
  }
42
63
 
43
64
  function loadJsonConfig(): Partial<Config> {
44
- // First, search up from cwd to find nearest config
45
65
  const ancestorConfig = findConfigInAncestors(process.cwd());
46
66
  if (ancestorConfig) {
47
67
  try {
48
- const content = readFileSync(ancestorConfig, "utf-8");
68
+ const content = fsAdapter.readFileSync(ancestorConfig, "utf-8");
49
69
  return JSON.parse(content);
50
70
  } catch {}
51
71
  }
52
72
 
53
- // Fall back to home directory
54
73
  const homeConfig = join(homedir(), CONFIG_FILENAME);
55
- if (existsSync(homeConfig)) {
74
+ if (fsAdapter.existsSync(homeConfig)) {
56
75
  try {
57
- const content = readFileSync(homeConfig, "utf-8");
76
+ const content = fsAdapter.readFileSync(homeConfig, "utf-8");
58
77
  return JSON.parse(content);
59
78
  } catch {}
60
79
  }