zod-codegen 1.7.0 → 1.7.2

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/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [1.7.2](https://github.com/julienandreu/zod-codegen/compare/v1.7.1...v1.7.2) (2026-03-10)
2
+
3
+ ### 🐛 Bug Fixes
4
+
5
+ - redact credentials from source URL in generated file headers ([#88](https://github.com/julienandreu/zod-codegen/issues/88)) ([692901d](https://github.com/julienandreu/zod-codegen/commit/692901d0b3ea8a53cbb92102bc9c3f7f90aa44aa))
6
+
7
+ ## [1.7.1](https://github.com/julienandreu/zod-codegen/compare/v1.7.0...v1.7.1) (2026-03-10)
8
+
9
+ ### 🐛 Bug Fixes
10
+
11
+ - properly handle URL inputs with authentication support ([#87](https://github.com/julienandreu/zod-codegen/issues/87)) ([49ba2c4](https://github.com/julienandreu/zod-codegen/commit/49ba2c483458397d9f12e03b8f6c634981e4454a))
12
+
1
13
  ## [1.7.0](https://github.com/julienandreu/zod-codegen/compare/v1.6.3...v1.7.0) (2026-03-09)
2
14
 
3
15
  ### 🚀 Features
@@ -1,7 +1,9 @@
1
1
  import type { OpenApiFileParser, OpenApiFileReader } from '../interfaces/file-reader';
2
2
  import type { OpenApiSpecType } from '../types/openapi';
3
3
  export declare class SyncFileReaderService implements OpenApiFileReader {
4
+ private isUrl;
4
5
  readFile(path: string): Promise<string>;
6
+ private fetchUrl;
5
7
  }
6
8
  export declare class OpenApiFileParserService implements OpenApiFileParser<OpenApiSpecType> {
7
9
  parse(input: unknown): OpenApiSpecType;
@@ -1 +1 @@
1
- {"version":3,"file":"file-reader.service.d.ts","sourceRoot":"","sources":["../../../src/services/file-reader.service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACtF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGxD,qBAAa,qBAAsB,YAAW,iBAAiB;IACvD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAgB9C;AAED,qBAAa,wBAAyB,YAAW,iBAAiB,CAAC,eAAe,CAAC;IACjF,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe;CAgBvC"}
1
+ {"version":3,"file":"file-reader.service.d.ts","sourceRoot":"","sources":["../../../src/services/file-reader.service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACtF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGxD,qBAAa,qBAAsB,YAAW,iBAAiB;IAC7D,OAAO,CAAC,KAAK;IASP,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAQ/B,QAAQ;CAkBvB;AAED,qBAAa,wBAAyB,YAAW,iBAAiB,CAAC,eAAe,CAAC;IACjF,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe;CAgBvC"}
@@ -2,21 +2,35 @@ import { load } from 'js-yaml';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { OpenApiSpec } from '../types/openapi.js';
4
4
  export class SyncFileReaderService {
5
- async readFile(path) {
6
- // Check if path is a URL
5
+ isUrl(path) {
7
6
  try {
8
7
  const url = new URL(path);
9
- // If it's a valid URL, fetch it
10
- const response = await fetch(url.toString());
11
- if (!response.ok) {
12
- throw new Error(`Failed to fetch ${path}: ${String(response.status)} ${response.statusText}`);
13
- }
14
- return await response.text();
8
+ return url.protocol === 'http:' || url.protocol === 'https:';
15
9
  }
16
10
  catch {
17
- // If URL parsing fails, treat it as a local file path
18
- return readFileSync(path, 'utf8');
11
+ return false;
12
+ }
13
+ }
14
+ async readFile(path) {
15
+ if (this.isUrl(path)) {
16
+ return await this.fetchUrl(path);
17
+ }
18
+ return readFileSync(path, 'utf8');
19
+ }
20
+ async fetchUrl(path) {
21
+ const url = new URL(path);
22
+ const headers = {};
23
+ if (url.username || url.password) {
24
+ const credentials = `${decodeURIComponent(url.username)}:${decodeURIComponent(url.password)}`;
25
+ headers['Authorization'] = `Basic ${btoa(credentials)}`;
26
+ url.username = '';
27
+ url.password = '';
28
+ }
29
+ const response = await fetch(url.toString(), { headers });
30
+ if (!response.ok) {
31
+ throw new Error(`Failed to fetch ${path}: ${String(response.status)} ${response.statusText}`);
19
32
  }
33
+ return await response.text();
20
34
  }
21
35
  }
22
36
  export class OpenApiFileParserService {
@@ -4,6 +4,7 @@ export declare class SyncFileWriterService implements FileWriter {
4
4
  private readonly version;
5
5
  private readonly inputPath;
6
6
  constructor(name: string, version: string, inputPath: string);
7
+ private redactUrl;
7
8
  writeFile(filePath: string, content: string): void;
8
9
  resolveOutputPath(outputDir: string, fileName?: string): string;
9
10
  }
@@ -1 +1 @@
1
- {"version":3,"file":"file-writer.service.d.ts","sourceRoot":"","sources":["../../../src/services/file-writer.service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAE/D,qBAAa,qBAAsB,YAAW,UAAU;IAEpD,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAFT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM;IAGpC,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAqBlD,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,SAAW,GAAG,MAAM;CAGlE"}
1
+ {"version":3,"file":"file-writer.service.d.ts","sourceRoot":"","sources":["../../../src/services/file-writer.service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAE/D,qBAAa,qBAAsB,YAAW,UAAU;IAEpD,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAFT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM;IAGpC,OAAO,CAAC,SAAS;IAajB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAqBlD,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,SAAW,GAAG,MAAM;CAGlE"}
@@ -9,12 +9,25 @@ export class SyncFileWriterService {
9
9
  this.version = version;
10
10
  this.inputPath = inputPath;
11
11
  }
12
+ redactUrl(path) {
13
+ try {
14
+ const url = new URL(path);
15
+ if ((url.protocol === 'http:' || url.protocol === 'https:') && url.password) {
16
+ url.password = '***';
17
+ return url.toString();
18
+ }
19
+ }
20
+ catch {
21
+ // Not a URL, return as-is
22
+ }
23
+ return path;
24
+ }
12
25
  writeFile(filePath, content) {
13
26
  const generatedContent = [
14
27
  '// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.',
15
28
  `// Built with ${this.name}@${this.version}`,
16
29
  `// Latest edit: ${new Date().toUTCString()}`,
17
- `// Source file: ${this.inputPath}`,
30
+ `// Source file: ${this.redactUrl(this.inputPath)}`,
18
31
  '/* eslint-disable */',
19
32
  '// @ts-nocheck',
20
33
  '',
@@ -106,5 +106,18 @@ describe('CLI Comprehensive Integration', () => {
106
106
  const outputFile = resolve(testOutputDir, 'api.ts');
107
107
  expect(existsSync(outputFile)).toBe(true);
108
108
  });
109
+ it('should generate code from a remote URL', () => {
110
+ execSync(`node ./dist/src/cli.js --input https://petstore3.swagger.io/api/v3/openapi.json --output ${testOutputDir}`, {
111
+ encoding: 'utf-8',
112
+ cwd,
113
+ timeout: 30000
114
+ });
115
+ const outputFile = resolve(testOutputDir, 'api.ts');
116
+ expect(existsSync(outputFile)).toBe(true);
117
+ const content = readFileSync(outputFile, 'utf-8');
118
+ expect(content).toContain('export default class');
119
+ expect(content).toContain('class ResponseValidationError<T> extends Error');
120
+ expect(content).toContain('import { z }');
121
+ });
109
122
  });
110
123
  });
@@ -1,8 +1,7 @@
1
+ import { dirname, join } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
1
3
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
4
  import { OpenApiFileParserService, SyncFileReaderService } from '../../src/services/file-reader.service';
3
- import { join } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { dirname } from 'node:path';
6
5
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
6
  describe('SyncFileReaderService', () => {
8
7
  let reader;
@@ -25,7 +24,6 @@ describe('SyncFileReaderService', () => {
25
24
  await expect(reader.readFile('./non-existent-file.json')).rejects.toThrow();
26
25
  });
27
26
  it('should handle URLs', async () => {
28
- // Mock fetch for URL test
29
27
  const originalFetch = global.fetch;
30
28
  global.fetch = vi.fn().mockResolvedValue({
31
29
  ok: true,
@@ -33,6 +31,7 @@ describe('SyncFileReaderService', () => {
33
31
  });
34
32
  const content = await reader.readFile('https://example.com/openapi.json');
35
33
  expect(content).toBeTruthy();
34
+ expect(global.fetch).toHaveBeenCalledWith('https://example.com/openapi.json', { headers: {} });
36
35
  global.fetch = originalFetch;
37
36
  });
38
37
  it('should handle URL fetch errors with non-ok responses', async () => {
@@ -43,11 +42,40 @@ describe('SyncFileReaderService', () => {
43
42
  statusText: 'Not Found'
44
43
  });
45
44
  global.fetch = mockFetch;
46
- await expect(reader.readFile('https://example.com/not-found.json')).rejects.toThrow();
47
- // Verify fetch was called
48
- expect(mockFetch).toHaveBeenCalledWith('https://example.com/not-found.json');
45
+ await expect(reader.readFile('https://example.com/not-found.json')).rejects.toThrow('Failed to fetch');
46
+ global.fetch = originalFetch;
47
+ });
48
+ it('should propagate network errors for URLs instead of falling back to readFileSync', async () => {
49
+ const originalFetch = global.fetch;
50
+ global.fetch = vi.fn().mockRejectedValue(new TypeError('fetch failed'));
51
+ await expect(reader.readFile('https://example.com/openapi.json')).rejects.toThrow('fetch failed');
52
+ global.fetch = originalFetch;
53
+ });
54
+ it('should extract basic auth credentials from URL into Authorization header', async () => {
55
+ const originalFetch = global.fetch;
56
+ const mockFetch = vi.fn().mockResolvedValue({
57
+ ok: true,
58
+ text: async () => '{}'
59
+ });
60
+ global.fetch = mockFetch;
61
+ await reader.readFile('https://user:pass@example.com/openapi.json');
62
+ expect(mockFetch).toHaveBeenCalledWith('https://example.com/openapi.json', { headers: { Authorization: `Basic ${btoa('user:pass')}` } });
63
+ global.fetch = originalFetch;
64
+ });
65
+ it('should decode percent-encoded credentials from URL', async () => {
66
+ const originalFetch = global.fetch;
67
+ const mockFetch = vi.fn().mockResolvedValue({
68
+ ok: true,
69
+ text: async () => '{}'
70
+ });
71
+ global.fetch = mockFetch;
72
+ await reader.readFile('https://julien%2Bstaging%40saris.ai:qwerty123@api.staging.saris.ai/api/openapi.json');
73
+ expect(mockFetch).toHaveBeenCalledWith('https://api.staging.saris.ai/api/openapi.json', { headers: { Authorization: `Basic ${btoa('julien+staging@saris.ai:qwerty123')}` } });
49
74
  global.fetch = originalFetch;
50
75
  });
76
+ it('should not treat non-http schemes as URLs', async () => {
77
+ await expect(reader.readFile('file:///etc/passwd')).rejects.toThrow();
78
+ });
51
79
  });
52
80
  });
53
81
  describe('OpenApiFileParserService', () => {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=file-writer.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-writer.test.d.ts","sourceRoot":"","sources":["../../../tests/unit/file-writer.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,53 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { SyncFileWriterService } from '../../src/services/file-writer.service';
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const testOutputDir = join(__dirname, '../../test-output-writer');
8
+ describe('SyncFileWriterService', () => {
9
+ beforeEach(() => {
10
+ if (existsSync(testOutputDir)) {
11
+ rmSync(testOutputDir, { recursive: true, force: true });
12
+ }
13
+ mkdirSync(testOutputDir, { recursive: true });
14
+ });
15
+ afterEach(() => {
16
+ if (existsSync(testOutputDir)) {
17
+ rmSync(testOutputDir, { recursive: true, force: true });
18
+ }
19
+ });
20
+ describe('credential redaction', () => {
21
+ it('should redact password from URL with embedded credentials', () => {
22
+ const writer = new SyncFileWriterService('zod-codegen', '1.0.0', 'https://user:secretpass@api.example.com/openapi.json');
23
+ const outPath = join(testOutputDir, 'api.ts');
24
+ writer.writeFile(outPath, 'const x = 1;');
25
+ const content = readFileSync(outPath, 'utf-8');
26
+ expect(content).toContain('// Source file: https://user:***@api.example.com/openapi.json');
27
+ expect(content).not.toContain('secretpass');
28
+ });
29
+ it('should redact password from URL with percent-encoded credentials', () => {
30
+ const writer = new SyncFileWriterService('zod-codegen', '1.0.0', 'https://api%2Bstaging%40saris.ai:hnt.xjx3rby8YKM8wnm@api.staging.saris.ai/api/openapi.json');
31
+ const outPath = join(testOutputDir, 'api.ts');
32
+ writer.writeFile(outPath, 'const x = 1;');
33
+ const content = readFileSync(outPath, 'utf-8');
34
+ expect(content).toContain('api%2Bstaging%40saris.ai');
35
+ expect(content).toContain(':***@');
36
+ expect(content).not.toContain('hnt.xjx3rby8YKM8wnm');
37
+ });
38
+ it('should leave URL without credentials unchanged', () => {
39
+ const writer = new SyncFileWriterService('zod-codegen', '1.0.0', 'https://api.example.com/openapi.json');
40
+ const outPath = join(testOutputDir, 'api.ts');
41
+ writer.writeFile(outPath, 'const x = 1;');
42
+ const content = readFileSync(outPath, 'utf-8');
43
+ expect(content).toContain('// Source file: https://api.example.com/openapi.json');
44
+ });
45
+ it('should leave local file paths unchanged', () => {
46
+ const writer = new SyncFileWriterService('zod-codegen', '1.0.0', './samples/openapi.json');
47
+ const outPath = join(testOutputDir, 'api.ts');
48
+ writer.writeFile(outPath, 'const x = 1;');
49
+ const content = readFileSync(outPath, 'utf-8');
50
+ expect(content).toContain('// Source file: ./samples/openapi.json');
51
+ });
52
+ });
53
+ });
package/package.json CHANGED
@@ -101,5 +101,5 @@
101
101
  "release": "semantic-release",
102
102
  "release:dry": "semantic-release --dry-run"
103
103
  },
104
- "version": "1.7.0"
104
+ "version": "1.7.2"
105
105
  }
@@ -5,21 +5,40 @@ import type { OpenApiSpecType } from '../types/openapi';
5
5
  import { OpenApiSpec } from '../types/openapi';
6
6
 
7
7
  export class SyncFileReaderService implements OpenApiFileReader {
8
- async readFile(path: string): Promise<string> {
9
- // Check if path is a URL
8
+ private isUrl(path: string): boolean {
10
9
  try {
11
10
  const url = new URL(path);
12
- // If it's a valid URL, fetch it
13
- const response = await fetch(url.toString());
14
- if (!response.ok) {
15
- throw new Error(`Failed to fetch ${path}: ${String(response.status)} ${response.statusText}`);
16
- }
17
-
18
- return await response.text();
11
+ return url.protocol === 'http:' || url.protocol === 'https:';
19
12
  } catch {
20
- // If URL parsing fails, treat it as a local file path
21
- return readFileSync(path, 'utf8');
13
+ return false;
14
+ }
15
+ }
16
+
17
+ async readFile(path: string): Promise<string> {
18
+ if (this.isUrl(path)) {
19
+ return await this.fetchUrl(path);
20
+ }
21
+
22
+ return readFileSync(path, 'utf8');
23
+ }
24
+
25
+ private async fetchUrl(path: string): Promise<string> {
26
+ const url = new URL(path);
27
+
28
+ const headers: Record<string, string> = {};
29
+ if (url.username || url.password) {
30
+ const credentials = `${decodeURIComponent(url.username)}:${decodeURIComponent(url.password)}`;
31
+ headers['Authorization'] = `Basic ${btoa(credentials)}`;
32
+ url.username = '';
33
+ url.password = '';
22
34
  }
35
+
36
+ const response = await fetch(url.toString(), { headers });
37
+ if (!response.ok) {
38
+ throw new Error(`Failed to fetch ${path}: ${String(response.status)} ${response.statusText}`);
39
+ }
40
+
41
+ return await response.text();
23
42
  }
24
43
  }
25
44
 
@@ -9,12 +9,25 @@ export class SyncFileWriterService implements FileWriter {
9
9
  private readonly inputPath: string
10
10
  ) {}
11
11
 
12
+ private redactUrl(path: string): string {
13
+ try {
14
+ const url = new URL(path);
15
+ if ((url.protocol === 'http:' || url.protocol === 'https:') && url.password) {
16
+ url.password = '***';
17
+ return url.toString();
18
+ }
19
+ } catch {
20
+ // Not a URL, return as-is
21
+ }
22
+ return path;
23
+ }
24
+
12
25
  writeFile(filePath: string, content: string): void {
13
26
  const generatedContent = [
14
27
  '// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.',
15
28
  `// Built with ${this.name}@${this.version}`,
16
29
  `// Latest edit: ${new Date().toUTCString()}`,
17
- `// Source file: ${this.inputPath}`,
30
+ `// Source file: ${this.redactUrl(this.inputPath)}`,
18
31
  '/* eslint-disable */',
19
32
  '// @ts-nocheck',
20
33
  '',
@@ -125,5 +125,21 @@ describe('CLI Comprehensive Integration', () => {
125
125
  const outputFile = resolve(testOutputDir, 'api.ts');
126
126
  expect(existsSync(outputFile)).toBe(true);
127
127
  });
128
+
129
+ it('should generate code from a remote URL', () => {
130
+ execSync(`node ./dist/src/cli.js --input https://petstore3.swagger.io/api/v3/openapi.json --output ${testOutputDir}`, {
131
+ encoding: 'utf-8',
132
+ cwd,
133
+ timeout: 30000
134
+ });
135
+
136
+ const outputFile = resolve(testOutputDir, 'api.ts');
137
+ expect(existsSync(outputFile)).toBe(true);
138
+
139
+ const content = readFileSync(outputFile, 'utf-8');
140
+ expect(content).toContain('export default class');
141
+ expect(content).toContain('class ResponseValidationError<T> extends Error');
142
+ expect(content).toContain('import { z }');
143
+ });
128
144
  });
129
145
  });
@@ -1,8 +1,7 @@
1
+ import { dirname, join } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
1
3
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
4
  import { OpenApiFileParserService, SyncFileReaderService } from '../../src/services/file-reader.service';
3
- import { join } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { dirname } from 'node:path';
6
5
 
7
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
7
 
@@ -32,7 +31,6 @@ describe('SyncFileReaderService', () => {
32
31
  });
33
32
 
34
33
  it('should handle URLs', async () => {
35
- // Mock fetch for URL test
36
34
  const originalFetch = global.fetch;
37
35
  global.fetch = vi.fn().mockResolvedValue({
38
36
  ok: true,
@@ -41,6 +39,7 @@ describe('SyncFileReaderService', () => {
41
39
 
42
40
  const content = await reader.readFile('https://example.com/openapi.json');
43
41
  expect(content).toBeTruthy();
42
+ expect(global.fetch).toHaveBeenCalledWith('https://example.com/openapi.json', { headers: {} });
44
43
 
45
44
  global.fetch = originalFetch;
46
45
  });
@@ -54,12 +53,53 @@ describe('SyncFileReaderService', () => {
54
53
  } as Response);
55
54
  global.fetch = mockFetch;
56
55
 
57
- await expect(reader.readFile('https://example.com/not-found.json')).rejects.toThrow();
56
+ await expect(reader.readFile('https://example.com/not-found.json')).rejects.toThrow('Failed to fetch');
57
+
58
+ global.fetch = originalFetch;
59
+ });
60
+
61
+ it('should propagate network errors for URLs instead of falling back to readFileSync', async () => {
62
+ const originalFetch = global.fetch;
63
+ global.fetch = vi.fn().mockRejectedValue(new TypeError('fetch failed'));
64
+
65
+ await expect(reader.readFile('https://example.com/openapi.json')).rejects.toThrow('fetch failed');
66
+
67
+ global.fetch = originalFetch;
68
+ });
69
+
70
+ it('should extract basic auth credentials from URL into Authorization header', async () => {
71
+ const originalFetch = global.fetch;
72
+ const mockFetch = vi.fn().mockResolvedValue({
73
+ ok: true,
74
+ text: async () => '{}'
75
+ } as Response);
76
+ global.fetch = mockFetch;
77
+
78
+ await reader.readFile('https://user:pass@example.com/openapi.json');
79
+
80
+ expect(mockFetch).toHaveBeenCalledWith('https://example.com/openapi.json', { headers: { Authorization: `Basic ${btoa('user:pass')}` } });
81
+
82
+ global.fetch = originalFetch;
83
+ });
84
+
85
+ it('should decode percent-encoded credentials from URL', async () => {
86
+ const originalFetch = global.fetch;
87
+ const mockFetch = vi.fn().mockResolvedValue({
88
+ ok: true,
89
+ text: async () => '{}'
90
+ } as Response);
91
+ global.fetch = mockFetch;
92
+
93
+ await reader.readFile('https://julien%2Bstaging%40saris.ai:qwerty123@api.staging.saris.ai/api/openapi.json');
94
+
95
+ expect(mockFetch).toHaveBeenCalledWith('https://api.staging.saris.ai/api/openapi.json', { headers: { Authorization: `Basic ${btoa('julien+staging@saris.ai:qwerty123')}` } });
58
96
 
59
- // Verify fetch was called
60
- expect(mockFetch).toHaveBeenCalledWith('https://example.com/not-found.json');
61
97
  global.fetch = originalFetch;
62
98
  });
99
+
100
+ it('should not treat non-http schemes as URLs', async () => {
101
+ await expect(reader.readFile('file:///etc/passwd')).rejects.toThrow();
102
+ });
63
103
  });
64
104
  });
65
105
 
@@ -0,0 +1,69 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { SyncFileWriterService } from '../../src/services/file-writer.service';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const testOutputDir = join(__dirname, '../../test-output-writer');
9
+
10
+ describe('SyncFileWriterService', () => {
11
+ beforeEach(() => {
12
+ if (existsSync(testOutputDir)) {
13
+ rmSync(testOutputDir, { recursive: true, force: true });
14
+ }
15
+
16
+ mkdirSync(testOutputDir, { recursive: true });
17
+ });
18
+
19
+ afterEach(() => {
20
+ if (existsSync(testOutputDir)) {
21
+ rmSync(testOutputDir, { recursive: true, force: true });
22
+ }
23
+ });
24
+
25
+ describe('credential redaction', () => {
26
+ it('should redact password from URL with embedded credentials', () => {
27
+ const writer = new SyncFileWriterService('zod-codegen', '1.0.0', 'https://user:secretpass@api.example.com/openapi.json');
28
+
29
+ const outPath = join(testOutputDir, 'api.ts');
30
+ writer.writeFile(outPath, 'const x = 1;');
31
+
32
+ const content = readFileSync(outPath, 'utf-8');
33
+ expect(content).toContain('// Source file: https://user:***@api.example.com/openapi.json');
34
+ expect(content).not.toContain('secretpass');
35
+ });
36
+
37
+ it('should redact password from URL with percent-encoded credentials', () => {
38
+ const writer = new SyncFileWriterService('zod-codegen', '1.0.0', 'https://api%2Bstaging%40saris.ai:hnt.xjx3rby8YKM8wnm@api.staging.saris.ai/api/openapi.json');
39
+
40
+ const outPath = join(testOutputDir, 'api.ts');
41
+ writer.writeFile(outPath, 'const x = 1;');
42
+
43
+ const content = readFileSync(outPath, 'utf-8');
44
+ expect(content).toContain('api%2Bstaging%40saris.ai');
45
+ expect(content).toContain(':***@');
46
+ expect(content).not.toContain('hnt.xjx3rby8YKM8wnm');
47
+ });
48
+
49
+ it('should leave URL without credentials unchanged', () => {
50
+ const writer = new SyncFileWriterService('zod-codegen', '1.0.0', 'https://api.example.com/openapi.json');
51
+
52
+ const outPath = join(testOutputDir, 'api.ts');
53
+ writer.writeFile(outPath, 'const x = 1;');
54
+
55
+ const content = readFileSync(outPath, 'utf-8');
56
+ expect(content).toContain('// Source file: https://api.example.com/openapi.json');
57
+ });
58
+
59
+ it('should leave local file paths unchanged', () => {
60
+ const writer = new SyncFileWriterService('zod-codegen', '1.0.0', './samples/openapi.json');
61
+
62
+ const outPath = join(testOutputDir, 'api.ts');
63
+ writer.writeFile(outPath, 'const x = 1;');
64
+
65
+ const content = readFileSync(outPath, 'utf-8');
66
+ expect(content).toContain('// Source file: ./samples/openapi.json');
67
+ });
68
+ });
69
+ });