Skip to content

Commit c825b05

Browse files
committed
add insecure resolver example
1 parent 6e19cd9 commit c825b05

File tree

2 files changed

+282
-0
lines changed

2 files changed

+282
-0
lines changed

AGENTS.md

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# AGENTS.md
2+
3+
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
4+
5+
## Development Commands
6+
7+
### Building
8+
9+
```bash
10+
yarn build # Compile TypeScript to dist/
11+
yarn typecheck # Run TypeScript type checking without emitting files
12+
```
13+
14+
### Testing
15+
16+
```bash
17+
yarn test # Run all tests with coverage (Vitest)
18+
yarn test:node # Run tests in Node.js environment
19+
yarn test:browser # Run tests in browser environment (jsdom)
20+
yarn test:watch # Run tests in watch mode
21+
yarn test:update # Update test snapshots
22+
```
23+
24+
### Code Quality
25+
26+
```bash
27+
yarn lint # Run ESLint on lib/ directory
28+
yarn prettier # Format all code files
29+
```
30+
31+
### Running Individual Tests
32+
33+
To run a single test file:
34+
35+
```bash
36+
npx vitest test/specs/circular/circular.spec.ts
37+
```
38+
39+
To run tests matching a pattern:
40+
41+
```bash
42+
npx vitest --grep "circular"
43+
```
44+
45+
## Architecture Overview
46+
47+
### Core Purpose
48+
49+
This library parses, resolves, and dereferences JSON Schema `$ref` pointers. It handles references to:
50+
51+
- External files (local filesystem)
52+
- HTTP/HTTPS URLs
53+
- Internal JSON pointers within schemas
54+
- Mixed JSON and YAML formats
55+
- Circular/recursive references
56+
57+
### Key Architecture Components
58+
59+
#### 1. $RefParser (lib/index.ts)
60+
61+
The main entry point and orchestrator class. Provides four primary operations:
62+
63+
- **parse()**: Reads a single schema file (JSON/YAML) without resolving references
64+
- **resolve()**: Parses and resolves all `$ref` pointers, returns a `$Refs` object mapping references to values
65+
- **bundle()**: Converts external `$ref` pointers to internal ones (single file output)
66+
- **dereference()**: Replaces all `$ref` pointers with their actual values (fully expanded schema)
67+
68+
All methods support both callback and Promise-based APIs, with multiple overload signatures.
69+
70+
#### 2. $Refs (lib/refs.ts)
71+
72+
A map/registry of all resolved JSON references and their values. Tracks:
73+
74+
- All file paths/URLs encountered
75+
- Circular reference detection
76+
- Helper methods to query references by type (file, http, etc.)
77+
78+
#### 3. Pointer (lib/pointer.ts)
79+
80+
Represents a single JSON pointer (`#/definitions/person`) and implements JSON Pointer RFC 6901 spec:
81+
82+
- Parses JSON pointer syntax (`/`, `~0`, `~1` escaping)
83+
- Resolves pointers to actual values within objects
84+
- Handles edge cases (null values, missing properties)
85+
86+
#### 4. $Ref (lib/ref.ts)
87+
88+
Wraps a single reference with metadata:
89+
90+
- The reference path/URL
91+
- The resolved value
92+
- Path type (file, http, etc.)
93+
- Error information (when continueOnError is enabled)
94+
95+
#### 5. Plugin System
96+
97+
Two types of plugins, both configurable via options:
98+
99+
**Parsers** (lib/parsers/):
100+
101+
- JSON parser (json.ts)
102+
- YAML parser (yaml.ts) - uses js-yaml
103+
- Text parser (text.ts)
104+
- Binary parser (binary.ts)
105+
- Execute in order based on `order` property and `canParse()` matching
106+
107+
**Resolvers** (lib/resolvers/):
108+
109+
- File resolver (file.ts) - reads from filesystem (Node.js only)
110+
- HTTP resolver (http.ts) - fetches from URLs using native fetch
111+
- Custom resolvers can be added via options
112+
- Execute in order based on `order` property and `canRead()` matching
113+
114+
#### 6. Core Operations
115+
116+
**lib/parse.ts**: Entry point for parsing a single schema file
117+
**lib/resolve-external.ts**: Crawls schema to find and resolve external `$ref` pointers
118+
**lib/bundle.ts**: Replaces external refs with internal refs
119+
**lib/dereference.ts**: Replaces all `$ref` pointers with actual values, handles circular references
120+
121+
#### 7. Options System (lib/options.ts)
122+
123+
Hierarchical configuration with defaults for:
124+
125+
- Which parsers/resolvers to enable
126+
- Circular reference handling (boolean or "ignore")
127+
- External reference resolution (relative vs root)
128+
- Continue on error mode (collect all errors vs fail fast)
129+
- Bundle/dereference callbacks and matchers
130+
- Input mutation control (mutateInputSchema)
131+
132+
### Key Design Patterns
133+
134+
1. **Flexible Arguments**: normalizeArgs() (lib/normalize-args.ts) unifies various call signatures into consistent internal format
135+
136+
2. **Path Handling**: Automatic conversion between filesystem paths and file:// URLs. Cross-platform support via util/url.ts and util/convert-path-to-posix.ts
137+
138+
3. **Error Handling**:
139+
- Fail-fast by default
140+
- Optional continueOnError mode collects errors in JSONParserErrorGroup
141+
- Specific error types: JSONParserError, InvalidPointerError, MissingPointerError, ResolverError, ParserError
142+
143+
4. **Circular Reference Management**:
144+
- Detected during dereference
145+
- Can throw error, ignore, or handle via dereference.circular option
146+
- Reference equality maintained (same `$ref` → same object instance)
147+
148+
5. **Browser/Node Compatibility**:
149+
- Uses native fetch (requires Node 18+)
150+
- File resolver disabled in browser builds (package.json browser field)
151+
- Tests run in both environments
152+
153+
## Testing Strategy
154+
155+
Tests are organized in test/specs/ by scenario:
156+
157+
- Each scenario has test files (\*.spec.ts) and fixture data
158+
- Tests validate parse, resolve, bundle, and dereference operations
159+
- Extensive coverage of edge cases: circular refs, deep nesting, special characters in paths
160+
- Browser-specific tests use test/fixtures/server.ts for HTTP mocking
161+
162+
Test utilities:
163+
164+
- test/utils/helper.js: Common test patterns
165+
- test/utils/path.js: Path handling for cross-platform tests
166+
- test/utils/serializeJson.ts: Custom snapshot serializer
167+
168+
## Important Constraints
169+
170+
1. **TypeScript Strict Mode**: Project uses strict TypeScript including exactOptionalPropertyTypes
171+
2. **JSON Schema Support**: Compatible with JSON Schema v4, v6, and v7
172+
3. **Minimum Node Version**: Requires Node >= 20 (for native fetch support)
173+
4. **Circular JSON**: Dereferenced schemas may contain circular references (not JSON.stringify safe)
174+
5. **Path Normalization**: Always converts filesystem paths to POSIX format internally
175+
6. **URL Safety**: HTTP resolver has safeUrlResolver option to block internal URLs (default: unsafe allowed)

