@bbradar/mcp 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -29,17 +29,53 @@ const DEFAULT_FILTER_TARGET_SAMPLE_PROGRAMS = 10;
29
29
  const MAX_FILTER_TARGET_SAMPLE_PROGRAMS = 25;
30
30
  const MAX_LOCAL_EXPORT_RESOURCES = 25;
31
31
  const EXPORT_PREVIEW_LIMIT = 25;
32
+ const STALE_PROGRAM_ID_RESOLVE_PAGES = 5;
33
+ const STALE_PROGRAM_ID_RESOLVE_BUDGET = 5;
34
+ const MAX_RESOLVE_SEARCH_QUERIES = 4;
32
35
  const SDK_VERSION = "1.29.0";
33
36
  const WEB3_TAGS = ["web3", "crypto", "blockchain", "smart-contract", "smart contract", "defi", "nft", "ethereum", "solana", "solidity"];
34
37
  const WEB3_CONTEST_SIGNALS = ["contest", "audit", "competitive", "code4rena", "sherlock", "cantina", "codehawks", "hats", "spearbit", "warden"];
35
38
  const VDP_SIGNALS = ["vdp", "disclosure", "responsible disclosure", "vulnerability disclosure", "no bounty", "non-bounty", "no reward"];
39
+ const PROGRAM_RESOLUTION_STOP_WORDS = new Set([
40
+ "program",
41
+ "programs",
42
+ "target",
43
+ "targets",
44
+ "scope",
45
+ "scopes",
46
+ "new",
47
+ "latest",
48
+ "recent",
49
+ "recently",
50
+ "added",
51
+ "additions",
52
+ "for",
53
+ "from",
54
+ "the",
55
+ "a",
56
+ "an",
57
+ "of",
58
+ "on",
59
+ "in",
60
+ "with",
61
+ "to",
62
+ "get",
63
+ "find",
64
+ "show",
65
+ "list",
66
+ "check",
67
+ "does",
68
+ "has",
69
+ "have",
70
+ "any"
71
+ ]);
36
72
  const targetListModeSchema = z.enum(["identifiers", "compact", "full"]);
37
73
  const changeListModeSchema = z.enum(["compact", "full"]);
38
74
  const programListModeSchema = z.enum(["compact", "full"]);
39
75
  const rewardThresholdModeSchema = z.enum(["max_at_least", "min_at_least"]);
40
76
  const vdpModeSchema = z.enum(["include", "exclude", "only_likely", "only_known_no_bounty"]);
41
77
  const targetMatchModeSchema = z.enum(["contains", "exact", "suffix", "wildcard"]);
