@apitap/core 1.0.10 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -384,6 +384,10 @@ npm run build # Compile to dist/
384
384
  npx tsx src/cli.ts capture <url> # Run from source
385
385
  ```
386
386
 
387
+ ## Contact
388
+
389
+ Questions, feedback, or issues? → **[hello@apitap.io](mailto:hello@apitap.io)**
390
+
387
391
  ## License
388
392
 
389
393
  [Business Source License 1.1](./LICENSE) — **free for all non-competing use** (personal, internal, educational, research, open source). Cannot be rebranded and sold as a competing service. Converts to Apache 2.0 on February 7, 2029.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apitap/core",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "Intercept web API traffic during browsing. Generate portable skill files so AI agents can call APIs directly instead of scraping.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -27,6 +27,11 @@ const TRACKING_COOKIE_PATTERNS = [
27
27
  /^_ga/i, /^_gid/i, /^_fb/i, /^_gcl/i, /^__utm/i,
28
28
  ];
29
29
 
30
+ // Common anonymous/bootstrap cookies that should not end auth flow
31
+ const ANONYMOUS_COOKIE_PATTERNS = [
32
+ /^anon/i, /^guest/i, /^visitor/i, /^ab[_-]?test/i, /^optanon/i, /^consent/i,
33
+ ];
34
+
30
35
  /**
31
36
  * Detect whether a response indicates successful login.
32
37
  * Checks for session-like Set-Cookie headers on 2xx responses.
@@ -57,6 +62,33 @@ export function detectLoginSuccess(
57
62
  return SESSION_COOKIE_PATTERNS.some(p => p.test(cookieName));
58
63
  }
59
64
 
65
+ function isSessionLikeCookieName(name: string): boolean {
66
+ return SESSION_COOKIE_PATTERNS.some(p => p.test(name));
67
+ }
68
+
69
+ function isTrackingCookieName(name: string): boolean {
70
+ return TRACKING_COOKIE_PATTERNS.some(p => p.test(name));
71
+ }
72
+
73
+ function isAnonymousCookieName(name: string): boolean {
74
+ return ANONYMOUS_COOKIE_PATTERNS.some(p => p.test(name));
75
+ }
76
+
77
+ export function hasHighConfidenceAuthTransition(
78
+ baselineCookieValues: Map<string, string>,
79
+ currentCookies: Array<{ name: string; value: string }>,
80
+ ): boolean {
81
+ return currentCookies.some((cookie) => {
82
+ const baseline = baselineCookieValues.get(cookie.name);
83
+ const changedOrNew = baseline === undefined || baseline !== cookie.value;
84
+ if (!changedOrNew) return false;
85
+ if (!isSessionLikeCookieName(cookie.name)) return false;
86
+ if (isTrackingCookieName(cookie.name)) return false;
87
+ if (isAnonymousCookieName(cookie.name)) return false;
88
+ return true;
89
+ });
90
+ }
91
+
60
92
  // Mutex to prevent concurrent handoffs for the same domain
61
93
  const handoffLocks = new Map<string, Promise<HandoffResult>>();
62
94
 
@@ -144,9 +176,11 @@ async function doHandoff(
144
176
  // Navigate to login page
145
177
  await page.goto(loginUrl, { waitUntil: 'domcontentloaded', timeout: 30_000 });
146
178
 
147
- // Baseline: snapshot cookie names present BEFORE login so we can detect new ones
179
+ // Baseline: snapshot cookie values present BEFORE login so we can detect real transitions
148
180
  const baselineCookies = await context.cookies();
149
- const baselineCookieNames = new Set(baselineCookies.map(c => c.name));
181
+ const baselineCookieValues = new Map(
182
+ baselineCookies.map(c => [c.name, c.value])
183
+ );
150
184
 
151
185
  // Poll for login success: check for NEW session-like cookies
152
186
  const startTime = Date.now();
@@ -155,15 +189,10 @@ async function doHandoff(
155
189
  while (Date.now() - startTime < timeout) {
156
190
  await page.waitForTimeout(2000);
157
191
 
158
- // Only trigger on NEW session-like cookies not present at page load
159
192
  const cookies = await context.cookies();
160
- const hasNewSessionCookie = cookies.some(c =>
161
- !baselineCookieNames.has(c.name) &&
162
- SESSION_COOKIE_PATTERNS.some(p => p.test(c.name)) &&
163
- !TRACKING_COOKIE_PATTERNS.some(p => p.test(c.name))
164
- );
193
+ const hasAuthTransition = hasHighConfidenceAuthTransition(baselineCookieValues, cookies);
165
194
 
166
- if (hasNewSessionCookie || authDetected) {
195
+ if (hasAuthTransition || authDetected) {
167
196
  // Grace period: 4 additional polls at 2s each (~8s total)
168
197
  // Allows time for MFA, CAPTCHAs, and post-login redirects
169
198
  for (let grace = 0; grace < 4; grace++) {
@@ -9,6 +9,8 @@ const CHROME_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit
9
9
  export function getLaunchArgs(): string[] {
10
10
  return [
11
11
  '--disable-blink-features=AutomationControlled',
12
+ '--disable-dev-shm-usage',
13
+ '--disable-features=IsolateOrigins,site-per-process',
12
14
  ];
13
15
  }
14
16
 
@@ -19,6 +21,10 @@ export function getChromeUserAgent(): string {
19
21
  return CHROME_USER_AGENT;
20
22
  }
21
23
 
24
+ export function shouldPreferSystemChrome(): boolean {
25
+ return process.env.APITAP_PREFER_SYSTEM_CHROME === '1';
26
+ }
27
+
22
28
  /**
23
29
  * Launch a Chromium browser with anti-detection measures.
24
30
  *
@@ -31,20 +37,51 @@ export function getChromeUserAgent(): string {
31
37
  export async function launchBrowser(options: { headless: boolean }): Promise<{ browser: Browser; context: BrowserContext }> {
32
38
  const { chromium } = await import('playwright');
33
39
 
34
- const browser = await chromium.launch({
40
+ const launchOptions = {
35
41
  headless: options.headless,
36
42
  args: getLaunchArgs(),
37
- });
43
+ };
44
+
45
+ let browser: Browser;
46
+ if (shouldPreferSystemChrome()) {
47
+ try {
48
+ browser = await chromium.launch({
49
+ ...launchOptions,
50
+ channel: 'chrome',
51
+ });
52
+ } catch {
53
+ browser = await chromium.launch(launchOptions);
54
+ }
55
+ } else {
56
+ browser = await chromium.launch(launchOptions);
57
+ }
38
58
 
39
59
  const context = await browser.newContext({
40
60
  userAgent: CHROME_USER_AGENT,
41
61
  viewport: { width: 1920, height: 1080 },
62
+ locale: 'en-US',
63
+ extraHTTPHeaders: { 'accept-language': 'en-US,en;q=0.9' },
42
64
  });
43
65
 
44
66
  await context.addInitScript(() => {
45
67
  Object.defineProperty(navigator, 'webdriver', {
46
68
  get: () => false,
69
+ configurable: true,
70
+ });
71
+ Object.defineProperty(navigator, 'languages', {
72
+ get: () => ['en-US', 'en'],
73
+ configurable: true,
74
+ });
75
+ Object.defineProperty(navigator, 'plugins', {
76
+ get: () => [1, 2, 3],
77
+ configurable: true,
47
78
  });
79
+ if (!(window as any).chrome) {
80
+ Object.defineProperty(window, 'chrome', {
81
+ value: { runtime: {} },
82
+ configurable: true,
83
+ });
84
+ }
48
85
  });
49
86
 
50
87
  return { browser, context };
@@ -6,6 +6,9 @@ import { SkillGenerator, type GeneratorOptions } from '../skill/generator.js';
6
6
  import { IdleTracker } from './idle.js';
7
7
  import { detectCaptcha } from '../auth/refresh.js';
8
8
  import { launchBrowser } from './browser.js';
9
+ import { AuthManager, getMachineId } from '../auth/manager.js';
10
+ import { homedir } from 'node:os';
11
+ import { join } from 'node:path';
9
12
  import type { CapturedExchange } from '../types.js';
10
13
 
11
14
  export interface CaptureOptions {
@@ -18,6 +21,7 @@ export interface CaptureOptions {
18
21
  allDomains?: boolean;
19
22
  enablePreview?: boolean;
20
23
  scrub?: boolean;
24
+ authDir?: string;
21
25
  onEndpoint?: (endpoint: { id: string; method: string; path: string }) => void;
22
26
  onFiltered?: () => void;
23
27
  onIdle?: () => void;
@@ -31,6 +35,7 @@ export interface CaptureResult {
31
35
  }
32
36
 
33
37
  const DEFAULT_CDP_PORTS = [18792, 18800, 9222];
38
+ const APITAP_DIR = process.env.APITAP_DIR || join(homedir(), '.apitap');
34
39
 
35
40
  async function connectToBrowser(options: CaptureOptions): Promise<{ browser: Browser; launched: boolean; launchContext?: import('playwright').BrowserContext }> {
36
41
  if (!options.launch) {
@@ -171,6 +176,20 @@ export async function capture(options: CaptureOptions): Promise<CaptureResult> {
171
176
  }
172
177
  });
173
178
 
179
+ // Inject cached session cookies if available
180
+ try {
181
+ const authDir = options.authDir ?? APITAP_DIR;
182
+ const machineId = await getMachineId();
183
+ const authManager = new AuthManager(authDir, machineId);
184
+ const domain = new URL(targetUrl.startsWith('http') ? targetUrl : `https://${targetUrl}`).hostname;
185
+ const cachedSession = await authManager.retrieveSessionWithFallback(domain);
186
+ if (cachedSession?.cookies?.length) {
187
+ await page.context().addCookies(cachedSession.cookies);
188
+ }
189
+ } catch {
190
+ // Auth retrieval failed — proceed without cached session
191
+ }
192
+
174
193
  await page.goto(options.url, { waitUntil: 'domcontentloaded' });
