@bbradar/mcp 0.2.2 → 0.2.5
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 +48 -5
- package/benchmark/mcp-benchmark.mjs +98 -5
- package/dist/bbradarClient.d.ts +7 -4
- package/dist/bbradarClient.js +41 -9
- package/dist/bbradarClient.js.map +1 -1
- package/dist/sanitize.js +14 -1
- package/dist/sanitize.js.map +1 -1
- package/dist/server.js +993 -67
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -6,7 +6,7 @@ import { NumericCursorStore } from "./cursorStore.js";
|
|
|
6
6
|
import { stableStringify, stripUndefined } from "./json.js";
|
|
7
7
|
import { FixedWindowRateLimiter } from "./rateLimit.js";
|
|
8
8
|
import { readArray, readBoolean, readNumber, readObject, readString, sanitizeJson, sanitizeChange, sanitizeProgram, sanitizeRepositoryChange, sanitizeTarget, sanitizeTargetsForExport } from "./sanitize.js";
|
|
9
|
-
const SERVER_VERSION = "0.2.
|
|
9
|
+
const SERVER_VERSION = "0.2.5";
|
|
10
10
|
const MAX_PROGRAMS_PER_SEARCH = 100;
|
|
11
11
|
const MAX_TARGET_EXPORT = 500;
|
|
12
12
|
const MAX_RECENT_CHANGES = 100;
|
|
@@ -21,8 +21,13 @@ const MAX_FIND_TARGET_SAMPLE_PROGRAMS = 10;
|
|
|
21
21
|
const MAX_FIND_TARGET_SAMPLES = 20;
|
|
22
22
|
const MAX_FIND_UPSTREAM_REQUEST_BUDGET = 50;
|
|
23
23
|
const DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET = 20;
|
|
24
|
+
const DEFAULT_HUNT_PROGRAM_PAGES = 7;
|
|
25
|
+
const MAX_HUNT_RESULTS = 15;
|
|
26
|
+
const MAX_HUNT_TARGETS_PER_RESULT = 10;
|
|
27
|
+
const MAX_BULK_PROGRAM_RESOURCE_LINKS = 5;
|
|
24
28
|
const DEFAULT_RESOLVE_UPSTREAM_REQUEST_BUDGET = 12;
|
|
25
29
|
const MAX_ACCEPTED_NUMERIC_LIMIT = 1_000;
|
|
30
|
+
const MAX_UPSTREAM_PAGE = 500;
|
|
26
31
|
const PROGRAM_COLLECTION_CONCURRENCY = 4;
|
|
27
32
|
const TARGET_SAMPLE_CONCURRENCY = 3;
|
|
28
33
|
const DEFAULT_FILTER_DISCOVERY_PAGES = 2;
|
|
@@ -50,6 +55,7 @@ const CORE_TOOL_NAMES = new Set([
|
|
|
50
55
|
"get_program_targets",
|
|
51
56
|
"get_recent_changes",
|
|
52
57
|
"get_repository_changes",
|
|
58
|
+
"find_hunt_targets",
|
|
53
59
|
"find_repository_hunt_targets",
|
|
54
60
|
"get_latest_added_targets",
|
|
55
61
|
"get_opportunities",
|
|
@@ -61,6 +67,7 @@ const CORE_TOOL_NAMES = new Set([
|
|
|
61
67
|
"export_targets"
|
|
62
68
|
]);
|
|
63
69
|
const TOOL_ENDPOINT_REQUIREMENTS = {
|
|
70
|
+
search_targets: "target_search",
|
|
64
71
|
get_repository_changes: "repository_changes",
|
|
65
72
|
find_repository_hunt_targets: "repository_changes",
|
|
66
73
|
export_targets: "target_export",
|
|
@@ -178,6 +185,8 @@ const vdpModeSchema = z.enum(["include", "exclude", "only_likely", "only_known_n
|
|
|
178
185
|
const targetMatchModeSchema = z
|
|
179
186
|
.enum(["smart", "contains", "exact", "suffix", "wildcard"])
|
|
180
187
|
.describe("How to match target_query. smart is forgiving for URLs, wildcards, target names, and partial words.");
|
|
188
|
+
const apiTargetMatchModeSchema = z.enum(["contains", "exact", "suffix", "wildcard"]);
|
|
189
|
+
const targetChangeTypeSchema = z.enum(["added", "readded", "updated", "removed"]);
|
|
181
190
|
const programNameActionSchema = z.enum(["new_targets", "targets", "scope_delta", "brief", "target_breakdown"]);
|
|
182
191
|
const guideTopicSchema = z.enum(["start", "program", "discovery", "freshness", "targets", "compare", "export", "filters"]).default("start");
|
|
183
192
|
const readOnlyAnnotations = {
|
|
@@ -236,6 +245,15 @@ const targetQuerySchema = z
|
|
|
236
245
|
})
|
|
237
246
|
.describe("Target identifier, domain, URL, wildcard, app/package/repo name, or partial target text. URLs and wildcards are normalized automatically.");
|
|
238
247
|
const isoDateTimeSchema = z.string().datetime({ offset: true });
|
|
248
|
+
const stableSnapshotSchema = z
|
|
249
|
+
.string()
|
|
250
|
+
.trim()
|
|
251
|
+
.min(1)
|
|
252
|
+
.max(1_024)
|
|
253
|
+
.refine((value) => !/[\u0000-\u001F\u007F]/.test(value), {
|
|
254
|
+
message: "Snapshot tokens cannot contain control characters."
|
|
255
|
+
})
|
|
256
|
+
.describe("Optional stable-page token. Send 'start' on page 1, then reuse meta.snapshot with identical filters. Snapshot creation is consistency-first and can be slower; omit it for the fastest live result.");
|
|
239
257
|
const pageSchema = z.number().int().min(1).default(1);
|
|
240
258
|
const programsPageSizeSchema = z.number().int().min(1).max(MAX_PROGRAMS_PER_SEARCH).default(20);
|
|
241
259
|
const recentChangesPageSizeSchema = z.number().int().min(1).max(MAX_RECENT_CHANGES).default(50);
|
|
@@ -397,14 +415,16 @@ export function createBbradarServer(client, config) {
|
|
|
397
415
|
"Use BBRadar only for passive bug bounty intelligence; never scan or contact targets.",
|
|
398
416
|
"This server uses the compact core tool profile. Start with get_bbradar_guide when unsure.",
|
|
399
417
|
"Use resolve_program or run_program_name_action for a named program.",
|
|
400
|
-
"Use
|
|
418
|
+
"Use find_hunt_targets once for requests combining good rewards, broad scope, target quality, and recent target additions. Use find_repository_hunt_targets instead when Git or repository activity is explicitly required.",
|
|
419
|
+
"Use find_programs for lower-level program-only discovery and find_programs_by_target for reverse target lookup.",
|
|
401
420
|
"Default to bounty-eligible in-scope data unless the user explicitly requests broader results.",
|
|
402
421
|
"Prefer compact/identifier outputs and disclose sampled ranking scope when present."
|
|
403
422
|
]
|
|
404
423
|
: [
|
|
405
424
|
"Use BBRadar only for passive bug bounty intelligence; never scan or contact targets.",
|
|
406
425
|
"When unsure which BBRadar tool to use, call get_bbradar_guide first. It is local-only and cheap.",
|
|
407
|
-
"Simple default path:
|
|
426
|
+
"Simple default path: find_hunt_targets once for ranked target/program discovery, run_program_name_action for one named program, get_program_eligible_targets_by_name for exhaustive target lists by program name.",
|
|
427
|
+
"Do not repeat program discovery with alternate sorts or fan out get_program_targets across a shortlist; find_hunt_targets already includes exact bounded samples and recent target additions.",
|
|
408
428
|
"Use advanced tools only after the user's goal is clear or the guide points to them.",
|
|
409
429
|
"Partial program name + action: run_program_name_action. Partial program name only: resolve_program.",
|
|
410
430
|
"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.",
|
|
@@ -412,7 +432,7 @@ export function createBbradarServer(client, config) {
|
|
|
412
432
|
"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.",
|
|
413
433
|
"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.",
|
|
414
434
|
"Repository activity: get_repository_changes defaults to compact program-only events. Seed with after_id, then resume the in-memory per-filter cursor.",
|
|
415
|
-
"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.",
|
|
435
|
+
"Discovery: find_hunt_targets is the one-call default; 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, and find_low_noise_programs are lower-level alternatives.",
|
|
416
436
|
"Details: get_program_brief, get_program_scope_summary, get_program_target_breakdown, find_programs_by_target, compare_programs_compact.",
|
|
417
437
|
"Prefer compact/identifier outputs and disclose sampled ranking scope when present."
|
|
418
438
|
];
|
|
@@ -429,6 +449,7 @@ export function createBbradarServer(client, config) {
|
|
|
429
449
|
const feedCursorStore = new NumericCursorStore(config.cursorStatePath, MAX_IN_MEMORY_FEED_CURSORS);
|
|
430
450
|
const maxProgramsPageSize = client.endpointNumber("programs", "max_page_size", MAX_PROGRAMS_PER_SEARCH);
|
|
431
451
|
const maxProgramTargetsPageSize = client.endpointNumber("program_targets", "max_page_size", MAX_TARGETS_PER_PROGRAM);
|
|
452
|
+
const maxTargetSearchPageSize = client.endpointNumber("target_search", "max_page_size", MAX_TARGETS_PER_PROGRAM);
|
|
432
453
|
const maxTargetChangesPageSize = client.endpointNumber("target_changes", "max_page_size", MAX_RECENT_CHANGES);
|
|
433
454
|
const maxRepositoryChangesPageSize = client.endpointNumber("repository_changes", "max_page_size", MAX_RECENT_CHANGES);
|
|
434
455
|
const maxTargetExport = client.endpointNumber("target_export", "max_targets", MAX_TARGET_EXPORT);
|
|
@@ -437,14 +458,16 @@ export function createBbradarServer(client, config) {
|
|
|
437
458
|
const recentChangesPageSizeSchema = z.number().int().min(1).max(maxTargetChangesPageSize).default(Math.min(50, maxTargetChangesPageSize));
|
|
438
459
|
const repositoryChangesPageSizeSchema = z.number().int().min(1).max(maxRepositoryChangesPageSize).default(Math.min(50, maxRepositoryChangesPageSize));
|
|
439
460
|
const acceptedTargetLimitSchema = z.number().int().min(1).max(maxProgramTargetsPageSize);
|
|
461
|
+
const targetSearchPageSizeSchema = z.number().int().min(1).max(maxTargetSearchPageSize).default(Math.min(50, maxTargetSearchPageSize));
|
|
440
462
|
registerTool("search_programs", {
|
|
441
463
|
title: "Search BBRadar Programs",
|
|
442
|
-
description: `Search or list active platform
|
|
464
|
+
description: `Search or list active and confirmed paused platform programs plus active Self-Hosted programs. page_size max ${maxProgramsPageSize}. Stable snapshots are opt-in because creating one can be slower than live indexed pagination.`,
|
|
443
465
|
inputSchema: {
|
|
444
466
|
search: searchTextSchema.optional().describe("Server-side program name, handle, platform, policy-host, or target search."),
|
|
445
467
|
platforms: platformListSchema(),
|
|
446
468
|
tags: tagListSchema(),
|
|
447
469
|
updated_since: isoDateTimeSchema.optional().describe("ISO datetime."),
|
|
470
|
+
snapshot: stableSnapshotSchema.optional(),
|
|
448
471
|
vdp_mode: vdpModeSchema.default("exclude").describe("VDP/no-bounty filter mode."),
|
|
449
472
|
page: pageSchema,
|
|
450
473
|
page_size: programsPageSizeSchema
|
|
@@ -453,6 +476,7 @@ export function createBbradarServer(client, config) {
|
|
|
453
476
|
annotations: readOnlyAnnotations
|
|
454
477
|
}, (args) => runTool("search_programs", config, rateLimiter, async () => {
|
|
455
478
|
const { vdp_mode, ...filters } = args;
|
|
479
|
+
assertStableSnapshotSupported(client, "programs", filters.snapshot);
|
|
456
480
|
const api = await client.listPrograms(toQuery(filters));
|
|
457
481
|
const data = readObject(api.data);
|
|
458
482
|
const programs = readArray(data?.programs)
|
|
@@ -464,6 +488,47 @@ export function createBbradarServer(client, config) {
|
|
|
464
488
|
meta: sanitizeJson(data?.meta)
|
|
465
489
|
});
|
|
466
490
|
}));
|
|
491
|
+
registerTool("search_targets", {
|
|
492
|
+
title: "Search BBRadar Targets",
|
|
493
|
+
description: `Low-level reverse target search and current target-inventory paging. q may be omitted for inventory traversal. page_size max ${maxTargetSearchPageSize}. Stable snapshots are opt-in and intentionally not used by find_hunt_targets.`,
|
|
494
|
+
inputSchema: {
|
|
495
|
+
q: targetQuerySchema.optional(),
|
|
496
|
+
match_mode: apiTargetMatchModeSchema.default("contains"),
|
|
497
|
+
snapshot: stableSnapshotSchema.optional(),
|
|
498
|
+
platforms: platformListSchema(),
|
|
499
|
+
tags: tagListSchema(),
|
|
500
|
+
include_out_of_scope: z.boolean().default(false),
|
|
501
|
+
include_ineligible: z.boolean().default(false),
|
|
502
|
+
page: pageSchema,
|
|
503
|
+
page_size: targetSearchPageSizeSchema,
|
|
504
|
+
output_mode: targetListModeSchema.default("compact")
|
|
505
|
+
},
|
|
506
|
+
outputSchema: {
|
|
507
|
+
...apiEnvelopeOutputShape(),
|
|
508
|
+
results: z.array(jsonRecordSchema()).optional(),
|
|
509
|
+
meta: jsonRecordSchema().optional(),
|
|
510
|
+
continuation: jsonRecordSchema().optional()
|
|
511
|
+
},
|
|
512
|
+
annotations: readOnlyAnnotations
|
|
513
|
+
}, (args) => runTool("search_targets", config, rateLimiter, async () => {
|
|
514
|
+
const { output_mode, ...filters } = args;
|
|
515
|
+
assertStableSnapshotSupported(client, "target_search", filters.snapshot);
|
|
516
|
+
const api = await client.searchTargets(toQuery(filters));
|
|
517
|
+
const data = readObject(api.data);
|
|
518
|
+
const results = readArray(data?.results).map((value) => {
|
|
519
|
+
const row = readObject(value) ?? {};
|
|
520
|
+
const program = sanitizeProgram(row.program, config.webBaseUrl);
|
|
521
|
+
const target = sanitizeTarget(row.target);
|
|
522
|
+
return stripUndefined({
|
|
523
|
+
program: formatProgram(program, output_mode === "full" ? "full" : "compact"),
|
|
524
|
+
target: output_mode === "full" ? target : compactTarget(target)
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
return applyCollectionContinuation(withApiMetadata(api, {
|
|
528
|
+
results,
|
|
529
|
+
meta: sanitizeJson(data?.meta)
|
|
530
|
+
}), "results", "target search results", continuationStore, config);
|
|
531
|
+
}));
|
|
467
532
|
registerTool("get_program", {
|
|
468
533
|
title: "Get BBRadar Program",
|
|
469
534
|
description: "Get one program by platform:handle program_id, handle, or human program name. Names are resolved automatically when unambiguous.",
|
|
@@ -622,7 +687,8 @@ export function createBbradarServer(client, config) {
|
|
|
622
687
|
title: "Get Recent Target Changes",
|
|
623
688
|
description: `List recent target changes. page_size max ${MAX_RECENT_CHANGES}.`,
|
|
624
689
|
inputSchema: {
|
|
625
|
-
change_type:
|
|
690
|
+
change_type: targetChangeTypeSchema.optional(),
|
|
691
|
+
snapshot: stableSnapshotSchema.optional(),
|
|
626
692
|
after_id: z.number().int().min(0).optional().describe("Incremental cursor; returns changes after this id in ascending order."),
|
|
627
693
|
resume_cursor: z.boolean().default(false).describe("Resume the in-memory cursor for this exact normalized filter set. Seed it once with after_id."),
|
|
628
694
|
max_cursor_pages: z.number().int().min(1).max(10).default(2),
|
|
@@ -651,16 +717,24 @@ export function createBbradarServer(client, config) {
|
|
|
651
717
|
annotations: readOnlyAnnotations
|
|
652
718
|
}, (args) => runTool("get_recent_changes", config, rateLimiter, async () => {
|
|
653
719
|
const { output_mode, resume_cursor, max_cursor_pages, cursor_time_budget_ms, ...filters } = args;
|
|
720
|
+
assertStableSnapshotSupported(client, "target_changes", filters.snapshot);
|
|
654
721
|
const cursor = resolveFeedCursor(feedCursorStore, "target_changes", filters, resume_cursor);
|
|
655
|
-
const { min_target_score, target_opportunity_label, competition_risk, ...upstreamFilters } = filters;
|
|
656
|
-
const collected = await collectCursorFeedPages(feedCursorStore, cursor, max_cursor_pages, cursor_time_budget_ms, (afterId) => client.getRecentChanges(toQuery({
|
|
722
|
+
const { min_target_score, target_opportunity_label, competition_risk, snapshot, ...upstreamFilters } = filters;
|
|
723
|
+
const collected = await collectCursorFeedPages(feedCursorStore, cursor, max_cursor_pages, cursor_time_budget_ms, snapshot, (afterId, recoveryProbe, activeSnapshot) => client.getRecentChanges(toQuery({
|
|
657
724
|
...upstreamFilters,
|
|
658
725
|
minTargetScore: min_target_score,
|
|
659
726
|
targetOpportunityLabel: target_opportunity_label,
|
|
660
727
|
competitionRisk: competition_risk,
|
|
728
|
+
snapshot: activeSnapshot,
|
|
661
729
|
after_id: afterId,
|
|
662
730
|
page: afterId === undefined ? filters.page : 1
|
|
663
|
-
})))
|
|
731
|
+
}), recoveryProbe ? { cache: false, coalesce: false } : {}), (startPage, recoveryProbe) => refreshInventoryBaseline((page, pageProbe) => client.searchTargets(toQuery({
|
|
732
|
+
q: upstreamFilters.search,
|
|
733
|
+
platforms: upstreamFilters.platforms,
|
|
734
|
+
tags: upstreamFilters.tags,
|
|
735
|
+
page,
|
|
736
|
+
page_size: client.endpointNumber("target_search", "max_page_size", 100)
|
|
737
|
+
}), pageProbe ? { cache: false, coalesce: false } : {}), "results", max_cursor_pages, cursor_time_budget_ms, startPage, recoveryProbe));
|
|
664
738
|
const api = collected.lastApi;
|
|
665
739
|
const data = collected.lastData;
|
|
666
740
|
const sanitizedMeta = readObject(sanitizeJson(data?.meta));
|
|
@@ -675,7 +749,9 @@ export function createBbradarServer(client, config) {
|
|
|
675
749
|
pages_fetched: collected.pagesFetched,
|
|
676
750
|
returned_across_pages: changes.length,
|
|
677
751
|
cursor_page_budget_exhausted: collected.pageBudgetExhausted,
|
|
678
|
-
cursor_time_budget_exhausted: collected.timeBudgetExhausted
|
|
752
|
+
cursor_time_budget_exhausted: collected.timeBudgetExhausted,
|
|
753
|
+
history_gap: collected.gapRecovery !== undefined,
|
|
754
|
+
inventory_baseline: collected.gapRecovery
|
|
679
755
|
}),
|
|
680
756
|
facets: sanitizeJson(data?.facets),
|
|
681
757
|
cursor: collected.cursor,
|
|
@@ -687,6 +763,7 @@ export function createBbradarServer(client, config) {
|
|
|
687
763
|
description: "Read-only repository push/change feed for direct inspection or polling. Defaults to lightweight program summaries; use after_id for efficient incremental polling. Use find_repository_hunt_targets instead when reward or target-quality ranking is also required.",
|
|
688
764
|
inputSchema: {
|
|
689
765
|
after_id: z.number().int().min(0).optional().describe("Incremental cursor; returns events after this id in ascending order."),
|
|
766
|
+
snapshot: stableSnapshotSchema.optional(),
|
|
690
767
|
resume_cursor: z.boolean().default(false).describe("Resume the in-memory cursor for this exact normalized filter set. Seed it once with after_id."),
|
|
691
768
|
max_cursor_pages: z.number().int().min(1).max(10).default(2),
|
|
692
769
|
cursor_time_budget_ms: z.number().int().min(100).max(10_000).default(3_000),
|
|
@@ -717,12 +794,20 @@ export function createBbradarServer(client, config) {
|
|
|
717
794
|
throw new Error("The negotiated BBRadar API contract does not expose repository changes.");
|
|
718
795
|
}
|
|
719
796
|
const { output_mode, resume_cursor, max_cursor_pages, cursor_time_budget_ms, ...filters } = args;
|
|
797
|
+
assertStableSnapshotSupported(client, "repository_changes", filters.snapshot);
|
|
720
798
|
const cursor = resolveFeedCursor(feedCursorStore, "repository_changes", filters, resume_cursor);
|
|
721
|
-
const
|
|
722
|
-
|
|
799
|
+
const { snapshot, ...upstreamFilters } = filters;
|
|
800
|
+
const collected = await collectCursorFeedPages(feedCursorStore, cursor, max_cursor_pages, cursor_time_budget_ms, snapshot, (afterId, recoveryProbe, activeSnapshot) => client.getRepositoryChanges(toQuery({
|
|
801
|
+
...upstreamFilters,
|
|
802
|
+
snapshot: activeSnapshot,
|
|
723
803
|
after_id: afterId,
|
|
724
804
|
page: afterId === undefined ? filters.page : 1
|
|
725
|
-
})))
|
|
805
|
+
}), recoveryProbe ? { cache: false, coalesce: false } : {}), (startPage, recoveryProbe) => refreshInventoryBaseline((page, pageProbe) => client.getRepositoryChanges(toQuery({
|
|
806
|
+
...upstreamFilters,
|
|
807
|
+
after_id: undefined,
|
|
808
|
+
page,
|
|
809
|
+
page_size: client.endpointNumber("repository_changes", "max_page_size", 100)
|
|
810
|
+
}), pageProbe ? { cache: false, coalesce: false } : {}), "results", max_cursor_pages, cursor_time_budget_ms, startPage, recoveryProbe));
|
|
726
811
|
const api = collected.lastApi;
|
|
727
812
|
const data = collected.lastData;
|
|
728
813
|
return applyCollectionContinuation(withApiMetadata(api, {
|
|
@@ -733,7 +818,9 @@ export function createBbradarServer(client, config) {
|
|
|
733
818
|
pages_fetched: collected.pagesFetched,
|
|
734
819
|
returned_across_pages: collected.results.length,
|
|
735
820
|
cursor_page_budget_exhausted: collected.pageBudgetExhausted,
|
|
736
|
-
cursor_time_budget_exhausted: collected.timeBudgetExhausted
|
|
821
|
+
cursor_time_budget_exhausted: collected.timeBudgetExhausted,
|
|
822
|
+
history_gap: collected.gapRecovery !== undefined,
|
|
823
|
+
inventory_baseline: collected.gapRecovery
|
|
737
824
|
}),
|
|
738
825
|
facets: sanitizeJson(data?.facets),
|
|
739
826
|
available_facets: sanitizeJson(data?.available_facets),
|
|
@@ -741,9 +828,54 @@ export function createBbradarServer(client, config) {
|
|
|
741
828
|
source_requests: collected.sourceRequests.length > 1 ? collected.sourceRequests : undefined
|
|
742
829
|
}), "repository_changes", "repository changes", continuationStore, config);
|
|
743
830
|
}));
|
|
831
|
+
registerTool("find_hunt_targets", {
|
|
832
|
+
title: "Find Best Hunt Targets",
|
|
833
|
+
description: "Preferred one-call path for requests combining good rewards, broad Web2/Web3 scope, target quality, competition, and recently added targets. Returns a final ranked shortlist with exact bounded target samples and recent additions; do not fan out into repeated find_programs/get_program_targets calls. Self-Hosted is supported through current inventory and automatically skips its unavailable change feed. Use find_repository_hunt_targets instead only when Git or repository activity is explicitly requested.",
|
|
834
|
+
inputSchema: {
|
|
835
|
+
platforms: platformListSchema(),
|
|
836
|
+
exclude_platforms: stringListSchema("Platforms to exclude from the final shortlist."),
|
|
837
|
+
tags: tagListSchema(),
|
|
838
|
+
scope_tags: scopeTagListSchema(),
|
|
839
|
+
target_types: targetTypeListSchema(),
|
|
840
|
+
language_tags: languageTagListSchema(),
|
|
841
|
+
surface_mode: z.enum(["any", "web2", "web3"]).default("any"),
|
|
842
|
+
min_bounty_max: z.number().int().min(0).optional(),
|
|
843
|
+
require_bounty: z.boolean().default(true),
|
|
844
|
+
max_public_report_count: z.number().int().min(0).optional(),
|
|
845
|
+
include_unknown_report_count: z.boolean().default(true),
|
|
846
|
+
min_eligible_targets: z.number().int().min(0).default(3),
|
|
847
|
+
min_total_targets: z.number().int().min(0).optional(),
|
|
848
|
+
min_wildcards: z.number().int().min(0).max(1000).default(0),
|
|
849
|
+
min_added_7d: z.number().int().min(0).optional(),
|
|
850
|
+
fresh_launch_days: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).optional(),
|
|
851
|
+
updated_since: isoDateTimeSchema.optional(),
|
|
852
|
+
min_target_score: z.number().int().min(0).max(100).optional(),
|
|
853
|
+
target_opportunity_label: targetOpportunityLabelSchema.optional(),
|
|
854
|
+
competition_risk: competitionRiskSchema.optional(),
|
|
855
|
+
include_recent_changes: z.boolean().default(true),
|
|
856
|
+
recent_target_days: z.number().int().min(1).max(365).default(30),
|
|
857
|
+
recent_changes_page_size: recentChangesPageSizeSchema.default(Math.min(MAX_RECENT_CHANGES, maxTargetChangesPageSize)),
|
|
858
|
+
max_program_pages: z.number().int().min(1).max(MAX_FIND_PROGRAM_PAGES).default(DEFAULT_HUNT_PROGRAM_PAGES),
|
|
859
|
+
max_results: z.number().int().min(1).max(MAX_HUNT_RESULTS).default(10),
|
|
860
|
+
targets_per_result: z.number().int().min(1).max(MAX_HUNT_TARGETS_PER_RESULT).default(5),
|
|
861
|
+
ranking_mode: z.enum(["balanced", "highest_reward", "widest_scope", "freshest_targets", "lowest_competition"]).default("balanced"),
|
|
862
|
+
upstream_request_budget: z.number().int().min(2).max(MAX_FIND_UPSTREAM_REQUEST_BUDGET).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
|
|
863
|
+
},
|
|
864
|
+
outputSchema: {
|
|
865
|
+
...apiEnvelopeOutputShape(),
|
|
866
|
+
matches: z.array(jsonRecordSchema()).optional(),
|
|
867
|
+
filters: jsonRecordSchema().optional(),
|
|
868
|
+
errors: z.array(jsonRecordSchema()).optional(),
|
|
869
|
+
upstream_budget: jsonRecordSchema().optional(),
|
|
870
|
+
ranking_scope: jsonRecordSchema().optional(),
|
|
871
|
+
result_guidance: jsonRecordSchema().optional(),
|
|
872
|
+
meta: jsonRecordSchema().optional()
|
|
873
|
+
},
|
|
874
|
+
annotations: readOnlyAnnotations
|
|
875
|
+
}, (args) => runTool("find_hunt_targets", config, rateLimiter, async () => findHuntTargets(client, config, args)));
|
|
744
876
|
registerTool("find_repository_hunt_targets", {
|
|
745
877
|
title: "Find Repository-Aware Hunt Targets",
|
|
746
|
-
description: "Fast one-call path for requests that combine recent repository activity with program rewards and target quality. Fetches program rankings and repository changes in parallel, joins them locally, and works with arbitrary
|
|
878
|
+
description: "Fast one-call path for requests that combine recent repository activity with program rewards and target quality. Fetches program rankings and repository changes in parallel, joins them locally, and works with arbitrary supported platform catalogs, Web2/Web3 modes, reward thresholds, target types, languages, and freshness criteria. Self-Hosted has no repository stream and is rejected without upstream work.",
|
|
747
879
|
inputSchema: {
|
|
748
880
|
platforms: platformListSchema(),
|
|
749
881
|
exclude_platforms: stringListSchema("Platforms to exclude from final matches."),
|
|
@@ -864,7 +996,7 @@ export function createBbradarServer(client, config) {
|
|
|
864
996
|
inputSchema: {
|
|
865
997
|
program_id: programIdentifierSchema,
|
|
866
998
|
since: isoDateTimeSchema.optional(),
|
|
867
|
-
change_type:
|
|
999
|
+
change_type: targetChangeTypeSchema.optional(),
|
|
868
1000
|
target_type: filterValueSchema().optional(),
|
|
869
1001
|
language_tags: languageTagListSchema(),
|
|
870
1002
|
include_removed: z.boolean().default(true),
|
|
@@ -888,7 +1020,7 @@ export function createBbradarServer(client, config) {
|
|
|
888
1020
|
title: "Get Recent Target Activity",
|
|
889
1021
|
description: "Recent target changes grouped by program.",
|
|
890
1022
|
inputSchema: {
|
|
891
|
-
change_type:
|
|
1023
|
+
change_type: targetChangeTypeSchema.optional(),
|
|
892
1024
|
target_type: filterValueSchema().optional(),
|
|
893
1025
|
include_removed: z.boolean().default(true),
|
|
894
1026
|
include_out_of_scope: z.boolean().default(false),
|
|
@@ -989,7 +1121,7 @@ export function createBbradarServer(client, config) {
|
|
|
989
1121
|
}));
|
|
990
1122
|
registerTool("find_programs", {
|
|
991
1123
|
title: "Find BBRadar Programs",
|
|
992
|
-
description: "
|
|
1124
|
+
description: "Lower-level program-only discovery. For normal 'find good targets/programs' requests, prefer one find_hunt_targets call because it already combines ranking, exact bounded target samples, and recent additions. Use this tool when raw program filtering without target enrichment is specifically needed; keep output_mode=compact and avoid parallel alternate-sort calls.",
|
|
993
1125
|
inputSchema: {
|
|
994
1126
|
platforms: platformListSchema(),
|
|
995
1127
|
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."),
|
|
@@ -1374,7 +1506,7 @@ export function createBbradarServer(client, config) {
|
|
|
1374
1506
|
description: "Recent changes by target type grouped by program.",
|
|
1375
1507
|
inputSchema: {
|
|
1376
1508
|
target_type: filterValueSchema(),
|
|
1377
|
-
change_type:
|
|
1509
|
+
change_type: targetChangeTypeSchema.default("added"),
|
|
1378
1510
|
include_removed: z.boolean().default(false),
|
|
1379
1511
|
include_out_of_scope: z.boolean().default(false),
|
|
1380
1512
|
include_ineligible: z.boolean().default(false),
|
|
@@ -1881,10 +2013,10 @@ function bbradarGuide(topic, profile, client) {
|
|
|
1881
2013
|
}
|
|
1882
2014
|
const primaryEntryPoints = [
|
|
1883
2015
|
{
|
|
1884
|
-
tool: "
|
|
1885
|
-
use_when: "The user wants good programs
|
|
2016
|
+
tool: "find_hunt_targets",
|
|
2017
|
+
use_when: "The user wants good targets or programs ranked by rewards, scope, competition, target quality, or recent target additions without requiring repository activity.",
|
|
1886
2018
|
minimum_arguments: {},
|
|
1887
|
-
useful_arguments: ["platforms", "target_types", "language_tags", "min_bounty_max", "max_public_report_count", "
|
|
2019
|
+
useful_arguments: ["platforms", "surface_mode", "target_types", "language_tags", "min_bounty_max", "max_public_report_count", "recent_target_days", "ranking_mode"]
|
|
1888
2020
|
},
|
|
1889
2021
|
{
|
|
1890
2022
|
tool: "find_repository_hunt_targets",
|
|
@@ -1913,13 +2045,13 @@ function bbradarGuide(topic, profile, client) {
|
|
|
1913
2045
|
const decisionTree = [
|
|
1914
2046
|
{
|
|
1915
2047
|
user_goal: "find programs to hunt",
|
|
1916
|
-
first_tool: "
|
|
1917
|
-
then: "
|
|
2048
|
+
first_tool: "find_hunt_targets",
|
|
2049
|
+
then: "Treat its ranked matches, exact bounded target samples, and recent additions as final unless the user explicitly requests an exhaustive scope dump for one selected program."
|
|
1918
2050
|
},
|
|
1919
2051
|
{
|
|
1920
2052
|
user_goal: "Web2 programs with broad scope, good rewards, recent target activity, and excluded platforms",
|
|
1921
|
-
first_tool: "
|
|
1922
|
-
then: "
|
|
2053
|
+
first_tool: "find_hunt_targets",
|
|
2054
|
+
then: "Call once with surface_mode=web2 and the requested platforms/exclusions. Do not split the platforms or ranking dimensions into separate calls."
|
|
1923
2055
|
},
|
|
1924
2056
|
{
|
|
1925
2057
|
user_goal: "named program summary",
|
|
@@ -1991,7 +2123,7 @@ function bbradarGuide(topic, profile, client) {
|
|
|
1991
2123
|
}
|
|
1992
2124
|
];
|
|
1993
2125
|
const advancedTools = {
|
|
1994
|
-
discovery: ["find_wildcard_programs", "find_low_competition_high_reward", "find_stack_matches", "find_language_programs", "find_target_type_programs", "find_reward_programs", "find_paid_programs", "find_web3_contests", "find_low_noise_programs"],
|
|
2126
|
+
discovery: ["find_hunt_targets", "find_wildcard_programs", "find_low_competition_high_reward", "find_stack_matches", "find_language_programs", "find_target_type_programs", "find_reward_programs", "find_paid_programs", "find_web3_contests", "find_low_noise_programs"],
|
|
1995
2127
|
freshness: ["find_recently_added_wildcards", "find_recently_updated_programs", "find_fresh_hunt_candidates", "get_latest_added_targets", "get_recent_target_activity", "get_program_scope_delta", "check_watchlist_new_targets"],
|
|
1996
2128
|
details: ["get_program_brief", "get_program_scope_summary", "get_program_target_breakdown", "find_programs_by_target"],
|
|
1997
2129
|
exports: ["export_targets"]
|
|
@@ -2064,9 +2196,10 @@ function bbradarGuide(topic, profile, client) {
|
|
|
2064
2196
|
function coreBbradarGuide(topic, client) {
|
|
2065
2197
|
const decisionTree = [
|
|
2066
2198
|
{
|
|
2067
|
-
user_goal: "find programs
|
|
2068
|
-
first_tool: "
|
|
2069
|
-
arguments: { max_results: 10,
|
|
2199
|
+
user_goal: "find good hunt targets or programs using rewards, scope, competition, target quality, or recent target additions",
|
|
2200
|
+
first_tool: "find_hunt_targets",
|
|
2201
|
+
arguments: { max_results: 10, ranking_mode: "balanced" },
|
|
2202
|
+
then: "Use this single result as the final shortlist; do not repeat find_programs with alternate sorts or fetch each program's targets."
|
|
2070
2203
|
},
|
|
2071
2204
|
{
|
|
2072
2205
|
user_goal: "named program targets",
|
|
@@ -2102,7 +2235,7 @@ function coreBbradarGuide(topic, client) {
|
|
|
2102
2235
|
{
|
|
2103
2236
|
user_goal: "unknown filter values",
|
|
2104
2237
|
first_tool: "list_filters",
|
|
2105
|
-
then: "Reuse observed platforms, tags, target types, and language tags in
|
|
2238
|
+
then: "Reuse observed platforms, tags, target types, and language tags in one find_hunt_targets call."
|
|
2106
2239
|
}
|
|
2107
2240
|
];
|
|
2108
2241
|
return {
|
|
@@ -2118,6 +2251,7 @@ function coreBbradarGuide(topic, client) {
|
|
|
2118
2251
|
require_bounty: true
|
|
2119
2252
|
},
|
|
2120
2253
|
primary_entry_points: [
|
|
2254
|
+
"find_hunt_targets",
|
|
2121
2255
|
"find_programs",
|
|
2122
2256
|
"find_repository_hunt_targets",
|
|
2123
2257
|
"run_program_name_action",
|
|
@@ -4353,21 +4487,50 @@ function latestAddedProgramMatches(change, input, platformFilters) {
|
|
|
4353
4487
|
return true;
|
|
4354
4488
|
}
|
|
4355
4489
|
async function getProgramTargetsPayload(client, input, programId) {
|
|
4356
|
-
const
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4490
|
+
const pageSize = client.endpointNumber("program_targets", "max_page_size", MAX_TARGETS_PER_PROGRAM);
|
|
4491
|
+
// `eligibility=eligible` is an API-level in-scope bounty-eligibility filter,
|
|
4492
|
+
// so the common eligible-only path needs no second local scope pass.
|
|
4493
|
+
const directOffsetPaging = !input.include_ineligible || (!input.strict_scope_filter && input.include_out_of_scope);
|
|
4494
|
+
const collected = [];
|
|
4495
|
+
let matchedSeen = directOffsetPaging ? Math.floor(input.offset / pageSize) * pageSize : 0;
|
|
4496
|
+
let page = directOffsetPaging ? Math.floor(input.offset / pageSize) + 1 : 1;
|
|
4497
|
+
const firstPage = page;
|
|
4498
|
+
let api;
|
|
4499
|
+
let upstreamMeta;
|
|
4500
|
+
let upstreamHasMore = false;
|
|
4501
|
+
do {
|
|
4502
|
+
api = await client.getProgramTargets(programId, {
|
|
4503
|
+
search: input.search,
|
|
4504
|
+
target_type: input.target_type,
|
|
4505
|
+
eligibility: input.include_ineligible ? "all" : "eligible",
|
|
4506
|
+
minTargetScore: input.min_target_score,
|
|
4507
|
+
targetOpportunityLabel: input.target_opportunity_label,
|
|
4508
|
+
competitionRisk: input.competition_risk,
|
|
4509
|
+
page,
|
|
4510
|
+
page_size: pageSize
|
|
4511
|
+
});
|
|
4512
|
+
const data = readObject(api.data);
|
|
4513
|
+
upstreamMeta = readObject(data?.meta);
|
|
4514
|
+
for (const target of readArray(data?.targets).map(sanitizeTarget)) {
|
|
4515
|
+
if (!targetHasAllowedScope(target, input))
|
|
4516
|
+
continue;
|
|
4517
|
+
if (matchedSeen >= input.offset && collected.length < input.limit)
|
|
4518
|
+
collected.push(target);
|
|
4519
|
+
matchedSeen += 1;
|
|
4520
|
+
}
|
|
4521
|
+
upstreamHasMore = readBoolean(upstreamMeta?.has_more) === true;
|
|
4522
|
+
const totalPages = readNumber(upstreamMeta?.total_pages);
|
|
4523
|
+
page += 1;
|
|
4524
|
+
if (totalPages !== undefined && page > totalPages) {
|
|
4525
|
+
upstreamHasMore = false;
|
|
4526
|
+
}
|
|
4527
|
+
} while (upstreamHasMore &&
|
|
4528
|
+
page <= MAX_UPSTREAM_PAGE &&
|
|
4529
|
+
collected.length < input.limit);
|
|
4530
|
+
if (!api) {
|
|
4531
|
+
throw new Error("No program target response was collected.");
|
|
4532
|
+
}
|
|
4533
|
+
const targets = collected;
|
|
4371
4534
|
return withApiMetadata(api, {
|
|
4372
4535
|
program_id: programId,
|
|
4373
4536
|
targets: formatTargetList(targets, input.output_mode),
|
|
@@ -4375,12 +4538,16 @@ async function getProgramTargetsPayload(client, input, programId) {
|
|
|
4375
4538
|
offset: input.offset,
|
|
4376
4539
|
limit: input.limit,
|
|
4377
4540
|
output_mode: input.output_mode,
|
|
4378
|
-
total_active_targets: readNumber(upstreamMeta?.total) ??
|
|
4379
|
-
total_after_filters:
|
|
4541
|
+
total_active_targets: readNumber(upstreamMeta?.total) ?? matchedSeen,
|
|
4542
|
+
total_after_filters: upstreamHasMore ? undefined : matchedSeen,
|
|
4380
4543
|
returned: targets.length,
|
|
4381
|
-
has_more: input.offset +
|
|
4382
|
-
upstream_has_more:
|
|
4383
|
-
possibly_incomplete:
|
|
4544
|
+
has_more: upstreamHasMore || matchedSeen > input.offset + targets.length,
|
|
4545
|
+
upstream_has_more: upstreamHasMore,
|
|
4546
|
+
possibly_incomplete: upstreamHasMore,
|
|
4547
|
+
upstream_pages_fetched: page - firstPage,
|
|
4548
|
+
upstream_start_page: firstPage,
|
|
4549
|
+
direct_offset_paging: directOffsetPaging,
|
|
4550
|
+
upstream_page_limit_reached: upstreamHasMore && page > MAX_UPSTREAM_PAGE,
|
|
4384
4551
|
data_capabilities: readObject(upstreamMeta?.data_capabilities),
|
|
4385
4552
|
target_source: "program_targets"
|
|
4386
4553
|
}
|
|
@@ -5633,7 +5800,7 @@ function previewExportText(payload) {
|
|
|
5633
5800
|
}
|
|
5634
5801
|
function collectResourceLinks(payload) {
|
|
5635
5802
|
const links = new Map();
|
|
5636
|
-
for (const program of extractProgramsFromPayload(payload)) {
|
|
5803
|
+
for (const program of extractProgramsFromPayload(payload).slice(0, MAX_BULK_PROGRAM_RESOURCE_LINKS)) {
|
|
5637
5804
|
const id = stringField(program, "id");
|
|
5638
5805
|
const resourceLinks = readObject(program.resource_links);
|
|
5639
5806
|
const programUri = stringField(resourceLinks, "program");
|
|
@@ -5735,6 +5902,8 @@ function compactProgram(program) {
|
|
|
5735
5902
|
name: stringField(program, "name"),
|
|
5736
5903
|
platform: stringField(program, "platform"),
|
|
5737
5904
|
catalog: stringField(program, "catalog"),
|
|
5905
|
+
program_status: stringField(program, "program_status"),
|
|
5906
|
+
is_active: readBoolean(program.is_active),
|
|
5738
5907
|
program_type: stringField(program, "program_type"),
|
|
5739
5908
|
platform_url: stringField(program, "platform_url"),
|
|
5740
5909
|
bbradar_url: stringField(program, "bbradar_url"),
|
|
@@ -5753,6 +5922,9 @@ function compactProgram(program) {
|
|
|
5753
5922
|
last_updated: stringField(program, "last_updated"),
|
|
5754
5923
|
targets_updated_at: stringField(program, "targets_updated_at"),
|
|
5755
5924
|
scope_summary: readObject(program.scope_summary),
|
|
5925
|
+
match_qualities: Array.isArray(program.match_qualities)
|
|
5926
|
+
? readStringArrayField(program, "match_qualities")
|
|
5927
|
+
: undefined,
|
|
5756
5928
|
resource_links: readObject(program.resource_links)
|
|
5757
5929
|
});
|
|
5758
5930
|
}
|
|
@@ -5813,6 +5985,7 @@ function formatRepositoryChange(change, mode) {
|
|
|
5813
5985
|
})
|
|
5814
5986
|
: undefined,
|
|
5815
5987
|
changes_url: stringField(change, "changes_url"),
|
|
5988
|
+
comparison: sanitizeJson(change.comparison),
|
|
5816
5989
|
programs: readArray(change.programs).map((program) => formatProgram(readObject(program) ?? {}, "compact")),
|
|
5817
5990
|
targets: Array.isArray(change.targets)
|
|
5818
5991
|
? change.targets.map((target) => compactTarget(readObject(target) ?? {}))
|
|
@@ -5981,15 +6154,15 @@ function candidateTradeoffs(candidate) {
|
|
|
5981
6154
|
}
|
|
5982
6155
|
function candidateNextAction(candidate) {
|
|
5983
6156
|
if (candidate.wildcardCount > 0) {
|
|
5984
|
-
return "
|
|
6157
|
+
return "Use included target samples for shortlisting; call get_program_targets only if the user explicitly requests the selected program's exhaustive wildcard scope.";
|
|
5985
6158
|
}
|
|
5986
6159
|
if (candidateHasApiSurface(candidate)) {
|
|
5987
|
-
return "
|
|
6160
|
+
return "Use included target samples for shortlisting; call get_program_targets only if the user explicitly requests the selected program's exhaustive API scope.";
|
|
5988
6161
|
}
|
|
5989
6162
|
if (candidateHasWebSurface(candidate)) {
|
|
5990
|
-
return "
|
|
6163
|
+
return "Use included target samples for shortlisting; call get_program_targets only if the user explicitly requests the selected program's exhaustive web scope.";
|
|
5991
6164
|
}
|
|
5992
|
-
return "
|
|
6165
|
+
return "The program-level ranking is sufficient for a shortlist; fetch exact scope only if the user requests exhaustive details for this program.";
|
|
5993
6166
|
}
|
|
5994
6167
|
function targetSurfaceScore(candidate) {
|
|
5995
6168
|
return candidate.targetSurfaceScore;
|
|
@@ -6294,13 +6467,595 @@ function clampLimit(name, value, max, warnings) {
|
|
|
6294
6467
|
warnings.push(`${name} was clamped from ${value} to ${max}.`);
|
|
6295
6468
|
return max;
|
|
6296
6469
|
}
|
|
6470
|
+
async function findHuntTargets(client, config, input) {
|
|
6471
|
+
const warnings = [];
|
|
6472
|
+
const errors = [];
|
|
6473
|
+
const selfHostedOnly = isSelfHostedOnlyPlatformSelection(input.platforms);
|
|
6474
|
+
const targetChangesEndpointSupported = client.supportsEndpoint("target_changes");
|
|
6475
|
+
const recentChangesEnabled = input.include_recent_changes &&
|
|
6476
|
+
!selfHostedOnly &&
|
|
6477
|
+
targetChangesEndpointSupported;
|
|
6478
|
+
const desiredRecentChangeQueries = recentChangesEnabled ? huntRecentChangeQueries(input) : [];
|
|
6479
|
+
const recentChangeQueries = desiredRecentChangeQueries.slice(0, Math.max(0, input.upstream_request_budget - 2));
|
|
6480
|
+
const recentRequestReserve = recentChangeQueries.length;
|
|
6481
|
+
const candidatePoolSize = Math.min(MAX_FIND_PROGRAM_RESULTS, Math.max(input.max_results, input.max_results * 3));
|
|
6482
|
+
const programRequestBudget = Math.min(input.max_program_pages, Math.max(1, Math.ceil((input.upstream_request_budget - recentRequestReserve) / 2)));
|
|
6483
|
+
const programInput = {
|
|
6484
|
+
platforms: input.platforms,
|
|
6485
|
+
exclude_platforms: input.exclude_platforms,
|
|
6486
|
+
tags: input.tags,
|
|
6487
|
+
scope_tags: input.scope_tags,
|
|
6488
|
+
target_types: input.target_types,
|
|
6489
|
+
language_tags: input.language_tags,
|
|
6490
|
+
web3: input.surface_mode === "web3",
|
|
6491
|
+
exclude_web3: input.surface_mode === "web2",
|
|
6492
|
+
opportunity_levels: [],
|
|
6493
|
+
updated_since: input.updated_since,
|
|
6494
|
+
min_bounty_min: undefined,
|
|
6495
|
+
min_bounty_max: input.min_bounty_max,
|
|
6496
|
+
max_bounty_max: undefined,
|
|
6497
|
+
require_bounty: input.require_bounty,
|
|
6498
|
+
max_public_report_count: input.max_public_report_count,
|
|
6499
|
+
require_known_report_count: false,
|
|
6500
|
+
include_unknown_report_count: input.include_unknown_report_count,
|
|
6501
|
+
exclude_ctf: true,
|
|
6502
|
+
exclude_mobile_only: input.surface_mode === "web2",
|
|
6503
|
+
exclude_non_web: input.surface_mode === "web2",
|
|
6504
|
+
has_api_scope: false,
|
|
6505
|
+
contest_like: false,
|
|
6506
|
+
vdp_mode: "exclude",
|
|
6507
|
+
fresh_launch_days: input.fresh_launch_days,
|
|
6508
|
+
min_wildcards: input.min_wildcards,
|
|
6509
|
+
min_eligible_targets: input.min_eligible_targets,
|
|
6510
|
+
min_total_targets: input.min_total_targets,
|
|
6511
|
+
min_added_24h: undefined,
|
|
6512
|
+
min_added_7d: input.min_added_7d,
|
|
6513
|
+
max_pages: input.max_program_pages,
|
|
6514
|
+
max_results: candidatePoolSize,
|
|
6515
|
+
sort_by: huntProgramSort(input.ranking_mode),
|
|
6516
|
+
include_target_samples: false,
|
|
6517
|
+
target_sample_programs: 0,
|
|
6518
|
+
target_sample_size: 1,
|
|
6519
|
+
only_in_scope_targets: true,
|
|
6520
|
+
only_bounty_eligible_targets: true,
|
|
6521
|
+
target_samples_only_wildcards: false,
|
|
6522
|
+
output_mode: "compact",
|
|
6523
|
+
upstream_request_budget: programRequestBudget
|
|
6524
|
+
};
|
|
6525
|
+
if (input.include_recent_changes && selfHostedOnly) {
|
|
6526
|
+
warnings.push("Self-Hosted does not expose target-change history; recent target evidence was derived only from current target-inventory timestamps.");
|
|
6527
|
+
}
|
|
6528
|
+
else if (input.include_recent_changes && !recentChangesEnabled) {
|
|
6529
|
+
warnings.push("The negotiated API contract does not expose target changes; ranked current programs without recent-change enrichment.");
|
|
6530
|
+
}
|
|
6531
|
+
else if (recentChangeQueries.length < desiredRecentChangeQueries.length) {
|
|
6532
|
+
warnings.push(`Recent target-change queries were reduced from ${desiredRecentChangeQueries.length} to ${recentChangeQueries.length} to preserve the upstream request budget.`);
|
|
6533
|
+
}
|
|
6534
|
+
const [programPayload, recentResult] = await Promise.all([
|
|
6535
|
+
findPrograms(client, config, programInput),
|
|
6536
|
+
recentChangesEnabled
|
|
6537
|
+
? fetchHuntRecentChanges(client, config, input, recentChangeQueries)
|
|
6538
|
+
: Promise.resolve({ changes: [], sourceRequests: [], requestsUsed: 0, errors: [] })
|
|
6539
|
+
]);
|
|
6540
|
+
const programWarnings = readArray(programPayload.warnings).filter((warning) => typeof warning === "string");
|
|
6541
|
+
warnings.push(...programWarnings);
|
|
6542
|
+
errors.push(...recentResult.errors);
|
|
6543
|
+
const programBudget = readObject(programPayload.upstream_budget);
|
|
6544
|
+
const programRequestsUsed = readNumber(programBudget?.used) ?? 0;
|
|
6545
|
+
const targetRequestBudget = Math.max(0, input.upstream_request_budget - programRequestsUsed - recentResult.requestsUsed);
|
|
6546
|
+
const programEntries = readArray(programPayload.programs)
|
|
6547
|
+
.map(readObject)
|
|
6548
|
+
.filter((program) => program !== undefined);
|
|
6549
|
+
const recentByProgram = groupRecentHuntChanges(recentResult.changes, input);
|
|
6550
|
+
const preliminaryRankedEntries = programEntries
|
|
6551
|
+
.map((entry) => ({
|
|
6552
|
+
entry,
|
|
6553
|
+
ranking: buildHuntTargetMatch(entry, new Map(), recentByProgram, input)
|
|
6554
|
+
}))
|
|
6555
|
+
.sort((left, right) => compareHuntTargetMatches(left.ranking, right.ranking, input.ranking_mode));
|
|
6556
|
+
const sampledProgramCount = Math.min(input.max_results, preliminaryRankedEntries.length, targetRequestBudget);
|
|
6557
|
+
const finalistEntries = preliminaryRankedEntries
|
|
6558
|
+
.slice(0, sampledProgramCount)
|
|
6559
|
+
.map((entry) => entry.entry);
|
|
6560
|
+
const targetResult = await collectHuntTargetSamples(client, finalistEntries, input);
|
|
6561
|
+
errors.push(...targetResult.errors);
|
|
6562
|
+
if (sampledProgramCount < Math.min(input.max_results, programEntries.length)) {
|
|
6563
|
+
warnings.push(`The upstream request budget limited the final exact target shortlist to ${sampledProgramCount} of ${Math.min(input.max_results, programEntries.length)} requested results.`);
|
|
6564
|
+
}
|
|
6565
|
+
const ranked = finalistEntries
|
|
6566
|
+
.map((entry) => buildHuntTargetMatch(entry, targetResult.targetsByProgram, recentByProgram, input))
|
|
6567
|
+
.sort((left, right) => compareHuntTargetMatches(left, right, input.ranking_mode))
|
|
6568
|
+
.slice(0, input.max_results);
|
|
6569
|
+
const matches = ranked.map((entry, index) => ({
|
|
6570
|
+
rank: index + 1,
|
|
6571
|
+
...entry.payload
|
|
6572
|
+
}));
|
|
6573
|
+
const targetRequestsUsed = targetResult.requestsUsed;
|
|
6574
|
+
const totalRequestsUsed = programRequestsUsed + recentResult.requestsUsed + targetRequestsUsed;
|
|
6575
|
+
return stripUndefined({
|
|
6576
|
+
request_id: randomUUID(),
|
|
6577
|
+
fetched_at: new Date().toISOString(),
|
|
6578
|
+
source_requests: [
|
|
6579
|
+
...readArray(programPayload.source_requests),
|
|
6580
|
+
...recentResult.sourceRequests,
|
|
6581
|
+
...targetResult.sourceRequests
|
|
6582
|
+
],
|
|
6583
|
+
warnings: uniqueStrings(warnings).length > 0 ? uniqueStrings(warnings) : undefined,
|
|
6584
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
6585
|
+
filters: stripUndefined({
|
|
6586
|
+
platforms: input.platforms.length > 0 ? input.platforms : undefined,
|
|
6587
|
+
exclude_platforms: input.exclude_platforms.length > 0 ? input.exclude_platforms : undefined,
|
|
6588
|
+
tags: input.tags.length > 0 ? input.tags : undefined,
|
|
6589
|
+
scope_tags: input.scope_tags.length > 0 ? input.scope_tags : undefined,
|
|
6590
|
+
target_types: input.target_types.length > 0 ? input.target_types : undefined,
|
|
6591
|
+
language_tags: input.language_tags.length > 0 ? input.language_tags : undefined,
|
|
6592
|
+
surface_mode: input.surface_mode,
|
|
6593
|
+
min_bounty_max: input.min_bounty_max,
|
|
6594
|
+
require_bounty: input.require_bounty,
|
|
6595
|
+
max_public_report_count: input.max_public_report_count,
|
|
6596
|
+
min_eligible_targets: input.min_eligible_targets,
|
|
6597
|
+
min_total_targets: input.min_total_targets,
|
|
6598
|
+
min_wildcards: input.min_wildcards,
|
|
6599
|
+
min_added_7d: input.min_added_7d,
|
|
6600
|
+
fresh_launch_days: input.fresh_launch_days,
|
|
6601
|
+
updated_since: input.updated_since,
|
|
6602
|
+
min_target_score: input.min_target_score,
|
|
6603
|
+
target_opportunity_label: input.target_opportunity_label,
|
|
6604
|
+
competition_risk: input.competition_risk,
|
|
6605
|
+
recent_target_days: input.include_recent_changes ? input.recent_target_days : undefined,
|
|
6606
|
+
ranking_mode: input.ranking_mode
|
|
6607
|
+
}),
|
|
6608
|
+
upstream_budget: {
|
|
6609
|
+
initial: input.upstream_request_budget,
|
|
6610
|
+
used: totalRequestsUsed,
|
|
6611
|
+
remaining: Math.max(0, input.upstream_request_budget - totalRequestsUsed)
|
|
6612
|
+
},
|
|
6613
|
+
ranking_scope: stripUndefined({
|
|
6614
|
+
...(readObject(programPayload.ranking_scope) ?? {}),
|
|
6615
|
+
single_program_catalog_pass: true,
|
|
6616
|
+
recent_change_window_days: input.include_recent_changes ? input.recent_target_days : undefined,
|
|
6617
|
+
recent_change_page_size: input.include_recent_changes ? input.recent_changes_page_size : undefined
|
|
6618
|
+
}),
|
|
6619
|
+
result_guidance: {
|
|
6620
|
+
final_shortlist: true,
|
|
6621
|
+
follow_up_calls_required: false,
|
|
6622
|
+
instruction: "Use these matches as the final shortlist. Exact bounded target samples and recent additions are already included; call get_program_targets only if the user explicitly requests an exhaustive scope dump for one selected program."
|
|
6623
|
+
},
|
|
6624
|
+
matches,
|
|
6625
|
+
meta: {
|
|
6626
|
+
returned: matches.length,
|
|
6627
|
+
program_candidates: programEntries.length,
|
|
6628
|
+
exact_target_programs_sampled: targetResult.targetsByProgram.size,
|
|
6629
|
+
recent_changes_scanned: recentResult.changes.length,
|
|
6630
|
+
recent_programs_matched: recentByProgram.size,
|
|
6631
|
+
recent_change_catalog_supported: !selfHostedOnly && targetChangesEndpointSupported,
|
|
6632
|
+
model_round_trips_expected: 1,
|
|
6633
|
+
parallel_recent_change_fetch: recentChangeQueries.length > 0,
|
|
6634
|
+
bulk_resource_links_suppressed: true,
|
|
6635
|
+
partial_success: errors.length > 0
|
|
6636
|
+
}
|
|
6637
|
+
});
|
|
6638
|
+
}
|
|
6639
|
+
function huntProgramSort(mode) {
|
|
6640
|
+
if (mode === "highest_reward") {
|
|
6641
|
+
return "highest_reward";
|
|
6642
|
+
}
|
|
6643
|
+
if (mode === "widest_scope") {
|
|
6644
|
+
return "most_eligible_targets";
|
|
6645
|
+
}
|
|
6646
|
+
if (mode === "freshest_targets") {
|
|
6647
|
+
return "most_new_targets";
|
|
6648
|
+
}
|
|
6649
|
+
if (mode === "lowest_competition") {
|
|
6650
|
+
return "lowest_reports";
|
|
6651
|
+
}
|
|
6652
|
+
return "best_hunt_value";
|
|
6653
|
+
}
|
|
6654
|
+
async function fetchHuntRecentChanges(client, config, input, targetTypes) {
|
|
6655
|
+
const results = await Promise.all(targetTypes.map(async (targetType) => {
|
|
6656
|
+
try {
|
|
6657
|
+
const api = await client.getRecentChanges(toQuery({
|
|
6658
|
+
change_type: "added",
|
|
6659
|
+
minTargetScore: input.min_target_score,
|
|
6660
|
+
targetOpportunityLabel: input.target_opportunity_label,
|
|
6661
|
+
competitionRisk: input.competition_risk,
|
|
6662
|
+
include_removed: false,
|
|
6663
|
+
include_ineligible: false,
|
|
6664
|
+
include_out_of_scope: false,
|
|
6665
|
+
platforms: input.platforms,
|
|
6666
|
+
tags: targetType ? [targetType] : undefined,
|
|
6667
|
+
page: 1,
|
|
6668
|
+
page_size: input.recent_changes_page_size
|
|
6669
|
+
}));
|
|
6670
|
+
const data = readObject(api.data);
|
|
6671
|
+
return {
|
|
6672
|
+
changes: readArray(data?.results).map((change) => sanitizeChange(change, config.webBaseUrl)),
|
|
6673
|
+
sourceRequest: {
|
|
6674
|
+
source: "recent_changes",
|
|
6675
|
+
target_type: targetType,
|
|
6676
|
+
request_id: api.requestId,
|
|
6677
|
+
upstream_request_id: api.upstreamRequestId,
|
|
6678
|
+
...apiSourceMetadata(api),
|
|
6679
|
+
page: 1
|
|
6680
|
+
},
|
|
6681
|
+
requestsUsed: api.cached || api.coalesced ? 0 : 1,
|
|
6682
|
+
error: undefined
|
|
6683
|
+
};
|
|
6684
|
+
}
|
|
6685
|
+
catch (error) {
|
|
6686
|
+
return {
|
|
6687
|
+
changes: [],
|
|
6688
|
+
sourceRequest: undefined,
|
|
6689
|
+
requestsUsed: error instanceof BBRadarApiError && (error.cached || error.coalesced) ? 0 : 1,
|
|
6690
|
+
error: apiOperationError(targetType ? `recent ${targetType} target changes` : "recent target changes", error)
|
|
6691
|
+
};
|
|
6692
|
+
}
|
|
6693
|
+
}));
|
|
6694
|
+
const changes = new Map();
|
|
6695
|
+
for (const result of results) {
|
|
6696
|
+
for (const change of result.changes) {
|
|
6697
|
+
const id = readNumber(change.id) ?? stringField(change, "id");
|
|
6698
|
+
const target = readObject(change.target);
|
|
6699
|
+
const program = readObject(change.program);
|
|
6700
|
+
const fallbackKey = `${stringField(program, "id") ?? ""}:${targetIdentifier(target ?? {}) ?? ""}:${stringField(change, "changed_at") ?? ""}`;
|
|
6701
|
+
changes.set(id === undefined ? fallbackKey : String(id), change);
|
|
6702
|
+
}
|
|
6703
|
+
}
|
|
6704
|
+
return {
|
|
6705
|
+
changes: [...changes.values()],
|
|
6706
|
+
sourceRequests: results
|
|
6707
|
+
.map((result) => result.sourceRequest)
|
|
6708
|
+
.filter((request) => request !== undefined),
|
|
6709
|
+
requestsUsed: results.reduce((total, result) => total + result.requestsUsed, 0),
|
|
6710
|
+
errors: results
|
|
6711
|
+
.map((result) => result.error)
|
|
6712
|
+
.filter((error) => error !== undefined)
|
|
6713
|
+
};
|
|
6714
|
+
}
|
|
6715
|
+
function huntRecentChangeQueries(input) {
|
|
6716
|
+
const requested = uniqueStrings(input.target_types);
|
|
6717
|
+
if (requested.length > 0 && requested.length <= 3) {
|
|
6718
|
+
return requested;
|
|
6719
|
+
}
|
|
6720
|
+
if (requested.length > 3) {
|
|
6721
|
+
return [undefined];
|
|
6722
|
+
}
|
|
6723
|
+
if (input.surface_mode === "web2") {
|
|
6724
|
+
return ["domain", "api", "wildcard"];
|
|
6725
|
+
}
|
|
6726
|
+
if (input.surface_mode === "web3") {
|
|
6727
|
+
return ["source code", "smart contract", "blockchain-dlt"];
|
|
6728
|
+
}
|
|
6729
|
+
return [undefined];
|
|
6730
|
+
}
|
|
6731
|
+
async function collectHuntTargetSamples(client, programs, input) {
|
|
6732
|
+
const targetsByProgram = new Map();
|
|
6733
|
+
const sourceRequestsByIndex = new Array(programs.length);
|
|
6734
|
+
const errorsByIndex = new Array(programs.length);
|
|
6735
|
+
let requestsUsed = 0;
|
|
6736
|
+
await mapWithConcurrency(programs.map((program, index) => ({ program, index })), TARGET_SAMPLE_CONCURRENCY, async ({ program, index }) => {
|
|
6737
|
+
const programId = stringField(program, "id");
|
|
6738
|
+
if (!programId) {
|
|
6739
|
+
return;
|
|
6740
|
+
}
|
|
6741
|
+
try {
|
|
6742
|
+
const requestedPageSize = Math.max(input.targets_per_result, input.target_types.length > 1 ? input.targets_per_result * 10 : input.targets_per_result * 4, 20);
|
|
6743
|
+
const api = await client.getProgramTargets(programId, {
|
|
6744
|
+
target_type: input.target_types.length === 1 ? input.target_types[0] : undefined,
|
|
6745
|
+
eligibility: "eligible",
|
|
6746
|
+
minTargetScore: input.min_target_score,
|
|
6747
|
+
targetOpportunityLabel: input.target_opportunity_label,
|
|
6748
|
+
competitionRisk: input.competition_risk,
|
|
6749
|
+
page: 1,
|
|
6750
|
+
page_size: Math.min(client.endpointNumber("program_targets", "max_page_size", MAX_TARGETS_PER_PROGRAM), requestedPageSize)
|
|
6751
|
+
});
|
|
6752
|
+
const data = readObject(api.data);
|
|
6753
|
+
const matchingTargets = readArray(data?.targets)
|
|
6754
|
+
.map(sanitizeTarget)
|
|
6755
|
+
.filter((target) => huntTargetMatches(target, input));
|
|
6756
|
+
const targets = selectHuntTargetSamples(matchingTargets, input.targets_per_result, input.recent_target_days);
|
|
6757
|
+
targetsByProgram.set(programId, targets);
|
|
6758
|
+
sourceRequestsByIndex[index] = {
|
|
6759
|
+
source: "program_targets",
|
|
6760
|
+
program_id: programId,
|
|
6761
|
+
request_id: api.requestId,
|
|
6762
|
+
upstream_request_id: api.upstreamRequestId,
|
|
6763
|
+
...apiSourceMetadata(api)
|
|
6764
|
+
};
|
|
6765
|
+
if (!api.cached && !api.coalesced) {
|
|
6766
|
+
requestsUsed += 1;
|
|
6767
|
+
}
|
|
6768
|
+
}
|
|
6769
|
+
catch (error) {
|
|
6770
|
+
errorsByIndex[index] = apiOperationError(programId, error);
|
|
6771
|
+
if (!(error instanceof BBRadarApiError) || (!error.cached && !error.coalesced)) {
|
|
6772
|
+
requestsUsed += 1;
|
|
6773
|
+
}
|
|
6774
|
+
}
|
|
6775
|
+
});
|
|
6776
|
+
return {
|
|
6777
|
+
targetsByProgram,
|
|
6778
|
+
sourceRequests: sourceRequestsByIndex.filter((request) => request !== undefined),
|
|
6779
|
+
requestsUsed,
|
|
6780
|
+
errors: errorsByIndex.filter((error) => error !== undefined)
|
|
6781
|
+
};
|
|
6782
|
+
}
|
|
6783
|
+
function huntTargetMatches(target, input) {
|
|
6784
|
+
if (target.in_scope !== true || target.eligible_for_bounty !== true) {
|
|
6785
|
+
return false;
|
|
6786
|
+
}
|
|
6787
|
+
if (input.target_types.length > 0 && !input.target_types.some((targetType) => targetMatchesRequestedType(target, targetType))) {
|
|
6788
|
+
return false;
|
|
6789
|
+
}
|
|
6790
|
+
if (input.scope_tags.length > 0 && !arraysIntersectNormalized(new Set(input.scope_tags.map(normalizeTag)), readStringArrayField(target, "scope_tags"))) {
|
|
6791
|
+
return false;
|
|
6792
|
+
}
|
|
6793
|
+
if (input.language_tags.length > 0 && !arraysIntersectNormalized(new Set(input.language_tags.map(normalizeTag)), readStringArrayField(target, "language_tags"))) {
|
|
6794
|
+
return false;
|
|
6795
|
+
}
|
|
6796
|
+
if (input.min_target_score !== undefined && (readNumber(target.target_opportunity_score) ?? -1) < input.min_target_score) {
|
|
6797
|
+
return false;
|
|
6798
|
+
}
|
|
6799
|
+
if (input.target_opportunity_label !== undefined && stringField(target, "target_opportunity_label") !== input.target_opportunity_label) {
|
|
6800
|
+
return false;
|
|
6801
|
+
}
|
|
6802
|
+
if (input.competition_risk !== undefined && stringField(target, "competition_risk") !== input.competition_risk) {
|
|
6803
|
+
return false;
|
|
6804
|
+
}
|
|
6805
|
+
return true;
|
|
6806
|
+
}
|
|
6807
|
+
function compareHuntTargets(left, right) {
|
|
6808
|
+
return (compareNumbersDesc(readNumber(left.target_opportunity_score), readNumber(right.target_opportunity_score)) ||
|
|
6809
|
+
Number(readBoolean(right.wildcard) === true) - Number(readBoolean(left.wildcard) === true) ||
|
|
6810
|
+
(targetIdentifier(left) ?? "").localeCompare(targetIdentifier(right) ?? ""));
|
|
6811
|
+
}
|
|
6812
|
+
function selectHuntTargetSamples(targets, limit, recentTargetDays) {
|
|
6813
|
+
const selected = new Map();
|
|
6814
|
+
const cutoff = Date.now() - recentTargetDays * 24 * 60 * 60 * 1000;
|
|
6815
|
+
const newest = [...targets]
|
|
6816
|
+
.filter((target) => huntTargetAddedTimestamp(target) >= cutoff)
|
|
6817
|
+
.sort((left, right) => huntTargetAddedTimestamp(right) - huntTargetAddedTimestamp(left));
|
|
6818
|
+
const byQuality = [...targets].sort(compareHuntTargets);
|
|
6819
|
+
for (const target of newest.slice(0, Math.min(2, limit))) {
|
|
6820
|
+
const identifier = targetIdentifier(target);
|
|
6821
|
+
if (identifier) {
|
|
6822
|
+
selected.set(identifier, target);
|
|
6823
|
+
}
|
|
6824
|
+
}
|
|
6825
|
+
for (const target of byQuality) {
|
|
6826
|
+
if (selected.size >= limit) {
|
|
6827
|
+
break;
|
|
6828
|
+
}
|
|
6829
|
+
const identifier = targetIdentifier(target);
|
|
6830
|
+
if (identifier && !selected.has(identifier)) {
|
|
6831
|
+
selected.set(identifier, target);
|
|
6832
|
+
}
|
|
6833
|
+
}
|
|
6834
|
+
return [...selected.values()];
|
|
6835
|
+
}
|
|
6836
|
+
function huntTargetAddedTimestamp(target) {
|
|
6837
|
+
const addedAt = stringField(target, "added_at") ?? stringField(target, "source_added_at");
|
|
6838
|
+
const timestamp = addedAt ? Date.parse(addedAt) : Number.NaN;
|
|
6839
|
+
return Number.isFinite(timestamp) ? timestamp : 0;
|
|
6840
|
+
}
|
|
6841
|
+
function groupRecentHuntChanges(changes, input) {
|
|
6842
|
+
const cutoff = Date.now() - input.recent_target_days * 24 * 60 * 60 * 1000;
|
|
6843
|
+
const byProgram = new Map();
|
|
6844
|
+
for (const change of changes) {
|
|
6845
|
+
if (stringField(change, "change_type") !== "added" || timestampField(change, "changed_at") < cutoff) {
|
|
6846
|
+
continue;
|
|
6847
|
+
}
|
|
6848
|
+
const program = readObject(change.program);
|
|
6849
|
+
const target = readObject(change.target);
|
|
6850
|
+
const programId = stringField(program, "id");
|
|
6851
|
+
if (!programId || !target || !huntTargetMatches(target, input)) {
|
|
6852
|
+
continue;
|
|
6853
|
+
}
|
|
6854
|
+
const existing = byProgram.get(programId) ?? [];
|
|
6855
|
+
const identifier = targetIdentifier(target);
|
|
6856
|
+
if (!identifier || !existing.some((entry) => targetIdentifier(readObject(entry.target) ?? {}) === identifier)) {
|
|
6857
|
+
existing.push(change);
|
|
6858
|
+
existing.sort((left, right) => timestampField(right, "changed_at") - timestampField(left, "changed_at"));
|
|
6859
|
+
byProgram.set(programId, existing);
|
|
6860
|
+
}
|
|
6861
|
+
}
|
|
6862
|
+
return byProgram;
|
|
6863
|
+
}
|
|
6864
|
+
function buildHuntTargetMatch(entry, targetsByProgram, recentByProgram, input) {
|
|
6865
|
+
const programId = stringField(entry, "id") ?? "";
|
|
6866
|
+
const targetSamples = targetsByProgram.get(programId) ?? [];
|
|
6867
|
+
const recentChanges = recentByProgram.get(programId) ?? [];
|
|
6868
|
+
const recentTargets = recentHuntTargetEntries(targetSamples, recentChanges, input.recent_target_days);
|
|
6869
|
+
const rankFactors = readObject(entry.rank_factors) ?? {};
|
|
6870
|
+
const latestRecentAt = recentTargets[0]?.addedAt;
|
|
6871
|
+
const rewardScore = normalizedRewardScore(readNumber(rankFactors.bounty_max));
|
|
6872
|
+
const scopeScore = Math.min(100, (readNumber(rankFactors.eligible_targets) ?? 0) * 2 +
|
|
6873
|
+
(readNumber(rankFactors.wildcard_targets) ?? 0) * 5 +
|
|
6874
|
+
(readNumber(rankFactors.target_surface_score) ?? 0) * 0.25);
|
|
6875
|
+
const freshnessScore = huntFreshnessScore(latestRecentAt, recentTargets.length, readNumber(rankFactors.added_7d));
|
|
6876
|
+
const publicReports = readNumber(rankFactors.public_report_count);
|
|
6877
|
+
// Missing report evidence is unknown, not low competition. Keep the
|
|
6878
|
+
// internal ranking conservative and do not expose a made-up numeric signal.
|
|
6879
|
+
const competitionScore = publicReports === undefined ? 20 : Math.max(0, 100 - Math.min(100, publicReports / 10));
|
|
6880
|
+
const bestTargetScore = Math.max(0, ...targetSamples.map((target) => readNumber(target.target_opportunity_score) ?? 0));
|
|
6881
|
+
const baseHuntScore = readNumber(rankFactors.hunt_value_score) ?? 0;
|
|
6882
|
+
const compositeScore = huntCompositeScore(input.ranking_mode, baseHuntScore, rewardScore, scopeScore, freshnessScore, competitionScore, bestTargetScore);
|
|
6883
|
+
const program = compactProgram(entry);
|
|
6884
|
+
delete program.resource_links;
|
|
6885
|
+
const existingReasons = readArray(entry.why_ranked_here).filter((reason) => typeof reason === "string");
|
|
6886
|
+
const reasons = [...existingReasons];
|
|
6887
|
+
if (recentTargets.length > 0) {
|
|
6888
|
+
reasons.push(`${recentTargets.length} eligible target additions in the last ${input.recent_target_days} days`);
|
|
6889
|
+
}
|
|
6890
|
+
if (bestTargetScore > 0) {
|
|
6891
|
+
reasons.push(`best sampled target score ${bestTargetScore}`);
|
|
6892
|
+
}
|
|
6893
|
+
return {
|
|
6894
|
+
compositeScore,
|
|
6895
|
+
rewardScore,
|
|
6896
|
+
scopeScore,
|
|
6897
|
+
freshnessScore,
|
|
6898
|
+
competitionScore,
|
|
6899
|
+
payload: stripUndefined({
|
|
6900
|
+
program,
|
|
6901
|
+
match_score: compositeScore,
|
|
6902
|
+
rank_factors: {
|
|
6903
|
+
...rankFactors,
|
|
6904
|
+
reward_score: roundScore(rewardScore),
|
|
6905
|
+
scope_score: roundScore(scopeScore),
|
|
6906
|
+
freshness_score: roundScore(freshnessScore),
|
|
6907
|
+
competition_score: publicReports === undefined ? undefined : roundScore(competitionScore),
|
|
6908
|
+
competition_signal: publicReports === undefined ? "unknown" : "public_report_count",
|
|
6909
|
+
best_sample_target_score: bestTargetScore > 0 ? bestTargetScore : undefined,
|
|
6910
|
+
recent_added_targets_in_window: recentTargets.length,
|
|
6911
|
+
latest_added_target_at: latestRecentAt,
|
|
6912
|
+
composite_score: compositeScore
|
|
6913
|
+
},
|
|
6914
|
+
why_ranked_here: uniqueStrings(reasons),
|
|
6915
|
+
tradeoffs: readArray(entry.tradeoffs).filter((tradeoff) => typeof tradeoff === "string"),
|
|
6916
|
+
target_samples: targetSamples.map(compactHuntTarget),
|
|
6917
|
+
recent_added_targets: recentTargets.slice(0, input.targets_per_result).map((recent) => ({
|
|
6918
|
+
...compactHuntTarget(recent.target),
|
|
6919
|
+
added_at: recent.addedAt,
|
|
6920
|
+
freshness_source: recent.source
|
|
6921
|
+
}))
|
|
6922
|
+
})
|
|
6923
|
+
};
|
|
6924
|
+
}
|
|
6925
|
+
function compactHuntTarget(target) {
|
|
6926
|
+
return stripUndefined({
|
|
6927
|
+
...compactTarget(target),
|
|
6928
|
+
added_at: stringField(target, "added_at"),
|
|
6929
|
+
source_added_at: stringField(target, "source_added_at"),
|
|
6930
|
+
last_changed_at: stringField(target, "last_changed_at")
|
|
6931
|
+
});
|
|
6932
|
+
}
|
|
6933
|
+
function recentHuntTargetEntries(targetSamples, recentChanges, recentTargetDays) {
|
|
6934
|
+
const cutoff = Date.now() - recentTargetDays * 24 * 60 * 60 * 1000;
|
|
6935
|
+
const entries = new Map();
|
|
6936
|
+
const remember = (target, addedAt, source) => {
|
|
6937
|
+
const identifier = targetIdentifier(target);
|
|
6938
|
+
const timestamp = addedAt ? Date.parse(addedAt) : Number.NaN;
|
|
6939
|
+
if (!identifier || !addedAt || !Number.isFinite(timestamp) || timestamp < cutoff) {
|
|
6940
|
+
return;
|
|
6941
|
+
}
|
|
6942
|
+
const existing = entries.get(identifier);
|
|
6943
|
+
if (!existing || timestamp > existing.timestamp || (timestamp === existing.timestamp && source === "target_change")) {
|
|
6944
|
+
entries.set(identifier, { target, addedAt, source, timestamp });
|
|
6945
|
+
}
|
|
6946
|
+
};
|
|
6947
|
+
for (const change of recentChanges) {
|
|
6948
|
+
const target = readObject(change.target);
|
|
6949
|
+
if (target) {
|
|
6950
|
+
remember(target, stringField(change, "changed_at"), "target_change");
|
|
6951
|
+
}
|
|
6952
|
+
}
|
|
6953
|
+
for (const target of targetSamples) {
|
|
6954
|
+
remember(target, stringField(target, "added_at") ?? stringField(target, "source_added_at"), "target_inventory");
|
|
6955
|
+
}
|
|
6956
|
+
return [...entries.values()].sort((left, right) => right.timestamp - left.timestamp);
|
|
6957
|
+
}
|
|
6958
|
+
function normalizedRewardScore(reward) {
|
|
6959
|
+
if (reward === undefined || reward <= 0) {
|
|
6960
|
+
return 0;
|
|
6961
|
+
}
|
|
6962
|
+
return Math.min(100, Math.log10(reward + 1) / Math.log10(100_001) * 100);
|
|
6963
|
+
}
|
|
6964
|
+
function huntFreshnessScore(latestAt, recentCount, added7d) {
|
|
6965
|
+
if (latestAt) {
|
|
6966
|
+
const timestamp = Date.parse(latestAt);
|
|
6967
|
+
if (Number.isFinite(timestamp)) {
|
|
6968
|
+
const ageDays = Math.max(0, (Date.now() - timestamp) / (24 * 60 * 60 * 1000));
|
|
6969
|
+
return Math.min(100, Math.max(0, 100 - ageDays * 2) + Math.min(20, recentCount * 4));
|
|
6970
|
+
}
|
|
6971
|
+
}
|
|
6972
|
+
return Math.min(100, Math.max(0, added7d ?? 0) * 12);
|
|
6973
|
+
}
|
|
6974
|
+
function huntCompositeScore(mode, baseHuntScore, rewardScore, scopeScore, freshnessScore, competitionScore, bestTargetScore) {
|
|
6975
|
+
if (mode === "highest_reward") {
|
|
6976
|
+
return roundScore(rewardScore * 0.7 + baseHuntScore * 0.2 + scopeScore * 0.1);
|
|
6977
|
+
}
|
|
6978
|
+
if (mode === "widest_scope") {
|
|
6979
|
+
return roundScore(scopeScore * 0.7 + baseHuntScore * 0.2 + rewardScore * 0.1);
|
|
6980
|
+
}
|
|
6981
|
+
if (mode === "freshest_targets") {
|
|
6982
|
+
return roundScore(freshnessScore * 0.7 + bestTargetScore * 0.15 + baseHuntScore * 0.15);
|
|
6983
|
+
}
|
|
6984
|
+
if (mode === "lowest_competition") {
|
|
6985
|
+
return roundScore(competitionScore * 0.65 + baseHuntScore * 0.2 + rewardScore * 0.15);
|
|
6986
|
+
}
|
|
6987
|
+
return roundScore(baseHuntScore * 0.35 +
|
|
6988
|
+
rewardScore * 0.2 +
|
|
6989
|
+
scopeScore * 0.2 +
|
|
6990
|
+
freshnessScore * 0.15 +
|
|
6991
|
+
bestTargetScore * 0.1);
|
|
6992
|
+
}
|
|
6993
|
+
function roundScore(value) {
|
|
6994
|
+
return Math.round(value * 100) / 100;
|
|
6995
|
+
}
|
|
6996
|
+
function compareHuntTargetMatches(left, right, mode) {
|
|
6997
|
+
if (mode === "highest_reward") {
|
|
6998
|
+
return right.rewardScore - left.rewardScore || right.compositeScore - left.compositeScore;
|
|
6999
|
+
}
|
|
7000
|
+
if (mode === "widest_scope") {
|
|
7001
|
+
return right.scopeScore - left.scopeScore || right.compositeScore - left.compositeScore;
|
|
7002
|
+
}
|
|
7003
|
+
if (mode === "freshest_targets") {
|
|
7004
|
+
return right.freshnessScore - left.freshnessScore || right.compositeScore - left.compositeScore;
|
|
7005
|
+
}
|
|
7006
|
+
if (mode === "lowest_competition") {
|
|
7007
|
+
return right.competitionScore - left.competitionScore || right.compositeScore - left.compositeScore;
|
|
7008
|
+
}
|
|
7009
|
+
return right.compositeScore - left.compositeScore;
|
|
7010
|
+
}
|
|
6297
7011
|
async function findRepositoryHuntTargets(client, config, input) {
|
|
6298
7012
|
const warnings = [];
|
|
7013
|
+
const repositoryPlatforms = input.platforms.filter((platform) => !isSelfHostedPlatform(platform));
|
|
7014
|
+
const selfHostedRequested = repositoryPlatforms.length !== input.platforms.length;
|
|
7015
|
+
if (input.platforms.length > 0 && repositoryPlatforms.length === 0) {
|
|
7016
|
+
return {
|
|
7017
|
+
request_id: randomUUID(),
|
|
7018
|
+
fetched_at: new Date().toISOString(),
|
|
7019
|
+
source_requests: [],
|
|
7020
|
+
warnings: [
|
|
7021
|
+
"Self-Hosted does not expose repository-change events or target intelligence, so repository-aware hunting is unavailable for this catalog. Use find_hunt_targets for current Self-Hosted program and target inventory."
|
|
7022
|
+
],
|
|
7023
|
+
filters: {
|
|
7024
|
+
platforms: input.platforms,
|
|
7025
|
+
surface_mode: input.surface_mode,
|
|
7026
|
+
ranking_mode: input.ranking_mode,
|
|
7027
|
+
output_mode: input.output_mode
|
|
7028
|
+
},
|
|
7029
|
+
upstream_budget: {
|
|
7030
|
+
initial: input.upstream_request_budget,
|
|
7031
|
+
used: 0,
|
|
7032
|
+
remaining: input.upstream_request_budget
|
|
7033
|
+
},
|
|
7034
|
+
ranking_scope: {
|
|
7035
|
+
mode: "unsupported_catalog",
|
|
7036
|
+
possibly_incomplete: false
|
|
7037
|
+
},
|
|
7038
|
+
programs: [],
|
|
7039
|
+
matches: [],
|
|
7040
|
+
meta: {
|
|
7041
|
+
returned: 0,
|
|
7042
|
+
matched_programs: 0,
|
|
7043
|
+
program_candidates: 0,
|
|
7044
|
+
repository_events_scanned: 0,
|
|
7045
|
+
qualifying_targets_before_result_limit: 0,
|
|
7046
|
+
repository_request_attempts: 0,
|
|
7047
|
+
parallel_source_fetch: false
|
|
7048
|
+
}
|
|
7049
|
+
};
|
|
7050
|
+
}
|
|
7051
|
+
if (selfHostedRequested) {
|
|
7052
|
+
warnings.push("Self-Hosted was excluded from repository-aware matching because that catalog has no repository-change stream; other requested platforms were processed normally.");
|
|
7053
|
+
}
|
|
6299
7054
|
const repositoryRequestReserve = input.upstream_request_budget >= 3 ? 2 : 1;
|
|
6300
7055
|
const programRequestBudget = Math.max(1, input.upstream_request_budget - repositoryRequestReserve);
|
|
6301
7056
|
const programInput = {
|
|
6302
|
-
platforms:
|
|
6303
|
-
exclude_platforms: input.exclude_platforms,
|
|
7057
|
+
platforms: repositoryPlatforms,
|
|
7058
|
+
exclude_platforms: uniqueStrings([...input.exclude_platforms, "Self-Hosted"]),
|
|
6304
7059
|
tags: input.tags,
|
|
6305
7060
|
scope_tags: input.scope_tags,
|
|
6306
7061
|
target_types: input.target_types,
|
|
@@ -6342,7 +7097,7 @@ async function findRepositoryHuntTargets(client, config, input) {
|
|
|
6342
7097
|
};
|
|
6343
7098
|
const [programPayload, repository] = await Promise.all([
|
|
6344
7099
|
findPrograms(client, config, programInput),
|
|
6345
|
-
fetchRepositoryHuntPage(client, input, warnings, repositoryRequestReserve)
|
|
7100
|
+
fetchRepositoryHuntPage(client, { ...input, platforms: repositoryPlatforms }, warnings, repositoryRequestReserve)
|
|
6346
7101
|
]);
|
|
6347
7102
|
const candidatePrograms = readArray(programPayload.programs)
|
|
6348
7103
|
.map(readObject)
|
|
@@ -7153,6 +7908,10 @@ function toProgramCandidate(rawProgram, webBaseUrl) {
|
|
|
7153
7908
|
return candidate;
|
|
7154
7909
|
}
|
|
7155
7910
|
function programCandidateMatches(candidate, filters) {
|
|
7911
|
+
const programStatus = normalizeTag(stringField(candidate.program, "program_status") ?? "");
|
|
7912
|
+
if (readBoolean(candidate.program.is_active) === false || programStatus === "paused") {
|
|
7913
|
+
return false;
|
|
7914
|
+
}
|
|
7156
7915
|
if (filters.platforms.length > 0 && !filters.platforms.map(normalizeTag).includes(normalizeTag(stringField(candidate.program, "platform") ?? ""))) {
|
|
7157
7916
|
return false;
|
|
7158
7917
|
}
|
|
@@ -7478,6 +8237,12 @@ function uniqueStrings(values) {
|
|
|
7478
8237
|
function normalizeTag(value) {
|
|
7479
8238
|
return value.trim().toLowerCase();
|
|
7480
8239
|
}
|
|
8240
|
+
function isSelfHostedPlatform(value) {
|
|
8241
|
+
return normalizeTag(value).replace(/[\s_]+/g, "-") === "self-hosted";
|
|
8242
|
+
}
|
|
8243
|
+
function isSelfHostedOnlyPlatformSelection(platforms) {
|
|
8244
|
+
return platforms.length > 0 && platforms.every(isSelfHostedPlatform);
|
|
8245
|
+
}
|
|
7481
8246
|
function tokenSet(value) {
|
|
7482
8247
|
return new Set(value.split(/[^a-z0-9]+/).filter((token) => token.length >= 2));
|
|
7483
8248
|
}
|
|
@@ -7490,9 +8255,11 @@ function readStringArrayField(value, field) {
|
|
|
7490
8255
|
}
|
|
7491
8256
|
function registerPrompts(server, profile) {
|
|
7492
8257
|
const discoveryToolInstructions = profile === "core"
|
|
7493
|
-
? `- Start with
|
|
7494
|
-
- Express language, target type, reward, Web3, wildcard,
|
|
7495
|
-
|
|
8258
|
+
? `- Start with find_hunt_targets for ranked target/program discovery. Make one call and use its shortlist as final; do not repeat find_programs with alternate sorts or fetch each candidate's targets.
|
|
8259
|
+
- Express language, target type, reward, Web2/Web3, wildcard, competition, freshness, and platform needs through find_hunt_targets filters.
|
|
8260
|
+
- Use find_repository_hunt_targets instead when the user explicitly requires Git or repository activity.`
|
|
8261
|
+
: `- Start with find_hunt_targets for ranked target/program discovery; use its included target samples and recent additions as the final shortlist.
|
|
8262
|
+
- Use find_program_candidates only for lower-level program-only discovery; pass opportunity_levels from the requested value below.
|
|
7496
8263
|
- Use find_language_programs for language asks, find_target_type_programs for API/mobile/source-code/domain asks, find_reward_programs or find_paid_programs for reward asks, find_vdp_programs only for explicit VDP/no-bounty asks, find_web3_contests for Web3 contest/audit asks, find_web3_programs for general Web3 requests, and find_wildcard_programs for wildcard-specific requests.`;
|
|
7497
8264
|
server.registerPrompt("find_best_programs_to_hunt", {
|
|
7498
8265
|
title: "Find Best Programs To Hunt",
|
|
@@ -7507,7 +8274,7 @@ function registerPrompts(server, profile) {
|
|
|
7507
8274
|
|
|
7508
8275
|
Find the best BBRadar program candidates.
|
|
7509
8276
|
${discoveryToolInstructions}
|
|
7510
|
-
-
|
|
8277
|
+
- Treat this opportunity level JSON array as a ranking preference, not a strict first-pass filter: ${promptJson([args.opportunity_level ?? "elite"])}.
|
|
7511
8278
|
- Apply platform filters from this JSON string if provided: ${promptJson(args.platforms ?? "")}.
|
|
7512
8279
|
- Apply tag filters from this JSON string if provided: ${promptJson(args.tags ?? "")}.
|
|
7513
8280
|
- Keep the shortlist to this JSON value: ${promptJson(args.max_programs ?? "10")}.
|
|
@@ -7586,7 +8353,7 @@ function advanceFeedCursor(store, cursor, rawMeta) {
|
|
|
7586
8353
|
stored_in_memory: true
|
|
7587
8354
|
};
|
|
7588
8355
|
}
|
|
7589
|
-
async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudgetMs, fetchPage) {
|
|
8356
|
+
async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudgetMs, initialSnapshot, fetchPage, refreshInventory) {
|
|
7590
8357
|
const startedAt = Date.now();
|
|
7591
8358
|
const results = [];
|
|
7592
8359
|
const sourceRequests = [];
|
|
@@ -7598,9 +8365,110 @@ async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudget
|
|
|
7598
8365
|
let hasMore = false;
|
|
7599
8366
|
let pageBudgetExhausted = false;
|
|
7600
8367
|
let timeBudgetExhausted = false;
|
|
8368
|
+
let gapRecovery;
|
|
8369
|
+
let activeSnapshot = initialSnapshot;
|
|
7601
8370
|
do {
|
|
7602
|
-
|
|
8371
|
+
let api;
|
|
8372
|
+
try {
|
|
8373
|
+
api = await fetchPage(currentCursor.afterId, undefined, activeSnapshot);
|
|
8374
|
+
feedRecoveryProgress.get(store)?.delete(currentCursor.key);
|
|
8375
|
+
}
|
|
8376
|
+
catch (error) {
|
|
8377
|
+
if (!(error instanceof BBRadarApiError) ||
|
|
8378
|
+
error.status !== 410 ||
|
|
8379
|
+
currentCursor.afterId === undefined ||
|
|
8380
|
+
activeSnapshot !== undefined ||
|
|
8381
|
+
!refreshInventory) {
|
|
8382
|
+
throw error;
|
|
8383
|
+
}
|
|
8384
|
+
const recoveryNonce = `${Date.now()}-${currentCursor.afterId}`;
|
|
8385
|
+
const markerApi = await fetchPage(undefined, `${recoveryNonce}-marker`);
|
|
8386
|
+
const markerData = readObject(markerApi.data);
|
|
8387
|
+
const markerAfterId = feedBaselineAfterId(markerData);
|
|
8388
|
+
let progress = feedRecoveryProgress.get(store)?.get(currentCursor.key);
|
|
8389
|
+
let restartedForDrift = false;
|
|
8390
|
+
if (progress && (progress.previousAfterId !== currentCursor.afterId ||
|
|
8391
|
+
progress.baselineAfterId !== markerAfterId)) {
|
|
8392
|
+
feedRecoveryProgress.get(store)?.delete(currentCursor.key);
|
|
8393
|
+
progress = undefined;
|
|
8394
|
+
restartedForDrift = true;
|
|
8395
|
+
}
|
|
8396
|
+
const beforeAfterId = progress?.baselineAfterId ?? markerAfterId;
|
|
8397
|
+
const startPage = progress?.nextPage ?? 1;
|
|
8398
|
+
const inventoryPart = await refreshInventory(startPage, `${recoveryNonce}-inventory`);
|
|
8399
|
+
const inventory = {
|
|
8400
|
+
...inventoryPart,
|
|
8401
|
+
rows_observed: (progress?.rowsObserved ?? 0) + (readNumber(inventoryPart.rows_observed) ?? 0),
|
|
8402
|
+
pages_fetched: (progress?.pagesFetched ?? 0) + (readNumber(inventoryPart.pages_fetched) ?? 0),
|
|
8403
|
+
resumed_from_page: startPage,
|
|
8404
|
+
restarted_for_feed_drift: restartedForDrift
|
|
8405
|
+
};
|
|
8406
|
+
const inventoryComplete = readBoolean(inventory.complete) === true;
|
|
8407
|
+
const endpointPageLimitReached = readBoolean(inventory.endpoint_page_limit_reached) === true;
|
|
8408
|
+
const afterApi = inventoryComplete ? await fetchPage(undefined, `${recoveryNonce}-after`) : markerApi;
|
|
8409
|
+
const afterData = readObject(afterApi.data);
|
|
8410
|
+
const afterAfterId = feedBaselineAfterId(afterData);
|
|
8411
|
+
const baselineStable = inventoryComplete && beforeAfterId !== undefined && beforeAfterId === afterAfterId;
|
|
8412
|
+
const nextAfterId = baselineStable ? beforeAfterId : currentCursor.afterId;
|
|
8413
|
+
if (baselineStable) {
|
|
8414
|
+
store.set(currentCursor.key, nextAfterId);
|
|
8415
|
+
feedRecoveryProgress.get(store)?.delete(currentCursor.key);
|
|
8416
|
+
}
|
|
8417
|
+
else if (!inventoryComplete && !endpointPageLimitReached && beforeAfterId !== undefined) {
|
|
8418
|
+
// Persist only the unchanged expired bookmark so resume_cursor can
|
|
8419
|
+
// continue recovery without treating partial inventory as a baseline.
|
|
8420
|
+
store.set(currentCursor.key, currentCursor.afterId);
|
|
8421
|
+
setRecoveryCheckpoint(store, currentCursor.key, {
|
|
8422
|
+
previousAfterId: currentCursor.afterId,
|
|
8423
|
+
baselineAfterId: beforeAfterId,
|
|
8424
|
+
nextPage: readNumber(inventoryPart.next_page) ?? startPage,
|
|
8425
|
+
rowsObserved: readNumber(inventory.rows_observed) ?? 0,
|
|
8426
|
+
pagesFetched: readNumber(inventory.pages_fetched) ?? 0
|
|
8427
|
+
});
|
|
8428
|
+
}
|
|
8429
|
+
else {
|
|
8430
|
+
// A failed first explicit recovery must remain resumable even when no
|
|
8431
|
+
// safe inventory checkpoint can be retained.
|
|
8432
|
+
store.set(currentCursor.key, currentCursor.afterId);
|
|
8433
|
+
feedRecoveryProgress.get(store)?.delete(currentCursor.key);
|
|
8434
|
+
}
|
|
8435
|
+
gapRecovery = {
|
|
8436
|
+
historical_gap: true,
|
|
8437
|
+
current_inventory_refreshed: inventoryComplete,
|
|
8438
|
+
recovery_complete: baselineStable,
|
|
8439
|
+
retained_previous_cursor: !baselineStable,
|
|
8440
|
+
incomplete_reason: !inventoryComplete
|
|
8441
|
+
? endpointPageLimitReached ? "inventory_endpoint_page_limit_reached" : "inventory_page_or_time_budget_exhausted"
|
|
8442
|
+
: beforeAfterId === undefined
|
|
8443
|
+
? "feed_baseline_unavailable"
|
|
8444
|
+
: beforeAfterId !== afterAfterId
|
|
8445
|
+
? "feed_changed_during_inventory_refresh"
|
|
8446
|
+
: undefined,
|
|
8447
|
+
inventory,
|
|
8448
|
+
next_inventory_page: baselineStable ? undefined : readNumber(inventoryPart.next_page),
|
|
8449
|
+
previous_after_id: currentCursor.afterId,
|
|
8450
|
+
baseline_after_id: baselineStable ? nextAfterId : undefined,
|
|
8451
|
+
baseline_events_emitted: 0,
|
|
8452
|
+
limitation: "Changes retired before recovery cannot be reconstructed."
|
|
8453
|
+
};
|
|
8454
|
+
lastApi = afterApi;
|
|
8455
|
+
lastData = afterData;
|
|
8456
|
+
cursorPayload = {
|
|
8457
|
+
endpoint: currentCursor.endpoint,
|
|
8458
|
+
mode: baselineStable ? "recovered_baseline" : "recovery_incomplete",
|
|
8459
|
+
used_after_id: currentCursor.afterId,
|
|
8460
|
+
next_after_id: nextAfterId,
|
|
8461
|
+
has_more: false,
|
|
8462
|
+
stored_in_memory: true
|
|
8463
|
+
};
|
|
8464
|
+
pagesFetched += 1;
|
|
8465
|
+
break;
|
|
8466
|
+
}
|
|
7603
8467
|
const data = readObject(api.data);
|
|
8468
|
+
const returnedSnapshot = readString(readObject(data?.meta)?.snapshot);
|
|
8469
|
+
if (activeSnapshot === "start" && returnedSnapshot) {
|
|
8470
|
+
activeSnapshot = returnedSnapshot;
|
|
8471
|
+
}
|
|
7604
8472
|
const pageResults = readArray(data?.results);
|
|
7605
8473
|
lastApi = api;
|
|
7606
8474
|
lastData = data;
|
|
@@ -7643,12 +8511,61 @@ async function collectCursorFeedPages(store, initialCursor, maxPages, timeBudget
|
|
|
7643
8511
|
pagesFetched,
|
|
7644
8512
|
pageBudgetExhausted,
|
|
7645
8513
|
timeBudgetExhausted,
|
|
7646
|
-
sourceRequests
|
|
8514
|
+
sourceRequests,
|
|
8515
|
+
gapRecovery
|
|
8516
|
+
};
|
|
8517
|
+
}
|
|
8518
|
+
const feedRecoveryProgress = new WeakMap();
|
|
8519
|
+
function recoveryProgressFor(store) {
|
|
8520
|
+
let progress = feedRecoveryProgress.get(store);
|
|
8521
|
+
if (!progress) {
|
|
8522
|
+
progress = new Map();
|
|
8523
|
+
feedRecoveryProgress.set(store, progress);
|
|
8524
|
+
}
|
|
8525
|
+
return progress;
|
|
8526
|
+
}
|
|
8527
|
+
function setRecoveryCheckpoint(store, key, checkpoint) {
|
|
8528
|
+
const progress = recoveryProgressFor(store);
|
|
8529
|
+
if (!progress.has(key) && progress.size >= 500) {
|
|
8530
|
+
const oldest = progress.keys().next().value;
|
|
8531
|
+
if (oldest !== undefined)
|
|
8532
|
+
progress.delete(oldest);
|
|
8533
|
+
}
|
|
8534
|
+
progress.delete(key);
|
|
8535
|
+
progress.set(key, checkpoint);
|
|
8536
|
+
}
|
|
8537
|
+
function feedBaselineAfterId(data) {
|
|
8538
|
+
const metaAfterId = readNumber(readObject(data?.meta)?.next_after_id);
|
|
8539
|
+
const ids = readArray(data?.results)
|
|
8540
|
+
.map((entry) => readNumber(readObject(entry)?.id))
|
|
8541
|
+
.filter((value) => value !== undefined);
|
|
8542
|
+
return ids.length > 0 ? Math.max(metaAfterId ?? 0, ...ids) : metaAfterId;
|
|
8543
|
+
}
|
|
8544
|
+
async function refreshInventoryBaseline(fetchPage, collection, maxPages, timeBudgetMs, startPage = 1, recoveryProbe = "inventory") {
|
|
8545
|
+
const startedAt = Date.now();
|
|
8546
|
+
let page = startPage;
|
|
8547
|
+
let rows = 0;
|
|
8548
|
+
let hasMore = false;
|
|
8549
|
+
do {
|
|
8550
|
+
const api = await fetchPage(page, `${recoveryProbe}-${page}`);
|
|
8551
|
+
const data = readObject(api.data);
|
|
8552
|
+
rows += readArray(data?.[collection]).length;
|
|
8553
|
+
hasMore = readBoolean(readObject(data?.meta)?.has_more) === true;
|
|
8554
|
+
page += 1;
|
|
8555
|
+
} while (hasMore && page < startPage + maxPages && Date.now() - startedAt < timeBudgetMs);
|
|
8556
|
+
return {
|
|
8557
|
+
collection,
|
|
8558
|
+
rows_observed: rows,
|
|
8559
|
+
pages_fetched: page - startPage,
|
|
8560
|
+
next_page: hasMore && page <= MAX_UPSTREAM_PAGE ? page : undefined,
|
|
8561
|
+
endpoint_page_limit_reached: hasMore && page > MAX_UPSTREAM_PAGE,
|
|
8562
|
+
complete: !hasMore,
|
|
8563
|
+
bounded: true
|
|
7647
8564
|
};
|
|
7648
8565
|
}
|
|
7649
8566
|
function normalizeFeedCursorFilters(filters) {
|
|
7650
8567
|
return Object.fromEntries(Object.entries(filters)
|
|
7651
|
-
.filter(([key, value]) => key !== "after_id" && key !== "page" && value !== undefined)
|
|
8568
|
+
.filter(([key, value]) => key !== "after_id" && key !== "page" && key !== "snapshot" && value !== undefined)
|
|
7652
8569
|
.map(([key, value]) => [key, normalizeFeedCursorFilterValue(value)]));
|
|
7653
8570
|
}
|
|
7654
8571
|
function normalizeFeedCursorFilterValue(value) {
|
|
@@ -7855,6 +8772,11 @@ function splitFilterList(value) {
|
|
|
7855
8772
|
function toQuery(values) {
|
|
7856
8773
|
return compactRecord(values);
|
|
7857
8774
|
}
|
|
8775
|
+
function assertStableSnapshotSupported(client, endpoint, snapshot) {
|
|
8776
|
+
if (snapshot !== undefined && !client.supportsStableSnapshot(endpoint)) {
|
|
8777
|
+
throw new Error(`The negotiated BBRadar API contract does not advertise stable snapshots for ${endpoint}. Omit snapshot to use live pagination.`);
|
|
8778
|
+
}
|
|
8779
|
+
}
|
|
7858
8780
|
function compactRecord(values) {
|
|
7859
8781
|
return Object.fromEntries(Object.entries(values).filter(([, value]) => {
|
|
7860
8782
|
if (value === undefined || value === null) {
|
|
@@ -7903,6 +8825,10 @@ function compactTarget(target) {
|
|
|
7903
8825
|
in_scope: readBoolean(target.in_scope),
|
|
7904
8826
|
eligible_for_bounty: readBoolean(target.eligible_for_bounty),
|
|
7905
8827
|
wildcard: readBoolean(target.wildcard),
|
|
8828
|
+
path_kind: stringField(target, "path_kind"),
|
|
8829
|
+
path_prefix: stringField(target, "path_prefix"),
|
|
8830
|
+
match_quality: stringField(target, "match_quality"),
|
|
8831
|
+
match_reason: stringField(target, "match_reason"),
|
|
7906
8832
|
target_opportunity_score: readNumber(target.target_opportunity_score),
|
|
7907
8833
|
target_opportunity_label: stringField(target, "target_opportunity_label"),
|
|
7908
8834
|
competition_risk: stringField(target, "competition_risk"),
|