42
- const programNameActionSchema = z.enum(["new_targets", "scope_delta", "brief", "target_breakdown"]);
78
+ const programNameActionSchema = z.enum(["new_targets", "targets", "scope_delta", "brief", "target_breakdown"]);
43
79
  const readOnlyAnnotations = {
44
80
  readOnlyHint: true,
45
81
  destructiveHint: false,
@@ -124,8 +160,11 @@ const apiEnvelopeOutputShape = {
124
160
  upstream_request_id: z.string().optional(),
125
161
  fetched_at: z.string().optional(),
126
162
  cache: cacheOutputSchema.optional(),
163
+ source_requests: z.array(jsonRecordSchema).optional(),
127
164
  mcp_rate_limit: rateLimitOutputSchema.optional(),
128
165
  mcp_timing: toolTimingOutputSchema.optional(),
166
+ warnings: z.array(z.string()).optional(),
167
+ program_id_resolution: jsonRecordSchema.optional(),
129
168
  error: errorOutputSchema.optional()
130
169
  };
131
170
  const programListOutputShape = {
@@ -187,7 +226,8 @@ export function createBbradarServer(client, config) {
187
226
  instructions: [
188
227
  "Use BBRadar only for passive bug bounty intelligence; never scan or contact targets.",
189
228
  "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.",
229
+ "Partial program name + action: run_program_name_action. Partial program name only: resolve_program.",
230
+ "Default to bounty-eligible in-scope data. Include out-of-scope, ineligible, VDP, disclosure-only, or no-bounty data only when the user explicitly asks.",
191
231
  "Freshness: get_latest_added_targets, check_program_new_targets, check_watchlist_new_targets, find_recent_by_type, get_program_scope_delta.",
192
232
  "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
233
  "Details: get_program_brief, get_program_scope_summary, get_program_target_breakdown, find_programs_by_target, compare_programs_compact.",
@@ -203,15 +243,20 @@ export function createBbradarServer(client, config) {
203
243
  platforms: stringListSchema,
204
244
  tags: stringListSchema,
205
245
  updated_since: isoDateTimeSchema.optional().describe("ISO datetime."),
246
+ vdp_mode: vdpModeSchema.default("exclude").describe("VDP/no-bounty filter mode."),
206
247
  page: pageSchema,
207
248
  page_size: programsPageSizeSchema
208
249
  },
209
250
  outputSchema: programListOutputShape,
210
251
  annotations: readOnlyAnnotations
211
252
  }, (args) => runTool("search_programs", config, rateLimiter, async () => {
212
- const api = await client.listPrograms(toQuery(args));
253
+ const { vdp_mode, ...filters } = args;
254
+ const api = await client.listPrograms(toQuery(filters));
213
255
  const data = readObject(api.data);
214
- const programs = readArray(data?.programs).map((program) => addProgramResourceLinks(sanitizeProgram(program, config.webBaseUrl)));
256
+ const programs = readArray(data?.programs)
257
+ .map((program) => toProgramCandidate(program, config.webBaseUrl))
258
+ .filter((candidate) => candidateMatchesVdpMode(candidate, vdp_mode))
259
+ .map((candidate) => candidate.program);
215
260
  return withApiMetadata(api, {
216
261
  programs,
217
262
  meta: sanitizeJson(data?.meta)
@@ -225,18 +270,29 @@ export function createBbradarServer(client, config) {
225
270
  },
226
271
  outputSchema: {
227
272
  ...apiEnvelopeOutputShape,
228
- program: programOutputSchema.optional()
273
+ program: programOutputSchema.optional(),
274
+ meta: jsonRecordSchema.optional()
229
275
  },
230
276
  annotations: readOnlyAnnotations
231
277
  }, (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
- });
278
+ return runProgramIdTool(client, config, args.program_id, async (programId) => {
279
+ const api = await client.getProgram(programId);
280
+ return withApiMetadata(api, {
281
+ program: addProgramResourceLinks(sanitizeProgram(api.data, config.webBaseUrl))
282
+ });
283
+ }, async (indexFallback) => ({
284
+ request_id: randomUUID(),
285
+ warnings: [`Returned program metadata from the BBRadar program index because the per-program detail endpoint returned 404 for ${indexFallback.resolvedProgramId}.`],
286
+ program: indexFallback.program,
287
+ meta: {
288
+ detail_source: "program_index",
289
+ detail_endpoint_unavailable: true
290
+ }
291
+ }));
236
292
  }));
237
293
  server.registerTool("resolve_program", {
238
294
  title: "Resolve Program",
239
- description: "Resolve name/handle text to likely program_ids.",
295
+ description: "Resolve partial name/handle text to likely program_ids. Use this when the user gives a program name fragment like Nado.",
240
296
  inputSchema: {
241
297
  query: searchTextSchema,
242
298
  platforms: stringListSchema,
@@ -260,7 +316,7 @@ export function createBbradarServer(client, config) {
260
316
  }, (args) => runTool("resolve_program", config, rateLimiter, async () => resolveProgram(client, config, args)));
261
317
  server.registerTool("run_program_name_action", {
262
318
  title: "Run Program Name Action",
263
- description: "Resolve a name, then run new_targets, scope_delta, brief, or target_breakdown.",
319
+ description: "Resolve a partial program name, then run targets, new_targets, scope_delta, brief, or target_breakdown.",
264
320
  inputSchema: {
265
321
  query: searchTextSchema,
266
322
  action: programNameActionSchema.default("new_targets"),
@@ -312,24 +368,7 @@ export function createBbradarServer(client, config) {
312
368
  },
313
369
  annotations: readOnlyAnnotations
314
370
  }, (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
- });
371
+ return runProgramIdTool(client, config, args.program_id, async (programId) => getProgramTargetsPayload(client, args, programId), async (indexFallback) => getProgramTargetsExportFallbackPayload(client, args, indexFallback.resolvedProgramId));
333
372
  }));
334
373
  server.registerTool("get_recent_changes", {
335
374
  title: "Get Recent Target Changes",
@@ -381,8 +420,8 @@ export function createBbradarServer(client, config) {
381
420
  include_full_target_list: z.boolean().default(true),
382
421
  full_target_list_mode: targetListModeSchema.default("identifiers"),
383
422
  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)
423
+ full_list_include_out_of_scope: z.boolean().default(false),
424
+ full_list_include_ineligible: z.boolean().default(false)
386
425
  },
387
426
  outputSchema: {
388
427
  ...apiEnvelopeOutputShape,
@@ -403,8 +442,8 @@ export function createBbradarServer(client, config) {
403
442
  program_id: programIdSchema,
404
443
  target_list_mode: targetListModeSchema.default("identifiers"),
405
444
  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)
445
+ include_out_of_scope: z.boolean().default(false),
446
+ include_ineligible: z.boolean().default(false)
408
447
  },
409
448
  outputSchema: {
410
449
  ...apiEnvelopeOutputShape,
@@ -414,7 +453,7 @@ export function createBbradarServer(client, config) {
414
453
  meta: jsonRecordSchema.optional()
415
454
  },
416
455
  annotations: readOnlyAnnotations
417
- }, (args) => runTool("get_program_scope_summary", config, rateLimiter, async () => getProgramScopeSummary(client, config, args)));
456
+ }, (args) => runTool("get_program_scope_summary", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramScopeSummary(client, config, { ...args, program_id: programId }), (indexFallback) => getProgramScopeSummaryFromIndexFallback(client, indexFallback, { ...args, program_id: indexFallback.resolvedProgramId }))));
418
457
  server.registerTool("get_program_target_breakdown", {
419
458
  title: "Get Program Target Breakdown",
420
459
  description: "Target type, scope tag, language tag, and bucket counts for one program.",
@@ -422,8 +461,8 @@ export function createBbradarServer(client, config) {
422
461
  program_id: programIdSchema,
423
462
  target_list_mode: targetListModeSchema.default("identifiers"),
424
463
  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)
464
+ include_out_of_scope: z.boolean().default(false),
465
+ include_ineligible: z.boolean().default(false)
427
466
  },
428
467
  outputSchema: {
429
468
  ...apiEnvelopeOutputShape,
@@ -433,7 +472,7 @@ export function createBbradarServer(client, config) {
433
472
  meta: jsonRecordSchema.optional()
434
473
  },
435
474
  annotations: readOnlyAnnotations
436
- }, (args) => runTool("get_program_target_breakdown", config, rateLimiter, async () => getProgramTargetBreakdown(client, config, args)));
475
+ }, (args) => runTool("get_program_target_breakdown", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramTargetBreakdown(client, config, { ...args, program_id: programId }), (indexFallback) => getProgramTargetBreakdownFromIndexFallback(client, indexFallback, { ...args, program_id: indexFallback.resolvedProgramId }))));
437
476
  server.registerTool("get_program_scope_delta", {
438
477
  title: "Get Program Scope Delta",
439
478
  description: "Compact recent scope changes for one program.",
@@ -459,7 +498,7 @@ export function createBbradarServer(client, config) {
459
498
  meta: jsonRecordSchema.optional()
460
499
  },
461
500
  annotations: readOnlyAnnotations
462
- }, (args) => runTool("get_program_scope_delta", config, rateLimiter, async () => getProgramScopeDelta(client, config, args)));
501
+ }, (args) => runTool("get_program_scope_delta", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramScopeDelta(client, config, { ...args, program_id: programId }))));
463
502
  server.registerTool("get_recent_target_activity", {
464
503
  title: "Get Recent Target Activity",
465
504
  description: "Recent target changes grouped by program.",
@@ -510,7 +549,7 @@ export function createBbradarServer(client, config) {
510
549
  meta: jsonRecordSchema.optional()
511
550
  },
512
551
  annotations: readOnlyAnnotations
513
- }, (args) => runTool("check_program_new_targets", config, rateLimiter, async () => checkProgramNewTargets(client, config, args)));
552
+ }, (args) => runTool("check_program_new_targets", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => checkProgramNewTargets(client, config, { ...args, program_id: programId }), (indexFallback) => checkProgramNewTargetsFromIndexedProgram(client, config, { ...args, program_id: indexFallback.resolvedProgramId }, indexFallback.program))));
514
553
  server.registerTool("check_watchlist_new_targets", {
515
554
  title: "Check Watchlist New Targets",
516
555
  description: "New in-scope targets across many known program_ids.",
@@ -540,6 +579,7 @@ export function createBbradarServer(client, config) {
540
579
  platforms: stringListSchema,
541
580
  tags: stringListSchema,
542
581
  updated_since: isoDateTimeSchema.optional(),
582
+ vdp_mode: vdpModeSchema.default("exclude").describe("VDP/no-bounty filter mode."),
543
583
  page: pageSchema,
544
584
  page_size: programsPageSizeSchema
545
585
  },
@@ -549,10 +589,13 @@ export function createBbradarServer(client, config) {
549
589
  },
550
590
  annotations: readOnlyAnnotations
551
591
  }, (args) => runTool("get_opportunities", config, rateLimiter, async () => {
552
- const { level, ...filters } = args;
592
+ const { level, vdp_mode, ...filters } = args;
553
593
  const api = await client.getOpportunities(level, toQuery(filters));
554
594
  const data = readObject(api.data);
555
- const programs = readArray(data?.programs).map((program) => addProgramResourceLinks(sanitizeProgram(program, config.webBaseUrl)));
595
+ const programs = readArray(data?.programs)
596
+ .map((program) => toProgramCandidate(program, config.webBaseUrl))
597
+ .filter((candidate) => candidateMatchesVdpMode(candidate, vdp_mode))
598
+ .map((candidate) => candidate.program);
556
599
  return withApiMetadata(api, {
557
600
  level,
558
601
  programs,
@@ -583,7 +626,7 @@ export function createBbradarServer(client, config) {
583
626
  exclude_non_web: z.boolean().default(false),
584
627
  has_api_scope: z.boolean().default(false),
585
628
  contest_like: z.boolean().default(false).describe("Require contest/audit signals."),
586
- vdp_mode: vdpModeSchema.default("include").describe("VDP/no-bounty filter mode."),
629
+ vdp_mode: vdpModeSchema.default("exclude").describe("VDP/no-bounty filter mode."),
587
630
  fresh_launch_days: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).optional(),
588
631
  min_wildcards: z.number().int().min(0).max(1000).default(0),
589
632
  min_eligible_targets: z.number().int().min(0).optional(),
@@ -791,7 +834,7 @@ export function createBbradarServer(client, config) {
791
834
  min_reward: z.number().int().min(0).optional(),
792
835
  reward_threshold_mode: rewardThresholdModeSchema.default("max_at_least"),
793
836
  require_bounty: z.boolean().default(false),
794
- exclude_vdp: z.boolean().default(false),
837
+ exclude_vdp: z.boolean().default(true),
795
838
  max_public_report_count: z.number().int().min(0).optional(),
796
839
  require_known_report_count: z.boolean().default(false),
797
840
  include_unknown_report_count: z.boolean().default(true),
@@ -821,7 +864,7 @@ export function createBbradarServer(client, config) {
821
864
  require_bounty: z.boolean().default(false),
822
865
  fresh_only: z.boolean().default(false),
823
866
  min_added_7d: z.number().int().min(0).optional(),
824
- exclude_vdp: z.boolean().default(false),
867
+ exclude_vdp: z.boolean().default(true),
825
868
  max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(3),
826
869
  max_results: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(10),
827
870
  include_target_samples: z.boolean().default(false),
@@ -890,7 +933,7 @@ export function createBbradarServer(client, config) {
890
933
  web3: z.boolean().default(false),
891
934
  contest_like: z.boolean().default(false),
892
935
  require_bounty: z.boolean().default(false),
893
- exclude_vdp: z.boolean().default(false),
936
+ exclude_vdp: z.boolean().default(true),
894
937
  max_public_report_count: z.number().int().min(0).optional(),
895
938
  min_eligible_targets: z.number().int().min(0).optional(),
896
939
  fresh_only: z.boolean().default(false),
@@ -969,8 +1012,8 @@ export function createBbradarServer(client, config) {
969
1012
  use_api_search: z.boolean().default(true),
970
1013
  max_pages: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(2),
971
1014
  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),
1015
+ include_out_of_scope: z.boolean().default(false),
1016
+ include_ineligible: z.boolean().default(false),
974
1017
  max_targets_per_program: z.number().int().min(1).max(50).default(10),
975
1018
  target_list_mode: targetListModeSchema.default("identifiers"),
976
1019
  upstream_request_budget: z.number().int().min(1).max(MAX_ACCEPTED_NUMERIC_LIMIT).default(30)
@@ -1155,7 +1198,7 @@ export function createBbradarServer(client, config) {
1155
1198
  changes: z.array(jsonRecordSchema).optional()
1156
1199
  },
1157
1200
  annotations: readOnlyAnnotations
1158
- }, (args) => runTool("summarize_program_activity", config, rateLimiter, async () => summarizeProgramActivity(client, config, args.program_id, args.recent_changes_limit)));
1201
+ }, (args) => runTool("summarize_program_activity", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => summarizeProgramActivity(client, config, programId, args.recent_changes_limit))));
1159
1202
  server.registerTool("get_program_brief", {
1160
1203
  title: "Get Program Brief",
1161
1204
  description: "Compact worth-hunting brief for one program.",
@@ -1178,7 +1221,7 @@ export function createBbradarServer(client, config) {
1178
1221
  meta: jsonRecordSchema.optional()
1179
1222
  },
1180
1223
  annotations: readOnlyAnnotations
1181
- }, (args) => runTool("get_program_brief", config, rateLimiter, async () => getProgramBrief(client, config, args)));
1224
+ }, (args) => runTool("get_program_brief", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramBrief(client, config, { ...args, program_id: programId }))));
1182
1225
  server.registerTool("find_recently_added_wildcards", {
1183
1226
  title: "Find Recently Added Wildcards",
1184
1227
  description: "Recently added wildcard programs.",
@@ -1307,7 +1350,7 @@ export function createBbradarServer(client, config) {
1307
1350
  meta: jsonRecordSchema.optional()
1308
1351
  },
1309
1352
  annotations: readOnlyAnnotations
1310
- }, (args) => runTool("get_program_delta", config, rateLimiter, async () => getProgramDelta(client, config, args)));
1353
+ }, (args) => runTool("get_program_delta", config, rateLimiter, async () => runProgramIdTool(client, config, args.program_id, (programId) => getProgramDelta(client, config, { ...args, program_id: programId }))));
1311
1354
  server.registerTool("export_targets", {
1312
1355
  title: "Export BBRadar Targets",
1313
1356
  description: `Read-only target export. limit max ${MAX_TARGET_EXPORT}.`,
@@ -1374,9 +1417,11 @@ function registerResources(server, client, config, exportStore) {
1374
1417
  mimeType: "application/json"
1375
1418
  }, async (uri) => {
1376
1419
  const programId = parseProgramIdFromResourceUri(uri);
1377
- const api = await client.getProgram(programId);
1378
- const payload = withApiMetadata(api, {
1379
- program: addProgramResourceLinks(sanitizeProgram(api.data, config.webBaseUrl))
1420
+ const payload = await runProgramIdTool(client, config, programId, async (resolvedProgramId) => {
1421
+ const api = await client.getProgram(resolvedProgramId);
1422
+ return withApiMetadata(api, {
1423
+ program: addProgramResourceLinks(sanitizeProgram(api.data, config.webBaseUrl))
1424
+ });
1380
1425
  });
1381
1426
  return jsonResource(uri.toString(), payload);
1382
1427
  });
@@ -1386,15 +1431,17 @@ function registerResources(server, client, config, exportStore) {
1386
1431
  mimeType: "application/json"
1387
1432
  }, async (uri) => {
1388
1433
  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
- }
1434
+ const payload = await runProgramIdTool(client, config, programId, async (resolvedProgramId) => {
1435
+ const api = await client.getProgramTargets(resolvedProgramId);
1436
+ const data = readObject(api.data);
1437
+ const targets = readArray(data?.targets).map(sanitizeTarget);
1438
+ return withApiMetadata(api, {
1439
+ program_id: resolvedProgramId,
1440
+ targets,
1441
+ meta: {
1442
+ total_active_targets: targets.length
1443
+ }
1444
+ });
1398
1445
  });
1399
1446
  return jsonResource(uri.toString(), payload);
1400
1447
  });
@@ -1433,6 +1480,228 @@ function registerResources(server, client, config, exportStore) {
1433
1480
  }));
