@bbradar/mcp 0.1.19 → 0.2.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 +74 -2
- package/benchmark/mcp-benchmark.mjs +167 -0
- package/dist/bbradarClient.d.ts +158 -3
- package/dist/bbradarClient.js +797 -57
- package/dist/bbradarClient.js.map +1 -1
- package/dist/cache.d.ts +13 -2
- package/dist/cache.js +36 -7
- package/dist/cache.js.map +1 -1
- package/dist/config.d.ts +11 -0
- package/dist/config.js +53 -6
- package/dist/config.js.map +1 -1
- package/dist/cursorStore.d.ts +33 -0
- package/dist/cursorStore.js +123 -0
- package/dist/cursorStore.js.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/sanitize.d.ts +1 -0
- package/dist/sanitize.js +164 -7
- package/dist/sanitize.js.map +1 -1
- package/dist/server.js +1119 -161
- package/dist/server.js.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @bbradar/mcp
|
|
2
2
|
|
|
3
|
-
Local STDIO MCP server for [bbradar.io](https://bbradar.io) Pro subscribers. It lets MCP clients query [bbradar.io](https://bbradar.io)
|
|
3
|
+
Local STDIO MCP server for [bbradar.io](https://bbradar.io) Pro subscribers. It lets MCP clients query [bbradar.io](https://bbradar.io) platform and Self-Hosted program profiles, active targets, target-level opportunity intelligence, recent target changes, and repository-change events through a local npm package.
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
@@ -106,13 +106,84 @@ claude mcp add bbradar_io --scope user --env BBRADAR_API_KEY="your_pro_mcp_api_k
|
|
|
106
106
|
|
|
107
107
|
After adding the MCP config, restart your client and call `get_mcp_status`. A working setup returns server status, timing, and rate-limit metadata.
|
|
108
108
|
|
|
109
|
+
Every server process negotiates once against `GET /api/v1/pro/capabilities` and retains the contract only in memory. `get_mcp_status` reports the negotiated version and endpoint set. Negotiation is best-effort by default; when startup validation is enabled, authentication or entitlement failures stop startup. Older API deployments without the capability route fall back to the program-list validation probe.
|
|
110
|
+
|
|
109
111
|
For simple tool selection, call `get_bbradar_guide`. It returns the recommended entry points, common filter meanings, and example calls without contacting the BBRadar API.
|
|
110
112
|
|
|
111
113
|
Most tools that accept `program_id` also accept a human program name or handle. The server resolves names automatically when there is one clear match; for ambiguous names, call `resolve_program` first and retry with the returned `platform:handle` id.
|
|
112
114
|
|
|
115
|
+
Self-Hosted programs use IDs such as `Self-Hosted:program-slug`. They are available through program search, exact program detail, and program-target tools. Their `data_capabilities` field identifies unavailable source signals; the MCP server does not invent opportunity scores, public report counts, target-change history, submission costs, or repository changes for that catalog.
|
|
116
|
+
|
|
117
|
+
`search_programs` accepts a `search` value and sends it to the API for indexed server-side filtering. Name resolution uses the same path automatically, avoiding broad page scans for exact or partial program names.
|
|
118
|
+
|
|
113
119
|
`find_programs_by_target` defaults to flexible target matching. It accepts bare domains, full URLs, wildcard scopes, target names, package/repo-style identifiers, or short phrases like `known api target`.
|
|
120
|
+
It uses the Pro API's direct reverse-target-search route first, including for Self-Hosted targets, and only falls back to sampled target lists when an older API deployment does not expose that route.
|
|
121
|
+
|
|
122
|
+
`export_targets` uses the bounded bearer `POST /api/v1/pro/targets/export` route directly. JSON exports preserve `program_id` on every target; CSV previews are capped in the tool result while the complete payload remains available for ten minutes through the returned `bbradar://exports` resource. The local resource store is capped at 25 entries and 64 MiB in aggregate, and CSV formula/control-byte neutralization is applied independently of the API.
|
|
123
|
+
|
|
124
|
+
`get_program_targets` forwards text, target type, eligibility, minimum Target Opportunity Score, Target Opportunity Label, and competition-risk filters to the API. Compact output retains the score, label, competition risk, and Why This Target; full output also includes the score breakdown and reason fields.
|
|
125
|
+
|
|
126
|
+
`get_repository_changes` exposes the read-only repository event feed. It defaults to `include_targets=false` and compact output, never contains source code or diffs, and supports `after_id` cursor polling. Both change-feed tools keep at most 500 cursors keyed separately by endpoint and normalized filters. Seed a stream once with `after_id`, then use `resume_cursor=true`; the server stores `next_after_id` even for an empty page. Cursor reads continue sequentially while `has_more=true`, bounded by `max_cursor_pages` (default `2`) and `cursor_time_budget_ms` (default `3000`). Cursor requests bypass the response cache so an empty page cannot hide newly arriving changes.
|
|
127
|
+
|
|
128
|
+
Cursors are memory-only by default. Set `BBRADAR_MCP_CURSOR_STATE_PATH` to opt into restart-safe cursor persistence. The state file contains only endpoint-prefixed SHA-256 filter hashes and non-negative numeric IDs—never raw filters, API responses, program or target records, credentials, or authorization headers.
|
|
129
|
+
|
|
130
|
+
Large target and change collections return at most 50 inline records by default. The remainder is held in a bounded, ten-minute in-memory continuation and exposed through a `bbradar://continuations` resource link. This keeps model context small without losing access to the complete bounded result.
|
|
131
|
+
|
|
132
|
+
## Performance Tuning
|
|
133
|
+
|
|
134
|
+
The defaults expose the 16-tool core surface and compact text results while enabling bounded concurrency, positive response caching, in-flight request coalescing, stale-while-revalidate program metadata, endpoint circuit breakers, and short negative caching for repeated 404/405 lookups. Set the tool profile to `full` when all specialized tools are needed.
|
|
135
|
+
|
|
136
|
+
| Environment variable | Default | Purpose |
|
|
137
|
+
| --- | ---: | --- |
|
|
138
|
+
| `BBRADAR_MCP_CACHE_TTL_SECONDS` | `30` | Successful GET response cache lifetime. Set to `0` to disable. |
|
|
139
|
+
| `BBRADAR_MCP_NEGATIVE_CACHE_TTL_SECONDS` | `15` | Repeated 404/405 cache lifetime. Set to `0` to disable. |
|
|
140
|
+
| `BBRADAR_MCP_CACHE_MAX_ENTRIES` | `500` | Maximum entries in each local response cache. |
|
|
141
|
+
| `BBRADAR_MCP_MAX_CONCURRENT_REQUESTS` | `6` | Maximum simultaneous upstream BBRadar API requests. |
|
|
142
|
+
| `BBRADAR_MCP_REQUEST_TIMEOUT_SECONDS` | `30` | Timeout for each active upstream request; queue time is excluded. |
|
|
143
|
+
| `BBRADAR_MCP_COMPACT_TOOL_TEXT` | `true` | Avoid duplicating full results for clients that consume MCP `structuredContent`. Set to `false` for legacy text-only clients. |
|
|
144
|
+
| `BBRADAR_MCP_TOOL_PROFILE` | `core` | Compact 16-tool discovery surface. Set to `full` for every specialized tool. |
|
|
145
|
+
| `BBRADAR_MCP_PROGRAM_STALE_WHILE_REVALIDATE_SECONDS` | `300` | Maximum stale window for exact program metadata while one background refresh runs. |
|
|
146
|
+
| `BBRADAR_MCP_CIRCUIT_BREAKER_FAILURE_THRESHOLD` | `3` | Consecutive retryable endpoint failures before its circuit opens. |
|
|
147
|
+
| `BBRADAR_MCP_CIRCUIT_BREAKER_COOLDOWN_SECONDS` | `30` | Endpoint circuit cooldown before a new request is admitted. |
|
|
148
|
+
| `BBRADAR_MCP_CURSOR_STATE_PATH` | unset | Optional path for numeric cursor-only restart persistence. API responses are never written there. |
|
|
149
|
+
| `BBRADAR_MCP_CONTINUATION_INLINE_LIMIT` | `50` | Maximum records kept inline before a local continuation resource is returned. |
|
|
150
|
+
| `BBRADAR_MCP_CONTINUATION_TTL_SECONDS` | `600` | Lifetime of in-memory continuation results. |
|
|
151
|
+
| `BBRADAR_MCP_ALLOW_INSECURE_HTTP` | `false` | Explicitly allow an HTTP API base URL outside loopback for a trusted private development network. Never enable for internet traffic. |
|
|
152
|
+
|
|
153
|
+
No performance variables are required. Capability-derived page limits, caching, request coalescing, server-side name resolution, reverse-target search, early stopping, bounded concurrency, aggregate-call parallelism, a 64 KiB request-body ceiling, a 16 MiB upstream response ceiling, automatic HTTP compression handling, connection reuse, and upstream quota protection are always active. Unsupported capability-gated tools are removed from discovery. Target responses expose upstream pagination state so an AI client does not mistake a capped page for a complete inventory.
|
|
154
|
+
|
|
155
|
+
The client learns the API's `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers per endpoint group. Once an observed group reaches zero, it blocks additional requests to that group locally until reset instead of repeatedly hitting the API, while unrelated endpoint groups remain usable. Target and repository changes share one quota state and one conservative two-request concurrency pool. A rolling 60-second guard also enforces the documented shared key-and-IP ceiling across uncached upstream requests. Real `423`, `429`, and `503` responses honor `Retry-After` as a local cooldown, capped at ten minutes so a malformed upstream header cannot lock the MCP process indefinitely. Requests are not automatically retried, so one MCP action cannot amplify an admission or rate-limit response into more API calls. The global upstream concurrency default is six and bulk exports are capped at two; cursor page walking is strictly sequential.
|
|
156
|
+
|
|
157
|
+
The bearer API destination must use HTTPS. Loopback HTTP remains available for local development; non-loopback HTTP requires the explicit `BBRADAR_MCP_ALLOW_INSECURE_HTTP=true` opt-in. API base URLs containing embedded credentials, query strings, fragments, or non-HTTP schemes are rejected before the key can be sent.
|
|
158
|
+
|
|
159
|
+
Call `get_mcp_status` to inspect cache and stale-hit rates, circuit states, cursor persistence, concurrency, negotiated capabilities, upstream quotas, actual request counts, per-endpoint status totals, and timing.
|
|
160
|
+
|
|
161
|
+
Run `npm run benchmark` for the deterministic local performance benchmark. It reports request and byte reductions for reverse target discovery, exact-program cache efficiency, and context reduction from bounded continuations.
|
|
162
|
+
|
|
163
|
+
### Flexible multi-signal discovery
|
|
164
|
+
|
|
165
|
+
Use `find_programs` as the generic ranked path for combinations of platform, reward, opportunity, scope breadth, technology, competition, and freshness signals. It supports both inclusion and exclusion through `platforms`, `exclude_platforms`, `web3`, `exclude_web3`, and the remaining scope/reward/activity filters. When a user describes an exclusion without naming exact platform values, call `list_filters` first and reuse the observed values. If an intentionally strict combination returns no candidates, relax one local threshold at a time; the upstream program pages are cached, so these fallback passes normally add no API requests.
|
|
166
|
+
|
|
167
|
+
For repository-aware hunting, call `get_repository_changes` and `find_programs` in parallel with the same requested platform set. The repository feed supplies recent events and scored targets while `find_programs` supplies reward and ranking data. Intersect their program ids, discard non-requested programs that merely share the same repository, and treat targets as event-level associations when the feed omits `program_id`. This remains generic across platforms and technology stacks and avoids a specialized tool for every prompt variation.
|
|
168
|
+
|
|
169
|
+
### Optimized wildcard discovery
|
|
170
|
+
|
|
171
|
+
For the latest added wildcard targets from one platform with reward and report-count limits, use `get_latest_added_targets`:
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"target_type": "wildcard",
|
|
176
|
+
"platforms": ["HackerOne"],
|
|
177
|
+
"min_reward": 5000,
|
|
178
|
+
"max_public_report_count": 10,
|
|
179
|
+
"require_known_report_count": true,
|
|
180
|
+
"include_full_target_list": false
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
This path uses one recent-changes request. Use `find_wildcard_programs` for a ranked current-program shortlist, or `find_recently_added_wildcards` when recent wildcard activity matters. Both accept platform, minimum reward, report-count, and bounded target-sample filters. Wildcard shortlist workflows automatically send the `wildcard` target-type tag upstream before applying reward and report filters locally, so their page budget is spent only on relevant programs. `find_low_competition_high_reward` also accepts `min_wildcards`.
|
|
114
185
|
|
|
115
|
-
##
|
|
186
|
+
## Full-profile tools
|
|
116
187
|
|
|
117
188
|
- `search_programs`
|
|
118
189
|
- `get_program`
|
|
@@ -120,6 +191,7 @@ Most tools that accept `program_id` also accept a human program name or handle.
|
|
|
120
191
|
- `run_program_name_action`
|
|
121
192
|
- `get_program_targets`
|
|
122
193
|
- `get_recent_changes`
|
|
194
|
+
- `get_repository_changes`
|
|
123
195
|
- `get_opportunities`
|
|
124
196
|
- `find_programs`
|
|
125
197
|
- `find_wildcard_programs`
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
|
|
4
|
+
import { BBRadarClient } from "../dist/bbradarClient.js";
|
|
5
|
+
|
|
6
|
+
const programCount = 100;
|
|
7
|
+
const targetsPerProgram = 20;
|
|
8
|
+
const programs = Array.from({ length: programCount }, (_, index) => ({
|
|
9
|
+
id: `Platform:program-${index}`,
|
|
10
|
+
name: `Program ${index}`,
|
|
11
|
+
platform: "Platform"
|
|
12
|
+
}));
|
|
13
|
+
const targets = new Map(programs.map((program, programIndex) => [
|
|
14
|
+
program.id,
|
|
15
|
+
Array.from({ length: targetsPerProgram }, (_, targetIndex) => ({
|
|
16
|
+
identifier: `api-${targetIndex}.program-${programIndex}.example`,
|
|
17
|
+
target_type: "domain",
|
|
18
|
+
in_scope: true,
|
|
19
|
+
eligible_for_bounty: true
|
|
20
|
+
}))
|
|
21
|
+
]));
|
|
22
|
+
const counters = { requests: 0, responseBytes: 0 };
|
|
23
|
+
|
|
24
|
+
const api = http.createServer((req, res) => {
|
|
25
|
+
counters.requests += 1;
|
|
26
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
27
|
+
let payload;
|
|
28
|
+
|
|
29
|
+
if (url.pathname === "/api/v1/pro/programs") {
|
|
30
|
+
payload = { programs, meta: { total: programs.length, total_pages: 1 } };
|
|
31
|
+
} else if (url.pathname === "/api/v1/pro/targets/search") {
|
|
32
|
+
payload = {
|
|
33
|
+
results: [{ program: programs[42], target: targets.get(programs[42].id)[0] }],
|
|
34
|
+
meta: { total: 1, returned: 1 }
|
|
35
|
+
};
|
|
36
|
+
} else if (url.pathname === "/api/v1/pro/programs/Platform:program-42") {
|
|
37
|
+
payload = programs[42];
|
|
38
|
+
} else {
|
|
39
|
+
const match = url.pathname.match(/^\/api\/v1\/pro\/programs\/(.+)\/targets$/);
|
|
40
|
+
payload = match
|
|
41
|
+
? { targets: targets.get(decodeURIComponent(match[1])) ?? [], meta: { total: targetsPerProgram } }
|
|
42
|
+
: { detail: "not found" };
|
|
43
|
+
if (!match) {
|
|
44
|
+
res.statusCode = 404;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const body = JSON.stringify(payload);
|
|
49
|
+
counters.responseBytes += Buffer.byteLength(body);
|
|
50
|
+
res.setHeader("content-type", "application/json");
|
|
51
|
+
res.end(body);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await new Promise((resolve) => api.listen(0, "127.0.0.1", resolve));
|
|
55
|
+
const { port } = api.address();
|
|
56
|
+
const baseUrl = `http://127.0.0.1:${port}/api/v1`;
|
|
57
|
+
const headers = { authorization: "Bearer benchmark" };
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
resetCounters();
|
|
61
|
+
const baselineStarted = performance.now();
|
|
62
|
+
const listed = await getJson(`${baseUrl}/pro/programs`, headers);
|
|
63
|
+
let baselineMatch;
|
|
64
|
+
for (const program of listed.programs) {
|
|
65
|
+
const response = await getJson(`${baseUrl}/pro/programs/${encodeURIComponent(program.id)}/targets`, headers);
|
|
66
|
+
baselineMatch = response.targets.find((target) => target.identifier === "api-0.program-42.example") ?? baselineMatch;
|
|
67
|
+
}
|
|
68
|
+
const baseline = snapshot(performance.now() - baselineStarted);
|
|
69
|
+
|
|
70
|
+
resetCounters();
|
|
71
|
+
const optimizedStarted = performance.now();
|
|
72
|
+
const optimizedMatch = await getJson(
|
|
73
|
+
`${baseUrl}/pro/targets/search?q=api-0.program-42.example&match_mode=exact&page=1&page_size=1`,
|
|
74
|
+
headers
|
|
75
|
+
);
|
|
76
|
+
const optimized = snapshot(performance.now() - optimizedStarted);
|
|
77
|
+
|
|
78
|
+
const client = new BBRadarClient({
|
|
79
|
+
apiKey: "benchmark",
|
|
80
|
+
apiBaseUrl: baseUrl,
|
|
81
|
+
webBaseUrl: "https://bbradar.io",
|
|
82
|
+
cacheTtlMs: 30_000,
|
|
83
|
+
negativeCacheTtlMs: 15_000,
|
|
84
|
+
cacheMaxEntries: 500,
|
|
85
|
+
defaultRateLimitPerMinute: 60,
|
|
86
|
+
exportRateLimitPerMinute: 15,
|
|
87
|
+
maxConcurrentRequests: 6,
|
|
88
|
+
compactToolText: true,
|
|
89
|
+
toolProfile: "core",
|
|
90
|
+
requestTimeoutMs: 5_000,
|
|
91
|
+
validateOnStartup: false
|
|
92
|
+
});
|
|
93
|
+
resetCounters();
|
|
94
|
+
const cacheStarted = performance.now();
|
|
95
|
+
for (let index = 0; index < 50; index += 1) {
|
|
96
|
+
await client.getProgram("Platform:program-42");
|
|
97
|
+
}
|
|
98
|
+
const cachedMetadata = snapshot(performance.now() - cacheStarted);
|
|
99
|
+
|
|
100
|
+
const largeResult = Array.from({ length: 500 }, (_, index) => ({
|
|
101
|
+
identifier: `target-${index}.example`,
|
|
102
|
+
target_opportunity_score: 70 + (index % 20),
|
|
103
|
+
why_this_target: "Compact benchmark fixture"
|
|
104
|
+
}));
|
|
105
|
+
const fullContextBytes = Buffer.byteLength(JSON.stringify(largeResult));
|
|
106
|
+
const inlineContextBytes = Buffer.byteLength(JSON.stringify(largeResult.slice(0, 50)));
|
|
107
|
+
const requestReduction = percentReduction(baseline.requests, optimized.requests);
|
|
108
|
+
const byteReduction = percentReduction(baseline.response_bytes, optimized.response_bytes);
|
|
109
|
+
const contextReduction = percentReduction(fullContextBytes, inlineContextBytes);
|
|
110
|
+
|
|
111
|
+
if (
|
|
112
|
+
requestReduction < 90 ||
|
|
113
|
+
byteReduction < 90 ||
|
|
114
|
+
cachedMetadata.requests !== 1 ||
|
|
115
|
+
contextReduction < 80
|
|
116
|
+
) {
|
|
117
|
+
throw new Error("Performance regression threshold failed.");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
process.stdout.write(`${JSON.stringify({
|
|
121
|
+
fixture: { program_count: programCount, targets_per_program: targetsPerProgram },
|
|
122
|
+
reverse_target_discovery: {
|
|
123
|
+
baseline,
|
|
124
|
+
optimized,
|
|
125
|
+
request_reduction_percent: requestReduction,
|
|
126
|
+
byte_reduction_percent: byteReduction,
|
|
127
|
+
match_found: Boolean(baselineMatch && optimizedMatch.results?.length === 1)
|
|
128
|
+
},
|
|
129
|
+
repeated_exact_program_metadata: {
|
|
130
|
+
calls: 50,
|
|
131
|
+
upstream: cachedMetadata,
|
|
132
|
+
cache_hit_ratio: client.diagnostics().response_cache.hits / 50
|
|
133
|
+
},
|
|
134
|
+
bounded_model_context: {
|
|
135
|
+
full_500_record_bytes: fullContextBytes,
|
|
136
|
+
inline_50_record_bytes: inlineContextBytes,
|
|
137
|
+
byte_reduction_percent: contextReduction
|
|
138
|
+
}
|
|
139
|
+
}, null, 2)}\n`);
|
|
140
|
+
} finally {
|
|
141
|
+
api.close();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function getJson(url, requestHeaders) {
|
|
145
|
+
const response = await fetch(url, { headers: requestHeaders });
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
throw new Error(`Benchmark request failed: ${response.status}`);
|
|
148
|
+
}
|
|
149
|
+
return response.json();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function resetCounters() {
|
|
153
|
+
counters.requests = 0;
|
|
154
|
+
counters.responseBytes = 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function snapshot(durationMs) {
|
|
158
|
+
return {
|
|
159
|
+
requests: counters.requests,
|
|
160
|
+
response_bytes: counters.responseBytes,
|
|
161
|
+
duration_ms: Math.round(durationMs * 100) / 100
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function percentReduction(before, after) {
|
|
166
|
+
return before === 0 ? 0 : Math.round((1 - after / before) * 10_000) / 100;
|
|
167
|
+
}
|
package/dist/bbradarClient.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type CacheStats } from "./cache.js";
|
|
2
1
|
import type { ServerConfig } from "./config.js";
|
|
3
2
|
export type QueryValue = string | number | boolean | null | undefined | readonly string[] | readonly number[] | readonly boolean[];
|
|
4
3
|
export type ApiRequestOptions = {
|
|
@@ -8,6 +7,7 @@ export type ApiRequestOptions = {
|
|
|
8
7
|
body?: unknown;
|
|
9
8
|
cache?: boolean;
|
|
10
9
|
coalesce?: boolean;
|
|
10
|
+
staleWhileRevalidateMs?: number;
|
|
11
11
|
};
|
|
12
12
|
export type ApiResult<T> = {
|
|
13
13
|
data: T;
|
|
@@ -18,6 +18,13 @@ export type ApiResult<T> = {
|
|
|
18
18
|
status: number;
|
|
19
19
|
fetchedAt: string;
|
|
20
20
|
cacheExpiresAt?: string | undefined;
|
|
21
|
+
stale?: boolean | undefined;
|
|
22
|
+
};
|
|
23
|
+
type CapabilityNegotiationState = {
|
|
24
|
+
status: "not_started" | "negotiated" | "legacy" | "failed";
|
|
25
|
+
negotiatedAt?: string | undefined;
|
|
26
|
+
contract?: unknown;
|
|
27
|
+
error?: string | undefined;
|
|
21
28
|
};
|
|
22
29
|
export declare class BBRadarApiError extends Error {
|
|
23
30
|
readonly requestId: string;
|
|
@@ -25,6 +32,9 @@ export declare class BBRadarApiError extends Error {
|
|
|
25
32
|
readonly status: number | undefined;
|
|
26
33
|
readonly detail?: unknown;
|
|
27
34
|
readonly errors?: unknown;
|
|
35
|
+
readonly cached: boolean;
|
|
36
|
+
readonly coalesced: boolean;
|
|
37
|
+
readonly cacheExpiresAt: string | undefined;
|
|
28
38
|
constructor(params: {
|
|
29
39
|
message: string;
|
|
30
40
|
requestId: string;
|
|
@@ -32,30 +42,175 @@ export declare class BBRadarApiError extends Error {
|
|
|
32
42
|
status?: number | undefined;
|
|
33
43
|
detail?: unknown;
|
|
34
44
|
errors?: unknown;
|
|
45
|
+
cached?: boolean | undefined;
|
|
46
|
+
coalesced?: boolean | undefined;
|
|
47
|
+
cacheExpiresAt?: string | undefined;
|
|
35
48
|
cause?: unknown;
|
|
36
49
|
});
|
|
37
50
|
}
|
|
38
51
|
export declare class BBRadarClient {
|
|
39
52
|
private readonly config;
|
|
40
53
|
private readonly cache;
|
|
54
|
+
private readonly negativeCache;
|
|
41
55
|
private readonly inFlight;
|
|
56
|
+
private readonly endpointMetrics;
|
|
57
|
+
private readonly requestQueue;
|
|
58
|
+
private readonly endpointRequestQueues;
|
|
59
|
+
private readonly endpointActiveRequests;
|
|
60
|
+
private readonly upstreamRateLimits;
|
|
61
|
+
private readonly circuitBreakers;
|
|
62
|
+
private readonly upstreamRequestTimestampsMs;
|
|
63
|
+
private activeRequests;
|
|
64
|
+
private globalBlockedUntilMs;
|
|
65
|
+
private capabilityNegotiation;
|
|
66
|
+
private capabilityState;
|
|
42
67
|
constructor(config: ServerConfig);
|
|
43
68
|
diagnostics(): {
|
|
44
|
-
response_cache:
|
|
69
|
+
response_cache: {
|
|
70
|
+
entries: number;
|
|
71
|
+
max_entries: number;
|
|
72
|
+
ttl_ms: number;
|
|
73
|
+
hits: number;
|
|
74
|
+
misses: number;
|
|
75
|
+
sets: number;
|
|
76
|
+
evictions: number;
|
|
77
|
+
expirations: number;
|
|
78
|
+
stale_hits: number;
|
|
79
|
+
enabled: boolean;
|
|
80
|
+
};
|
|
81
|
+
negative_response_cache: {
|
|
82
|
+
entries: number;
|
|
83
|
+
max_entries: number;
|
|
84
|
+
ttl_ms: number;
|
|
85
|
+
hits: number;
|
|
86
|
+
misses: number;
|
|
87
|
+
sets: number;
|
|
88
|
+
evictions: number;
|
|
89
|
+
expirations: number;
|
|
90
|
+
stale_hits: number;
|
|
45
91
|
enabled: boolean;
|
|
46
92
|
};
|
|
93
|
+
capability_negotiation: CapabilityNegotiationState;
|
|
94
|
+
circuit_breakers: {
|
|
95
|
+
failure_threshold: number;
|
|
96
|
+
cooldown_ms: number;
|
|
97
|
+
by_endpoint: {
|
|
98
|
+
[k: string]: {
|
|
99
|
+
state: string;
|
|
100
|
+
consecutive_failures: number;
|
|
101
|
+
open_until: string | undefined;
|
|
102
|
+
last_failure_at: string | undefined;
|
|
103
|
+
last_success_at: string | undefined;
|
|
104
|
+
last_status: number | undefined;
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
};
|
|
47
108
|
in_flight_requests: number;
|
|
109
|
+
request_concurrency: {
|
|
110
|
+
limit: number;
|
|
111
|
+
active: number;
|
|
112
|
+
queued: number;
|
|
113
|
+
endpoint_limits: {
|
|
114
|
+
change_feeds: {
|
|
115
|
+
limit: number;
|
|
116
|
+
active: number;
|
|
117
|
+
queued: number;
|
|
118
|
+
};
|
|
119
|
+
target_export: {
|
|
120
|
+
limit: number;
|
|
121
|
+
active: number;
|
|
122
|
+
queued: number;
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
upstream_request_window: {
|
|
127
|
+
enforced: boolean;
|
|
128
|
+
limit: number;
|
|
129
|
+
window_ms: number;
|
|
130
|
+
used: number;
|
|
131
|
+
remaining: number | undefined;
|
|
132
|
+
resets_at: string | undefined;
|
|
133
|
+
};
|
|
134
|
+
upstream_rate_limit: {
|
|
135
|
+
observed: boolean;
|
|
136
|
+
bucket: string | undefined;
|
|
137
|
+
limit: number | undefined;
|
|
138
|
+
remaining: number | undefined;
|
|
139
|
+
reset_at: string | undefined;
|
|
140
|
+
blocked: boolean;
|
|
141
|
+
globally_blocked: boolean;
|
|
142
|
+
blocked_until: string | undefined;
|
|
143
|
+
observed_at: string | undefined;
|
|
144
|
+
last_status: number | undefined;
|
|
145
|
+
buckets: {
|
|
146
|
+
[k: string]: {
|
|
147
|
+
limit: number | undefined;
|
|
148
|
+
remaining: number | undefined;
|
|
149
|
+
reset_at: string | undefined;
|
|
150
|
+
blocked: boolean;
|
|
151
|
+
blocked_until: string | undefined;
|
|
152
|
+
observed_at: string | undefined;
|
|
153
|
+
last_status: number | undefined;
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
upstream_requests: {
|
|
158
|
+
actual_requests: number;
|
|
159
|
+
errors: number;
|
|
160
|
+
avg_ms: number;
|
|
161
|
+
max_ms: number;
|
|
162
|
+
by_endpoint: {
|
|
163
|
+
[k: string]: {
|
|
164
|
+
requests: number;
|
|
165
|
+
errors: number;
|
|
166
|
+
avg_ms: number;
|
|
167
|
+
max_ms: number;
|
|
168
|
+
last_ms: number;
|
|
169
|
+
statuses: {
|
|
170
|
+
[k: string]: number;
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
};
|
|
48
175
|
};
|
|
49
176
|
validateKey(): Promise<void>;
|
|
177
|
+
negotiateCapabilities(): Promise<void>;
|
|
178
|
+
capabilitySnapshot(): CapabilityNegotiationState;
|
|
179
|
+
supportsEndpoint(endpoint: string): boolean;
|
|
180
|
+
endpointCapability(endpoint: string): Record<string, unknown> | undefined;
|
|
181
|
+
endpointNumber(endpoint: string, field: string, fallback: number): number;
|
|
182
|
+
endpointStringArray(endpoint: string, field: string, fallback: readonly string[]): string[];
|
|
183
|
+
private fetchCapabilityContract;
|
|
50
184
|
listPrograms(query: Record<string, QueryValue>): Promise<ApiResult<unknown>>;
|
|
185
|
+
getCapabilities(options?: Pick<ApiRequestOptions, "cache" | "coalesce">): Promise<ApiResult<unknown>>;
|
|
51
186
|
getProgram(programId: string): Promise<ApiResult<unknown>>;
|
|
52
|
-
getProgramTargets(programId: string): Promise<ApiResult<unknown>>;
|
|
187
|
+
getProgramTargets(programId: string, query?: Record<string, QueryValue>): Promise<ApiResult<unknown>>;
|
|
53
188
|
getRecentChanges(query: Record<string, QueryValue>): Promise<ApiResult<unknown>>;
|
|
189
|
+
getRepositoryChanges(query: Record<string, QueryValue>): Promise<ApiResult<unknown>>;
|
|
54
190
|
searchTargets(query: Record<string, QueryValue>): Promise<ApiResult<unknown>>;
|
|
55
191
|
getOpportunities(level: string, query: Record<string, QueryValue>): Promise<ApiResult<unknown>>;
|
|
56
192
|
exportTargets(body: Record<string, unknown>): Promise<ApiResult<unknown>>;
|
|
57
193
|
request<T = unknown>(options: ApiRequestOptions): Promise<ApiResult<T>>;
|
|
58
194
|
private fetchFromApi;
|
|
195
|
+
private refreshInBackground;
|
|
196
|
+
private withRequestSlot;
|
|
197
|
+
private withGlobalRequestSlot;
|
|
198
|
+
private withEndpointRequestSlot;
|
|
199
|
+
private recordEndpointMetric;
|
|
200
|
+
private reserveUpstreamRequest;
|
|
201
|
+
private observeUpstreamRateLimit;
|
|
202
|
+
private observeUpstreamErrorBody;
|
|
203
|
+
private rateLimitState;
|
|
204
|
+
private refreshUpstreamRequestWindow;
|
|
205
|
+
private upstreamRequestWindowDiagnostics;
|
|
206
|
+
private shouldEnforceSharedRequestWindow;
|
|
207
|
+
private refreshUpstreamRateLimitWindow;
|
|
208
|
+
private upstreamRateLimitDiagnostics;
|
|
209
|
+
private assertCircuitClosed;
|
|
210
|
+
private observeCircuitResponse;
|
|
211
|
+
private observeCircuitFailure;
|
|
212
|
+
private circuitBreakerDiagnostics;
|
|
59
213
|
private buildUrl;
|
|
60
214
|
private headers;
|
|
61
215
|
}
|
|
216
|
+
export {};
|