@1nder-labs/schemalint 1.0.1

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 (46) hide show
  1. package/LICENSE-APACHE +176 -0
  2. package/LICENSE-MIT +19 -0
  3. package/README.md +52 -0
  4. package/bin/schemalint-zod.js +6 -0
  5. package/dist/discover.d.ts +43 -0
  6. package/dist/discover.d.ts.map +1 -0
  7. package/dist/discover.js +145 -0
  8. package/dist/discover.js.map +1 -0
  9. package/dist/discover_ast.d.ts +30 -0
  10. package/dist/discover_ast.d.ts.map +1 -0
  11. package/dist/discover_ast.js +202 -0
  12. package/dist/discover_ast.js.map +1 -0
  13. package/dist/evaluate.d.ts +18 -0
  14. package/dist/evaluate.d.ts.map +1 -0
  15. package/dist/evaluate.js +112 -0
  16. package/dist/evaluate.js.map +1 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +2 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/main.d.ts +2 -0
  22. package/dist/main.d.ts.map +1 -0
  23. package/dist/main.js +3 -0
  24. package/dist/main.js.map +1 -0
  25. package/dist/server.d.ts +2 -0
  26. package/dist/server.d.ts.map +1 -0
  27. package/dist/server.js +82 -0
  28. package/dist/server.js.map +1 -0
  29. package/dist/target_emit.d.ts +12 -0
  30. package/dist/target_emit.d.ts.map +1 -0
  31. package/dist/target_emit.js +111 -0
  32. package/dist/target_emit.js.map +1 -0
  33. package/dist/target_imports.d.ts +19 -0
  34. package/dist/target_imports.d.ts.map +1 -0
  35. package/dist/target_imports.js +79 -0
  36. package/dist/target_imports.js.map +1 -0
  37. package/dist/target_resolution.d.ts +20 -0
  38. package/dist/target_resolution.d.ts.map +1 -0
  39. package/dist/target_resolution.js +174 -0
  40. package/dist/target_resolution.js.map +1 -0
  41. package/dist/targets.d.ts +5 -0
  42. package/dist/targets.d.ts.map +1 -0
  43. package/dist/targets.js +116 -0
  44. package/dist/targets.js.map +1 -0
  45. package/index.cjs +504 -0
  46. package/package.json +34 -0
