@agentcash/discovery 1.0.1 → 1.1.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.
package/README.md CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  Canonical discovery runtime for the agentcash ecosystem.
4
4
 
5
- Use one library for CLI, server, and client so discovery behavior is identical everywhere.
5
+ Use one library for MCP, CLI, router, and audit so discovery behavior is identical everywhere.
6
6
 
7
7
  ## Why One Library
8
8
 
9
9
  - Same parsing logic across surfaces: no CLI/server/client drift.
10
+ - Shared Zod schema that the router can test against at compile-time.
10
11
  - Same warning codes and precedence rules: fewer integration surprises.
11
12
  - Same compatibility adapters in one place: legacy behavior is isolated and removable.
12
- - Same L0-L5 harness model: easier context-budget auditing and rollout decisions.
13
13
 
14
14
  ## L0-L5 Mental Model
15
15
 
@@ -22,196 +22,128 @@ Use one library for CLI, server, and client so discovery behavior is identical e
22
22
 
23
23
  Design rule: `L0` + `L1` are zero-hop critical. `L2+` should be fetched on demand.
24
24
 
25
+ In practice, each layer should guide the agent to discover the next:
26
+
27
+ | Layer | Surface | What the agent gets |
28
+ | ------ | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
29
+ | **L0** | MCP tool description / `agentcash --help` | First impression of agentcash. Should encourage the agent to use it and explicitly explain `discoverOriginSchema` and `checkEndpointSchema`. |
30
+ | **L1** | Same location as L0 | List of domains available to the agent. Each entry should be descriptive enough for the agent to understand what it does at a high level. |
31
+ | **L2** | `discoverOriginSchema` result | Detailed description of the origin and its supported endpoints. |
32
+ | **L3** | `checkEndpointSchema` result | Specific guidance for a single endpoint: input/output schema, auth mode, and a detailed description of what the endpoint does. |
33
+ | **L4** | `discoverOriginSchema` with `includeGuidance: true` | Composition guidance for 2+ resources at an origin. Sourced from the `guidance` field in OpenAPI. |
34
+
25
35
  ## Install
26
36
 
27
37
  ```bash
28
38
  pnpm add @agentcash/discovery
29
39
  ```
30
40
 
31
- ## CLI Usage
41
+ ## CLI
32
42
 
33
- Quick audit:
43
+ Two commands: `discover` (list endpoints at an origin) and `check` (inspect a specific URL).
34
44
 
35
45
  ```bash
46
+ # Discover all endpoints at an origin
36
47
  npx @agentcash/discovery stabletravel.dev
37
- ```
48
+ npx @agentcash/discovery discover stabletravel.dev
38
49
 
39
- Verbose matrices:
40
-
41
- ```bash
42
- npx @agentcash/discovery stabletravel.dev -v
50
+ # Inspect a specific endpoint URL
51
+ npx @agentcash/discovery check https://stabletravel.dev/search
43
52
  ```
44
53
 
45
- Machine-readable output:
54
+ Flags:
46
55
 
47
- ```bash
48
- npx @agentcash/discovery stabletravel.dev --json
49
- ```
56
+ | Flag | Description |
57
+ | -------- | -------------------------------------------------- |
58
+ | `--json` | Machine-readable JSON output |
59
+ | `-v` | Verbose — includes guidance text and warning hints |
50
60
 
51
- L0-L5 context harness summary for a client:
61
+ JSON output shape (`discover`):
52
62
 
53
- ```bash
54
- npx @agentcash/discovery stabletravel.dev --harness --client claude-code
63
+ ```json
64
+ {
65
+ "ok": true,
66
+ "selectedStage": "openapi",
67
+ "resources": [{ "resourceKey": "GET /search", "method": "GET", "path": "/search" }],
68
+ "warnings": [],
69
+ "meta": { "origin": "https://stabletravel.dev", "specUrl": "..." }
70
+ }
55
71
  ```
56
72
 
57
- L0-L5 harness verbose with explicit budget:
58
-
59
- ```bash
60
- npx @agentcash/discovery stabletravel.dev --harness -v --client skill-cli --context-window-tokens 200000
73
+ JSON output shape (`check`):
74
+
75
+ ```json
76
+ {
77
+ "url": "https://stabletravel.dev/search",
78
+ "found": true,
79
+ "origin": "https://stabletravel.dev",
80
+ "path": "/search",
81
+ "advisories": [{ "method": "GET", "authMode": "bearer", "estimatedPrice": "$0.01" }],
82
+ "warnings": []
83
+ }
61
84
  ```
