@apitap/core 1.0.10 → 1.0.12
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 +4 -0
- package/dist/auth/handoff.d.ts +4 -0
- package/dist/auth/handoff.js +45 -11
- package/dist/auth/handoff.js.map +1 -1
- package/dist/auth/manager.d.ts +13 -0
- package/dist/auth/manager.js +33 -0
- package/dist/auth/manager.js.map +1 -1
- package/dist/auth/oauth-refresh.js +10 -1
- package/dist/auth/oauth-refresh.js.map +1 -1
- package/dist/auth/refresh.js +3 -4
- package/dist/auth/refresh.js.map +1 -1
- package/dist/capture/browser.d.ts +25 -0
- package/dist/capture/browser.js +79 -0
- package/dist/capture/browser.js.map +1 -0
- package/dist/capture/monitor.d.ts +1 -0
- package/dist/capture/monitor.js +27 -4
- package/dist/capture/monitor.js.map +1 -1
- package/dist/capture/oauth-detector.d.ts +1 -0
- package/dist/capture/oauth-detector.js +32 -1
- package/dist/capture/oauth-detector.js.map +1 -1
- package/dist/capture/session.d.ts +2 -0
- package/dist/capture/session.js +27 -16
- package/dist/capture/session.js.map +1 -1
- package/dist/cli.js +22 -2
- package/dist/cli.js.map +1 -1
- package/dist/skill/generator.d.ts +3 -0
- package/dist/skill/generator.js +6 -0
- package/dist/skill/generator.js.map +1 -1
- package/package.json +3 -2
- package/src/auth/handoff.ts +38 -9
- package/src/capture/browser.ts +39 -2
- package/src/capture/monitor.ts +19 -0
- package/src/capture/session.ts +1 -1
- package/src/cli.ts +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apitap/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
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",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"build": "tsc",
|
|
13
13
|
"dev": "tsx src/cli.ts",
|
|
14
14
|
"test": "node --import tsx --test 'test/**/*.test.ts'",
|
|
15
|
-
"typecheck": "tsc --noEmit"
|
|
15
|
+
"typecheck": "tsc --noEmit",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
16
17
|
},
|
|
17
18
|
"license": "BSL-1.1",
|
|
18
19
|
"repository": {
|
package/src/auth/handoff.ts
CHANGED
|
@@ -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
|
|
179
|
+
// Baseline: snapshot cookie values present BEFORE login so we can detect real transitions
|
|
148
180
|
const baselineCookies = await context.cookies();
|
|
149
|
-
const
|
|
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
|
|
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 (
|
|
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++) {
|
package/src/capture/browser.ts
CHANGED
|
@@ -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
|
|
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 };
|
package/src/capture/monitor.ts
CHANGED
|
@@ -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
|
package/src/capture/session.ts
CHANGED
|
@@ -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;
|