@bbradar/mcp 0.1.20 → 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 +43 -8
- package/benchmark/mcp-benchmark.mjs +167 -0
- package/dist/bbradarClient.d.ts +51 -2
- package/dist/bbradarClient.js +331 -36
- package/dist/bbradarClient.js.map +1 -1
- package/dist/cache.d.ts +8 -1
- package/dist/cache.js +16 -5
- package/dist/cache.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.js +48 -8
- 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 +597 -59
- package/dist/server.js.map +1 -1
- package/package.json +4 -2
package/dist/server.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { BBRadarApiError } from "./bbradarClient.js";
|
|
5
|
+
import { NumericCursorStore } from "./cursorStore.js";
|
|
5
6
|
import { stableStringify, stripUndefined } from "./json.js";
|
|
6
7
|
import { FixedWindowRateLimiter } from "./rateLimit.js";
|
|
7
|
-
import { readArray, readBoolean, readNumber, readObject, readString, sanitizeJson, sanitizeChange, sanitizeProgram, sanitizeTarget, sanitizeTargetsForExport } from "./sanitize.js";
|
|
8
|
-
const SERVER_VERSION = "0.
|
|
8
|
+
import { readArray, readBoolean, readNumber, readObject, readString, sanitizeJson, sanitizeChange, sanitizeProgram, sanitizeRepositoryChange, sanitizeTarget, sanitizeTargetsForExport } from "./sanitize.js";
|
|
9
|
+
const SERVER_VERSION = "0.2.0";
|
|
9
10
|
const MAX_PROGRAMS_PER_SEARCH = 100;
|
|
10
11
|
const MAX_TARGET_EXPORT = 500;
|
|
11
12
|
const MAX_RECENT_CHANGES = 100;
|
|
@@ -29,10 +30,13 @@ const MAX_FILTER_DISCOVERY_PAGES = 5;
|
|
|
29
30
|
const DEFAULT_FILTER_TARGET_SAMPLE_PROGRAMS = 10;
|
|
30
31
|
const MAX_FILTER_TARGET_SAMPLE_PROGRAMS = 25;
|
|
31
32
|
const MAX_LOCAL_EXPORT_RESOURCES = 25;
|
|
33
|
+
const MAX_LOCAL_EXPORT_BYTES = 64 * 1024 * 1024;
|
|
34
|
+
const LOCAL_EXPORT_TTL_MS = 10 * 60 * 1000;
|
|
32
35
|
const EXPORT_PREVIEW_LIMIT = 25;
|
|
33
36
|
const STALE_PROGRAM_ID_RESOLVE_PAGES = 1;
|
|
34
37
|
const STALE_PROGRAM_ID_RESOLVE_BUDGET = 12;
|
|
35
38
|
const PROGRAM_FALLBACK_CACHE_MAX_ENTRIES = 200;
|
|
39
|
+
const MAX_IN_MEMORY_FEED_CURSORS = 500;
|
|
36
40
|
const MAX_RESOLVE_SEARCH_QUERIES = 4;
|
|
37
41
|
const FAST_RESOLVE_SEARCH_QUERIES = 2;
|
|
38
42
|
const MAX_DIRECT_HANDLE_RESOLUTION_REQUESTS = 96;
|
|
@@ -45,6 +49,7 @@ const CORE_TOOL_NAMES = new Set([
|
|
|
45
49
|
"get_program_eligible_targets_by_name",
|
|
46
50
|
"get_program_targets",
|
|
47
51
|
"get_recent_changes",
|
|
52
|
+
"get_repository_changes",
|
|
48
53
|
"get_latest_added_targets",
|
|
49
54
|
"get_opportunities",
|
|
50
55
|
"find_programs",
|
|
@@ -54,6 +59,11 @@ const CORE_TOOL_NAMES = new Set([
|
|
|
54
59
|
"get_mcp_status",
|
|
55
60
|
"export_targets"
|
|
56
61
|
]);
|
|
62
|
+
const TOOL_ENDPOINT_REQUIREMENTS = {
|
|
63
|
+
get_repository_changes: "repository_changes",
|
|
64
|
+
export_targets: "target_export",
|
|
65
|
+
get_opportunities: "opportunities"
|
|
66
|
+
};
|
|
57
67
|
const PROGRAM_ID_PLATFORM_GUESSES = [
|
|
58
68
|
"HackerOne",
|
|
59
69
|
"BugCrowd",
|
|
@@ -159,6 +169,7 @@ const TARGET_QUERY_STOP_WORDS = new Set([
|
|
|
159
169
|
]);
|
|
160
170
|
const targetListModeSchema = z.enum(["identifiers", "compact", "full"]);
|
|
161
171
|
const changeListModeSchema = z.enum(["compact", "full"]);
|
|
172
|
+
const repositoryChangeListModeSchema = z.enum(["compact", "full"]);
|
|
162
173
|
const programListModeSchema = z.enum(["compact", "full"]);
|
|
163
174
|
const rewardThresholdModeSchema = z.enum(["max_at_least", "min_at_least"]);
|
|
164
175
|
const vdpModeSchema = z.enum(["include", "exclude", "only_likely", "only_known_no_bounty"]);
|
|
@@ -227,6 +238,8 @@ const pageSchema = z.number().int().min(1).default(1);
|
|
|
227
238
|
const programsPageSizeSchema = z.number().int().min(1).max(MAX_PROGRAMS_PER_SEARCH).default(20);
|
|
228
239
|
const recentChangesPageSizeSchema = z.number().int().min(1).max(MAX_RECENT_CHANGES).default(50);
|
|
229
240
|
const opportunityLevelSchema = z.enum(["elite", "hot", "strong", "potential"]);
|
|
241
|
+
const targetOpportunityLabelSchema = z.enum(["Elite Target", "Strong Target", "Potential Target", "Normal"]);
|
|
242
|
+
const competitionRiskSchema = z.enum(["Low", "Medium", "High", "Unknown"]);
|
|
230
243
|
const programSortSchema = z
|
|
231
244
|
.enum([
|
|
232
245
|
"best_match",
|
|
@@ -278,6 +291,7 @@ function toolTimingOutputSchema() {
|
|
|
278
291
|
function cacheOutputSchema() {
|
|
279
292
|
return z.object({
|
|
280
293
|
hit: z.boolean(),
|
|
294
|
+
stale_while_revalidate: z.boolean().optional(),
|
|
281
295
|
coalesced_live_request: z.boolean().optional(),
|
|
282
296
|
expires_at: z.string().optional()
|
|
283
297
|
});
|
|
@@ -364,10 +378,12 @@ const programCandidatePresetInputSchema = {
|
|
|
364
378
|
output_mode: programListModeSchema.default("compact"),
|
|
365
379
|
upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(30)
|
|
366
380
|
};
|
|
367
|
-
function createProfiledToolRegistrar(server, profile) {
|
|
381
|
+
function createProfiledToolRegistrar(server, profile, client) {
|
|
368
382
|
return (name, toolConfig, callback) => {
|
|
369
383
|
const registeredTool = server.registerTool(name, toolConfig, callback);
|
|
370
|
-
|
|
384
|
+
const requiredEndpoint = TOOL_ENDPOINT_REQUIREMENTS[name];
|
|
385
|
+
if ((profile === "core" && !CORE_TOOL_NAMES.has(name)) ||
|
|
386
|
+
(requiredEndpoint !== undefined && !client.supportsEndpoint(requiredEndpoint))) {
|
|
371
387
|
registeredTool.disable();
|
|
372
388
|
}
|
|
373
389
|
return registeredTool;
|
|
@@ -391,7 +407,9 @@ export function createBbradarServer(client, config) {
|
|
|
391
407
|
"Partial program name + action: run_program_name_action. Partial program name only: resolve_program.",
|
|
392
408
|
"Default to bounty-eligible in-scope data. Include out-of-scope, ineligible, VDP, disclosure-only, or no-bounty data only when the user explicitly asks.",
|
|
393
409
|
"Recently updated programs across platforms: find_recently_updated_programs. Do not infer a platform such as Immunefi unless the user explicitly names it.",
|
|
410
|
+
"Self-Hosted programs use Self-Hosted:slug ids and are available through program search, exact detail, and target tools. Respect data_capabilities; do not infer missing opportunity, report-count, target-change, submission-cost, or repository signals.",
|
|
394
411
|
"Freshness: find_recently_updated_programs, get_latest_added_targets, check_program_new_targets, check_watchlist_new_targets, find_recent_by_type, get_program_scope_delta.",
|
|
412
|
+
"Repository activity: get_repository_changes defaults to compact program-only events. Seed with after_id, then resume the in-memory per-filter cursor.",
|
|
395
413
|
"Discovery: find_program_candidates, find_wildcard_programs, find_recently_added_wildcards, find_low_competition_high_reward, find_stack_matches, find_language_programs, find_target_type_programs, find_reward_programs, find_paid_programs, find_vdp_programs, find_web3_contests, find_low_noise_programs.",
|
|
396
414
|
"Details: get_program_brief, get_program_scope_summary, get_program_target_breakdown, find_programs_by_target, compare_programs_compact.",
|
|
397
415
|
"Prefer compact/identifier outputs and disclose sampled ranking scope when present."
|
|
@@ -402,13 +420,26 @@ export function createBbradarServer(client, config) {
|
|
|
402
420
|
}, {
|
|
403
421
|
instructions: instructions.join("\n")
|
|
404
422
|
});
|
|
405
|
-
const registerTool = createProfiledToolRegistrar(server, config.toolProfile);
|
|
423
|
+
const registerTool = createProfiledToolRegistrar(server, config.toolProfile, client);
|
|
406
424
|
const rateLimiter = new FixedWindowRateLimiter();
|
|
407
425
|
const exportStore = new Map();
|
|
426
|
+
const continuationStore = new Map();
|
|
427
|
+
const feedCursorStore = new NumericCursorStore(config.cursorStatePath, MAX_IN_MEMORY_FEED_CURSORS);
|
|
428
|
+
const maxProgramsPageSize = client.endpointNumber("programs", "max_page_size", MAX_PROGRAMS_PER_SEARCH);
|
|
429
|
+
const maxProgramTargetsPageSize = client.endpointNumber("program_targets", "max_page_size", MAX_TARGETS_PER_PROGRAM);
|
|
430
|
+
const maxTargetChangesPageSize = client.endpointNumber("target_changes", "max_page_size", MAX_RECENT_CHANGES);
|
|
431
|
+
const maxRepositoryChangesPageSize = client.endpointNumber("repository_changes", "max_page_size", MAX_RECENT_CHANGES);
|
|
432
|
+
const maxTargetExport = client.endpointNumber("target_export", "max_targets", MAX_TARGET_EXPORT);
|
|
433
|
+
const maxTargetExportProgramIds = client.endpointNumber("target_export", "max_program_ids", 50);
|
|
434
|
+
const programsPageSizeSchema = z.number().int().min(1).max(maxProgramsPageSize).default(Math.min(20, maxProgramsPageSize));
|
|
435
|
+
const recentChangesPageSizeSchema = z.number().int().min(1).max(maxTargetChangesPageSize).default(Math.min(50, maxTargetChangesPageSize));
|
|
436
|
+
const repositoryChangesPageSizeSchema = z.number().int().min(1).max(maxRepositoryChangesPageSize).default(Math.min(50, maxRepositoryChangesPageSize));
|
|
437
|
+
const acceptedTargetLimitSchema = z.number().int().min(1).max(maxProgramTargetsPageSize);
|
|
408
438
|
registerTool("search_programs", {
|
|
409
439
|
title: "Search BBRadar Programs",
|
|
410
|
-
description: `
|
|
440
|
+
description: `Search or list active platform and Self-Hosted programs. page_size max ${MAX_PROGRAMS_PER_SEARCH}.`,
|
|
411
441
|
inputSchema: {
|
|
442
|
+
search: searchTextSchema.optional().describe("Server-side program name, handle, platform, policy-host, or target search."),
|
|
412
443
|
platforms: platformListSchema(),
|
|
413
444
|
tags: tagListSchema(),
|
|
414
445
|
updated_since: isoDateTimeSchema.optional().describe("ISO datetime."),
|
|
@@ -505,8 +536,8 @@ export function createBbradarServer(client, config) {
|
|
|
505
536
|
language_tags: languageTagListSchema(),
|
|
506
537
|
include_out_of_scope: z.boolean().default(false),
|
|
507
538
|
include_ineligible: z.boolean().default(false),
|
|
508
|
-
page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
509
|
-
max_targets: acceptedTargetLimitSchema.default(25),
|
|
539
|
+
page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
540
|
+
max_targets: acceptedTargetLimitSchema.default(Math.min(25, maxProgramTargetsPageSize)),
|
|
510
541
|
group_limit: z.number().int().min(1).max(MAX_TARGETS_PER_PROGRAM).default(100),
|
|
511
542
|
target_list_mode: targetListModeSchema.default("identifiers"),
|
|
512
543
|
max_resolve_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
|
|
@@ -549,6 +580,7 @@ export function createBbradarServer(client, config) {
|
|
|
549
580
|
resolved_program: jsonRecordSchema().optional(),
|
|
550
581
|
program_id: z.string().optional(),
|
|
551
582
|
targets: z.array(z.unknown()).optional(),
|
|
583
|
+
continuation: jsonRecordSchema().optional(),
|
|
552
584
|
warnings: z.array(z.string()).optional(),
|
|
553
585
|
upstream_budget: jsonRecordSchema().optional(),
|
|
554
586
|
ranking_scope: jsonRecordSchema().optional(),
|
|
@@ -558,14 +590,19 @@ export function createBbradarServer(client, config) {
|
|
|
558
590
|
}, (args) => runTool("get_program_eligible_targets_by_name", config, rateLimiter, async () => getProgramEligibleTargetsByName(client, config, args)));
|
|
559
591
|
registerTool("get_program_targets", {
|
|
560
592
|
title: "Get Program Targets",
|
|
561
|
-
description: `Get active targets for one program_id, handle, or human program name. This is a final source for eligible target lists; do not call summary/breakdown unless grouped counts are needed. For a program name query, get_program_eligible_targets_by_name is the simplest path. limit max ${
|
|
593
|
+
description: `Get active targets for one program_id, handle, or human program name. This is a final source for eligible target lists; do not call summary/breakdown unless grouped counts are needed. For a program name query, get_program_eligible_targets_by_name is the simplest path. limit max ${maxProgramTargetsPageSize}.`,
|
|
562
594
|
inputSchema: {
|
|
563
595
|
program_id: programIdentifierSchema,
|
|
596
|
+
search: searchTextSchema.optional(),
|
|
597
|
+
target_type: filterValueSchema().optional(),
|
|
598
|
+
min_target_score: z.number().int().min(0).max(100).optional(),
|
|
599
|
+
target_opportunity_label: targetOpportunityLabelSchema.optional(),
|
|
600
|
+
competition_risk: competitionRiskSchema.optional(),
|
|
564
601
|
include_out_of_scope: z.boolean().default(false),
|
|
565
602
|
include_ineligible: z.boolean().default(false),
|
|
566
603
|
strict_scope_filter: z.boolean().default(true),
|
|
567
604
|
offset: z.number().int().min(0).default(0),
|
|
568
|
-
limit: z.number().int().min(1).max(
|
|
605
|
+
limit: z.number().int().min(1).max(maxProgramTargetsPageSize).default(Math.min(100, maxProgramTargetsPageSize)),
|
|
569
606
|
output_mode: targetListModeSchema.default("compact")
|
|
570
607
|
},
|
|
571
608
|
outputSchema: {
|
|
@@ -576,13 +613,21 @@ export function createBbradarServer(client, config) {
|
|
|
576
613
|
},
|
|
577
614
|
annotations: readOnlyAnnotations
|
|
578
615
|
}, (args) => runTool("get_program_targets", config, rateLimiter, async () => {
|
|
579
|
-
|
|
616
|
+
const payload = await runProgramIdTool(client, config, args.program_id, async (programId) => getProgramTargetsPayload(client, args, programId), async (indexFallback) => getProgramTargetsExportFallbackPayload(client, args, indexFallback.resolvedProgramId));
|
|
617
|
+
return applyCollectionContinuation(payload, "targets", "program targets", continuationStore, config);
|
|
580
618
|
}));
|
|
581
619
|
registerTool("get_recent_changes", {
|
|
582
620
|
title: "Get Recent Target Changes",
|
|
583
621
|
description: `List recent target changes. page_size max ${MAX_RECENT_CHANGES}.`,
|
|
584
622
|
inputSchema: {
|
|
585
623
|
change_type: z.enum(["added", "updated", "removed"]).optional(),
|
|
624
|
+
after_id: z.number().int().min(0).optional().describe("Incremental cursor; returns changes after this id in ascending order."),
|
|
625
|
+
resume_cursor: z.boolean().default(false).describe("Resume the in-memory cursor for this exact normalized filter set. Seed it once with after_id."),
|
|
626
|
+
max_cursor_pages: z.number().int().min(1).max(10).default(2),
|
|
627
|
+
cursor_time_budget_ms: z.number().int().min(100).max(10_000).default(3_000),
|
|
628
|
+
min_target_score: z.number().int().min(0).max(100).optional(),
|
|
629
|
+
target_opportunity_label: targetOpportunityLabelSchema.optional(),
|
|
630
|
+
competition_risk: competitionRiskSchema.optional(),
|
|
586
631
|
include_removed: z.boolean().default(false),
|
|
587
632
|
include_ineligible: z.boolean().default(false),
|
|
588
633
|
include_out_of_scope: z.boolean().default(false),
|
|
@@ -597,25 +642,102 @@ export function createBbradarServer(client, config) {
|
|
|
597
642
|
...apiEnvelopeOutputShape(),
|
|
598
643
|
changes: z.array(jsonRecordSchema()).optional(),
|
|
599
644
|
meta: jsonRecordSchema().optional(),
|
|
600
|
-
facets: jsonRecordSchema().optional()
|
|
645
|
+
facets: jsonRecordSchema().optional(),
|
|
646
|
+
cursor: jsonRecordSchema().optional(),
|
|
647
|
+
continuation: jsonRecordSchema().optional()
|
|
601
648
|
},
|
|
602
649
|
annotations: readOnlyAnnotations
|
|
603
650
|
}, (args) => runTool("get_recent_changes", config, rateLimiter, async () => {
|
|
604
|
-
const { output_mode, ...filters } = args;
|
|
605
|
-
const
|
|
606
|
-
const
|
|
651
|
+
const { output_mode, resume_cursor, max_cursor_pages, cursor_time_budget_ms, ...filters } = args;
|
|
652
|
+
const cursor = resolveFeedCursor(feedCursorStore, "target_changes", filters, resume_cursor);
|
|
653
|
+
const { min_target_score, target_opportunity_label, competition_risk, ...upstreamFilters } = filters;
|
|
654
|
+
const collected = await collectCursorFeedPages(feedCursorStore, cursor, max_cursor_pages, cursor_time_budget_ms, (afterId) => client.getRecentChanges(toQuery({
|
|
655
|
+
...upstreamFilters,
|
|
656
|
+
minTargetScore: min_target_score,
|
|
657
|
+
targetOpportunityLabel: target_opportunity_label,
|
|
658
|
+
competitionRisk: competition_risk,
|
|
659
|
+
after_id: afterId,
|
|
660
|
+
page: afterId === undefined ? filters.page : 1
|
|
661
|
+
})));
|
|
662
|
+
const api = collected.lastApi;
|
|
663
|
+
const data = collected.lastData;
|
|
607
664
|
const sanitizedMeta = readObject(sanitizeJson(data?.meta));
|
|
608
|
-
const changes =
|
|
665
|
+
const changes = collected.results
|
|
609
666
|
.map((change) => sanitizeChange(change, config.webBaseUrl))
|
|
610
667
|
.map((change) => formatChange(change, output_mode));
|
|
611
|
-
return withApiMetadata(api, {
|
|
668
|
+
return applyCollectionContinuation(withApiMetadata(api, {
|
|
612
669
|
changes,
|
|
613
670
|
meta: stripUndefined({
|
|
614
671
|
...(sanitizedMeta ?? {}),
|
|
615
|
-
output_mode
|
|
672
|
+
output_mode,
|
|
673
|
+
pages_fetched: collected.pagesFetched,
|
|
674
|
+
returned_across_pages: changes.length,
|
|
675
|
+
cursor_page_budget_exhausted: collected.pageBudgetExhausted,
|
|
676
|
+
cursor_time_budget_exhausted: collected.timeBudgetExhausted
|
|
616
677
|
}),
|
|
617
|
-
facets: sanitizeJson(data?.facets)
|
|
618
|
-
|
|
678
|
+
facets: sanitizeJson(data?.facets),
|
|
679
|
+
cursor: collected.cursor,
|
|
680
|
+
source_requests: collected.sourceRequests.length > 1 ? collected.sourceRequests : undefined
|
|
681
|
+
}), "changes", "target changes", continuationStore, config);
|
|
682
|
+
}));
|
|
683
|
+
registerTool("get_repository_changes", {
|
|
684
|
+
title: "Get Repository Changes",
|
|
685
|
+
description: "Read-only repository push/change feed. Defaults to lightweight program summaries; use after_id for efficient incremental polling. For rewarded Web3 repository targets, call this in parallel with find_programs using the same platforms, then intersect event program ids with the ranked rewarded programs.",
|
|
686
|
+
inputSchema: {
|
|
687
|
+
after_id: z.number().int().min(0).optional().describe("Incremental cursor; returns events after this id in ascending order."),
|
|
688
|
+
resume_cursor: z.boolean().default(false).describe("Resume the in-memory cursor for this exact normalized filter set. Seed it once with after_id."),
|
|
689
|
+
max_cursor_pages: z.number().int().min(1).max(10).default(2),
|
|
690
|
+
cursor_time_budget_ms: z.number().int().min(100).max(10_000).default(3_000),
|
|
691
|
+
include_targets: z.boolean().default(false).describe("Include matched targets. False is faster and returns affected programs only."),
|
|
692
|
+
search: searchTextSchema.optional(),
|
|
693
|
+
platforms: platformListSchema(),
|
|
694
|
+
tags: tagListSchema(),
|
|
695
|
+
free_submission_only: z.boolean().default(false),
|
|
696
|
+
include_removed: z.boolean().default(false),
|
|
697
|
+
include_ineligible: z.boolean().default(false),
|
|
698
|
+
include_out_of_scope: z.boolean().default(false),
|
|
699
|
+
page: pageSchema,
|
|
700
|
+
page_size: repositoryChangesPageSizeSchema,
|
|
701
|
+
output_mode: repositoryChangeListModeSchema.default("compact")
|
|
702
|
+
},
|
|
703
|
+
outputSchema: {
|
|
704
|
+
...apiEnvelopeOutputShape(),
|
|
705
|
+
repository_changes: z.array(jsonRecordSchema()).optional(),
|
|
706
|
+
meta: jsonRecordSchema().optional(),
|
|
707
|
+
facets: jsonRecordSchema().optional(),
|
|
708
|
+
available_facets: jsonRecordSchema().optional(),
|
|
709
|
+
cursor: jsonRecordSchema().optional(),
|
|
710
|
+
continuation: jsonRecordSchema().optional()
|
|
711
|
+
},
|
|
712
|
+
annotations: readOnlyAnnotations
|
|
713
|
+
}, (args) => runTool("get_repository_changes", config, rateLimiter, async () => {
|
|
714
|
+
if (!client.supportsEndpoint("repository_changes")) {
|
|
715
|
+
throw new Error("The negotiated BBRadar API contract does not expose repository changes.");
|
|
716
|
+
}
|
|
717
|
+
const { output_mode, resume_cursor, max_cursor_pages, cursor_time_budget_ms, ...filters } = args;
|
|
718
|
+
const cursor = resolveFeedCursor(feedCursorStore, "repository_changes", filters, resume_cursor);
|
|
719
|
+
const collected = await collectCursorFeedPages(feedCursorStore, cursor, max_cursor_pages, cursor_time_budget_ms, (afterId) => client.getRepositoryChanges(toQuery({
|
|
720
|
+
...filters,
|
|
721
|
+
after_id: afterId,
|
|
722
|
+
page: afterId === undefined ? filters.page : 1
|
|
723
|
+
})));
|
|
724
|
+
const api = collected.lastApi;
|
|
725
|
+
const data = collected.lastData;
|
|
726
|
+
return applyCollectionContinuation(withApiMetadata(api, {
|
|
727
|
+
repository_changes: collected.results.map((change) => sanitizeRepositoryChange(change, config.webBaseUrl)).map((change) => formatRepositoryChange(change, output_mode)),
|
|
728
|
+
meta: stripUndefined({
|
|
729
|
+
...(readObject(sanitizeJson(data?.meta)) ?? {}),
|
|
730
|
+
output_mode,
|
|
731
|
+
pages_fetched: collected.pagesFetched,
|
|
732
|
+
returned_across_pages: collected.results.length,
|
|
733
|
+
cursor_page_budget_exhausted: collected.pageBudgetExhausted,
|
|
734
|
+
cursor_time_budget_exhausted: collected.timeBudgetExhausted
|
|
735
|
+
}),
|
|
736
|
+
facets: sanitizeJson(data?.facets),
|
|
737
|
+
available_facets: sanitizeJson(data?.available_facets),
|
|
738
|
+
cursor: collected.cursor,
|
|
739
|
+
source_requests: collected.sourceRequests.length > 1 ? collected.sourceRequests : undefined
|
|
740
|
+
}), "repository_changes", "repository changes", continuationStore, config);
|
|
619
741
|
}));
|
|
620
742
|
registerTool("get_latest_added_targets", {
|
|
621
743
|
title: "Get Latest Added Targets",
|
|
@@ -630,7 +752,7 @@ export function createBbradarServer(client, config) {
|
|
|
630
752
|
include_unknown_report_count: z.boolean().default(true),
|
|
631
753
|
include_out_of_scope: z.boolean().default(false).describe("Include out-of-scope."),
|
|
632
754
|
include_ineligible: z.boolean().default(false).describe("Include ineligible."),
|
|
633
|
-
recent_changes_page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
755
|
+
recent_changes_page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
634
756
|
max_results: z.number().int().min(1).max(MAX_RECENT_CHANGES).default(10),
|
|
635
757
|
include_full_target_list: z.boolean().default(true).describe("Fetch the newest matching program's complete target list. Set false for the fastest cross-program discovery."),
|
|
636
758
|
full_target_list_mode: targetListModeSchema.default("identifiers"),
|
|
@@ -701,8 +823,8 @@ export function createBbradarServer(client, config) {
|
|
|
701
823
|
include_removed: z.boolean().default(true),
|
|
702
824
|
include_out_of_scope: z.boolean().default(false),
|
|
703
825
|
include_ineligible: z.boolean().default(false),
|
|
704
|
-
page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
705
|
-
max_targets: acceptedTargetLimitSchema.default(50),
|
|
826
|
+
page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
827
|
+
max_targets: acceptedTargetLimitSchema.default(Math.min(50, maxProgramTargetsPageSize)),
|
|
706
828
|
target_list_mode: targetListModeSchema.default("identifiers")
|
|
707
829
|
},
|
|
708
830
|
outputSchema: {
|
|
@@ -728,7 +850,7 @@ export function createBbradarServer(client, config) {
|
|
|
728
850
|
platforms: platformListSchema(),
|
|
729
851
|
tags: tagListSchema(),
|
|
730
852
|
language_tags: languageTagListSchema(),
|
|
731
|
-
page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
853
|
+
page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
732
854
|
max_programs: z.number().int().min(1).max(50).default(10),
|
|
733
855
|
sample_size: z.number().int().min(1).max(20).default(5),
|
|
734
856
|
target_list_mode: targetListModeSchema.default("identifiers")
|
|
@@ -749,8 +871,8 @@ export function createBbradarServer(client, config) {
|
|
|
749
871
|
since: isoDateTimeSchema.optional().describe("ISO datetime."),
|
|
750
872
|
target_type: filterValueSchema().optional(),
|
|
751
873
|
include_ineligible: z.boolean().default(false).describe("Include ineligible."),
|
|
752
|
-
page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
753
|
-
max_targets: acceptedTargetLimitSchema.default(25),
|
|
874
|
+
page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
875
|
+
max_targets: acceptedTargetLimitSchema.default(Math.min(25, maxProgramTargetsPageSize)),
|
|
754
876
|
target_list_mode: targetListModeSchema.default("identifiers")
|
|
755
877
|
},
|
|
756
878
|
outputSchema: {
|
|
@@ -774,7 +896,7 @@ export function createBbradarServer(client, config) {
|
|
|
774
896
|
since: isoDateTimeSchema.optional(),
|
|
775
897
|
target_type: filterValueSchema().optional(),
|
|
776
898
|
include_ineligible: z.boolean().default(false),
|
|
777
|
-
page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
899
|
+
page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
778
900
|
max_targets_per_program: z.number().int().min(1).max(50).default(10),
|
|
779
901
|
target_list_mode: targetListModeSchema.default("identifiers")
|
|
780
902
|
},
|
|
@@ -820,14 +942,16 @@ export function createBbradarServer(client, config) {
|
|
|
820
942
|
}));
|
|
821
943
|
registerTool("find_programs", {
|
|
822
944
|
title: "Find BBRadar Programs",
|
|
823
|
-
description: "Ranked program discovery with filters and compact output.",
|
|
945
|
+
description: "Ranked program discovery with positive and negative platform/surface filters and compact output. Strict-filter retries reuse cached upstream pages, so relax one local filter at a time when a narrow query returns no candidates.",
|
|
824
946
|
inputSchema: {
|
|
825
947
|
platforms: platformListSchema(),
|
|
948
|
+
exclude_platforms: stringListSchema("Platforms to exclude after retrieval. Use list_filters first when the user asks for a geographic/platform exclusion without naming exact platform values."),
|
|
826
949
|
tags: tagListSchema(),
|
|
827
950
|
scope_tags: scopeTagListSchema(),
|
|
828
951
|
target_types: targetTypeListSchema(),
|
|
829
952
|
language_tags: languageTagListSchema(),
|
|
830
953
|
web3: z.boolean().default(false).describe("Require Web3 signals."),
|
|
954
|
+
exclude_web3: z.boolean().default(false).describe("Exclude programs with Web3, blockchain, smart-contract, crypto, DeFi, NFT, Solidity, Ethereum, or Solana signals. Use this for Web2-only discovery."),
|
|
831
955
|
opportunity_levels: z.array(opportunityLevelSchema).max(4).default([]),
|
|
832
956
|
updated_since: isoDateTimeSchema.optional(),
|
|
833
957
|
min_bounty_min: z.number().int().min(0).optional(),
|
|
@@ -1211,7 +1335,7 @@ export function createBbradarServer(client, config) {
|
|
|
1211
1335
|
platforms: platformListSchema(),
|
|
1212
1336
|
tags: tagListSchema(),
|
|
1213
1337
|
language_tags: languageTagListSchema(),
|
|
1214
|
-
page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
|
|
1338
|
+
page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
1215
1339
|
max_programs: z.number().int().min(1).max(50).default(10),
|
|
1216
1340
|
sample_size: z.number().int().min(1).max(20).default(5),
|
|
1217
1341
|
target_list_mode: targetListModeSchema.default("identifiers")
|
|
@@ -1387,7 +1511,7 @@ export function createBbradarServer(client, config) {
|
|
|
1387
1511
|
annotations: readOnlyAnnotations
|
|
1388
1512
|
}, (args) => runTool("get_bbradar_guide", config, rateLimiter, async () => ({
|
|
1389
1513
|
request_id: randomUUID(),
|
|
1390
|
-
guide: bbradarGuide(args.topic, config.toolProfile)
|
|
1514
|
+
guide: bbradarGuide(args.topic, config.toolProfile, client)
|
|
1391
1515
|
})));
|
|
1392
1516
|
registerTool("get_mcp_status", {
|
|
1393
1517
|
title: "Get BBRadar MCP Status",
|
|
@@ -1417,6 +1541,13 @@ export function createBbradarServer(client, config) {
|
|
|
1417
1541
|
compact_tool_text: config.compactToolText,
|
|
1418
1542
|
tool_profile: config.toolProfile,
|
|
1419
1543
|
request_timeout_ms: config.requestTimeoutMs,
|
|
1544
|
+
feed_cursors: feedCursorStore.stats(),
|
|
1545
|
+
continuations: {
|
|
1546
|
+
entries: continuationStore.size,
|
|
1547
|
+
max_entries: MAX_LOCAL_EXPORT_RESOURCES,
|
|
1548
|
+
inline_limit: config.continuationInlineLimit ?? 50,
|
|
1549
|
+
ttl_ms: config.continuationTtlMs ?? LOCAL_EXPORT_TTL_MS
|
|
1550
|
+
},
|
|
1420
1551
|
validate_on_startup: config.validateOnStartup,
|
|
1421
1552
|
transport: "stdio",
|
|
1422
1553
|
remote_streamable_http: "not_enabled_for_local_setup",
|
|
@@ -1626,7 +1757,7 @@ export function createBbradarServer(client, config) {
|
|
|
1626
1757
|
include_removed: z.boolean().default(true),
|
|
1627
1758
|
include_ineligible: z.boolean().default(false),
|
|
1628
1759
|
include_out_of_scope: z.boolean().default(false),
|
|
1629
|
-
page_size: recentChangesPageSizeSchema.default(50)
|
|
1760
|
+
page_size: recentChangesPageSizeSchema.default(Math.min(50, maxTargetChangesPageSize))
|
|
1630
1761
|
},
|
|
1631
1762
|
outputSchema: {
|
|
1632
1763
|
...apiEnvelopeOutputShape(),
|
|
@@ -1638,16 +1769,16 @@ export function createBbradarServer(client, config) {
|
|
|
1638
1769
|
}, (args) => runTool("get_program_delta", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramDelta(client, config, { ...args, program_id: programId }))));
|
|
1639
1770
|
registerTool("export_targets", {
|
|
1640
1771
|
title: "Export BBRadar Targets",
|
|
1641
|
-
description: `Read-only target export. limit max ${
|
|
1772
|
+
description: `Read-only target export. limit max ${maxTargetExport}.`,
|
|
1642
1773
|
inputSchema: {
|
|
1643
|
-
program_ids: z.array(programIdentifierSchema).max(
|
|
1774
|
+
program_ids: z.array(programIdentifierSchema).max(maxTargetExportProgramIds).default([]),
|
|
1644
1775
|
platforms: platformListSchema(),
|
|
1645
1776
|
tags: tagListSchema(),
|
|
1646
1777
|
opportunity_levels: z.array(z.enum(["elite", "hot", "strong", "potential"])).max(4).default([]),
|
|
1647
1778
|
include_out_of_scope: z.boolean().default(false),
|
|
1648
1779
|
include_ineligible: z.boolean().default(false),
|
|
1649
1780
|
format: z.enum(["json", "csv"]).default("json"),
|
|
1650
|
-
limit: z.number().int().min(1).max(
|
|
1781
|
+
limit: z.number().int().min(1).max(maxTargetExport).default(maxTargetExport)
|
|
1651
1782
|
},
|
|
1652
1783
|
outputSchema: {
|
|
1653
1784
|
...apiEnvelopeOutputShape(),
|
|
@@ -1694,12 +1825,12 @@ export function createBbradarServer(client, config) {
|
|
|
1694
1825
|
}
|
|
1695
1826
|
}));
|
|
1696
1827
|
registerPrompts(server, config.toolProfile);
|
|
1697
|
-
registerResources(server, client, config, exportStore);
|
|
1828
|
+
registerResources(server, client, config, exportStore, continuationStore);
|
|
1698
1829
|
return server;
|
|
1699
1830
|
}
|
|
1700
|
-
function bbradarGuide(topic, profile) {
|
|
1831
|
+
function bbradarGuide(topic, profile, client) {
|
|
1701
1832
|
if (profile === "core") {
|
|
1702
|
-
return coreBbradarGuide(topic);
|
|
1833
|
+
return coreBbradarGuide(topic, client);
|
|
1703
1834
|
}
|
|
1704
1835
|
const primaryEntryPoints = [
|
|
1705
1836
|
{
|
|
@@ -1732,6 +1863,11 @@ function bbradarGuide(topic, profile) {
|
|
|
1732
1863
|
first_tool: "find_program_candidates",
|
|
1733
1864
|
then: "Use get_program_brief on the best program_id if the user wants details."
|
|
1734
1865
|
},
|
|
1866
|
+
{
|
|
1867
|
+
user_goal: "Web2 programs with broad scope, good rewards, recent target activity, and excluded platforms",
|
|
1868
|
+
first_tool: "list_filters",
|
|
1869
|
+
then: "Resolve the excluded platform names, then call find_programs with exclude_platforms, exclude_web3=true, exclude_non_web=true, reward/scope/freshness thresholds, and sort_by=most_new_targets. If strict local filters return zero, relax one threshold and retry; cached source pages make the retry cheap."
|
|
1870
|
+
},
|
|
1735
1871
|
{
|
|
1736
1872
|
user_goal: "named program summary",
|
|
1737
1873
|
first_tool: "run_program_name_action",
|
|
@@ -1752,6 +1888,22 @@ function bbradarGuide(topic, profile) {
|
|
|
1752
1888
|
first_tool: "find_recently_updated_programs",
|
|
1753
1889
|
then: "Use get_latest_added_targets for global added targets, or check_program_new_targets when the user names one program."
|
|
1754
1890
|
},
|
|
1891
|
+
{
|
|
1892
|
+
user_goal: "repository changes or repository polling",
|
|
1893
|
+
first_tool: "get_repository_changes",
|
|
1894
|
+
arguments: { include_targets: false, max_cursor_pages: 2 },
|
|
1895
|
+
then: "For incremental polling, seed once with after_id and later set resume_cursor=true with the same filters."
|
|
1896
|
+
},
|
|
1897
|
+
{
|
|
1898
|
+
user_goal: "rewarded Web3 targets with recent repository changes on named platforms",
|
|
1899
|
+
first_tool: "get_repository_changes",
|
|
1900
|
+
arguments: { platforms: ["requested platforms"], include_targets: true, output_mode: "full", page_size: 100 },
|
|
1901
|
+
parallel_with: {
|
|
1902
|
+
tool: "find_programs",
|
|
1903
|
+
arguments: { platforms: ["requested platforms"], web3: true, require_bounty: true, min_bounty_max: 5000, sort_by: "highest_reward", include_target_samples: false }
|
|
1904
|
+
},
|
|
1905
|
+
then: "Intersect repository-event program ids with ranked program ids. Targets belong to the repository event association even when target rows omit program_id; keep strong target scores/labels and only the requested program platforms."
|
|
1906
|
+
},
|
|
1755
1907
|
{
|
|
1756
1908
|
user_goal: "latest added wildcard targets with platform, reward, or report-count filters",
|
|
1757
1909
|
first_tool: "get_latest_added_targets",
|
|
@@ -1797,10 +1949,12 @@ function bbradarGuide(topic, profile) {
|
|
|
1797
1949
|
};
|
|
1798
1950
|
const filters = {
|
|
1799
1951
|
platforms: "Only set when the user explicitly names a platform. Do not infer Immunefi, HackerOne, etc.",
|
|
1952
|
+
exclude_platforms: "Use exact observed values from list_filters for user-requested platform or geographic exclusions.",
|
|
1800
1953
|
tags: "Broad combined tag filter across scope, language, and target-type signals.",
|
|
1801
1954
|
scope_tags: "Scope category signals such as domain, api, mobile, smart-contract, wildcard.",
|
|
1802
1955
|
target_types: "Target-kind signals such as domain, api, url, mobile, smart-contract.",
|
|
1803
1956
|
language_tags: "Technology/language signals such as javascript, solidity, web3, php, python.",
|
|
1957
|
+
exclude_web3: "Set true for Web2-only requests.",
|
|
1804
1958
|
vdp_mode: "Default exclude. Include VDP/no-bounty data only when asked."
|
|
1805
1959
|
};
|
|
1806
1960
|
const examples = [
|
|
@@ -1851,19 +2005,25 @@ function bbradarGuide(topic, profile) {
|
|
|
1851
2005
|
require_bounty: true
|
|
1852
2006
|
},
|
|
1853
2007
|
primary_entry_points: topic === "start" ? primaryEntryPoints : primaryEntryPoints.filter((entry) => entry.tool !== "get_bbradar_guide"),
|
|
1854
|
-
decision_tree: decisionTree.filter((entry) =>
|
|
2008
|
+
decision_tree: decisionTree.filter((entry) => toolAvailableForContract(client, String(entry.first_tool)) &&
|
|
2009
|
+
(topic === "start" || guideEntryMatchesTopic(topic, entry))),
|
|
1855
2010
|
filters,
|
|
1856
2011
|
advanced_tools: advancedTools,
|
|
1857
2012
|
examples
|
|
1858
2013
|
};
|
|
1859
2014
|
}
|
|
1860
|
-
function coreBbradarGuide(topic) {
|
|
2015
|
+
function coreBbradarGuide(topic, client) {
|
|
1861
2016
|
const decisionTree = [
|
|
1862
2017
|
{
|
|
1863
2018
|
user_goal: "find programs to hunt",
|
|
1864
2019
|
first_tool: "find_programs",
|
|
1865
2020
|
arguments: { max_results: 10, output_mode: "compact" }
|
|
1866
2021
|
},
|
|
2022
|
+
{
|
|
2023
|
+
user_goal: "Web2 programs with broad scope, good rewards, recent target activity, and excluded platforms",
|
|
2024
|
+
first_tool: "list_filters",
|
|
2025
|
+
then: "Resolve excluded platform names, then call find_programs with exclude_platforms, exclude_web3=true, exclude_non_web=true, reward/scope/freshness thresholds, and sort_by=most_new_targets. Relax one local threshold on an empty result; cached source pages make retries fast."
|
|
2026
|
+
},
|
|
1867
2027
|
{
|
|
1868
2028
|
user_goal: "named program summary or action",
|
|
1869
2029
|
first_tool: "run_program_name_action",
|
|
@@ -1884,6 +2044,21 @@ function coreBbradarGuide(topic) {
|
|
|
1884
2044
|
first_tool: "get_recent_changes",
|
|
1885
2045
|
then: "Use get_latest_added_targets when only newly added targets are needed."
|
|
1886
2046
|
},
|
|
2047
|
+
{
|
|
2048
|
+
user_goal: "repository changes or repository polling",
|
|
2049
|
+
first_tool: "get_repository_changes",
|
|
2050
|
+
arguments: { include_targets: false, max_cursor_pages: 2 }
|
|
2051
|
+
},
|
|
2052
|
+
{
|
|
2053
|
+
user_goal: "rewarded Web3 targets with recent repository changes on named platforms",
|
|
2054
|
+
first_tool: "get_repository_changes",
|
|
2055
|
+
arguments: { platforms: ["requested platforms"], include_targets: true, output_mode: "full", page_size: 100 },
|
|
2056
|
+
parallel_with: {
|
|
2057
|
+
tool: "find_programs",
|
|
2058
|
+
arguments: { platforms: ["requested platforms"], web3: true, require_bounty: true, min_bounty_max: 5000, sort_by: "highest_reward", include_target_samples: false }
|
|
2059
|
+
},
|
|
2060
|
+
then: "Intersect event program ids with ranked program ids. Treat targets as event-level associations when target rows omit program_id, keep strong target scores/labels, and discard non-requested associated platforms."
|
|
2061
|
+
},
|
|
1887
2062
|
{
|
|
1888
2063
|
user_goal: "export targets",
|
|
1889
2064
|
first_tool: "export_targets",
|
|
@@ -1914,10 +2089,15 @@ function coreBbradarGuide(topic) {
|
|
|
1914
2089
|
"find_programs_by_target",
|
|
1915
2090
|
"get_bbradar_guide"
|
|
1916
2091
|
],
|
|
1917
|
-
decision_tree: decisionTree.filter((entry) =>
|
|
1918
|
-
|
|
2092
|
+
decision_tree: decisionTree.filter((entry) => toolAvailableForContract(client, String(entry.first_tool)) &&
|
|
2093
|
+
(topic === "start" || guideEntryMatchesTopic(topic, entry))),
|
|
2094
|
+
available_tools: [...CORE_TOOL_NAMES].filter((name) => toolAvailableForContract(client, name))
|
|
1919
2095
|
};
|
|
1920
2096
|
}
|
|
2097
|
+
function toolAvailableForContract(client, toolName) {
|
|
2098
|
+
const endpoint = TOOL_ENDPOINT_REQUIREMENTS[toolName];
|
|
2099
|
+
return endpoint === undefined || client.supportsEndpoint(endpoint);
|
|
2100
|
+
}
|
|
1921
2101
|
function guideEntryMatchesTopic(topic, entry) {
|
|
1922
2102
|
const goal = typeof entry.user_goal === "string" ? entry.user_goal : "";
|
|
1923
2103
|
const firstTool = typeof entry.first_tool === "string" ? entry.first_tool : "";
|
|
@@ -1944,15 +2124,52 @@ function guideEntryMatchesTopic(topic, entry) {
|
|
|
1944
2124
|
}
|
|
1945
2125
|
return true;
|
|
1946
2126
|
}
|
|
1947
|
-
function registerResources(server, client, config, exportStore) {
|
|
2127
|
+
function registerResources(server, client, config, exportStore, continuationStore) {
|
|
2128
|
+
server.registerResource("continuation", new ResourceTemplate("bbradar://continuations{?continuation_id,offset,limit}", { list: undefined }), {
|
|
2129
|
+
title: "BBRadar Result Continuation",
|
|
2130
|
+
description: "Bounded, short-lived in-memory continuation page for a large tool result.",
|
|
2131
|
+
mimeType: "application/json"
|
|
2132
|
+
}, async (uri) => {
|
|
2133
|
+
const continuationId = uri.searchParams.get("continuation_id") ?? "";
|
|
2134
|
+
pruneExpiredContinuations(continuationStore);
|
|
2135
|
+
const stored = continuationStore.get(continuationId);
|
|
2136
|
+
if (!stored) {
|
|
2137
|
+
return jsonResource(uri.toString(), {
|
|
2138
|
+
request_id: randomUUID(),
|
|
2139
|
+
error: { message: "Continuation not found or expired from local memory." }
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
const offset = boundedResourceInteger(uri.searchParams.get("offset"), 0, 0, stored.items.length);
|
|
2143
|
+
const limit = boundedResourceInteger(uri.searchParams.get("limit"), config.continuationInlineLimit ?? 50, 1, 100);
|
|
2144
|
+
const items = stored.items.slice(offset, offset + limit);
|
|
2145
|
+
const nextOffset = offset + items.length;
|
|
2146
|
+
const hasMore = nextOffset < stored.items.length;
|
|
2147
|
+
return jsonResource(uri.toString(), {
|
|
2148
|
+
request_id: randomUUID(),
|
|
2149
|
+
continuation_id: continuationId,
|
|
2150
|
+
label: stored.label,
|
|
2151
|
+
items,
|
|
2152
|
+
meta: {
|
|
2153
|
+
offset,
|
|
2154
|
+
limit,
|
|
2155
|
+
returned: items.length,
|
|
2156
|
+
total: stored.items.length,
|
|
2157
|
+
has_more: hasMore,
|
|
2158
|
+
next_resource_uri: hasMore
|
|
2159
|
+
? continuationResourceUri(continuationId, nextOffset, limit)
|
|
2160
|
+
: undefined
|
|
2161
|
+
}
|
|
2162
|
+
});
|
|
2163
|
+
});
|
|
1948
2164
|
server.registerResource("target-export", new ResourceTemplate("bbradar://exports{?export_id}", { list: undefined }), {
|
|
1949
2165
|
title: "BBRadar Target Export",
|
|
1950
|
-
description: "
|
|
2166
|
+
description: "Bounded, short-lived in-memory target export payload by export_id query parameter.",
|
|
1951
2167
|
mimeType: "application/json"
|
|
1952
2168
|
}, async (uri) => {
|
|
1953
2169
|
const exportId = uri.searchParams.get("export_id") ?? "";
|
|
1954
|
-
|
|
1955
|
-
|
|
2170
|
+
pruneExpiredExports(exportStore);
|
|
2171
|
+
const storedExport = exportStore.get(exportId);
|
|
2172
|
+
if (!storedExport) {
|
|
1956
2173
|
return jsonResource(uri.toString(), {
|
|
1957
2174
|
request_id: randomUUID(),
|
|
1958
2175
|
error: {
|
|
@@ -1963,7 +2180,7 @@ function registerResources(server, client, config, exportStore) {
|
|
|
1963
2180
|
return jsonResource(uri.toString(), {
|
|
1964
2181
|
request_id: randomUUID(),
|
|
1965
2182
|
export_id: exportId,
|
|
1966
|
-
export: payload
|
|
2183
|
+
export: storedExport.payload
|
|
1967
2184
|
});
|
|
1968
2185
|
});
|
|
1969
2186
|
server.registerResource("program", new ResourceTemplate("bbradar://program{?program_id}", { list: undefined }), {
|
|
@@ -3498,6 +3715,10 @@ function programsByTargetResult(input, warnings, sourceRequests, matches, budget
|
|
|
3498
3715
|
});
|
|
3499
3716
|
}
|
|
3500
3717
|
async function searchProgramsByTargetApi(client, config, input, budget, warnings, sourceRequests) {
|
|
3718
|
+
if (!client.supportsEndpoint("target_search")) {
|
|
3719
|
+
warnings.push("The negotiated API contract does not expose direct target search; using sampled program target lists.");
|
|
3720
|
+
return undefined;
|
|
3721
|
+
}
|
|
3501
3722
|
const searchQueries = targetApiSearchQueries(input.target_query);
|
|
3502
3723
|
const grouped = new Map();
|
|
3503
3724
|
let rawMatchesScanned = 0;
|
|
@@ -3519,7 +3740,7 @@ async function searchProgramsByTargetApi(client, config, input, budget, warnings
|
|
|
3519
3740
|
include_out_of_scope: input.include_out_of_scope,
|
|
3520
3741
|
include_ineligible: input.include_ineligible,
|
|
3521
3742
|
page: 1,
|
|
3522
|
-
page_size: Math.max(input.max_results * input.max_targets_per_program, input.max_results)
|
|
3743
|
+
page_size: Math.min(client.endpointNumber("target_search", "max_page_size", MAX_TARGETS_PER_PROGRAM), Math.max(input.max_results * input.max_targets_per_program, input.max_results))
|
|
3523
3744
|
});
|
|
3524
3745
|
refundBudgetIfNoUpstreamRequest(budget, api);
|
|
3525
3746
|
}
|
|
@@ -4096,8 +4317,18 @@ function latestAddedProgramMatches(change, input, platformFilters) {
|
|
|
4096
4317
|
return true;
|
|
4097
4318
|
}
|
|
4098
4319
|
async function getProgramTargetsPayload(client, input, programId) {
|
|
4099
|
-
const api = await client.getProgramTargets(programId
|
|
4320
|
+
const api = await client.getProgramTargets(programId, {
|
|
4321
|
+
search: input.search,
|
|
4322
|
+
target_type: input.target_type,
|
|
4323
|
+
eligibility: input.include_ineligible ? "all" : "eligible",
|
|
4324
|
+
minTargetScore: input.min_target_score,
|
|
4325
|
+
targetOpportunityLabel: input.target_opportunity_label,
|
|
4326
|
+
competitionRisk: input.competition_risk,
|
|
4327
|
+
page: 1,
|
|
4328
|
+
page_size: client.endpointNumber("program_targets", "max_page_size", MAX_TARGETS_PER_PROGRAM)
|
|
4329
|
+
});
|
|
4100
4330
|
const data = readObject(api.data);
|
|
4331
|
+
const upstreamMeta = readObject(data?.meta);
|
|
4101
4332
|
const rawTargets = readArray(data?.targets);
|
|
4102
4333
|
const sanitizedTargets = rawTargets.map(sanitizeTarget).filter((target) => targetHasAllowedScope(target, input));
|
|
4103
4334
|
const targets = sanitizedTargets.slice(input.offset, input.offset + input.limit);
|
|
@@ -4108,10 +4339,13 @@ async function getProgramTargetsPayload(client, input, programId) {
|
|
|
4108
4339
|
offset: input.offset,
|
|
4109
4340
|
limit: input.limit,
|
|
4110
4341
|
output_mode: input.output_mode,
|
|
4111
|
-
total_active_targets: rawTargets.length,
|
|
4342
|
+
total_active_targets: readNumber(upstreamMeta?.total) ?? rawTargets.length,
|
|
4112
4343
|
total_after_filters: sanitizedTargets.length,
|
|
4113
4344
|
returned: targets.length,
|
|
4114
|
-
has_more: input.offset + input.limit < sanitizedTargets.length,
|
|
4345
|
+
has_more: input.offset + input.limit < sanitizedTargets.length || readBoolean(upstreamMeta?.has_more) === true,
|
|
4346
|
+
upstream_has_more: readBoolean(upstreamMeta?.has_more),
|
|
4347
|
+
possibly_incomplete: readBoolean(upstreamMeta?.has_more) === true,
|
|
4348
|
+
data_capabilities: readObject(upstreamMeta?.data_capabilities),
|
|
4115
4349
|
target_source: "program_targets"
|
|
4116
4350
|
}
|
|
4117
4351
|
});
|
|
@@ -5196,6 +5430,79 @@ function programTargetsResourceUri(programId) {
|
|
|
5196
5430
|
function exportResourceUri(exportId) {
|
|
5197
5431
|
return `bbradar://exports?export_id=${encodeURIComponent(exportId)}`;
|
|
5198
5432
|
}
|
|
5433
|
+
function continuationResourceUri(continuationId, offset, limit) {
|
|
5434
|
+
return `bbradar://continuations?continuation_id=${encodeURIComponent(continuationId)}&offset=${offset}&limit=${limit}`;
|
|
5435
|
+
}
|
|
5436
|
+
function applyCollectionContinuation(payload, field, label, store, config) {
|
|
5437
|
+
const items = Array.isArray(payload[field]) ? payload[field] : undefined;
|
|
5438
|
+
const inlineLimit = Math.min(100, Math.max(1, config.continuationInlineLimit ?? 50));
|
|
5439
|
+
if (!items || items.length <= inlineLimit) {
|
|
5440
|
+
return payload;
|
|
5441
|
+
}
|
|
5442
|
+
const continuationId = randomUUID();
|
|
5443
|
+
const expiresAt = Date.now() + (config.continuationTtlMs ?? LOCAL_EXPORT_TTL_MS);
|
|
5444
|
+
const stored = rememberContinuation(store, continuationId, label, items, expiresAt);
|
|
5445
|
+
if (!stored) {
|
|
5446
|
+
return {
|
|
5447
|
+
...payload,
|
|
5448
|
+
[field]: items.slice(0, inlineLimit),
|
|
5449
|
+
warnings: uniqueStrings([
|
|
5450
|
+
...readArray(payload.warnings).filter((entry) => typeof entry === "string"),
|
|
5451
|
+
"The complete result exceeded the local continuation-store byte limit; only the inline prefix is available."
|
|
5452
|
+
])
|
|
5453
|
+
};
|
|
5454
|
+
}
|
|
5455
|
+
return {
|
|
5456
|
+
...payload,
|
|
5457
|
+
[field]: items.slice(0, inlineLimit),
|
|
5458
|
+
continuation: {
|
|
5459
|
+
continuation_id: continuationId,
|
|
5460
|
+
resource_uri: continuationResourceUri(continuationId, inlineLimit, inlineLimit),
|
|
5461
|
+
collection: field,
|
|
5462
|
+
total: items.length,
|
|
5463
|
+
inline_returned: inlineLimit,
|
|
5464
|
+
remaining: items.length - inlineLimit,
|
|
5465
|
+
expires_at: new Date(expiresAt).toISOString()
|
|
5466
|
+
}
|
|
5467
|
+
};
|
|
5468
|
+
}
|
|
5469
|
+
function rememberContinuation(store, id, label, items, expiresAt) {
|
|
5470
|
+
const byteLength = Buffer.byteLength(JSON.stringify(items), "utf8");
|
|
5471
|
+
pruneExpiredContinuations(store);
|
|
5472
|
+
if (byteLength > MAX_LOCAL_EXPORT_BYTES) {
|
|
5473
|
+
return false;
|
|
5474
|
+
}
|
|
5475
|
+
while (store.size >= MAX_LOCAL_EXPORT_RESOURCES ||
|
|
5476
|
+
retainedContinuationBytes(store) + byteLength > MAX_LOCAL_EXPORT_BYTES) {
|
|
5477
|
+
const oldest = store.keys().next().value;
|
|
5478
|
+
if (oldest === undefined) {
|
|
5479
|
+
break;
|
|
5480
|
+
}
|
|
5481
|
+
store.delete(oldest);
|
|
5482
|
+
}
|
|
5483
|
+
store.set(id, { items, label, byteLength, expiresAt });
|
|
5484
|
+
return true;
|
|
5485
|
+
}
|
|
5486
|
+
function pruneExpiredContinuations(store, now = Date.now()) {
|
|
5487
|
+
for (const [id, value] of store) {
|
|
5488
|
+
if (value.expiresAt <= now) {
|
|
5489
|
+
store.delete(id);
|
|
5490
|
+
}
|
|
5491
|
+
}
|
|
5492
|
+
}
|
|
5493
|
+
function retainedContinuationBytes(store) {
|
|
5494
|
+
let total = 0;
|
|
5495
|
+
for (const value of store.values()) {
|
|
5496
|
+
total += value.byteLength;
|
|
5497
|
+
}
|
|
5498
|
+
return total;
|
|
5499
|
+
}
|
|
5500
|
+
function boundedResourceInteger(raw, fallback, minimum, maximum) {
|
|
5501
|
+
const parsed = raw === null ? fallback : Number.parseInt(raw, 10);
|
|
5502
|
+
return Number.isFinite(parsed)
|
|
5503
|
+
? Math.min(maximum, Math.max(minimum, parsed))
|
|
5504
|
+
: fallback;
|
|
5505
|
+
}
|
|
5199
5506
|
function jsonResource(uri, payload) {
|
|
5200
5507
|
return {
|
|
5201
5508
|
contents: [
|
|
@@ -5208,41 +5515,86 @@ function jsonResource(uri, payload) {
|
|
|
5208
5515
|
};
|
|
5209
5516
|
}
|
|
5210
5517
|
function rememberExport(exportStore, exportId, payload) {
|
|
5518
|
+
const now = Date.now();
|
|
5519
|
+
const byteLength = Buffer.byteLength(JSON.stringify(payload), "utf8");
|
|
5520
|
+
pruneExpiredExports(exportStore, now);
|
|
5211
5521
|
if (exportStore.has(exportId)) {
|
|
5212
5522
|
exportStore.delete(exportId);
|
|
5213
5523
|
}
|
|
5214
|
-
while (exportStore.size >= MAX_LOCAL_EXPORT_RESOURCES
|
|
5524
|
+
while (exportStore.size >= MAX_LOCAL_EXPORT_RESOURCES ||
|
|
5525
|
+
retainedExportBytes(exportStore) + byteLength > MAX_LOCAL_EXPORT_BYTES) {
|
|
5215
5526
|
const oldestKey = exportStore.keys().next().value;
|
|
5216
5527
|
if (oldestKey === undefined) {
|
|
5217
5528
|
break;
|
|
5218
5529
|
}
|
|
5219
5530
|
exportStore.delete(oldestKey);
|
|
5220
5531
|
}
|
|
5221
|
-
exportStore.set(exportId,
|
|
5532
|
+
exportStore.set(exportId, {
|
|
5533
|
+
payload,
|
|
5534
|
+
byteLength,
|
|
5535
|
+
expiresAt: now + LOCAL_EXPORT_TTL_MS
|
|
5536
|
+
});
|
|
5537
|
+
}
|
|
5538
|
+
function pruneExpiredExports(exportStore, now = Date.now()) {
|
|
5539
|
+
for (const [exportId, storedExport] of exportStore) {
|
|
5540
|
+
if (storedExport.expiresAt <= now) {
|
|
5541
|
+
exportStore.delete(exportId);
|
|
5542
|
+
}
|
|
5543
|
+
}
|
|
5544
|
+
}
|
|
5545
|
+
function retainedExportBytes(exportStore) {
|
|
5546
|
+
let total = 0;
|
|
5547
|
+
for (const storedExport of exportStore.values()) {
|
|
5548
|
+
total += storedExport.byteLength;
|
|
5549
|
+
}
|
|
5550
|
+
return total;
|
|
5222
5551
|
}
|
|
5223
5552
|
function previewExportPayload(payload) {
|
|
5553
|
+
if (typeof payload === "string") {
|
|
5554
|
+
return previewExportText(payload);
|
|
5555
|
+
}
|
|
5224
5556
|
if (Array.isArray(payload)) {
|
|
5225
5557
|
return payload.slice(0, EXPORT_PREVIEW_LIMIT);
|
|
5226
5558
|
}
|
|
5227
|
-
|
|
5559
|
+
const object = readObject(payload);
|
|
5560
|
+
if (object && typeof object.content === "string") {
|
|
5228
5561
|
return {
|
|
5229
|
-
...
|
|
5230
|
-
|
|
5562
|
+
...object,
|
|
5563
|
+
content: previewExportText(object.content)
|
|
5564
|
+
};
|
|
5565
|
+
}
|
|
5566
|
+
if (object && Array.isArray(object.targets)) {
|
|
5567
|
+
return {
|
|
5568
|
+
...object,
|
|
5569
|
+
targets: object.targets.slice(0, EXPORT_PREVIEW_LIMIT)
|
|
5231
5570
|
};
|
|
5232
5571
|
}
|
|
5233
5572
|
return payload;
|
|
5234
5573
|
}
|
|
5235
5574
|
function previewExportCount(payload) {
|
|
5575
|
+
if (typeof payload === "string") {
|
|
5576
|
+
return Math.min(Math.max(0, payload.split(/\r?\n/).length - 1), EXPORT_PREVIEW_LIMIT);
|
|
5577
|
+
}
|
|
5236
5578
|
if (Array.isArray(payload)) {
|
|
5237
5579
|
return Math.min(payload.length, EXPORT_PREVIEW_LIMIT);
|
|
5238
5580
|
}
|
|
5239
5581
|
const object = readObject(payload);
|
|
5582
|
+
if (typeof object?.content === "string") {
|
|
5583
|
+
return Math.min(Math.max(0, object.content.split(/\r?\n/).length - 1), EXPORT_PREVIEW_LIMIT);
|
|
5584
|
+
}
|
|
5240
5585
|
const targets = object?.targets;
|
|
5241
5586
|
if (Array.isArray(targets)) {
|
|
5242
5587
|
return Math.min(targets.length, EXPORT_PREVIEW_LIMIT);
|
|
5243
5588
|
}
|
|
5244
5589
|
return payload === undefined || payload === null ? 0 : 1;
|
|
5245
5590
|
}
|
|
5591
|
+
function previewExportText(payload) {
|
|
5592
|
+
const lines = payload.split(/\r?\n/);
|
|
5593
|
+
const preview = lines.slice(0, EXPORT_PREVIEW_LIMIT + 1).join("\n");
|
|
5594
|
+
return lines.length > EXPORT_PREVIEW_LIMIT + 1
|
|
5595
|
+
? `${preview}\n[preview truncated; read the export resource for the complete payload]`
|
|
5596
|
+
: preview;
|
|
5597
|
+
}
|
|
5246
5598
|
function collectResourceLinks(payload) {
|
|
5247
5599
|
const links = new Map();
|
|
5248
5600
|
for (const program of extractProgramsFromPayload(payload)) {
|
|
@@ -5284,6 +5636,19 @@ function collectResourceLinks(payload) {
|
|
|
5284
5636
|
mimeType: "application/json"
|
|
5285
5637
|
});
|
|
5286
5638
|
}
|
|
5639
|
+
const continuation = readObject(payload.continuation);
|
|
5640
|
+
const continuationUri = stringField(continuation, "resource_uri");
|
|
5641
|
+
const continuationId = stringField(continuation, "continuation_id");
|
|
5642
|
+
if (continuationUri && continuationId) {
|
|
5643
|
+
links.set(continuationUri, {
|
|
5644
|
+
type: "resource_link",
|
|
5645
|
+
uri: continuationUri,
|
|
5646
|
+
name: `${continuationId} continuation`,
|
|
5647
|
+
title: "Result continuation",
|
|
5648
|
+
description: "Read the next bounded page of this local in-memory result.",
|
|
5649
|
+
mimeType: "application/json"
|
|
5650
|
+
});
|
|
5651
|
+
}
|
|
5287
5652
|
return [...links.values()];
|
|
5288
5653
|
}
|
|
5289
5654
|
function extractProgramsFromPayload(payload) {
|
|
@@ -5333,9 +5698,18 @@ function compactProgram(program) {
|
|
|
5333
5698
|
handle: stringField(program, "handle"),
|
|
5334
5699
|
name: stringField(program, "name"),
|
|
5335
5700
|
platform: stringField(program, "platform"),
|
|
5701
|
+
catalog: stringField(program, "catalog"),
|
|
5702
|
+
program_type: stringField(program, "program_type"),
|
|
5336
5703
|
platform_url: stringField(program, "platform_url"),
|
|
5337
5704
|
bbradar_url: stringField(program, "bbradar_url"),
|
|
5705
|
+
policy_url: stringField(program, "policy_url"),
|
|
5706
|
+
reporting_url: stringField(program, "reporting_url"),
|
|
5707
|
+
reward_status: stringField(program, "reward_status"),
|
|
5708
|
+
data_capabilities: readObject(program.data_capabilities),
|
|
5338
5709
|
opportunity_tag: readObject(program.opportunity_tag),
|
|
5710
|
+
competition_risk: stringField(program, "competition_risk"),
|
|
5711
|
+
competition_risk_reason: stringField(program, "competition_risk_reason"),
|
|
5712
|
+
submission_cost: readObject(program.submission_cost),
|
|
5339
5713
|
reward_range: readObject(program.reward_range),
|
|
5340
5714
|
public_report_count: readNumber(program.public_report_count),
|
|
5341
5715
|
public_report_count_label: stringField(program, "public_report_count_label"),
|
|
@@ -5385,6 +5759,30 @@ function formatChange(change, mode) {
|
|
|
5385
5759
|
target: target ? compactTarget(target) : undefined
|
|
5386
5760
|
});
|
|
5387
5761
|
}
|
|
5762
|
+
function formatRepositoryChange(change, mode) {
|
|
5763
|
+
if (mode === "full") {
|
|
5764
|
+
return change;
|
|
5765
|
+
}
|
|
5766
|
+
const repository = readObject(change.repository);
|
|
5767
|
+
return stripUndefined({
|
|
5768
|
+
id: change.id,
|
|
5769
|
+
event_type: stringField(change, "event_type"),
|
|
5770
|
+
ref_name: stringField(change, "ref_name"),
|
|
5771
|
+
observed_at: stringField(change, "observed_at"),
|
|
5772
|
+
repository: repository
|
|
5773
|
+
? stripUndefined({
|
|
5774
|
+
provider: stringField(repository, "provider"),
|
|
5775
|
+
full_name: stringField(repository, "full_name"),
|
|
5776
|
+
url: stringField(repository, "url")
|
|
5777
|
+
})
|
|
5778
|
+
: undefined,
|
|
5779
|
+
changes_url: stringField(change, "changes_url"),
|
|
5780
|
+
programs: readArray(change.programs).map((program) => formatProgram(readObject(program) ?? {}, "compact")),
|
|
5781
|
+
targets: Array.isArray(change.targets)
|
|
5782
|
+
? change.targets.map((target) => compactTarget(readObject(target) ?? {}))
|
|
5783
|
+
: undefined
|
|
5784
|
+
});
|
|
5785
|
+
}
|
|
5388
5786
|
function buildTargetScopeSummary(targets, mode, groupLimit, includeOutOfScope, includeIneligible) {
|
|
5389
5787
|
const bountyTargets = targets.filter((target) => target.in_scope === true && target.eligible_for_bounty === true);
|
|
5390
5788
|
const groups = {
|
|
@@ -5636,6 +6034,14 @@ function candidateLooksContestLike(candidate) {
|
|
|
5636
6034
|
return tagIncludesAnyNormalized(candidate.normalizedSearchSignals, WEB3_CONTEST_SIGNALS);
|
|
5637
6035
|
}
|
|
5638
6036
|
function candidateHasKnownNoBounty(candidate) {
|
|
6037
|
+
const programType = normalizeTag(stringField(candidate.program, "program_type") ?? "");
|
|
6038
|
+
const rewardStatus = normalizeTag(stringField(candidate.program, "reward_status") ?? "");
|
|
6039
|
+
if (programType === "vdp" || rewardStatus === "no_reward" || rewardStatus === "no reward") {
|
|
6040
|
+
return true;
|
|
6041
|
+
}
|
|
6042
|
+
if (programType === "paid" || rewardStatus === "paid") {
|
|
6043
|
+
return false;
|
|
6044
|
+
}
|
|
5639
6045
|
if (candidate.bountyMin === undefined && candidate.bountyMax === undefined) {
|
|
5640
6046
|
return false;
|
|
5641
6047
|
}
|
|
@@ -5824,6 +6230,16 @@ function normalizeFindProgramsInput(input) {
|
|
|
5824
6230
|
const warnings = [];
|
|
5825
6231
|
const normalized = { ...input };
|
|
5826
6232
|
normalized.vdp_mode ??= "exclude";
|
|
6233
|
+
normalized.exclude_platforms = uniqueStrings(normalized.exclude_platforms ?? []);
|
|
6234
|
+
if (normalized.web3 && normalized.exclude_web3) {
|
|
6235
|
+
normalized.web3 = false;
|
|
6236
|
+
warnings.push("exclude_web3=true took precedence over the contradictory web3=true filter.");
|
|
6237
|
+
}
|
|
6238
|
+
const includedPlatforms = new Set(normalized.platforms.map(normalizeTag));
|
|
6239
|
+
const overlappingPlatforms = normalized.exclude_platforms.filter((platform) => includedPlatforms.has(normalizeTag(platform)));
|
|
6240
|
+
if (overlappingPlatforms.length > 0) {
|
|
6241
|
+
warnings.push(`Excluded platform filters take precedence over included values: ${overlappingPlatforms.join(", ")}.`);
|
|
6242
|
+
}
|
|
5827
6243
|
normalized.max_pages = clampLimit("max_pages", normalized.max_pages, MAX_FIND_PROGRAM_PAGES, warnings);
|
|
5828
6244
|
normalized.max_results = clampLimit("max_results", normalized.max_results, MAX_FIND_PROGRAM_RESULTS, warnings);
|
|
5829
6245
|
normalized.target_sample_programs = clampLimit("target_sample_programs", normalized.target_sample_programs, MAX_FIND_TARGET_SAMPLE_PROGRAMS, warnings);
|
|
@@ -5890,8 +6306,10 @@ async function findPrograms(client, config, input) {
|
|
|
5890
6306
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
5891
6307
|
filters: stripUndefined({
|
|
5892
6308
|
platforms: input.platforms.length > 0 ? input.platforms : undefined,
|
|
6309
|
+
exclude_platforms: input.exclude_platforms && input.exclude_platforms.length > 0 ? input.exclude_platforms : undefined,
|
|
5893
6310
|
tags: upstreamTags.length > 0 ? upstreamTags : undefined,
|
|
5894
6311
|
web3: input.web3 ? true : undefined,
|
|
6312
|
+
exclude_web3: input.exclude_web3 ? true : undefined,
|
|
5895
6313
|
opportunity_levels: input.opportunity_levels.length > 0 ? input.opportunity_levels : undefined,
|
|
5896
6314
|
updated_since: input.updated_since,
|
|
5897
6315
|
min_bounty_min: input.min_bounty_min,
|
|
@@ -6423,12 +6841,19 @@ function programCandidateMatches(candidate, filters) {
|
|
|
6423
6841
|
if (filters.platforms.length > 0 && !filters.platforms.map(normalizeTag).includes(normalizeTag(stringField(candidate.program, "platform") ?? ""))) {
|
|
6424
6842
|
return false;
|
|
6425
6843
|
}
|
|
6844
|
+
if ((filters.exclude_platforms?.length ?? 0) > 0 &&
|
|
6845
|
+
filters.exclude_platforms?.map(normalizeTag).includes(normalizeTag(stringField(candidate.program, "platform") ?? ""))) {
|
|
6846
|
+
return false;
|
|
6847
|
+
}
|
|
6426
6848
|
if (filters.tags.length > 0 && !candidateMatchesAnyTag(candidate, filters.tags)) {
|
|
6427
6849
|
return false;
|
|
6428
6850
|
}
|
|
6429
6851
|
if (filters.web3 && !candidateHasWeb3Surface(candidate)) {
|
|
6430
6852
|
return false;
|
|
6431
6853
|
}
|
|
6854
|
+
if (filters.exclude_web3 && candidateHasWeb3Surface(candidate)) {
|
|
6855
|
+
return false;
|
|
6856
|
+
}
|
|
6432
6857
|
if (filters.scope_tags.length > 0 && !candidateMatchesAnyTag(candidate, filters.scope_tags)) {
|
|
6433
6858
|
return false;
|
|
6434
6859
|
}
|
|
@@ -6441,7 +6866,9 @@ function programCandidateMatches(candidate, filters) {
|
|
|
6441
6866
|
if (candidate.wildcardCount < filters.min_wildcards) {
|
|
6442
6867
|
return false;
|
|
6443
6868
|
}
|
|
6444
|
-
|
|
6869
|
+
const explicitlyPaid = normalizeTag(stringField(candidate.program, "program_type") ?? "") === "paid" ||
|
|
6870
|
+
normalizeTag(stringField(candidate.program, "reward_status") ?? "") === "paid";
|
|
6871
|
+
if (filters.require_bounty && !explicitlyPaid && (candidate.bountyMax ?? candidate.bountyMin ?? 0) <= 0) {
|
|
6445
6872
|
return false;
|
|
6446
6873
|
}
|
|
6447
6874
|
if (filters.min_bounty_min !== undefined && (candidate.bountyMin ?? 0) < filters.min_bounty_min) {
|
|
@@ -6812,6 +7239,111 @@ The plan must:
|
|
|
6812
7239
|
- Prioritize targets by bounty eligibility, freshness, wildcard status, target type, and severity labels.
|
|
6813
7240
|
- Include a short checklist of BBRadar follow-up tool calls and decision points.`));
|
|
6814
7241
|
}
|
|
7242
|
+
function resolveFeedCursor(store, endpoint, filters, resume) {
|
|
7243
|
+
const normalizedFilters = stableStringify(normalizeFeedCursorFilters(filters));
|
|
7244
|
+
const key = `${endpoint}:${createHash("sha256").update(normalizedFilters).digest("hex")}`;
|
|
7245
|
+
const explicitAfterId = readNumber(filters.after_id);
|
|
7246
|
+
if (explicitAfterId !== undefined) {
|
|
7247
|
+
return { endpoint, key, afterId: explicitAfterId, resumed: false };
|
|
7248
|
+
}
|
|
7249
|
+
if (!resume) {
|
|
7250
|
+
return { endpoint, key, afterId: undefined, resumed: false };
|
|
7251
|
+
}
|
|
7252
|
+
const stored = store.get(key);
|
|
7253
|
+
if (stored === undefined) {
|
|
7254
|
+
throw new Error(`No in-memory ${endpoint} cursor exists for this filter set. Provide after_id once to seed it.`);
|
|
7255
|
+
}
|
|
7256
|
+
return { endpoint, key, afterId: stored, resumed: true };
|
|
7257
|
+
}
|
|
7258
|
+
function advanceFeedCursor(store, cursor, rawMeta) {
|
|
7259
|
+
if (cursor.afterId === undefined) {
|
|
7260
|
+
return undefined;
|
|
7261
|
+
}
|
|
7262
|
+
const meta = readObject(rawMeta);
|
|
7263
|
+
const nextAfterId = readNumber(meta?.next_after_id) ?? cursor.afterId;
|
|
7264
|
+
store.set(cursor.key, nextAfterId);
|
|
7265
|
+
return {
|
|
7266
|
+
endpoint: cursor.endpoint,
|
|
7267
|
+
mode: cursor.resumed ? "resumed" : "seeded",
|
|
7268
|
+
used_after_id: cursor.afterId,
|
|
7269
|
+
next_after_id: nextAfterId,
|
|
7270
|
+
has_more: readBoolean(meta?.has_more) ?? false,
|
|
7271
|
+
stored_in_memory: true
|
|
7272
|
+
};
|
|
7273
|
+
}
|
|
7274
|
+
async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudgetMs, fetchPage) {
|
|
7275
|
+
const startedAt = Date.now();
|
|
7276
|
+
const results = [];
|
|
7277
|
+
const sourceRequests = [];
|
|
7278
|
+
let currentCursor = initialCursor;
|
|
7279
|
+
let lastApi;
|
|
7280
|
+
let lastData;
|
|
7281
|
+
let cursorPayload;
|
|
7282
|
+
let pagesFetched = 0;
|
|
7283
|
+
let hasMore = false;
|
|
7284
|
+
let pageBudgetExhausted = false;
|
|
7285
|
+
let timeBudgetExhausted = false;
|
|
7286
|
+
do {
|
|
7287
|
+
const api = await fetchPage(currentCursor.afterId);
|
|
7288
|
+
const data = readObject(api.data);
|
|
7289
|
+
const pageResults = readArray(data?.results);
|
|
7290
|
+
lastApi = api;
|
|
7291
|
+
lastData = data;
|
|
7292
|
+
results.push(...pageResults);
|
|
7293
|
+
pagesFetched += 1;
|
|
7294
|
+
sourceRequests.push({
|
|
7295
|
+
source: currentCursor.endpoint,
|
|
7296
|
+
page: pagesFetched,
|
|
7297
|
+
request_id: api.requestId,
|
|
7298
|
+
upstream_request_id: api.upstreamRequestId,
|
|
7299
|
+
...apiSourceMetadata(api)
|
|
7300
|
+
});
|
|
7301
|
+
cursorPayload = advanceFeedCursor(store, currentCursor, data?.meta);
|
|
7302
|
+
hasMore = readBoolean(readObject(data?.meta)?.has_more) === true;
|
|
7303
|
+
if (currentCursor.afterId === undefined || !hasMore) {
|
|
7304
|
+
break;
|
|
7305
|
+
}
|
|
7306
|
+
if (pagesFetched >= maxPages) {
|
|
7307
|
+
pageBudgetExhausted = true;
|
|
7308
|
+
break;
|
|
7309
|
+
}
|
|
7310
|
+
if (Date.now() - startedAt >= timeBudgetMs) {
|
|
7311
|
+
timeBudgetExhausted = true;
|
|
7312
|
+
break;
|
|
7313
|
+
}
|
|
7314
|
+
currentCursor = {
|
|
7315
|
+
...currentCursor,
|
|
7316
|
+
afterId: readNumber(cursorPayload?.next_after_id) ?? currentCursor.afterId,
|
|
7317
|
+
resumed: true
|
|
7318
|
+
};
|
|
7319
|
+
} while (true);
|
|
7320
|
+
if (!lastApi) {
|
|
7321
|
+
throw new Error(`No ${initialCursor.endpoint} response was collected.`);
|
|
7322
|
+
}
|
|
7323
|
+
return {
|
|
7324
|
+
lastApi,
|
|
7325
|
+
lastData,
|
|
7326
|
+
results,
|
|
7327
|
+
cursor: cursorPayload,
|
|
7328
|
+
pagesFetched,
|
|
7329
|
+
pageBudgetExhausted,
|
|
7330
|
+
timeBudgetExhausted,
|
|
7331
|
+
sourceRequests
|
|
7332
|
+
};
|
|
7333
|
+
}
|
|
7334
|
+
function normalizeFeedCursorFilters(filters) {
|
|
7335
|
+
return Object.fromEntries(Object.entries(filters)
|
|
7336
|
+
.filter(([key, value]) => key !== "after_id" && key !== "page" && value !== undefined)
|
|
7337
|
+
.map(([key, value]) => [key, normalizeFeedCursorFilterValue(value)]));
|
|
7338
|
+
}
|
|
7339
|
+
function normalizeFeedCursorFilterValue(value) {
|
|
7340
|
+
if (Array.isArray(value)) {
|
|
7341
|
+
return value
|
|
7342
|
+
.map((entry) => normalizeFeedCursorFilterValue(entry))
|
|
7343
|
+
.sort((left, right) => String(left).localeCompare(String(right)));
|
|
7344
|
+
}
|
|
7345
|
+
return typeof value === "string" ? value.trim().toLowerCase() : value;
|
|
7346
|
+
}
|
|
6815
7347
|
async function runTool(toolName, config, rateLimiter, callback) {
|
|
6816
7348
|
const startedAt = Date.now();
|
|
6817
7349
|
const decision = rateLimiter.consume(toolName, rateLimitForTool(toolName, config));
|
|
@@ -6870,7 +7402,8 @@ function compactToolText(payload, isError) {
|
|
|
6870
7402
|
[
|
|
6871
7403
|
readArray(payload.programs).length,
|
|
6872
7404
|
readArray(payload.targets).length,
|
|
6873
|
-
readArray(payload.changes).length
|
|
7405
|
+
readArray(payload.changes).length,
|
|
7406
|
+
readArray(payload.repository_changes).length
|
|
6874
7407
|
].find((count) => count > 0);
|
|
6875
7408
|
return JSON.stringify(stripUndefined({
|
|
6876
7409
|
request_id: payload.request_id,
|
|
@@ -6901,6 +7434,7 @@ function withApiMetadata(api, payload) {
|
|
|
6901
7434
|
upstream_request_id: api.upstreamRequestId,
|
|
6902
7435
|
cache: stripUndefined({
|
|
6903
7436
|
hit: api.cached,
|
|
7437
|
+
stale_while_revalidate: api.stale,
|
|
6904
7438
|
coalesced_live_request: api.coalesced,
|
|
6905
7439
|
expires_at: api.cacheExpiresAt
|
|
6906
7440
|
}),
|
|
@@ -7053,7 +7587,11 @@ function compactTarget(target) {
|
|
|
7053
7587
|
target_type: stringField(target, "target_type"),
|
|
7054
7588
|
in_scope: readBoolean(target.in_scope),
|
|
7055
7589
|
eligible_for_bounty: readBoolean(target.eligible_for_bounty),
|
|
7056
|
-
wildcard: readBoolean(target.wildcard)
|
|
7590
|
+
wildcard: readBoolean(target.wildcard),
|
|
7591
|
+
target_opportunity_score: readNumber(target.target_opportunity_score),
|
|
7592
|
+
target_opportunity_label: stringField(target, "target_opportunity_label"),
|
|
7593
|
+
competition_risk: stringField(target, "competition_risk"),
|
|
7594
|
+
why_this_target: stringField(target, "why_this_target")
|
|
7057
7595
|
});
|
|
7058
7596
|
}
|
|
7059
7597
|
function targetIdentifier(target) {
|