62
85
 
63
- Useful flags:
64
-
65
- - `--compat on|off|strict`
66
- - `--probe`
67
- - `--timeout-ms <ms>`
68
- - `--no-truncate`
69
- - `--no-color`
70
-
71
86
  ## Programmatic Usage
72
87
 
73
88
  ```ts
74
- import {
75
- auditContextHarness,
76
- discover,
77
- discoverDetailed,
78
- validatePaymentRequiredDetailed,
79
- type HarnessClientId,
80
- } from '@agentcash/discovery';
81
-
82
- const progressive = await discover({
83
- target: 'stabletravel.dev',
84
- compatMode: 'on',
85
- });
86
-
87
- const detailed = await discoverDetailed({
88
- target: 'stabletravel.dev',
89
- compatMode: 'strict',
90
- rawView: 'full',
91
- });
92
-
93
- const client: HarnessClientId = 'claude-code';
94
- const harness = await auditContextHarness({
95
- target: 'stabletravel.dev',
96
- client,
97
- contextWindowTokens: 200000,
98
- compatMode: 'on',
99
- });
100
-
101
- const validation = validatePaymentRequiredDetailed(payload, {
102
- compatMode: 'strict',
103
- metadata: {
104
- title: 'Example API',
105
- description: 'Sample description',
106
- favicon: 'https://example.com/favicon.ico',
107
- ogImages: [{ url: 'https://example.com/og.png' }],
108
- },
109
- });
110
- ```
111
-
112
- ## MCP Adapter Contract
113
-
114
- This section is the canonical contract for MCP-facing discovery adapters:
115
-
116
- - `discoverForMcp(options)` for L2 resources + L4 guidance policy projection.
117
- - `inspectEndpointForMcp(options)` for L3 OpenAPI advisory projection.
118
-
119
- `discoverForMcp` guarantees:
120
-
121
- - `guidanceAvailable` is always present.
122
- - `guidanceTokens` is present when guidance exists.
123
- - `guidance` is included when `includeGuidance=true`, excluded when
124
- `includeGuidance=false`, and auto-included under the token threshold when not
125
- specified.
126
-
127
- `inspectEndpointForMcp` guarantees:
128
-
129
- - Spec-derived HTTP methods for the selected endpoint path.
130
- - Per-method advisory data: summary, estimated price, protocols, auth mode, and
131
- input schema.
132
-
133
- Ownership boundary:
134
-
135
- - `@agentcash/discovery` owns discovery/advisory contracts.
136
- - Runtime probe truth (live 402 parsing/payment option extraction/divergence)
137
- belongs to the `agentcash` MCP package.
138
-
139
- Reference integration boundary:
140
-
141
- - `https://github.com/Merit-Systems/agentcash/blob/main/packages/external/mcp/docs/discovery-boundary.md`
89
+ import { discoverOriginSchema, checkEndpointSchema } from '@agentcash/discovery';
142
90
 
143
- ## Discovery Waterfall
91
+ // Discover all endpoints at an origin
92
+ const result = await discoverOriginSchema({ target: 'stabletravel.dev' });
93
+ // result.found === true → result.endpoints (L2Route[]), result.guidance?, result.guidanceTokens?
144
94
 
