@apitap/core 1.5.3 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +28 -8
  2. package/dist/auth/handoff.js +1 -1
  3. package/dist/auth/handoff.js.map +1 -1
  4. package/dist/capture/cdp-attach.d.ts +60 -0
  5. package/dist/capture/cdp-attach.js +422 -0
  6. package/dist/capture/cdp-attach.js.map +1 -0
  7. package/dist/capture/filter.js +6 -0
  8. package/dist/capture/filter.js.map +1 -1
  9. package/dist/capture/parameterize.d.ts +7 -6
  10. package/dist/capture/parameterize.js +204 -12
  11. package/dist/capture/parameterize.js.map +1 -1
  12. package/dist/capture/session.js +20 -10
  13. package/dist/capture/session.js.map +1 -1
  14. package/dist/cli.js +387 -20
  15. package/dist/cli.js.map +1 -1
  16. package/dist/discovery/openapi.js +23 -50
  17. package/dist/discovery/openapi.js.map +1 -1
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/mcp.js +12 -0
  22. package/dist/mcp.js.map +1 -1
  23. package/dist/native-host.js +5 -0
  24. package/dist/native-host.js.map +1 -1
  25. package/dist/plugin.js +10 -3
  26. package/dist/plugin.js.map +1 -1
  27. package/dist/replay/engine.d.ts +13 -0
  28. package/dist/replay/engine.js +20 -0
  29. package/dist/replay/engine.js.map +1 -1
  30. package/dist/skill/apis-guru.d.ts +35 -0
  31. package/dist/skill/apis-guru.js +128 -0
  32. package/dist/skill/apis-guru.js.map +1 -0
  33. package/dist/skill/generator.d.ts +7 -1
  34. package/dist/skill/generator.js +35 -3
  35. package/dist/skill/generator.js.map +1 -1
  36. package/dist/skill/merge.d.ts +29 -0
  37. package/dist/skill/merge.js +252 -0
  38. package/dist/skill/merge.js.map +1 -0
  39. package/dist/skill/openapi-converter.d.ts +31 -0
  40. package/dist/skill/openapi-converter.js +383 -0
  41. package/dist/skill/openapi-converter.js.map +1 -0
  42. package/dist/types.d.ts +41 -0
  43. package/package.json +1 -1
  44. package/src/auth/handoff.ts +1 -1
  45. package/src/capture/cdp-attach.ts +501 -0
  46. package/src/capture/filter.ts +5 -0
  47. package/src/capture/parameterize.ts +207 -11
  48. package/src/capture/session.ts +20 -10
  49. package/src/cli.ts +420 -18
  50. package/src/discovery/openapi.ts +25 -56
  51. package/src/index.ts +1 -0
  52. package/src/mcp.ts +12 -0
  53. package/src/native-host.ts +7 -0
  54. package/src/plugin.ts +10 -3
  55. package/src/replay/engine.ts +19 -0
  56. package/src/skill/apis-guru.ts +163 -0
  57. package/src/skill/generator.ts +38 -3
  58. package/src/skill/merge.ts +281 -0
  59. package/src/skill/openapi-converter.ts +426 -0
  60. package/src/types.ts +42 -1
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // src/cli.ts
3
3
  import { capture } from './capture/monitor.js';
4
4
  import { writeSkillFile, readSkillFile, listSkillFiles } from './skill/store.js';
5
- import { replayEndpoint } from './replay/engine.js';
5
+ import { replayEndpoint, getConfidenceHint } from './replay/engine.js';
6
6
  import { AuthManager, getMachineId } from './auth/manager.js';
7
7
  import { deriveSigningKey } from './auth/crypto.js';
8
8
  import { signSkillFile } from './skill/signing.js';
@@ -25,6 +25,10 @@ import { readFileSync } from 'node:fs';
25
25
  import { stat, unlink } from 'node:fs/promises';
26
26
  import { fileURLToPath } from 'node:url';
27
27
  import { createMcpServer } from './mcp.js';