1434
1481
  });
1435
1482
  }
1483
+ async function getProgramWithIdFallback(client, config, requestedProgramId) {
1484
+ try {
1485
+ return {
1486
+ api: await client.getProgram(requestedProgramId),
1487
+ programId: requestedProgramId,
1488
+ sourceRequests: [],
1489
+ warnings: []
1490
+ };
1491
+ }
1492
+ catch (error) {
1493
+ if (!isProgramNotFoundError(error)) {
1494
+ throw error;
1495
+ }
1496
+ const fallback = await resolveStaleProgramId(client, config, requestedProgramId, error);
1497
+ if (!fallback) {
1498
+ throw error;
1499
+ }
1500
+ return {
1501
+ api: await client.getProgram(fallback.resolvedProgramId),
1502
+ programId: fallback.resolvedProgramId,
1503
+ sourceRequests: fallback.sourceRequests,
1504
+ warnings: fallback.warnings,
1505
+ fallback
1506
+ };
1507
+ }
1508
+ }
1509
+ async function runProgramIdTool(client, config, requestedProgramId, callback, indexFallbackCallback) {
1510
+ try {
1511
+ return await callback(requestedProgramId);
1512
+ }
1513
+ catch (error) {
1514
+ if (!isProgramNotFoundError(error)) {
1515
+ throw error;
1516
+ }
1517
+ const fallback = await resolveStaleProgramId(client, config, requestedProgramId, error);
1518
+ if (!fallback) {
1519
+ if (!indexFallbackCallback) {
1520
+ throw error;
1521
+ }
1522
+ const indexFallback = await resolveIndexedProgramId(client, config, requestedProgramId, error);
1523
+ if (!indexFallback) {
1524
+ throw error;
1525
+ }
1526
+ const payload = await indexFallbackCallback(indexFallback);
1527
+ return withProgramIndexFallbackMetadata(payload, indexFallback);
1528
+ }
1529
+ const payload = await callback(fallback.resolvedProgramId);
1530
+ return withProgramIdFallbackMetadata(payload, fallback);
1531
+ }
1532
+ }
1533
+ async function resolveIndexedProgramId(client, config, requestedProgramId, staleError) {
1534
+ const query = programSearchText(requestedProgramId);
1535
+ if (query.length < 2) {
1536
+ return undefined;
1537
+ }
1538
+ const resolution = await resolveProgram(client, config, {
1539
+ query,
1540
+ platforms: [],
1541
+ tags: [],
1542
+ opportunity_levels: [],
1543
+ max_pages: STALE_PROGRAM_ID_RESOLVE_PAGES,
1544
+ max_results: 5,
1545
+ output_mode: "full",
1546
+ upstream_request_budget: STALE_PROGRAM_ID_RESOLVE_BUDGET
1547
+ });
1548
+ const match = selectProgramIndexFallbackMatch(requestedProgramId, resolution);
1549
+ if (!match) {
1550
+ return undefined;
1551
+ }
1552
+ const sourceRequests = readArray(resolution.source_requests).filter((request) => readObject(request) !== undefined);
1553
+ const warnings = readArray(resolution.warnings).filter((warning) => typeof warning === "string");
1554
+ const staleApiError = staleError instanceof BBRadarApiError ? staleError : undefined;
1555
+ const indexWarning = `program_id ${requestedProgramId} exists in the BBRadar program index, but the per-program detail endpoint returned 404.`;
1556
+ return {
1557
+ requestedProgramId,
1558
+ resolvedProgramId: match.resolvedProgramId,
1559
+ query,
1560
+ staleRequestId: staleApiError?.requestId,
1561
+ staleUpstreamRequestId: staleApiError?.upstreamRequestId,
1562
+ sourceRequests,
1563
+ warnings: uniqueStrings([...warnings, indexWarning]),
1564
+ resolution,
1565
+ program: match.program
1566
+ };
1567
+ }
1568
+ async function resolveStaleProgramId(client, config, requestedProgramId, staleError) {
1569
+ const query = programSearchText(requestedProgramId);
1570
+ if (query.length < 2) {
1571
+ return undefined;
1572
+ }
1573
+ const resolution = await resolveProgram(client, config, {
1574
+ query,
1575
+ platforms: [],
1576
+ tags: [],
1577
+ opportunity_levels: [],
1578
+ max_pages: STALE_PROGRAM_ID_RESOLVE_PAGES,
1579
+ max_results: 5,
1580
+ output_mode: "compact",
1581
+ upstream_request_budget: STALE_PROGRAM_ID_RESOLVE_BUDGET
1582
+ });
1583
+ const match = selectProgramIdFallbackMatch(requestedProgramId, resolution);
1584
+ if (!match) {
1585
+ return undefined;
1586
+ }
1587
+ const sourceRequests = readArray(resolution.source_requests).filter((request) => readObject(request) !== undefined);
1588
+ const warnings = readArray(resolution.warnings).filter((warning) => typeof warning === "string");
1589
+ const staleApiError = staleError instanceof BBRadarApiError ? staleError : undefined;
1590
+ const fallbackWarning = `program_id ${requestedProgramId} was stale; automatically retried with resolved program_id ${match.resolvedProgramId}.`;
1591
+ return {
1592
+ requestedProgramId,
1593
+ resolvedProgramId: match.resolvedProgramId,
1594
+ query,
1595
+ staleRequestId: staleApiError?.requestId,
1596
+ staleUpstreamRequestId: staleApiError?.upstreamRequestId,
1597
+ sourceRequests,
1598
+ warnings: uniqueStrings([...warnings, fallbackWarning]),
1599
+ resolution
1600
+ };
1601
+ }
1602
+ function selectProgramIdFallbackMatch(requestedProgramId, resolution) {
1603
+ const requestedHandle = normalizeTag(programHandleText(requestedProgramId));
1604
+ const requestedSearchText = normalizeTag(programSearchText(requestedProgramId));
1605
+ const candidates = readArray(resolution.matches)
1606
+ .map((entry) => readObject(entry))
1607
+ .filter((entry) => entry !== undefined)
1608
+ .map((entry) => {
1609
+ const program = readObject(entry.program);
1610
+ const resolvedProgramId = stringField(entry, "program_id") ?? stringField(program, "id");
1611
+ const candidateHandle = normalizeTag(stringField(program, "handle") ?? "");
1612
+ const candidateSearchText = resolvedProgramId ? normalizeTag(programSearchText(resolvedProgramId)) : "";
1613
+ const score = readNumber(entry.score) ?? 0;
1614
+ const exactHandleMatch = candidateHandle === requestedHandle ||
1615
+ candidateHandle === requestedSearchText ||
1616
+ candidateSearchText === requestedSearchText;
1617
+ return {
1618
+ resolvedProgramId,
1619
+ score,
1620
+ exactHandleMatch
1621
+ };
1622
+ })
1623
+ .filter((entry) => entry.resolvedProgramId !== undefined && entry.resolvedProgramId !== requestedProgramId && entry.exactHandleMatch && entry.score >= 80)
1624
+ .sort((left, right) => right.score - left.score || left.resolvedProgramId.localeCompare(right.resolvedProgramId));
1625
+ if (candidates.length === 0) {
1626
+ return undefined;
1627
+ }
1628
+ const [best, next] = candidates;
1629
+ if (!best || (next && best.score === next.score)) {
1630
+ return undefined;
1631
+ }
1632
+ return {
1633
+ resolvedProgramId: best.resolvedProgramId
1634
+ };
1635
+ }
1636
+ function selectProgramIndexFallbackMatch(requestedProgramId, resolution) {
1637
+ const requestedId = normalizeTag(requestedProgramId);
1638
+ const requestedHandle = normalizeTag(programHandleText(requestedProgramId));
1639
+ const requestedPlatform = normalizeTag(programPlatformText(requestedProgramId));
1640
+ const candidates = readArray(resolution.matches)
1641
+ .map((entry) => readObject(entry))
1642
+ .filter((entry) => entry !== undefined)
1643
+ .map((entry) => {
1644
+ const program = readObject(entry.program);
1645
+ const resolvedProgramId = stringField(entry, "program_id") ?? stringField(program, "id");
1646
+ const candidateHandle = normalizeTag(stringField(program, "handle") ?? (resolvedProgramId ? programHandleText(resolvedProgramId) : ""));
1647
+ const candidatePlatform = normalizeTag(stringField(program, "platform") ?? (resolvedProgramId ? programPlatformText(resolvedProgramId) : ""));
1648
+ const score = readNumber(entry.score) ?? 0;
1649
+ const idMatch = resolvedProgramId !== undefined && normalizeTag(resolvedProgramId) === requestedId;
1650
+ const platformHandleMatch = requestedPlatform.length > 0 &&
1651
+ candidatePlatform === requestedPlatform &&
1652
+ candidateHandle === requestedHandle;
1653
+ return {
1654
+ resolvedProgramId,
1655
+ program,
1656
+ score,
1657
+ exactMatch: idMatch || platformHandleMatch
1658
+ };
1659
+ })
1660
+ .filter((entry) => entry.resolvedProgramId !== undefined && entry.program !== undefined && entry.exactMatch && entry.score >= 80)
1661
+ .sort((left, right) => right.score - left.score || left.resolvedProgramId.localeCompare(right.resolvedProgramId));
1662
+ if (candidates.length === 0) {
1663
+ return undefined;
1664
+ }
1665
+ const [best, next] = candidates;
1666
+ if (!best || (next && best.score === next.score && normalizeTag(next.resolvedProgramId) !== normalizeTag(best.resolvedProgramId))) {
1667
+ return undefined;
1668
+ }
1669
+ return {
1670
+ resolvedProgramId: best.resolvedProgramId,
1671
+ program: addProgramResourceLinks(best.program)
1672
+ };
1673
+ }
1674
+ function withProgramIdFallbackMetadata(payload, fallback) {
1675
+ const sourceRequests = readArray(payload.source_requests).filter((request) => readObject(request) !== undefined);
1676
+ const warnings = readArray(payload.warnings).filter((warning) => typeof warning === "string");
1677
+ const resolution = stripUndefined({
1678
+ requested_program_id: fallback.requestedProgramId,
1679
+ resolved_program_id: fallback.resolvedProgramId,
1680
+ query: fallback.query,
1681
+ reason: "stale_program_id",
1682
+ stale_status: 404,
1683
+ stale_request_id: fallback.staleRequestId,
1684
+ stale_upstream_request_id: fallback.staleUpstreamRequestId,
1685
+ resolution_request_id: stringField(fallback.resolution, "request_id")
1686
+ });
1687
+ return stripUndefined({
1688
+ ...payload,
1689
+ source_requests: [...fallback.sourceRequests, ...sourceRequests],
1690
+ warnings: uniqueStrings([...fallback.warnings, ...warnings]),
1691
+ program_id_resolution: resolution
1692
+ });
1693
+ }
1694
+ function withProgramIndexFallbackMetadata(payload, fallback) {
1695
+ const enriched = withProgramIdFallbackMetadata(payload, fallback);
1696
+ const resolution = readObject(enriched.program_id_resolution);
1697
+ return {
1698
+ ...enriched,
1699
+ program_id_resolution: stripUndefined({
1700
+ ...(resolution ?? {}),
1701
+ reason: "program_index_detail_404"
1702
+ })
1703
+ };
1704
+ }
1436
1705
  async function resolveProgram(client, config, input) {
1437
1706
  const warnings = [];
1438
1707
  const budget = {
@@ -1440,10 +1709,11 @@ async function resolveProgram(client, config, input) {
1440
1709
  remaining: input.upstream_request_budget
1441
1710
  };
1442
1711
  const maxPages = clampLimit("max_pages", input.max_pages, MAX_FIND_PROGRAM_PAGES, warnings);
1443
- const collected = await collectPrograms(client, {
1712
+ const searchQueries = programResolutionSearchQueries(input.query);
1713
+ const collected = await collectProgramsForResolution(client, {
1714
+ searchQueries,
1444
1715
  platforms: input.platforms,
1445
1716
  tags: input.tags,
1446
- updated_since: undefined,
1447
1717
  opportunity_levels: input.opportunity_levels,
1448
1718
  max_pages: maxPages,
1449
1719
  budget,
@@ -1465,6 +1735,7 @@ async function resolveProgram(client, config, input) {
1465
1735
  warnings: warnings.length > 0 ? warnings : undefined,
1466
1736
  query: {
1467
1737
  text: input.query,
1738
+ search_queries: searchQueries,
1468
1739
  platforms: input.platforms.length > 0 ? input.platforms : undefined,
1469
1740
  tags: input.tags.length > 0 ? input.tags : undefined
1470
1741
  },
@@ -1572,6 +1843,31 @@ async function runResolvedProgramAction(client, config, programId, input) {
1572
1843
  include_ineligible: input.include_ineligible
1573
1844
  });
1574
1845
  }
1846
+ if (input.action === "targets") {
1847
+ const api = await client.getProgramTargets(programId);
1848
+ const data = readObject(api.data);
1849
+ const rawTargets = readArray(data?.targets);
1850
+ const sanitizedTargets = rawTargets
1851
+ .map(sanitizeTarget)
1852
+ .filter((target) => targetHasAllowedScope(target, {
1853
+ include_out_of_scope: input.include_out_of_scope,
1854
+ include_ineligible: input.include_ineligible,
1855
+ strict_scope_filter: true
1856
+ }));
1857
+ const targets = sanitizedTargets.slice(0, input.max_targets);
1858
+ return withApiMetadata(api, {
1859
+ program_id: programId,
1860
+ targets: formatTargetList(targets, input.target_list_mode),
1861
+ meta: {
1862
+ limit: input.max_targets,
1863
+ output_mode: input.target_list_mode,
1864
+ total_active_targets: rawTargets.length,
1865
+ total_after_filters: sanitizedTargets.length,
1866
+ returned: targets.length,
1867
+ has_more: sanitizedTargets.length > targets.length
1868
+ }
1869
+ });
1870
+ }
1575
1871
  return checkProgramNewTargets(client, config, {
1576
1872
  program_id: programId,
1577
1873
  since: input.since,
@@ -1937,7 +2233,7 @@ function findLowNoisePrograms(client, config, input) {
1937
2233
  exclude_mobile_only: false,
1938
2234
  exclude_non_web: false,
1939
2235
  has_api_scope: false,
1940
- vdp_mode: input.require_bounty ? "exclude" : "include",
2236
+ vdp_mode: "exclude",
1941
2237
  fresh_launch_days: undefined,
1942
2238
  min_wildcards: 0,
1943
2239
  min_eligible_targets: input.min_eligible_targets,
@@ -2268,17 +2564,22 @@ async function comparePrograms(client, config, input) {
2268
2564
  const candidates = [];
2269
2565
  const sourceRequests = [];
2270
2566
  const errors = [];
2567
+ const warnings = [];
2271
2568
  await mapWithConcurrency(input.program_ids, TARGET_SAMPLE_CONCURRENCY, async (programId) => {
2272
2569
  try {
2273
- const api = await client.getProgram(programId);
2274
- candidates.push(toProgramCandidate(api.data, config.webBaseUrl));
2275
- sourceRequests.push({
2570
+ const lookup = await getProgramWithIdFallback(client, config, programId);
2571
+ const api = lookup.api;
2572
+ const requestSource = stripUndefined({
2276
2573
  source: "program",
2277
- program_id: programId,
2574
+ program_id: lookup.programId,
2575
+ requested_program_id: lookup.programId === programId ? undefined : programId,
2278
2576
  request_id: api.requestId,
2279
2577
  upstream_request_id: api.upstreamRequestId,
2280
2578
  ...apiSourceMetadata(api)
2281
2579
  });
2580
+ candidates.push(toProgramCandidate(api.data, config.webBaseUrl));
2581
+ sourceRequests.push(...lookup.sourceRequests, requestSource);
2582
+ warnings.push(...lookup.warnings);
2282
2583
  }
2283
2584
  catch (error) {
2284
2585
  errors.push(programFetchError(programId, error));
@@ -2333,6 +2634,7 @@ async function comparePrograms(client, config, input) {
2333
2634
  partial_success: errors.length > 0,
2334
2635
  program_ids_requested: input.program_ids,
2335
2636
  failed_program_ids: errors.map((error) => error.program_id).filter((id) => typeof id === "string"),
2637
+ warnings: warnings.length > 0 ? uniqueStrings(warnings) : undefined,
2336
2638
  errors: errors.length > 0 ? errors : undefined,
2337
2639
  source_requests: sourceRequests,
2338
2640
  compared_programs: candidates.map((candidate, index) => candidateOutput(candidate, index, targetSamples, input.output_mode ?? "compact")),
@@ -2467,11 +2769,132 @@ async function getLatestAddedTargets(client, config, input) {
2467
2769
  })
2468
2770
  });
2469
2771
  }
2772
+ async function getProgramTargetsPayload(client, input, programId) {
2773
+ const api = await client.getProgramTargets(programId);
2774
+ const data = readObject(api.data);
2775
+ const rawTargets = readArray(data?.targets);
2776
+ const sanitizedTargets = rawTargets.map(sanitizeTarget).filter((target) => targetHasAllowedScope(target, input));
2777
+ const targets = sanitizedTargets.slice(input.offset, input.offset + input.limit);
2778
+ return withApiMetadata(api, {
2779
+ program_id: programId,
2780
+ targets: formatTargetList(targets, input.output_mode),
2781
+ meta: {
2782
+ offset: input.offset,
2783
+ limit: input.limit,
2784
+ output_mode: input.output_mode,
2785
+ total_active_targets: rawTargets.length,
2786
+ total_after_filters: sanitizedTargets.length,
2787
+ returned: targets.length,
2788
+ has_more: input.offset + input.limit < sanitizedTargets.length,
2789
+ target_source: "program_targets"
2790
+ }
2791
+ });
2792
+ }
2793
+ async function getProgramTargetsExportFallbackPayload(client, input, programId) {
2794
+ const exportLookup = await fetchProgramTargetsFromExportFallback(client, programId);
2795
+ const sanitizedTargets = exportLookup.rawTargets.filter((target) => targetHasAllowedScope(target, input));
2796
+ const targets = sanitizedTargets.slice(input.offset, input.offset + input.limit);
2797
+ return stripUndefined({
2798
+ request_id: exportLookup.requestId,
2799
+ upstream_request_id: exportLookup.upstreamRequestId,
2800
+ fetched_at: exportLookup.fetchedAt,
2801
+ cache: exportLookup.cache,
2802
+ source_requests: exportLookup.sourceRequests,
2803
+ warnings: exportLookup.warnings.length > 0 ? uniqueStrings(exportLookup.warnings) : undefined,
2804
+ program_id: programId,
2805
+ targets: formatTargetList(targets, input.output_mode),
2806
+ meta: stripUndefined({
2807
+ offset: input.offset,
2808
+ limit: input.limit,
2809
+ output_mode: input.output_mode,
2810
+ total_active_targets: exportLookup.rawTargets.length,
2811
+ total_after_filters: sanitizedTargets.length,
2812
+ returned: targets.length,
2813
+ has_more: input.offset + input.limit < sanitizedTargets.length,
2814
+ target_source: exportLookup.source,
2815
+ target_list_unavailable: exportLookup.unavailable,
2816
+ fallback_error: exportLookup.error
2817
+ })
2818
+ });
2819
+ }
2820
+ async function fetchProgramTargetsFromExportFallback(client, programId) {
2821
+ try {
2822
+ const api = await client.exportTargets({
2823
+ program_ids: [programId],
2824
+ include_out_of_scope: true,
2825
+ include_ineligible: true,
2826
+ format: "json",
2827
+ limit: MAX_TARGET_EXPORT
2828
+ });
2829
+ const rawTargets = readExportTargetRows(api.data).map((row) => sanitizeTarget(readObject(row)?.target ?? row));
2830
+ return {
2831
+ requestId: api.requestId,
2832
+ upstreamRequestId: api.upstreamRequestId,
2833
+ fetchedAt: api.fetchedAt,
2834
+ cache: readObject(stripUndefined({
2835
+ hit: api.cached,
2836
+ coalesced_live_request: api.coalesced,
2837
+ expires_at: api.cacheExpiresAt
2838
+ })),
2839
+ source: "target_export",
2840
+ rawTargets,
2841
+ sourceRequests: [
2842
+ {
2843
+ source: "target_export",
2844
+ program_id: programId,
2845
+ request_id: api.requestId,
2846
+ upstream_request_id: api.upstreamRequestId,
2847
+ ...apiSourceMetadata(api)
2848
+ }
2849
+ ],
2850
+ warnings: [`Used target export fallback because the per-program targets endpoint returned 404 for ${programId}.`],
2851
+ unavailable: false
2852
+ };
2853
+ }
2854
+ catch (error) {
2855
+ return {
2856
+ requestId: randomUUID(),
2857
+ source: "unavailable",
2858
+ rawTargets: [],
2859
+ sourceRequests: [
2860
+ {
2861
+ source: "target_export",
2862
+ program_id: programId,
2863
+ failed: true,
2864
+ error: programFetchError(programId, error)
2865
+ }
2866
+ ],
2867
+ warnings: [
2868
+ `The BBRadar program index contains ${programId}, but both the per-program targets endpoint and target export fallback were unavailable. Exact targets cannot be returned from the current API response.`
2869
+ ],
2870
+ unavailable: true,
2871
+ error: programFetchError(programId, error)
2872
+ };
2873
+ }
2874
+ }
2875
+ function readExportTargetRows(data) {
2876
+ if (Array.isArray(data)) {
2877
+ return data;
2878
+ }
2879
+ const object = readObject(data);
2880
+ if (!object) {
2881
+ return [];
2882
+ }
2883
+ return [object.targets, object.results, object.data]
2884
+ .map(readArray)
2885
+ .find((rows) => rows.length > 0) ?? [];
2886
+ }
2470
2887
  async function getProgramScopeSummary(client, config, input) {
2471
2888
  const [programApi, targetsApi] = await Promise.all([client.getProgram(input.program_id), client.getProgramTargets(input.program_id)]);
2472
2889
  const targetsData = readObject(targetsApi.data);
2473
2890
  const rawTargets = readArray(targetsData?.targets);
2474
- const targets = rawTargets.map(sanitizeTarget);
2891
+ const targets = rawTargets
2892
+ .map(sanitizeTarget)
2893
+ .filter((target) => targetHasAllowedScope(target, {
2894
+ include_out_of_scope: input.include_out_of_scope,
2895
+ include_ineligible: input.include_ineligible,
2896
+ strict_scope_filter: false
2897
+ }));
2475
2898
  return {
2476
2899
  request_id: randomUUID(),
2477
2900
  source_requests: [
@@ -2492,11 +2915,39 @@ async function getProgramScopeSummary(client, config, input) {
2492
2915
  scope: buildTargetScopeSummary(targets, input.target_list_mode, input.group_limit, input.include_out_of_scope, input.include_ineligible),
2493
2916
  meta: {
2494
2917
  total_active_targets: rawTargets.length,
2918
+ total_after_filters: targets.length,
2495
2919
  target_list_mode: input.target_list_mode,
2496
2920
  group_limit: input.group_limit
2497
2921
  }
2498
2922
  };
2499
2923
  }
2924
+ async function getProgramScopeSummaryFromIndexFallback(client, indexFallback, input) {
2925
+ const exportLookup = await fetchProgramTargetsFromExportFallback(client, input.program_id);
2926
+ const targets = exportLookup.rawTargets.filter((target) => targetHasAllowedScope(target, {
2927
+ include_out_of_scope: input.include_out_of_scope,
2928
+ include_ineligible: input.include_ineligible,
2929
+ strict_scope_filter: false
2930
+ }));
2931
+ return stripUndefined({
2932
+ request_id: exportLookup.requestId,
2933
+ upstream_request_id: exportLookup.upstreamRequestId,
2934
+ fetched_at: exportLookup.fetchedAt,
2935
+ cache: exportLookup.cache,
2936
+ source_requests: exportLookup.sourceRequests,
2937
+ warnings: exportLookup.warnings.length > 0 ? uniqueStrings(exportLookup.warnings) : undefined,
2938
+ program: formatProgram(indexFallback.program, "compact"),
2939
+ scope: buildTargetScopeSummary(targets, input.target_list_mode, input.group_limit, input.include_out_of_scope, input.include_ineligible),
2940
+ meta: stripUndefined({
2941
+ total_active_targets: exportLookup.rawTargets.length,
2942
+ total_after_filters: targets.length,
2943
+ target_list_mode: input.target_list_mode,
2944
+ group_limit: input.group_limit,
2945
+ target_source: exportLookup.source,
2946
+ target_list_unavailable: exportLookup.unavailable,
2947
+ fallback_error: exportLookup.error
2948
+ })
2949
+ });
2950
+ }
2500
2951
  async function getProgramTargetBreakdown(client, config, input) {
2501
2952
  const [programApi, targetsApi] = await Promise.all([client.getProgram(input.program_id), client.getProgramTargets(input.program_id)]);
2502
2953
  const targetsData = readObject(targetsApi.data);
@@ -2543,6 +2994,42 @@ async function getProgramTargetBreakdown(client, config, input) {
2543
2994
  }
2544
2995
  };
2545
2996
  }
2997
+ async function getProgramTargetBreakdownFromIndexFallback(client, indexFallback, input) {
2998
+ const exportLookup = await fetchProgramTargetsFromExportFallback(client, input.program_id);
2999
+ const targets = exportLookup.rawTargets.filter((target) => targetHasAllowedScope(target, {
3000
+ include_out_of_scope: input.include_out_of_scope,
3001
+ include_ineligible: input.include_ineligible,
3002
+ strict_scope_filter: false
3003
+ }));
3004
+ const scope = buildTargetScopeSummary(targets, input.target_list_mode, input.group_limit, input.include_out_of_scope, input.include_ineligible);
3005
+ return stripUndefined({
3006
+ request_id: exportLookup.requestId,
3007
+ upstream_request_id: exportLookup.upstreamRequestId,
3008
+ fetched_at: exportLookup.fetchedAt,
3009
+ cache: exportLookup.cache,
3010
+ source_requests: exportLookup.sourceRequests,
3011
+ warnings: exportLookup.warnings.length > 0 ? uniqueStrings(exportLookup.warnings) : undefined,
3012
+ program: formatProgram(indexFallback.program, "compact"),
3013
+ breakdown: {
3014
+ target_counts: readObject(scope.target_counts),
3015
+ by_target_type: countTargetValues(targets, (target) => stringField(target, "target_type")),
3016
+ by_scope_tag: countTargetValues(targets, (target) => readStringArrayField(target, "scope_tags")),
3017
+ by_language_tag: countTargetValues(targets, (target) => readStringArrayField(target, "language_tags")),
3018
+ group_counts: readObject(scope.group_counts),
3019
+ group_has_more: readObject(scope.group_has_more),
3020
+ groups: readObject(scope.groups)
3021
+ },
3022
+ meta: stripUndefined({
3023
+ total_active_targets: exportLookup.rawTargets.length,
3024
+ total_after_filters: targets.length,
3025
+ target_list_mode: input.target_list_mode,
3026
+ group_limit: input.group_limit,
3027
+ target_source: exportLookup.source,
3028
+ target_list_unavailable: exportLookup.unavailable,
3029
+ fallback_error: exportLookup.error
3030
+ })
3031
+ });
3032
+ }
2546
3033
  async function getProgramScopeDelta(client, config, input) {
2547
3034
  const programApi = await client.getProgram(input.program_id);
2548
3035
  const program = addProgramResourceLinks(sanitizeProgram(programApi.data, config.webBaseUrl));
@@ -2673,13 +3160,15 @@ async function getRecentTargetActivity(client, config, input) {
2673
3160
  };
2674
3161
  }
2675
3162
  async function checkProgramNewTargets(client, config, input) {
2676
- const handle = programSearchText(input.program_id);
3163
+ const programLookup = await getProgramWithIdFallback(client, config, input.program_id);
3164
+ const programId = programLookup.programId;
3165
+ const handle = programSearchText(programId);
2677
3166
  const api = await client.getRecentChanges({
2678
3167
  change_type: "added",
2679
3168
  include_removed: false,
2680
3169
  include_ineligible: input.include_ineligible,
2681
3170
  include_out_of_scope: false,
2682
- search: handle.length >= 2 ? handle : input.program_id,
3171
+ search: handle.length >= 2 ? handle : programId,
2683
3172
  tags: input.target_type ? [input.target_type] : [],
2684
3173
  page: 1,
2685
3174
  page_size: input.page_size
@@ -2693,7 +3182,88 @@ async function checkProgramNewTargets(client, config, input) {
2693
3182
  const target = readObject(change.target);
2694
3183
  const changedAt = timestampField(change, "changed_at");
2695
3184
  return (stringField(change, "change_type") === "added" &&
2696
- stringField(readObject(change.program), "id") === input.program_id &&
3185
+ stringField(readObject(change.program), "id") === programId &&
3186
+ target !== undefined &&
3187
+ (sinceTimestamp === undefined || changedAt >= sinceTimestamp) &&
3188
+ (!input.target_type || targetMatchesRequestedType(target, input.target_type)) &&
3189
+ targetHasAllowedScope(target, {
3190
+ include_out_of_scope: false,
3191
+ include_ineligible: input.include_ineligible,
3192
+ strict_scope_filter: true
3193
+ }));
3194
+ })
3195
+ .sort((left, right) => timestampField(right, "changed_at") - timestampField(left, "changed_at"));
3196
+ const limitedChanges = matchingChanges.slice(0, input.max_targets);
3197
+ const targets = limitedChanges
3198
+ .map((change) => readObject(change.target))
3199
+ .filter((target) => target !== undefined);
3200
+ const latest = matchingChanges[0];
3201
+ const sourceRequests = [
3202
+ {
3203
+ source: "program",
3204
+ program_id: programId,
3205
+ requested_program_id: programId === input.program_id ? undefined : input.program_id,
3206
+ request_id: programLookup.api.requestId,
3207
+ upstream_request_id: programLookup.api.upstreamRequestId,
3208
+ ...apiSourceMetadata(programLookup.api)
3209
+ },
3210
+ {
3211
+ source: "recent_changes",
3212
+ request_id: api.requestId,
3213
+ upstream_request_id: api.upstreamRequestId,
3214
+ ...apiSourceMetadata(api)
3215
+ }
3216
+ ];
3217
+ const payload = stripUndefined({
3218
+ request_id: randomUUID(),
3219
+ source_requests: sourceRequests,
3220
+ warnings: !programLookup.fallback && programLookup.warnings.length > 0 ? uniqueStrings(programLookup.warnings) : undefined,
3221
+ program_id: programId,
3222
+ program: latest
3223
+ ? formatProgram(addProgramResourceLinks(readObject(latest.program) ?? {}), "compact")
3224
+ : formatProgram(addProgramResourceLinks(sanitizeProgram(programLookup.api.data, config.webBaseUrl)), "compact"),
3225
+ has_new_targets: matchingChanges.length > 0,
3226
+ new_target_count: matchingChanges.length,
3227
+ latest_added_at: latest ? stringField(latest, "changed_at") : undefined,
3228
+ new_targets: formatTargetList(targets, input.target_list_mode),
3229
+ meta: stripUndefined({
3230
+ recent_changes_scanned: changes.length,
3231
+ returned: targets.length,
3232
+ has_more: matchingChanges.length > targets.length,
3233
+ since: input.since,
3234
+ target_type: input.target_type,
3235
+ in_scope_only: true,
3236
+ include_ineligible: input.include_ineligible,
3237
+ target_list_mode: input.target_list_mode,
3238
+ upstream_total_pages: readNumber(upstreamMeta?.total_pages),
3239
+ scan_may_be_incomplete: (readNumber(upstreamMeta?.total_pages) ?? 1) > 1
3240
+ })
3241
+ });
3242
+ return programLookup.fallback ? withProgramIdFallbackMetadata(payload, programLookup.fallback) : payload;
3243
+ }
3244
+ async function checkProgramNewTargetsFromIndexedProgram(client, config, input, indexedProgram) {
3245
+ const programId = input.program_id;
3246
+ const handle = programSearchText(programId);
3247
+ const api = await client.getRecentChanges({
3248
+ change_type: "added",
3249
+ include_removed: false,
3250
+ include_ineligible: input.include_ineligible,
3251
+ include_out_of_scope: false,
3252
+ search: handle.length >= 2 ? handle : programId,
3253
+ tags: input.target_type ? [input.target_type] : [],
3254
+ page: 1,
3255
+ page_size: input.page_size
3256
+ });
3257
+ const data = readObject(api.data);
3258
+ const upstreamMeta = readObject(sanitizeJson(data?.meta));
3259
+ const sinceTimestamp = input.since ? Date.parse(input.since) : undefined;
3260
+ const changes = readArray(data?.results).map((change) => sanitizeChange(change, config.webBaseUrl));
3261
+ const matchingChanges = changes
3262
+ .filter((change) => {
3263
+ const target = readObject(change.target);
3264
+ const changedAt = timestampField(change, "changed_at");
3265
+ return (stringField(change, "change_type") === "added" &&
3266
+ stringField(readObject(change.program), "id") === programId &&
2697
3267
  target !== undefined &&
2698
3268
  (sinceTimestamp === undefined || changedAt >= sinceTimestamp) &&
2699
3269
  (!input.target_type || targetMatchesRequestedType(target, input.target_type)) &&
@@ -2719,8 +3289,10 @@ async function checkProgramNewTargets(client, config, input) {
2719
3289
  ...apiSourceMetadata(api)
2720
3290
  }
2721
3291
  ],
2722
- program_id: input.program_id,
2723
- program: latest ? formatProgram(addProgramResourceLinks(readObject(latest.program) ?? {}), "compact") : undefined,
3292
+ program_id: programId,
3293
+ program: latest
3294
+ ? formatProgram(addProgramResourceLinks(readObject(latest.program) ?? {}), "compact")
3295
+ : formatProgram(indexedProgram, "compact"),
2724
3296
  has_new_targets: matchingChanges.length > 0,
2725
3297
  new_target_count: matchingChanges.length,
2726
3298
  latest_added_at: latest ? stringField(latest, "changed_at") : undefined,
@@ -2734,6 +3306,7 @@ async function checkProgramNewTargets(client, config, input) {
2734
3306
  in_scope_only: true,
2735
3307
  include_ineligible: input.include_ineligible,
2736
3308
  target_list_mode: input.target_list_mode,
3309
+ program_detail_source: "program_index",
2737
3310
  upstream_total_pages: readNumber(upstreamMeta?.total_pages),
2738
3311
  scan_may_be_incomplete: (readNumber(upstreamMeta?.total_pages) ?? 1) > 1
2739
3312
  })
@@ -3409,8 +3982,9 @@ function candidateLooksLikeVdp(candidate) {
3409
3982
  return candidateHasKnownNoBounty(candidate) || tagIncludesAnyNormalized(candidate.normalizedSearchSignals, VDP_SIGNALS);
3410
3983
  }
3411
3984
  function programResolutionMatch(candidate, query) {
3412
- const normalizedQuery = normalizeTag(query);
3413
- const queryTokens = tokenSet(normalizedQuery);
3985
+ const queryVariants = programResolutionQueryVariants(query);
3986
+ const primaryQuery = queryVariants[0] ?? normalizeTag(query);
3987
+ const queryTokens = meaningfulProgramQueryTokens(query);
3414
3988
  const fields = [
3415
3989
  { name: "id", value: stringField(candidate.program, "id") },
3416
3990
  { name: "handle", value: stringField(candidate.program, "handle") },
@@ -3428,16 +4002,23 @@ function programResolutionMatch(candidate, query) {
3428
4002
  }
3429
4003
  const normalizedValue = normalizeTag(field.value);
3430
4004
  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`);
4005
+ for (const queryVariant of queryVariants) {
4006
+ if (normalizedValue === queryVariant) {
4007
+ score = Math.max(score, field.name === "id" || field.name === "handle" ? 100 : 95);
4008
+ matchedFields.add(field.name);
4009
+ reasons.add(queryVariant === primaryQuery ? `${field.name} exact match` : `${field.name} exact meaningful-query match`);
4010
+ continue;
4011
+ }
4012
+ if (normalizedValue.includes(queryVariant)) {
4013
+ const containsScore = field.name === "name" || field.name === "handle"
4014
+ ? queryVariant.includes("-") || queryVariant.includes(" ")
4015
+ ? 90
4016
+ : 85
4017
+ : 70;
4018
+ matchedFields.add(field.name);
4019
+ score = Math.max(score, containsScore);
4020
+ reasons.add(queryVariant === primaryQuery ? `${field.name} contains query` : `${field.name} contains meaningful query`);
4021
+ }
3441
4022
  }
3442
4023
  const tokenOverlap = [...queryTokens].filter((token) => fieldTokens.has(token)).length;
3443
4024
  if (tokenOverlap > 0) {
@@ -3447,10 +4028,12 @@ function programResolutionMatch(candidate, query) {
3447
4028
  reasons.add(`${field.name} token match`);
3448
4029
  }
3449
4030
  }
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");
4031
+ for (const queryVariant of queryVariants) {
4032
+ if (candidate.normalizedSearchSignals.some((signal) => signal.includes(queryVariant))) {
4033
+ score = Math.max(score, queryVariant === primaryQuery ? 55 : 65);
4034
+ matchedFields.add("search_signals");
4035
+ reasons.add(queryVariant === primaryQuery ? "program search signals contain query" : "program search signals contain meaningful query");
4036
+ }
3454
4037
  }
3455
4038
  return {
3456
4039
  score,
@@ -3458,6 +4041,25 @@ function programResolutionMatch(candidate, query) {
3458
4041
  reasons: [...reasons]
3459
4042
  };
3460
4043
  }
4044
+ function programResolutionSearchQueries(query) {
4045
+ return programResolutionQueryVariants(query).slice(0, MAX_RESOLVE_SEARCH_QUERIES);
4046
+ }
4047
+ function programResolutionQueryVariants(query) {
4048
+ const normalized = normalizeTag(query);
4049
+ const meaningfulTokens = [...meaningfulProgramQueryTokens(query)];
4050
+ const meaningfulPhrase = meaningfulTokens.join(" ");
4051
+ const values = [
4052
+ normalized,
4053
+ meaningfulPhrase,
4054
+ meaningfulTokens.join("-"),
4055
+ ...meaningfulTokens
4056
+ ];
4057
+ return uniqueStrings(values.filter((value) => value.length >= 2));
4058
+ }
4059
+ function meaningfulProgramQueryTokens(query) {
4060
+ const tokens = [...tokenSet(normalizeTag(query))].filter((token) => !PROGRAM_RESOLUTION_STOP_WORDS.has(token));
4061
+ return new Set(tokens.length > 0 ? tokens : [...tokenSet(normalizeTag(query))]);
4062
+ }
3461
4063
  function normalizedCandidateValues(program, normalizedScopeTags) {
3462
4064
  return [
3463
4065
  stringField(program, "id"),
@@ -3504,9 +4106,18 @@ function programSearchText(programId) {
3504
4106
  const lastPathSegment = handle.split("/").filter(Boolean).at(-1) ?? handle;
3505
4107
  return lastPathSegment.length >= 2 ? lastPathSegment : handle;
3506
4108
  }
4109
+ function programHandleText(programId) {
4110
+ const colonIndex = programId.indexOf(":");
4111
+ return colonIndex >= 0 ? programId.slice(colonIndex + 1) : programId;
4112
+ }
4113
+ function programPlatformText(programId) {
4114
+ const colonIndex = programId.indexOf(":");
4115
+ return colonIndex >= 0 ? programId.slice(0, colonIndex) : "";
4116
+ }
3507
4117
  function normalizeFindProgramsInput(input) {
3508
4118
  const warnings = [];
3509
4119
  const normalized = { ...input };
4120
+ normalized.vdp_mode ??= "exclude";
3510
4121
  normalized.max_pages = clampLimit("max_pages", normalized.max_pages, MAX_FIND_PROGRAM_PAGES, warnings);
3511
4122
  normalized.max_results = clampLimit("max_results", normalized.max_results, MAX_FIND_PROGRAM_RESULTS, warnings);
3512
4123
  normalized.target_sample_programs = clampLimit("target_sample_programs", normalized.target_sample_programs, MAX_FIND_TARGET_SAMPLE_PROGRAMS, warnings);
@@ -3637,6 +4248,55 @@ async function collectPrograms(client, options) {
3637
4248
  totalPagesBySource
3638
4249
  };
3639
4250
  }
4251
+ async function collectProgramsForResolution(client, options) {
4252
+ const collections = [];
4253
+ const queries = options.searchQueries.length > 0 ? options.searchQueries : [undefined];
4254
+ for (const search of queries) {
4255
+ if (options.budget.remaining <= 0) {
4256
+ addUniqueWarning(options.warnings, "Upstream request budget was exhausted before all program name search variants could be checked.");
4257
+ break;
4258
+ }
4259
+ collections.push(await collectPrograms(client, {
4260
+ search,
4261
+ platforms: options.platforms,
4262
+ tags: options.tags,
4263
+ updated_since: undefined,
4264
+ opportunity_levels: options.opportunity_levels,
4265
+ max_pages: options.max_pages,
4266
+ budget: options.budget,
4267
+ warnings: options.warnings
4268
+ }));
4269
+ }
4270
+ if (collections.length === 0) {
4271
+ return {
4272
+ programs: [],
4273
+ sourceRequests: [],
4274
+ requestsScanned: 0,
4275
+ programsScanned: 0,
4276
+ totalPagesBySource: {}
4277
+ };
4278
+ }
4279
+ return mergeProgramCollections(collections);
4280
+ }
4281
+ function mergeProgramCollections(collections) {
4282
+ const programs = new Map();
4283
+ const totalPagesBySource = {};
4284
+ for (const collection of collections) {
4285
+ for (const program of collection.programs) {
4286
+ programs.set(programKey(program, programs.size), program);
4287
+ }
4288
+ for (const [sourceName, totalPages] of Object.entries(collection.totalPagesBySource)) {
4289
+ totalPagesBySource[sourceName] = totalPages;
4290
+ }
4291
+ }
4292
+ return {
4293
+ programs: [...programs.values()],
4294
+ sourceRequests: collections.flatMap((collection) => collection.sourceRequests),
4295
+ requestsScanned: collections.reduce((total, collection) => total + collection.requestsScanned, 0),
4296
+ programsScanned: programs.size,
4297
+ totalPagesBySource
4298
+ };
4299
+ }
3640
4300
  async function collectProgramSources(client, sources, options) {
3641
4301
  const pages = [];
3642
4302
  await mapWithConcurrency(sources.map((source, sourceIndex) => ({ source, sourceIndex })), PROGRAM_COLLECTION_CONCURRENCY, async ({ source, sourceIndex }) => {
@@ -3681,6 +4341,7 @@ async function fetchProgramPage(client, source, sourceIndex, page, options) {
3681
4341
  const runWithSlot = options.runWithProgramPageSlot ?? runImmediately;
3682
4342
  return runWithSlot(async () => {
3683
4343
  const query = toQuery({
4344
+ search: options.search,
3684
4345
  platforms: options.platforms,
3685
4346
  tags: options.tags,
3686
4347
  updated_since: options.updated_since,
@@ -3696,14 +4357,16 @@ async function fetchProgramPage(client, source, sourceIndex, page, options) {
3696
4357
  const data = readObject(api.data);
3697
4358
  const meta = readObject(data?.meta);
3698
4359
  const sourceName = source.kind === "opportunity" ? `opportunities:${source.level}` : "programs";
4360
+ const labeledSourceName = options.search ? `${sourceName}:search:${normalizeTag(options.search)}` : sourceName;
3699
4361
  return {
3700
- sourceName,
4362
+ sourceName: labeledSourceName,
3701
4363
  sourceIndex,
3702
4364
  page,
3703
4365
  programs: readArray(data?.programs),
3704
4366
  totalPages: readNumber(meta?.total_pages),
3705
4367
  sourceRequest: stripUndefined({
3706
- source: sourceName,
4368
+ source: labeledSourceName,
4369
+ search: options.search,
3707
4370
  request_id: api.requestId,
3708
4371
  upstream_request_id: api.upstreamRequestId,
3709
4372
  ...apiSourceMetadata(api),
@@ -3826,13 +4489,7 @@ function programCandidateMatches(candidate, filters) {
3826
4489
  if (filters.contest_like === true && !candidateLooksContestLike(candidate)) {
3827
4490
  return false;
3828
4491
  }
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)) {
4492
+ if (!candidateMatchesVdpMode(candidate, filters.vdp_mode ?? "exclude")) {
3836
4493
  return false;
3837
4494
  }
3838
4495
  if (filters.fresh_launch_days !== undefined) {
@@ -3893,6 +4550,18 @@ function compareProgramCandidates(left, right, sortBy) {
3893
4550
  right.firstSeenTime - left.firstSeenTime ||
3894
4551
  right.lastUpdatedTime - left.lastUpdatedTime);
3895
4552
  }
4553
+ function candidateMatchesVdpMode(candidate, vdpMode) {
4554
+ if (vdpMode === "exclude") {
4555
+ return !candidateLooksLikeVdp(candidate);
4556
+ }
4557
+ if (vdpMode === "only_likely") {
4558
+ return candidateLooksLikeVdp(candidate);
4559
+ }
4560
+ if (vdpMode === "only_known_no_bounty") {
4561
+ return candidateHasKnownNoBounty(candidate);
4562
+ }
4563
+ return true;
4564
+ }
3896
4565
  async function collectTargetSamples(client, candidates, input, budget) {
3897
4566
  const samples = {};
3898
4567
  const sampleCandidates = candidates.slice(0, input.target_sample_programs);
@@ -3984,6 +4653,9 @@ function addUniqueWarning(warnings, warning) {
3984
4653
  warnings.push(warning);
3985
4654
  }
3986
4655
  }
4656
+ function isProgramNotFoundError(error) {
4657
+ return error instanceof BBRadarApiError && error.status === 404;
4658
+ }
3987
4659
  function runImmediately(callback) {
3988
4660
  return callback();
3989
4661
  }
@@ -4092,7 +4764,7 @@ function registerPrompts(server) {
4092
4764
 
4093
4765
  Find the best BBRadar program candidates.
4094
4766
  - 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.
4767
+ - Use find_language_programs for language asks, find_target_type_programs for API/mobile/source-code/domain asks, find_reward_programs or find_paid_programs for reward asks, find_vdp_programs only for explicit VDP/no-bounty asks, find_web3_contests for Web3 contest/audit asks, find_web3_programs for general Web3 requests, and find_wildcard_programs for wildcard-specific requests.
4096
4768
  - Apply platform filters from this JSON string if provided: ${promptJson(args.platforms ?? "")}.
4097
4769
  - Apply tag filters from this JSON string if provided: ${promptJson(args.tags ?? "")}.
4098
4770
  - Keep the shortlist to this JSON value: ${promptJson(args.max_programs ?? "10")}.
@@ -4102,16 +4774,16 @@ Find the best BBRadar program candidates.
4102
4774
  - Stay passive-only; do not recommend scanning, probing, exploit attempts, or direct contact with targets.`));
4103
4775
  server.registerPrompt("summarize_program_scope", {
4104
4776
  title: "Summarize Program Scope",
4105
- description: "Summarize in-scope and out-of-scope BBRadar target data for one program.",
4777
+ description: "Summarize BBRadar target data for one program.",
4106
4778
  argsSchema: {
4107
4779
  program_id: programIdSchema
4108
4780
  }
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}.
4781
+ }, (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=false, include_ineligible=false, and limit=${MAX_TARGETS_PER_PROGRAM}. Only request out-of-scope or ineligible targets if the user explicitly asks for them.
4110
4782
 
4111
4783
  Summarize the program scope:
4112
4784
  - Program name, platform, reward range, public report count, first seen date, last updated date, and BBRadar URL.
4113
4785
  - In-scope bounty-eligible target categories.
4114
- - Out-of-scope or ineligible target categories.
4786
+ - Out-of-scope or ineligible target categories only if explicitly requested.
4115
4787
  - Wildcards, high-severity target labels, and notable language or scope tags.
4116
4788
  - Recent target update signals if present.
4117
4789
  - Do not contact, scan, probe, or validate any listed target.`));