145
- Default order:
146
-
147
- 1. Explicit override URLs (if provided)
148
- 2. OpenAPI (`/openapi.json`, then `/.well-known/openapi.json`)
149
- 3. `/.well-known/x402` (compat)
150
- 4. DNS TXT `_x402` pointers (compat)
151
- 5. Probe fallback (only when probe candidates are provided)
95
+ // Inspect a specific endpoint URL
96
+ const check = await checkEndpointSchema({ url: 'https://stabletravel.dev/search' });
97
+ // check.found === true check.advisories (per-method: authMode, estimatedPrice, protocols, inputSchema)
98
+ ```
152
99
 
153
- Behavior:
100
+ ## Exported API
154
101
 
155
- - `discover(...)` stops at first valid non-empty stage.
156
- - `discoverDetailed(...)` runs full waterfall and merges deterministically.
102
+ ### Core discovery
157
103
 
158
- Validation behavior:
104
+ | Export | Description |
105
+ | ------------------------ | --------------------------------------------------------- |
106
+ | `discoverOriginSchema()` | Progressive discovery — returns endpoints + advisory data |
107
+ | `checkEndpointSchema()` | Per-endpoint inspection — returns per-method advisories |
159
108
 
160
- - `validatePaymentRequiredDetailed(...)` uses Coinbase `@x402/core` schemas as the structural base gate.
161
- - Product policy diagnostics (network/schema/metadata) are layered on top with stable issue codes.
109
+ ### Layer fetchers (low-level)
162
110
 
163
- ## Compatibility Modes
111
+ | Export | Layer | Description |
112
+ | -------------------------- | ----- | ------------------------------------- |
113
+ | `getOpenAPI(origin)` | — | Fetch OpenAPI spec from origin |
114
+ | `getWellKnown(origin)` | — | Fetch `/.well-known/x402` document |
115
+ | `getProbe(url, body?)` | — | Live endpoint probe |
116
+ | `checkL2ForOpenAPI(spec)` | L2 | Extract route list from OpenAPI |
117
+ | `checkL2ForWellknown(doc)` | L2 | Extract route list from well-known |
118
+ | `getL3(origin, path)` | L3 | Get detailed metadata for an endpoint |
119
+ | `checkL4ForOpenAPI(spec)` | L4 | Extract guidance from OpenAPI |
120
+ | `checkL4ForWellknown(doc)` | L4 | Extract guidance from well-known |
164
121
 
165
- - `on` (default): legacy adapters enabled.
166
- - `off`: canonical-only behavior.
167
- - `strict`: legacy adapters enabled, selected warnings escalated.
122
+ ### Validation
168
123
 
169
- ## Contract Guarantees
124
+ | Export | Description |
125
+ | ----------------------------------- | -------------------------------------------- |
126
+ | `validatePaymentRequiredDetailed()` | Full 402 payload validation with diagnostics |
127
+ | `evaluateMetadataCompleteness()` | Metadata quality score |
128
+ | `VALIDATION_CODES` | Stable issue code constants |
170
129
 
171
- Resource identity:
130
+ ### Audit
172
131
 
173
- ```text
174
- ${origin} ${method} ${path}
175
- ```
132
+ | Export | Description |
133
+ | --------------------------- | ------------------------------ |
134
+ | `getWarningsForOpenAPI()` | Warnings for OpenAPI source |
135
+ | `getWarningsForWellKnown()` | Warnings for well-known source |
136
+ | `getWarningsForL2()` | Warnings for route list |
137
+ | `getWarningsForL3()` | Warnings for endpoint metadata |
138
+ | `getWarningsForL4()` | Warnings for guidance layer |
139
+ | `AUDIT_CODES` | Stable audit code constants |
176
140
 
177
- Required normalized fields:
141
+ Ownership boundary:
178
142
 
179
- - `resourceKey`
180
- - `origin`
181
- - `method`
182
- - `path`
183
- - `source`
184
- - `verified` (default `false`)
143
+ - `@agentcash/discovery` owns discovery/advisory contracts.
144
+ - `@agentcash` should own all signing logic, but should be composable with the methods for probing built in this package.
185
145
 
186
146
  Philosophy boundary:
187
147
 
188
148
  - Machine-parsable discovery metadata belongs in OpenAPI.
189
- - `llms.txt` is optional, unstructured guidance.
190
149
  - Discovery is advisory. Runtime payment challenge/probe is authoritative.
191
-
192
- ## Internal Registry Audit Harness
193
-
194
- For x402scan registry benchmarking:
195
-
196
- ```bash
197
- SCAN_DATABASE_URL='postgresql://...' pnpm audit:registry
198
- ```
199
-
200
- Quick sample:
201
-
202
- ```bash
203
- SCAN_DATABASE_URL='postgresql://...' pnpm audit:registry:quick
204
- ```
205
-
206
- Output:
207
-
208
- - `audit/registry-audit-<timestamp>.json`
209
- - `audit/registry-audit-latest.json`
210
-
211
- ## Deeper Docs
212
-
213
- Architecture and planning artifacts live in `.claude/`.
214
-
215
- Validation design doc:
216
-
217
- - `docs/VALIDATION_DIAGNOSTICS_DESIGN_2026-03-03.md`
package/dist/cli.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/cli.ts
@@ -76,6 +86,7 @@ var OpenApiDocSchema = import_zod.z.object({
76
86
  description: import_zod.z.string().optional(),
77
87
  guidance: import_zod.z.string().optional()
78
88
  }),
89
+ security: import_zod.z.array(import_zod.z.record(import_zod.z.string(), import_zod.z.array(import_zod.z.string()))).optional(),
79
90
  servers: import_zod.z.array(import_zod.z.object({ url: import_zod.z.string() })).optional(),
80
91
  tags: import_zod.z.array(import_zod.z.object({ name: import_zod.z.string() })).optional(),
81
92
  components: import_zod.z.object({ securitySchemes: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional() }).optional(),
@@ -105,20 +116,31 @@ var WellKnownParsedSchema = import_zod.z.object({
105
116
  function isRecord(value) {
106
117
  return value !== null && typeof value === "object" && !Array.isArray(value);
107
118
  }
108
- function hasSecurity(operation, scheme) {
109
- return operation.security?.some((s) => scheme in s) ?? false;
110
- }
111
- function has402Response(operation) {
112
- return Boolean(operation.responses?.["402"]);
119
+ function resolveSecurityFlags(requirements, securitySchemes) {
120
+ let hasApiKey = false;
121
+ let hasSiwx = false;
122
+ for (const requirement of requirements) {
123
+ for (const schemeName of Object.keys(requirement)) {
124
+ if (schemeName === "siwx") {
125
+ hasSiwx = true;
126
+ continue;
127
+ }
128
+ if (schemeName === "apiKey") {
129
+ hasApiKey = true;
130
+ continue;
131
+ }
132
+ const def = securitySchemes[schemeName];
133
+ if (isRecord(def) && def["type"] === "apiKey") hasApiKey = true;
134
+ }
135
+ }
136
+ return { hasApiKey, hasSiwx };
113
137
  }
114
- function inferAuthMode(operation) {
138
+ function inferAuthMode(operation, globalSecurity, securitySchemes) {
115
139
  const hasXPaymentInfo = Boolean(operation["x-payment-info"]);
116
- const hasPayment = hasXPaymentInfo || has402Response(operation);
117
- const hasApiKey = hasSecurity(operation, "apiKey");
118
- const hasSiwx = hasSecurity(operation, "siwx");
119
- if (hasPayment && hasApiKey) return "apiKey+paid";
140
+ const effectiveSecurity = operation.security !== void 0 && operation.security.length > 0 ? operation.security : globalSecurity ?? [];
141
+ const { hasApiKey, hasSiwx } = resolveSecurityFlags(effectiveSecurity, securitySchemes ?? {});
142
+ if (hasXPaymentInfo && hasApiKey) return "apiKey+paid";
120
143
  if (hasXPaymentInfo) return "paid";
121
- if (hasPayment) return hasSiwx ? "siwx" : "paid";
122
144
  if (hasApiKey) return "apiKey";
123
145
  if (hasSiwx) return "siwx";
124
146
  return void 0;
@@ -138,10 +160,12 @@ var HTTP_METHODS = /* @__PURE__ */ new Set([
138
160
  var DEFAULT_MISSING_METHOD = "POST";
139
161
 
140
162
  // src/core/lib/url.ts
141
- function normalizeOrigin(target) {
163
+ function ensureProtocol(target) {
142
164
  const trimmed = target.trim();
143
- const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
144
- const url = new URL(withProtocol);
165
+ return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
166
+ }
167
+ function normalizeOrigin(target) {
168
+ const url = new URL(ensureProtocol(target));
145
169
  url.pathname = "";
146
170
  url.search = "";
147
171
  url.hash = "";
@@ -180,7 +204,7 @@ function fetchSafe(url, init) {
180
204
  }
181
205
 
182
206
  // src/mmm-enabled.ts
183
- var isMmmEnabled = () => "1.0.1".includes("-mmm");
207
+ var isMmmEnabled = () => "1.1.0".includes("-mmm");
184
208
 
185
209
  // src/core/source/openapi/index.ts
186
210
  var OpenApiParsedSchema = OpenApiDocSchema.transform((doc) => {
@@ -189,15 +213,13 @@ var OpenApiParsedSchema = OpenApiDocSchema.transform((doc) => {
189
213
  for (const httpMethod of [...HTTP_METHODS]) {
190
214
  const operation = pathItem[httpMethod.toLowerCase()];
191
215
  if (!operation) continue;
192
- const authMode = inferAuthMode(operation) ?? void 0;
216
+ const authMode = inferAuthMode(operation, doc.security, doc.components?.securitySchemes) ?? void 0;
193
217
  if (!authMode) continue;
194
218
  const p = operation["x-payment-info"];
195
219
  const protocols = (p?.protocols ?? []).filter(
196
220
  (proto) => proto !== "mpp" || isMmmEnabled()
197
221
  );
198
- if ((authMode === "paid" || authMode === "siwx") && !has402Response(operation)) continue;
199
- if (authMode === "paid" && protocols.length === 0) continue;
200
- const pricing = authMode === "paid" && p ? {
222
+ const pricing = (authMode === "paid" || authMode === "apiKey+paid") && p ? {
201
223
  pricingMode: p.pricingMode,
202
224
  ...p.price ? { price: p.price } : {},
203
225
  ...p.minPrice ? { minPrice: p.minPrice } : {},
@@ -348,6 +370,7 @@ var AUDIT_CODES = {
348
370
  L2_NO_ROUTES: "L2_NO_ROUTES",
349
371
  L2_ROUTE_COUNT_HIGH: "L2_ROUTE_COUNT_HIGH",
350
372
  L2_AUTH_MODE_MISSING: "L2_AUTH_MODE_MISSING",
373
+ L2_NO_PAID_ROUTES: "L2_NO_PAID_ROUTES",
351
374
  L2_PRICE_MISSING_ON_PAID: "L2_PRICE_MISSING_ON_PAID",
352
375
  L2_PROTOCOLS_MISSING_ON_PAID: "L2_PROTOCOLS_MISSING_ON_PAID",
353
376
  // ─── L3 endpoint advisory checks ─────────────────────────────────────────────
@@ -409,6 +432,15 @@ function getWarningsForL2(l2) {
409
432
  });
410
433
  return warnings;
411
434
  }
435
+ const hasPaidRoute = l2.routes.some((r) => r.authMode === "paid" || r.authMode === "apiKey+paid");
436
+ if (!hasPaidRoute) {
437
+ warnings.push({
438
+ code: AUDIT_CODES.L2_NO_PAID_ROUTES,
439
+ severity: "info",
440
+ message: "No endpoints are marked as paid or apiKey+paid.",
441
+ hint: "Add x-payment-info to operations that require payment so agents know which endpoints are monetized."
442
+ });
443
+ }
412
444
  if (l2.routes.length > ROUTE_COUNT_HIGH) {
413
445
  warnings.push({
414
446
  code: AUDIT_CODES.L2_ROUTE_COUNT_HIGH,
@@ -495,8 +527,8 @@ function getWarningsForL4(l4) {
495
527
  {
496
528
  code: AUDIT_CODES.L4_GUIDANCE_MISSING,
497
529
  severity: "info",
498
- message: "No guidance text found (llms.txt or OpenAPI info.guidance).",
499
- hint: "Add an info.guidance field to your OpenAPI spec or expose /llms.txt for agent-readable instructions."
530
+ message: "No guidance text found (OpenAPI info.guidance).",
531
+ hint: "Add an info.guidance field to your OpenAPI spec for agent-readable instructions."
500
532
  }
501
533
  ];
502
534
  }
@@ -805,54 +837,31 @@ function extractPaymentOptions4(wwwAuthenticate) {
805
837
  return options;
806
838
  }
807
839
 
808
- // src/core/layers/l3.ts
809
- function findMatchingOpenApiPath(paths, targetPath) {
810
- const exact = paths[targetPath];
811
- if (isRecord(exact)) return { matchedPath: targetPath, pathItem: exact };
812
- for (const [specPath, entry] of Object.entries(paths)) {
813
- if (!isRecord(entry)) continue;
814
- const pattern = specPath.replace(/\{[^}]+\}/g, "[^/]+");
815
- const regex = new RegExp(`^${pattern}$`);
816
- if (regex.test(targetPath)) {
817
- return { matchedPath: specPath, pathItem: entry };
818
- }
819
- }
820
- return null;
821
- }
822
- function resolveRef(document, ref, seen) {
823
- if (!ref.startsWith("#/")) return void 0;
824
- if (seen.has(ref)) return { $circular: ref };
825
- seen.add(ref);
826
- const parts = ref.slice(2).split("/");
827
- let current = document;
828
- for (const part of parts) {
829
- if (!isRecord(current)) return void 0;
830
- current = current[part];
831
- if (current === void 0) return void 0;
832
- }
833
- if (isRecord(current)) return resolveRefs(document, current, seen);
834
- return current;
840
+ // src/core/lib/resolve-ref.ts
841
+ var import_dereference_json_schema = __toESM(require("dereference-json-schema"), 1);
842
+ var { resolveRefSync } = import_dereference_json_schema.default;
843
+ function isRecord2(value) {
844
+ return value !== null && typeof value === "object" && !Array.isArray(value);
835
845
  }
836
- function resolveRefs(document, obj, seen, depth = 0) {
837
- if (depth > 4) return obj;
846
+ function deepResolveRefs(document, obj) {
838
847
  const resolved = {};
839
848
  for (const [key, value] of Object.entries(obj)) {
840
849
  if (key === "$ref" && typeof value === "string") {
841
- const deref = resolveRef(document, value, seen);
842
- if (isRecord(deref)) {
843
- Object.assign(resolved, deref);
850
+ const deref = resolveRefSync(document, value);
851
+ if (isRecord2(deref)) {
852
+ Object.assign(resolved, deepResolveRefs(document, deref));
844
853
  } else {
845
854
  resolved[key] = value;
846
855
  }
847
856
  continue;
848
857
  }
849
- if (isRecord(value)) {
850
- resolved[key] = resolveRefs(document, value, seen, depth + 1);
858
+ if (isRecord2(value)) {
859
+ resolved[key] = deepResolveRefs(document, value);
851
860
  continue;
852
861
  }
853
862
  if (Array.isArray(value)) {
854
863
  resolved[key] = value.map(
855
- (item) => isRecord(item) ? resolveRefs(document, item, seen, depth + 1) : item
864
+ (item) => isRecord2(item) ? deepResolveRefs(document, item) : item
856
865
  );
857
866
  continue;
858
867
  }
@@ -860,6 +869,24 @@ function resolveRefs(document, obj, seen, depth = 0) {
860
869
  }
861
870
  return resolved;
862
871
  }
872
+ function resolveRefs(obj, document) {
873
+ return deepResolveRefs(document, obj);
874
+ }
875
+
876
+ // src/core/layers/l3.ts
877
+ function findMatchingOpenApiPath(paths, targetPath) {
878
+ const exact = paths[targetPath];
879
+ if (isRecord(exact)) return { matchedPath: targetPath, pathItem: exact };
880
+ for (const [specPath, entry] of Object.entries(paths)) {
881
+ if (!isRecord(entry)) continue;
882
+ const pattern = specPath.replace(/\{[^}]+\}/g, "[^/]+");
883
+ const regex = new RegExp(`^${pattern}$`);
884
+ if (regex.test(targetPath)) {
885
+ return { matchedPath: specPath, pathItem: entry };
886
+ }
887
+ }
888
+ return null;
889
+ }
863
890
  function extractRequestBodySchema(operationSchema) {
864
891
  const requestBody = operationSchema.requestBody;
865
892
  if (!isRecord(requestBody)) return void 0;
@@ -939,7 +966,7 @@ function getL3ForOpenAPI(openApi, path, method) {
939
966
  if (!matched) return null;
940
967
  const operation = matched.pathItem[method.toLowerCase()];
941
968
  if (!isRecord(operation)) return null;
942
- const resolvedOperation = resolveRefs(document, operation, /* @__PURE__ */ new Set());
969
+ const resolvedOperation = resolveRefs(operation, document);
943
970
  const summary = typeof resolvedOperation.summary === "string" ? resolvedOperation.summary : typeof resolvedOperation.description === "string" ? resolvedOperation.description : void 0;
944
971
  return {
945
972
  source: "openapi",
@@ -985,12 +1012,12 @@ function getAdvisoriesForProbe(probe, path) {
985
1012
  });
986
1013
  }
987
1014
  async function checkEndpointSchema(options) {
988
- const endpoint = new URL(options.url);
1015
+ const endpoint = new URL(ensureProtocol(options.url));
989
1016
  const origin = normalizeOrigin(endpoint.origin);
990
1017
  const path = normalizePath(endpoint.pathname || "/");
991
1018
  if (options.sampleInputBody !== void 0) {
992
1019
  const probeResult2 = await getProbe(
993
- options.url,
1020
+ endpoint.href,
994
1021
  options.headers,
995
1022
  options.signal,
996
1023
  options.sampleInputBody
@@ -1015,7 +1042,7 @@ async function checkEndpointSchema(options) {
1015
1042
  if (advisories2.length > 0) return { found: true, origin, path, advisories: advisories2 };
1016
1043
  return { found: false, origin, path, cause: "not_found" };
1017
1044
  }
1018
- const probeResult = await getProbe(options.url, options.headers, options.signal);
1045
+ const probeResult = await getProbe(endpoint.href, options.headers, options.signal);
1019
1046
  if (probeResult.isErr()) {
1020
1047
  return {
1021
1048
  found: false,