@bbradar/mcp 0.1.3

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/dist/server.js ADDED
@@ -0,0 +1,4418 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { z } from "zod";
4
+ import { BBRadarApiError } from "./bbradarClient.js";
5
+ import { stripUndefined } from "./json.js";
6
+ 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.1.0";
9
+ const MAX_PROGRAMS_PER_SEARCH = 100;
10
+ const MAX_TARGET_EXPORT = 500;
11
+ const MAX_RECENT_CHANGES = 100;
12
+ const MAX_TARGETS_PER_PROGRAM = 500;
13
+ const MAX_WILDCARD_SEARCH_PAGES = 10;
14
+ const MAX_WILDCARD_RESULTS = 25;
15
+ const MAX_WILDCARD_TARGET_SAMPLE_PROGRAMS = 5;
16
+ const MAX_WILDCARD_TARGET_SAMPLES = 10;
17
+ const MAX_FIND_PROGRAM_PAGES = 10;
18
+ const MAX_FIND_PROGRAM_RESULTS = 50;
19
+ const MAX_FIND_TARGET_SAMPLE_PROGRAMS = 10;
20
+ const MAX_FIND_TARGET_SAMPLES = 20;
21
+ const MAX_FIND_UPSTREAM_REQUEST_BUDGET = 50;
22
+ const DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET = 20;
23
+ const MAX_ACCEPTED_NUMERIC_LIMIT = 1_000;
24
+ const PROGRAM_COLLECTION_CONCURRENCY = 4;
25
+ const TARGET_SAMPLE_CONCURRENCY = 3;
26
+ const DEFAULT_FILTER_DISCOVERY_PAGES = 2;
27
+ const MAX_FILTER_DISCOVERY_PAGES = 5;
28
+ const DEFAULT_FILTER_TARGET_SAMPLE_PROGRAMS = 10;
29
+ const MAX_FILTER_TARGET_SAMPLE_PROGRAMS = 25;
30
+ const MAX_LOCAL_EXPORT_RESOURCES = 25;
31
+ const EXPORT_PREVIEW_LIMIT = 25;
32
+ const SDK_VERSION = "1.29.0";
33
+ const WEB3_TAGS = ["web3", "crypto", "blockchain", "smart-contract", "smart contract", "defi", "nft", "ethereum", "solana", "solidity"];
34
+ const WEB3_CONTEST_SIGNALS = ["contest", "audit", "competitive", "code4rena", "sherlock", "cantina", "codehawks", "hats", "spearbit", "warden"];
35
+ const VDP_SIGNALS = ["vdp", "disclosure", "responsible disclosure", "vulnerability disclosure", "no bounty", "non-bounty", "no reward"];
36
+ const targetListModeSchema = z.enum(["identifiers", "compact", "full"]);
37
+ const changeListModeSchema = z.enum(["compact", "full"]);
38
+ const programListModeSchema = z.enum(["compact", "full"]);
39
+ const rewardThresholdModeSchema = z.enum(["max_at_least", "min_at_least"]);
40
+ const vdpModeSchema = z.enum(["include", "exclude", "only_likely", "only_known_no_bounty"]);
41
+ const targetMatchModeSchema = z.enum(["contains", "exact", "suffix", "wildcard"]);
42
+ const programNameActionSchema = z.enum(["new_targets", "scope_delta", "brief", "target_breakdown"]);
43
+ const readOnlyAnnotations = {
44
+ readOnlyHint: true,
45
+ destructiveHint: false,
46
+ idempotentHint: true,
47
+ openWorldHint: true
48
+ };
49
+ const TOOL_METRICS = new Map();
50
+ const filterValueSchema = z
51
+ .string()
52
+ .trim()
53
+ .min(1)
54
+ .max(128)
55
+ .refine((value) => !/[,\u0000-\u001F\u007F]/.test(value), {
56
+ message: "Filter values cannot contain commas or control characters."
57
+ });
58
+ const stringListSchema = z
59
+ .array(filterValueSchema)
60
+ .max(50)
61
+ .default([])
62
+ .describe("Filter values.");
63
+ const searchTextSchema = z
64
+ .string()
65
+ .trim()
66
+ .min(2)
67
+ .max(128)
68
+ .refine((value) => !/[\u0000-\u001F\u007F]/.test(value), {
69
+ message: "Search text cannot contain control characters."
70
+ });
71
+ const isoDateTimeSchema = z.string().datetime({ offset: true });
72
+ const pageSchema = z.number().int().min(1).default(1);
73
+ const programsPageSizeSchema = z.number().int().min(1).max(MAX_PROGRAMS_PER_SEARCH).default(20);
74
+ const recentChangesPageSizeSchema = z.number().int().min(1).max(MAX_RECENT_CHANGES).default(50);
75
+ const opportunityLevelSchema = z.enum(["elite", "hot", "strong", "potential"]);
76
+ const programSortSchema = z
77
+ .enum([
78
+ "best_match",
79
+ "lowest_reports",
80
+ "highest_reward",
81
+ "most_wildcards",
82
+ "most_eligible_targets",
83
+ "freshest",
84
+ "recently_updated",
85
+ "highest_opportunity_score",
86
+ "most_new_targets",
87
+ "best_hunt_value"
88
+ ])
89
+ .default("best_match");
90
+ const programIdSchema = z
91
+ .string()
92
+ .trim()
93
+ .min(3)
94
+ .max(256)
95
+ .regex(/^[A-Za-z0-9_-]+:[^?#]+$/, "Use platform:handle, for example HackerOne:example-handle.")
96
+ .refine((value) => !/[\u0000-\u001F\u007F]/.test(value), {
97
+ message: "program_id cannot contain control characters."
98
+ })
99
+ .describe("platform:handle id.");
100
+ const jsonRecordSchema = z.record(z.unknown());
101
+ const rateLimitOutputSchema = z.object({
102
+ remaining: z.number().int().min(0),
103
+ reset_at: z.string()
104
+ });
105
+ const toolTimingOutputSchema = z.object({
106
+ tool_name: z.string(),
107
+ duration_ms: z.number().int().min(0)
108
+ });
109
+ const cacheOutputSchema = z.object({
110
+ hit: z.boolean(),
111
+ coalesced_live_request: z.boolean().optional(),
112
+ expires_at: z.string().optional()
113
+ });
114
+ const errorOutputSchema = z.object({
115
+ message: z.string(),
116
+ status: z.number().int().optional(),
117
+ detail: z.unknown().optional(),
118
+ errors: z.unknown().optional()
119
+ });
120
+ const programOutputSchema = jsonRecordSchema;
121
+ const targetOutputSchema = jsonRecordSchema;
122
+ const apiEnvelopeOutputShape = {
123
+ request_id: z.string(),
124
+ upstream_request_id: z.string().optional(),
125
+ fetched_at: z.string().optional(),
126
+ cache: cacheOutputSchema.optional(),
127
+ mcp_rate_limit: rateLimitOutputSchema.optional(),
128
+ mcp_timing: toolTimingOutputSchema.optional(),
129
+ error: errorOutputSchema.optional()
130
+ };
131
+ const programListOutputShape = {
132
+ ...apiEnvelopeOutputShape,
133
+ programs: z.array(programOutputSchema).optional(),
134
+ meta: jsonRecordSchema.optional()
135
+ };
136
+ const findProgramsOutputShape = {
137
+ ...programListOutputShape,
138
+ source_requests: z.array(jsonRecordSchema).optional(),
139
+ filters: jsonRecordSchema.optional(),
140
+ warnings: z.array(z.string()).optional(),
141
+ errors: z.array(jsonRecordSchema).optional(),
142
+ upstream_budget: jsonRecordSchema.optional(),
143
+ ranking_scope: jsonRecordSchema.optional()
144
+ };
145
+ const decisionOutputShape = {
146
+ ...findProgramsOutputShape,
147
+ partial_success: z.boolean().optional(),
148
+ program_ids_requested: z.array(z.string()).optional(),
149
+ failed_program_ids: z.array(z.string()).optional(),
150
+ compared_programs: z.array(programOutputSchema).optional(),
151
+ activity: jsonRecordSchema.optional(),
152
+ changes: z.array(jsonRecordSchema).optional()
153
+ };
154
+ const programCandidatePresetInputSchema = {
155
+ platforms: stringListSchema,
156
+ tags: stringListSchema,
157
+ scope_tags: stringListSchema,
158
+ target_types: stringListSchema,
159
+ language_tags: stringListSchema,
160
+ opportunity_levels: z.array(opportunityLevelSchema).max(4).default(["elite", "hot", "strong", "potential"]),
161
+ web3: z.boolean().default(false),
162
+ wildcard: z.boolean().default(false),
163
+ low_reports: z.boolean().default(true),
164
+ bounty: z.boolean().default(true),
165
+ max_public_report_count: z.number().int().min(0).default(15),
166
+ min_bounty_max: z.number().int().min(0).optional(),
167
+ min_eligible_targets: z.number().int().min(0).default(1),
168
+ fresh_launch_days: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).optional(),
169
+ has_api_scope: z.boolean().default(false),
170
+ exclude_ctf: z.boolean().default(true),
171
+ exclude_mobile_only: z.boolean().default(false),
172
+ exclude_non_web: z.boolean().default(false),
173
+ updated_since: isoDateTimeSchema.optional(),
174
+ max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(5),
175
+ max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
176
+ include_target_samples: z.boolean().default(true),
177
+ target_sample_programs: z.number().int().min(0).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(5),
178
+ target_sample_size: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(5),
179
+ output_mode: programListModeSchema.default("compact"),
180
+ upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(30)
181
+ };
182
+ export function createBbradarServer(client, config) {
183
+ const server = new McpServer({
184
+ name: "bbradar.io",
185
+ version: SERVER_VERSION
186
+ }, {
187
+ instructions: [
188
+ "Use BBRadar only for passive bug bounty intelligence; never scan or contact targets.",
189
+ "For BBRadar asks, use these tools first and avoid external skills unless methodology is explicitly requested.",
190
+ "Name + action: run_program_name_action. Name only: resolve_program.",
191
+ "Freshness: get_latest_added_targets, check_program_new_targets, check_watchlist_new_targets, find_recent_by_type, get_program_scope_delta.",
192
+ "Discovery: find_program_candidates, 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.",
193
+ "Details: get_program_brief, get_program_scope_summary, get_program_target_breakdown, find_programs_by_target, compare_programs_compact.",
194
+ "Prefer compact/identifier outputs and disclose sampled ranking scope when present."
195
+ ].join("\n")
196
+ });
197
+ const rateLimiter = new FixedWindowRateLimiter();
198
+ const exportStore = new Map();
199
+ server.registerTool("search_programs", {
200
+ title: "Search BBRadar Programs",
201
+ description: `List active programs. page_size max ${MAX_PROGRAMS_PER_SEARCH}.`,
202
+ inputSchema: {
203
+ platforms: stringListSchema,
204
+ tags: stringListSchema,
205
+ updated_since: isoDateTimeSchema.optional().describe("ISO datetime."),
206
+ page: pageSchema,
207
+ page_size: programsPageSizeSchema
208
+ },
209
+ outputSchema: programListOutputShape,
210
+ annotations: readOnlyAnnotations
211
+ }, (args) => runTool("search_programs", config, rateLimiter, async () => {
212
+ const api = await client.listPrograms(toQuery(args));
213
+ const data = readObject(api.data);
214
+ const programs = readArray(data?.programs).map((program) => addProgramResourceLinks(sanitizeProgram(program, config.webBaseUrl)));
215
+ return withApiMetadata(api, {
216
+ programs,
217
+ meta: sanitizeJson(data?.meta)
218
+ });
219
+ }));
220
+ server.registerTool("get_program", {
221
+ title: "Get BBRadar Program",
222
+ description: "Get one program by id.",
223
+ inputSchema: {
224
+ program_id: programIdSchema
225
+ },
226
+ outputSchema: {
227
+ ...apiEnvelopeOutputShape,
228
+ program: programOutputSchema.optional()
229
+ },
230
+ annotations: readOnlyAnnotations
231
+ }, (args) => runTool("get_program", config, rateLimiter, async () => {
232
+ const api = await client.getProgram(args.program_id);
233
+ return withApiMetadata(api, {
234
+ program: addProgramResourceLinks(sanitizeProgram(api.data, config.webBaseUrl))
235
+ });
236
+ }));
237
+ server.registerTool("resolve_program", {
238
+ title: "Resolve Program",
239
+ description: "Resolve name/handle text to likely program_ids.",
240
+ inputSchema: {
241
+ query: searchTextSchema,
242
+ platforms: stringListSchema,
243
+ tags: stringListSchema,
244
+ opportunity_levels: z.array(opportunityLevelSchema).max(4).default([]),
245
+ max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
246
+ max_results: z.number().int().min(1).max(25).default(10),
247
+ output_mode: programListModeSchema.default("compact"),
248
+ upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
249
+ },
250
+ outputSchema: {
251
+ ...programListOutputShape,
252
+ source_requests: z.array(jsonRecordSchema).optional(),
253
+ query: jsonRecordSchema.optional(),
254
+ matches: z.array(jsonRecordSchema).optional(),
255
+ warnings: z.array(z.string()).optional(),
256
+ upstream_budget: jsonRecordSchema.optional(),
257
+ ranking_scope: jsonRecordSchema.optional()
258
+ },
259
+ annotations: readOnlyAnnotations
260
+ }, (args) => runTool("resolve_program", config, rateLimiter, async () => resolveProgram(client, config, args)));
261
+ server.registerTool("run_program_name_action", {
262
+ title: "Run Program Name Action",
263
+ description: "Resolve a name, then run new_targets, scope_delta, brief, or target_breakdown.",
264
+ inputSchema: {
265
+ query: searchTextSchema,
266
+ action: programNameActionSchema.default("new_targets"),
267
+ platforms: stringListSchema,
268
+ tags: stringListSchema,
269
+ opportunity_levels: z.array(opportunityLevelSchema).max(4).default([]),
270
+ since: isoDateTimeSchema.optional(),
271
+ target_type: filterValueSchema.optional(),
272
+ language_tags: stringListSchema,
273
+ include_out_of_scope: z.boolean().default(false),
274
+ include_ineligible: z.boolean().default(false),
275
+ page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
276
+ max_targets: z.number().int().min(1).max(MAX_RECENT_CHANGES).default(25),
277
+ group_limit: z.number().int().min(1).max(MAX_TARGETS_PER_PROGRAM).default(100),
278
+ target_list_mode: targetListModeSchema.default("identifiers"),
279
+ max_resolve_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
280
+ output_mode: programListModeSchema.default("compact"),
281
+ upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
282
+ },
283
+ outputSchema: {
284
+ ...apiEnvelopeOutputShape,
285
+ source_requests: z.array(jsonRecordSchema).optional(),
286
+ resolved_program: jsonRecordSchema.optional(),
287
+ action: z.string().optional(),
288
+ result: jsonRecordSchema.optional(),
289
+ warnings: z.array(z.string()).optional(),
290
+ upstream_budget: jsonRecordSchema.optional(),
291
+ ranking_scope: jsonRecordSchema.optional()
292
+ },
293
+ annotations: readOnlyAnnotations
294
+ }, (args) => runTool("run_program_name_action", config, rateLimiter, async () => runProgramNameAction(client, config, args)));
295
+ server.registerTool("get_program_targets", {
296
+ title: "Get Program Targets",
297
+ description: `Get active targets for one program. limit max ${MAX_TARGETS_PER_PROGRAM}.`,
298
+ inputSchema: {
299
+ program_id: programIdSchema,
300
+ include_out_of_scope: z.boolean().default(false),
301
+ include_ineligible: z.boolean().default(false),
302
+ strict_scope_filter: z.boolean().default(true),
303
+ offset: z.number().int().min(0).default(0),
304
+ limit: z.number().int().min(1).max(MAX_TARGETS_PER_PROGRAM).default(100),
305
+ output_mode: targetListModeSchema.default("compact")
306
+ },
307
+ outputSchema: {
308
+ ...apiEnvelopeOutputShape,
309
+ program_id: z.string().optional(),
310
+ targets: z.array(z.unknown()).optional(),
311
+ meta: jsonRecordSchema.optional()
312
+ },
313
+ annotations: readOnlyAnnotations
314
+ }, (args) => runTool("get_program_targets", config, rateLimiter, async () => {
315
+ const api = await client.getProgramTargets(args.program_id);
316
+ const data = readObject(api.data);
317
+ const rawTargets = readArray(data?.targets);
318
+ const sanitizedTargets = rawTargets.map(sanitizeTarget).filter((target) => targetHasAllowedScope(target, args));
319
+ const targets = sanitizedTargets.slice(args.offset, args.offset + args.limit);
320
+ return withApiMetadata(api, {
321
+ program_id: args.program_id,
322
+ targets: formatTargetList(targets, args.output_mode),
323
+ meta: {
324
+ offset: args.offset,
325
+ limit: args.limit,
326
+ output_mode: args.output_mode,
327
+ total_active_targets: rawTargets.length,
328
+ total_after_filters: sanitizedTargets.length,
329
+ returned: targets.length,
330
+ has_more: args.offset + args.limit < sanitizedTargets.length
331
+ }
332
+ });
333
+ }));
334
+ server.registerTool("get_recent_changes", {
335
+ title: "Get Recent Target Changes",
336
+ description: `List recent target changes. page_size max ${MAX_RECENT_CHANGES}.`,
337
+ inputSchema: {
338
+ change_type: z.enum(["added", "updated", "removed"]).optional(),
339
+ include_removed: z.boolean().default(false),
340
+ include_ineligible: z.boolean().default(false),
341
+ include_out_of_scope: z.boolean().default(false),
342
+ search: searchTextSchema.optional(),
343
+ platforms: stringListSchema,
344
+ tags: stringListSchema,
345
+ page: pageSchema,
346
+ page_size: recentChangesPageSizeSchema,
347
+ output_mode: changeListModeSchema.default("compact")
348
+ },
349
+ outputSchema: {
350
+ ...apiEnvelopeOutputShape,
351
+ changes: z.array(jsonRecordSchema).optional(),
352
+ meta: jsonRecordSchema.optional(),
353
+ facets: jsonRecordSchema.optional()
354
+ },
355
+ annotations: readOnlyAnnotations
356
+ }, (args) => runTool("get_recent_changes", config, rateLimiter, async () => {
357
+ const { output_mode, ...filters } = args;
358
+ const api = await client.getRecentChanges(toQuery(filters));
359
+ const data = readObject(api.data);
360
+ const sanitizedMeta = readObject(sanitizeJson(data?.meta));
361
+ const changes = readArray(data?.results)
362
+ .map((change) => sanitizeChange(change, config.webBaseUrl))
363
+ .map((change) => formatChange(change, output_mode));
364
+ return withApiMetadata(api, {
365
+ changes,
366
+ meta: stripUndefined({
367
+ ...(sanitizedMeta ?? {}),
368
+ output_mode
369
+ }),
370
+ facets: sanitizeJson(data?.facets)
371
+ });
372
+ }));
373
+ server.registerTool("get_latest_added_targets", {
374
+ title: "Get Latest Added Targets",
375
+ description: "Latest added targets by type, optionally with that program's target list.",
376
+ inputSchema: {
377
+ target_type: filterValueSchema.default("domain"),
378
+ include_out_of_scope: z.boolean().default(false).describe("Include out-of-scope."),
379
+ include_ineligible: z.boolean().default(false).describe("Include ineligible."),
380
+ recent_changes_page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
381
+ include_full_target_list: z.boolean().default(true),
382
+ full_target_list_mode: targetListModeSchema.default("identifiers"),
383
+ full_target_limit: z.number().int().min(1).max(MAX_TARGETS_PER_PROGRAM).default(MAX_TARGETS_PER_PROGRAM),
384
+ full_list_include_out_of_scope: z.boolean().default(true),
385
+ full_list_include_ineligible: z.boolean().default(true)
386
+ },
387
+ outputSchema: {
388
+ ...apiEnvelopeOutputShape,
389
+ source_requests: z.array(jsonRecordSchema).optional(),
390
+ query: jsonRecordSchema.optional(),
391
+ latest_added_at: z.string().optional(),
392
+ program: programOutputSchema.optional(),
393
+ new_targets: z.array(z.unknown()).optional(),
394
+ full_targets: z.array(z.unknown()).optional(),
395
+ meta: jsonRecordSchema.optional()
396
+ },
397
+ annotations: readOnlyAnnotations
398
+ }, (args) => runTool("get_latest_added_targets", config, rateLimiter, async () => getLatestAddedTargets(client, config, args)));
399
+ server.registerTool("get_program_scope_summary", {
400
+ title: "Get Program Scope Summary",
401
+ description: "Grouped compact scope for one program.",
402
+ inputSchema: {
403
+ program_id: programIdSchema,
404
+ target_list_mode: targetListModeSchema.default("identifiers"),
405
+ group_limit: z.number().int().min(1).max(MAX_TARGETS_PER_PROGRAM).default(MAX_TARGETS_PER_PROGRAM),
406
+ include_out_of_scope: z.boolean().default(true),
407
+ include_ineligible: z.boolean().default(true)
408
+ },
409
+ outputSchema: {
410
+ ...apiEnvelopeOutputShape,
411
+ source_requests: z.array(jsonRecordSchema).optional(),
412
+ program: programOutputSchema.optional(),
413
+ scope: jsonRecordSchema.optional(),
414
+ meta: jsonRecordSchema.optional()
415
+ },
416
+ annotations: readOnlyAnnotations
417
+ }, (args) => runTool("get_program_scope_summary", config, rateLimiter, async () => getProgramScopeSummary(client, config, args)));
418
+ server.registerTool("get_program_target_breakdown", {
419
+ title: "Get Program Target Breakdown",
420
+ description: "Target type, scope tag, language tag, and bucket counts for one program.",
421
+ inputSchema: {
422
+ program_id: programIdSchema,
423
+ target_list_mode: targetListModeSchema.default("identifiers"),
424
+ group_limit: z.number().int().min(1).max(MAX_TARGETS_PER_PROGRAM).default(100),
425
+ include_out_of_scope: z.boolean().default(true),
426
+ include_ineligible: z.boolean().default(true)
427
+ },
428
+ outputSchema: {
429
+ ...apiEnvelopeOutputShape,
430
+ source_requests: z.array(jsonRecordSchema).optional(),
431
+ program: programOutputSchema.optional(),
432
+ breakdown: jsonRecordSchema.optional(),
433
+ meta: jsonRecordSchema.optional()
434
+ },
435
+ annotations: readOnlyAnnotations
436
+ }, (args) => runTool("get_program_target_breakdown", config, rateLimiter, async () => getProgramTargetBreakdown(client, config, args)));
437
+ server.registerTool("get_program_scope_delta", {
438
+ title: "Get Program Scope Delta",
439
+ description: "Compact recent scope changes for one program.",
440
+ inputSchema: {
441
+ program_id: programIdSchema,
442
+ since: isoDateTimeSchema.optional(),
443
+ change_type: z.enum(["added", "updated", "removed"]).optional(),
444
+ target_type: filterValueSchema.optional(),
445
+ language_tags: stringListSchema,
446
+ include_removed: z.boolean().default(true),
447
+ include_out_of_scope: z.boolean().default(false),
448
+ include_ineligible: z.boolean().default(false),
449
+ page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
450
+ max_targets: z.number().int().min(1).max(MAX_RECENT_CHANGES).default(50),
451
+ target_list_mode: targetListModeSchema.default("identifiers")
452
+ },
453
+ outputSchema: {
454
+ ...apiEnvelopeOutputShape,
455
+ source_requests: z.array(jsonRecordSchema).optional(),
456
+ program: programOutputSchema.optional(),
457
+ delta: jsonRecordSchema.optional(),
458
+ changes: z.array(jsonRecordSchema).optional(),
459
+ meta: jsonRecordSchema.optional()
460
+ },
461
+ annotations: readOnlyAnnotations
462
+ }, (args) => runTool("get_program_scope_delta", config, rateLimiter, async () => getProgramScopeDelta(client, config, args)));
463
+ server.registerTool("get_recent_target_activity", {
464
+ title: "Get Recent Target Activity",
465
+ description: "Recent target changes grouped by program.",
466
+ inputSchema: {
467
+ change_type: z.enum(["added", "updated", "removed"]).optional(),
468
+ target_type: filterValueSchema.optional(),
469
+ include_removed: z.boolean().default(true),
470
+ include_out_of_scope: z.boolean().default(false),
471
+ include_ineligible: z.boolean().default(false),
472
+ search: searchTextSchema.optional(),
473
+ platforms: stringListSchema,
474
+ tags: stringListSchema,
475
+ language_tags: stringListSchema,
476
+ page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
477
+ max_programs: z.number().int().min(1).max(50).default(10),
478
+ sample_size: z.number().int().min(1).max(20).default(5),
479
+ target_list_mode: targetListModeSchema.default("identifiers")
480
+ },
481
+ outputSchema: {
482
+ ...apiEnvelopeOutputShape,
483
+ source_requests: z.array(jsonRecordSchema).optional(),
484
+ activity: z.array(jsonRecordSchema).optional(),
485
+ meta: jsonRecordSchema.optional()
486
+ },
487
+ annotations: readOnlyAnnotations
488
+ }, (args) => runTool("get_recent_target_activity", config, rateLimiter, async () => getRecentTargetActivity(client, config, args)));
489
+ server.registerTool("check_program_new_targets", {
490
+ title: "Check Program New Targets",
491
+ description: "Yes/no new in-scope targets for one program.",
492
+ inputSchema: {
493
+ program_id: programIdSchema,
494
+ since: isoDateTimeSchema.optional().describe("ISO datetime."),
495
+ target_type: filterValueSchema.optional(),
496
+ include_ineligible: z.boolean().default(false).describe("Include ineligible."),
497
+ page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
498
+ max_targets: z.number().int().min(1).max(MAX_RECENT_CHANGES).default(25),
499
+ target_list_mode: targetListModeSchema.default("identifiers")
500
+ },
501
+ outputSchema: {
502
+ ...apiEnvelopeOutputShape,
503
+ source_requests: z.array(jsonRecordSchema).optional(),
504
+ program_id: z.string().optional(),
505
+ program: programOutputSchema.optional(),
506
+ has_new_targets: z.boolean().optional(),
507
+ new_target_count: z.number().int().min(0).optional(),
508
+ latest_added_at: z.string().optional(),
509
+ new_targets: z.array(z.unknown()).optional(),
510
+ meta: jsonRecordSchema.optional()
511
+ },
512
+ annotations: readOnlyAnnotations
513
+ }, (args) => runTool("check_program_new_targets", config, rateLimiter, async () => checkProgramNewTargets(client, config, args)));
514
+ server.registerTool("check_watchlist_new_targets", {
515
+ title: "Check Watchlist New Targets",
516
+ description: "New in-scope targets across many known program_ids.",
517
+ inputSchema: {
518
+ program_ids: z.array(programIdSchema).min(1).max(50),
519
+ since: isoDateTimeSchema.optional(),
520
+ target_type: filterValueSchema.optional(),
521
+ include_ineligible: z.boolean().default(false),
522
+ page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
523
+ max_targets_per_program: z.number().int().min(1).max(50).default(10),
524
+ target_list_mode: targetListModeSchema.default("identifiers")
525
+ },
526
+ outputSchema: {
527
+ ...apiEnvelopeOutputShape,
528
+ source_requests: z.array(jsonRecordSchema).optional(),
529
+ programs: z.array(jsonRecordSchema).optional(),
530
+ any_new_targets: z.boolean().optional(),
531
+ meta: jsonRecordSchema.optional()
532
+ },
533
+ annotations: readOnlyAnnotations
534
+ }, (args) => runTool("check_watchlist_new_targets", config, rateLimiter, async () => checkWatchlistNewTargets(client, config, args)));
535
+ server.registerTool("get_opportunities", {
536
+ title: "Get BBRadar Opportunities",
537
+ description: `List programs in one opportunity tier. page_size max ${MAX_PROGRAMS_PER_SEARCH}.`,
538
+ inputSchema: {
539
+ level: opportunityLevelSchema.default("elite"),
540
+ platforms: stringListSchema,
541
+ tags: stringListSchema,
542
+ updated_since: isoDateTimeSchema.optional(),
543
+ page: pageSchema,
544
+ page_size: programsPageSizeSchema
545
+ },
546
+ outputSchema: {
547
+ ...programListOutputShape,
548
+ level: opportunityLevelSchema.optional()
549
+ },
550
+ annotations: readOnlyAnnotations
551
+ }, (args) => runTool("get_opportunities", config, rateLimiter, async () => {
552
+ const { level, ...filters } = args;
553
+ const api = await client.getOpportunities(level, toQuery(filters));
554
+ const data = readObject(api.data);
555
+ const programs = readArray(data?.programs).map((program) => addProgramResourceLinks(sanitizeProgram(program, config.webBaseUrl)));
556
+ return withApiMetadata(api, {
557
+ level,
558
+ programs,
559
+ meta: sanitizeJson(data?.meta)
560
+ });
561
+ }));
562
+ server.registerTool("find_programs", {
563
+ title: "Find BBRadar Programs",
564
+ description: "Ranked program discovery with filters and compact output.",
565
+ inputSchema: {
566
+ platforms: stringListSchema,
567
+ tags: stringListSchema,
568
+ scope_tags: stringListSchema.describe("Scope tags."),
569
+ target_types: stringListSchema.describe("Target-type tags."),
570
+ language_tags: stringListSchema.describe("Language tags."),
571
+ web3: z.boolean().default(false).describe("Require Web3 signals."),
572
+ opportunity_levels: z.array(opportunityLevelSchema).max(4).default([]),
573
+ updated_since: isoDateTimeSchema.optional(),
574
+ min_bounty_min: z.number().int().min(0).optional(),
575
+ min_bounty_max: z.number().int().min(0).optional(),
576
+ max_bounty_max: z.number().int().min(0).optional(),
577
+ require_bounty: z.boolean().default(false),
578
+ max_public_report_count: z.number().int().min(0).optional(),
579
+ require_known_report_count: z.boolean().default(false),
580
+ include_unknown_report_count: z.boolean().default(false),
581
+ exclude_ctf: z.boolean().default(false),
582
+ exclude_mobile_only: z.boolean().default(false),
583
+ exclude_non_web: z.boolean().default(false),
584
+ has_api_scope: z.boolean().default(false),
585
+ contest_like: z.boolean().default(false).describe("Require contest/audit signals."),
586
+ vdp_mode: vdpModeSchema.default("include").describe("VDP/no-bounty filter mode."),
587
+ fresh_launch_days: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).optional(),
588
+ min_wildcards: z.number().int().min(0).max(1000).default(0),
589
+ min_eligible_targets: z.number().int().min(0).optional(),
590
+ min_total_targets: z.number().int().min(0).optional(),
591
+ min_added_24h: z.number().int().min(0).optional(),
592
+ min_added_7d: z.number().int().min(0).optional(),
593
+ max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
594
+ max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
595
+ sort_by: programSortSchema,
596
+ include_target_samples: z.boolean().default(false),
597
+ target_sample_programs: z.number().int().min(0).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
598
+ target_sample_size: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(5),
599
+ only_in_scope_targets: z.boolean().default(true),
600
+ only_bounty_eligible_targets: z.boolean().default(true),
601
+ output_mode: programListModeSchema.default("compact"),
602
+ upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
603
+ },
604
+ outputSchema: findProgramsOutputShape,
605
+ annotations: readOnlyAnnotations
606
+ }, (args) => runTool("find_programs", config, rateLimiter, async () => findPrograms(client, config, {
607
+ ...args,
608
+ target_samples_only_wildcards: args.min_wildcards > 0
609
+ })));
610
+ server.registerTool("find_wildcard_programs", {
611
+ title: "Find Wildcard Programs",
612
+ description: "Wildcard program shortlist.",
613
+ inputSchema: {
614
+ platforms: stringListSchema,
615
+ tags: stringListSchema,
616
+ updated_since: isoDateTimeSchema.optional(),
617
+ max_public_report_count: z.number().int().min(0).default(25),
618
+ require_known_report_count: z.boolean().default(true),
619
+ include_unknown_report_count: z.boolean().default(false),
620
+ exclude_ctf: z.boolean().default(true),
621
+ require_bounty: z.boolean().default(true),
622
+ min_wildcards: z.number().int().min(1).max(100).default(1),
623
+ max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
624
+ max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
625
+ include_target_samples: z.boolean().default(false),
626
+ target_sample_programs: z.number().int().min(0).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
627
+ target_sample_size: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(5),
628
+ upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
629
+ },
630
+ outputSchema: findProgramsOutputShape,
631
+ annotations: readOnlyAnnotations
632
+ }, (args) => runTool("find_wildcard_programs", config, rateLimiter, async () => findPrograms(client, config, {
633
+ platforms: args.platforms,
634
+ tags: args.tags,
635
+ scope_tags: [],
636
+ target_types: [],
637
+ language_tags: [],
638
+ web3: false,
639
+ opportunity_levels: [],
640
+ updated_since: args.updated_since,
641
+ min_bounty_min: undefined,
642
+ min_bounty_max: undefined,
643
+ max_bounty_max: undefined,
644
+ require_bounty: args.require_bounty,
645
+ max_public_report_count: args.max_public_report_count,
646
+ require_known_report_count: args.require_known_report_count,
647
+ include_unknown_report_count: args.include_unknown_report_count,
648
+ exclude_ctf: args.exclude_ctf,
649
+ exclude_mobile_only: false,
650
+ exclude_non_web: false,
651
+ has_api_scope: false,
652
+ contest_like: false,
653
+ fresh_launch_days: undefined,
654
+ min_wildcards: args.min_wildcards,
655
+ min_eligible_targets: undefined,
656
+ min_total_targets: undefined,
657
+ min_added_24h: undefined,
658
+ min_added_7d: undefined,
659
+ max_pages: args.max_pages,
660
+ max_results: args.max_results,
661
+ sort_by: "best_hunt_value",
662
+ include_target_samples: args.include_target_samples,
663
+ target_sample_programs: args.target_sample_programs,
664
+ target_sample_size: args.target_sample_size,
665
+ only_in_scope_targets: true,
666
+ only_bounty_eligible_targets: true,
667
+ target_samples_only_wildcards: true,
668
+ upstream_request_budget: args.upstream_request_budget
669
+ })));
670
+ server.registerTool("find_web3_programs", {
671
+ title: "Find Web3 Programs",
672
+ description: "Web3/blockchain/smart-contract program shortlist.",
673
+ inputSchema: {
674
+ platforms: stringListSchema,
675
+ tags: stringListSchema,
676
+ updated_since: isoDateTimeSchema.optional(),
677
+ max_public_report_count: z.number().int().min(0).optional(),
678
+ require_known_report_count: z.boolean().default(false),
679
+ include_unknown_report_count: z.boolean().default(true),
680
+ require_bounty: z.boolean().default(false),
681
+ min_bounty_max: z.number().int().min(0).optional(),
682
+ min_wildcards: z.number().int().min(0).max(1000).default(0),
683
+ exclude_ctf: z.boolean().default(true),
684
+ max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(5),
685
+ max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
686
+ include_target_samples: z.boolean().default(false),
687
+ target_sample_programs: z.number().int().min(0).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
688
+ target_sample_size: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(5),
689
+ upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
690
+ },
691
+ outputSchema: findProgramsOutputShape,
692
+ annotations: readOnlyAnnotations
693
+ }, (args) => runTool("find_web3_programs", config, rateLimiter, async () => findPrograms(client, config, {
694
+ platforms: args.platforms,
695
+ tags: args.tags,
696
+ scope_tags: [],
697
+ target_types: [],
698
+ language_tags: [],
699
+ web3: true,
700
+ opportunity_levels: [],
701
+ updated_since: args.updated_since,
702
+ min_bounty_min: undefined,
703
+ min_bounty_max: args.min_bounty_max,
704
+ max_bounty_max: undefined,
705
+ require_bounty: args.require_bounty,
706
+ max_public_report_count: args.max_public_report_count,
707
+ require_known_report_count: args.require_known_report_count,
708
+ include_unknown_report_count: args.include_unknown_report_count,
709
+ exclude_ctf: args.exclude_ctf,
710
+ exclude_mobile_only: false,
711
+ exclude_non_web: false,
712
+ has_api_scope: false,
713
+ contest_like: false,
714
+ fresh_launch_days: undefined,
715
+ min_wildcards: args.min_wildcards,
716
+ min_eligible_targets: undefined,
717
+ min_total_targets: undefined,
718
+ min_added_24h: undefined,
719
+ min_added_7d: undefined,
720
+ max_pages: args.max_pages,
721
+ max_results: args.max_results,
722
+ sort_by: "best_hunt_value",
723
+ include_target_samples: args.include_target_samples,
724
+ target_sample_programs: args.target_sample_programs,
725
+ target_sample_size: args.target_sample_size,
726
+ only_in_scope_targets: true,
727
+ only_bounty_eligible_targets: true,
728
+ target_samples_only_wildcards: args.min_wildcards > 0,
729
+ upstream_request_budget: args.upstream_request_budget
730
+ })));
731
+ server.registerTool("find_reward_programs", {
732
+ title: "Find Reward Programs",
733
+ description: "Programs matching reward thresholds.",
734
+ inputSchema: {
735
+ platforms: stringListSchema,
736
+ tags: stringListSchema,
737
+ target_types: stringListSchema,
738
+ language_tags: stringListSchema,
739
+ min_reward: z.number().int().min(0).default(5_000),
740
+ reward_threshold_mode: rewardThresholdModeSchema.default("max_at_least"),
741
+ web3: z.boolean().default(false),
742
+ max_public_report_count: z.number().int().min(0).optional(),
743
+ require_known_report_count: z.boolean().default(false),
744
+ include_unknown_report_count: z.boolean().default(true),
745
+ min_eligible_targets: z.number().int().min(0).default(1),
746
+ exclude_ctf: z.boolean().default(true),
747
+ exclude_mobile_only: z.boolean().default(false),
748
+ exclude_non_web: z.boolean().default(false),
749
+ max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
750
+ max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
751
+ include_target_samples: z.boolean().default(false),
752
+ target_sample_programs: z.number().int().min(0).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
753
+ target_sample_size: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
754
+ output_mode: programListModeSchema.default("compact"),
755
+ upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
756
+ },
757
+ outputSchema: findProgramsOutputShape,
758
+ annotations: readOnlyAnnotations
759
+ }, (args) => runTool("find_reward_programs", config, rateLimiter, async () => findRewardPrograms(client, config, args)));
760
+ server.registerTool("find_web3_contests", {
761
+ title: "Find Web3 Contests",
762
+ description: "Web3 contest or competitive audit shortlist.",
763
+ inputSchema: {
764
+ platforms: stringListSchema,
765
+ tags: stringListSchema,
766
+ min_reward: z.number().int().min(0).optional(),
767
+ reward_threshold_mode: rewardThresholdModeSchema.default("max_at_least"),
768
+ require_contest_signal: z.boolean().default(true),
769
+ require_bounty: z.boolean().default(false),
770
+ max_public_report_count: z.number().int().min(0).optional(),
771
+ include_unknown_report_count: z.boolean().default(true),
772
+ max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(5),
773
+ max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
774
+ include_target_samples: z.boolean().default(false),
775
+ target_sample_programs: z.number().int().min(0).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
776
+ target_sample_size: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
777
+ output_mode: programListModeSchema.default("compact"),
778
+ upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(30)
779
+ },
780
+ outputSchema: findProgramsOutputShape,
781
+ annotations: readOnlyAnnotations
782
+ }, (args) => runTool("find_web3_contests", config, rateLimiter, async () => findWeb3Contests(client, config, args)));
783
+ server.registerTool("find_language_programs", {
784
+ title: "Find Language Programs",
785
+ description: "Language-specific program shortlist.",
786
+ inputSchema: {
787
+ platforms: stringListSchema,
788
+ tags: stringListSchema,
789
+ language_tags: z.array(filterValueSchema).min(1).max(50),
790
+ target_types: stringListSchema,
791
+ min_reward: z.number().int().min(0).optional(),
792
+ reward_threshold_mode: rewardThresholdModeSchema.default("max_at_least"),
793
+ require_bounty: z.boolean().default(false),
794
+ exclude_vdp: z.boolean().default(false),
795
+ max_public_report_count: z.number().int().min(0).optional(),
796
+ require_known_report_count: z.boolean().default(false),
797
+ include_unknown_report_count: z.boolean().default(true),
798
+ min_eligible_targets: z.number().int().min(0).optional(),
799
+ max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
800
+ max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
801
+ include_target_samples: z.boolean().default(false),
802
+ target_sample_programs: z.number().int().min(0).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(5),
803
+ target_sample_size: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
804
+ output_mode: programListModeSchema.default("compact"),
805
+ upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
806
+ },
807
+ outputSchema: findProgramsOutputShape,
808
+ annotations: readOnlyAnnotations
809
+ }, (args) => runTool("find_language_programs", config, rateLimiter, async () => findLanguagePrograms(client, config, args)));
810
+ server.registerTool("find_target_type_programs", {
811
+ title: "Find Target Type Programs",
812
+ description: "Target-type program shortlist.",
813
+ inputSchema: {
814
+ platforms: stringListSchema,
815
+ tags: stringListSchema,
816
+ target_types: z.array(filterValueSchema).min(1).max(50),
817
+ scope_tags: stringListSchema,
818
+ language_tags: stringListSchema,
819
+ min_reward: z.number().int().min(0).optional(),
820
+ reward_threshold_mode: rewardThresholdModeSchema.default("max_at_least"),
821
+ require_bounty: z.boolean().default(false),
822
+ fresh_only: z.boolean().default(false),
823
+ min_added_7d: z.number().int().min(0).optional(),
824
+ exclude_vdp: z.boolean().default(false),
825
+ max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
826
+ max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
827
+ include_target_samples: z.boolean().default(false),
828
+ target_sample_programs: z.number().int().min(0).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(5),
829
+ target_sample_size: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
830
+ output_mode: programListModeSchema.default("compact"),
831
+ upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
832
+ },
833
+ outputSchema: findProgramsOutputShape,
834
+ annotations: readOnlyAnnotations
835
+ }, (args) => runTool("find_target_type_programs", config, rateLimiter, async () => findTargetTypePrograms(client, config, args)));
836
+ server.registerTool("find_vdp_programs", {
837
+ title: "Find VDP Programs",
838
+ description: "VDP/disclosure-only/no-bounty program shortlist.",
839
+ inputSchema: {
840
+ platforms: stringListSchema,
841
+ tags: stringListSchema,
842
+ target_types: stringListSchema,
843
+ language_tags: stringListSchema,
844
+ vdp_mode: z.enum(["only_likely", "only_known_no_bounty"]).default("only_likely"),
845
+ max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
846
+ max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
847
+ include_target_samples: z.boolean().default(false),
848
+ target_sample_programs: z.number().int().min(0).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
849
+ target_sample_size: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
850
+ output_mode: programListModeSchema.default("compact"),
851
+ upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
852
+ },
853
+ outputSchema: findProgramsOutputShape,
854
+ annotations: readOnlyAnnotations
855
+ }, (args) => runTool("find_vdp_programs", config, rateLimiter, async () => findVdpPrograms(client, config, args)));
856
+ server.registerTool("find_paid_programs", {
857
+ title: "Find Paid Programs",
858
+ description: "Paid bounty program shortlist.",
859
+ inputSchema: {
860
+ platforms: stringListSchema,
861
+ tags: stringListSchema,
862
+ target_types: stringListSchema,
863
+ language_tags: stringListSchema,
864
+ min_reward: z.number().int().min(0).optional(),
865
+ reward_threshold_mode: rewardThresholdModeSchema.default("max_at_least"),
866
+ min_eligible_targets: z.number().int().min(0).default(1),
867
+ max_public_report_count: z.number().int().min(0).optional(),
868
+ max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
869
+ max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
870
+ include_target_samples: z.boolean().default(false),
871
+ target_sample_programs: z.number().int().min(0).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
872
+ target_sample_size: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
873
+ output_mode: programListModeSchema.default("compact"),
874
+ upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
875
+ },
876
+ outputSchema: findProgramsOutputShape,
877
+ annotations: readOnlyAnnotations
878
+ }, (args) => runTool("find_paid_programs", config, rateLimiter, async () => findPaidPrograms(client, config, args)));
879
+ server.registerTool("find_stack_matches", {
880
+ title: "Find Stack Matches",
881
+ description: "Combined stack/language/type/reward shortlist.",
882
+ inputSchema: {
883
+ platforms: stringListSchema,
884
+ tags: stringListSchema,
885
+ language_tags: stringListSchema,
886
+ target_types: stringListSchema,
887
+ scope_tags: stringListSchema,
888
+ min_reward: z.number().int().min(0).optional(),
889
+ reward_threshold_mode: rewardThresholdModeSchema.default("max_at_least"),
890
+ web3: z.boolean().default(false),
891
+ contest_like: z.boolean().default(false),
892
+ require_bounty: z.boolean().default(false),
893
+ exclude_vdp: z.boolean().default(false),
894
+ max_public_report_count: z.number().int().min(0).optional(),
895
+ min_eligible_targets: z.number().int().min(0).optional(),
896
+ fresh_only: z.boolean().default(false),
897
+ min_added_7d: z.number().int().min(0).optional(),
898
+ max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
899
+ max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
900
+ include_target_samples: z.boolean().default(false),
901
+ target_sample_programs: z.number().int().min(0).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(5),
902
+ target_sample_size: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
903
+ output_mode: programListModeSchema.default("compact"),
904
+ upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(30)
905
+ },
906
+ outputSchema: findProgramsOutputShape,
907
+ annotations: readOnlyAnnotations
908
+ }, (args) => runTool("find_stack_matches", config, rateLimiter, async () => findStackMatches(client, config, args)));
909
+ server.registerTool("find_low_noise_programs", {
910
+ title: "Find Low Noise Programs",
911
+ description: "Low-public-report program shortlist.",
912
+ inputSchema: {
913
+ platforms: stringListSchema,
914
+ tags: stringListSchema,
915
+ target_types: stringListSchema,
916
+ language_tags: stringListSchema,
917
+ min_reward: z.number().int().min(0).optional(),
918
+ reward_threshold_mode: rewardThresholdModeSchema.default("max_at_least"),
919
+ max_public_report_count: z.number().int().min(0).default(5),
920
+ require_known_report_count: z.boolean().default(true),
921
+ include_unknown_report_count: z.boolean().default(false),
922
+ require_bounty: z.boolean().default(true),
923
+ min_eligible_targets: z.number().int().min(0).default(1),
924
+ max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
925
+ max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
926
+ include_target_samples: z.boolean().default(false),
927
+ target_sample_programs: z.number().int().min(0).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
928
+ target_sample_size: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
929
+ output_mode: programListModeSchema.default("compact"),
930
+ upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
931
+ },
932
+ outputSchema: findProgramsOutputShape,
933
+ annotations: readOnlyAnnotations
934
+ }, (args) => runTool("find_low_noise_programs", config, rateLimiter, async () => findLowNoisePrograms(client, config, args)));
935
+ server.registerTool("find_recent_by_type", {
936
+ title: "Find Recent By Type",
937
+ description: "Recent changes by target type grouped by program.",
938
+ inputSchema: {
939
+ target_type: filterValueSchema,
940
+ change_type: z.enum(["added", "updated", "removed"]).default("added"),
941
+ include_removed: z.boolean().default(false),
942
+ include_out_of_scope: z.boolean().default(false),
943
+ include_ineligible: z.boolean().default(false),
944
+ search: searchTextSchema.optional(),
945
+ platforms: stringListSchema,
946
+ tags: stringListSchema,
947
+ language_tags: stringListSchema,
948
+ page_size: recentChangesPageSizeSchema.default(MAX_RECENT_CHANGES),
949
+ max_programs: z.number().int().min(1).max(50).default(10),
950
+ sample_size: z.number().int().min(1).max(20).default(5),
951
+ target_list_mode: targetListModeSchema.default("identifiers")
952
+ },
953
+ outputSchema: {
954
+ ...apiEnvelopeOutputShape,
955
+ source_requests: z.array(jsonRecordSchema).optional(),
956
+ activity: z.array(jsonRecordSchema).optional(),
957
+ meta: jsonRecordSchema.optional()
958
+ },
959
+ annotations: readOnlyAnnotations
960
+ }, (args) => runTool("find_recent_by_type", config, rateLimiter, async () => findRecentByType(client, config, args)));
961
+ server.registerTool("find_programs_by_target", {
962
+ title: "Find Programs By Target",
963
+ description: "Reverse lookup target/domain ownership.",
964
+ inputSchema: {
965
+ target_query: searchTextSchema,
966
+ match_mode: targetMatchModeSchema.default("contains"),
967
+ platforms: stringListSchema,
968
+ tags: stringListSchema,
969
+ use_api_search: z.boolean().default(true),
970
+ max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(2),
971
+ max_results: z.number().int().min(1).max(25).default(10),
972
+ include_out_of_scope: z.boolean().default(true),
973
+ include_ineligible: z.boolean().default(true),
974
+ max_targets_per_program: z.number().int().min(1).max(50).default(10),
975
+ target_list_mode: targetListModeSchema.default("identifiers"),
976
+ upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(30)
977
+ },
978
+ outputSchema: {
979
+ ...findProgramsOutputShape,
980
+ matches: z.array(jsonRecordSchema).optional()
981
+ },
982
+ annotations: readOnlyAnnotations
983
+ }, (args) => runTool("find_programs_by_target", config, rateLimiter, async () => findProgramsByTarget(client, config, args)));
984
+ server.registerTool("find_program_candidates", {
985
+ title: "Find BBRadar Program Candidates",
986
+ description: "Default ranked BBRadar discovery preset.",
987
+ inputSchema: programCandidatePresetInputSchema,
988
+ outputSchema: findProgramsOutputShape,
989
+ annotations: readOnlyAnnotations
990
+ }, (args) => runTool("find_program_candidates", config, rateLimiter, async () => findProgramCandidates(client, config, args)));
991
+ server.registerTool("find_hunt_candidates", {
992
+ title: "Find BBRadar Hunt Candidates",
993
+ description: "Legacy alias for find_program_candidates.",
994
+ inputSchema: programCandidatePresetInputSchema,
995
+ outputSchema: findProgramsOutputShape,
996
+ annotations: readOnlyAnnotations
997
+ }, (args) => runTool("find_hunt_candidates", config, rateLimiter, async () => findProgramCandidates(client, config, args)));
998
+ server.registerTool("find_fresh_hunt_candidates", {
999
+ title: "Find Fresh Hunt Candidates",
1000
+ description: "Freshness-first hunt shortlist.",
1001
+ inputSchema: {
1002
+ platforms: stringListSchema,
1003
+ tags: stringListSchema,
1004
+ target_types: z.array(filterValueSchema).max(50).default(["domain"]),
1005
+ language_tags: stringListSchema,
1006
+ opportunity_levels: z.array(opportunityLevelSchema).max(4).default(["elite", "hot", "strong", "potential"]),
1007
+ min_added_7d: z.number().int().min(0).default(1),
1008
+ max_public_report_count: z.number().int().min(0).default(25),
1009
+ require_known_report_count: z.boolean().default(false),
1010
+ include_unknown_report_count: z.boolean().default(false),
1011
+ require_bounty: z.boolean().default(true),
1012
+ min_bounty_max: z.number().int().min(0).optional(),
1013
+ min_eligible_targets: z.number().int().min(0).default(1),
1014
+ has_api_scope: z.boolean().default(false),
1015
+ wildcard: z.boolean().default(false),
1016
+ exclude_ctf: z.boolean().default(true),
1017
+ exclude_mobile_only: z.boolean().default(false),
1018
+ exclude_non_web: z.boolean().default(false),
1019
+ max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
1020
+ max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
1021
+ include_target_samples: z.boolean().default(true),
1022
+ target_sample_programs: z.number().int().min(0).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(5),
1023
+ target_sample_size: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
1024
+ output_mode: programListModeSchema.default("compact"),
1025
+ upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(30)
1026
+ },
1027
+ outputSchema: findProgramsOutputShape,
1028
+ annotations: readOnlyAnnotations
1029
+ }, (args) => runTool("find_fresh_hunt_candidates", config, rateLimiter, async () => findPrograms(client, config, {
1030
+ platforms: args.platforms,
1031
+ tags: args.tags,
1032
+ scope_tags: [],
1033
+ target_types: args.target_types,
1034
+ language_tags: args.language_tags,
1035
+ web3: false,
1036
+ opportunity_levels: args.opportunity_levels,
1037
+ updated_since: undefined,
1038
+ min_bounty_min: undefined,
1039
+ min_bounty_max: args.min_bounty_max,
1040
+ max_bounty_max: undefined,
1041
+ require_bounty: args.require_bounty,
1042
+ max_public_report_count: args.max_public_report_count,
1043
+ require_known_report_count: args.require_known_report_count,
1044
+ include_unknown_report_count: args.include_unknown_report_count,
1045
+ exclude_ctf: args.exclude_ctf,
1046
+ exclude_mobile_only: args.exclude_mobile_only,
1047
+ exclude_non_web: args.exclude_non_web,
1048
+ has_api_scope: args.has_api_scope,
1049
+ fresh_launch_days: undefined,
1050
+ min_wildcards: args.wildcard ? 1 : 0,
1051
+ min_eligible_targets: args.min_eligible_targets,
1052
+ min_total_targets: undefined,
1053
+ min_added_24h: undefined,
1054
+ min_added_7d: args.min_added_7d,
1055
+ max_pages: args.max_pages,
1056
+ max_results: args.max_results,
1057
+ sort_by: "most_new_targets",
1058
+ include_target_samples: args.include_target_samples,
1059
+ target_sample_programs: args.target_sample_programs,
1060
+ target_sample_size: args.target_sample_size,
1061
+ only_in_scope_targets: true,
1062
+ only_bounty_eligible_targets: true,
1063
+ target_samples_only_wildcards: args.wildcard,
1064
+ output_mode: args.output_mode,
1065
+ upstream_request_budget: args.upstream_request_budget
1066
+ })));
1067
+ server.registerTool("list_filters", {
1068
+ title: "List BBRadar Filters",
1069
+ description: "Discover observed filter values.",
1070
+ inputSchema: {
1071
+ platforms: stringListSchema,
1072
+ tags: stringListSchema,
1073
+ max_pages: z.number().int().min(1).max(MAX_FILTER_DISCOVERY_PAGES).default(DEFAULT_FILTER_DISCOVERY_PAGES),
1074
+ include_target_facets: z.boolean().default(true),
1075
+ target_sample_programs: z.number().int().min(0).max(MAX_FILTER_TARGET_SAMPLE_PROGRAMS).default(DEFAULT_FILTER_TARGET_SAMPLE_PROGRAMS),
1076
+ upstream_request_budget: z.number().int().min(1).max(MAX_FIND_UPSTREAM_REQUEST_BUDGET).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
1077
+ },
1078
+ outputSchema: {
1079
+ ...apiEnvelopeOutputShape,
1080
+ filters: jsonRecordSchema.optional(),
1081
+ observed_from: jsonRecordSchema.optional(),
1082
+ upstream_budget: jsonRecordSchema.optional()
1083
+ },
1084
+ annotations: readOnlyAnnotations
1085
+ }, (args) => runTool("list_filters", config, rateLimiter, async () => listFilters(client, args)));
1086
+ server.registerTool("get_mcp_status", {
1087
+ title: "Get BBRadar MCP Status",
1088
+ description: "Local MCP status and timing metrics.",
1089
+ inputSchema: {},
1090
+ outputSchema: {
1091
+ ...apiEnvelopeOutputShape,
1092
+ status: jsonRecordSchema.optional()
1093
+ },
1094
+ annotations: readOnlyAnnotations
1095
+ }, () => runTool("get_mcp_status", config, rateLimiter, async () => ({
1096
+ request_id: randomUUID(),
1097
+ status: {
1098
+ server_name: "bbradar.io",
1099
+ server_version: SERVER_VERSION,
1100
+ sdk_version: SDK_VERSION,
1101
+ api_base_url: config.apiBaseUrl,
1102
+ web_base_url: config.webBaseUrl,
1103
+ response_cache_enabled: config.cacheTtlMs > 0 && config.cacheMaxEntries > 0,
1104
+ cache_ttl_ms: config.cacheTtlMs,
1105
+ cache_max_entries: config.cacheMaxEntries,
1106
+ local_program_index_enabled: false,
1107
+ default_rate_limit_per_minute: config.defaultRateLimitPerMinute,
1108
+ export_rate_limit_per_minute: config.exportRateLimitPerMinute,
1109
+ request_timeout_ms: config.requestTimeoutMs,
1110
+ validate_on_startup: config.validateOnStartup,
1111
+ transport: "stdio",
1112
+ remote_streamable_http: "not_enabled_for_local_setup",
1113
+ tool_metrics: toolMetricsSnapshot(),
1114
+ client: client.diagnostics()
1115
+ }
1116
+ })));
1117
+ server.registerTool("compare_programs", {
1118
+ title: "Compare BBRadar Programs",
1119
+ description: "Compare programs.",
1120
+ inputSchema: {
1121
+ program_ids: z.array(programIdSchema).min(2).max(10),
1122
+ include_target_samples: z.boolean().default(false),
1123
+ target_sample_size: z.number().int().min(1).max(MAX_FIND_TARGET_SAMPLES).default(5),
1124
+ only_in_scope_targets: z.boolean().default(true),
1125
+ only_bounty_eligible_targets: z.boolean().default(true),
1126
+ output_mode: programListModeSchema.default("compact")
1127
+ },
1128
+ outputSchema: decisionOutputShape,
1129
+ annotations: readOnlyAnnotations
1130
+ }, (args) => runTool("compare_programs", config, rateLimiter, async () => comparePrograms(client, config, args)));
1131
+ server.registerTool("compare_programs_compact", {
1132
+ title: "Compare Programs Compact",
1133
+ description: "Compact program comparison.",
1134
+ inputSchema: {
1135
+ program_ids: z.array(programIdSchema).min(2).max(10),
1136
+ include_target_samples: z.boolean().default(false),
1137
+ target_sample_size: z.number().int().min(1).max(5).default(3),
1138
+ only_in_scope_targets: z.boolean().default(true),
1139
+ only_bounty_eligible_targets: z.boolean().default(true)
1140
+ },
1141
+ outputSchema: decisionOutputShape,
1142
+ annotations: readOnlyAnnotations
1143
+ }, (args) => runTool("compare_programs_compact", config, rateLimiter, async () => comparePrograms(client, config, { ...args, output_mode: "compact" })));
1144
+ server.registerTool("summarize_program_activity", {
1145
+ title: "Summarize Program Activity",
1146
+ description: "Summarize one program's activity.",
1147
+ inputSchema: {
1148
+ program_id: programIdSchema,
1149
+ recent_changes_limit: z.number().int().min(0).max(MAX_RECENT_CHANGES).default(25)
1150
+ },
1151
+ outputSchema: {
1152
+ ...apiEnvelopeOutputShape,
1153
+ program: programOutputSchema.optional(),
1154
+ activity: jsonRecordSchema.optional(),
1155
+ changes: z.array(jsonRecordSchema).optional()
1156
+ },
1157
+ annotations: readOnlyAnnotations
1158
+ }, (args) => runTool("summarize_program_activity", config, rateLimiter, async () => summarizeProgramActivity(client, config, args.program_id, args.recent_changes_limit)));
1159
+ server.registerTool("get_program_brief", {
1160
+ title: "Get Program Brief",
1161
+ description: "Compact worth-hunting brief for one program.",
1162
+ inputSchema: {
1163
+ program_id: programIdSchema,
1164
+ target_sample_size: z.number().int().min(1).max(50).default(10),
1165
+ target_list_mode: targetListModeSchema.default("compact"),
1166
+ include_recent_changes: z.boolean().default(true),
1167
+ recent_changes_limit: z.number().int().min(0).max(MAX_RECENT_CHANGES).default(10),
1168
+ include_out_of_scope: z.boolean().default(false),
1169
+ include_ineligible: z.boolean().default(false)
1170
+ },
1171
+ outputSchema: {
1172
+ ...apiEnvelopeOutputShape,
1173
+ source_requests: z.array(jsonRecordSchema).optional(),
1174
+ program: programOutputSchema.optional(),
1175
+ brief: jsonRecordSchema.optional(),
1176
+ target_samples: jsonRecordSchema.optional(),
1177
+ recent_changes: z.array(jsonRecordSchema).optional(),
1178
+ meta: jsonRecordSchema.optional()
1179
+ },
1180
+ annotations: readOnlyAnnotations
1181
+ }, (args) => runTool("get_program_brief", config, rateLimiter, async () => getProgramBrief(client, config, args)));
1182
+ server.registerTool("find_recently_added_wildcards", {
1183
+ title: "Find Recently Added Wildcards",
1184
+ description: "Recently added wildcard programs.",
1185
+ inputSchema: {
1186
+ platforms: stringListSchema,
1187
+ tags: stringListSchema,
1188
+ min_added_7d: z.number().int().min(1).default(1),
1189
+ max_public_report_count: z.number().int().min(0).optional(),
1190
+ require_bounty: z.boolean().default(true),
1191
+ max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
1192
+ max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
1193
+ upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
1194
+ },
1195
+ outputSchema: findProgramsOutputShape,
1196
+ annotations: readOnlyAnnotations
1197
+ }, (args) => runTool("find_recently_added_wildcards", config, rateLimiter, async () => findPrograms(client, config, {
1198
+ platforms: args.platforms,
1199
+ tags: args.tags,
1200
+ scope_tags: [],
1201
+ target_types: [],
1202
+ language_tags: [],
1203
+ web3: false,
1204
+ opportunity_levels: [],
1205
+ updated_since: undefined,
1206
+ min_bounty_min: undefined,
1207
+ min_bounty_max: undefined,
1208
+ max_bounty_max: undefined,
1209
+ require_bounty: args.require_bounty,
1210
+ max_public_report_count: args.max_public_report_count,
1211
+ require_known_report_count: false,
1212
+ include_unknown_report_count: false,
1213
+ exclude_ctf: true,
1214
+ exclude_mobile_only: false,
1215
+ exclude_non_web: false,
1216
+ has_api_scope: false,
1217
+ fresh_launch_days: undefined,
1218
+ min_wildcards: 1,
1219
+ min_eligible_targets: undefined,
1220
+ min_total_targets: undefined,
1221
+ min_added_24h: undefined,
1222
+ min_added_7d: args.min_added_7d,
1223
+ max_pages: args.max_pages,
1224
+ max_results: args.max_results,
1225
+ sort_by: "best_hunt_value",
1226
+ include_target_samples: true,
1227
+ target_sample_programs: Math.min(args.max_results, MAX_FIND_TARGET_SAMPLE_PROGRAMS),
1228
+ target_sample_size: 5,
1229
+ only_in_scope_targets: true,
1230
+ only_bounty_eligible_targets: true,
1231
+ target_samples_only_wildcards: true,
1232
+ upstream_request_budget: args.upstream_request_budget
1233
+ })));
1234
+ server.registerTool("find_low_competition_high_reward", {
1235
+ title: "Find Low Competition High Reward",
1236
+ description: "Legacy low-report high-reward shortlist.",
1237
+ inputSchema: {
1238
+ platforms: stringListSchema,
1239
+ tags: stringListSchema,
1240
+ target_types: stringListSchema,
1241
+ language_tags: stringListSchema,
1242
+ max_public_report_count: z.number().int().min(0).default(10),
1243
+ min_bounty_max: z.number().int().min(0).default(5_000),
1244
+ min_eligible_targets: z.number().int().min(0).default(1),
1245
+ exclude_ctf: z.boolean().default(true),
1246
+ exclude_mobile_only: z.boolean().default(false),
1247
+ exclude_non_web: z.boolean().default(false),
1248
+ has_api_scope: z.boolean().default(false),
1249
+ fresh_launch_days: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).optional(),
1250
+ max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
1251
+ max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
1252
+ upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
1253
+ },
1254
+ outputSchema: findProgramsOutputShape,
1255
+ annotations: readOnlyAnnotations
1256
+ }, (args) => runTool("find_low_competition_high_reward", config, rateLimiter, async () => findPrograms(client, config, {
1257
+ platforms: args.platforms,
1258
+ tags: args.tags,
1259
+ scope_tags: [],
1260
+ target_types: args.target_types,
1261
+ language_tags: args.language_tags,
1262
+ web3: false,
1263
+ opportunity_levels: [],
1264
+ updated_since: undefined,
1265
+ min_bounty_min: undefined,
1266
+ min_bounty_max: args.min_bounty_max,
1267
+ max_bounty_max: undefined,
1268
+ require_bounty: true,
1269
+ max_public_report_count: args.max_public_report_count,
1270
+ require_known_report_count: true,
1271
+ include_unknown_report_count: false,
1272
+ exclude_ctf: args.exclude_ctf,
1273
+ exclude_mobile_only: args.exclude_mobile_only,
1274
+ exclude_non_web: args.exclude_non_web,
1275
+ has_api_scope: args.has_api_scope,
1276
+ fresh_launch_days: args.fresh_launch_days,
1277
+ min_wildcards: 0,
1278
+ min_eligible_targets: args.min_eligible_targets,
1279
+ min_total_targets: undefined,
1280
+ min_added_24h: undefined,
1281
+ min_added_7d: undefined,
1282
+ max_pages: args.max_pages,
1283
+ max_results: args.max_results,
1284
+ sort_by: "best_hunt_value",
1285
+ include_target_samples: true,
1286
+ target_sample_programs: Math.min(args.max_results, MAX_FIND_TARGET_SAMPLE_PROGRAMS),
1287
+ target_sample_size: 5,
1288
+ only_in_scope_targets: true,
1289
+ only_bounty_eligible_targets: true,
1290
+ target_samples_only_wildcards: false,
1291
+ upstream_request_budget: args.upstream_request_budget
1292
+ })));
1293
+ server.registerTool("get_program_delta", {
1294
+ title: "Get Program Delta",
1295
+ description: "Recent target changes for one program.",
1296
+ inputSchema: {
1297
+ program_id: programIdSchema,
1298
+ include_removed: z.boolean().default(true),
1299
+ include_ineligible: z.boolean().default(false),
1300
+ include_out_of_scope: z.boolean().default(false),
1301
+ page_size: recentChangesPageSizeSchema.default(50)
1302
+ },
1303
+ outputSchema: {
1304
+ ...apiEnvelopeOutputShape,
1305
+ program: programOutputSchema.optional(),
1306
+ changes: z.array(jsonRecordSchema).optional(),
1307
+ meta: jsonRecordSchema.optional()
1308
+ },
1309
+ annotations: readOnlyAnnotations
1310
+ }, (args) => runTool("get_program_delta", config, rateLimiter, async () => getProgramDelta(client, config, args)));
1311
+ server.registerTool("export_targets", {
1312
+ title: "Export BBRadar Targets",
1313
+ description: `Read-only target export. limit max ${MAX_TARGET_EXPORT}.`,
1314
+ inputSchema: {
1315
+ program_ids: z.array(programIdSchema).max(50).default([]),
1316
+ platforms: stringListSchema,
1317
+ tags: stringListSchema,
1318
+ opportunity_levels: z.array(z.enum(["elite", "hot", "strong", "potential"])).max(4).default([]),
1319
+ include_out_of_scope: z.boolean().default(false),
1320
+ include_ineligible: z.boolean().default(false),
1321
+ format: z.enum(["json", "csv"]).default("json"),
1322
+ limit: z.number().int().min(1).max(MAX_TARGET_EXPORT).default(MAX_TARGET_EXPORT)
1323
+ },
1324
+ outputSchema: {
1325
+ ...apiEnvelopeOutputShape,
1326
+ export: z.unknown().optional()
1327
+ },
1328
+ annotations: readOnlyAnnotations
1329
+ }, (args) => runTool("export_targets", config, rateLimiter, async () => {
1330
+ const api = await client.exportTargets(compactRecord(args));
1331
+ const exportId = randomUUID();
1332
+ const sanitizedExport = sanitizeTargetsForExport(api.data, args.limit);
1333
+ const resourceUri = exportResourceUri(exportId);
1334
+ rememberExport(exportStore, exportId, sanitizedExport);
1335
+ return withApiMetadata(api, {
1336
+ export: {
1337
+ export_id: exportId,
1338
+ resource_uri: resourceUri,
1339
+ preview: previewExportPayload(sanitizedExport),
1340
+ returned_preview_count: previewExportCount(sanitizedExport),
1341
+ limit: args.limit
1342
+ }
1343
+ });
1344
+ }));
1345
+ registerPrompts(server);
1346
+ registerResources(server, client, config, exportStore);
1347
+ return server;
1348
+ }
1349
+ function registerResources(server, client, config, exportStore) {
1350
+ server.registerResource("target-export", new ResourceTemplate("bbradar://exports{?export_id}", { list: undefined }), {
1351
+ title: "BBRadar Target Export",
1352
+ description: "In-memory local target export payload by export_id query parameter.",
1353
+ mimeType: "application/json"
1354
+ }, async (uri) => {
1355
+ const exportId = uri.searchParams.get("export_id") ?? "";
1356
+ const payload = exportStore.get(exportId);
1357
+ if (!payload) {
1358
+ return jsonResource(uri.toString(), {
1359
+ request_id: randomUUID(),
1360
+ error: {
1361
+ message: "Export resource not found or expired from local memory."
1362
+ }
1363
+ });
1364
+ }
1365
+ return jsonResource(uri.toString(), {
1366
+ request_id: randomUUID(),
1367
+ export_id: exportId,
1368
+ export: payload
1369
+ });
1370
+ });
1371
+ server.registerResource("program", new ResourceTemplate("bbradar://program{?program_id}", { list: undefined }), {
1372
+ title: "BBRadar Program",
1373
+ description: "Sanitized BBRadar program details by program_id query parameter.",
1374
+ mimeType: "application/json"
1375
+ }, async (uri) => {
1376
+ const programId = parseProgramIdFromResourceUri(uri);
1377
+ const api = await client.getProgram(programId);
1378
+ const payload = withApiMetadata(api, {
1379
+ program: addProgramResourceLinks(sanitizeProgram(api.data, config.webBaseUrl))
1380
+ });
1381
+ return jsonResource(uri.toString(), payload);
1382
+ });
1383
+ server.registerResource("program-targets", new ResourceTemplate("bbradar://program/targets{?program_id}", { list: undefined }), {
1384
+ title: "BBRadar Program Targets",
1385
+ description: "Sanitized active target list by program_id query parameter.",
1386
+ mimeType: "application/json"
1387
+ }, async (uri) => {
1388
+ const programId = parseProgramIdFromResourceUri(uri);
1389
+ const api = await client.getProgramTargets(programId);
1390
+ const data = readObject(api.data);
1391
+ const targets = readArray(data?.targets).map(sanitizeTarget);
1392
+ const payload = withApiMetadata(api, {
1393
+ program_id: programId,
1394
+ targets,
1395
+ meta: {
1396
+ total_active_targets: targets.length
1397
+ }
1398
+ });
1399
+ return jsonResource(uri.toString(), payload);
1400
+ });
1401
+ server.registerResource("opportunities", new ResourceTemplate("bbradar://opportunities{?level}", { list: undefined }), {
1402
+ title: "BBRadar Opportunities",
1403
+ description: "Sanitized opportunity programs by level query parameter.",
1404
+ mimeType: "application/json"
1405
+ }, async (uri) => {
1406
+ const level = opportunityLevelSchema.parse(uri.searchParams.get("level") ?? "elite");
1407
+ const api = await client.getOpportunities(level, { page: 1, page_size: MAX_PROGRAMS_PER_SEARCH });
1408
+ const data = readObject(api.data);
1409
+ const programs = readArray(data?.programs).map((program) => addProgramResourceLinks(sanitizeProgram(program, config.webBaseUrl)));
1410
+ return jsonResource(uri.toString(), withApiMetadata(api, {
1411
+ level,
1412
+ programs,
1413
+ meta: sanitizeJson(data?.meta)
1414
+ }));
1415
+ });
1416
+ server.registerResource("recent-changes", new ResourceTemplate("bbradar://recent-changes{?change_type}", { list: undefined }), {
1417
+ title: "BBRadar Recent Changes",
1418
+ description: "Sanitized recent target changes with optional change_type query parameter.",
1419
+ mimeType: "application/json"
1420
+ }, async (uri) => {
1421
+ const changeType = uri.searchParams.get("change_type") ?? undefined;
1422
+ const api = await client.getRecentChanges(toQuery({
1423
+ change_type: changeType,
1424
+ page: 1,
1425
+ page_size: MAX_RECENT_CHANGES
1426
+ }));
1427
+ const data = readObject(api.data);
1428
+ const changes = readArray(data?.results).map((change) => sanitizeChange(change, config.webBaseUrl));
1429
+ return jsonResource(uri.toString(), withApiMetadata(api, {
1430
+ changes,
1431
+ meta: sanitizeJson(data?.meta),
1432
+ facets: sanitizeJson(data?.facets)
1433
+ }));
1434
+ });
1435
+ }
1436
+ async function resolveProgram(client, config, input) {
1437
+ const warnings = [];
1438
+ const budget = {
1439
+ initial: input.upstream_request_budget,
1440
+ remaining: input.upstream_request_budget
1441
+ };
1442
+ const maxPages = clampLimit("max_pages", input.max_pages, MAX_FIND_PROGRAM_PAGES, warnings);
1443
+ const collected = await collectPrograms(client, {
1444
+ platforms: input.platforms,
1445
+ tags: input.tags,
1446
+ updated_since: undefined,
1447
+ opportunity_levels: input.opportunity_levels,
1448
+ max_pages: maxPages,
1449
+ budget,
1450
+ warnings
1451
+ });
1452
+ const matches = collected.programs
1453
+ .map((program) => {
1454
+ const candidate = toProgramCandidate(program, config.webBaseUrl);
1455
+ const match = programResolutionMatch(candidate, input.query);
1456
+ return { candidate, match };
1457
+ })
1458
+ .filter((entry) => entry.match.score > 0)
1459
+ .sort((left, right) => right.match.score - left.match.score || compareProgramCandidates(left.candidate, right.candidate, "best_hunt_value"))
1460
+ .slice(0, input.max_results);
1461
+ const programs = matches.map((entry) => formatProgram(entry.candidate.program, input.output_mode));
1462
+ return stripUndefined({
1463
+ request_id: randomUUID(),
1464
+ source_requests: collected.sourceRequests,
1465
+ warnings: warnings.length > 0 ? warnings : undefined,
1466
+ query: {
1467
+ text: input.query,
1468
+ platforms: input.platforms.length > 0 ? input.platforms : undefined,
1469
+ tags: input.tags.length > 0 ? input.tags : undefined
1470
+ },
1471
+ programs,
1472
+ matches: matches.map((entry) => stripUndefined({
1473
+ program_id: stringField(entry.candidate.program, "id"),
1474
+ score: entry.match.score,
1475
+ matched_fields: entry.match.fields,
1476
+ reasons: entry.match.reasons,
1477
+ program: formatProgram(entry.candidate.program, input.output_mode)
1478
+ })),
1479
+ upstream_budget: {
1480
+ used: budget.initial - budget.remaining,
1481
+ remaining: budget.remaining
1482
+ },
1483
+ ranking_scope: {
1484
+ mode: "sampled",
1485
+ max_pages_scanned_per_source: maxPages,
1486
+ upstream_total_pages: collected.totalPagesBySource,
1487
+ possibly_incomplete: isRankingPossiblyIncomplete(collected.totalPagesBySource, maxPages)
1488
+ },
1489
+ meta: {
1490
+ returned: programs.length,
1491
+ programs_scanned: collected.programsScanned,
1492
+ upstream_requests_scanned: collected.requestsScanned
1493
+ }
1494
+ });
1495
+ }
1496
+ async function runProgramNameAction(client, config, input) {
1497
+ const resolution = await resolveProgram(client, config, {
1498
+ query: input.query,
1499
+ platforms: input.platforms,
1500
+ tags: input.tags,
1501
+ opportunity_levels: input.opportunity_levels,
1502
+ max_pages: input.max_resolve_pages,
1503
+ max_results: 1,
1504
+ output_mode: input.output_mode,
1505
+ upstream_request_budget: input.upstream_request_budget
1506
+ });
1507
+ const bestMatch = readObject(readArray(resolution.matches)[0]);
1508
+ const programId = stringField(bestMatch, "program_id");
1509
+ const resolvedProgram = readObject(bestMatch?.program);
1510
+ const warnings = readArray(resolution.warnings).filter((warning) => typeof warning === "string");
1511
+ if (!programId) {
1512
+ return stripUndefined({
1513
+ request_id: randomUUID(),
1514
+ source_requests: readArray(resolution.source_requests).filter((request) => readObject(request) !== undefined),
1515
+ warnings: [...warnings, "No matching program_id was resolved for the requested program name."],
1516
+ resolved_program: undefined,
1517
+ action: input.action,
1518
+ result: {
1519
+ found: false
1520
+ },
1521
+ upstream_budget: readObject(resolution.upstream_budget),
1522
+ ranking_scope: readObject(resolution.ranking_scope)
1523
+ });
1524
+ }
1525
+ const actionPayload = await runResolvedProgramAction(client, config, programId, input);
1526
+ const resolutionSourceRequests = readArray(resolution.source_requests).filter((request) => readObject(request) !== undefined);
1527
+ const actionSourceRequests = readArray(actionPayload.source_requests).filter((request) => readObject(request) !== undefined);
1528
+ return stripUndefined({
1529
+ request_id: randomUUID(),
1530
+ source_requests: [...resolutionSourceRequests, ...actionSourceRequests],
1531
+ warnings: warnings.length > 0 ? warnings : undefined,
1532
+ resolved_program: resolvedProgram,
1533
+ action: input.action,
1534
+ result: compactNestedActionResult(actionPayload),
1535
+ upstream_budget: readObject(resolution.upstream_budget),
1536
+ ranking_scope: readObject(resolution.ranking_scope)
1537
+ });
1538
+ }
1539
+ async function runResolvedProgramAction(client, config, programId, input) {
1540
+ if (input.action === "scope_delta") {
1541
+ return getProgramScopeDelta(client, config, {
1542
+ program_id: programId,
1543
+ since: input.since,
1544
+ change_type: undefined,
1545
+ target_type: input.target_type,
1546
+ language_tags: input.language_tags,
1547
+ include_removed: true,
1548
+ include_out_of_scope: input.include_out_of_scope,
1549
+ include_ineligible: input.include_ineligible,
1550
+ page_size: input.page_size,
1551
+ max_targets: input.max_targets,
1552
+ target_list_mode: input.target_list_mode
1553
+ });
1554
+ }
1555
+ if (input.action === "brief") {
1556
+ return getProgramBrief(client, config, {
1557
+ program_id: programId,
1558
+ target_sample_size: Math.min(input.max_targets, 50),
1559
+ target_list_mode: input.target_list_mode,
1560
+ include_recent_changes: true,
1561
+ recent_changes_limit: Math.min(input.page_size, MAX_RECENT_CHANGES),
1562
+ include_out_of_scope: input.include_out_of_scope,
1563
+ include_ineligible: input.include_ineligible
1564
+ });
1565
+ }
1566
+ if (input.action === "target_breakdown") {
1567
+ return getProgramTargetBreakdown(client, config, {
1568
+ program_id: programId,
1569
+ target_list_mode: input.target_list_mode,
1570
+ group_limit: input.group_limit,
1571
+ include_out_of_scope: input.include_out_of_scope,
1572
+ include_ineligible: input.include_ineligible
1573
+ });
1574
+ }
1575
+ return checkProgramNewTargets(client, config, {
1576
+ program_id: programId,
1577
+ since: input.since,
1578
+ target_type: input.target_type,
1579
+ include_ineligible: input.include_ineligible,
1580
+ page_size: input.page_size,
1581
+ max_targets: input.max_targets,
1582
+ target_list_mode: input.target_list_mode
1583
+ });
1584
+ }
1585
+ function compactNestedActionResult(payload) {
1586
+ const { source_requests: _sourceRequests, request_id: requestId, ...rest } = payload;
1587
+ return stripUndefined({
1588
+ action_request_id: typeof requestId === "string" ? requestId : undefined,
1589
+ ...rest
1590
+ });
1591
+ }
1592
+ function findProgramCandidates(client, config, input) {
1593
+ return findPrograms(client, config, {
1594
+ platforms: input.platforms,
1595
+ tags: input.tags,
1596
+ scope_tags: input.scope_tags,
1597
+ target_types: input.target_types,
1598
+ language_tags: input.language_tags,
1599
+ web3: input.web3,
1600
+ opportunity_levels: input.opportunity_levels,
1601
+ updated_since: input.updated_since,
1602
+ min_bounty_min: undefined,
1603
+ min_bounty_max: input.min_bounty_max,
1604
+ max_bounty_max: undefined,
1605
+ require_bounty: input.bounty,
1606
+ max_public_report_count: input.low_reports ? input.max_public_report_count : undefined,
1607
+ require_known_report_count: input.low_reports,
1608
+ include_unknown_report_count: false,
1609
+ exclude_ctf: input.exclude_ctf,
1610
+ exclude_mobile_only: input.exclude_mobile_only,
1611
+ exclude_non_web: input.exclude_non_web,
1612
+ has_api_scope: input.has_api_scope,
1613
+ fresh_launch_days: input.fresh_launch_days,
1614
+ min_wildcards: input.wildcard ? 1 : 0,
1615
+ min_eligible_targets: input.min_eligible_targets,
1616
+ min_total_targets: undefined,
1617
+ min_added_24h: undefined,
1618
+ min_added_7d: undefined,
1619
+ max_pages: input.max_pages,
1620
+ max_results: input.max_results,
1621
+ sort_by: "best_hunt_value",
1622
+ include_target_samples: input.include_target_samples,
1623
+ target_sample_programs: input.target_sample_programs,
1624
+ target_sample_size: input.target_sample_size,
1625
+ only_in_scope_targets: true,
1626
+ only_bounty_eligible_targets: true,
1627
+ target_samples_only_wildcards: input.wildcard,
1628
+ output_mode: input.output_mode ?? "compact",
1629
+ upstream_request_budget: input.upstream_request_budget
1630
+ });
1631
+ }
1632
+ function findRewardPrograms(client, config, input) {
1633
+ return findPrograms(client, config, {
1634
+ platforms: input.platforms,
1635
+ tags: input.tags,
1636
+ scope_tags: [],
1637
+ target_types: input.target_types,
1638
+ language_tags: input.language_tags,
1639
+ web3: input.web3,
1640
+ opportunity_levels: [],
1641
+ updated_since: undefined,
1642
+ min_bounty_min: input.reward_threshold_mode === "min_at_least" ? input.min_reward : undefined,
1643
+ min_bounty_max: input.reward_threshold_mode === "max_at_least" ? input.min_reward : undefined,
1644
+ max_bounty_max: undefined,
1645
+ require_bounty: true,
1646
+ max_public_report_count: input.max_public_report_count,
1647
+ require_known_report_count: input.require_known_report_count,
1648
+ include_unknown_report_count: input.include_unknown_report_count,
1649
+ exclude_ctf: input.exclude_ctf,
1650
+ exclude_mobile_only: input.exclude_mobile_only,
1651
+ exclude_non_web: input.exclude_non_web,
1652
+ has_api_scope: false,
1653
+ fresh_launch_days: undefined,
1654
+ min_wildcards: 0,
1655
+ min_eligible_targets: input.min_eligible_targets,
1656
+ min_total_targets: undefined,
1657
+ min_added_24h: undefined,
1658
+ min_added_7d: undefined,
1659
+ max_pages: input.max_pages,
1660
+ max_results: input.max_results,
1661
+ sort_by: "highest_reward",
1662
+ include_target_samples: input.include_target_samples,
1663
+ target_sample_programs: input.target_sample_programs,
1664
+ target_sample_size: input.target_sample_size,
1665
+ only_in_scope_targets: true,
1666
+ only_bounty_eligible_targets: true,
1667
+ target_samples_only_wildcards: false,
1668
+ output_mode: input.output_mode,
1669
+ upstream_request_budget: input.upstream_request_budget
1670
+ });
1671
+ }
1672
+ function findWeb3Contests(client, config, input) {
1673
+ return findPrograms(client, config, {
1674
+ platforms: input.platforms,
1675
+ tags: input.tags,
1676
+ scope_tags: [],
1677
+ target_types: [],
1678
+ language_tags: [],
1679
+ web3: true,
1680
+ opportunity_levels: [],
1681
+ updated_since: undefined,
1682
+ min_bounty_min: input.min_reward !== undefined && input.reward_threshold_mode === "min_at_least" ? input.min_reward : undefined,
1683
+ min_bounty_max: input.min_reward !== undefined && input.reward_threshold_mode === "max_at_least" ? input.min_reward : undefined,
1684
+ max_bounty_max: undefined,
1685
+ require_bounty: input.require_bounty,
1686
+ max_public_report_count: input.max_public_report_count,
1687
+ require_known_report_count: false,
1688
+ include_unknown_report_count: input.include_unknown_report_count,
1689
+ exclude_ctf: false,
1690
+ exclude_mobile_only: false,
1691
+ exclude_non_web: false,
1692
+ has_api_scope: false,
1693
+ contest_like: input.require_contest_signal,
1694
+ fresh_launch_days: undefined,
1695
+ min_wildcards: 0,
1696
+ min_eligible_targets: undefined,
1697
+ min_total_targets: undefined,
1698
+ min_added_24h: undefined,
1699
+ min_added_7d: undefined,
1700
+ max_pages: input.max_pages,
1701
+ max_results: input.max_results,
1702
+ sort_by: input.min_reward !== undefined ? "highest_reward" : "best_hunt_value",
1703
+ include_target_samples: input.include_target_samples,
1704
+ target_sample_programs: input.target_sample_programs,
1705
+ target_sample_size: input.target_sample_size,
1706
+ only_in_scope_targets: true,
1707
+ only_bounty_eligible_targets: true,
1708
+ target_samples_only_wildcards: false,
1709
+ output_mode: input.output_mode,
1710
+ upstream_request_budget: input.upstream_request_budget
1711
+ });
1712
+ }
1713
+ function findLanguagePrograms(client, config, input) {
1714
+ const rewardFilters = rewardThresholdFilters(input.min_reward, input.reward_threshold_mode);
1715
+ return findPrograms(client, config, {
1716
+ platforms: input.platforms,
1717
+ tags: input.tags,
1718
+ scope_tags: [],
1719
+ target_types: input.target_types,
1720
+ language_tags: input.language_tags,
1721
+ web3: false,
1722
+ opportunity_levels: [],
1723
+ updated_since: undefined,
1724
+ ...rewardFilters,
1725
+ max_bounty_max: undefined,
1726
+ require_bounty: input.require_bounty,
1727
+ max_public_report_count: input.max_public_report_count,
1728
+ require_known_report_count: input.require_known_report_count,
1729
+ include_unknown_report_count: input.include_unknown_report_count,
1730
+ exclude_ctf: true,
1731
+ exclude_mobile_only: false,
1732
+ exclude_non_web: false,
1733
+ has_api_scope: false,
1734
+ vdp_mode: input.exclude_vdp ? "exclude" : "include",
1735
+ fresh_launch_days: undefined,
1736
+ min_wildcards: 0,
1737
+ min_eligible_targets: input.min_eligible_targets,
1738
+ min_total_targets: undefined,
1739
+ min_added_24h: undefined,
1740
+ min_added_7d: undefined,
1741
+ max_pages: input.max_pages,
1742
+ max_results: input.max_results,
1743
+ sort_by: input.min_reward !== undefined ? "highest_reward" : "best_hunt_value",
1744
+ include_target_samples: input.include_target_samples,
1745
+ target_sample_programs: input.target_sample_programs,
1746
+ target_sample_size: input.target_sample_size,
1747
+ only_in_scope_targets: true,
1748
+ only_bounty_eligible_targets: true,
1749
+ target_samples_only_wildcards: false,
1750
+ output_mode: input.output_mode,
1751
+ upstream_request_budget: input.upstream_request_budget
1752
+ });
1753
+ }
1754
+ function findTargetTypePrograms(client, config, input) {
1755
+ const rewardFilters = rewardThresholdFilters(input.min_reward, input.reward_threshold_mode);
1756
+ return findPrograms(client, config, {
1757
+ platforms: input.platforms,
1758
+ tags: input.tags,
1759
+ scope_tags: input.scope_tags,
1760
+ target_types: input.target_types,
1761
+ language_tags: input.language_tags,
1762
+ web3: false,
1763
+ opportunity_levels: [],
1764
+ updated_since: undefined,
1765
+ ...rewardFilters,
1766
+ max_bounty_max: undefined,
1767
+ require_bounty: input.require_bounty,
1768
+ max_public_report_count: undefined,
1769
+ require_known_report_count: false,
1770
+ include_unknown_report_count: true,
1771
+ exclude_ctf: true,
1772
+ exclude_mobile_only: false,
1773
+ exclude_non_web: false,
1774
+ has_api_scope: input.target_types.some((targetType) => normalizeTag(targetType).includes("api")),
1775
+ vdp_mode: input.exclude_vdp ? "exclude" : "include",
1776
+ fresh_launch_days: undefined,
1777
+ min_wildcards: input.target_types.some((targetType) => normalizeTag(targetType).includes("wildcard")) ? 1 : 0,
1778
+ min_eligible_targets: undefined,
1779
+ min_total_targets: undefined,
1780
+ min_added_24h: undefined,
1781
+ min_added_7d: input.fresh_only ? input.min_added_7d ?? 1 : input.min_added_7d,
1782
+ max_pages: input.max_pages,
1783
+ max_results: input.max_results,
1784
+ sort_by: input.fresh_only ? "most_new_targets" : input.min_reward !== undefined ? "highest_reward" : "best_hunt_value",
1785
+ include_target_samples: input.include_target_samples,
1786
+ target_sample_programs: input.target_sample_programs,
1787
+ target_sample_size: input.target_sample_size,
1788
+ only_in_scope_targets: true,
1789
+ only_bounty_eligible_targets: true,
1790
+ target_samples_only_wildcards: input.target_types.some((targetType) => normalizeTag(targetType).includes("wildcard")),
1791
+ output_mode: input.output_mode,
1792
+ upstream_request_budget: input.upstream_request_budget
1793
+ });
1794
+ }
1795
+ function findVdpPrograms(client, config, input) {
1796
+ return findPrograms(client, config, {
1797
+ platforms: input.platforms,
1798
+ tags: input.tags,
1799
+ scope_tags: [],
1800
+ target_types: input.target_types,
1801
+ language_tags: input.language_tags,
1802
+ web3: false,
1803
+ opportunity_levels: [],
1804
+ updated_since: undefined,
1805
+ min_bounty_min: undefined,
1806
+ min_bounty_max: undefined,
1807
+ max_bounty_max: undefined,
1808
+ require_bounty: false,
1809
+ max_public_report_count: undefined,
1810
+ require_known_report_count: false,
1811
+ include_unknown_report_count: true,
1812
+ exclude_ctf: true,
1813
+ exclude_mobile_only: false,
1814
+ exclude_non_web: false,
1815
+ has_api_scope: false,
1816
+ vdp_mode: input.vdp_mode,
1817
+ fresh_launch_days: undefined,
1818
+ min_wildcards: 0,
1819
+ min_eligible_targets: undefined,
1820
+ min_total_targets: undefined,
1821
+ min_added_24h: undefined,
1822
+ min_added_7d: undefined,
1823
+ max_pages: input.max_pages,
1824
+ max_results: input.max_results,
1825
+ sort_by: "best_hunt_value",
1826
+ include_target_samples: input.include_target_samples,
1827
+ target_sample_programs: input.target_sample_programs,
1828
+ target_sample_size: input.target_sample_size,
1829
+ only_in_scope_targets: true,
1830
+ only_bounty_eligible_targets: false,
1831
+ target_samples_only_wildcards: false,
1832
+ output_mode: input.output_mode,
1833
+ upstream_request_budget: input.upstream_request_budget
1834
+ });
1835
+ }
1836
+ function findPaidPrograms(client, config, input) {
1837
+ const rewardFilters = rewardThresholdFilters(input.min_reward, input.reward_threshold_mode);
1838
+ return findPrograms(client, config, {
1839
+ platforms: input.platforms,
1840
+ tags: input.tags,
1841
+ scope_tags: [],
1842
+ target_types: input.target_types,
1843
+ language_tags: input.language_tags,
1844
+ web3: false,
1845
+ opportunity_levels: [],
1846
+ updated_since: undefined,
1847
+ ...rewardFilters,
1848
+ max_bounty_max: undefined,
1849
+ require_bounty: true,
1850
+ max_public_report_count: input.max_public_report_count,
1851
+ require_known_report_count: false,
1852
+ include_unknown_report_count: true,
1853
+ exclude_ctf: true,
1854
+ exclude_mobile_only: false,
1855
+ exclude_non_web: false,
1856
+ has_api_scope: false,
1857
+ vdp_mode: "exclude",
1858
+ fresh_launch_days: undefined,
1859
+ min_wildcards: 0,
1860
+ min_eligible_targets: input.min_eligible_targets,
1861
+ min_total_targets: undefined,
1862
+ min_added_24h: undefined,
1863
+ min_added_7d: undefined,
1864
+ max_pages: input.max_pages,
1865
+ max_results: input.max_results,
1866
+ sort_by: input.min_reward !== undefined ? "highest_reward" : "best_hunt_value",
1867
+ include_target_samples: input.include_target_samples,
1868
+ target_sample_programs: input.target_sample_programs,
1869
+ target_sample_size: input.target_sample_size,
1870
+ only_in_scope_targets: true,
1871
+ only_bounty_eligible_targets: true,
1872
+ target_samples_only_wildcards: false,
1873
+ output_mode: input.output_mode,
1874
+ upstream_request_budget: input.upstream_request_budget
1875
+ });
1876
+ }
1877
+ function findStackMatches(client, config, input) {
1878
+ const rewardFilters = rewardThresholdFilters(input.min_reward, input.reward_threshold_mode);
1879
+ return findPrograms(client, config, {
1880
+ platforms: input.platforms,
1881
+ tags: input.tags,
1882
+ scope_tags: input.scope_tags,
1883
+ target_types: input.target_types,
1884
+ language_tags: input.language_tags,
1885
+ web3: input.web3,
1886
+ opportunity_levels: [],
1887
+ updated_since: undefined,
1888
+ ...rewardFilters,
1889
+ max_bounty_max: undefined,
1890
+ require_bounty: input.require_bounty,
1891
+ max_public_report_count: input.max_public_report_count,
1892
+ require_known_report_count: false,
1893
+ include_unknown_report_count: true,
1894
+ exclude_ctf: true,
1895
+ exclude_mobile_only: false,
1896
+ exclude_non_web: false,
1897
+ has_api_scope: input.target_types.some((targetType) => normalizeTag(targetType).includes("api")),
1898
+ contest_like: input.contest_like,
1899
+ vdp_mode: input.exclude_vdp ? "exclude" : "include",
1900
+ fresh_launch_days: undefined,
1901
+ min_wildcards: input.target_types.some((targetType) => normalizeTag(targetType).includes("wildcard")) ? 1 : 0,
1902
+ min_eligible_targets: input.min_eligible_targets,
1903
+ min_total_targets: undefined,
1904
+ min_added_24h: undefined,
1905
+ min_added_7d: input.fresh_only ? input.min_added_7d ?? 1 : input.min_added_7d,
1906
+ max_pages: input.max_pages,
1907
+ max_results: input.max_results,
1908
+ sort_by: input.fresh_only ? "most_new_targets" : input.min_reward !== undefined ? "highest_reward" : "best_hunt_value",
1909
+ include_target_samples: input.include_target_samples,
1910
+ target_sample_programs: input.target_sample_programs,
1911
+ target_sample_size: input.target_sample_size,
1912
+ only_in_scope_targets: true,
1913
+ only_bounty_eligible_targets: true,
1914
+ target_samples_only_wildcards: input.target_types.some((targetType) => normalizeTag(targetType).includes("wildcard")),
1915
+ output_mode: input.output_mode,
1916
+ upstream_request_budget: input.upstream_request_budget
1917
+ });
1918
+ }
1919
+ function findLowNoisePrograms(client, config, input) {
1920
+ const rewardFilters = rewardThresholdFilters(input.min_reward, input.reward_threshold_mode);
1921
+ return findPrograms(client, config, {
1922
+ platforms: input.platforms,
1923
+ tags: input.tags,
1924
+ scope_tags: [],
1925
+ target_types: input.target_types,
1926
+ language_tags: input.language_tags,
1927
+ web3: false,
1928
+ opportunity_levels: [],
1929
+ updated_since: undefined,
1930
+ ...rewardFilters,
1931
+ max_bounty_max: undefined,
1932
+ require_bounty: input.require_bounty,
1933
+ max_public_report_count: input.max_public_report_count,
1934
+ require_known_report_count: input.require_known_report_count,
1935
+ include_unknown_report_count: input.include_unknown_report_count,
1936
+ exclude_ctf: true,
1937
+ exclude_mobile_only: false,
1938
+ exclude_non_web: false,
1939
+ has_api_scope: false,
1940
+ vdp_mode: input.require_bounty ? "exclude" : "include",
1941
+ fresh_launch_days: undefined,
1942
+ min_wildcards: 0,
1943
+ min_eligible_targets: input.min_eligible_targets,
1944
+ min_total_targets: undefined,
1945
+ min_added_24h: undefined,
1946
+ min_added_7d: undefined,
1947
+ max_pages: input.max_pages,
1948
+ max_results: input.max_results,
1949
+ sort_by: input.min_reward !== undefined ? "highest_reward" : "best_hunt_value",
1950
+ include_target_samples: input.include_target_samples,
1951
+ target_sample_programs: input.target_sample_programs,
1952
+ target_sample_size: input.target_sample_size,
1953
+ only_in_scope_targets: true,
1954
+ only_bounty_eligible_targets: true,
1955
+ target_samples_only_wildcards: false,
1956
+ output_mode: input.output_mode,
1957
+ upstream_request_budget: input.upstream_request_budget
1958
+ });
1959
+ }
1960
+ function findRecentByType(client, config, input) {
1961
+ return getRecentTargetActivity(client, config, {
1962
+ change_type: input.change_type,
1963
+ target_type: input.target_type,
1964
+ include_removed: input.include_removed || input.change_type === "removed",
1965
+ include_out_of_scope: input.include_out_of_scope,
1966
+ include_ineligible: input.include_ineligible,
1967
+ search: input.search,
1968
+ platforms: input.platforms,
1969
+ tags: input.tags,
1970
+ language_tags: input.language_tags,
1971
+ page_size: input.page_size,
1972
+ max_programs: input.max_programs,
1973
+ sample_size: input.sample_size,
1974
+ target_list_mode: input.target_list_mode
1975
+ });
1976
+ }
1977
+ async function findProgramsByTarget(client, config, input) {
1978
+ const warnings = [];
1979
+ if (input.use_api_search) {
1980
+ const directResult = await searchProgramsByTargetApi(client, config, input, warnings);
1981
+ if (directResult) {
1982
+ return directResult;
1983
+ }
1984
+ }
1985
+ const budget = {
1986
+ initial: input.upstream_request_budget,
1987
+ remaining: input.upstream_request_budget
1988
+ };
1989
+ const maxPages = clampLimit("max_pages", input.max_pages, MAX_FIND_PROGRAM_PAGES, warnings);
1990
+ const collected = await collectPrograms(client, {
1991
+ platforms: input.platforms,
1992
+ tags: input.tags,
1993
+ updated_since: undefined,
1994
+ opportunity_levels: [],
1995
+ max_pages: maxPages,
1996
+ budget,
1997
+ warnings
1998
+ });
1999
+ const sourceRequests = [...collected.sourceRequests];
2000
+ const matches = [];
2001
+ const candidates = collected.programs
2002
+ .map((program) => toProgramCandidate(program, config.webBaseUrl))
2003
+ .sort((left, right) => compareProgramCandidates(left, right, "best_hunt_value"));
2004
+ for (const candidate of candidates) {
2005
+ if (matches.length >= input.max_results) {
2006
+ break;
2007
+ }
2008
+ const programId = stringField(candidate.program, "id");
2009
+ if (!programId) {
2010
+ continue;
2011
+ }
2012
+ if (!tryConsumeBudget(budget)) {
2013
+ addUniqueWarning(warnings, "Upstream request budget was exhausted before all candidate target lists could be checked.");
2014
+ break;
2015
+ }
2016
+ const targetsApi = await client.getProgramTargets(programId);
2017
+ refundBudgetIfNoUpstreamRequest(budget, targetsApi);
2018
+ sourceRequests.push({
2019
+ source: "program_targets",
2020
+ program_id: programId,
2021
+ request_id: targetsApi.requestId,
2022
+ upstream_request_id: targetsApi.upstreamRequestId,
2023
+ ...apiSourceMetadata(targetsApi)
2024
+ });
2025
+ const targetsData = readObject(targetsApi.data);
2026
+ const targets = readArray(targetsData?.targets)
2027
+ .map(sanitizeTarget)
2028
+ .filter((target) => targetHasAllowedScope(target, {
2029
+ include_out_of_scope: input.include_out_of_scope,
2030
+ include_ineligible: input.include_ineligible,
2031
+ strict_scope_filter: false
2032
+ }));
2033
+ const matchingTargets = targets.filter((target) => targetMatchesQuery(target, input.target_query, input.match_mode));
2034
+ if (matchingTargets.length === 0) {
2035
+ continue;
2036
+ }
2037
+ matches.push(stripUndefined({
2038
+ program: formatProgram(candidate.program, "compact"),
2039
+ match_count: matchingTargets.length,
2040
+ matching_targets: formatTargetList(matchingTargets.slice(0, input.max_targets_per_program), input.target_list_mode),
2041
+ matching_targets_has_more: matchingTargets.length > input.max_targets_per_program,
2042
+ total_targets_checked: targets.length
2043
+ }));
2044
+ }
2045
+ return stripUndefined({
2046
+ request_id: randomUUID(),
2047
+ source_requests: sourceRequests,
2048
+ warnings: warnings.length > 0 ? warnings : undefined,
2049
+ filters: {
2050
+ target_query: input.target_query,
2051
+ match_mode: input.match_mode,
2052
+ platforms: input.platforms.length > 0 ? input.platforms : undefined,
2053
+ tags: input.tags.length > 0 ? input.tags : undefined,
2054
+ source: "sampled_program_targets",
2055
+ include_out_of_scope: input.include_out_of_scope,
2056
+ include_ineligible: input.include_ineligible,
2057
+ target_list_mode: input.target_list_mode
2058
+ },
2059
+ matches,
2060
+ programs: matches.map((match) => readObject(match.program)).filter((program) => program !== undefined),
2061
+ upstream_budget: {
2062
+ used: budget.initial - budget.remaining,
2063
+ remaining: budget.remaining
2064
+ },
2065
+ ranking_scope: {
2066
+ mode: "sampled",
2067
+ max_pages_scanned_per_source: maxPages,
2068
+ upstream_total_pages: collected.totalPagesBySource,
2069
+ possibly_incomplete: isRankingPossiblyIncomplete(collected.totalPagesBySource, maxPages)
2070
+ },
2071
+ meta: {
2072
+ returned: matches.length,
2073
+ programs_scanned: collected.programsScanned,
2074
+ target_lists_checked: sourceRequests.filter((request) => request.source === "program_targets").length
2075
+ }
2076
+ });
2077
+ }
2078
+ async function searchProgramsByTargetApi(client, config, input, warnings) {
2079
+ let api;
2080
+ try {
2081
+ api = await client.searchTargets({
2082
+ q: input.target_query,
2083
+ match_mode: input.match_mode,
2084
+ platforms: input.platforms,
2085
+ tags: input.tags,
2086
+ include_out_of_scope: input.include_out_of_scope,
2087
+ include_ineligible: input.include_ineligible,
2088
+ page: 1,
2089
+ page_size: Math.max(input.max_results * input.max_targets_per_program, input.max_results)
2090
+ });
2091
+ }
2092
+ catch (error) {
2093
+ if (error instanceof BBRadarApiError && (error.status === 404 || error.status === 405)) {
2094
+ warnings.push("Direct target search API is unavailable; fell back to sampled program target lists.");
2095
+ return undefined;
2096
+ }
2097
+ throw error;
2098
+ }
2099
+ const data = readObject(api.data);
2100
+ const rawMatches = readTargetSearchRows(data);
2101
+ const grouped = new Map();
2102
+ for (const row of rawMatches) {
2103
+ const program = addProgramResourceLinks(sanitizeProgram(readObject(row.program) ?? readObject(readObject(row.target)?.program) ?? row, config.webBaseUrl));
2104
+ const programId = stringField(program, "id") ?? stringField(row, "program_id");
2105
+ const target = sanitizeTarget(readObject(row.target) ?? row);
2106
+ if (!programId || !targetIdentifier(target)) {
2107
+ continue;
2108
+ }
2109
+ if (!targetHasAllowedScope(target, {
2110
+ include_out_of_scope: input.include_out_of_scope,
2111
+ include_ineligible: input.include_ineligible,
2112
+ strict_scope_filter: false
2113
+ })) {
2114
+ continue;
2115
+ }
2116
+ if (!targetMatchesQuery(target, input.target_query, input.match_mode)) {
2117
+ continue;
2118
+ }
2119
+ const existing = grouped.get(programId) ?? { program: { ...program, id: programId }, targets: [] };
2120
+ existing.targets.push(target);
2121
+ grouped.set(programId, existing);
2122
+ }
2123
+ const matches = [...grouped.values()].slice(0, input.max_results).map((entry) => stripUndefined({
2124
+ program: formatProgram(entry.program, "compact"),
2125
+ match_count: entry.targets.length,
2126
+ matching_targets: formatTargetList(entry.targets.slice(0, input.max_targets_per_program), input.target_list_mode),
2127
+ matching_targets_has_more: entry.targets.length > input.max_targets_per_program
2128
+ }));
2129
+ const sanitizedMeta = readObject(sanitizeJson(data?.meta));
2130
+ return stripUndefined({
2131
+ request_id: randomUUID(),
2132
+ source_requests: [
2133
+ {
2134
+ source: "target_search",
2135
+ request_id: api.requestId,
2136
+ upstream_request_id: api.upstreamRequestId,
2137
+ ...apiSourceMetadata(api)
2138
+ }
2139
+ ],
2140
+ warnings: warnings.length > 0 ? warnings : undefined,
2141
+ filters: {
2142
+ target_query: input.target_query,
2143
+ match_mode: input.match_mode,
2144
+ platforms: input.platforms.length > 0 ? input.platforms : undefined,
2145
+ tags: input.tags.length > 0 ? input.tags : undefined,
2146
+ source: "target_search_api",
2147
+ include_out_of_scope: input.include_out_of_scope,
2148
+ include_ineligible: input.include_ineligible,
2149
+ target_list_mode: input.target_list_mode
2150
+ },
2151
+ matches,
2152
+ programs: matches.map((match) => readObject(match.program)).filter((program) => program !== undefined),
2153
+ meta: stripUndefined({
2154
+ ...(sanitizedMeta ?? {}),
2155
+ returned: matches.length,
2156
+ target_search_rows_scanned: rawMatches.length
2157
+ })
2158
+ });
2159
+ }
2160
+ function readTargetSearchRows(data) {
2161
+ return [data?.results, data?.matches, data?.targets]
2162
+ .map(readArray)
2163
+ .find((rows) => rows.length > 0)
2164
+ ?.map((row) => readObject(row))
2165
+ .filter((row) => row !== undefined) ?? [];
2166
+ }
2167
+ function rewardThresholdFilters(minReward, mode) {
2168
+ return {
2169
+ min_bounty_min: minReward !== undefined && mode === "min_at_least" ? minReward : undefined,
2170
+ min_bounty_max: minReward !== undefined && mode === "max_at_least" ? minReward : undefined
2171
+ };
2172
+ }
2173
+ async function listFilters(client, input) {
2174
+ const budget = {
2175
+ initial: input.upstream_request_budget,
2176
+ remaining: input.upstream_request_budget
2177
+ };
2178
+ const collected = await collectPrograms(client, {
2179
+ platforms: input.platforms,
2180
+ tags: input.tags,
2181
+ updated_since: undefined,
2182
+ opportunity_levels: [],
2183
+ max_pages: input.max_pages,
2184
+ budget
2185
+ });
2186
+ const platforms = new Set();
2187
+ const scopeTags = new Set();
2188
+ const targetTypes = new Set();
2189
+ const languageTags = new Set();
2190
+ const sourceRequests = [...collected.sourceRequests];
2191
+ for (const rawProgram of collected.programs) {
2192
+ const program = sanitizeProgram(rawProgram, "");
2193
+ addSetValue(platforms, stringField(program, "platform"));
2194
+ addSetValues(scopeTags, readStringArrayField(readObject(program.scope_summary) ?? {}, "tags"));
2195
+ }
2196
+ try {
2197
+ if (tryConsumeBudget(budget)) {
2198
+ const changesApi = await client.getRecentChanges({ page: 1, page_size: MAX_RECENT_CHANGES });
2199
+ refundBudgetIfNoUpstreamRequest(budget, changesApi);
2200
+ sourceRequests.push({
2201
+ source: "recent_changes_facets",
2202
+ request_id: changesApi.requestId,
2203
+ upstream_request_id: changesApi.upstreamRequestId,
2204
+ ...apiSourceMetadata(changesApi)
2205
+ });
2206
+ const changesData = readObject(changesApi.data);
2207
+ const facets = readObject(changesData?.facets);
2208
+ for (const platform of readFacetValues(facets?.platforms)) {
2209
+ addSetValue(platforms, platform);
2210
+ }
2211
+ for (const tag of readFacetValues(facets?.tags)) {
2212
+ addSetValue(scopeTags, tag);
2213
+ }
2214
+ }
2215
+ }
2216
+ catch {
2217
+ // Facets are best-effort; list_filters should still return observed program filters.
2218
+ }
2219
+ if (input.include_target_facets && input.target_sample_programs > 0) {
2220
+ const programIds = collected.programs
2221
+ .map((program) => readString(readObject(program)?.id))
2222
+ .filter((id) => id !== undefined)
2223
+ .slice(0, input.target_sample_programs);
2224
+ await mapWithConcurrency(programIds, TARGET_SAMPLE_CONCURRENCY, async (programId) => {
2225
+ if (!tryConsumeBudget(budget)) {
2226
+ return;
2227
+ }
2228
+ const targetsApi = await client.getProgramTargets(programId);
2229
+ refundBudgetIfNoUpstreamRequest(budget, targetsApi);
2230
+ const targetsData = readObject(targetsApi.data);
2231
+ sourceRequests.push({
2232
+ source: "target_facets",
2233
+ program_id: programId,
2234
+ request_id: targetsApi.requestId,
2235
+ upstream_request_id: targetsApi.upstreamRequestId,
2236
+ ...apiSourceMetadata(targetsApi)
2237
+ });
2238
+ for (const target of readArray(targetsData?.targets).map(sanitizeTarget)) {
2239
+ addSetValue(targetTypes, stringField(target, "target_type"));
2240
+ addSetValues(scopeTags, readStringArrayField(target, "scope_tags"));
2241
+ addSetValues(languageTags, readStringArrayField(target, "language_tags"));
2242
+ }
2243
+ });
2244
+ }
2245
+ return {
2246
+ request_id: randomUUID(),
2247
+ filters: {
2248
+ platforms: sortedValues(platforms),
2249
+ tags: sortedValues(scopeTags),
2250
+ scope_tags: sortedValues(scopeTags),
2251
+ target_types: sortedValues(targetTypes),
2252
+ language_tags: sortedValues(languageTags),
2253
+ opportunity_levels: opportunityLevelSchema.options
2254
+ },
2255
+ observed_from: {
2256
+ programs_scanned: collected.programsScanned,
2257
+ max_pages_scanned: input.max_pages,
2258
+ target_facets_enabled: input.include_target_facets,
2259
+ source_requests: sourceRequests
2260
+ },
2261
+ upstream_budget: {
2262
+ used: budget.initial - budget.remaining,
2263
+ remaining: budget.remaining
2264
+ }
2265
+ };
2266
+ }
2267
+ async function comparePrograms(client, config, input) {
2268
+ const candidates = [];
2269
+ const sourceRequests = [];
2270
+ const errors = [];
2271
+ await mapWithConcurrency(input.program_ids, TARGET_SAMPLE_CONCURRENCY, async (programId) => {
2272
+ try {
2273
+ const api = await client.getProgram(programId);
2274
+ candidates.push(toProgramCandidate(api.data, config.webBaseUrl));
2275
+ sourceRequests.push({
2276
+ source: "program",
2277
+ program_id: programId,
2278
+ request_id: api.requestId,
2279
+ upstream_request_id: api.upstreamRequestId,
2280
+ ...apiSourceMetadata(api)
2281
+ });
2282
+ }
2283
+ catch (error) {
2284
+ errors.push(programFetchError(programId, error));
2285
+ }
2286
+ });
2287
+ candidates.sort((left, right) => compareProgramCandidates(left, right, "best_hunt_value"));
2288
+ const sampleInput = {
2289
+ platforms: [],
2290
+ tags: [],
2291
+ scope_tags: [],
2292
+ target_types: [],
2293
+ language_tags: [],
2294
+ web3: false,
2295
+ opportunity_levels: [],
2296
+ updated_since: undefined,
2297
+ min_bounty_min: undefined,
2298
+ min_bounty_max: undefined,
2299
+ max_bounty_max: undefined,
2300
+ require_bounty: false,
2301
+ max_public_report_count: undefined,
2302
+ require_known_report_count: false,
2303
+ include_unknown_report_count: true,
2304
+ exclude_ctf: false,
2305
+ exclude_mobile_only: false,
2306
+ exclude_non_web: false,
2307
+ has_api_scope: false,
2308
+ fresh_launch_days: undefined,
2309
+ min_wildcards: 0,
2310
+ min_eligible_targets: undefined,
2311
+ min_total_targets: undefined,
2312
+ min_added_24h: undefined,
2313
+ min_added_7d: undefined,
2314
+ max_pages: 1,
2315
+ max_results: candidates.length,
2316
+ sort_by: "best_match",
2317
+ include_target_samples: input.include_target_samples,
2318
+ target_sample_programs: input.include_target_samples ? candidates.length : 0,
2319
+ target_sample_size: input.target_sample_size,
2320
+ only_in_scope_targets: input.only_in_scope_targets,
2321
+ only_bounty_eligible_targets: input.only_bounty_eligible_targets,
2322
+ target_samples_only_wildcards: false,
2323
+ output_mode: input.output_mode ?? "compact",
2324
+ upstream_request_budget: Math.max(candidates.length + 1, DEFAULT_FIND_UPSTREAM_REQUEST_BUDGET)
2325
+ };
2326
+ const budget = {
2327
+ initial: sampleInput.upstream_request_budget,
2328
+ remaining: sampleInput.upstream_request_budget
2329
+ };
2330
+ const targetSamples = input.include_target_samples ? await collectTargetSamples(client, candidates, sampleInput, budget) : {};
2331
+ return stripUndefined({
2332
+ request_id: randomUUID(),
2333
+ partial_success: errors.length > 0,
2334
+ program_ids_requested: input.program_ids,
2335
+ failed_program_ids: errors.map((error) => error.program_id).filter((id) => typeof id === "string"),
2336
+ errors: errors.length > 0 ? errors : undefined,
2337
+ source_requests: sourceRequests,
2338
+ compared_programs: candidates.map((candidate, index) => candidateOutput(candidate, index, targetSamples, input.output_mode ?? "compact")),
2339
+ upstream_budget: {
2340
+ used: budget.initial - budget.remaining,
2341
+ remaining: budget.remaining
2342
+ }
2343
+ });
2344
+ }
2345
+ async function summarizeProgramActivity(client, config, programId, recentChangesLimit) {
2346
+ const programApi = await client.getProgram(programId);
2347
+ const program = addProgramResourceLinks(sanitizeProgram(programApi.data, config.webBaseUrl));
2348
+ const scopeSummary = readObject(program.scope_summary);
2349
+ const targetActivity = readObject(scopeSummary?.target_activity);
2350
+ const changes = recentChangesLimit > 0 ? await fetchProgramChanges(client, config, programId, recentChangesLimit, true, true, true) : undefined;
2351
+ return {
2352
+ request_id: randomUUID(),
2353
+ source_requests: [
2354
+ {
2355
+ source: "program",
2356
+ request_id: programApi.requestId,
2357
+ upstream_request_id: programApi.upstreamRequestId,
2358
+ ...apiSourceMetadata(programApi)
2359
+ },
2360
+ ...(changes?.sourceRequests ?? [])
2361
+ ],
2362
+ program,
2363
+ activity: stripUndefined({
2364
+ target_counts: readObject(scopeSummary?.target_counts),
2365
+ target_activity: targetActivity,
2366
+ recent_change_count: changes?.changes.length
2367
+ }),
2368
+ changes: changes?.changes ?? []
2369
+ };
2370
+ }
2371
+ async function getLatestAddedTargets(client, config, input) {
2372
+ const recentApi = await client.getRecentChanges({
2373
+ change_type: "added",
2374
+ include_removed: false,
2375
+ include_ineligible: input.include_ineligible,
2376
+ include_out_of_scope: input.include_out_of_scope,
2377
+ tags: [input.target_type],
2378
+ page: 1,
2379
+ page_size: input.recent_changes_page_size
2380
+ });
2381
+ const recentData = readObject(recentApi.data);
2382
+ const changes = readArray(recentData?.results).map((change) => sanitizeChange(change, config.webBaseUrl));
2383
+ const matchingChanges = changes
2384
+ .filter((change) => {
2385
+ const target = readObject(change.target);
2386
+ const programId = stringField(readObject(change.program), "id");
2387
+ return (stringField(change, "change_type") === "added" &&
2388
+ programId !== undefined &&
2389
+ target !== undefined &&
2390
+ targetMatchesRequestedType(target, input.target_type) &&
2391
+ targetHasAllowedScope(target, {
2392
+ include_out_of_scope: input.include_out_of_scope,
2393
+ include_ineligible: input.include_ineligible,
2394
+ strict_scope_filter: true
2395
+ }));
2396
+ })
2397
+ .sort((left, right) => timestampField(right, "changed_at") - timestampField(left, "changed_at"));
2398
+ const latest = matchingChanges[0];
2399
+ const sourceRequests = [
2400
+ {
2401
+ source: "recent_changes",
2402
+ request_id: recentApi.requestId,
2403
+ upstream_request_id: recentApi.upstreamRequestId,
2404
+ ...apiSourceMetadata(recentApi)
2405
+ }
2406
+ ];
2407
+ if (!latest) {
2408
+ return {
2409
+ request_id: randomUUID(),
2410
+ source_requests: sourceRequests,
2411
+ query: latestAddedTargetsQuery(input),
2412
+ new_targets: [],
2413
+ meta: {
2414
+ recent_changes_scanned: changes.length,
2415
+ no_match: true
2416
+ }
2417
+ };
2418
+ }
2419
+ const latestAddedAt = stringField(latest, "changed_at");
2420
+ const programId = stringField(readObject(latest.program), "id");
2421
+ const program = addProgramResourceLinks(readObject(latest.program) ?? {});
2422
+ const latestNewTargets = matchingChanges
2423
+ .filter((change) => stringField(change, "changed_at") === latestAddedAt && stringField(readObject(change.program), "id") === programId)
2424
+ .map((change) => readObject(change.target))
2425
+ .filter((target) => target !== undefined);
2426
+ let fullTargets = [];
2427
+ let totalActiveTargets;
2428
+ let totalAfterFilters;
2429
+ if (input.include_full_target_list && programId) {
2430
+ const targetsApi = await client.getProgramTargets(programId);
2431
+ const targetsData = readObject(targetsApi.data);
2432
+ const rawTargets = readArray(targetsData?.targets);
2433
+ fullTargets = rawTargets
2434
+ .map(sanitizeTarget)
2435
+ .filter((target) => targetHasAllowedScope(target, {
2436
+ include_out_of_scope: input.full_list_include_out_of_scope,
2437
+ include_ineligible: input.full_list_include_ineligible,
2438
+ strict_scope_filter: false
2439
+ }));
2440
+ totalActiveTargets = rawTargets.length;
2441
+ totalAfterFilters = fullTargets.length;
2442
+ sourceRequests.push({
2443
+ source: "program_targets",
2444
+ program_id: programId,
2445
+ request_id: targetsApi.requestId,
2446
+ upstream_request_id: targetsApi.upstreamRequestId,
2447
+ ...apiSourceMetadata(targetsApi)
2448
+ });
2449
+ }
2450
+ const limitedFullTargets = fullTargets.slice(0, input.full_target_limit);
2451
+ return stripUndefined({
2452
+ request_id: randomUUID(),
2453
+ source_requests: sourceRequests,
2454
+ query: latestAddedTargetsQuery(input),
2455
+ latest_added_at: latestAddedAt,
2456
+ program,
2457
+ new_targets: formatTargetList(latestNewTargets, input.full_target_list_mode),
2458
+ full_targets: input.include_full_target_list ? formatTargetList(limitedFullTargets, input.full_target_list_mode) : undefined,
2459
+ meta: stripUndefined({
2460
+ recent_changes_scanned: changes.length,
2461
+ matching_changes_scanned: matchingChanges.length,
2462
+ new_targets_returned: formatTargetList(latestNewTargets, input.full_target_list_mode).length,
2463
+ full_targets_total_active: totalActiveTargets,
2464
+ full_targets_total_after_filters: totalAfterFilters,
2465
+ full_targets_returned: input.include_full_target_list ? formatTargetList(limitedFullTargets, input.full_target_list_mode).length : undefined,
2466
+ full_targets_has_more: input.include_full_target_list ? limitedFullTargets.length < fullTargets.length : undefined
2467
+ })
2468
+ });
2469
+ }
2470
+ async function getProgramScopeSummary(client, config, input) {
2471
+ const [programApi, targetsApi] = await Promise.all([client.getProgram(input.program_id), client.getProgramTargets(input.program_id)]);
2472
+ const targetsData = readObject(targetsApi.data);
2473
+ const rawTargets = readArray(targetsData?.targets);
2474
+ const targets = rawTargets.map(sanitizeTarget);
2475
+ return {
2476
+ request_id: randomUUID(),
2477
+ source_requests: [
2478
+ {
2479
+ source: "program",
2480
+ request_id: programApi.requestId,
2481
+ upstream_request_id: programApi.upstreamRequestId,
2482
+ ...apiSourceMetadata(programApi)
2483
+ },
2484
+ {
2485
+ source: "program_targets",
2486
+ request_id: targetsApi.requestId,
2487
+ upstream_request_id: targetsApi.upstreamRequestId,
2488
+ ...apiSourceMetadata(targetsApi)
2489
+ }
2490
+ ],
2491
+ program: formatProgram(addProgramResourceLinks(sanitizeProgram(programApi.data, config.webBaseUrl)), "compact"),
2492
+ scope: buildTargetScopeSummary(targets, input.target_list_mode, input.group_limit, input.include_out_of_scope, input.include_ineligible),
2493
+ meta: {
2494
+ total_active_targets: rawTargets.length,
2495
+ target_list_mode: input.target_list_mode,
2496
+ group_limit: input.group_limit
2497
+ }
2498
+ };
2499
+ }
2500
+ async function getProgramTargetBreakdown(client, config, input) {
2501
+ const [programApi, targetsApi] = await Promise.all([client.getProgram(input.program_id), client.getProgramTargets(input.program_id)]);
2502
+ const targetsData = readObject(targetsApi.data);
2503
+ const rawTargets = readArray(targetsData?.targets);
2504
+ const targets = rawTargets
2505
+ .map(sanitizeTarget)
2506
+ .filter((target) => targetHasAllowedScope(target, {
2507
+ include_out_of_scope: input.include_out_of_scope,
2508
+ include_ineligible: input.include_ineligible,
2509
+ strict_scope_filter: false
2510
+ }));
2511
+ const scope = buildTargetScopeSummary(targets, input.target_list_mode, input.group_limit, input.include_out_of_scope, input.include_ineligible);
2512
+ return {
2513
+ request_id: randomUUID(),
2514
+ source_requests: [
2515
+ {
2516
+ source: "program",
2517
+ request_id: programApi.requestId,
2518
+ upstream_request_id: programApi.upstreamRequestId,
2519
+ ...apiSourceMetadata(programApi)
2520
+ },
2521
+ {
2522
+ source: "program_targets",
2523
+ request_id: targetsApi.requestId,
2524
+ upstream_request_id: targetsApi.upstreamRequestId,
2525
+ ...apiSourceMetadata(targetsApi)
2526
+ }
2527
+ ],
2528
+ program: formatProgram(addProgramResourceLinks(sanitizeProgram(programApi.data, config.webBaseUrl)), "compact"),
2529
+ breakdown: {
2530
+ target_counts: readObject(scope.target_counts),
2531
+ by_target_type: countTargetValues(targets, (target) => stringField(target, "target_type")),
2532
+ by_scope_tag: countTargetValues(targets, (target) => readStringArrayField(target, "scope_tags")),
2533
+ by_language_tag: countTargetValues(targets, (target) => readStringArrayField(target, "language_tags")),
2534
+ group_counts: readObject(scope.group_counts),
2535
+ group_has_more: readObject(scope.group_has_more),
2536
+ groups: readObject(scope.groups)
2537
+ },
2538
+ meta: {
2539
+ total_active_targets: rawTargets.length,
2540
+ total_after_filters: targets.length,
2541
+ target_list_mode: input.target_list_mode,
2542
+ group_limit: input.group_limit
2543
+ }
2544
+ };
2545
+ }
2546
+ async function getProgramScopeDelta(client, config, input) {
2547
+ const programApi = await client.getProgram(input.program_id);
2548
+ const program = addProgramResourceLinks(sanitizeProgram(programApi.data, config.webBaseUrl));
2549
+ const changesPayload = await fetchProgramChanges(client, config, input.program_id, input.page_size, input.include_removed || input.change_type === "removed", input.include_ineligible, input.include_out_of_scope);
2550
+ const sinceTimestamp = input.since ? Date.parse(input.since) : undefined;
2551
+ const filteredChanges = changesPayload.changes
2552
+ .filter((change) => !input.change_type || stringField(change, "change_type") === input.change_type)
2553
+ .filter((change) => sinceTimestamp === undefined || timestampField(change, "changed_at") >= sinceTimestamp)
2554
+ .filter((change) => {
2555
+ const target = readObject(change.target);
2556
+ return !input.target_type || (target !== undefined && targetMatchesRequestedType(target, input.target_type));
2557
+ })
2558
+ .filter((change) => {
2559
+ const target = readObject(change.target);
2560
+ return input.language_tags.length === 0 || (target !== undefined && targetMatchesAnySignal(target, input.language_tags));
2561
+ });
2562
+ const limitedChanges = filteredChanges.slice(0, input.max_targets);
2563
+ return stripUndefined({
2564
+ request_id: randomUUID(),
2565
+ source_requests: [
2566
+ {
2567
+ source: "program",
2568
+ request_id: programApi.requestId,
2569
+ upstream_request_id: programApi.upstreamRequestId,
2570
+ ...apiSourceMetadata(programApi)
2571
+ },
2572
+ ...changesPayload.sourceRequests
2573
+ ],
2574
+ program: formatProgram(program, "compact"),
2575
+ delta: buildScopeDelta(limitedChanges, input.target_list_mode),
2576
+ changes: limitedChanges.map((change) => formatChange(change, "compact")),
2577
+ meta: stripUndefined({
2578
+ recent_changes_scanned: changesPayload.changes.length,
2579
+ changes_after_filters: filteredChanges.length,
2580
+ returned: limitedChanges.length,
2581
+ has_more: filteredChanges.length > limitedChanges.length,
2582
+ since: input.since,
2583
+ change_type: input.change_type,
2584
+ target_type: input.target_type,
2585
+ language_tags: input.language_tags.length > 0 ? input.language_tags : undefined,
2586
+ target_list_mode: input.target_list_mode
2587
+ })
2588
+ });
2589
+ }
2590
+ async function getRecentTargetActivity(client, config, input) {
2591
+ const query = toQuery({
2592
+ change_type: input.change_type,
2593
+ include_removed: input.include_removed,
2594
+ include_ineligible: input.include_ineligible,
2595
+ include_out_of_scope: input.include_out_of_scope,
2596
+ search: input.search,
2597
+ platforms: input.platforms,
2598
+ tags: uniqueStrings([...input.tags, ...(input.target_type ? [input.target_type] : []), ...input.language_tags]),
2599
+ page: 1,
2600
+ page_size: input.page_size
2601
+ });
2602
+ const api = await client.getRecentChanges(query);
2603
+ const data = readObject(api.data);
2604
+ const changes = readArray(data?.results)
2605
+ .map((change) => sanitizeChange(change, config.webBaseUrl))
2606
+ .filter((change) => {
2607
+ const target = readObject(change.target);
2608
+ return !input.target_type || (target !== undefined && targetMatchesRequestedType(target, input.target_type));
2609
+ })
2610
+ .filter((change) => {
2611
+ const target = readObject(change.target);
2612
+ return input.language_tags.length === 0 || (target !== undefined && targetMatchesAnySignal(target, input.language_tags));
2613
+ });
2614
+ const grouped = new Map();
2615
+ for (const change of changes) {
2616
+ const program = readObject(change.program);
2617
+ const programId = stringField(program, "id");
2618
+ const target = readObject(change.target);
2619
+ if (!program || !programId || !target) {
2620
+ continue;
2621
+ }
2622
+ const existing = grouped.get(programId) ??
2623
+ {
2624
+ program: addProgramResourceLinks(program),
2625
+ latestChangedAt: undefined,
2626
+ latestTimestamp: 0,
2627
+ changes: [],
2628
+ targets: [],
2629
+ targetTypes: new Set(),
2630
+ changeCounts: {}
2631
+ };
2632
+ const changedAt = stringField(change, "changed_at");
2633
+ const timestamp = timestampField(change, "changed_at");
2634
+ const changeType = stringField(change, "change_type") ?? "unknown";
2635
+ existing.changes.push(change);
2636
+ existing.targets.push(target);
2637
+ existing.targetTypes.add(stringField(target, "target_type") ?? "unknown");
2638
+ existing.changeCounts[changeType] = (existing.changeCounts[changeType] ?? 0) + 1;
2639
+ if (timestamp >= existing.latestTimestamp) {
2640
+ existing.latestTimestamp = timestamp;
2641
+ existing.latestChangedAt = changedAt;
2642
+ }
2643
+ grouped.set(programId, existing);
2644
+ }
2645
+ const activity = [...grouped.values()]
2646
+ .sort((left, right) => right.latestTimestamp - left.latestTimestamp)
2647
+ .slice(0, input.max_programs)
2648
+ .map((entry) => stripUndefined({
2649
+ program: formatProgram(entry.program, "compact"),
2650
+ latest_changed_at: entry.latestChangedAt,
2651
+ change_counts: entry.changeCounts,
2652
+ target_types: sortedValues(entry.targetTypes),
2653
+ sample_targets: formatTargetList(entry.targets.slice(0, input.sample_size), input.target_list_mode),
2654
+ sample_changes: entry.changes.slice(0, input.sample_size).map((change) => formatChange(change, "compact"))
2655
+ }));
2656
+ return {
2657
+ request_id: randomUUID(),
2658
+ source_requests: [
2659
+ {
2660
+ source: "recent_changes",
2661
+ request_id: api.requestId,
2662
+ upstream_request_id: api.upstreamRequestId,
2663
+ ...apiSourceMetadata(api)
2664
+ }
2665
+ ],
2666
+ activity,
2667
+ meta: {
2668
+ recent_changes_scanned: changes.length,
2669
+ programs_matched: grouped.size,
2670
+ returned: activity.length,
2671
+ target_list_mode: input.target_list_mode
2672
+ }
2673
+ };
2674
+ }
2675
+ async function checkProgramNewTargets(client, config, input) {
2676
+ const handle = programSearchText(input.program_id);
2677
+ const api = await client.getRecentChanges({
2678
+ change_type: "added",
2679
+ include_removed: false,
2680
+ include_ineligible: input.include_ineligible,
2681
+ include_out_of_scope: false,
2682
+ search: handle.length >= 2 ? handle : input.program_id,
2683
+ tags: input.target_type ? [input.target_type] : [],
2684
+ page: 1,
2685
+ page_size: input.page_size
2686
+ });
2687
+ const data = readObject(api.data);
2688
+ const upstreamMeta = readObject(sanitizeJson(data?.meta));
2689
+ const sinceTimestamp = input.since ? Date.parse(input.since) : undefined;
2690
+ const changes = readArray(data?.results).map((change) => sanitizeChange(change, config.webBaseUrl));
2691
+ const matchingChanges = changes
2692
+ .filter((change) => {
2693
+ const target = readObject(change.target);
2694
+ const changedAt = timestampField(change, "changed_at");
2695
+ return (stringField(change, "change_type") === "added" &&
2696
+ stringField(readObject(change.program), "id") === input.program_id &&
2697
+ target !== undefined &&
2698
+ (sinceTimestamp === undefined || changedAt >= sinceTimestamp) &&
2699
+ (!input.target_type || targetMatchesRequestedType(target, input.target_type)) &&
2700
+ targetHasAllowedScope(target, {
2701
+ include_out_of_scope: false,
2702
+ include_ineligible: input.include_ineligible,
2703
+ strict_scope_filter: true
2704
+ }));
2705
+ })
2706
+ .sort((left, right) => timestampField(right, "changed_at") - timestampField(left, "changed_at"));
2707
+ const limitedChanges = matchingChanges.slice(0, input.max_targets);
2708
+ const targets = limitedChanges
2709
+ .map((change) => readObject(change.target))
2710
+ .filter((target) => target !== undefined);
2711
+ const latest = matchingChanges[0];
2712
+ return stripUndefined({
2713
+ request_id: randomUUID(),
2714
+ source_requests: [
2715
+ {
2716
+ source: "recent_changes",
2717
+ request_id: api.requestId,
2718
+ upstream_request_id: api.upstreamRequestId,
2719
+ ...apiSourceMetadata(api)
2720
+ }
2721
+ ],
2722
+ program_id: input.program_id,
2723
+ program: latest ? formatProgram(addProgramResourceLinks(readObject(latest.program) ?? {}), "compact") : undefined,
2724
+ has_new_targets: matchingChanges.length > 0,
2725
+ new_target_count: matchingChanges.length,
2726
+ latest_added_at: latest ? stringField(latest, "changed_at") : undefined,
2727
+ new_targets: formatTargetList(targets, input.target_list_mode),
2728
+ meta: stripUndefined({
2729
+ recent_changes_scanned: changes.length,
2730
+ returned: targets.length,
2731
+ has_more: matchingChanges.length > targets.length,
2732
+ since: input.since,
2733
+ target_type: input.target_type,
2734
+ in_scope_only: true,
2735
+ include_ineligible: input.include_ineligible,
2736
+ target_list_mode: input.target_list_mode,
2737
+ upstream_total_pages: readNumber(upstreamMeta?.total_pages),
2738
+ scan_may_be_incomplete: (readNumber(upstreamMeta?.total_pages) ?? 1) > 1
2739
+ })
2740
+ });
2741
+ }
2742
+ async function checkWatchlistNewTargets(client, config, input) {
2743
+ const watchlist = new Set(input.program_ids);
2744
+ const api = await client.getRecentChanges({
2745
+ change_type: "added",
2746
+ include_removed: false,
2747
+ include_ineligible: input.include_ineligible,
2748
+ include_out_of_scope: false,
2749
+ tags: input.target_type ? [input.target_type] : [],
2750
+ page: 1,
2751
+ page_size: input.page_size
2752
+ });
2753
+ const data = readObject(api.data);
2754
+ const upstreamMeta = readObject(sanitizeJson(data?.meta));
2755
+ const sinceTimestamp = input.since ? Date.parse(input.since) : undefined;
2756
+ const changes = readArray(data?.results).map((change) => sanitizeChange(change, config.webBaseUrl));
2757
+ const grouped = new Map();
2758
+ for (const programId of input.program_ids) {
2759
+ grouped.set(programId, {
2760
+ program: undefined,
2761
+ changes: [],
2762
+ targets: [],
2763
+ latestTimestamp: 0,
2764
+ latestAddedAt: undefined
2765
+ });
2766
+ }
2767
+ for (const change of changes) {
2768
+ const program = readObject(change.program);
2769
+ const programId = stringField(program, "id");
2770
+ const target = readObject(change.target);
2771
+ const changedAt = timestampField(change, "changed_at");
2772
+ if (!programId ||
2773
+ !watchlist.has(programId) ||
2774
+ !target ||
2775
+ stringField(change, "change_type") !== "added" ||
2776
+ (sinceTimestamp !== undefined && changedAt < sinceTimestamp) ||
2777
+ (input.target_type && !targetMatchesRequestedType(target, input.target_type)) ||
2778
+ !targetHasAllowedScope(target, {
2779
+ include_out_of_scope: false,
2780
+ include_ineligible: input.include_ineligible,
2781
+ strict_scope_filter: true
2782
+ })) {
2783
+ continue;
2784
+ }
2785
+ const existing = grouped.get(programId);
2786
+ if (!existing) {
2787
+ continue;
2788
+ }
2789
+ existing.program = program ? addProgramResourceLinks(program) : undefined;
2790
+ existing.changes.push(change);
2791
+ existing.targets.push(target);
2792
+ if (changedAt >= existing.latestTimestamp) {
2793
+ existing.latestTimestamp = changedAt;
2794
+ existing.latestAddedAt = stringField(change, "changed_at");
2795
+ }
2796
+ }
2797
+ const programs = input.program_ids.map((programId) => {
2798
+ const entry = grouped.get(programId);
2799
+ const targets = entry?.targets ?? [];
2800
+ const limitedTargets = targets.slice(0, input.max_targets_per_program);
2801
+ return stripUndefined({
2802
+ program_id: programId,
2803
+ program: entry?.program ? formatProgram(entry.program, "compact") : undefined,
2804
+ has_new_targets: targets.length > 0,
2805
+ new_target_count: targets.length,
2806
+ latest_added_at: entry?.latestAddedAt,
2807
+ new_targets: formatTargetList(limitedTargets, input.target_list_mode),
2808
+ has_more: targets.length > limitedTargets.length
2809
+ });
2810
+ });
2811
+ return stripUndefined({
2812
+ request_id: randomUUID(),
2813
+ source_requests: [
2814
+ {
2815
+ source: "recent_changes",
2816
+ request_id: api.requestId,
2817
+ upstream_request_id: api.upstreamRequestId,
2818
+ ...apiSourceMetadata(api)
2819
+ }
2820
+ ],
2821
+ any_new_targets: programs.some((program) => program.has_new_targets === true),
2822
+ programs,
2823
+ meta: stripUndefined({
2824
+ recent_changes_scanned: changes.length,
2825
+ watchlist_size: input.program_ids.length,
2826
+ programs_with_new_targets: programs.filter((program) => program.has_new_targets === true).length,
2827
+ since: input.since,
2828
+ target_type: input.target_type,
2829
+ in_scope_only: true,
2830
+ include_ineligible: input.include_ineligible,
2831
+ target_list_mode: input.target_list_mode,
2832
+ upstream_total_pages: readNumber(upstreamMeta?.total_pages),
2833
+ scan_may_be_incomplete: (readNumber(upstreamMeta?.total_pages) ?? 1) > 1
2834
+ })
2835
+ });
2836
+ }
2837
+ async function getProgramBrief(client, config, input) {
2838
+ const programPromise = client.getProgram(input.program_id);
2839
+ const targetsPromise = client.getProgramTargets(input.program_id);
2840
+ const changesPromise = input.include_recent_changes && input.recent_changes_limit > 0
2841
+ ? fetchProgramChanges(client, config, input.program_id, input.recent_changes_limit, true, input.include_ineligible, input.include_out_of_scope)
2842
+ : Promise.resolve(undefined);
2843
+ const [programApi, targetsApi, changes] = await Promise.all([programPromise, targetsPromise, changesPromise]);
2844
+ const candidate = toProgramCandidate(programApi.data, config.webBaseUrl);
2845
+ const targetsData = readObject(targetsApi.data);
2846
+ const rawTargets = readArray(targetsData?.targets);
2847
+ const targets = rawTargets
2848
+ .map(sanitizeTarget)
2849
+ .filter((target) => targetHasAllowedScope(target, {
2850
+ include_out_of_scope: input.include_out_of_scope,
2851
+ include_ineligible: input.include_ineligible,
2852
+ strict_scope_filter: false
2853
+ }));
2854
+ const scope = buildTargetScopeSummary(targets, input.target_list_mode, input.target_sample_size, input.include_out_of_scope, input.include_ineligible);
2855
+ return stripUndefined({
2856
+ request_id: randomUUID(),
2857
+ source_requests: [
2858
+ {
2859
+ source: "program",
2860
+ request_id: programApi.requestId,
2861
+ upstream_request_id: programApi.upstreamRequestId,
2862
+ ...apiSourceMetadata(programApi)
2863
+ },
2864
+ {
2865
+ source: "program_targets",
2866
+ request_id: targetsApi.requestId,
2867
+ upstream_request_id: targetsApi.upstreamRequestId,
2868
+ ...apiSourceMetadata(targetsApi)
2869
+ },
2870
+ ...(changes?.sourceRequests ?? [])
2871
+ ],
2872
+ program: formatProgram(candidate.program, "compact"),
2873
+ brief: {
2874
+ rank_factors: rankFactors(candidate),
2875
+ target_surface_score: targetSurfaceScore(candidate),
2876
+ hunt_value_score: huntValueScore(candidate),
2877
+ why_ranked_here: candidateWhy(candidate),
2878
+ tradeoffs: candidateTradeoffs(candidate),
2879
+ next_action: candidateNextAction(candidate)
2880
+ },
2881
+ target_samples: scope,
2882
+ recent_changes: changes?.changes.map((change) => formatChange(change, "compact")),
2883
+ meta: {
2884
+ total_active_targets: rawTargets.length,
2885
+ total_targets_after_filters: targets.length,
2886
+ recent_changes_returned: changes?.changes.length ?? 0,
2887
+ target_list_mode: input.target_list_mode
2888
+ }
2889
+ });
2890
+ }
2891
+ async function getProgramDelta(client, config, input) {
2892
+ const programApi = await client.getProgram(input.program_id);
2893
+ const program = addProgramResourceLinks(sanitizeProgram(programApi.data, config.webBaseUrl));
2894
+ const changes = await fetchProgramChanges(client, config, input.program_id, input.page_size, input.include_removed, input.include_ineligible, input.include_out_of_scope);
2895
+ return {
2896
+ request_id: randomUUID(),
2897
+ source_requests: [
2898
+ {
2899
+ source: "program",
2900
+ request_id: programApi.requestId,
2901
+ upstream_request_id: programApi.upstreamRequestId,
2902
+ ...apiSourceMetadata(programApi)
2903
+ },
2904
+ ...changes.sourceRequests
2905
+ ],
2906
+ program,
2907
+ changes: changes.changes,
2908
+ meta: changes.meta
2909
+ };
2910
+ }
2911
+ async function fetchProgramChanges(client, config, programId, pageSize, includeRemoved, includeIneligible, includeOutOfScope) {
2912
+ const handle = programSearchText(programId);
2913
+ const api = await client.getRecentChanges({
2914
+ search: handle.length >= 2 ? handle : programId,
2915
+ include_removed: includeRemoved,
2916
+ include_ineligible: includeIneligible,
2917
+ include_out_of_scope: includeOutOfScope,
2918
+ page: 1,
2919
+ page_size: pageSize
2920
+ });
2921
+ const data = readObject(api.data);
2922
+ const changes = readArray(data?.results)
2923
+ .map((change) => sanitizeChange(change, config.webBaseUrl))
2924
+ .filter((change) => stringField(readObject(change.program), "id") === programId);
2925
+ return {
2926
+ changes,
2927
+ meta: sanitizeJson(data?.meta),
2928
+ sourceRequests: [
2929
+ {
2930
+ source: "recent_changes",
2931
+ request_id: api.requestId,
2932
+ upstream_request_id: api.upstreamRequestId,
2933
+ ...apiSourceMetadata(api)
2934
+ }
2935
+ ]
2936
+ };
2937
+ }
2938
+ function addProgramResourceLinks(program) {
2939
+ const id = stringField(program, "id");
2940
+ if (!id) {
2941
+ return program;
2942
+ }
2943
+ return {
2944
+ ...program,
2945
+ resource_links: {
2946
+ program: programResourceUri(id),
2947
+ targets: programTargetsResourceUri(id)
2948
+ }
2949
+ };
2950
+ }
2951
+ function parseProgramIdFromResourceUri(uri) {
2952
+ return programIdSchema.parse(uri.searchParams.get("program_id") ?? "");
2953
+ }
2954
+ function programResourceUri(programId) {
2955
+ return `bbradar://program?program_id=${encodeURIComponent(programId)}`;
2956
+ }
2957
+ function programTargetsResourceUri(programId) {
2958
+ return `bbradar://program/targets?program_id=${encodeURIComponent(programId)}`;
2959
+ }
2960
+ function exportResourceUri(exportId) {
2961
+ return `bbradar://exports?export_id=${encodeURIComponent(exportId)}`;
2962
+ }
2963
+ function jsonResource(uri, payload) {
2964
+ return {
2965
+ contents: [
2966
+ {
2967
+ uri,
2968
+ mimeType: "application/json",
2969
+ text: JSON.stringify(payload)
2970
+ }
2971
+ ]
2972
+ };
2973
+ }
2974
+ function rememberExport(exportStore, exportId, payload) {
2975
+ if (exportStore.has(exportId)) {
2976
+ exportStore.delete(exportId);
2977
+ }
2978
+ while (exportStore.size >= MAX_LOCAL_EXPORT_RESOURCES) {
2979
+ const oldestKey = exportStore.keys().next().value;
2980
+ if (oldestKey === undefined) {
2981
+ break;
2982
+ }
2983
+ exportStore.delete(oldestKey);
2984
+ }
2985
+ exportStore.set(exportId, payload);
2986
+ }
2987
+ function previewExportPayload(payload) {
2988
+ if (Array.isArray(payload)) {
2989
+ return payload.slice(0, EXPORT_PREVIEW_LIMIT);
2990
+ }
2991
+ if (readObject(payload) && Array.isArray(payload.targets)) {
2992
+ return {
2993
+ ...payload,
2994
+ targets: payload.targets.slice(0, EXPORT_PREVIEW_LIMIT)
2995
+ };
2996
+ }
2997
+ return payload;
2998
+ }
2999
+ function previewExportCount(payload) {
3000
+ if (Array.isArray(payload)) {
3001
+ return Math.min(payload.length, EXPORT_PREVIEW_LIMIT);
3002
+ }
3003
+ const object = readObject(payload);
3004
+ const targets = object?.targets;
3005
+ if (Array.isArray(targets)) {
3006
+ return Math.min(targets.length, EXPORT_PREVIEW_LIMIT);
3007
+ }
3008
+ return payload === undefined || payload === null ? 0 : 1;
3009
+ }
3010
+ function collectResourceLinks(payload) {
3011
+ const links = new Map();
3012
+ for (const program of extractProgramsFromPayload(payload)) {
3013
+ const id = stringField(program, "id");
3014
+ const resourceLinks = readObject(program.resource_links);
3015
+ const programUri = stringField(resourceLinks, "program");
3016
+ const targetsUri = stringField(resourceLinks, "targets");
3017
+ if (id && programUri) {
3018
+ links.set(programUri, {
3019
+ type: "resource_link",
3020
+ uri: programUri,
3021
+ name: `${id} program`,
3022
+ title: `${stringField(program, "name") ?? id} program`,
3023
+ description: "Read sanitized BBRadar program details.",
3024
+ mimeType: "application/json"
3025
+ });
3026
+ }
3027
+ if (id && targetsUri) {
3028
+ links.set(targetsUri, {
3029
+ type: "resource_link",
3030
+ uri: targetsUri,
3031
+ name: `${id} targets`,
3032
+ title: `${stringField(program, "name") ?? id} targets`,
3033
+ description: "Read sanitized BBRadar program targets.",
3034
+ mimeType: "application/json"
3035
+ });
3036
+ }
3037
+ }
3038
+ const exportObject = readObject(payload.export);
3039
+ const exportUri = stringField(exportObject, "resource_uri");
3040
+ const exportId = stringField(exportObject, "export_id");
3041
+ if (exportUri && exportId) {
3042
+ links.set(exportUri, {
3043
+ type: "resource_link",
3044
+ uri: exportUri,
3045
+ name: `${exportId} export`,
3046
+ title: "Target export",
3047
+ description: "Read the full local target export payload.",
3048
+ mimeType: "application/json"
3049
+ });
3050
+ }
3051
+ return [...links.values()];
3052
+ }
3053
+ function extractProgramsFromPayload(payload) {
3054
+ const programs = [];
3055
+ const program = readObject(payload.program);
3056
+ if (program) {
3057
+ programs.push(program);
3058
+ }
3059
+ for (const entry of readArray(payload.programs)) {
3060
+ const object = readObject(entry);
3061
+ if (object) {
3062
+ programs.push(object);
3063
+ }
3064
+ }
3065
+ for (const entry of readArray(payload.compared_programs)) {
3066
+ const object = readObject(entry);
3067
+ if (object) {
3068
+ programs.push(object);
3069
+ }
3070
+ }
3071
+ return programs;
3072
+ }
3073
+ function readFacetValues(value) {
3074
+ return readArray(value)
3075
+ .map((entry) => readString(readObject(entry)?.value) ?? readString(entry))
3076
+ .filter((entry) => entry !== undefined);
3077
+ }
3078
+ function addSetValue(values, value) {
3079
+ if (value) {
3080
+ values.add(value);
3081
+ }
3082
+ }
3083
+ function addSetValues(values, entries) {
3084
+ for (const entry of entries) {
3085
+ addSetValue(values, entry);
3086
+ }
3087
+ }
3088
+ function sortedValues(values) {
3089
+ return [...values].sort((left, right) => left.localeCompare(right));
3090
+ }
3091
+ function formatProgram(program, mode) {
3092
+ return mode === "full" ? program : compactProgram(program);
3093
+ }
3094
+ function compactProgram(program) {
3095
+ return stripUndefined({
3096
+ id: stringField(program, "id"),
3097
+ handle: stringField(program, "handle"),
3098
+ name: stringField(program, "name"),
3099
+ platform: stringField(program, "platform"),
3100
+ platform_url: stringField(program, "platform_url"),
3101
+ bbradar_url: stringField(program, "bbradar_url"),
3102
+ opportunity_tag: readObject(program.opportunity_tag),
3103
+ reward_range: readObject(program.reward_range),
3104
+ public_report_count: readNumber(program.public_report_count),
3105
+ public_report_count_label: stringField(program, "public_report_count_label"),
3106
+ first_seen_date: stringField(program, "first_seen_date"),
3107
+ last_updated: stringField(program, "last_updated"),
3108
+ targets_updated_at: stringField(program, "targets_updated_at"),
3109
+ scope_summary: readObject(program.scope_summary),
3110
+ resource_links: readObject(program.resource_links)
3111
+ });
3112
+ }
3113
+ function candidateOutput(candidate, index, targetSamples, mode) {
3114
+ const id = stringField(candidate.program, "id");
3115
+ const samples = id ? targetSamples[id] : undefined;
3116
+ const formattedSamples = samples ? formatTargetList(samples, mode === "full" ? "full" : "identifiers") : undefined;
3117
+ if (mode === "full") {
3118
+ return stripUndefined({
3119
+ rank: index + 1,
3120
+ ...candidate.program,
3121
+ rank_factors: rankFactors(candidate),
3122
+ target_surface_score: targetSurfaceScore(candidate),
3123
+ why_ranked_here: candidateWhy(candidate),
3124
+ tradeoffs: candidateTradeoffs(candidate),
3125
+ next_action: candidateNextAction(candidate),
3126
+ target_samples: formattedSamples
3127
+ });
3128
+ }
3129
+ return stripUndefined({
3130
+ rank: index + 1,
3131
+ ...compactProgram(candidate.program),
3132
+ rank_factors: rankFactors(candidate),
3133
+ why_ranked_here: candidateWhy(candidate),
3134
+ tradeoffs: candidateTradeoffs(candidate),
3135
+ next_action: candidateNextAction(candidate),
3136
+ target_samples: formattedSamples
3137
+ });
3138
+ }
3139
+ function formatChange(change, mode) {
3140
+ if (mode === "full") {
3141
+ return change;
3142
+ }
3143
+ const target = readObject(change.target);
3144
+ return stripUndefined({
3145
+ id: change.id,
3146
+ change_type: stringField(change, "change_type"),
3147
+ changed_at: stringField(change, "changed_at"),
3148
+ program: formatProgram(readObject(change.program) ?? {}, "compact"),
3149
+ target: target ? compactTarget(target) : undefined
3150
+ });
3151
+ }
3152
+ function buildTargetScopeSummary(targets, mode, groupLimit, includeOutOfScope, includeIneligible) {
3153
+ const bountyTargets = targets.filter((target) => target.in_scope === true && target.eligible_for_bounty === true);
3154
+ const groups = {
3155
+ in_scope_bounty_domains: bountyTargets.filter(targetIsDomain),
3156
+ wildcards: bountyTargets.filter(targetIsWildcard),
3157
+ apis: bountyTargets.filter(targetIsApi),
3158
+ mobile: bountyTargets.filter(targetIsMobile),
3159
+ source_code: bountyTargets.filter(targetIsSourceCode)
3160
+ };
3161
+ const classified = new Set(Object.values(groups).flat());
3162
+ groups.other_in_scope_bounty = bountyTargets.filter((target) => !classified.has(target));
3163
+ if (includeOutOfScope) {
3164
+ groups.out_of_scope = targets.filter((target) => target.in_scope === false);
3165
+ }
3166
+ if (includeIneligible) {
3167
+ groups.ineligible = targets.filter((target) => target.eligible_for_bounty === false);
3168
+ }
3169
+ const groupCounts = Object.fromEntries(Object.entries(groups).map(([name, entries]) => [name, entries.length]));
3170
+ const formattedGroups = Object.fromEntries(Object.entries(groups).map(([name, entries]) => [name, formatTargetList(entries.slice(0, groupLimit), mode)]));
3171
+ const groupHasMore = Object.fromEntries(Object.entries(groups).map(([name, entries]) => [name, entries.length > groupLimit]));
3172
+ return {
3173
+ target_counts: {
3174
+ total: targets.length,
3175
+ in_scope: targets.filter((target) => target.in_scope === true).length,
3176
+ out_of_scope: targets.filter((target) => target.in_scope === false).length,
3177
+ eligible_for_bounty: targets.filter((target) => target.eligible_for_bounty === true).length,
3178
+ in_scope_bounty: bountyTargets.length,
3179
+ wildcard: targets.filter(targetIsWildcard).length,
3180
+ api: targets.filter(targetIsApi).length,
3181
+ mobile: targets.filter(targetIsMobile).length,
3182
+ source_code: targets.filter(targetIsSourceCode).length
3183
+ },
3184
+ group_counts: groupCounts,
3185
+ group_has_more: groupHasMore,
3186
+ groups: formattedGroups
3187
+ };
3188
+ }
3189
+ function countTargetValues(targets, readValues) {
3190
+ const counts = new Map();
3191
+ for (const target of targets) {
3192
+ const rawValues = readValues(target);
3193
+ const values = Array.isArray(rawValues) ? rawValues : rawValues === undefined ? [] : [rawValues];
3194
+ for (const value of values) {
3195
+ const normalized = value.trim();
3196
+ if (normalized.length > 0) {
3197
+ counts.set(normalized, (counts.get(normalized) ?? 0) + 1);
3198
+ }
3199
+ }
3200
+ }
3201
+ return Object.fromEntries([...counts.entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])));
3202
+ }
3203
+ function buildScopeDelta(changes, targetListMode) {
3204
+ const byChangeType = new Map();
3205
+ let latestTimestamp = 0;
3206
+ let latestChangedAt;
3207
+ for (const change of changes) {
3208
+ const changeType = stringField(change, "change_type") ?? "unknown";
3209
+ const target = readObject(change.target);
3210
+ const timestamp = timestampField(change, "changed_at");
3211
+ const existing = byChangeType.get(changeType) ??
3212
+ {
3213
+ changes: [],
3214
+ targets: [],
3215
+ latestTimestamp: 0,
3216
+ latestChangedAt: undefined
3217
+ };
3218
+ existing.changes.push(change);
3219
+ if (target) {
3220
+ existing.targets.push(target);
3221
+ }
3222
+ if (timestamp >= existing.latestTimestamp) {
3223
+ existing.latestTimestamp = timestamp;
3224
+ existing.latestChangedAt = stringField(change, "changed_at");
3225
+ }
3226
+ if (timestamp >= latestTimestamp) {
3227
+ latestTimestamp = timestamp;
3228
+ latestChangedAt = stringField(change, "changed_at");
3229
+ }
3230
+ byChangeType.set(changeType, existing);
3231
+ }
3232
+ return stripUndefined({
3233
+ total_changes: changes.length,
3234
+ latest_changed_at: latestChangedAt,
3235
+ change_counts: Object.fromEntries([...byChangeType.entries()].map(([changeType, entry]) => [changeType, entry.changes.length])),
3236
+ targets_by_change_type: Object.fromEntries([...byChangeType.entries()].map(([changeType, entry]) => [changeType, formatTargetList(entry.targets, targetListMode)])),
3237
+ latest_by_change_type: Object.fromEntries([...byChangeType.entries()].map(([changeType, entry]) => [changeType, entry.latestChangedAt]))
3238
+ });
3239
+ }
3240
+ function rankFactors(candidate) {
3241
+ return stripUndefined({
3242
+ public_report_count: candidate.publicReportCount,
3243
+ public_report_count_label: stringField(candidate.program, "public_report_count_label"),
3244
+ bounty_min: candidate.bountyMin,
3245
+ bounty_max: candidate.bountyMax,
3246
+ total_targets: candidate.totalTargetCount,
3247
+ in_scope_targets: candidate.inScopeTargetCount,
3248
+ eligible_targets: candidate.eligibleTargetCount,
3249
+ wildcard_targets: candidate.wildcardCount,
3250
+ added_24h: candidate.added24h,
3251
+ added_7d: candidate.added7d,
3252
+ opportunity_tier: candidate.opportunityTier,
3253
+ opportunity_score: candidate.opportunityScore,
3254
+ target_surface_score: targetSurfaceScore(candidate),
3255
+ hunt_value_score: huntValueScore(candidate)
3256
+ });
3257
+ }
3258
+ function candidateWhy(candidate) {
3259
+ const reasons = [];
3260
+ if (candidate.publicReportCount !== undefined) {
3261
+ const label = stringField(candidate.program, "public_report_count_label");
3262
+ reasons.push(label ? `${candidate.publicReportCount} ${label}` : `${candidate.publicReportCount} public reports`);
3263
+ }
3264
+ if (candidate.bountyMax !== undefined) {
3265
+ reasons.push(`max bounty ${candidate.bountyMax}`);
3266
+ }
3267
+ if (candidate.eligibleTargetCount !== undefined) {
3268
+ reasons.push(`${candidate.eligibleTargetCount} eligible targets`);
3269
+ }
3270
+ if (candidate.wildcardCount > 0) {
3271
+ reasons.push(`${candidate.wildcardCount} wildcard targets`);
3272
+ }
3273
+ if (candidateHasWeb3Surface(candidate)) {
3274
+ reasons.push("Web3/blockchain scope signals");
3275
+ }
3276
+ if (candidate.added7d !== undefined && candidate.added7d > 0) {
3277
+ reasons.push(`${candidate.added7d} targets added in 7d`);
3278
+ }
3279
+ if (candidate.opportunityTier) {
3280
+ reasons.push(`${candidate.opportunityTier} opportunity tier`);
3281
+ }
3282
+ return reasons;
3283
+ }
3284
+ function candidateTradeoffs(candidate) {
3285
+ const tradeoffs = [];
3286
+ if (candidate.publicReportCount === undefined) {
3287
+ tradeoffs.push("public report count is unknown");
3288
+ }
3289
+ if ((candidate.bountyMax ?? candidate.bountyMin ?? 0) <= 0) {
3290
+ tradeoffs.push("no bounty range exposed");
3291
+ }
3292
+ else if ((candidate.bountyMax ?? 0) < 1_000) {
3293
+ tradeoffs.push("lower reward ceiling");
3294
+ }
3295
+ if (candidate.wildcardCount === 0) {
3296
+ tradeoffs.push("no wildcard targets in BBRadar counts");
3297
+ }
3298
+ else if (candidate.wildcardCount === 1) {
3299
+ tradeoffs.push("only one wildcard target in BBRadar counts");
3300
+ }
3301
+ if ((candidate.eligibleTargetCount ?? 0) <= 0) {
3302
+ tradeoffs.push("no bounty-eligible target count exposed");
3303
+ }
3304
+ if (candidateLooksLikeCtf(candidate)) {
3305
+ tradeoffs.push("program appears CTF-like");
3306
+ }
3307
+ if (candidateLooksMobileOnly(candidate)) {
3308
+ tradeoffs.push("appears mobile-only from available tags");
3309
+ }
3310
+ return tradeoffs;
3311
+ }
3312
+ function candidateNextAction(candidate) {
3313
+ if (candidate.wildcardCount > 0) {
3314
+ return "Review in-scope wildcard targets with get_program_targets and keep follow-up analysis passive and BBRadar-data-backed.";
3315
+ }
3316
+ if (candidateHasApiSurface(candidate)) {
3317
+ return "Review API targets with get_program_targets and summarize the allowed API scope from BBRadar data.";
3318
+ }
3319
+ if (candidateHasWebSurface(candidate)) {
3320
+ return "Review web/domain targets with get_program_targets and summarize freshness, reward, and report-count signals.";
3321
+ }
3322
+ return "Fetch exact scope with get_program_targets before making recommendations.";
3323
+ }
3324
+ function targetSurfaceScore(candidate) {
3325
+ return candidate.targetSurfaceScore;
3326
+ }
3327
+ function calculateTargetSurfaceScore(candidate) {
3328
+ let score = 0;
3329
+ score += Math.min(candidate.wildcardCount, 10) * 10;
3330
+ score += Math.min(candidate.eligibleTargetCount ?? 0, 25) * 2;
3331
+ if (candidateHasApiSurface(candidate)) {
3332
+ score += 20;
3333
+ }
3334
+ if (candidateHasWebSurface(candidate)) {
3335
+ score += 15;
3336
+ }
3337
+ if (candidateHasWeb3Surface(candidate)) {
3338
+ score += 15;
3339
+ }
3340
+ if (candidateHasMobileSurface(candidate)) {
3341
+ score += 8;
3342
+ }
3343
+ return score;
3344
+ }
3345
+ function huntValueScore(candidate) {
3346
+ return candidate.huntValueScore;
3347
+ }
3348
+ function calculateHuntValueScore(candidate) {
3349
+ const reports = candidate.publicReportCount;
3350
+ const competitionScore = reports === undefined ? 20 : Math.max(0, 100 - Math.min(reports, 100));
3351
+ const rewardScore = Math.min(candidate.bountyMax ?? candidate.bountyMin ?? 0, 50_000) / 500;
3352
+ const freshnessScore = freshnessScoreFor(candidate);
3353
+ const opportunityScore = candidate.opportunityScore ?? 0;
3354
+ const surfaceScore = Math.min(targetSurfaceScore(candidate), 100);
3355
+ const platformScore = platformWeight(candidate);
3356
+ const ctfPenalty = candidateLooksLikeCtf(candidate) ? 50 : 0;
3357
+ return Math.round((competitionScore * 0.3 + rewardScore * 0.2 + surfaceScore * 0.2 + freshnessScore * 0.15 + opportunityScore * 0.1 + platformScore * 0.05 - ctfPenalty) * 100) / 100;
3358
+ }
3359
+ function freshnessScoreFor(candidate) {
3360
+ const timestamps = [candidate.firstSeenTime, candidate.lastUpdatedTime, candidate.latestChangeTime].filter((value) => value > 0);
3361
+ if (timestamps.length === 0) {
3362
+ return 0;
3363
+ }
3364
+ const newest = Math.max(...timestamps);
3365
+ const ageDays = Math.max(0, (Date.now() - newest) / (24 * 60 * 60 * 1000));
3366
+ return Math.max(0, 100 - ageDays);
3367
+ }
3368
+ function platformWeight(candidate) {
3369
+ const platform = normalizeTag(stringField(candidate.program, "platform") ?? "");
3370
+ const weights = {
3371
+ hackerone: 100,
3372
+ bugcrowd: 95,
3373
+ intigriti: 90,
3374
+ yeswehack: 85,
3375
+ hackenproof: 80,
3376
+ issuehunt: 70,
3377
+ bugrap: 60
3378
+ };
3379
+ return weights[platform] ?? 50;
3380
+ }
3381
+ function candidateHasApiSurface(candidate) {
3382
+ return candidate.hasApiSurface;
3383
+ }
3384
+ function candidateHasWebSurface(candidate) {
3385
+ return candidate.hasWebSurface;
3386
+ }
3387
+ function candidateHasWeb3Surface(candidate) {
3388
+ return candidate.hasWeb3Surface;
3389
+ }
3390
+ function candidateHasMobileSurface(candidate) {
3391
+ return candidate.hasMobileSurface;
3392
+ }
3393
+ function candidateLooksMobileOnly(candidate) {
3394
+ return candidate.looksMobileOnly;
3395
+ }
3396
+ function candidateLooksLikeCtf(candidate) {
3397
+ return candidate.looksLikeCtf;
3398
+ }
3399
+ function candidateLooksContestLike(candidate) {
3400
+ return tagIncludesAnyNormalized(candidate.normalizedSearchSignals, WEB3_CONTEST_SIGNALS);
3401
+ }
3402
+ function candidateHasKnownNoBounty(candidate) {
3403
+ if (candidate.bountyMin === undefined && candidate.bountyMax === undefined) {
3404
+ return false;
3405
+ }
3406
+ return (candidate.bountyMin ?? 0) <= 0 && (candidate.bountyMax ?? 0) <= 0;
3407
+ }
3408
+ function candidateLooksLikeVdp(candidate) {
3409
+ return candidateHasKnownNoBounty(candidate) || tagIncludesAnyNormalized(candidate.normalizedSearchSignals, VDP_SIGNALS);
3410
+ }
3411
+ function programResolutionMatch(candidate, query) {
3412
+ const normalizedQuery = normalizeTag(query);
3413
+ const queryTokens = tokenSet(normalizedQuery);
3414
+ const fields = [
3415
+ { name: "id", value: stringField(candidate.program, "id") },
3416
+ { name: "handle", value: stringField(candidate.program, "handle") },
3417
+ { name: "name", value: stringField(candidate.program, "name") },
3418
+ { name: "platform", value: stringField(candidate.program, "platform") },
3419
+ { name: "platform_url", value: stringField(candidate.program, "platform_url") },
3420
+ { name: "bbradar_url", value: stringField(candidate.program, "bbradar_url") }
3421
+ ];
3422
+ let score = 0;
3423
+ const matchedFields = new Set();
3424
+ const reasons = new Set();
3425
+ for (const field of fields) {
3426
+ if (!field.value) {
3427
+ continue;
3428
+ }
3429
+ const normalizedValue = normalizeTag(field.value);
3430
+ const fieldTokens = tokenSet(normalizedValue);
3431
+ if (normalizedValue === normalizedQuery) {
3432
+ score = Math.max(score, field.name === "id" || field.name === "handle" ? 100 : 95);
3433
+ matchedFields.add(field.name);
3434
+ reasons.add(`${field.name} exact match`);
3435
+ continue;
3436
+ }
3437
+ if (normalizedValue.includes(normalizedQuery)) {
3438
+ score = Math.max(score, field.name === "name" || field.name === "handle" ? 80 : 65);
3439
+ matchedFields.add(field.name);
3440
+ reasons.add(`${field.name} contains query`);
3441
+ }
3442
+ const tokenOverlap = [...queryTokens].filter((token) => fieldTokens.has(token)).length;
3443
+ if (tokenOverlap > 0) {
3444
+ const tokenScore = Math.round((tokenOverlap / Math.max(queryTokens.size, 1)) * 70);
3445
+ score = Math.max(score, tokenScore);
3446
+ matchedFields.add(field.name);
3447
+ reasons.add(`${field.name} token match`);
3448
+ }
3449
+ }
3450
+ if (candidate.normalizedSearchSignals.some((signal) => signal.includes(normalizedQuery))) {
3451
+ score = Math.max(score, 55);
3452
+ matchedFields.add("search_signals");
3453
+ reasons.add("program search signals contain query");
3454
+ }
3455
+ return {
3456
+ score,
3457
+ fields: [...matchedFields],
3458
+ reasons: [...reasons]
3459
+ };
3460
+ }
3461
+ function normalizedCandidateValues(program, normalizedScopeTags) {
3462
+ return [
3463
+ stringField(program, "id"),
3464
+ stringField(program, "handle"),
3465
+ stringField(program, "name"),
3466
+ stringField(program, "platform"),
3467
+ stringField(program, "platform_url"),
3468
+ stringField(program, "bbradar_url")
3469
+ ]
3470
+ .filter((value) => value !== undefined)
3471
+ .map(normalizeTag)
3472
+ .concat(normalizedScopeTags);
3473
+ }
3474
+ function tagIncludesAnyNormalized(normalizedTags, needles) {
3475
+ return needles.some((needle) => normalizedTags.some((tag) => tag === needle || tag.includes(needle)));
3476
+ }
3477
+ function candidateMatchesAnyTag(candidate, values) {
3478
+ const normalizedValues = values.map(normalizeTag);
3479
+ return normalizedValues.some((value) => candidate.normalizedScopeTags.some((tag) => tag === value || tag.includes(value) || value.includes(tag)) ||
3480
+ candidate.normalizedSearchSignals.some((signal) => signal === value || signal.includes(value)));
3481
+ }
3482
+ function programFetchError(programId, error) {
3483
+ if (error instanceof BBRadarApiError) {
3484
+ return stripUndefined({
3485
+ program_id: programId,
3486
+ request_id: error.requestId,
3487
+ upstream_request_id: error.upstreamRequestId,
3488
+ status: error.status,
3489
+ message: error.message,
3490
+ detail: sanitizeJson(error.detail),
3491
+ errors: sanitizeJson(error.errors),
3492
+ suggested_fix: "Check that the program_id uses BBRadar platform:handle format. Spaces and slashes are supported and are encoded by the MCP server."
3493
+ });
3494
+ }
3495
+ return {
3496
+ program_id: programId,
3497
+ message: error instanceof Error ? error.message : String(error),
3498
+ suggested_fix: "Retry the program lookup or verify the BBRadar program id."
3499
+ };
3500
+ }
3501
+ function programSearchText(programId) {
3502
+ const colonIndex = programId.indexOf(":");
3503
+ const handle = colonIndex >= 0 ? programId.slice(colonIndex + 1) : programId;
3504
+ const lastPathSegment = handle.split("/").filter(Boolean).at(-1) ?? handle;
3505
+ return lastPathSegment.length >= 2 ? lastPathSegment : handle;
3506
+ }
3507
+ function normalizeFindProgramsInput(input) {
3508
+ const warnings = [];
3509
+ const normalized = { ...input };
3510
+ normalized.max_pages = clampLimit("max_pages", normalized.max_pages, MAX_FIND_PROGRAM_PAGES, warnings);
3511
+ normalized.max_results = clampLimit("max_results", normalized.max_results, MAX_FIND_PROGRAM_RESULTS, warnings);
3512
+ normalized.target_sample_programs = clampLimit("target_sample_programs", normalized.target_sample_programs, MAX_FIND_TARGET_SAMPLE_PROGRAMS, warnings);
3513
+ normalized.target_sample_size = clampLimit("target_sample_size", normalized.target_sample_size, MAX_FIND_TARGET_SAMPLES, warnings);
3514
+ normalized.upstream_request_budget = clampLimit("upstream_request_budget", normalized.upstream_request_budget, MAX_FIND_UPSTREAM_REQUEST_BUDGET, warnings);
3515
+ if (normalized.target_samples_only_wildcards) {
3516
+ normalized.target_sample_programs = clampLimit("target_sample_programs", normalized.target_sample_programs, MAX_WILDCARD_TARGET_SAMPLE_PROGRAMS, warnings);
3517
+ normalized.target_sample_size = clampLimit("target_sample_size", normalized.target_sample_size, MAX_WILDCARD_TARGET_SAMPLES, warnings);
3518
+ }
3519
+ return { input: normalized, warnings };
3520
+ }
3521
+ function clampLimit(name, value, max, warnings) {
3522
+ if (value <= max) {
3523
+ return value;
3524
+ }
3525
+ warnings.push(`${name} was clamped from ${value} to ${max}.`);
3526
+ return max;
3527
+ }
3528
+ async function findPrograms(client, config, input) {
3529
+ const normalized = normalizeFindProgramsInput(input);
3530
+ const warnings = normalized.warnings;
3531
+ input = normalized.input;
3532
+ const budget = {
3533
+ initial: input.upstream_request_budget,
3534
+ remaining: input.upstream_request_budget
3535
+ };
3536
+ const upstreamTags = uniqueStrings([...input.tags, ...input.scope_tags, ...input.target_types, ...input.language_tags]);
3537
+ const collected = await collectPrograms(client, {
3538
+ platforms: input.platforms,
3539
+ tags: upstreamTags,
3540
+ updated_since: input.updated_since,
3541
+ opportunity_levels: input.opportunity_levels,
3542
+ max_pages: input.max_pages,
3543
+ budget,
3544
+ warnings
3545
+ });
3546
+ const filteredCandidates = collected.programs
3547
+ .map((program) => toProgramCandidate(program, config.webBaseUrl))
3548
+ .filter((program) => programCandidateMatches(program, input))
3549
+ .sort((left, right) => compareProgramCandidates(left, right, input.sort_by));
3550
+ const candidates = filteredCandidates.slice(0, input.max_results);
3551
+ const targetSamplePrograms = Math.min(input.target_sample_programs, candidates.length, Math.max(0, budget.remaining));
3552
+ if (input.include_target_samples && input.target_sample_programs > targetSamplePrograms) {
3553
+ warnings.push(`target_sample_programs was reduced from ${input.target_sample_programs} to ${targetSamplePrograms} to stay within the upstream request budget.`);
3554
+ }
3555
+ const targetSamples = input.include_target_samples && targetSamplePrograms > 0
3556
+ ? await collectTargetSamples(client, candidates, { ...input, target_sample_programs: targetSamplePrograms }, budget)
3557
+ : {};
3558
+ const outputMode = input.output_mode ?? "compact";
3559
+ const programs = candidates.map((candidate, index) => candidateOutput(candidate, index, targetSamples, outputMode));
3560
+ return stripUndefined({
3561
+ request_id: randomUUID(),
3562
+ source_requests: collected.sourceRequests,
3563
+ fetched_at: new Date().toISOString(),
3564
+ warnings: warnings.length > 0 ? warnings : undefined,
3565
+ filters: stripUndefined({
3566
+ platforms: input.platforms.length > 0 ? input.platforms : undefined,
3567
+ tags: upstreamTags.length > 0 ? upstreamTags : undefined,
3568
+ web3: input.web3 ? true : undefined,
3569
+ opportunity_levels: input.opportunity_levels.length > 0 ? input.opportunity_levels : undefined,
3570
+ updated_since: input.updated_since,
3571
+ min_bounty_min: input.min_bounty_min,
3572
+ min_bounty_max: input.min_bounty_max,
3573
+ max_bounty_max: input.max_bounty_max,
3574
+ require_bounty: input.require_bounty,
3575
+ max_public_report_count: input.max_public_report_count,
3576
+ require_known_report_count: input.require_known_report_count,
3577
+ include_unknown_report_count: input.include_unknown_report_count,
3578
+ exclude_ctf: input.exclude_ctf,
3579
+ exclude_mobile_only: input.exclude_mobile_only,
3580
+ exclude_non_web: input.exclude_non_web,
3581
+ has_api_scope: input.has_api_scope,
3582
+ contest_like: input.contest_like ? true : undefined,
3583
+ vdp_mode: input.vdp_mode && input.vdp_mode !== "include" ? input.vdp_mode : undefined,
3584
+ fresh_launch_days: input.fresh_launch_days,
3585
+ min_wildcards: input.min_wildcards,
3586
+ min_eligible_targets: input.min_eligible_targets,
3587
+ min_total_targets: input.min_total_targets,
3588
+ min_added_24h: input.min_added_24h,
3589
+ min_added_7d: input.min_added_7d,
3590
+ sort_by: input.sort_by,
3591
+ output_mode: outputMode
3592
+ }),
3593
+ upstream_budget: {
3594
+ used: budget.initial - budget.remaining,
3595
+ remaining: budget.remaining
3596
+ },
3597
+ ranking_scope: {
3598
+ mode: "sampled",
3599
+ max_pages_scanned_per_source: input.max_pages,
3600
+ upstream_total_pages: collected.totalPagesBySource,
3601
+ possibly_incomplete: isRankingPossiblyIncomplete(collected.totalPagesBySource, input.max_pages)
3602
+ },
3603
+ programs,
3604
+ meta: {
3605
+ returned: programs.length,
3606
+ total_candidates_after_filters: filteredCandidates.length,
3607
+ upstream_requests_scanned: collected.requestsScanned,
3608
+ programs_scanned: collected.programsScanned,
3609
+ upstream_total_pages: collected.totalPagesBySource
3610
+ }
3611
+ });
3612
+ }
3613
+ async function collectPrograms(client, options) {
3614
+ const sources = options.opportunity_levels.length > 0
3615
+ ? options.opportunity_levels.map((level) => ({ kind: "opportunity", level }))
3616
+ : [{ kind: "programs" }];
3617
+ const collectionOptions = {
3618
+ ...options,
3619
+ runWithProgramPageSlot: createAsyncLimiter(PROGRAM_COLLECTION_CONCURRENCY)
3620
+ };
3621
+ const pagesBySource = await collectProgramSources(client, sources, collectionOptions);
3622
+ const programs = new Map();
3623
+ const sourceRequests = [];
3624
+ const totalPagesBySource = {};
3625
+ for (const page of pagesBySource.sort(compareProgramPageResults)) {
3626
+ for (const program of page.programs) {
3627
+ programs.set(programKey(program, programs.size), program);
3628
+ }
3629
+ totalPagesBySource[page.sourceName] = page.totalPages;
3630
+ sourceRequests.push(page.sourceRequest);
3631
+ }
3632
+ return {
3633
+ programs: [...programs.values()],
3634
+ sourceRequests,
3635
+ requestsScanned: pagesBySource.length,
3636
+ programsScanned: programs.size,
3637
+ totalPagesBySource
3638
+ };
3639
+ }
3640
+ async function collectProgramSources(client, sources, options) {
3641
+ const pages = [];
3642
+ await mapWithConcurrency(sources.map((source, sourceIndex) => ({ source, sourceIndex })), PROGRAM_COLLECTION_CONCURRENCY, async ({ source, sourceIndex }) => {
3643
+ pages.push(...(await collectProgramSource(client, source, sourceIndex, options)));
3644
+ });
3645
+ return pages;
3646
+ }
3647
+ async function collectProgramSource(client, source, sourceIndex, options) {
3648
+ const firstPage = await fetchProgramPage(client, source, sourceIndex, 1, options);
3649
+ if (!firstPage) {
3650
+ return [];
3651
+ }
3652
+ if (firstPage.programs.length === 0 || firstPage.totalPages === 1 || options.max_pages === 1) {
3653
+ return [firstPage];
3654
+ }
3655
+ const maxKnownPage = firstPage.totalPages === undefined ? undefined : Math.min(firstPage.totalPages, options.max_pages);
3656
+ if (maxKnownPage === undefined) {
3657
+ const pages = [firstPage];
3658
+ for (let page = 2; page <= options.max_pages; page += 1) {
3659
+ const nextPage = await fetchProgramPage(client, source, sourceIndex, page, options);
3660
+ if (!nextPage) {
3661
+ break;
3662
+ }
3663
+ pages.push(nextPage);
3664
+ if (nextPage.programs.length === 0 || (nextPage.totalPages !== undefined && page >= nextPage.totalPages)) {
3665
+ break;
3666
+ }
3667
+ }
3668
+ return pages;
3669
+ }
3670
+ const remainingPageNumbers = Array.from({ length: Math.max(0, maxKnownPage - 1) }, (_, index) => index + 2);
3671
+ const remainingPages = [];
3672
+ await mapWithConcurrency(remainingPageNumbers, PROGRAM_COLLECTION_CONCURRENCY, async (page) => {
3673
+ const result = await fetchProgramPage(client, source, sourceIndex, page, options);
3674
+ if (result) {
3675
+ remainingPages.push(result);
3676
+ }
3677
+ });
3678
+ return [firstPage, ...remainingPages];
3679
+ }
3680
+ async function fetchProgramPage(client, source, sourceIndex, page, options) {
3681
+ const runWithSlot = options.runWithProgramPageSlot ?? runImmediately;
3682
+ return runWithSlot(async () => {
3683
+ const query = toQuery({
3684
+ platforms: options.platforms,
3685
+ tags: options.tags,
3686
+ updated_since: options.updated_since,
3687
+ page,
3688
+ page_size: MAX_PROGRAMS_PER_SEARCH
3689
+ });
3690
+ if (!tryConsumeBudget(options.budget)) {
3691
+ addUniqueWarning(options.warnings, "Upstream request budget was exhausted before all requested program pages could be fetched.");
3692
+ return undefined;
3693
+ }
3694
+ const api = source.kind === "opportunity" ? await client.getOpportunities(source.level, query) : await client.listPrograms(query);
3695
+ refundBudgetIfNoUpstreamRequest(options.budget, api);
3696
+ const data = readObject(api.data);
3697
+ const meta = readObject(data?.meta);
3698
+ const sourceName = source.kind === "opportunity" ? `opportunities:${source.level}` : "programs";
3699
+ return {
3700
+ sourceName,
3701
+ sourceIndex,
3702
+ page,
3703
+ programs: readArray(data?.programs),
3704
+ totalPages: readNumber(meta?.total_pages),
3705
+ sourceRequest: stripUndefined({
3706
+ source: sourceName,
3707
+ request_id: api.requestId,
3708
+ upstream_request_id: api.upstreamRequestId,
3709
+ ...apiSourceMetadata(api),
3710
+ page
3711
+ })
3712
+ };
3713
+ });
3714
+ }
3715
+ function compareProgramPageResults(left, right) {
3716
+ return left.sourceIndex - right.sourceIndex || left.page - right.page;
3717
+ }
3718
+ function toProgramCandidate(rawProgram, webBaseUrl) {
3719
+ const program = addProgramResourceLinks(sanitizeProgram(rawProgram, webBaseUrl));
3720
+ const scopeSummary = readObject(program.scope_summary);
3721
+ const targetCounts = readObject(scopeSummary?.target_counts);
3722
+ const targetActivity = readObject(scopeSummary?.target_activity);
3723
+ const rewardRange = readObject(program.reward_range);
3724
+ const opportunityTag = readObject(program.opportunity_tag);
3725
+ const scopeTags = readStringArrayField(scopeSummary ?? {}, "tags");
3726
+ const normalizedScopeTags = scopeTags.map(normalizeTag);
3727
+ const normalizedSearchSignals = normalizedCandidateValues(program, normalizedScopeTags);
3728
+ const hasApiSurface = tagIncludesAnyNormalized(normalizedScopeTags, ["api", "graphql", "rest"]);
3729
+ const hasWebSurface = (numberField(targetCounts, "wildcard") ?? 0) > 0 ||
3730
+ tagIncludesAnyNormalized(normalizedScopeTags, ["domain", "web", "url", "api", "http", "javascript"]);
3731
+ const hasWeb3Surface = tagIncludesAnyNormalized(normalizedScopeTags, WEB3_TAGS) ||
3732
+ tagIncludesAnyNormalized(normalizedSearchSignals, WEB3_TAGS);
3733
+ const hasMobileSurface = tagIncludesAnyNormalized(normalizedScopeTags, ["android", "ios", "mobile"]);
3734
+ const looksLikeCtf = normalizedSearchSignals.some((value) => value.includes("ctf") || value.includes("capture the flag"));
3735
+ const candidate = {
3736
+ program,
3737
+ scopeTags,
3738
+ normalizedScopeTags,
3739
+ normalizedSearchSignals,
3740
+ publicReportCount: numberField(program, "public_report_count"),
3741
+ bountyMin: numberField(rewardRange, "min"),
3742
+ bountyMax: numberField(rewardRange, "max"),
3743
+ totalTargetCount: numberField(targetCounts, "total"),
3744
+ inScopeTargetCount: numberField(targetCounts, "in_scope"),
3745
+ outOfScopeTargetCount: numberField(targetCounts, "out_of_scope"),
3746
+ eligibleTargetCount: numberField(targetCounts, "eligible_for_bounty"),
3747
+ wildcardCount: numberField(targetCounts, "wildcard") ?? 0,
3748
+ added24h: numberField(targetActivity, "added_24h"),
3749
+ added7d: numberField(targetActivity, "added_7d"),
3750
+ removed7d: numberField(targetActivity, "removed_7d"),
3751
+ opportunityScore: numberField(opportunityTag, "opportunity_score"),
3752
+ opportunityTier: stringField(opportunityTag, "tier"),
3753
+ lastUpdatedTime: timestampField(program, "last_updated"),
3754
+ firstSeenTime: timestampField(program, "first_seen_date"),
3755
+ latestChangeTime: timestampField(targetActivity ?? {}, "latest_change_at"),
3756
+ hasApiSurface,
3757
+ hasWebSurface,
3758
+ hasWeb3Surface,
3759
+ hasMobileSurface,
3760
+ looksMobileOnly: hasMobileSurface && !tagIncludesAnyNormalized(normalizedScopeTags, ["domain", "web", "url", "api", "http", "javascript"]),
3761
+ looksLikeCtf,
3762
+ targetSurfaceScore: 0,
3763
+ huntValueScore: 0
3764
+ };
3765
+ candidate.targetSurfaceScore = calculateTargetSurfaceScore(candidate);
3766
+ candidate.huntValueScore = calculateHuntValueScore(candidate);
3767
+ return candidate;
3768
+ }
3769
+ function programCandidateMatches(candidate, filters) {
3770
+ if (filters.platforms.length > 0 && !filters.platforms.map(normalizeTag).includes(normalizeTag(stringField(candidate.program, "platform") ?? ""))) {
3771
+ return false;
3772
+ }
3773
+ if (filters.tags.length > 0 && !candidateMatchesAnyTag(candidate, filters.tags)) {
3774
+ return false;
3775
+ }
3776
+ if (filters.web3 && !candidateHasWeb3Surface(candidate)) {
3777
+ return false;
3778
+ }
3779
+ if (filters.scope_tags.length > 0 && !candidateMatchesAnyTag(candidate, filters.scope_tags)) {
3780
+ return false;
3781
+ }
3782
+ if (filters.target_types.length > 0 && !candidateMatchesAnyTag(candidate, filters.target_types)) {
3783
+ return false;
3784
+ }
3785
+ if (filters.language_tags.length > 0 && !candidateMatchesAnyTag(candidate, filters.language_tags)) {
3786
+ return false;
3787
+ }
3788
+ if (candidate.wildcardCount < filters.min_wildcards) {
3789
+ return false;
3790
+ }
3791
+ if (filters.require_bounty && (candidate.bountyMax ?? candidate.bountyMin ?? 0) <= 0) {
3792
+ return false;
3793
+ }
3794
+ if (filters.min_bounty_min !== undefined && (candidate.bountyMin ?? 0) < filters.min_bounty_min) {
3795
+ return false;
3796
+ }
3797
+ if (filters.min_bounty_max !== undefined && (candidate.bountyMax ?? 0) < filters.min_bounty_max) {
3798
+ return false;
3799
+ }
3800
+ if (filters.max_bounty_max !== undefined && (candidate.bountyMax ?? Number.POSITIVE_INFINITY) > filters.max_bounty_max) {
3801
+ return false;
3802
+ }
3803
+ if (filters.require_known_report_count && candidate.publicReportCount === undefined) {
3804
+ return false;
3805
+ }
3806
+ if (filters.max_public_report_count !== undefined) {
3807
+ if (candidate.publicReportCount === undefined) {
3808
+ return filters.include_unknown_report_count;
3809
+ }
3810
+ if (candidate.publicReportCount > filters.max_public_report_count) {
3811
+ return false;
3812
+ }
3813
+ }
3814
+ if (filters.exclude_ctf && candidateLooksLikeCtf(candidate)) {
3815
+ return false;
3816
+ }
3817
+ if (filters.exclude_mobile_only && candidateLooksMobileOnly(candidate)) {
3818
+ return false;
3819
+ }
3820
+ if (filters.exclude_non_web && !candidateHasWebSurface(candidate)) {
3821
+ return false;
3822
+ }
3823
+ if (filters.has_api_scope && !candidateHasApiSurface(candidate)) {
3824
+ return false;
3825
+ }
3826
+ if (filters.contest_like === true && !candidateLooksContestLike(candidate)) {
3827
+ return false;
3828
+ }
3829
+ if (filters.vdp_mode === "exclude" && candidateLooksLikeVdp(candidate)) {
3830
+ return false;
3831
+ }
3832
+ if (filters.vdp_mode === "only_likely" && !candidateLooksLikeVdp(candidate)) {
3833
+ return false;
3834
+ }
3835
+ if (filters.vdp_mode === "only_known_no_bounty" && !candidateHasKnownNoBounty(candidate)) {
3836
+ return false;
3837
+ }
3838
+ if (filters.fresh_launch_days !== undefined) {
3839
+ const firstSeen = candidate.firstSeenTime;
3840
+ const cutoff = Date.now() - filters.fresh_launch_days * 24 * 60 * 60 * 1000;
3841
+ if (firstSeen <= 0 || firstSeen < cutoff) {
3842
+ return false;
3843
+ }
3844
+ }
3845
+ if (filters.min_eligible_targets !== undefined && (candidate.eligibleTargetCount ?? 0) < filters.min_eligible_targets) {
3846
+ return false;
3847
+ }
3848
+ if (filters.min_total_targets !== undefined && (candidate.totalTargetCount ?? 0) < filters.min_total_targets) {
3849
+ return false;
3850
+ }
3851
+ if (filters.min_added_24h !== undefined && (candidate.added24h ?? 0) < filters.min_added_24h) {
3852
+ return false;
3853
+ }
3854
+ if (filters.min_added_7d !== undefined && (candidate.added7d ?? 0) < filters.min_added_7d) {
3855
+ return false;
3856
+ }
3857
+ return true;
3858
+ }
3859
+ function compareProgramCandidates(left, right, sortBy) {
3860
+ if (sortBy === "best_hunt_value") {
3861
+ return compareNumbersDesc(huntValueScore(left), huntValueScore(right)) || compareLowestReports(left, right);
3862
+ }
3863
+ if (sortBy === "lowest_reports") {
3864
+ return compareLowestReports(left, right);
3865
+ }
3866
+ if (sortBy === "highest_reward") {
3867
+ return compareNumbersDesc(left.bountyMax, right.bountyMax) || compareLowestReports(left, right);
3868
+ }
3869
+ if (sortBy === "most_wildcards") {
3870
+ return compareNumbersDesc(left.wildcardCount, right.wildcardCount) || compareLowestReports(left, right);
3871
+ }
3872
+ if (sortBy === "most_eligible_targets") {
3873
+ return compareNumbersDesc(left.eligibleTargetCount, right.eligibleTargetCount) || compareLowestReports(left, right);
3874
+ }
3875
+ if (sortBy === "freshest") {
3876
+ return right.firstSeenTime - left.firstSeenTime || compareLowestReports(left, right);
3877
+ }
3878
+ if (sortBy === "recently_updated") {
3879
+ return Math.max(right.lastUpdatedTime, right.latestChangeTime) - Math.max(left.lastUpdatedTime, left.latestChangeTime) || compareLowestReports(left, right);
3880
+ }
3881
+ if (sortBy === "highest_opportunity_score") {
3882
+ return compareNumbersDesc(left.opportunityScore, right.opportunityScore) || compareLowestReports(left, right);
3883
+ }
3884
+ if (sortBy === "most_new_targets") {
3885
+ return compareNumbersDesc(left.added7d, right.added7d) || compareNumbersDesc(left.added24h, right.added24h) || compareLowestReports(left, right);
3886
+ }
3887
+ return (compareNumbersDesc(huntValueScore(left), huntValueScore(right)) ||
3888
+ compareNumbersDesc(left.opportunityScore, right.opportunityScore) ||
3889
+ compareLowestReports(left, right) ||
3890
+ compareNumbersDesc(left.bountyMax, right.bountyMax) ||
3891
+ compareNumbersDesc(left.wildcardCount, right.wildcardCount) ||
3892
+ compareNumbersDesc(left.eligibleTargetCount, right.eligibleTargetCount) ||
3893
+ right.firstSeenTime - left.firstSeenTime ||
3894
+ right.lastUpdatedTime - left.lastUpdatedTime);
3895
+ }
3896
+ async function collectTargetSamples(client, candidates, input, budget) {
3897
+ const samples = {};
3898
+ const sampleCandidates = candidates.slice(0, input.target_sample_programs);
3899
+ const sampleFilters = targetSampleFilters(input);
3900
+ await mapWithConcurrency(sampleCandidates, TARGET_SAMPLE_CONCURRENCY, async (candidate) => {
3901
+ const programId = stringField(candidate.program, "id");
3902
+ if (!programId) {
3903
+ return;
3904
+ }
3905
+ try {
3906
+ if (!tryConsumeBudget(budget)) {
3907
+ samples[programId] = [];
3908
+ return;
3909
+ }
3910
+ const api = await client.getProgramTargets(programId);
3911
+ refundBudgetIfNoUpstreamRequest(budget, api);
3912
+ const data = readObject(api.data);
3913
+ const targets = readArray(data?.targets)
3914
+ .map(sanitizeTarget)
3915
+ .filter((target) => targetMatchesSampleFilters(target, sampleFilters))
3916
+ .slice(0, input.target_sample_size);
3917
+ samples[programId] = targets;
3918
+ }
3919
+ catch {
3920
+ samples[programId] = [];
3921
+ }
3922
+ });
3923
+ return samples;
3924
+ }
3925
+ function targetSampleFilters(input) {
3926
+ return {
3927
+ targetSamplesOnlyWildcards: input.target_samples_only_wildcards,
3928
+ onlyInScopeTargets: input.only_in_scope_targets,
3929
+ onlyBountyEligibleTargets: input.only_bounty_eligible_targets,
3930
+ targetTypes: new Set(input.target_types.map(normalizeTag)),
3931
+ scopeTags: new Set(input.scope_tags.map(normalizeTag)),
3932
+ languageTags: new Set(input.language_tags.map(normalizeTag))
3933
+ };
3934
+ }
3935
+ function targetMatchesSampleFilters(target, filters) {
3936
+ if (filters.targetSamplesOnlyWildcards && readBoolean(target.wildcard) !== true) {
3937
+ return false;
3938
+ }
3939
+ if (filters.onlyInScopeTargets && target.in_scope !== true) {
3940
+ return false;
3941
+ }
3942
+ if (filters.onlyBountyEligibleTargets && target.eligible_for_bounty !== true) {
3943
+ return false;
3944
+ }
3945
+ if (filters.targetTypes.size > 0 && !filters.targetTypes.has(normalizeTag(readString(target.target_type) ?? ""))) {
3946
+ return false;
3947
+ }
3948
+ if (filters.scopeTags.size > 0 && !arraysIntersectNormalized(filters.scopeTags, readStringArrayField(target, "scope_tags"))) {
3949
+ return false;
3950
+ }
3951
+ if (filters.languageTags.size > 0 && !arraysIntersectNormalized(filters.languageTags, readStringArrayField(target, "language_tags"))) {
3952
+ return false;
3953
+ }
3954
+ return true;
3955
+ }
3956
+ function compareLowestReports(left, right) {
3957
+ return (compareNumbersAsc(left.publicReportCount, right.publicReportCount) ||
3958
+ compareNumbersDesc(left.bountyMax, right.bountyMax) ||
3959
+ compareNumbersDesc(left.wildcardCount, right.wildcardCount) ||
3960
+ compareNumbersDesc(left.eligibleTargetCount, right.eligibleTargetCount) ||
3961
+ compareNumbersDesc(left.opportunityScore, right.opportunityScore) ||
3962
+ right.lastUpdatedTime - left.lastUpdatedTime ||
3963
+ right.firstSeenTime - left.firstSeenTime);
3964
+ }
3965
+ function programKey(program, fallback) {
3966
+ const object = readObject(program);
3967
+ const id = readString(object?.id);
3968
+ return id ?? `program:${fallback}`;
3969
+ }
3970
+ function tryConsumeBudget(budget, cost = 1) {
3971
+ if (budget.remaining < cost) {
3972
+ return false;
3973
+ }
3974
+ budget.remaining -= cost;
3975
+ return true;
3976
+ }
3977
+ function refundBudgetIfNoUpstreamRequest(budget, api, cost = 1) {
3978
+ if (api.cached || api.coalesced) {
3979
+ budget.remaining = Math.min(budget.initial, budget.remaining + cost);
3980
+ }
3981
+ }
3982
+ function addUniqueWarning(warnings, warning) {
3983
+ if (warnings && !warnings.includes(warning)) {
3984
+ warnings.push(warning);
3985
+ }
3986
+ }
3987
+ function runImmediately(callback) {
3988
+ return callback();
3989
+ }
3990
+ function createAsyncLimiter(concurrency) {
3991
+ const limit = Math.max(1, concurrency);
3992
+ let active = 0;
3993
+ const queue = [];
3994
+ return async (callback) => {
3995
+ if (active >= limit) {
3996
+ await new Promise((resolve) => queue.push(resolve));
3997
+ }
3998
+ else {
3999
+ active += 1;
4000
+ }
4001
+ try {
4002
+ return await callback();
4003
+ }
4004
+ finally {
4005
+ const next = queue.shift();
4006
+ if (next) {
4007
+ next();
4008
+ }
4009
+ else {
4010
+ active -= 1;
4011
+ }
4012
+ }
4013
+ };
4014
+ }
4015
+ async function mapWithConcurrency(items, concurrency, mapper) {
4016
+ let index = 0;
4017
+ async function worker() {
4018
+ while (index < items.length) {
4019
+ const current = index;
4020
+ index += 1;
4021
+ const item = items[current];
4022
+ if (item !== undefined) {
4023
+ await mapper(item);
4024
+ }
4025
+ }
4026
+ }
4027
+ await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
4028
+ }
4029
+ function isRankingPossiblyIncomplete(totalPagesBySource, maxPages) {
4030
+ const totals = Object.values(totalPagesBySource);
4031
+ if (totals.length === 0) {
4032
+ return true;
4033
+ }
4034
+ return totals.some((value) => typeof value !== "number" || value > maxPages);
4035
+ }
4036
+ function numberField(value, field) {
4037
+ return value ? readNumber(value[field]) : undefined;
4038
+ }
4039
+ function stringField(value, field) {
4040
+ return value ? readString(value[field]) : undefined;
4041
+ }
4042
+ function timestampField(value, field) {
4043
+ const text = stringField(value, field);
4044
+ if (!text) {
4045
+ return 0;
4046
+ }
4047
+ const timestamp = Date.parse(text);
4048
+ return Number.isFinite(timestamp) ? timestamp : 0;
4049
+ }
4050
+ function compareNumbersAsc(left, right) {
4051
+ if (left === undefined && right === undefined) {
4052
+ return 0;
4053
+ }
4054
+ if (left === undefined) {
4055
+ return 1;
4056
+ }
4057
+ if (right === undefined) {
4058
+ return -1;
4059
+ }
4060
+ return left - right;
4061
+ }
4062
+ function compareNumbersDesc(left, right) {
4063
+ return compareNumbersAsc(right, left);
4064
+ }
4065
+ function uniqueStrings(values) {
4066
+ return [...new Map(values.map((value) => [normalizeTag(value), value.trim()])).values()].filter(Boolean);
4067
+ }
4068
+ function normalizeTag(value) {
4069
+ return value.trim().toLowerCase();
4070
+ }
4071
+ function tokenSet(value) {
4072
+ return new Set(value.split(/[^a-z0-9]+/).filter((token) => token.length >= 2));
4073
+ }
4074
+ function arraysIntersectNormalized(left, right) {
4075
+ return right.some((value) => left.has(normalizeTag(value)));
4076
+ }
4077
+ function readStringArrayField(value, field) {
4078
+ const fieldValue = value[field];
4079
+ return Array.isArray(fieldValue) ? fieldValue.filter((entry) => typeof entry === "string") : [];
4080
+ }
4081
+ function registerPrompts(server) {
4082
+ server.registerPrompt("find_best_programs_to_hunt", {
4083
+ title: "Find Best Programs To Hunt",
4084
+ description: "Rank fresh BBRadar program data only. Do not invoke external bug bounty skills unless the user explicitly asks for methodology.",
4085
+ argsSchema: {
4086
+ opportunity_level: opportunityLevelSchema.optional(),
4087
+ platforms: z.string().optional().describe("Optional comma-separated platform filters."),
4088
+ tags: z.string().optional().describe("Optional comma-separated scope/language/target-type tags."),
4089
+ max_programs: z.string().optional().describe("Optional maximum shortlist size.")
4090
+ }
4091
+ }, (args) => promptResult(`Use the BBRadar MCP tools only. Do not invoke local bug bounty skills, methodology files, worklogs, or non-BBRadar tools unless the user explicitly asks for methodology. Do not perform active scanning or contact third-party targets. Treat results as fresh API reads; do not describe them as cached.
4092
+
4093
+ Find the best BBRadar program candidates.
4094
+ - Start with find_program_candidates unless the user needs custom filters; pass opportunity_levels from this JSON array unless the user asked for a broader search: ${promptJson([args.opportunity_level ?? "elite"])}.
4095
+ - 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 for 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.
4096
+ - Apply platform filters from this JSON string if provided: ${promptJson(args.platforms ?? "")}.
4097
+ - Apply tag filters from this JSON string if provided: ${promptJson(args.tags ?? "")}.
4098
+ - Keep the shortlist to this JSON value: ${promptJson(args.max_programs ?? "10")}.
4099
+ - Compare freshness, opportunity score, reward range, public report count, target counts, and scope tags.
4100
+ - For exact scope details on one candidate, call get_program_scope_summary or get_program_target_breakdown before falling back to get_program_targets.
4101
+ - Return a ranked list with concise rationale, likely target themes, and BBRadar URLs.
4102
+ - Stay passive-only; do not recommend scanning, probing, exploit attempts, or direct contact with targets.`));
4103
+ server.registerPrompt("summarize_program_scope", {
4104
+ title: "Summarize Program Scope",
4105
+ description: "Summarize in-scope and out-of-scope BBRadar target data for one program.",
4106
+ argsSchema: {
4107
+ program_id: programIdSchema
4108
+ }
4109
+ }, (args) => promptResult(`Use get_program for the program_id in this JSON string: ${promptJson(args.program_id)}. Then use get_program_targets with include_out_of_scope=true, include_ineligible=true, and limit=${MAX_TARGETS_PER_PROGRAM}.
4110
+
4111
+ Summarize the program scope:
4112
+ - Program name, platform, reward range, public report count, first seen date, last updated date, and BBRadar URL.
4113
+ - In-scope bounty-eligible target categories.
4114
+ - Out-of-scope or ineligible target categories.
4115
+ - Wildcards, high-severity target labels, and notable language or scope tags.
4116
+ - Recent target update signals if present.
4117
+ - Do not contact, scan, probe, or validate any listed target.`));
4118
+ server.registerPrompt("prepare_recon_plan", {
4119
+ title: "Prepare Passive Recon Plan",
4120
+ description: "Create a passive-only plan from BBRadar program and target data. Use only when planning is explicitly requested.",
4121
+ argsSchema: {
4122
+ program_id: programIdSchema.optional(),
4123
+ focus: z.string().optional().describe("Optional focus area such as APIs, domains, mobile, or cloud.")
4124
+ }
4125
+ }, (args) => promptResult(`Prepare a passive-only plan from BBRadar data. Program id JSON string, if present: ${promptJson(args.program_id ?? "")}.
4126
+
4127
+ Do not invoke local bug bounty skills, methodology files, worklogs, or non-BBRadar tools unless the user explicitly asks for external methodology.
4128
+
4129
+ Use BBRadar MCP data first:
4130
+ - If a program_id is provided, call get_program and get_program_targets.
4131
+ - If no program_id is provided, call get_opportunities and select a small set of candidate programs before planning.
4132
+ - Focus area JSON string: ${promptJson(args.focus ?? "highest-signal in-scope bounty-eligible targets")}.
4133
+
4134
+ The plan must:
4135
+ - Avoid active scanning, probing, exploit attempts, brute force, or third-party target calls.
4136
+ - Separate allowed passive enrichment ideas from actions that need explicit program authorization.
4137
+ - Prioritize targets by bounty eligibility, freshness, wildcard status, target type, and severity labels.
4138
+ - Include a short checklist of BBRadar follow-up tool calls and decision points.`));
4139
+ }
4140
+ async function runTool(toolName, config, rateLimiter, callback) {
4141
+ const startedAt = Date.now();
4142
+ const decision = rateLimiter.consume(toolName, rateLimitForTool(toolName, config));
4143
+ if (!decision.allowed) {
4144
+ const durationMs = Date.now() - startedAt;
4145
+ recordToolMetric(toolName, durationMs, "rate_limited");
4146
+ return toolResult({
4147
+ request_id: randomUUID(),
4148
+ error: {
4149
+ message: `MCP rate limit exceeded for ${toolName}. Try again after ${decision.resetAt}.`
4150
+ },
4151
+ mcp_rate_limit: toRateLimitPayload(decision),
4152
+ mcp_timing: toolTimingPayload(toolName, durationMs)
4153
+ }, true);
4154
+ }
4155
+ try {
4156
+ const payload = await callback();
4157
+ const durationMs = Date.now() - startedAt;
4158
+ recordToolMetric(toolName, durationMs, "ok");
4159
+ return toolResult({
4160
+ ...payload,
4161
+ mcp_rate_limit: toRateLimitPayload(decision),
4162
+ mcp_timing: toolTimingPayload(toolName, durationMs)
4163
+ });
4164
+ }
4165
+ catch (error) {
4166
+ const durationMs = Date.now() - startedAt;
4167
+ recordToolMetric(toolName, durationMs, "error");
4168
+ return toolResult(errorPayload(error, decision, toolName, durationMs), true);
4169
+ }
4170
+ }
4171
+ function toolResult(payload, isError = false) {
4172
+ const resourceLinks = collectResourceLinks(payload);
4173
+ return {
4174
+ content: [
4175
+ {
4176
+ type: "text",
4177
+ text: JSON.stringify(payload)
4178
+ },
4179
+ ...resourceLinks
4180
+ ],
4181
+ structuredContent: payload,
4182
+ ...(isError ? { isError: true } : {})
4183
+ };
4184
+ }
4185
+ function promptResult(text) {
4186
+ return {
4187
+ messages: [
4188
+ {
4189
+ role: "user",
4190
+ content: {
4191
+ type: "text",
4192
+ text
4193
+ }
4194
+ }
4195
+ ]
4196
+ };
4197
+ }
4198
+ function promptJson(value) {
4199
+ return JSON.stringify(value);
4200
+ }
4201
+ function withApiMetadata(api, payload) {
4202
+ return stripUndefined({
4203
+ request_id: api.requestId,
4204
+ upstream_request_id: api.upstreamRequestId,
4205
+ cache: stripUndefined({
4206
+ hit: api.cached,
4207
+ coalesced_live_request: api.coalesced,
4208
+ expires_at: api.cacheExpiresAt
4209
+ }),
4210
+ fetched_at: api.fetchedAt,
4211
+ ...payload
4212
+ });
4213
+ }
4214
+ function apiSourceMetadata(api) {
4215
+ return stripUndefined({
4216
+ cache_hit: api.cached,
4217
+ cache_expires_at: api.cacheExpiresAt,
4218
+ coalesced_live_request: api.coalesced
4219
+ });
4220
+ }
4221
+ function errorPayload(error, decision, toolName, durationMs) {
4222
+ if (error instanceof BBRadarApiError) {
4223
+ return stripUndefined({
4224
+ request_id: error.requestId,
4225
+ upstream_request_id: error.upstreamRequestId,
4226
+ error: stripUndefined({
4227
+ message: error.message,
4228
+ status: error.status,
4229
+ detail: sanitizeJson(error.detail),
4230
+ errors: sanitizeJson(error.errors)
4231
+ }),
4232
+ mcp_rate_limit: toRateLimitPayload(decision),
4233
+ mcp_timing: toolTimingPayload(toolName, durationMs)
4234
+ });
4235
+ }
4236
+ return {
4237
+ request_id: randomUUID(),
4238
+ error: {
4239
+ message: error instanceof Error ? error.message : String(error)
4240
+ },
4241
+ mcp_rate_limit: toRateLimitPayload(decision),
4242
+ mcp_timing: toolTimingPayload(toolName, durationMs)
4243
+ };
4244
+ }
4245
+ function recordToolMetric(toolName, durationMs, status) {
4246
+ const existing = TOOL_METRICS.get(toolName) ??
4247
+ {
4248
+ calls: 0,
4249
+ errors: 0,
4250
+ totalMs: 0,
4251
+ maxMs: 0,
4252
+ lastMs: 0,
4253
+ lastStatus: status,
4254
+ lastAt: new Date(0).toISOString()
4255
+ };
4256
+ existing.calls += 1;
4257
+ existing.errors += status === "error" || status === "rate_limited" ? 1 : 0;
4258
+ existing.totalMs += durationMs;
4259
+ existing.maxMs = Math.max(existing.maxMs, durationMs);
4260
+ existing.lastMs = durationMs;
4261
+ existing.lastStatus = status;
4262
+ existing.lastAt = new Date().toISOString();
4263
+ TOOL_METRICS.set(toolName, existing);
4264
+ }
4265
+ function toolTimingPayload(toolName, durationMs) {
4266
+ return {
4267
+ tool_name: toolName,
4268
+ duration_ms: durationMs
4269
+ };
4270
+ }
4271
+ function toolMetricsSnapshot() {
4272
+ return Object.fromEntries([...TOOL_METRICS.entries()].map(([toolName, metric]) => [
4273
+ toolName,
4274
+ {
4275
+ calls: metric.calls,
4276
+ errors: metric.errors,
4277
+ avg_ms: metric.calls > 0 ? Math.round(metric.totalMs / metric.calls) : 0,
4278
+ max_ms: metric.maxMs,
4279
+ last_ms: metric.lastMs,
4280
+ last_status: metric.lastStatus,
4281
+ last_at: metric.lastAt
4282
+ }
4283
+ ]));
4284
+ }
4285
+ function toRateLimitPayload(decision) {
4286
+ return {
4287
+ remaining: decision.remaining,
4288
+ reset_at: decision.resetAt
4289
+ };
4290
+ }
4291
+ function rateLimitForTool(toolName, config) {
4292
+ return toolName === "export_targets" ? config.exportRateLimitPerMinute : config.defaultRateLimitPerMinute;
4293
+ }
4294
+ function toQuery(values) {
4295
+ return compactRecord(values);
4296
+ }
4297
+ function compactRecord(values) {
4298
+ return Object.fromEntries(Object.entries(values).filter(([, value]) => {
4299
+ if (value === undefined || value === null) {
4300
+ return false;
4301
+ }
4302
+ if (Array.isArray(value)) {
4303
+ return value.length > 0;
4304
+ }
4305
+ return true;
4306
+ }));
4307
+ }
4308
+ function latestAddedTargetsQuery(input) {
4309
+ return {
4310
+ change_type: "added",
4311
+ target_type: input.target_type,
4312
+ include_out_of_scope: input.include_out_of_scope,
4313
+ include_ineligible: input.include_ineligible,
4314
+ recent_changes_page_size: input.recent_changes_page_size,
4315
+ include_full_target_list: input.include_full_target_list,
4316
+ full_target_list_mode: input.full_target_list_mode,
4317
+ full_target_limit: input.full_target_limit,
4318
+ full_list_include_out_of_scope: input.full_list_include_out_of_scope,
4319
+ full_list_include_ineligible: input.full_list_include_ineligible
4320
+ };
4321
+ }
4322
+ function formatTargetList(targets, mode) {
4323
+ if (mode === "identifiers") {
4324
+ return uniqueStrings(targets.map(targetIdentifier).filter((identifier) => identifier !== undefined));
4325
+ }
4326
+ if (mode === "compact") {
4327
+ return targets.map(compactTarget);
4328
+ }
4329
+ return targets;
4330
+ }
4331
+ function compactTarget(target) {
4332
+ return stripUndefined({
4333
+ identifier: targetIdentifier(target),
4334
+ target_type: stringField(target, "target_type"),
4335
+ in_scope: readBoolean(target.in_scope),
4336
+ eligible_for_bounty: readBoolean(target.eligible_for_bounty),
4337
+ wildcard: readBoolean(target.wildcard)
4338
+ });
4339
+ }
4340
+ function targetIdentifier(target) {
4341
+ return stringField(target, "identifier") ?? stringField(target, "display_name") ?? stringField(target, "normalized_identifier");
4342
+ }
4343
+ function targetMatchesRequestedType(target, targetType) {
4344
+ const normalizedTargetType = normalizeTag(targetType);
4345
+ const actualTargetType = normalizeTag(stringField(target, "target_type") ?? "");
4346
+ return actualTargetType === normalizedTargetType || readStringArrayField(target, "scope_tags").map(normalizeTag).includes(normalizedTargetType);
4347
+ }
4348
+ function targetMatchesQuery(target, query, mode) {
4349
+ const identifier = targetIdentifier(target);
4350
+ if (!identifier) {
4351
+ return false;
4352
+ }
4353
+ const normalizedIdentifier = normalizeTargetIdentifierForMatch(identifier);
4354
+ const normalizedQuery = normalizeTargetIdentifierForMatch(query);
4355
+ if (mode === "exact") {
4356
+ return normalizedIdentifier === normalizedQuery;
4357
+ }
4358
+ if (mode === "suffix") {
4359
+ return (normalizedIdentifier === normalizedQuery ||
4360
+ normalizedIdentifier.endsWith(`.${normalizedQuery}`) ||
4361
+ normalizedQuery.endsWith(`.${normalizedIdentifier}`));
4362
+ }
4363
+ if (mode === "wildcard") {
4364
+ return wildcardTargetMatch(normalizedQuery, normalizedIdentifier) || wildcardTargetMatch(normalizedIdentifier, normalizedQuery);
4365
+ }
4366
+ return normalizedIdentifier.includes(normalizedQuery) || normalizedQuery.includes(normalizedIdentifier);
4367
+ }
4368
+ function normalizeTargetIdentifierForMatch(value) {
4369
+ return value
4370
+ .trim()
4371
+ .toLowerCase()
4372
+ .replace(/^https?:\/\//, "")
4373
+ .replace(/^www\./, "")
4374
+ .replace(/\/+$/, "");
4375
+ }
4376
+ function wildcardTargetMatch(pattern, value) {
4377
+ if (!pattern.includes("*")) {
4378
+ return pattern === value || value.endsWith(`.${pattern}`);
4379
+ }
4380
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
4381
+ return new RegExp(`^${escaped}$`).test(value);
4382
+ }
4383
+ function targetIsWildcard(target) {
4384
+ return readBoolean(target.wildcard) === true || (targetIdentifier(target)?.includes("*.") ?? false);
4385
+ }
4386
+ function targetIsDomain(target) {
4387
+ return targetMatchesAnySignal(target, ["domain", "url", "web", "http"]);
4388
+ }
4389
+ function targetIsApi(target) {
4390
+ return targetMatchesAnySignal(target, ["api", "graphql", "rest"]);
4391
+ }
4392
+ function targetIsMobile(target) {
4393
+ return targetMatchesAnySignal(target, ["android", "ios", "mobile"]);
4394
+ }
4395
+ function targetIsSourceCode(target) {
4396
+ return targetMatchesAnySignal(target, ["source-code", "source code", "repository", "repo", "code"]);
4397
+ }
4398
+ function targetMatchesAnySignal(target, signals) {
4399
+ const values = [
4400
+ stringField(target, "target_type"),
4401
+ targetIdentifier(target),
4402
+ ...readStringArrayField(target, "scope_tags"),
4403
+ ...readStringArrayField(target, "language_tags")
4404
+ ]
4405
+ .filter((value) => value !== undefined)
4406
+ .map(normalizeTag);
4407
+ return signals.some((signal) => values.some((value) => value === signal || value.includes(signal)));
4408
+ }
4409
+ function targetHasAllowedScope(target, options) {
4410
+ if (!options.include_out_of_scope && (options.strict_scope_filter ? target.in_scope !== true : target.in_scope === false)) {
4411
+ return false;
4412
+ }
4413
+ if (!options.include_ineligible && (options.strict_scope_filter ? target.eligible_for_bounty !== true : target.eligible_for_bounty === false)) {
4414
+ return false;
4415
+ }
4416
+ return true;
4417
+ }
4418
+ //# sourceMappingURL=server.js.map