test/specs/resolvers/resolvers.spec.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,53 @@ import dereferencedSchema from "./dereferenced.js";
99
import { ResolverError, UnmatchedResolverError, JSONParserErrorGroup } from "../../../lib/util/errors.js";
1010
import type { ParserOptions } from "../../../lib/options";
1111

12+
const selfSignedKey = `-----BEGIN PRIVATE KEY-----
13+
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgtssiJGf4HoTmOR7a
14+
gQ0GVH3TzAHKnt246/BGc/CWMAuhRANCAARSXlthTDcst50Xy+gSjJKTqI8ut+gT
15+
U8wFnIbhZRGcoyYuJw0j790OVBo0RsPIkGn67/SbXbR5/KPuD1LSWYPO
16+
-----END PRIVATE KEY-----`;
17+
const selfSignedCert = `-----BEGIN CERTIFICATE-----
18+
MIIBZjCCAQ2gAwIBAgIUR6AsdFtpqVd0/Fo0MlIVV5R9cgYwCgYIKoZIzj0EAwIw
19+
FDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDMwODE4MDk0M1oXDTI3MDMwODE4
20+
MDk0M1owFDESMBAGA1UEAwwJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0D
21+
AQcDQgAEUl5bYUw3LLedF8voEoySk6iPLrfoE1PMBZyG4WURnKMmLicNI+/dDlQa
22+
NEbDyJBp+u/0m120efyj7g9S0lmDzqM9MDswGgYDVR0RBBMwEYIJbG9jYWxob3N0
23+
hwR/AAABMB0GA1UdDgQWBBTXsktzSvAT+42E/wiWneCMNc6UfzAKBggqhkjOPQQD
24+
AgNHADBEAiAjVAMO0sUWXFmrXDDYcm5S9T+hI1fKLaIWhbQgShEj7wIgA157RoMh
25+
mJf0C5aIMmJS5ZMmqUv6QrMeTkhHn+8OQw8=
26+
-----END CERTIFICATE-----`;
27+
28+
async function withSelfSignedHttpsServer(run: (schemaUrl: string) => Promise<void>) {
29+
const { createServer } = await import(["node", "https"].join(":"));
30+
const server = createServer({ key: selfSignedKey, cert: selfSignedCert }, (_request, response) => {
31+
response.writeHead(200, { "Content-Type": "application/json" });
32+
response.end(JSON.stringify({ type: "object", properties: { secure: { type: "boolean" } } }));
33+
});
34+
35+
await new Promise<void>((resolve) => {
36+
server.listen(0, "127.0.0.1", () => resolve());
37+
});
38+
39+
const address = server.address();
40+
if (!address || typeof address === "string") {
41+
throw new Error("Expected the HTTPS test server to listen on a TCP port");
42+
}
43+
44+
try {
45+
await run(`https://127.0.0.1:${address.port}/schema.json`);
46+
} finally {
47+
await new Promise<void>((resolve, reject) => {
48+
server.close((err) => {
49+
if (err) {
50+
reject(err);
51+
} else {
52+
resolve();
53+
}
54+
});
55+
});
56+
}
57+
}
58+
1259
describe("options.resolve", () => {
1360
it('should not resolve external links if "resolve.external" is disabled', async () => {
1461
const schema = await $RefParser.dereference(path.abs("test/specs/resolvers/resolvers.yaml"), {
@@ -66,6 +113,66 @@ describe("options.resolve", () => {
66113
expect(schema).to.deep.equal(dereferencedSchema);
67114
});
68115

116+
it("should allow a custom HTTP resolver to control TLS behavior", async () => {
117+
if (typeof window !== "undefined") {
118+
return;
119+
}
120+
121+
await withSelfSignedHttpsServer(async (schemaUrl) => {
122+
const insecureHttpResolver = {
123+
order: 1,
124+
canRead: /^https?:\/\//i,
125+
async read(file: FileInfo) {
126+
const requestUrl = new URL(file.url);
127+
const moduleName = requestUrl.protocol === "https:" ? "node:https" : "node:http";
128+
const { request } = await import(moduleName);
129+
130+
return await new Promise<Buffer>((resolve, reject) => {
131+
const req = request(
132+
requestUrl,
133+
{
134+
method: "GET",
135+
...(requestUrl.protocol === "https:" ? { rejectUnauthorized: false } : {}),
136+
},
137+
(res: any) => {
138+
const chunks: Buffer[] = [];
139+
140+
res.on("data", (chunk: string | Buffer) => {
141+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
142+
});
143+
res.on("end", () => resolve(Buffer.concat(chunks)));
144+
},
145+
);
146+
147+
req.on("error", reject);
148+
req.end();
149+
});
150+
},
151+
};
152+
153+
const schema = await $RefParser.dereference(
154+
{
155+
$ref: schemaUrl,
156+
},
157+
{
158+
resolve: {
159+
http: false,
160+
insecureHttp: insecureHttpResolver,
161+
},
162+
} as ParserOptions,
163+
);
164+
165+
expect(schema).to.deep.equal({
166+
type: "object",
167+
properties: {
168+
secure: {
169+
type: "boolean",
170+
},
171+
},
172+
});
173+
});
174+
});
175+
69176
it("should return _file url as it's written", async () => {
70177
const schema = await $RefParser.dereference(path.abs("test/specs/resolvers/resolvers.yaml"), {
71178
resolve: {

0 commit comments

Comments
 (0)