package/index.cjs ADDED
@@ -0,0 +1,504 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { spawnSync } = require('child_process');
5
+ const crypto = require('crypto');
6
+ const fs = require('fs');
7
+ const https = require('https');
8
+ const os = require('os');
9
+ const path = require('path');
10
+
11
+ const VERSION = require('./package.json').version;
12
+ if (!VERSION) {
13
+ throw new Error('Missing or invalid version in package.json');
14
+ }
15
+ const REPO = '1nder-labs/schemalint';
16
+
17
+ // [P1 — resource exhaustion] Hard cap on bytes accepted from any single
18
+ // download. 200 MiB is generous for a CLI binary archive and its tiny
19
+ // SHA-256 sidecar; both go through downloadFile so the cap applies to both.
20
+ const MAX_DOWNLOAD_BYTES = 200 * 1024 * 1024;
21
+
22
+ // [P0 — supply chain] Allowlist of hosts permitted for downloads and redirects.
23
+ // Only GitHub release delivery hosts are trusted; any other host (including
24
+ // same-registrable-domain lookalikes) is rejected before a connection is made.
25
+ const ALLOWED_HOSTS = new Set([
26
+ 'github.com',
27
+ 'objects.githubusercontent.com',
28
+ 'release-assets.githubusercontent.com',
29
+ 'codeload.github.com',
30
+ ]);
31
+
32
+ const TARGET_MAP = {
33
+ 'darwin-x64': 'x86_64-apple-darwin',
34
+ 'darwin-arm64': 'aarch64-apple-darwin',
35
+ 'linux-x64': 'x86_64-unknown-linux-gnu',
36
+ 'linux-arm64': 'aarch64-unknown-linux-gnu',
37
+ 'win32-x64': 'x86_64-pc-windows-msvc',
38
+ };
39
+
40
+ const EXE_EXT = process.platform === 'win32' ? '.exe' : '';
41
+
42
+ function getTarget() {
43
+ const plat = `${process.platform}-${process.arch}`;
44
+ const target = TARGET_MAP[plat];
45
+ if (!target) {
46
+ throw new Error(
47
+ `Unsupported platform: ${plat}. Supported: ${Object.keys(TARGET_MAP).join(', ')}`
48
+ );
49
+ }
50
+ return target;
51
+ }
52
+
53
+ function getCacheDir() {
54
+ const dir = process.env.SCHEMALINT_CACHE_DIR
55
+ || path.join(os.homedir(), '.cache', 'schemalint-npm');
56
+ return path.join(dir, VERSION);
57
+ }
58
+
59
+ function getBinaryPath() {
60
+ const target = getTarget();
61
+ return path.join(getCacheDir(), target, `schemalint${EXE_EXT}`);
62
+ }
63
+
64
+ function sleep(ms) {
65
+ return new Promise((resolve) => setTimeout(resolve, ms));
66
+ }
67
+
68
+ /**
69
+ * Compute the SHA-256 hash of a file and return it as a lowercase hex string.
70
+ */
71
+ function sha256File(filePath) {
72
+ return new Promise((resolve, reject) => {
73
+ const hash = crypto.createHash('sha256');
74
+ const stream = fs.createReadStream(filePath);
75
+ stream.on('data', (chunk) => hash.update(chunk));
76
+ stream.on('end', () => resolve(hash.digest('hex')));
77
+ stream.on('error', reject);
78
+ });
79
+ }
80
+
81
+ /**
82
+ * [P0 — supply chain] Assert that a URL's hostname is in the allowed set.
83
+ * Parses with the WHATWG URL API so scheme, port, and hostname are all
84
+ * properly separated — substring/endsWith matching on the raw string is
85
+ * deliberately avoided to prevent `github.com.evil.com`-style bypasses.
86
+ * Throws for unparseable URLs or relative Location headers (fail closed).
87
+ */
88
+ function assertAllowedHost(url) {
89
+ let parsed;
90
+ try {
91
+ parsed = new URL(url);
92
+ } catch {
93
+ throw new Error(`Refusing download: could not parse URL: ${url}`);
94
+ }
95
+ if (parsed.protocol !== 'https:') {
96
+ throw new Error(`Refusing non-HTTPS URL: ${url}`);
97
+ }
98
+ if (!ALLOWED_HOSTS.has(parsed.hostname)) {
99
+ throw new Error(
100
+ `Refusing download from disallowed host "${parsed.hostname}". ` +
101
+ `Allowed hosts: ${[...ALLOWED_HOSTS].join(', ')}`
102
+ );
103
+ }
104
+ }
105
+
106
+ function downloadFile(url, destPath, depth = 0) {
107
+ // [P0 — supply chain] Validate initial URL and every redirect target before
108
+ // opening a connection. assertAllowedHost also enforces HTTPS, making the
109
+ // old ternary that conditionally required 'http' unnecessary.
110
+ assertAllowedHost(url);
111
+
112
+ return new Promise((resolve, reject) => {
113
+ const request = https.get(url, (response) => {
114
+ if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
115
+ response.resume();
116
+ if (depth > 5) {
117
+ reject(new Error('too many redirects'));
118
+ return;
119
+ }
120
+ // assertAllowedHost is called at the top of the recursive call, which
121
+ // covers both the https-only and host-allowlist checks for the redirect.
122
+ // The try/catch converts any synchronous throw (e.g. disallowed host,
123
+ // unparseable Location) into a rejection rather than an uncaught exception
124
+ // escaping the response callback.
125
+ try {
126
+ downloadFile(response.headers.location, destPath, depth + 1).then(resolve).catch(reject);
127
+ } catch (e) {
128
+ reject(e);
129
+ }
130
+ return;
131
+ }
132
+ if (response.statusCode !== 200) {
133
+ response.resume();
134
+ reject(new Error(`HTTP ${response.statusCode}`));
135
+ return;
136
+ }
137
+
138
+ // [P1 — resource exhaustion] Fast-reject on Content-Length header when
139
+ // present. The header can be absent or lie, so we also enforce the cap
140
+ // on actual bytes received below.
141
+ const contentLength = parseInt(response.headers['content-length'] || '0', 10);
142
+ if (contentLength > MAX_DOWNLOAD_BYTES) {
143
+ response.destroy();
144
+ fs.unlink(destPath, () => {});
145
+ reject(new Error(
146
+ `Download refused: Content-Length ${contentLength} exceeds ` +
147
+ `the ${MAX_DOWNLOAD_BYTES}-byte cap`
148
+ ));
149
+ return;
150
+ }
151
+
152
+ // [P1 — resource exhaustion] Track cumulative bytes received. If the
153
+ // server lies about Content-Length (or omits it) and streams more data
154
+ // than the cap allows, destroy the request and reject.
155
+ let bytesReceived = 0;
156
+ const file = fs.createWriteStream(destPath);
157
+
158
+ function abortOversized() {
159
+ request.destroy();
160
+ file.destroy();
161
+ fs.unlink(destPath, () => {});
162
+ reject(new Error(
163
+ `Download aborted: received more than ${MAX_DOWNLOAD_BYTES} bytes`
164
+ ));
165
+ }
166
+
167
+ response.on('data', (chunk) => {
168
+ bytesReceived += chunk.length;
169
+ if (bytesReceived > MAX_DOWNLOAD_BYTES) {
170
+ abortOversized();
171
+ }
172
+ });
173
+
174
+ response.pipe(file);
175
+ file.on('finish', () => {
176
+ file.close((err) => {
177
+ if (err) reject(err);
178
+ else resolve();
179
+ });
180
+ });
181
+ file.on('error', (err) => {
182
+ file.destroy();
183
+ fs.unlink(destPath, () => {});
184
+ reject(err);
185
+ });
186
+ response.on('error', (err) => {
187
+ file.destroy();
188
+ fs.unlink(destPath, () => {});
189
+ reject(err);
190
+ });
191
+ });
192
+ request.on('error', (err) => {
193
+ fs.unlink(destPath, () => {});
194
+ reject(err);
195
+ });
196
+ request.setTimeout(120000, () => {
197
+ request.destroy();
198
+ fs.unlink(destPath, () => {});
199
+ reject(new Error('Request timeout'));
200
+ });
201
+ });
202
+ }
203
+
204
+ /**
205
+ * Recursively search `dir` for a file named `binaryName`, up to `maxDepth`
206
+ * levels deep. Symlinked directories are NOT followed (Zip-Slip guard).
207
+ * Returns all matching file paths (not directories, not symlinks-to-files).
208
+ */
209
+ function findBinaryInDir(dir, binaryName, maxDepth, currentDepth = 0) {
210
+ const results = [];
211
+ let entries;
212
+ try {
213
+ entries = fs.readdirSync(dir, { withFileTypes: true });
214
+ } catch {
215
+ return results;
216
+ }
217
+ for (const entry of entries) {
218
+ const fullPath = path.join(dir, entry.name);
219
+ if (entry.isFile() && entry.name === binaryName) {
220
+ results.push(fullPath);
221
+ } else if (entry.isDirectory() && !entry.isSymbolicLink() && currentDepth < maxDepth) {
222
+ // entry.isSymbolicLink() is always false when using withFileTypes on a
223
+ // plain entry because isDirectory() and isSymbolicLink() are mutually
224
+ // exclusive for Dirent. Explicitly check via lstatSync for safety.
225
+ const stat = (() => { try { return fs.lstatSync(fullPath); } catch { return null; } })();
226
+ if (stat && stat.isDirectory() && !stat.isSymbolicLink()) {
227
+ results.push(...findBinaryInDir(fullPath, binaryName, maxDepth, currentDepth + 1));
228
+ }
229
+ }
230
+ }
231
+ return results;
232
+ }
233
+
234
+ // Monotonic counter to disambiguate temp dirs created within the same process
235
+ // during the same millisecond (rare but possible in test harnesses).
236
+ let _tmpCounter = 0;
237
+
238
+ async function ensureBinary() {
239
+ const binPath = getBinaryPath();
240
+ const sentinelPath = binPath + '.verified';
241
+
242
+ // [P0 — integrity] Cache-hit path: re-verify the cached binary against the
243
+ // SHA-256 recorded in the sentinel on every invocation. An empty, unreadable,
244
+ // or malformed sentinel is treated as a mismatch and triggers a fresh
245
+ // download+verify cycle.
246
+ if (fs.existsSync(binPath) && fs.existsSync(sentinelPath)) {
247
+ let recordedHash = '';
248
+ try {
249
+ recordedHash = fs.readFileSync(sentinelPath, 'utf8').trim();
250
+ } catch {
251
+ // Unreadable sentinel — treat as mismatch; fall through.
252
+ }
253
+ if (/^[0-9a-f]{64}$/.test(recordedHash)) {
254
+ let actualHash = '';
255
+ try {
256
+ actualHash = await sha256File(binPath);
257
+ } catch {
258
+ // Unreadable binary — fall through to fresh download.
259
+ }
260
+ if (actualHash === recordedHash) {
261
+ return binPath;
262
+ }
263
+ // Hash mismatch or unreadable binary: wipe both so the clean-slate
264
+ // unlinks below complete a tidy state before re-downloading.
265
+ }
266
+ // Sentinel absent, empty, malformed, or hash mismatch — fall through.
267
+ }
268
+
269
+ // Cache miss or failed integrity check — clean slate.
270
+ try { fs.unlinkSync(sentinelPath); } catch {}
271
+ try { fs.unlinkSync(binPath); } catch {}
272
+
273
+ const target = getTarget();
274
+ const ext = process.platform === 'win32' ? '.zip' : '.tar.gz';
275
+ const archiveName = `schemalint-${target}${ext}`;
276
+ const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${archiveName}`;
277
+ const cacheDir = path.dirname(binPath);
278
+
279
+ // Ensure the final destination directory exists before the temp-dir sibling
280
+ // is created (both share the same parent, so mkdirSync is only called once).
281
+ fs.mkdirSync(cacheDir, { recursive: true });
282
+
283
+ // [Concurrency] Use a unique temp work directory that is a SIBLING of
284
+ // cacheDir (same parent → same filesystem), so the final fs.renameSync into
285
+ // binPath is a within-filesystem atomic operation (never EXDEV).
286
+ // The pid + monotonic counter + random suffix make collisions practically
287
+ // impossible even under a test harness that spawns many processes rapidly.
288
+ const randomSuffix = crypto.randomBytes(4).toString('hex');
289
+ const workDir = path.join(
290
+ path.dirname(cacheDir),
291
+ `schemalint-tmp.${process.pid}.${++_tmpCounter}.${randomSuffix}`
292
+ );
293
+ fs.mkdirSync(workDir, { recursive: true });
294
+
295
+ try {
296
+ // All temp artifacts (archive, checksum, extracted tree) live inside
297
+ // workDir so that a crashed/killed process never leaves partial state in
298
+ // cacheDir visible to a concurrent healthy process.
299
+ const archivePath = path.join(workDir, archiveName);
300
+
301
+ // Download with retry (exponential backoff, max 3 attempts).
302
+ let lastError = null;
303
+ for (let attempt = 1; attempt <= 3; attempt++) {
304
+ try {
305
+ await downloadFile(url, archivePath);
306
+ lastError = null;
307
+ break;
308
+ } catch (e) {
309
+ lastError = e.message;
310
+ if (attempt < 3) {
311
+ try { fs.unlinkSync(archivePath); } catch {}
312
+ const delayMs = Math.pow(2, attempt) * 1000;
313
+ await sleep(delayMs);
314
+ }
315
+ }
316
+ }
317
+
318
+ if (lastError !== null) {
319
+ throw new Error(
320
+ `Failed to download schemalint binary from ${url}: ${lastError}. ` +
321
+ `Make sure the GitHub Release for v${VERSION} exists.`
322
+ );
323
+ }
324
+
325
+ // Verify archive integrity against the published per-artifact SHA-256
326
+ // checksum. cargo-dist (v0.31.0) emits a <archive>.sha256 sidecar for
327
+ // every archive and uploads it alongside the archive in the GitHub Release.
328
+ const checksumUrl = `${url}.sha256`;
329
+ const checksumPath = archivePath + '.sha256';
330
+ try {
331
+ // Download checksum with retry (same backoff shape as archive download).
332
+ let checksumLastError = null;
333
+ for (let attempt = 1; attempt <= 3; attempt++) {
334
+ try {
335
+ await downloadFile(checksumUrl, checksumPath);
336
+ checksumLastError = null;
337
+ break;
338
+ } catch (e) {
339
+ checksumLastError = e.message;
340
+ if (attempt < 3) {
341
+ try { fs.unlinkSync(checksumPath); } catch {}
342
+ const delayMs = Math.pow(2, attempt) * 1000;
343
+ await sleep(delayMs);
344
+ }
345
+ }
346
+ }
347
+ if (checksumLastError !== null) {
348
+ throw new Error(
349
+ `Failed to download checksum from ${checksumUrl}: ${checksumLastError}`
350
+ );
351
+ }
352
+
353
+ const checksumContent = fs.readFileSync(checksumPath, 'utf8');
354
+ // Handles both bare-hash and "hash filename" formats.
355
+ const expectedHash = checksumContent.trim().split(/\s+/)[0].toLowerCase();
356
+ const actualHash = await sha256File(archivePath);
357
+ if (actualHash !== expectedHash) {
358
+ throw new Error(
359
+ `SHA-256 mismatch for ${archiveName}: expected ${expectedHash}, got ${actualHash}`
360
+ );
361
+ }
362
+ } finally {
363
+ // Always remove the checksum file — it is ephemeral and must not linger.
364
+ try { fs.unlinkSync(checksumPath); } catch {}
365
+ }
366
+
367
+ // Extract into workDir. Every platform extracts to the same isolated
368
+ // temp tree; the archive is then deleted, and the binary is searched for
369
+ // within that tree regardless of how cargo-dist laid out the subdirectory.
370
+ try {
371
+ if (process.platform === 'win32') {
372
+ const result = spawnSync(
373
+ 'powershell',
374
+ ['-Command', 'Expand-Archive', '-Path', archivePath, '-DestinationPath', workDir],
375
+ { stdio: 'pipe' }
376
+ );
377
+ if (result.error) throw result.error;
378
+ if (result.status !== 0) throw new Error(result.stderr.toString().trim());
379
+ } else {
380
+ const result = spawnSync(
381
+ 'tar',
382
+ ['-xzf', archivePath, '-C', workDir],
383
+ { stdio: 'pipe' }
384
+ );
385
+ if (result.error) throw result.error;
386
+ if (result.status !== 0) throw new Error(result.stderr.toString().trim());
387
+ }
388
+ } catch (e) {
389
+ const reason = e.stderr ? e.stderr.toString().trim() : e.message;
390
+ throw new Error(`Failed to extract schemalint binary: ${reason}`);
391
+ }
392
+
393
+ // Archive is no longer needed once extraction succeeds.
394
+ try { fs.unlinkSync(archivePath); } catch {}
395
+
396
+ // [P0 — layout-agnostic binary location] cargo-dist 0.31 archives may
397
+ // place the binary at the archive root OR inside a `schemalint-<target>/`
398
+ // subdirectory. Search workDir recursively (bounded to depth 3) for a file
399
+ // named `schemalint[.exe]`, then enforce exactly one match.
400
+ const binaryName = `schemalint${EXE_EXT}`;
401
+ const candidates = findBinaryInDir(workDir, binaryName, 3);
402
+
403
+ if (candidates.length === 0) {
404
+ throw new Error(
405
+ `Binary "${binaryName}" not found anywhere inside the extracted archive. ` +
406
+ `Searched up to depth 3 in ${workDir}.`
407
+ );
408
+ }
409
+ if (candidates.length > 1) {
410
+ throw new Error(
411
+ `Ambiguous extraction: found ${candidates.length} files named "${binaryName}" ` +
412
+ `inside the archive: ${candidates.join(', ')}`
413
+ );
414
+ }
415
+
416
+ const foundPath = candidates[0];
417
+
418
+ // [P0 — path traversal] Resolve the found path through realpathSync and
419
+ // verify it is inside workDir before trusting it. This guards against
420
+ // Tar-Slip / Zip-Slip attacks that place a symlink pointing outside the
421
+ // extraction directory. Note: GNU tar `--no-absolute-filenames` is
422
+ // deliberately NOT used because macOS ships bsdtar which does not
423
+ // recognise that long option. The realpath containment check is the
424
+ // portable guard for both tar and Expand-Archive paths.
425
+ let realFound;
426
+ try {
427
+ realFound = fs.realpathSync(foundPath);
428
+ } catch (e) {
429
+ throw new Error(`Binary not accessible after extraction: ${e.message}`);
430
+ }
431
+ const realWorkDir = fs.realpathSync(workDir);
432
+ if (!realFound.startsWith(realWorkDir + path.sep)) {
433
+ throw new Error(
434
+ `Path traversal detected: extracted binary "${realFound}" escapes ` +
435
+ `work directory "${realWorkDir}"`
436
+ );
437
+ }
438
+
439
+ // [P0 — defense in depth] Hard-link guard. A legitimate cargo-dist binary
440
+ // extracted from a freshly-downloaded archive always has nlink === 1 (a
441
+ // single directory entry pointing to the inode). A file with nlink > 1 is
442
+ // either a tar hard-link member aliasing another path in the archive, or an
443
+ // attempt by a compromised archive to alias an existing on-disk file. The
444
+ // archive has already been SHA-256-verified, so this is defense-in-depth,
445
+ // but it is cheap and closes the edge case.
446
+ const lstat = fs.lstatSync(realFound);
447
+ if (lstat.nlink > 1) {
448
+ try { fs.unlinkSync(realFound); } catch {}
449
+ throw new Error(
450
+ `Hard-link detected: extracted binary "${realFound}" has nlink=${lstat.nlink} ` +
451
+ `(expected 1). Archive rejected as potentially unsafe.`
452
+ );
453
+ }
454
+
455
+ // Make executable on Unix before moving into place.
456
+ if (process.platform !== 'win32') {
457
+ fs.chmodSync(realFound, 0o755);
458
+ }
459
+
460
+ // [Concurrency] Atomically rename the verified binary into its final
461
+ // location. Because workDir is a sibling of cacheDir (same filesystem),
462
+ // renameSync is always within-device and therefore atomic. A racing process
463
+ // that already placed binPath will simply have its file replaced by an
464
+ // equally valid binary — both came from the same verified archive.
465
+ // The sentinel is written AFTER the rename, so the cache-hit path (which
466
+ // requires both binPath and sentinelPath to exist) can never observe a
467
+ // partial state left by a crash between the two writes.
468
+ fs.renameSync(realFound, binPath);
469
+
470
+ // [P0 — integrity] Write the SHA-256 of the installed binary into the
471
+ // sentinel. On future cache hits this hash is re-verified against the live
472
+ // binary before the cached path is returned, ensuring a tampered binary is
473
+ // caught on the very next invocation.
474
+ const binaryHash = await sha256File(binPath);
475
+ fs.writeFileSync(sentinelPath, binaryHash);
476
+
477
+ } finally {
478
+ // Always clean up the temp work directory, even on error. This removes
479
+ // any partially extracted or partially downloaded artifacts so a failed
480
+ // run never leaves corrupt state visible to concurrent or subsequent runs.
481
+ try { fs.rmSync(workDir, { recursive: true, force: true }); } catch {}
482
+ }
483
+
484
+ return binPath;
485
+ }
486
+
487
+ (async () => {
488
+ try {
489
+ const binPath = await ensureBinary();
490
+ const result = spawnSync(binPath, process.argv.slice(2), {
491
+ stdio: 'inherit',
492
+ env: {
493
+ ...process.env,
494
+ SCHEMALINT_ZOD_HELPER:
495
+ process.env.SCHEMALINT_ZOD_HELPER ||
496
+ path.join(__dirname, 'bin', 'schemalint-zod.js'),
497
+ },
498
+ });
499
+ process.exit(result.status ?? 1);
500
+ } catch (err) {
501
+ console.error(`schemalint: ${err.message}`);
502
+ process.exit(1);
503
+ }
504
+ })();
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@1nder-labs/schemalint",
3
+ "version": "1.0.1",
4
+ "description": "Lint JSON Schema and Zod schemas before OpenAI or Anthropic structured-output APIs reject them.",
5
+ "license": "MIT OR Apache-2.0",
6
+ "author": "Lahfir",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/1nder-labs/schemalint.git",
10
+ "directory": "npm/schemalint"
11
+ },
12
+ "homepage": "https://1nder-labs.github.io/schemalint",
13
+ "bugs": { "url": "https://github.com/1nder-labs/schemalint/issues" },
14
+ "keywords": ["json-schema", "zod", "openai", "anthropic", "structured-output", "llm", "schema", "lint", "validation", "pydantic"],
15
+ "engines": { "node": ">=18" },
16
+ "type": "module",
17
+ "bin": { "schemalint": "./index.cjs" },
18
+ "files": ["index.cjs", "dist/", "bin/", "LICENSE-MIT", "LICENSE-APACHE"],
19
+ "dependencies": { "picomatch": "^4.0.0", "zod-to-json-schema": "^3.24.0" },
20
+ "peerDependencies": { "typescript": ">=4.9", "zod": ">=3.20" },
21
+ "peerDependenciesMeta": {
22
+ "typescript": { "optional": true },
23
+ "zod": { "optional": true }
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^25.6.0",
27
+ "tsx": "^4.21.0",
28
+ "typescript": "^5.5.0",
29
+ "vitest": "^2.0.0",
30
+ "zod": "^3.23.0"
31
+ },
32
+ "scripts": { "build": "tsc", "test": "vitest run" },
33
+ "publishConfig": { "access": "public" }
34
+ }