175
194
 
176
195
  // Start idle check interval (every 5s) for interactive capture
@@ -15,7 +15,7 @@ import { homedir } from 'node:os';
15
15
  import { join } from 'node:path';
16
16
  import type { CapturedExchange, PageSnapshot, PageElement, InteractionResult, FinishResult } from '../types.js';
17
17
 
18
- const APITAP_DIR = join(homedir(), '.apitap');
18
+ const APITAP_DIR = process.env.APITAP_DIR || join(homedir(), '.apitap');
19
19
  const MAX_ELEMENTS = 100;
20
20
  const MAX_TEXT_LENGTH = 200;
21
21
  const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
package/src/cli.ts CHANGED
@@ -23,6 +23,7 @@ import { homedir } from 'node:os';
23
23
  import { join } from 'node:path';
24
24
  import { readFileSync } from 'node:fs';
25
25
  import { fileURLToPath } from 'node:url';
26
+ import { createMcpServer } from './mcp.js';
26
27
 
27
28
  const __dirname = fileURLToPath(new URL('.', import.meta.url));
28
29
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
@@ -73,6 +74,7 @@ function printUsage(): void {
73
74
  apitap import <file> Import a skill file with safety validation
74
75
  apitap refresh <domain> Refresh auth tokens via browser
75
76
  apitap auth [domain] View or manage stored auth
77
+ apitap mcp Run the full ApiTap MCP server over stdio
76
78
  apitap serve <domain> Serve a skill file as an MCP server
77
79
  apitap browse <url> Browse a URL (discover + replay in one step)
78
80
  apitap peek <url> Zero-cost triage (HEAD only)
@@ -169,6 +171,7 @@ async function handleCapture(positional: string[], flags: Record<string, string
169
171
  port,
170
172
  launch: flags.launch === true,
171
173
  attach: flags.attach === true,
174
+ authDir: APITAP_DIR,
172
175
  allDomains: flags['all-domains'] === true,
173
176
  enablePreview: flags.preview === true,
174
177
  scrub: flags['no-scrub'] !== true,
@@ -672,6 +675,16 @@ async function handleServe(positional: string[], flags: Record<string, string |
672
675
  }
673
676
  }
674
677
 
678
+ async function handleMcp(): Promise<void> {
679
+ const server = createMcpServer({
680
+ skillsDir: SKILLS_DIR,
681
+ _skipSsrfCheck: process.env.APITAP_SKIP_SSRF_CHECK === '1',
682
+ });
683
+ const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
684
+ const transport = new StdioServerTransport();
685
+ await server.connect(transport);
686
+ }
687
+
675
688
  function timeAgo(isoDate: string): string {
676
689
  const diff = Date.now() - new Date(isoDate).getTime();
677
690
  const minutes = Math.floor(diff / 60000);
@@ -705,6 +718,7 @@ async function handleInspect(positional: string[], flags: Record<string, string
705
718
  port: typeof flags.port === 'string' ? parseInt(flags.port, 10) : undefined,
706
719
  launch: flags.launch === true,
707
720
  attach: flags.attach === true,
721
+ authDir: APITAP_DIR,
708
722
  duration,
709
723
  allDomains: flags['all-domains'] === true,
710
724
  enablePreview: false,
@@ -1009,6 +1023,9 @@ async function main(): Promise<void> {
1009
1023
  case 'serve':
1010
1024
  await handleServe(positional, flags);
1011
1025
  break;
1026
+ case 'mcp':
1027
+ await handleMcp();
1028
+ break;
1012
1029
  case 'inspect':
1013
1030
  await handleInspect(positional, flags);
1014
1031
  break;