28
+ import { attach, parseDomainPatterns } from './capture/cdp-attach.js';
29
+ import { isOpenAPISpec, convertOpenAPISpec } from './skill/openapi-converter.js';
30
+ import { mergeSkillFile } from './skill/merge.js';
31
+ import { fetchApisGuruList, filterEntries, fetchSpec } from './skill/apis-guru.js';
28
32
  const __dirname = fileURLToPath(new URL('.', import.meta.url));
29
33
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
30
34
  const VERSION = pkg.version;
@@ -56,6 +60,8 @@ function printUsage() {
56
60
 
57
61
  Usage:
58
62
  apitap capture <url> Capture API traffic from a website
63
+ apitap attach [--port 9222] [--domain *.github.com]
64
+ Attach to running Chrome and capture API traffic
59
65
  apitap discover <url> Detect APIs without a browser (fast recon)
60
66
  apitap inspect <url> Discover APIs without saving (X-ray vision)
61
67
  apitap search <query> Search skill files for a domain or endpoint
@@ -63,7 +69,9 @@ function printUsage() {
63
69
  apitap show <domain> Show endpoints for a domain
64
70
  apitap replay <domain> <endpoint-id> [key=value...]
65
71
  Replay an API endpoint
66
- apitap import <file> Import a skill file with safety validation
72
+ apitap import <file|url> Import a skill file or OpenAPI spec
73
+ apitap import --from apis-guru
74
+ Bulk-import from APIs.guru directory
67
75
  apitap refresh <domain> Refresh auth tokens via browser
68
76
  apitap auth [domain] View or manage stored auth
69
77
  apitap mcp Run the full ApiTap MCP server over stdio
@@ -115,6 +123,11 @@ function printUsage() {
115
123
 
116
124
  Import options:
117
125
  --yes Skip confirmation prompt
126
+ --dry-run Show what would change without writing
127
+ --json Output machine-readable JSON
128
+ --from apis-guru Bulk-import from APIs.guru directory
129
+ --search <term> Filter APIs.guru entries by name/title
130
+ --limit <n> Max APIs to import (default: 100)
118
131
 
119
132
  Serve options:
120
133
  --json Output tool list as JSON on stderr
@@ -405,40 +418,145 @@ async function handleReplay(positional, flags) {
405
418
  }, null, 2));
406
419
  }
407
420
  else {
421
+ const hint = endpoint ? getConfidenceHint(endpoint.confidence) : null;
422
+ if (hint) {
423
+ console.error(` Note: ${hint}`);
424
+ }
408
425
  console.log(`\n Status: ${result.status}\n`);
409
426
  console.log(JSON.stringify(result.data, null, 2));
410
427
  console.log();
411
428
  }
412
429
  }
