@bbradar/mcp 0.3.0 → 0.3.2
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/CHANGELOG.md +19 -0
- package/README.md +89 -9
- package/benchmark/live-api-check.mjs +126 -0
- package/benchmark/live-hunt.mjs +105 -0
- package/benchmark/live-stdio.mjs +80 -0
- package/dist/activityContract.d.ts +2 -0
- package/dist/activityContract.js +32 -0
- package/dist/activityContract.js.map +1 -1
- package/dist/bbradarClient.d.ts +5 -1
- package/dist/bbradarClient.js +24 -3
- package/dist/bbradarClient.js.map +1 -1
- package/dist/sanitize.js +37 -0
- package/dist/sanitize.js.map +1 -1
- package/dist/server.js +271 -122
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Release notes
|
|
2
2
|
|
|
3
|
+
## 0.3.2 — new Activity data, contest awards and dependency security (2026-11-07)
|
|
4
|
+
|
|
5
|
+
- `find_programs` and `find_hunt_targets` now report the sort direction the API actually applied in `ranking_scope.sort_direction` and `scan.sort_direction`. With no `sort_direction`, the API ranks `first_response` and `time_to_bounty` fastest first (ascending), and these fields previously said `desc`. When the API does not echo a direction, the same rule applies.
|
|
6
|
+
- Fixed Activity, payout, History and analytics results failing in MCP clients that validate structured output against the discovered schema. Those schemas rejected unknown top-level fields, so Status rankings fail validation against an API with `facets` (added 2026-10-13), as do Value pages with `labels` and program payouts with `value_period`. The 21 tools that return one Pro API payload as it is now list these fields and also accept fields the API adds later.
|
|
7
|
+
- `get_activity_rankings` and `get_activity_value` accept `period` (`7d`, `30d` or `90d`) for Value, as the API now does; each period is used only when capabilities advertise it. `24h`, `all_time` and launch cohorts remain local errors. Descriptions cover per-period Value measures and labels, the default landing shortlist named by the page's `labels`, and the 90-day-only signals.
|
|
8
|
+
- `get_program` and `get_program_brief` keep `contest_awards`, the final award allocations of a contest or Huntr challenge, with decimal totals in the contest's own token and a null currency where the platform names none. Pending results stay `null`; other program kinds omit the field. The guide routes finished-contest award questions to `get_program`; a finished contest is readable only there, by exact `platform:handle`, since target reads exclude it. This needs the bbradar-web change that serves awards and finished contests on the API key detail route (`/v1/pro/programs/{program_id}`); before it, the field is absent and finished contests are not found.
|
|
9
|
+
- Updated `@modelcontextprotocol/sdk` to 1.32.2 and `zod` to 4.6.7, fixing the high-severity advisories that failed `npm audit` and the package check (GHSA-22jm-h49p-29qw, GHSA-46jr-4wx8-v453, GHSA-rvq5-wwqv-78pq and GHSA-6prh-2h8m-c8cw in the SDK; GHSA-mrhx-c7xc-m3w5 in zod, which has no 3.x fix), and refreshed patched transitive dependencies (body-parser, fast-uri, hono, `@hono/node-server`, ip-address, qs). Advertised tool fields, defaults and required inputs are unchanged. Zod 4 adds `propertyNames` to record schemas, safe-integer bounds to integers and an RFC 3339 pattern to date-times, and `find_programs.sort_by` stays one flat enum. `updated_since` now rejects date-times without seconds or with an offset lacking its colon, as its `date-time` format always stated.
|
|
10
|
+
- Tool descriptions and the README cover the API's new data: Status `facets`, payout `average_paid` and its sort, Rewards USD display bounds, per-provider Response definitions and windows, program payout facts (`paid_average`, `paid_maximum`, `last_paid_on`, per-severity figures, triage and resolution times, response rate, researcher counts) and `value_period`.
|
|
11
|
+
|
|
12
|
+
## 0.3.1 — hunt deadlines and target accuracy (2026-10-13)
|
|
13
|
+
|
|
14
|
+
- Reduced unnecessary upstream target rows for small direct reads while preserving offset/limit semantics and full-page scans for local filters. Added a live STDIO check with fresh prefix comparison and cache diagnostics. Catalog concurrency remains four: smaller-pool trials did not establish a reliable improvement under changing API latency.
|
|
15
|
+
- Fixed target inventory pagination in latest-addition and named-program workflows, added language-filter continuations, and exposed remaining offsets. Named target actions now honor target-type and language filters.
|
|
16
|
+
- Marked bounded scope counts and capped legacy exports as incomplete instead of full inventory totals. Export fallbacks reject upstream filters they cannot preserve. Malformed collection pages now fail explicitly rather than becoming empty successes.
|
|
17
|
+
- Rejected unknown arguments across all tools, respected negotiated aggregate page limits, and corrected SDK version diagnostics with a packaged-server regression check. Added live workflow smoke checks that report aggregate measurements and stop on failure without retries.
|
|
18
|
+
- Reserved catalog, recent-change and target-sampling time within the hunt's existing tool deadline. Slow optional reads no longer consume the entire call; completed catalog pages survive later-page timeouts with explicit incomplete coverage, errors and phase timings.
|
|
19
|
+
- Applied existing hunt VDP exclusion upstream when negotiated, preserving unknown classifications and the local filters. Added a read-only live hunt benchmark with aggregate timings and quota reporting, plus deadline, cancellation, coverage and legacy-API regression fixtures. Retained the four-request catalog limit after live concurrency comparisons showed no reliable gain from six.
|
|
20
|
+
- Rejected Activity version IDs on catalog/feed snapshot inputs and validated required signal periods and payout metric/period combinations before sending requests. Tool descriptions and guides distinguish the two snapshot contracts and preserve user filters after empty or partial results.
|
|
21
|
+
|
|
3
22
|
## 0.3.0 — Activity and payout views (2026-10-12)
|
|
4
23
|
|
|
5
24
|
- Added five Activity tools to the default 30-tool `core` profile: `get_activity_capabilities`, `get_activity_snapshot`, `get_activity_rankings`, `get_program_payouts`, and `get_activity_change_detail`. Rankings cover payouts, Value, scope, rewards, updates, reports, response, status and all changes. The `full` profile also exposes nine per-view aliases.
|
package/README.md
CHANGED
|
@@ -116,6 +116,7 @@ Self-Hosted programs use IDs such as `Self-Hosted:program-slug`. They are availa
|
|
|
116
116
|
|
|
117
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
118
|
Use `program_kind: "bounty"`, `"contest"`, or `"challenge"` with `search_programs` or `find_programs` for source-classified program filtering. `find_web3_contests` uses this classification instead of inferring contests from a name or platform.
|
|
119
|
+
`get_program` and `get_program_brief` keep `contest_awards` for a contest or Huntr challenge: the final award allocations its platform publishes (`contest_id`, `currency`, `awarded_total`, `pool_amount`, `awarded_count`, `participants`, `source_url`, `checked_at`, `first_final_at`), or `null` before a final result. Totals are decimal strings in the contest's own token, never USD or a payment record; `currency` is `null` when the platform names none (Huntr). A finished contest with a final result stays readable through `get_program` by exact `platform:handle`, without an opportunity tag; discovery, name resolution and target reads (so also `get_program_brief`) exclude ended contests. The field needs an API whose key-authenticated detail route (`/v1/pro/programs/{program_id}`) serves it; an older API omits it and answers 404 for finished contests.
|
|
119
120
|
The program catalog now includes confirmed paused platform programs. Raw search preserves `program_status` and `is_active`; ranked discovery excludes paused programs so retained scope and historical rewards cannot be presented as huntable. Exact paused-program reads remain available when explicitly requested.
|
|
120
121
|
|
|
121
122
|
The full tool profile also exposes `search_targets`, a one-request low-level wrapper for reverse search and paginated current target inventory. It supports optional `q`, all API match modes, platform/tag filters, eligibility controls, pages up to the negotiated 500-row limit, and compact or full target output.
|
|
@@ -135,6 +136,8 @@ Repository results preserve the API's comparison completeness/method/ancestry ev
|
|
|
135
136
|
|
|
136
137
|
`search_programs`, `search_targets`, `get_recent_changes`, and `get_repository_changes` accept the API's optional stable `snapshot` token. Send `snapshot: "start"` on page 1 and reuse the returned `meta.snapshot` with identical filters until `meta.snapshot_expires_at`. Snapshot responses bypass the local TTL cache, and an expired snapshot 410 is not mistaken for an expired change-feed cursor. Snapshot creation receives a 60-second minimum request timeout because it performs consistency work and can be materially slower; the fast `find_hunt_targets` workflow intentionally keeps live indexed paging and the ordinary configured timeout.
|
|
137
138
|
|
|
139
|
+
Catalog and feed snapshot tokens have at most 32 characters. `find_programs` uses that same contract. The 64-character version ID from `get_activity_snapshot` binds only Activity capabilities, rankings, event details and program payouts; do not pass it to catalog discovery.
|
|
140
|
+
|
|
138
141
|
### Activity and payouts
|
|
139
142
|
|
|
140
143
|
Use `get_activity_capabilities` before choosing a ranking. Its versioned views give
|
|
@@ -153,7 +156,8 @@ silently replace its period or basis.
|
|
|
153
156
|
| `get_activity_change_detail` | One bounded page of an exact Activity event's before/after items |
|
|
154
157
|
|
|
155
158
|
`get_activity_rankings` takes `tab`: `payouts` (reported or tracked, the two bases
|
|
156
|
-
kept separate), `value` (frozen Value map and label-specific shortlists
|
|
159
|
+
kept separate), `value` (frozen Value map and label-specific shortlists for a 7, 30
|
|
160
|
+
or 90-day period),
|
|
157
161
|
`scope`, `rewards`, `updates`, `reports`, `response`, `status` (lifecycle events;
|
|
158
162
|
first seen is not a launch date) or `all_changes` (catalog events, including
|
|
159
163
|
eligible events for departed programs). Each tab accepts only its own inputs:
|
|
@@ -174,9 +178,30 @@ An API that does not advertise the filter is told so before any request.
|
|
|
174
178
|
|
|
175
179
|
Ranking pages omit the capabilities document the API repeats on every page; read it
|
|
176
180
|
once with `get_activity_capabilities`. When a tab has no enabled default view the
|
|
177
|
-
empty page carries that tab's `available_views` instead.
|
|
178
|
-
|
|
179
|
-
|
|
181
|
+
empty page carries that tab's `available_views` instead. Status pages add `facets`:
|
|
182
|
+
event counts per `change_type` and `current_status` for the period and filters, each
|
|
183
|
+
counted under every other filter but not its own selection, with zero counts omitted.
|
|
184
|
+
|
|
185
|
+
Payout rows also carry `average_paid`: the platform's own current average payment
|
|
186
|
+
(an exact mean, or HackerOne's 45th to 55th percentile range) with its
|
|
187
|
+
`source_label`, `period`, native currency and USD conversion. It is the same in every
|
|
188
|
+
period, and `sort: "average_paid"` ranks it where capabilities advertise it. Rewards
|
|
189
|
+
rows add `minimum_usd`/`maximum_usd` to `before`/`after` at the version's current
|
|
190
|
+
quotes for display; the change percentage still compares native maxima. Response
|
|
191
|
+
cells keep each provider's definition, label and window (`first_response_label`,
|
|
192
|
+
`first_response_window`, `first_response_precision`, `time_to_bounty_window`), and
|
|
193
|
+
duration sorts (`first_response`, `time_to_bounty`) rank fastest first when
|
|
194
|
+
`direction` is omitted.
|
|
195
|
+
|
|
196
|
+
`tab: "value"` takes `period` `7d`, `30d` or `90d`, each only once
|
|
197
|
+
`get_activity_capabilities` advertises it: report volume covers that period, while
|
|
198
|
+
payout per processed report is lifetime. Rows and points carry the period's count and
|
|
199
|
+
percentile in `value`; `signals` keep the lifetime and 90-day figures. Without a
|
|
200
|
+
period, a request naming `labels` or `sort` means 90 days, and a bare request opens
|
|
201
|
+
the advertised default view, which can be a label shortlist: the page's `labels`
|
|
202
|
+
names the list applied (empty for the map's programs). High value, Popular and Low
|
|
203
|
+
per report exist in each period; Pays near top, No payouts seen and processed versus
|
|
204
|
+
received exist only at 90 days. With `include_map: true` its `points` cover the
|
|
180
205
|
full filtered map population from the same snapshot (up to 5,000); by default the
|
|
181
206
|
map is replaced by `map_points_omitted` with its count, because thousands of points
|
|
182
207
|
rarely help a text answer. `rows` are a separately paginated shortlist. Percentiles stay frozen within their original platform cohorts after
|
|
@@ -187,8 +212,14 @@ rejected rather than replaced.
|
|
|
187
212
|
|
|
188
213
|
Rankings and `get_program_payouts` return server-owned `signals`, `labels` and
|
|
189
214
|
`label_details`, including explanations and destinations. Program detail also
|
|
190
|
-
returns `value_points` for same-platform peers
|
|
191
|
-
|
|
215
|
+
returns `value_points` for same-platform peers in the Value period named by
|
|
216
|
+
`value_period` when called with `include_map: true` (otherwise `map_points_omitted`
|
|
217
|
+
gives their count). Its `facts` also carry each platform's own published figures,
|
|
218
|
+
each with its `measure`, `unit`, `period` and `source_label`: `paid_average`,
|
|
219
|
+
`paid_maximum` and `last_paid_on`, HackerOne's 90-day `paid_average_<severity>` and
|
|
220
|
+
lifetime `severity_reports_<severity>`, triage and resolution times, PatchDay's
|
|
221
|
+
response rate and researcher counts. They are program-detail facts with no Activity
|
|
222
|
+
column or sort; keep each definition rather than comparing platforms as one measure. “Last payout seen” describes an
|
|
192
223
|
observed counter increase, never a payment date. “Pays near top” is the platform's
|
|
193
224
|
top-quarter rule: retain the actual amount/percentage in the explanation. Processed
|
|
194
225
|
vs received can exceed 100% and does not measure response speed. Never recalculate
|
|
@@ -236,8 +267,10 @@ Reports and resolved counters support `7d`, `30d`, `90d`, `all_time`, and API-on
|
|
|
236
267
|
`24h` where enabled. All-time uses the fresh lifetime counter; it has no Previous
|
|
237
268
|
or Change. Finite report comparisons retain the same basis within each program;
|
|
238
269
|
report volume does not establish researcher competition. Scope, Rewards, Updates,
|
|
239
|
-
Status and All changes support only `7d`, `30d` and `90d`. Response
|
|
240
|
-
|
|
270
|
+
Status and All changes support only `7d`, `30d` and `90d`. Response publishes only
|
|
271
|
+
`7d` and `30d` until resolved counters hold 90 days of history; use the periods
|
|
272
|
+
capabilities advertise. Response counts retain the provider's term (resolved,
|
|
273
|
+
accepted, paid or rewarded); time to bounty measures award timing, not settlement. Label
|
|
241
274
|
filters must come from current capabilities, and unsupported labels remain errors.
|
|
242
275
|
|
|
243
276
|
Use exact `platform:handle` identities for `get_program_payouts`; slash-containing
|
|
@@ -246,6 +279,11 @@ definitions and source timestamps remain intact in structured output. Human
|
|
|
246
279
|
summaries should show the available numbers and units, keeping internal collection
|
|
247
280
|
and completeness metadata in the structured result.
|
|
248
281
|
|
|
282
|
+
Tools that return one Pro API payload as it is (the Activity, payout, History and
|
|
283
|
+
analytics tools) list its known fields in their output schemas and also accept new
|
|
284
|
+
top-level fields. Clients validate structured results against the schema they
|
|
285
|
+
discovered, so a field the API adds later reaches them instead of failing the call.
|
|
286
|
+
|
|
249
287
|
`find_programs` and `find_hunt_targets` accept combined Activity predicates
|
|
250
288
|
(`search_programs` and `get_opportunities` reject them; use `find_programs`, with one
|
|
251
289
|
`opportunity_levels` entry for a tier). The API applies them together before
|
|
@@ -253,7 +291,9 @@ pagination, preserving one global ordering and one stable snapshot. For example,
|
|
|
253
291
|
`{"payout_period":"30d","payout_metric":"payout_increase","payout_min":1000,"reports_period":"7d","reports_max":10,"sort_by":"payout_amount","sort_direction":"desc"}`
|
|
254
292
|
to combine measured payouts in the last 30 days with report activity in the last
|
|
255
293
|
7 days. Add `snapshot: "start"` on the first page and reuse `meta.snapshot` with
|
|
256
|
-
identical filters. The default order remains `updated` descending
|
|
294
|
+
identical filters. The default order remains `updated` descending; an omitted
|
|
295
|
+
`sort_direction` for `first_response` or `time_to_bounty` means ascending (fastest
|
|
296
|
+
first), and `ranking_scope.sort_direction` reports the order applied. Bounds sorts
|
|
257
297
|
require `proven_bounds`: `payout_lower_bound` descends and `payout_upper_bound`
|
|
258
298
|
ascends. `payout_positive: false` requires an exact observed zero; omit it when
|
|
259
299
|
both zero and positive payers should qualify.
|
|
@@ -344,6 +384,22 @@ Already-retired historical events cannot be reconstructed. `get_program_targets`
|
|
|
344
384
|
follows upstream API pages until it can satisfy the requested offset/limit or the
|
|
345
385
|
inventory ends. Missing public scope is treated as out of scope.
|
|
346
386
|
|
|
387
|
+
Target lists expose `meta.has_more` and `meta.next_offset`; repeat the same filters
|
|
388
|
+
with that offset to continue. `language_tags` applies while scanning pages, so
|
|
389
|
+
sparse matches do not skip eligible targets. Named target actions honor their
|
|
390
|
+
target-type and language filters. `get_latest_added_targets` also pages its optional
|
|
391
|
+
inventory and returns `full_targets_next_offset` for `get_program_targets`.
|
|
392
|
+
Scope summaries and breakdowns label sampled counts with `count_scope` and
|
|
393
|
+
`possibly_incomplete`; unknown full filtered totals are omitted. Legacy export
|
|
394
|
+
fallbacks disclose their cap and reject filters whose upstream semantics they
|
|
395
|
+
cannot preserve. All tools reject unknown arguments, and malformed collection
|
|
396
|
+
responses return errors instead of claiming the inventory is empty.
|
|
397
|
+
|
|
398
|
+
Small direct target reads request only the prefix needed for their offset and limit,
|
|
399
|
+
up to the negotiated page size, instead of always loading a 500-row page. Local
|
|
400
|
+
scope and language post-filters retain full pages to find sparse matches. This
|
|
401
|
+
reduces excess transfer and processing while preserving fresh reads and row offsets.
|
|
402
|
+
|
|
347
403
|
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.
|
|
348
404
|
|
|
349
405
|
For cacheable responses, large target and change collections return at most 50 inline records by default; the remainder is held in a bounded, ten-minute continuation resource. Private or `no-store` pages stay in the current structured result and are never put in a reusable continuation. Compact tool text keeps model context small.
|
|
@@ -379,6 +435,24 @@ Call `get_mcp_status` to inspect cache and stale-hit rates, circuit states, curs
|
|
|
379
435
|
|
|
380
436
|
Run `npm run benchmark` for deterministic local benchmarks. They report request and byte counts, model-context bytes, latency percentiles, and memory under representative History and analytics concurrency. Fixture timings are separate from live API latency.
|
|
381
437
|
|
|
438
|
+
With `BBRADAR_API_KEY` already configured, run `npm run build` then `node benchmark/live-hunt.mjs` for one authenticated all-platform Web2 hunt (domain/API, $5,000 maximum bounty, at least three eligible and five total targets, 15 results). It reports aggregate phase timings, request counts, quota and sampled coverage without printing the key or program/target records. This consumes live API quota and makes no automatic retries. Live catalog updates can change rows between pages; signatures help distinguish result changes from timing changes.
|
|
439
|
+
|
|
440
|
+
`node benchmark/live-api-check.mjs` checks negotiated capabilities, discovery,
|
|
441
|
+
all nine Activity tabs, program and target reads, payouts, History, daily analytics,
|
|
442
|
+
feeds, combined filters and the same hunt. It emits aggregate measurements and
|
|
443
|
+
stops at the first failure. An optional section (`activity`, `programs`, `analytics`,
|
|
444
|
+
`feeds`, or `hunt`) runs only that workflow and its prerequisites, so independent
|
|
445
|
+
checks can continue without repeating successful sections. These are live reads
|
|
446
|
+
and consume quota; a smoke pass is not an uptime or complete-catalog guarantee.
|
|
447
|
+
|
|
448
|
+
`node benchmark/live-stdio.mjs` performs the original hunt through the actual
|
|
449
|
+
STDIO server entry point using the configured key, then compares a fresh small
|
|
450
|
+
target page with the prefix of a fresh wider page. Use `targets` to run the target
|
|
451
|
+
check independently. It reports cache activity, timings and counts, never target
|
|
452
|
+
identifiers. Partial hunts fail the benchmark instead of counting as speed gains.
|
|
453
|
+
`live-hunt.mjs --catalog-concurrency=2` can test a lower catalog request cap;
|
|
454
|
+
inspect both timings and coverage before adopting a concurrency change.
|
|
455
|
+
|
|
382
456
|
Maintainers can compare the working tree against a committed revision with `npm run build` followed by `node --expose-gc benchmark/compare-revisions.mjs HEAD`. This uses the same fixtures and alternating samples for both revisions, reports discovery and result sizes, and measures repeated private reads separately from cacheable reads. It requires the repository and development dependencies.
|
|
383
457
|
|
|
384
458
|
Run `npm run test:package` to pack the release, install its production dependencies in a temporary directory, audit them, and exercise the packaged STDIO server. CI runs the checks on Node 20, 22, and 24.
|
|
@@ -393,6 +467,12 @@ Use `find_hunt_targets` as the default one-call path when a request combines goo
|
|
|
393
467
|
|
|
394
468
|
`find_hunt_targets` supports arbitrary platform inclusion/exclusion, scope/target/language filters, reward and breadth thresholds, Web2/Web3 modes, target-intelligence filters, freshness windows, `free_submission_only`, and balanced/reward/scope/freshness/competition ranking. Activity inputs such as `payout_period: "30d"` with `payout_positive: true`, or `scope_period: "30d"` with `scope_added_min: 1`, first narrow its candidates on the server in global order; the remaining filters and `ranking_mode` then apply locally, and `scan` reports how much of the server result was examined. Unknown inputs are rejected rather than ignored. It suppresses bulk `bbradar://` resource links while preserving each program's public BBRadar and platform URLs, preventing large shortlists from flooding model context.
|
|
395
469
|
|
|
470
|
+
The hunt reserves time within the configured tool deadline: catalog discovery uses at most 60%, optional recent-change queries 30% in parallel, and target sampling finishes by 90%, leaving time to return the result. With the default 30 seconds, those bounds are 18, 9 and 27 seconds. A stalled later catalog page preserves completed pages; Activity scans retain only their completed ordered prefix. A failed first catalog page remains an error. Partial reads include `errors`, warnings, `meta.partial_success`, `result_guidance.coverage_incomplete` and `meta.phase_timing`. Missing enrichment is unavailable evidence. Keep the user's platform scope and thresholds; a sampled change feed cannot identify the complete supported platform set.
|
|
471
|
+
|
|
472
|
+
Legacy catalog hunts apply their existing VDP exclusion upstream when the API advertises that mode, then retain the local filters. Programs with unknown classifications and positive rewards remain eligible. Older APIs keep local exclusion. Catalog paging retains its four-request limit within the configured global pool; Activity scans keep their separate two-request limit.
|
|
473
|
+
|
|
474
|
+
Combined Activity filters need independent periods. `change_category` requires `updates_period`; filtering additions with `scope_period` and `scope_added_min` alone needs no category. `payout_total` requires `payout_period: "all_time"`; use `payout_increase` for tracked 24h/7d/30d/90d changes, or an enabled `payout_native_90d` comparison with 90d for provider-reported values. Invalid combinations fail locally with guidance. An empty valid result does not authorize changing requested thresholds.
|
|
475
|
+
|
|
396
476
|
```json
|
|
397
477
|
{
|
|
398
478
|
"platforms": ["Immunefi", "HackenProof", "BugCrowd", "YesWeHack"],
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Read-only production smoke check. Requires BBRADAR_API_KEY; emits aggregate
|
|
2
|
+
// measurements only and stops at the first failed call without retrying.
|
|
3
|
+
// Usage: node benchmark/live-api-check.mjs [all|activity|programs|analytics|feeds|hunt]
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
7
|
+
import { BBRadarClient } from "../dist/bbradarClient.js";
|
|
8
|
+
import { loadConfig } from "../dist/config.js";
|
|
9
|
+
import { createBbradarServer } from "../dist/server.js";
|
|
10
|
+
|
|
11
|
+
const config = loadConfig();
|
|
12
|
+
config.toolProfile = "core";
|
|
13
|
+
const section = process.argv[2] ?? "all";
|
|
14
|
+
assert.ok(["all", "activity", "programs", "analytics", "feeds", "hunt"].includes(section), "Use all, activity, programs, analytics, feeds, or hunt.");
|
|
15
|
+
const enabled = name => section === "all" || section === name;
|
|
16
|
+
const upstream = new BBRadarClient(config);
|
|
17
|
+
const observations = [];
|
|
18
|
+
const report = { measured_at: new Date().toISOString(), fixture: false, section, observations };
|
|
19
|
+
let server, client;
|
|
20
|
+
async function call(name, args = {}) {
|
|
21
|
+
const before = upstream.diagnostics().upstream_requests.actual_requests;
|
|
22
|
+
const result = await client.callTool({ name, arguments: args });
|
|
23
|
+
const data = result.structuredContent;
|
|
24
|
+
observations.push({ tool: name, tab: args.tab, success: !result.isError,
|
|
25
|
+
status: data?.error?.status,
|
|
26
|
+
error: data?.error?.message?.replaceAll(config.apiKey, "[redacted]"),
|
|
27
|
+
duration_ms: data?.mcp_timing?.duration_ms,
|
|
28
|
+
requests: upstream.diagnostics().upstream_requests.actual_requests - before,
|
|
29
|
+
rows: (data?.rows ?? data?.programs ?? data?.targets ?? data?.changes ?? data?.series ?? data?.matches)?.length,
|
|
30
|
+
no_store: data?.cache?.no_store, partial_success: data?.meta?.partial_success,
|
|
31
|
+
coverage_incomplete: data?.result_guidance?.coverage_incomplete,
|
|
32
|
+
phase_timing: data?.meta?.phase_timing });
|
|
33
|
+
if (result.isError) throw new Error(`${name} failed with status ${data?.error?.status ?? "unknown"}`);
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
await upstream.negotiateCapabilities();
|
|
39
|
+
const contract = upstream.capabilitySnapshot();
|
|
40
|
+
assert.equal(contract.status, "negotiated");
|
|
41
|
+
report.contract_version = contract.contract.contract_version;
|
|
42
|
+
report.api_endpoints = Object.keys(contract.contract.endpoints).sort();
|
|
43
|
+
server = createBbradarServer(upstream, config);
|
|
44
|
+
client = new Client({ name: "bbradar-live-check", version: "1" });
|
|
45
|
+
const [a, b] = InMemoryTransport.createLinkedPair();
|
|
46
|
+
await Promise.all([client.connect(a), server.connect(b)]);
|
|
47
|
+
const discovery = await client.listTools();
|
|
48
|
+
report.core_tool_count = discovery.tools.length;
|
|
49
|
+
report.discovery_bytes = Buffer.byteLength(JSON.stringify(discovery));
|
|
50
|
+
const status = await call("get_mcp_status");
|
|
51
|
+
report.server_version = status.status.server_version;
|
|
52
|
+
report.sdk_version = status.status.sdk_version;
|
|
53
|
+
await call("get_bbradar_guide");
|
|
54
|
+
let snapshot;
|
|
55
|
+
if (enabled("activity") || enabled("programs")) {
|
|
56
|
+
const version = await call("get_activity_snapshot");
|
|
57
|
+
snapshot = version.snapshot_id;
|
|
58
|
+
}
|
|
59
|
+
if (enabled("activity")) {
|
|
60
|
+
const capabilities = await call("get_activity_capabilities", { snapshot });
|
|
61
|
+
for (const tab of capabilities.tabs) {
|
|
62
|
+
// Let the server select its advertised default; an unqualified default
|
|
63
|
+
// legitimately returns no rows plus available_views, never an invented view.
|
|
64
|
+
const page = await call("get_activity_rankings", { tab: tab.id, snapshot, limit: 1 });
|
|
65
|
+
assert.equal(page.tab, tab.id);
|
|
66
|
+
assert.equal(page.meta.snapshot_id, snapshot);
|
|
67
|
+
assert.equal(page.cache.no_store, true);
|
|
68
|
+
assert.ok(Array.isArray(page.rows));
|
|
69
|
+
if (page.sort === null) assert.ok(Array.isArray(page.available_views));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
let program;
|
|
73
|
+
if (enabled("programs") || enabled("analytics")) {
|
|
74
|
+
const catalog = await call("search_programs", { page_size: 5 });
|
|
75
|
+
program = catalog.programs.find(row => row.platform !== "Self-Hosted");
|
|
76
|
+
assert.ok(program?.id, "The bounded live catalog had no program for the detail checks.");
|
|
77
|
+
}
|
|
78
|
+
if (enabled("programs")) {
|
|
79
|
+
await call("get_program", { program_id: program.id });
|
|
80
|
+
await call("get_program_targets", { program_id: program.id, limit: 2 });
|
|
81
|
+
await call("get_program_payouts", { program_id: program.id, snapshot });
|
|
82
|
+
await call("get_program_activity", { program_id: program.id });
|
|
83
|
+
await call("get_program_history", { program_id: program.id, limit: 1 });
|
|
84
|
+
await call("get_history_changes", { limit: 1 });
|
|
85
|
+
}
|
|
86
|
+
if (enabled("analytics")) {
|
|
87
|
+
await call("get_analytics_platform_series", { platform: program.platform, limit: 1 });
|
|
88
|
+
await call("get_analytics_program_detail", { program_id: program.id, limit: 1 });
|
|
89
|
+
await call("get_analytics_program_activity", { limit: 1 });
|
|
90
|
+
}
|
|
91
|
+
if (enabled("feeds")) {
|
|
92
|
+
await call("get_recent_changes", { page_size: 1 });
|
|
93
|
+
await call("get_repository_changes", { page_size: 1, include_targets: false });
|
|
94
|
+
await call("get_opportunities", { level: "elite", page_size: 1 });
|
|
95
|
+
const combined = await call("find_programs", {
|
|
96
|
+
payout_period: "30d", payout_positive: true, require_bounty: true, max_pages: 1, max_results: 1
|
|
97
|
+
});
|
|
98
|
+
assert.ok(combined.scan);
|
|
99
|
+
}
|
|
100
|
+
if (enabled("hunt")) {
|
|
101
|
+
const hunt = await call("find_hunt_targets", {
|
|
102
|
+
surface_mode: "web2", target_types: ["domain", "api"], require_bounty: true,
|
|
103
|
+
min_bounty_max: 5000, min_eligible_targets: 3, min_total_targets: 5,
|
|
104
|
+
include_recent_changes: true, include_unknown_report_count: true,
|
|
105
|
+
recent_target_days: 90, max_results: 15, targets_per_result: 3,
|
|
106
|
+
ranking_mode: "balanced", upstream_request_budget: 50
|
|
107
|
+
});
|
|
108
|
+
assert.ok(hunt.matches.length > 0);
|
|
109
|
+
assert.equal(hunt.filters.min_bounty_max, 5000);
|
|
110
|
+
assert.equal(hunt.filters.surface_mode, "web2");
|
|
111
|
+
assert.ok(hunt.upstream_budget.used <= 50);
|
|
112
|
+
report.hunt = { returned: hunt.matches.length, target_samples: hunt.matches.reduce((sum, row) => sum + row.target_samples.length, 0),
|
|
113
|
+
ranking_scope: hunt.ranking_scope, upstream_budget: hunt.upstream_budget, partial_success: hunt.meta.partial_success };
|
|
114
|
+
if (hunt.meta.partial_success) process.exitCode = 1;
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
report.failure = String(error.message).replaceAll(config.apiKey, "[redacted]");
|
|
118
|
+
process.exitCode = 1;
|
|
119
|
+
} finally {
|
|
120
|
+
await client?.close();
|
|
121
|
+
await server?.close();
|
|
122
|
+
report.requests = upstream.diagnostics().upstream_requests.actual_requests;
|
|
123
|
+
report.concurrency = upstream.diagnostics().request_concurrency;
|
|
124
|
+
report.request_window = upstream.diagnostics().upstream_request_window;
|
|
125
|
+
console.log(JSON.stringify(report, null, 2));
|
|
126
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// One authenticated read-only hunt. Only aggregate measurements reach stdout;
|
|
2
|
+
// credentials and program/target records are never written to disk.
|
|
3
|
+
// With BBRADAR_API_KEY configured: node benchmark/live-hunt.mjs
|
|
4
|
+
// Optional experiment: node benchmark/live-hunt.mjs --catalog-concurrency=2
|
|
5
|
+
// This only lowers catalog concurrency; it never changes filters or page counts.
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
9
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
10
|
+
import { BBRadarClient } from "../dist/bbradarClient.js";
|
|
11
|
+
import { loadConfig } from "../dist/config.js";
|
|
12
|
+
import { createBbradarServer } from "../dist/server.js";
|
|
13
|
+
|
|
14
|
+
const args = {
|
|
15
|
+
surface_mode: "web2", target_types: ["domain", "api"], require_bounty: true,
|
|
16
|
+
min_bounty_max: 5000, min_eligible_targets: 3, min_total_targets: 5,
|
|
17
|
+
include_recent_changes: true, include_unknown_report_count: true,
|
|
18
|
+
recent_target_days: 90, max_results: 15, targets_per_result: 3,
|
|
19
|
+
ranking_mode: "balanced", upstream_request_budget: 50
|
|
20
|
+
};
|
|
21
|
+
const config = loadConfig();
|
|
22
|
+
const catalogOption = process.argv.slice(2);
|
|
23
|
+
assert.ok(catalogOption.length <= 1 && (!catalogOption.length || /^--catalog-concurrency=[1-4]$/.test(catalogOption[0])),
|
|
24
|
+
"Use --catalog-concurrency=1, 2, 3 or 4, or omit it for the server default.");
|
|
25
|
+
const catalogConcurrency = catalogOption.length ? Number(catalogOption[0].split("=")[1]) : Number.POSITIVE_INFINITY;
|
|
26
|
+
let activeCatalog = 0;
|
|
27
|
+
let peakCatalog = 0;
|
|
28
|
+
const catalogWaiters = [];
|
|
29
|
+
async function acquireCatalog() {
|
|
30
|
+
if (activeCatalog < catalogConcurrency) {
|
|
31
|
+
activeCatalog += 1;
|
|
32
|
+
peakCatalog = Math.max(peakCatalog, activeCatalog);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
await new Promise(resolve => catalogWaiters.push(resolve));
|
|
36
|
+
}
|
|
37
|
+
function releaseCatalog() {
|
|
38
|
+
const next = catalogWaiters.shift();
|
|
39
|
+
if (next) next(); else activeCatalog -= 1;
|
|
40
|
+
}
|
|
41
|
+
const upstream = new BBRadarClient(config);
|
|
42
|
+
const request = upstream.request.bind(upstream);
|
|
43
|
+
const redact = value => String(value).replaceAll(config.apiKey, "[redacted]");
|
|
44
|
+
let timings = [];
|
|
45
|
+
let catalogIds = new Set();
|
|
46
|
+
upstream.request = async options => {
|
|
47
|
+
const start = performance.now();
|
|
48
|
+
const endpoint = options.path.replace(/\/programs\/.+\/targets$/, "/programs/:program_id/targets");
|
|
49
|
+
const isCatalog = options.path === "/pro/programs";
|
|
50
|
+
if (isCatalog) await acquireCatalog();
|
|
51
|
+
try {
|
|
52
|
+
const result = await request(options);
|
|
53
|
+
for (const program of result.data?.programs ?? []) catalogIds.add(program.id);
|
|
54
|
+
timings.push({ endpoint, page: options.query?.page, elapsed_ms: Math.round(performance.now() - start),
|
|
55
|
+
status: result.status, cached: result.cached, coalesced: result.coalesced,
|
|
56
|
+
rows: result.data?.programs?.length ?? result.data?.results?.length ?? result.data?.targets?.length });
|
|
57
|
+
return result;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
timings.push({ endpoint, page: options.query?.page, elapsed_ms: Math.round(performance.now() - start),
|
|
60
|
+
status: error.status ?? null, error: redact(error.message) });
|
|
61
|
+
throw error;
|
|
62
|
+
} finally {
|
|
63
|
+
if (isCatalog) releaseCatalog();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const hash = values => createHash("sha256").update(JSON.stringify([...values].sort())).digest("hex");
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
await upstream.negotiateCapabilities();
|
|
70
|
+
const server = createBbradarServer(upstream, config);
|
|
71
|
+
const client = new Client({ name: "bbradar-live-benchmark", version: "1" });
|
|
72
|
+
const [a, b] = InMemoryTransport.createLinkedPair();
|
|
73
|
+
await Promise.all([client.connect(a), server.connect(b)]);
|
|
74
|
+
try {
|
|
75
|
+
timings = [];
|
|
76
|
+
catalogIds = new Set();
|
|
77
|
+
const before = upstream.diagnostics().upstream_requests.actual_requests;
|
|
78
|
+
const result = await client.callTool({ name: "find_hunt_targets", arguments: args });
|
|
79
|
+
const data = result.structuredContent;
|
|
80
|
+
const matches = data?.matches ?? [];
|
|
81
|
+
const observation = {
|
|
82
|
+
concurrency: config.maxConcurrentRequests,
|
|
83
|
+
catalog_concurrency_cap: Number.isFinite(catalogConcurrency) ? catalogConcurrency : "server_default",
|
|
84
|
+
catalog_request_peak: peakCatalog, is_error: result.isError ?? false,
|
|
85
|
+
error: data?.error ? { status: data.error.status, message: redact(data.error.message) } : undefined,
|
|
86
|
+
timing: data?.mcp_timing, phase_timing: data?.meta?.phase_timing,
|
|
87
|
+
returned: matches.length, target_samples: matches.reduce((count, match) => count + match.target_samples.length, 0),
|
|
88
|
+
programs_without_target_samples: matches.filter(match => !match.target_samples.length).length,
|
|
89
|
+
partial_success: data?.meta?.partial_success, coverage_incomplete: data?.result_guidance?.coverage_incomplete,
|
|
90
|
+
ranking_scope: data?.ranking_scope,
|
|
91
|
+
upstream_budget: data?.upstream_budget, actual_requests: upstream.diagnostics().upstream_requests.actual_requests - before,
|
|
92
|
+
catalog_rows: catalogIds.size, catalog_signature: hash(catalogIds),
|
|
93
|
+
shortlist_signature: hash(matches.map(match => match.program.id)),
|
|
94
|
+
errors: data?.errors?.map(error => ({ status: error.status, message: redact(error.message) })),
|
|
95
|
+
timings
|
|
96
|
+
};
|
|
97
|
+
if (result.isError || data?.meta?.partial_success) process.exitCode = 1;
|
|
98
|
+
console.log(JSON.stringify({ fixture: false, measured_at: new Date().toISOString(), arguments: args, observations: [observation],
|
|
99
|
+
quota: upstream.diagnostics().upstream_rate_limit,
|
|
100
|
+
request_window: upstream.diagnostics().upstream_request_window }, null, 2));
|
|
101
|
+
} finally { await client.close(); await server.close(); }
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error(JSON.stringify({ status: error.status ?? null, message: redact(error.message) }));
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Authenticated production smoke check through the actual STDIO entry point.
|
|
2
|
+
// Emits aggregates only; never writes credentials or program/target records.
|
|
3
|
+
// node benchmark/live-stdio.mjs [hunt|targets]
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
8
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
9
|
+
import { loadConfig } from "../dist/config.js";
|
|
10
|
+
|
|
11
|
+
const config = loadConfig();
|
|
12
|
+
const mode = process.argv[2] ?? "hunt";
|
|
13
|
+
assert.ok(["hunt", "targets"].includes(mode), "Use hunt or targets.");
|
|
14
|
+
const client = new Client({ name: "bbradar-live-stdio", version: "1" });
|
|
15
|
+
const transport = new StdioClientTransport({ command: process.execPath,
|
|
16
|
+
args: [fileURLToPath(new URL("../dist/index.js", import.meta.url))], stderr: "pipe",
|
|
17
|
+
env: { BBRADAR_API_KEY: config.apiKey, BBRADAR_API_BASE_URL: config.apiBaseUrl,
|
|
18
|
+
BBRADAR_MCP_TOOL_PROFILE: "core", BBRADAR_MCP_VALIDATE_ON_STARTUP: "true" } });
|
|
19
|
+
const report = { measured_at: new Date().toISOString(), fixture: false, transport: "stdio", mode };
|
|
20
|
+
async function call(name, args = {}) {
|
|
21
|
+
const result = await client.callTool({ name, arguments: args });
|
|
22
|
+
if (result.isError) throw new Error(`${name}: ${result.structuredContent?.error?.message ?? "MCP call failed"}`);
|
|
23
|
+
return result.structuredContent;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
await client.connect(transport);
|
|
27
|
+
const tools = await client.listTools();
|
|
28
|
+
report.core_tools = tools.tools.length;
|
|
29
|
+
const before = (await call("get_mcp_status")).status.client;
|
|
30
|
+
assert.equal(before.capability_negotiation.status, "negotiated");
|
|
31
|
+
let programs;
|
|
32
|
+
if (mode === "hunt") {
|
|
33
|
+
const hunt = await call("find_hunt_targets", {
|
|
34
|
+
surface_mode: "web2", target_types: ["domain", "api"], require_bounty: true,
|
|
35
|
+
min_bounty_max: 5000, min_eligible_targets: 3, min_total_targets: 5,
|
|
36
|
+
include_recent_changes: true, include_unknown_report_count: true,
|
|
37
|
+
recent_target_days: 90, max_results: 15, targets_per_result: 3,
|
|
38
|
+
ranking_mode: "balanced", upstream_request_budget: 50
|
|
39
|
+
});
|
|
40
|
+
report.hunt = { duration_ms: hunt.mcp_timing.duration_ms, phase_timing: hunt.meta.phase_timing,
|
|
41
|
+
programs: hunt.matches.length, target_samples: hunt.matches.reduce((n, row) => n + row.target_samples.length, 0),
|
|
42
|
+
upstream_requests: hunt.upstream_budget.used, partial_success: hunt.meta.partial_success,
|
|
43
|
+
ranking_scope: hunt.ranking_scope, catalog_pages_read: hunt.source_requests.filter(row => row.source === "programs").length,
|
|
44
|
+
errors: hunt.errors?.map(error => ({ status: error.status, message: error.message?.replaceAll(config.apiKey, "[redacted]") })),
|
|
45
|
+
shortlist_signature: createHash("sha256").update(JSON.stringify(hunt.matches.map(row => row.program.id).sort())).digest("hex") };
|
|
46
|
+
assert.equal(hunt.meta.partial_success, false, "The hunt returned incomplete work; do not count it as a successful performance run.");
|
|
47
|
+
assert.ok(hunt.matches.length > 0);
|
|
48
|
+
programs = hunt.matches.map(row => row.program);
|
|
49
|
+
} else {
|
|
50
|
+
programs = (await call("search_programs", { page_size: 10 })).programs;
|
|
51
|
+
}
|
|
52
|
+
// Compare the requested prefix with a fresh wider read. A live source change
|
|
53
|
+
// remains a failed comparison, never an excuse to substitute old data.
|
|
54
|
+
const selected = [...programs].sort((a, b) => (b.scope_summary?.target_counts?.eligible_for_bounty ?? 0)
|
|
55
|
+
- (a.scope_summary?.target_counts?.eligible_for_bounty ?? 0))[0];
|
|
56
|
+
assert.ok(selected?.id, "The bounded catalog returned no target candidate.");
|
|
57
|
+
const args = { program_id: selected.id, output_mode: "identifiers" };
|
|
58
|
+
const wide = await call("get_program_targets", { ...args, limit: 500 });
|
|
59
|
+
const small = await call("get_program_targets", { ...args, limit: 3 });
|
|
60
|
+
assert.ok(wide.targets.length > 0);
|
|
61
|
+
assert.ok(JSON.stringify(wide.targets.slice(0, 3)) === JSON.stringify(small.targets), "Fresh target prefixes differ; inspect source drift before claiming equivalence.");
|
|
62
|
+
assert.equal(wide.cache.no_store, true);
|
|
63
|
+
assert.equal(small.cache.no_store, true);
|
|
64
|
+
report.target_prefix = { equal: true, wide_read_rows: wide.targets.length, small_read_rows: small.targets.length,
|
|
65
|
+
wide_read_ms: wide.mcp_timing.duration_ms, small_read_ms: small.mcp_timing.duration_ms,
|
|
66
|
+
small_read_has_more: small.meta.has_more, small_read_next_offset: small.meta.next_offset, both_no_store: true };
|
|
67
|
+
const after = (await call("get_mcp_status")).status.client;
|
|
68
|
+
report.upstream_requests = after.upstream_requests.actual_requests - before.upstream_requests.actual_requests;
|
|
69
|
+
report.response_cache = after.response_cache;
|
|
70
|
+
report.request_window = after.upstream_request_window;
|
|
71
|
+
assert.equal(after.request_concurrency.active, 0);
|
|
72
|
+
assert.equal(after.request_concurrency.queued, 0);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
report.failure = String(error.message).replaceAll(config.apiKey, "[redacted]");
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
} finally {
|
|
77
|
+
await client.close();
|
|
78
|
+
await transport.close();
|
|
79
|
+
console.log(JSON.stringify(report, null, 2));
|
|
80
|
+
}
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
/** Catalog metadata stays available when Activity computation is disabled. */
|
|
2
2
|
export declare function requiresActivityQuery(input?: Record<string, unknown>): boolean;
|
|
3
|
+
/** Validate dependencies without supplying or changing the user's periods. */
|
|
4
|
+
export declare function validateActivityCatalogInput(input: Record<string, unknown>): void;
|
package/dist/activityContract.js
CHANGED
|
@@ -12,4 +12,36 @@ export function requiresActivityQuery(input = {}) {
|
|
|
12
12
|
return Object.entries(input).some(([key, value]) => value !== undefined && value !== null
|
|
13
13
|
&& (key === "sort_by" ? value !== "updated" : ACTIVITY_FILTERS.has(key)));
|
|
14
14
|
}
|
|
15
|
+
/** Validate dependencies without supplying or changing the user's periods. */
|
|
16
|
+
export function validateActivityCatalogInput(input) {
|
|
17
|
+
const requirements = {
|
|
18
|
+
scope: ["scope_added_min", "scope_removed_min", "scope_net_min", "scope_net_max"],
|
|
19
|
+
reward: ["reward_change", "reward_change_min_pct"],
|
|
20
|
+
updates: ["active_days_min", "changes_min", "change_category"],
|
|
21
|
+
reports: ["reports_min", "reports_max", "reports_change", "reports_change_min_pct"],
|
|
22
|
+
resolved: ["resolved_min"], status: ["status_change"],
|
|
23
|
+
payout: ["payout_min", "payout_max", "payout_metric", "payout_activity", "payout_positive", "payout_precision"]
|
|
24
|
+
};
|
|
25
|
+
const sortPeriods = {
|
|
26
|
+
payout_amount: "payout", payout_lower_bound: "payout", payout_upper_bound: "payout",
|
|
27
|
+
scope_added: "scope", scope_net: "scope", reward_change: "reward", active_days: "updates",
|
|
28
|
+
last_change: "updates", reports: "reports", reports_change: "reports", resolved: "resolved"
|
|
29
|
+
};
|
|
30
|
+
for (const [prefix, keys] of Object.entries(requirements)) {
|
|
31
|
+
const signal = keys.find((key) => input[key] !== undefined);
|
|
32
|
+
if ((signal || sortPeriods[String(input.sort_by)] === prefix) && input[`${prefix}_period`] === undefined) {
|
|
33
|
+
throw new Error(`Specify ${prefix}_period for ${signal ?? `sort_by=${String(input.sort_by)}`}. Periods are never inferred from another signal.`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const period = input.payout_period;
|
|
37
|
+
const metric = input.payout_metric ?? (period === "all_time" ? "payout_total" : "payout_increase");
|
|
38
|
+
if (period !== undefined && ((metric === "payout_total") !== (period === "all_time")
|
|
39
|
+
|| (metric === "payout_native_90d" && period !== "90d"))) {
|
|
40
|
+
throw new Error("Payout metric and period are incompatible: payout_total requires all_time; payout_increase uses 24h, 7d, 30d or 90d; payout_native_90d requires 90d. Keep the requested period and choose its supported metric.");
|
|
41
|
+
}
|
|
42
|
+
if (input.payout_activity !== undefined && (metric !== "payout_increase"
|
|
43
|
+
|| (input.payout_activity !== "increase" && period !== "30d"))) {
|
|
44
|
+
throw new Error("payout_activity requires payout_increase; no_increase and lower require payout_period=30d.");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
15
47
|
//# sourceMappingURL=activityContract.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"activityContract.js","sourceRoot":"","sources":["../src/activityContract.ts"],"names":[],"mappings":"AAAA,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC;IAC/B,eAAe,EAAE,eAAe,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,YAAY,EAAE,YAAY;IACnG,iBAAiB,EAAE,iBAAiB,EAAE,cAAc,EAAE,iBAAiB,EAAE,mBAAmB;IAC5F,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,eAAe,EAAE,uBAAuB;IAC3F,gBAAgB,EAAE,iBAAiB,EAAE,aAAa,EAAE,iBAAiB,EAAE,gBAAgB;IACvF,aAAa,EAAE,aAAa,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,iBAAiB;IAC3F,iBAAiB,EAAE,cAAc,EAAE,sBAAsB,EAAE,0BAA0B;IACrF,0BAA0B,EAAE,eAAe,EAAE,eAAe,EAAE,gBAAgB,EAAE,iBAAiB;CAClG,CAAC,CAAC;AAEH,8EAA8E;AAC9E,MAAM,UAAU,qBAAqB,CAAC,QAAiC,EAAE;IACvE,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;WACpF,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAC9E,CAAC"}
|
|
1
|
+
{"version":3,"file":"activityContract.js","sourceRoot":"","sources":["../src/activityContract.ts"],"names":[],"mappings":"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"}
|
package/dist/bbradarClient.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export type ApiRequestOptions = {
|
|
|
10
10
|
staleWhileRevalidateMs?: number;
|
|
11
11
|
timeoutMs?: number;
|
|
12
12
|
signal?: AbortSignal;
|
|
13
|
+
expectedArray?: "programs" | "targets" | "results";
|
|
13
14
|
};
|
|
14
15
|
export type ApiReadControls = Pick<ApiRequestOptions, "cache" | "coalesce" | "timeoutMs" | "signal">;
|
|
15
16
|
export type ApiResult<T> = {
|
|
@@ -73,7 +74,10 @@ export declare class BBRadarClient {
|
|
|
73
74
|
private capabilityNegotiation;
|
|
74
75
|
private capabilityState;
|
|
75
76
|
constructor(config: ServerConfig);
|
|
76
|
-
withRequestContext<T>(signal: AbortSignal, callback: () => T | Promise<T>, snapshotStart?: boolean): Promise<T>;
|
|
77
|
+
withRequestContext<T>(signal: AbortSignal, callback: () => T | Promise<T>, snapshotStart?: boolean, timeoutMs?: number): Promise<T>;
|
|
78
|
+
/** Give one aggregate stage a shorter budget without extending its parent. */
|
|
79
|
+
withRequestTimeout<T>(timeoutMs: number, callback: () => T | Promise<T>): Promise<T>;
|
|
80
|
+
throwIfRequestAborted(): void;
|
|
77
81
|
diagnostics(): {
|
|
78
82
|
response_cache: {
|
|
79
83
|
entries: number;
|