ty-fetch 0.0.2-beta.0 → 0.0.2-beta.10

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Alister Norris
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,34 +1,66 @@
1
- # ty-fetch
1
+ # ty-fetch
2
2
 
3
- TypeScript tooling that validates API calls against OpenAPI specs. Get autocomplete, diagnostics, and fully typed responses with zero manual types.
3
+ **Automatic TypeScript types for any REST API. No codegen. No manual types. Just fetch.**
4
+
5
+ ty-fetch is a TypeScript language service plugin that reads OpenAPI specs and gives you fully typed API calls — response types, request bodies, query params, headers, path validation, and autocomplete — all without a single line of generated code.
4
6
 
5
7
  ```ts
6
8
  import tf from "ty-fetch";
7
9
 
8
- const customers = await tf.get("https://api.stripe.com/v1/customers").json();
9
- // customers is fully typed — data, has_more, object, url all autocomplete
10
+ // Fully typed — response, query params, headers all inferred from the spec
11
+ const data = await tf.get("https://api.stripe.com/v1/customers", {
12
+ params: { query: { limit: 10 } },
13
+ }).json();
14
+ data.data // Customer[] — autocomplete just works
10
15
 
16
+ // ❌ Typo? Caught instantly with a suggestion
11
17
  tf.get("https://api.stripe.com/v1/cutsomers");
12
18
  // ~~~~~~~~~~
13
19
  // Error: Path '/v1/cutsomers' does not exist in Stripe API.
14
20
  // Did you mean '/v1/customers'?
15
21
  ```
16
22
 
17
- ## What it does
23
+ ---
24
+
25
+ ## 🤔 Why ty-fetch?
26
+
27
+ Every other OpenAPI tool makes you **run a codegen step**. You generate a client, import from generated files, and re-run the generator when the spec changes. It works, but it's friction.
28
+
29
+ ty-fetch takes a completely different approach:
30
+
31
+ | | Traditional codegen | ty-fetch |
32
+ |---|---|---|
33
+ | **Setup** | Install generator, run codegen, import client | `npm install ty-fetch` and go |
34
+ | **When spec changes** | Re-run generator, fix imports | Types update automatically |
35
+ | **What you write** | `client.customers.list()` | `tf.get("https://api.stripe.com/v1/customers")` |
36
+ | **Build step** | Required | None |
37
+ | **Generated files** | Committed to repo or `.gitignore`'d | None — types live in node_modules |
38
+
39
+ **You write real URLs. The types just appear.**
18
40
 
19
- - **Path validation** — red squiggles for typos in API URLs, with "did you mean?" suggestions
20
- - **Typed responses** — response types generated from OpenAPI schemas, no manual `as` casts
21
- - **Typed request bodies** — body params validated against the spec
22
- - **Path & query params** — typed `params.path` and `params.query` based on the endpoint
23
- - **Autocomplete** — URL path completions inside string literals, filtered by HTTP method
24
- - **Hover info** — hover over a URL to see available methods and descriptions
41
+ ---
25
42
 
26
- Works as both a **TS language service plugin** (editor DX) and a **CLI** (CI validation).
43
+ ## 🚀 Features
27
44
 
28
- ## Setup
45
+ - 🔮 **Zero codegen** — types generated on-the-fly by a TS plugin, not a build step
46
+ - 📦 **Typed responses** — `.json()` returns the actual response type from the spec
47
+ - ✏️ **Typed request bodies** — body params validated against the schema
48
+ - 🔗 **Typed path & query params** — `params.path` and `params.query` based on the endpoint
49
+ - 🔑 **Typed headers** — required headers (API keys, auth) from security schemes
50
+ - 🚨 **Path validation** — red squiggles for typos, with "did you mean?" suggestions
51
+ - 💡 **Autocomplete** — URL path completions inside string literals
52
+ - 📖 **JSDoc descriptions** — property descriptions from the spec in hover tooltips
53
+ - 🔍 **Auto-discovery** — probes well-known paths (`/openapi.json`, `/.well-known/openapi.yaml`) when no spec is configured
54
+ - 📄 **YAML + JSON** — specs can be either format, local files or remote URLs
55
+ - 🧠 **Example inference** — generates types from response `example` when `schema` is missing
56
+ - ⚡ **On-demand** — only fetches specs and generates types for APIs you actually call
57
+
58
+ ---
59
+
60
+ ## 📦 Setup
29
61
 