413
430
  async function handleImport(positional, flags) {
414
- const filePath = positional[0];
415
- if (!filePath) {
416
- console.error('Error: File path required. Usage: apitap import <file>');
431
+ // --from apis-guru: bulk import mode
432
+ if (flags['from'] === 'apis-guru') {
433
+ await handleApisGuruImport(flags);
434
+ return;
435
+ }
436
+ const source = positional[0];
437
+ if (!source) {
438
+ console.error('Error: File path or URL required. Usage: apitap import <file|url>');
417
439
  process.exit(1);
418
440
  }
419
441
  const json = flags.json === true;
442
+ // Reject YAML files with helpful message
443
+ if (/\.ya?ml$/i.test(source)) {
444
+ const msg = 'YAML specs not yet supported. Convert to JSON first: npx swagger-cli bundle spec.yaml -o spec.json';
445
+ if (json) {
446
+ console.log(JSON.stringify({ success: false, reason: msg }));
447
+ }
448
+ else {
449
+ console.error(`Error: ${msg}`);
450
+ }
451
+ process.exit(1);
452
+ }
453
+ // Load content — from URL or file
454
+ let rawText;
455
+ let sourceUrl = source;
456
+ const isUrl = source.startsWith('http://') || source.startsWith('https://');
457
+ if (isUrl) {
458
+ try {
459
+ const ssrfCheck = await resolveAndValidateUrl(source);
460
+ if (!ssrfCheck.safe) {
461
+ throw new Error(`SSRF check failed: ${ssrfCheck.reason}`);
462
+ }
463
+ const response = await fetch(source, { signal: AbortSignal.timeout(30_000) });
464
+ if (!response.ok) {
465
+ throw new Error(`HTTP ${response.status} ${response.statusText}`);
466
+ }
467
+ rawText = await response.text();
468
+ }
469
+ catch (err) {
470
+ const msg = `Failed to fetch ${source}: ${err.message}`;
471
+ if (json) {
472
+ console.log(JSON.stringify({ success: false, reason: msg }));
473
+ }
474
+ else {
475
+ console.error(`Error: ${msg}`);
476
+ }
477
+ process.exit(1);
478
+ }
479
+ }
480
+ else {
481
+ try {
482
+ const { readFile } = await import('node:fs/promises');
483
+ rawText = await readFile(source, 'utf-8');
484
+ sourceUrl = `file://${resolve(source)}`;
485
+ }
486
+ catch (err) {
487
+ const msg = `Failed to read ${source}: ${err.message}`;
488
+ if (json) {
489
+ console.log(JSON.stringify({ success: false, reason: msg }));
490
+ }
491
+ else {
492
+ console.error(`Error: ${msg}`);
493
+ }
494
+ process.exit(1);
495
+ }
496
+ }
497
+ // Parse JSON
498
+ let parsed;
499
+ try {
500
+ parsed = JSON.parse(rawText);
501
+ }
502
+ catch {
503
+ const msg = `Invalid JSON in ${source}`;
504
+ if (json) {
505
+ console.log(JSON.stringify({ success: false, reason: msg }));
506
+ }
507
+ else {
508
+ console.error(`Error: ${msg}`);
509
+ }
510
+ process.exit(1);
511
+ }
512
+ // Route: OpenAPI spec vs SkillFile
513
+ if (isOpenAPISpec(parsed)) {
514
+ await handleOpenAPIImport(parsed, sourceUrl, flags);
515
+ return;
516
+ }
517
+ // --- Existing SkillFile import flow (unchanged) ---
518
+ if (isUrl) {
519
+ // SkillFile imports from URLs: write to temp file first
520
+ const { writeFile: writeTmp } = await import('node:fs/promises');
521
+ const { tmpdir } = await import('node:os');
522
+ const tmpPath = join(tmpdir(), `apitap-import-${Date.now()}.json`);
523
+ await writeTmp(tmpPath, rawText);
524
+ try {
525
+ await handleSkillFileImport(tmpPath, json);
526
+ }
527
+ finally {
528
+ const { unlink: unlinkTmp } = await import('node:fs/promises');
529
+ await unlinkTmp(tmpPath).catch(() => { });
530
+ }
531
+ }
532
+ else {
533
+ await handleSkillFileImport(source, json);
534
+ }
535
+ }
536
+ async function handleSkillFileImport(filePath, json) {
420
537
  // Get local key for signature verification
421
538
  const machineId = await getMachineId();
422
539
  const key = deriveSigningKey(machineId);
423
540
  // DNS-resolving SSRF check before importing (prevents DNS rebinding attacks)
541
+ let raw;
424
542
  try {
425
- const raw = JSON.parse(await import('node:fs/promises').then(fs => fs.readFile(filePath, 'utf-8')));
426
- if (raw.baseUrl) {
427
- const dnsCheck = await resolveAndValidateUrl(raw.baseUrl);
428
- if (!dnsCheck.safe) {
429
- const msg = `DNS rebinding risk: ${dnsCheck.reason}`;
430
- if (json) {
431
- console.log(JSON.stringify({ success: false, reason: msg }));
432
- }
433
- else {
434
- console.error(`Error: ${msg}`);
435
- }
436
- process.exit(1);
437
- }
438
- }
543
+ raw = JSON.parse(await import('node:fs/promises').then(fs => fs.readFile(filePath, 'utf-8')));
439
544
  }
440
545
  catch {
441
- // Parse errors will be caught by importSkillFile
546
+ // Parse errors will be caught by importSkillFile below
547
+ }
548
+ if (raw?.baseUrl) {
549
+ const dnsCheck = await resolveAndValidateUrl(raw.baseUrl);
550
+ if (!dnsCheck.safe) {
551
+ const msg = `DNS rebinding risk: ${dnsCheck.reason}`;
552
+ if (json) {
553
+ console.log(JSON.stringify({ success: false, reason: msg }));
554
+ }
555
+ else {
556
+ console.error(`Error: ${msg}`);
557
+ }
558
+ process.exit(1);
559
+ }
442
560
  }
443
561
  const result = await importSkillFile(filePath, undefined, key);
444
562
  if (!result.success) {
@@ -457,6 +575,243 @@ async function handleImport(positional, flags) {
457
575
  console.log(`\n ✓ Imported skill file: ${result.skillFile}\n`);
458
576
  }
459
577
  }
578
+ async function handleOpenAPIImport(spec, specUrl, flags) {
579
+ const json = flags.json === true;
580
+ const dryRun = flags['dry-run'] === true;
581
+ const skillsDir = SKILLS_DIR || join(APITAP_DIR, 'skills');
582
+ // Convert OpenAPI spec to endpoints
583
+ let importResult;
584
+ try {
585
+ importResult = convertOpenAPISpec(spec, specUrl);
586
+ }
587
+ catch (err) {
588
+ const msg = `Failed to convert OpenAPI spec: ${err.message}`;
589
+ if (json) {
590
+ console.log(JSON.stringify({ success: false, reason: msg }));
591
+ }
592
+ else {
593
+ console.error(`Error: ${msg}`);
594
+ }
595
+ process.exit(1);
596
+ }
597
+ const { domain, endpoints, meta } = importResult;
598
+ const specVersion = spec.openapi || spec.swagger || 'unknown';
599
+ if (!json) {
600
+ console.log(`\n Importing ${domain} from OpenAPI ${specVersion} spec...\n`);
601
+ }
602
+ // SSRF validate the domain
603
+ const dnsCheck = await resolveAndValidateUrl(`https://${domain}`);
604
+ if (!dnsCheck.safe) {
605
+ const msg = `DNS rebinding risk for ${domain}: ${dnsCheck.reason}`;
606
+ if (json) {
607
+ console.log(JSON.stringify({ success: false, reason: msg }));
608
+ }
609
+ else {
610
+ console.error(`Error: ${msg}`);
611
+ }
612
+ process.exit(1);
613
+ }
614
+ // Read existing skill file (if any)
615
+ let existing = null;
616
+ try {
617
+ existing = await readSkillFile(domain, skillsDir, {
618
+ verifySignature: true,
619
+ trustUnsigned: true,
620
+ });
621
+ }
622
+ catch (err) {
623
+ if (err?.code !== 'ENOENT') {
624
+ if (!json)
625
+ console.error(` Warning: could not read existing skill file for ${domain}: ${err.message}`);
626
+ }
627
+ }
628
+ // Merge
629
+ const { skillFile, diff } = mergeSkillFile(existing, endpoints, meta);
630
+ // Ensure domain and baseUrl reflect the API, not the spec source URL
631
+ skillFile.domain = domain;
632
+ skillFile.baseUrl = `https://${domain}`;
633
+ if (dryRun) {
634
+ if (json) {
635
+ console.log(JSON.stringify({
636
+ success: true,
637
+ dryRun: true,
638
+ domain,
639
+ diff,
640
+ totalEndpoints: skillFile.endpoints.length,
641
+ }));
642
+ }
643
+ else {
644
+ printOpenAPIDiff(diff, skillFile, skillsDir);
645
+ console.log(' (dry run — no changes written)\n');
646
+ }
647
+ return;
648
+ }
649
+ // Sign and write
650
+ const machineId = await getMachineId();
651
+ const key = deriveSigningKey(machineId);
652
+ const signed = signSkillFile(skillFile, key);
653
+ const filePath = await writeSkillFile(signed, skillsDir);
654
+ if (json) {
655
+ console.log(JSON.stringify({
656
+ success: true,
657
+ domain,
658
+ skillFile: filePath,
659
+ diff,
660
+ totalEndpoints: signed.endpoints.length,
661
+ }));
662
+ }
663
+ else {
664
+ printOpenAPIDiff(diff, signed, filePath);
665
+ }
666
+ }
667
+ function printOpenAPIDiff(diff, skillFile, pathOrDir) {
668
+ console.log(` ✓ ${diff.preserved} existing captured endpoints preserved`);
669
+ console.log(` + ${diff.added} new endpoints added from OpenAPI spec`);
670
+ console.log(` ~ ${diff.enriched} endpoints enriched with spec metadata`);
671
+ console.log(` · ${diff.skipped} skipped (already imported)`);
672
+ console.log();
673
+ console.log(` Skill file: ${pathOrDir} (${skillFile.endpoints.length} endpoints)\n`);
674
+ }
675
+ async function handleApisGuruImport(flags) {
676
+ const json = flags.json === true;
677
+ const dryRun = flags['dry-run'] === true;
678
+ const limit = typeof flags.limit === 'string' ? parseInt(flags.limit, 10) : 100;
679
+ const search = typeof flags.search === 'string' ? flags.search : undefined;
680
+ const skillsDir = SKILLS_DIR || join(APITAP_DIR, 'skills');
681
+ if (!json) {
682
+ console.log(`\n Importing from APIs.guru (limit: ${limit})...\n`);
683
+ }
684
+ // Fetch and filter
685
+ let entries;
686
+ try {
687
+ const allEntries = await fetchApisGuruList();
688
+ entries = filterEntries(allEntries, { search, limit, preferOpenapi3: true });
689
+ }
690
+ catch (err) {
691
+ const msg = `Failed to fetch APIs.guru list: ${err.message}`;
692
+ if (json) {
693
+ console.log(JSON.stringify({ success: false, reason: msg }));
694
+ }
695
+ else {
696
+ console.error(`Error: ${msg}`);
697
+ }
698
+ process.exit(1);
699
+ }
700
+ if (entries.length === 0) {
701
+ if (json) {
702
+ console.log(JSON.stringify({ success: true, imported: 0, failed: 0, skipped: 0, totalEndpoints: 0 }));
703
+ }
704
+ else {
705
+ console.log(' No matching APIs found.\n');
706
+ }
707
+ return;
708
+ }
709
+ const total = entries.length;
710
+ let imported = 0;
711
+ let failed = 0;
712
+ let skippedApis = 0;
713
+ let totalEndpointsAdded = 0;
714
+ const machineId = await getMachineId();
715
+ const key = deriveSigningKey(machineId);
716
+ const results = [];
717
+ for (let i = 0; i < entries.length; i++) {
718
+ const entry = entries[i];
719
+ const idx = String(i + 1).padStart(String(total).length, ' ');
720
+ try {
721
+ // Fetch spec
722
+ const spec = await fetchSpec(entry.specUrl);
723
+ // Convert
724
+ const importResult = convertOpenAPISpec(spec, entry.specUrl);
725
+ const { domain, endpoints, meta } = importResult;
726
+ if (endpoints.length === 0) {
727
+ if (!json) {
728
+ console.log(` [${idx}/${total}] SKIP ${domain.padEnd(24)} 0 endpoints (${entry.title})`);
729
+ }
730
+ results.push({ index: i + 1, status: 'skip', domain, title: entry.title, endpointsAdded: 0 });
731
+ skippedApis++;
732
+ continue;
733
+ }
734
+ // SSRF validate
735
+ const dnsCheck = await resolveAndValidateUrl(`https://${domain}`);
736
+ if (!dnsCheck.safe) {
737
+ if (!json) {
738
+ console.log(` [${idx}/${total}] SKIP ${domain.padEnd(24)} SSRF risk (${entry.title})`);
739
+ }
740
+ results.push({ index: i + 1, status: 'skip', domain, title: entry.title, endpointsAdded: 0 });
741
+ skippedApis++;
742
+ continue;
743
+ }
744
+ // Read existing
745
+ let existing = null;
746
+ try {
747
+ existing = await readSkillFile(domain, skillsDir, {
748
+ verifySignature: true,
749
+ trustUnsigned: true,
750
+ });
751
+ }
752
+ catch (err) {
753
+ if (err?.code !== 'ENOENT') {
754
+ if (!json)
755
+ console.error(` Warning: could not read existing skill file for ${domain}: ${err.message}`);
756
+ }
757
+ }
758
+ // Merge
759
+ const { skillFile, diff } = mergeSkillFile(existing, endpoints, meta);
760
+ // Ensure domain and baseUrl reflect the API, not the spec source URL
761
+ skillFile.domain = domain;
762
+ skillFile.baseUrl = `https://${domain}`;
763
+ if (!dryRun) {
764
+ // Sign and write
765
+ const signed = signSkillFile(skillFile, key);
766
+ await writeSkillFile(signed, skillsDir);
767
+ }
768
+ if (!json) {
769
+ console.log(` [${idx}/${total}] OK ${domain.padEnd(24)} +${diff.added} endpoints (${entry.title})`);
770
+ }
771
+ results.push({ index: i + 1, status: 'ok', domain, title: entry.title, endpointsAdded: diff.added });
772
+ imported++;
773
+ totalEndpointsAdded += diff.added;
774
+ }
775
+ catch (err) {
776
+ if (!json) {
777
+ console.log(` [${idx}/${total}] FAIL ${entry.providerName.padEnd(24)} ${err.message}`);
778
+ }
779
+ results.push({
780
+ index: i + 1,
781
+ status: 'fail',
782
+ domain: entry.providerName,
783
+ title: entry.title,
784
+ endpointsAdded: 0,
785
+ error: err.message,
786
+ });
787
+ failed++;
788
+ }
789
+ // Small delay between requests to be polite to APIs.guru
790
+ if (i < entries.length - 1) {
791
+ await new Promise(r => setTimeout(r, 100));
792
+ }
793
+ }
794
+ if (json) {
795
+ console.log(JSON.stringify({
796
+ success: true,
797
+ dryRun,
798
+ imported,
799
+ failed,
800
+ skipped: skippedApis,
801
+ totalEndpoints: totalEndpointsAdded,
802
+ results,
803
+ }));
804
+ }
805
+ else {
806
+ console.log();
807
+ console.log(` Done: ${imported} imported, ${failed} failed, ${skippedApis} skipped`);
808
+ console.log(` ${totalEndpointsAdded.toLocaleString()} endpoints added across ${imported} APIs`);
809
+ if (dryRun) {
810
+ console.log(' (dry run — no changes written)');
811
+ }
812
+ console.log();
813
+ }
814
+ }
460
815
  async function handleRefresh(positional, flags) {
461
816
  const domain = positional[0];
462
817
  if (!domain) {
@@ -1115,6 +1470,15 @@ async function handleExtension(positional, flags) {
1115
1470
  console.error('Usage: apitap extension install --extension-id <id>');
1116
1471
  process.exit(1);
1117
1472
  }
1473
+ async function handleAttach(_positional, flags) {
1474
+ const port = typeof flags.port === 'string' ? parseInt(flags.port, 10) : 9222;
1475
+ const domainPatterns = parseDomainPatterns(typeof flags.domain === 'string' ? flags.domain : undefined);
1476
+ const json = flags.json === true;
1477
+ const result = await attach({ port, domainPatterns, json });
1478
+ if (json) {
1479
+ console.log(JSON.stringify(result, null, 2));
1480
+ }
1481
+ }
1118
1482
  async function main() {
1119
1483
  const { command, positional, flags } = parseArgs(process.argv.slice(2));
1120
1484
  // Handle --version flag before command dispatch
@@ -1180,6 +1544,9 @@ async function main() {
1180
1544
  case 'extension':
1181
1545
  await handleExtension(positional, flags);
1182
1546
  break;
1547
+ case 'attach':
1548
+ await handleAttach(positional, flags);
1549
+ break;
1183
1550
  default:
1184
1551
  printUsage();
1185
1552
  }