30
62
  ```bash
31
- npm install github:alnorris/ty-fetch
63
+ npm install ty-fetch
32
64
  ```
33
65
 
34
66
  Add the plugin to your `tsconfig.json`:
@@ -41,66 +73,75 @@ Add the plugin to your `tsconfig.json`:
41
73
  }
42
74
  ```
43
75
 
44
- In VS Code, make sure you're using the workspace TypeScript version (not the built-in one). Open the command palette and run **TypeScript: Select TypeScript Version** > **Use Workspace Version**.
76
+ In VS Code, use the workspace TypeScript version:
77
+ **Command Palette** → **TypeScript: Select TypeScript Version** → **Use Workspace Version**
45
78
 
46
- ## Usage
79
+ That's it. Start writing `tf.get("https://...")` and types appear automatically. ✨
47
80
 
48
- ### The `ty-fetch` client
81
+ ---
49
82
 
50
- A lightweight HTTP client (similar to [ky](https://github.com/sindresorhus/ky)) with typed methods:
83
+ ## 🔧 Usage
51
84
 
52
85
  ```ts
53
86
  import tf from "ty-fetch";
54
87
 
55
- // GET with typed response
88
+ // 📥 GET with typed response
56
89
  const customers = await tf.get("https://api.stripe.com/v1/customers").json();
57
90
 
58
- // POST with typed body
91
+ // 📤 POST with typed body
59
92
  const customer = await tf.post("https://api.stripe.com/v1/customers", {
60
93
  body: { name: "Jane Doe", email: "jane@example.com" },
61
94
  }).json();
62
95
 
63
- // Path params
96
+ // 🔗 Path params
64
97
  const repo = await tf.get("https://api.github.com/repos/{owner}/{repo}", {
65
98
  params: { path: { owner: "anthropics", repo: "claude-code" } },
66
99
  }).json();
67
100
 
68
- // Query params
69
- const pets = await tf.get("https://petstore3.swagger.io/api/v3/pet/findByStatus", {
70
- params: { query: { status: "available" } },
101
+ // 🔍 Query params
102
+ const results = await tf.get("https://hn.algolia.com/api/v1/search_by_date", {
103
+ params: { query: { query: "typescript", hitsPerPage: 10 } },
104
+ }).json();
105
+
106
+ // 🔑 Headers (typed from security schemes)
107
+ const data = await tf.get("https://api.example.com/v1/data", {
108
+ headers: { "x-api-key": process.env.API_KEY },
71
109
  }).json();
72
110
  ```
73
111
 
74
- Response methods:
112
+ ### Response methods
75
113
 
76
114
  | Method | Returns |
77
115
  |---|---|
78
- | `.json()` | `Promise<T>` (typed from spec) |
116
+ | `.json()` | `Promise<T>` typed from spec |
79
117
  | `.text()` | `Promise<string>` |
80
118
  | `.blob()` | `Promise<Blob>` |
81
119
  | `.arrayBuffer()` | `Promise<ArrayBuffer>` |
82
- | `await` directly | `T` (same as `.json()`) |
120
+ | `await` directly | `T` same as `.json()` |
83
121
 
84
- ### CLI
122
+ ---
85
123
 
86
- Run validation in CI or from the terminal:
124
+ ## 🔍 Spec discovery
87
125
 
88
- ```bash
89
- npx ty-fetch # uses ./tsconfig.json
90
- npx ty-fetch tsconfig.json # explicit path
91
- npx ty-fetch --verbose # show spec fetching details
92
- ```
126
+ ### Auto-discovery (zero config)
93
127
 
94
- ```
95
- example.ts:21:11 - error TF99001: Path '/v1/cutsomers' does not exist in Stripe API. Did you mean '/v1/customers'?
96
- example.ts:57:11 - error TF99001: Path '/pets' does not exist in Swagger Petstore. Did you mean '/pet'?
128
+ When ty-fetch encounters an API domain it hasn't seen before, it automatically probes these well-known paths:
97
129
 
98
- 2 error(s) found.
99
130
  ```
131
+ /.well-known/openapi.json
132
+ /.well-known/openapi.yaml
133
+ /openapi.json
134
+ /openapi.yaml
135
+ /api/openapi.json
136
+ /docs/openapi.json
137
+ /swagger.json
138
+ ```
139
+
140
+ If any return a valid OpenAPI spec → types are generated automatically. No config needed.
100
141
 
101
- ## Custom specs
142
+ ### Custom specs
102
143
 
103
- Map domains to local files or URLs in your tsconfig plugin config:
144
+ Map domains to local files or remote URLs in your tsconfig:
104
145
 
105
146
  ```jsonc
106
147
  {
@@ -109,7 +150,7 @@ Map domains to local files or URLs in your tsconfig plugin config:
109
150
  {
110
151
  "name": "ty-fetch/plugin",
111
152
  "specs": {
112
- "api.internal.company.com": "./specs/internal-api.json",
153
+ "api.internal.company.com": "./specs/internal-api.yaml",
113
154
  "api.partner.com": "https://partner.com/openapi.json"
114
155
  }
115
156
  }
@@ -118,55 +159,53 @@ Map domains to local files or URLs in your tsconfig plugin config:
118
159
  }
119
160
  ```
120
161
 
121
- - **File paths** are resolved relative to the tsconfig directory
122
- - **URLs** are fetched over HTTPS
123
- - Custom specs override built-in defaults for the same domain
162
+ - 📁 File paths resolved relative to tsconfig directory
163
+ - 🌐 URLs fetched over HTTPS
164
+ - 📄 JSON and YAML both supported
165
+ - Custom specs override auto-discovery for the same domain
166
+ - Works in both the editor plugin and the CLI
124
167
 
125
- This works in both the editor plugin and the CLI.
168
+ ---
126
169
 
127
- ### Built-in specs
170
+ ## 🖥️ CLI
128
171
 
129
- These APIs are supported out of the box (no config needed):
172
+ Validate API calls in CI no editor required:
130
173
 
131
- | Domain | API | Paths |
132
- |---|---|---|
133
- | `api.stripe.com` | Stripe API | 414 |
134
- | `petstore3.swagger.io` | Swagger Petstore | 13 |
135
- | `api.github.com` | GitHub REST API | 551 |
174
+ ```bash
175
+ npx ty-fetch # uses ./tsconfig.json
176
+ npx ty-fetch tsconfig.json # explicit path
177
+ npx ty-fetch --verbose # show spec fetching details
178
+ ```
179
+
180
+ ```
181
+ src/api.ts:21:11 - error TF99001: Path '/v1/cutsomers' does not exist in Stripe API.
182
+ Did you mean '/v1/customers'?
136
183
 
137
- ## How it works
184
+ 1 error(s) found.
185
+ ```
138
186
 
139
- 1. Plugin intercepts the TS language service (`getSemanticDiagnostics`, `getCompletionsAtPosition`, `getQuickInfoAtPosition`)
140
- 2. Finds `fetch()` / `tf.get()` / `tf.post()` etc. calls with string literal URLs
141
- 3. Extracts the domain and fetches the OpenAPI spec on-demand (cached after first fetch)
142
- 4. Validates paths against the spec, suggests corrections via Levenshtein distance
143
- 5. Generates typed overloads into `node_modules/ty-fetch/index.d.ts` using interface declaration merging — only for URLs actually used in your code
187
+ ---
144
188
 
145
- Spec fetching is async. On first encounter of a domain, the plugin fires a background fetch and returns no extra diagnostics. When the spec arrives, `refreshDiagnostics()` triggers the editor to re-check. This follows the same pattern as [graphqlsp](https://github.com/0no-co/graphqlsp).
189
+ ## ⚙️ How it works
146
190
 
147
- ## Architecture
191
+ 1. 🔌 Plugin intercepts the TS language service
192
+ 2. 🔎 Finds `tf.get()` / `tf.post()` / `fetch()` calls with string literal URLs
193
+ 3. 📡 Extracts the domain, fetches the OpenAPI spec on-demand (cached after first fetch)
194
+ 4. ✅ Validates paths, suggests corrections via Levenshtein distance
195
+ 5. 🏗️ Generates typed overloads into `node_modules/ty-fetch/index.d.ts` via declaration merging — **only for URLs you actually use**
148
196
 
149
- ```
150
- src/
151
- plugin/index.ts TS language service plugin (diagnostics, completions, hover)
152
- cli/index.ts CLI entry point for CI validation
153
- core/ Shared logic (URL parsing, spec cache, path matching, body validation)
154
- generate-types.ts OpenAPI schema -> TypeScript type declarations
155
- test-project/ Example project using the plugin
156
- test/ Unit tests
157
- ```
197
+ Types are generated **only for endpoints you call** — not the entire spec. A 500-path API might produce just 5 overloads if that's all you use. This keeps TypeScript fast.
198
+
199
+ ---
158
200
 
159
- ## Development
201
+ ## 🧪 Development
160
202
 
161
203
  ```bash
162
204
  npm run build # compile TypeScript
163
205
  npm run watch # compile in watch mode
164
- npm test # run unit tests
206
+ npm test # run unit tests (74 tests)
165
207
  ```
166
208
 
167
- To test the editor experience:
209
+ ## License
168
210
 
169
- 1. Open `test-project/` in VS Code
170
- 2. Select the workspace TypeScript version
171
- 3. Restart the TS server (`TypeScript: Restart TS Server`)
172
- 4. Edit `test-project/example.ts` and observe diagnostics/completions
211
+ MIT
package/base.d.ts CHANGED
@@ -6,12 +6,14 @@ export interface Options<
6
6
  TBody = never,
7
7
  TPathParams = never,
8
8
  TQueryParams = never,
9
- > extends Omit<RequestInit, 'body'> {
9
+ THeaders extends Record<string, string> = Record<string, string>,
10
+ > extends Omit<RequestInit, 'body' | 'headers'> {
10
11
  body?: TBody;
11
12
  params?: {
12
13
  path?: TPathParams;
13
14
  query?: TQueryParams;
14
15
  };
16
+ headers?: THeaders & Record<string, string>;
15
17
  prefixUrl?: string;
16
18
  }
17
19
 
@@ -23,16 +25,19 @@ export interface ResponsePromise<T = unknown> extends PromiseLike<T> {
23
25
  formData(): Promise<FormData>;
24
26
  }
25
27
 
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
+ type BaseOptions = Options<any, Record<string, any>, Record<string, any>>;
30
+
26
31
  export interface TyFetch {
27
- (url: string, options?: Options): ResponsePromise;
28
- get(url: string, options?: Options): ResponsePromise;
29
- post(url: string, options?: Options): ResponsePromise;
30
- put(url: string, options?: Options): ResponsePromise;
31
- patch(url: string, options?: Options): ResponsePromise;
32
- delete(url: string, options?: Options): ResponsePromise;
33
- head(url: string, options?: Options): ResponsePromise;
34
- create(defaults?: Options<unknown>): TyFetch;
35
- extend(defaults?: Options<unknown>): TyFetch;
32
+ (url: string, options?: BaseOptions): ResponsePromise<any>;
33
+ get(url: string, options?: BaseOptions): ResponsePromise<any>;
34
+ post(url: string, options?: BaseOptions): ResponsePromise<any>;
35
+ put(url: string, options?: BaseOptions): ResponsePromise<any>;
36
+ patch(url: string, options?: BaseOptions): ResponsePromise<any>;
37
+ delete(url: string, options?: BaseOptions): ResponsePromise<any>;
38
+ head(url: string, options?: BaseOptions): ResponsePromise<any>;
39
+ create(defaults?: BaseOptions): TyFetch;
40
+ extend(defaults?: BaseOptions): TyFetch;
36
41
  HTTPError: typeof HTTPError;
37
42
  }
38
43
 
package/dist/cli/index.js CHANGED
@@ -39,6 +39,16 @@ const path = __importStar(require("path"));
39
39
  const core_1 = require("../core");
40
40
  async function main() {
41
41
  const args = process.argv.slice(2);
42
+ if (args.includes("--help") || args.includes("-h")) {
43
+ console.log(`Usage: ty-fetch [tsconfig.json] [--verbose]
44
+
45
+ Validate API calls against OpenAPI specs.
46
+
47
+ Options:
48
+ --verbose Show spec fetching details
49
+ --help, -h Show this help message`);
50
+ process.exit(0);
51
+ }
42
52
  const tsconfigPath = args[0] ?? "tsconfig.json";
43
53
  const configFile = ts.readConfigFile(path.resolve(tsconfigPath), ts.sys.readFile);
44
54
  if (configFile.error) {
@@ -32,17 +32,17 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
35
38
  Object.defineProperty(exports, "__esModule", { value: true });
36
39
  exports.specCache = exports.KNOWN_SPECS = void 0;
37
40
  exports.registerSpecs = registerSpecs;
38
41
  exports.ensureSpec = ensureSpec;
39
42
  exports.ensureSpecSync = ensureSpecSync;
40
43
  exports.fetchSpecForDomain = fetchSpecForDomain;
41
- exports.KNOWN_SPECS = {
42
- "api.stripe.com": "https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json",
43
- "petstore3.swagger.io": "https://petstore3.swagger.io/api/v3/openapi.json",
44
- "api.github.com": "https://api.apis.guru/v2/specs/github.com/api.github.com/1.1.4/openapi.json",
45
- };
44
+ const js_yaml_1 = __importDefault(require("js-yaml"));
45
+ exports.KNOWN_SPECS = {};
46
46
  exports.specCache = new Map();
47
47
  /**
48
48
  * Register user-provided spec mappings (from tsconfig plugin config).
@@ -73,26 +73,43 @@ function ensureSpec(domain, log, onLoaded) {
73
73
  if (existing)
74
74
  return existing;
75
75
  const specUrl = exports.KNOWN_SPECS[domain];
76
- if (!specUrl) {
77
- const entry = { status: "not-found", spec: null, fetchedAt: Date.now() };
78
- exports.specCache.set(domain, entry);
79
- return entry;
80
- }
81
76
  const entry = { status: "loading", spec: null, fetchedAt: Date.now() };
82
77
  exports.specCache.set(domain, entry);
83
- log(`Fetching spec for ${domain} from ${specUrl}`);
84
- fetchSpec(specUrl)
85
- .then((spec) => {
86
- entry.status = "loaded";
87
- entry.spec = spec;
88
- const pathCount = Object.keys(spec.paths || {}).length;
89
- log(`Spec loaded for ${domain}: ${spec.info?.title ?? "unknown"} (${pathCount} paths)`);
90
- onLoaded?.(domain, spec);
91
- })
92
- .catch((err) => {
93
- entry.status = "not-found";
94
- log(`Failed to fetch spec for ${domain}: ${err}`);
95
- });
78
+ if (specUrl) {
79
+ log(`Fetching spec for ${domain} from ${specUrl}`);
80
+ fetchSpec(specUrl)
81
+ .then((spec) => {
82
+ entry.status = "loaded";
83
+ entry.spec = spec;
84
+ const pathCount = Object.keys(spec.paths || {}).length;
85
+ log(`Spec loaded for ${domain}: ${spec.info?.title ?? "unknown"} (${pathCount} paths)`);
86
+ onLoaded?.(domain, spec);
87
+ })
88
+ .catch((err) => {
89
+ entry.status = "not-found";
90
+ log(`Failed to fetch spec for ${domain}: ${err}`);
91
+ });
92
+ }
93
+ else {
94
+ log(`No spec configured for ${domain}, probing well-known URLs...`);
95
+ probeWellKnownSpecs(domain, log)
96
+ .then((spec) => {
97
+ if (spec) {
98
+ entry.status = "loaded";
99
+ entry.spec = spec;
100
+ const pathCount = Object.keys(spec.paths || {}).length;
101
+ log(`Spec discovered for ${domain}: ${spec.info?.title ?? "unknown"} (${pathCount} paths)`);
102
+ onLoaded?.(domain, spec);
103
+ }
104
+ else {
105
+ entry.status = "not-found";
106
+ log(`No spec found for ${domain} at well-known URLs`);
107
+ }
108
+ })
109
+ .catch(() => {
110
+ entry.status = "not-found";
111
+ });
112
+ }
96
113
  return entry;
97
114
  }
98
115
  /**
@@ -110,33 +127,86 @@ async function fetchSpecForDomain(domain, log) {
110
127
  if (existing && existing.status === "loaded")
111
128
  return existing;
112
129
  const specUrl = exports.KNOWN_SPECS[domain];
113
- if (!specUrl) {
114
- const entry = { status: "not-found", spec: null, fetchedAt: Date.now() };
115
- exports.specCache.set(domain, entry);
116
- return entry;
130
+ if (specUrl) {
131
+ log(`Fetching spec for ${domain} from ${specUrl}`);
132
+ try {
133
+ const spec = await fetchSpec(specUrl);
134
+ const entry = { status: "loaded", spec, fetchedAt: Date.now() };
135
+ exports.specCache.set(domain, entry);
136
+ const pathCount = Object.keys(spec.paths || {}).length;
137
+ log(`Spec loaded for ${domain}: ${spec.info?.title ?? "unknown"} (${pathCount} paths)`);
138
+ return entry;
139
+ }
140
+ catch (err) {
141
+ const entry = { status: "not-found", spec: null, fetchedAt: Date.now() };
142
+ exports.specCache.set(domain, entry);
143
+ log(`Failed to fetch spec for ${domain}: ${err}`);
144
+ return entry;
145
+ }
117
146
  }
118
- log(`Fetching spec for ${domain} from ${specUrl}`);
119
- try {
120
- const spec = await fetchSpec(specUrl);
147
+ log(`No spec configured for ${domain}, probing well-known URLs...`);
148
+ const spec = await probeWellKnownSpecs(domain, log);
149
+ if (spec) {
121
150
  const entry = { status: "loaded", spec, fetchedAt: Date.now() };
122
151
  exports.specCache.set(domain, entry);
123
152
  const pathCount = Object.keys(spec.paths || {}).length;
124
- log(`Spec loaded for ${domain}: ${spec.info?.title ?? "unknown"} (${pathCount} paths)`);
153
+ log(`Spec discovered for ${domain}: ${spec.info?.title ?? "unknown"} (${pathCount} paths)`);
125
154
  return entry;
126
155
  }
127
- catch (err) {
128
- const entry = { status: "not-found", spec: null, fetchedAt: Date.now() };
129
- exports.specCache.set(domain, entry);
130
- log(`Failed to fetch spec for ${domain}: ${err}`);
131
- return entry;
156
+ const entry = { status: "not-found", spec: null, fetchedAt: Date.now() };
157
+ exports.specCache.set(domain, entry);
158
+ log(`No spec found for ${domain} at well-known URLs`);
159
+ return entry;
160
+ }
161
+ const WELL_KNOWN_PATHS = [
162
+ "/.well-known/openapi.json",
163
+ "/.well-known/openapi.yaml",
164
+ "/.well-known/openapi.yml",
165
+ "/openapi.json",
166
+ "/openapi.yaml",
167
+ "/openapi.yml",
168
+ "/api/openapi.json",
169
+ "/api/openapi.yaml",
170
+ "/docs/openapi.json",
171
+ "/docs/openapi.yaml",
172
+ "/swagger.json",
173
+ "/api-docs/openapi.json",
174
+ ];
175
+ async function probeWellKnownSpecs(domain, log) {
176
+ // Try HTTPS first, then HTTP for local/dev servers
177
+ const protocols = domain.startsWith("127.0.0.1") || domain.startsWith("localhost")
178
+ ? ["http"] : ["https", "http"];
179
+ for (const proto of protocols) {
180
+ for (const path of WELL_KNOWN_PATHS) {
181
+ const url = `${proto}://${domain}${path}`;
182
+ try {
183
+ const spec = await fetchSpec(url);
184
+ if (spec?.openapi || spec?.swagger) {
185
+ log(`Found spec at ${url}`);
186
+ return spec;
187
+ }
188
+ }
189
+ catch {
190
+ // try next
191
+ }
192
+ }
193
+ }
194
+ return null;
195
+ }
196
+ function parseSpec(data, source) {
197
+ const isYaml = /\.ya?ml$/i.test(source) ||
198
+ (!source.endsWith(".json") && !data.trimStart().startsWith("{"));
199
+ if (isYaml) {
200
+ return js_yaml_1.default.load(data);
132
201
  }
202
+ return JSON.parse(data);
133
203
  }
134
204
  async function fetchSpec(urlOrPath) {
135
205
  // Local file path — read from disk
136
206
  if (!urlOrPath.startsWith("http://") && !urlOrPath.startsWith("https://")) {
137
207
  const fs = require("fs");
138
208
  const data = fs.readFileSync(urlOrPath, "utf-8");
139
- return JSON.parse(data);
209
+ return parseSpec(data, urlOrPath);
140
210
  }
141
211
  // Remote URL — fetch via HTTPS/HTTP
142
212
  const mod = urlOrPath.startsWith("https://") ? await Promise.resolve().then(() => __importStar(require("https"))) : await Promise.resolve().then(() => __importStar(require("http")));
@@ -146,17 +216,21 @@ async function fetchSpec(urlOrPath) {
146
216
  fetchSpec(res.headers.location).then(resolve, reject);
147
217
  return;
148
218
  }
219
+ if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
220
+ reject(new Error(`HTTP ${res.statusCode}`));
221
+ return;
222
+ }
149
223
  let data = "";
150
224
  res.on("data", (chunk) => (data += chunk));
151
225
  res.on("end", () => {
152
226
  try {
153
- resolve(JSON.parse(data));
227
+ resolve(parseSpec(data, urlOrPath));
154
228
  }
155
- catch {
156
- reject(new Error("Invalid JSON"));
229
+ catch (err) {
230
+ reject(new Error(`Failed to parse spec: ${err}`));
157
231
  }
158
232
  });
159
233
  res.on("error", reject);
160
- });
234
+ }).on("error", reject);
161
235
  });
162
236
  }
@@ -1,4 +1,6 @@
1
1
  export interface OpenAPISpec {
2
+ openapi?: string;
3
+ swagger?: string;
2
4
  paths: Record<string, Record<string, any>>;
3
5
  info?: {
4
6
  title?: string;
@@ -10,6 +12,7 @@ export interface OpenAPISpec {
10
12
  components?: {
11
13
  schemas?: Record<string, any>;
12
14
  };
15
+ security?: Array<Record<string, string[]>>;
13
16
  }
14
17
  export interface SpecEntry {
15
18
  status: "loading" | "loaded" | "not-found";
@@ -20,6 +20,7 @@ interface OpenAPIOperation {
20
20
  responses?: Record<string, {
21
21
  content?: Record<string, {
22
22
  schema?: OpenAPISchema;
23
+ example?: unknown;
23
24
  }>;
24
25
  description?: string;
25
26
  }>;
@@ -42,7 +43,14 @@ interface FullOpenAPISpec {
42
43
  paths: Record<string, Record<string, OpenAPIOperation>>;
43
44
  components?: {
44
45
  schemas?: Record<string, OpenAPISchema>;
46
+ securitySchemes?: Record<string, {
47
+ type?: string;
48
+ in?: string;
49
+ name?: string;
50
+ description?: string;
51
+ }>;
45
52
  };
53
+ security?: Array<Record<string, string[]>>;
46
54
  servers?: Array<{
47
55
  url?: string;
48
56
  }>;