@apitap/core 1.0.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.
- package/LICENSE +60 -0
- package/README.md +362 -0
- package/SKILL.md +270 -0
- package/dist/auth/crypto.d.ts +31 -0
- package/dist/auth/crypto.js +66 -0
- package/dist/auth/crypto.js.map +1 -0
- package/dist/auth/handoff.d.ts +29 -0
- package/dist/auth/handoff.js +180 -0
- package/dist/auth/handoff.js.map +1 -0
- package/dist/auth/manager.d.ts +46 -0
- package/dist/auth/manager.js +127 -0
- package/dist/auth/manager.js.map +1 -0
- package/dist/auth/oauth-refresh.d.ts +16 -0
- package/dist/auth/oauth-refresh.js +91 -0
- package/dist/auth/oauth-refresh.js.map +1 -0
- package/dist/auth/refresh.d.ts +43 -0
- package/dist/auth/refresh.js +217 -0
- package/dist/auth/refresh.js.map +1 -0
- package/dist/capture/anti-bot.d.ts +15 -0
- package/dist/capture/anti-bot.js +43 -0
- package/dist/capture/anti-bot.js.map +1 -0
- package/dist/capture/blocklist.d.ts +6 -0
- package/dist/capture/blocklist.js +70 -0
- package/dist/capture/blocklist.js.map +1 -0
- package/dist/capture/body-diff.d.ts +8 -0
- package/dist/capture/body-diff.js +102 -0
- package/dist/capture/body-diff.js.map +1 -0
- package/dist/capture/body-variables.d.ts +13 -0
- package/dist/capture/body-variables.js +142 -0
- package/dist/capture/body-variables.js.map +1 -0
- package/dist/capture/domain.d.ts +8 -0
- package/dist/capture/domain.js +34 -0
- package/dist/capture/domain.js.map +1 -0
- package/dist/capture/entropy.d.ts +33 -0
- package/dist/capture/entropy.js +100 -0
- package/dist/capture/entropy.js.map +1 -0
- package/dist/capture/filter.d.ts +11 -0
- package/dist/capture/filter.js +49 -0
- package/dist/capture/filter.js.map +1 -0
- package/dist/capture/graphql.d.ts +21 -0
- package/dist/capture/graphql.js +99 -0
- package/dist/capture/graphql.js.map +1 -0
- package/dist/capture/idle.d.ts +23 -0
- package/dist/capture/idle.js +44 -0
- package/dist/capture/idle.js.map +1 -0
- package/dist/capture/monitor.d.ts +26 -0
- package/dist/capture/monitor.js +183 -0
- package/dist/capture/monitor.js.map +1 -0
- package/dist/capture/oauth-detector.d.ts +18 -0
- package/dist/capture/oauth-detector.js +96 -0
- package/dist/capture/oauth-detector.js.map +1 -0
- package/dist/capture/pagination.d.ts +9 -0
- package/dist/capture/pagination.js +40 -0
- package/dist/capture/pagination.js.map +1 -0
- package/dist/capture/parameterize.d.ts +17 -0
- package/dist/capture/parameterize.js +63 -0
- package/dist/capture/parameterize.js.map +1 -0
- package/dist/capture/scrubber.d.ts +5 -0
- package/dist/capture/scrubber.js +38 -0
- package/dist/capture/scrubber.js.map +1 -0
- package/dist/capture/session.d.ts +46 -0
- package/dist/capture/session.js +445 -0
- package/dist/capture/session.js.map +1 -0
- package/dist/capture/token-detector.d.ts +16 -0
- package/dist/capture/token-detector.js +62 -0
- package/dist/capture/token-detector.js.map +1 -0
- package/dist/capture/verifier.d.ts +17 -0
- package/dist/capture/verifier.js +147 -0
- package/dist/capture/verifier.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +930 -0
- package/dist/cli.js.map +1 -0
- package/dist/discovery/auth.d.ts +17 -0
- package/dist/discovery/auth.js +81 -0
- package/dist/discovery/auth.js.map +1 -0
- package/dist/discovery/fetch.d.ts +17 -0
- package/dist/discovery/fetch.js +59 -0
- package/dist/discovery/fetch.js.map +1 -0
- package/dist/discovery/frameworks.d.ts +11 -0
- package/dist/discovery/frameworks.js +249 -0
- package/dist/discovery/frameworks.js.map +1 -0
- package/dist/discovery/index.d.ts +21 -0
- package/dist/discovery/index.js +219 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/discovery/openapi.d.ts +13 -0
- package/dist/discovery/openapi.js +175 -0
- package/dist/discovery/openapi.js.map +1 -0
- package/dist/discovery/probes.d.ts +9 -0
- package/dist/discovery/probes.js +70 -0
- package/dist/discovery/probes.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect/report.d.ts +52 -0
- package/dist/inspect/report.js +191 -0
- package/dist/inspect/report.js.map +1 -0
- package/dist/mcp.d.ts +8 -0
- package/dist/mcp.js +526 -0
- package/dist/mcp.js.map +1 -0
- package/dist/orchestration/browse.d.ts +38 -0
- package/dist/orchestration/browse.js +198 -0
- package/dist/orchestration/browse.js.map +1 -0
- package/dist/orchestration/cache.d.ts +15 -0
- package/dist/orchestration/cache.js +24 -0
- package/dist/orchestration/cache.js.map +1 -0
- package/dist/plugin.d.ts +17 -0
- package/dist/plugin.js +158 -0
- package/dist/plugin.js.map +1 -0
- package/dist/read/decoders/deepwiki.d.ts +2 -0
- package/dist/read/decoders/deepwiki.js +148 -0
- package/dist/read/decoders/deepwiki.js.map +1 -0
- package/dist/read/decoders/grokipedia.d.ts +2 -0
- package/dist/read/decoders/grokipedia.js +210 -0
- package/dist/read/decoders/grokipedia.js.map +1 -0
- package/dist/read/decoders/hackernews.d.ts +2 -0
- package/dist/read/decoders/hackernews.js +168 -0
- package/dist/read/decoders/hackernews.js.map +1 -0
- package/dist/read/decoders/index.d.ts +2 -0
- package/dist/read/decoders/index.js +12 -0
- package/dist/read/decoders/index.js.map +1 -0
- package/dist/read/decoders/reddit.d.ts +2 -0
- package/dist/read/decoders/reddit.js +142 -0
- package/dist/read/decoders/reddit.js.map +1 -0
- package/dist/read/decoders/twitter.d.ts +12 -0
- package/dist/read/decoders/twitter.js +187 -0
- package/dist/read/decoders/twitter.js.map +1 -0
- package/dist/read/decoders/wikipedia.d.ts +2 -0
- package/dist/read/decoders/wikipedia.js +66 -0
- package/dist/read/decoders/wikipedia.js.map +1 -0
- package/dist/read/decoders/youtube.d.ts +2 -0
- package/dist/read/decoders/youtube.js +69 -0
- package/dist/read/decoders/youtube.js.map +1 -0
- package/dist/read/extract.d.ts +25 -0
- package/dist/read/extract.js +320 -0
- package/dist/read/extract.js.map +1 -0
- package/dist/read/index.d.ts +14 -0
- package/dist/read/index.js +66 -0
- package/dist/read/index.js.map +1 -0
- package/dist/read/peek.d.ts +9 -0
- package/dist/read/peek.js +137 -0
- package/dist/read/peek.js.map +1 -0
- package/dist/read/types.d.ts +44 -0
- package/dist/read/types.js +3 -0
- package/dist/read/types.js.map +1 -0
- package/dist/replay/engine.d.ts +53 -0
- package/dist/replay/engine.js +441 -0
- package/dist/replay/engine.js.map +1 -0
- package/dist/replay/truncate.d.ts +16 -0
- package/dist/replay/truncate.js +92 -0
- package/dist/replay/truncate.js.map +1 -0
- package/dist/serve.d.ts +31 -0
- package/dist/serve.js +149 -0
- package/dist/serve.js.map +1 -0
- package/dist/skill/generator.d.ts +44 -0
- package/dist/skill/generator.js +419 -0
- package/dist/skill/generator.js.map +1 -0
- package/dist/skill/importer.d.ts +26 -0
- package/dist/skill/importer.js +80 -0
- package/dist/skill/importer.js.map +1 -0
- package/dist/skill/search.d.ts +19 -0
- package/dist/skill/search.js +51 -0
- package/dist/skill/search.js.map +1 -0
- package/dist/skill/signing.d.ts +16 -0
- package/dist/skill/signing.js +34 -0
- package/dist/skill/signing.js.map +1 -0
- package/dist/skill/ssrf.d.ts +27 -0
- package/dist/skill/ssrf.js +210 -0
- package/dist/skill/ssrf.js.map +1 -0
- package/dist/skill/store.d.ts +7 -0
- package/dist/skill/store.js +93 -0
- package/dist/skill/store.js.map +1 -0
- package/dist/stats/report.d.ts +26 -0
- package/dist/stats/report.js +157 -0
- package/dist/stats/report.js.map +1 -0
- package/dist/types.d.ts +214 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +58 -0
- package/src/auth/crypto.ts +92 -0
- package/src/auth/handoff.ts +229 -0
- package/src/auth/manager.ts +140 -0
- package/src/auth/oauth-refresh.ts +120 -0
- package/src/auth/refresh.ts +300 -0
- package/src/capture/anti-bot.ts +63 -0
- package/src/capture/blocklist.ts +75 -0
- package/src/capture/body-diff.ts +109 -0
- package/src/capture/body-variables.ts +156 -0
- package/src/capture/domain.ts +34 -0
- package/src/capture/entropy.ts +121 -0
- package/src/capture/filter.ts +56 -0
- package/src/capture/graphql.ts +124 -0
- package/src/capture/idle.ts +45 -0
- package/src/capture/monitor.ts +224 -0
- package/src/capture/oauth-detector.ts +106 -0
- package/src/capture/pagination.ts +49 -0
- package/src/capture/parameterize.ts +68 -0
- package/src/capture/scrubber.ts +49 -0
- package/src/capture/session.ts +502 -0
- package/src/capture/token-detector.ts +76 -0
- package/src/capture/verifier.ts +171 -0
- package/src/cli.ts +1031 -0
- package/src/discovery/auth.ts +99 -0
- package/src/discovery/fetch.ts +85 -0
- package/src/discovery/frameworks.ts +231 -0
- package/src/discovery/index.ts +256 -0
- package/src/discovery/openapi.ts +230 -0
- package/src/discovery/probes.ts +76 -0
- package/src/index.ts +26 -0
- package/src/inspect/report.ts +247 -0
- package/src/mcp.ts +618 -0
- package/src/orchestration/browse.ts +250 -0
- package/src/orchestration/cache.ts +37 -0
- package/src/plugin.ts +188 -0
- package/src/read/decoders/deepwiki.ts +180 -0
- package/src/read/decoders/grokipedia.ts +246 -0
- package/src/read/decoders/hackernews.ts +198 -0
- package/src/read/decoders/index.ts +15 -0
- package/src/read/decoders/reddit.ts +158 -0
- package/src/read/decoders/twitter.ts +211 -0
- package/src/read/decoders/wikipedia.ts +75 -0
- package/src/read/decoders/youtube.ts +75 -0
- package/src/read/extract.ts +396 -0
- package/src/read/index.ts +78 -0
- package/src/read/peek.ts +175 -0
- package/src/read/types.ts +37 -0
- package/src/replay/engine.ts +559 -0
- package/src/replay/truncate.ts +116 -0
- package/src/serve.ts +189 -0
- package/src/skill/generator.ts +473 -0
- package/src/skill/importer.ts +107 -0
- package/src/skill/search.ts +76 -0
- package/src/skill/signing.ts +36 -0
- package/src/skill/ssrf.ts +238 -0
- package/src/skill/store.ts +107 -0
- package/src/stats/report.ts +208 -0
- package/src/types.ts +233 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// src/capture/oauth-detector.ts
|
|
2
|
+
|
|
3
|
+
export interface OAuthInfo {
|
|
4
|
+
tokenEndpoint: string;
|
|
5
|
+
clientId: string;
|
|
6
|
+
grantType: 'refresh_token' | 'client_credentials';
|
|
7
|
+
scope?: string;
|
|
8
|
+
clientSecret?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Detect OAuth2 token endpoint requests from captured traffic.
|
|
13
|
+
* Only recognizes refreshable flows (refresh_token, client_credentials).
|
|
14
|
+
* Ignores authorization_code (initial auth, not refreshable in isolation).
|
|
15
|
+
*/
|
|
16
|
+
export function isOAuthTokenRequest(req: {
|
|
17
|
+
url: string;
|
|
18
|
+
method: string;
|
|
19
|
+
headers: Record<string, string>;
|
|
20
|
+
postData?: string;
|
|
21
|
+
}): OAuthInfo | null {
|
|
22
|
+
// Only POST requests
|
|
23
|
+
if (req.method.toUpperCase() !== 'POST') return null;
|
|
24
|
+
|
|
25
|
+
// URL heuristic: must contain /token or /oauth
|
|
26
|
+
const urlLower = req.url.toLowerCase();
|
|
27
|
+
if (!urlLower.includes('/token') && !urlLower.includes('/oauth')) return null;
|
|
28
|
+
|
|
29
|
+
if (!req.postData) return null;
|
|
30
|
+
|
|
31
|
+
// Parse body — support URL-encoded and JSON
|
|
32
|
+
const params = parseBody(req.postData, req.headers['content-type'] ?? '');
|
|
33
|
+
if (!params) return null;
|
|
34
|
+
|
|
35
|
+
const grantType = params.get('grant_type');
|
|
36
|
+
if (!grantType) return null;
|
|
37
|
+
|
|
38
|
+
// Only refreshable flows
|
|
39
|
+
if (grantType !== 'refresh_token' && grantType !== 'client_credentials') return null;
|
|
40
|
+
|
|
41
|
+
// Extract client_id — may also be in Basic auth header
|
|
42
|
+
let clientId = params.get('client_id') ?? '';
|
|
43
|
+
let clientSecret = params.get('client_secret');
|
|
44
|
+
|
|
45
|
+
if (!clientId) {
|
|
46
|
+
const basic = parseBasicAuth(req.headers['authorization'] ?? '');
|
|
47
|
+
if (basic) {
|
|
48
|
+
clientId = basic.username;
|
|
49
|
+
if (!clientSecret) clientSecret = basic.password;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!clientId) return null;
|
|
54
|
+
|
|
55
|
+
const result: OAuthInfo = {
|
|
56
|
+
tokenEndpoint: req.url.split('?')[0]!, // strip query params
|
|
57
|
+
clientId,
|
|
58
|
+
grantType: grantType as 'refresh_token' | 'client_credentials',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const scope = params.get('scope');
|
|
62
|
+
if (scope) result.scope = scope;
|
|
63
|
+
if (clientSecret) result.clientSecret = clientSecret;
|
|
64
|
+
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseBody(body: string, contentType: string): Map<string, string> | null {
|
|
69
|
+
try {
|
|
70
|
+
if (contentType.includes('application/json')) {
|
|
71
|
+
const obj = JSON.parse(body);
|
|
72
|
+
if (typeof obj !== 'object' || obj === null) return null;
|
|
73
|
+
const map = new Map<string, string>();
|
|
74
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
75
|
+
if (typeof v === 'string') map.set(k, v);
|
|
76
|
+
}
|
|
77
|
+
return map;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Default: URL-encoded (+ is space in application/x-www-form-urlencoded)
|
|
81
|
+
const map = new Map<string, string>();
|
|
82
|
+
const pairs = body.split('&');
|
|
83
|
+
for (const pair of pairs) {
|
|
84
|
+
const idx = pair.indexOf('=');
|
|
85
|
+
if (idx === -1) continue;
|
|
86
|
+
const key = decodeURIComponent(pair.slice(0, idx).replace(/\+/g, ' '));
|
|
87
|
+
const val = decodeURIComponent(pair.slice(idx + 1).replace(/\+/g, ' '));
|
|
88
|
+
map.set(key, val);
|
|
89
|
+
}
|
|
90
|
+
return map.size > 0 ? map : null;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseBasicAuth(header: string): { username: string; password: string } | null {
|
|
97
|
+
if (!header.startsWith('Basic ')) return null;
|
|
98
|
+
try {
|
|
99
|
+
const decoded = Buffer.from(header.slice(6), 'base64').toString('utf-8');
|
|
100
|
+
const idx = decoded.indexOf(':');
|
|
101
|
+
if (idx === -1) return null;
|
|
102
|
+
return { username: decoded.slice(0, idx), password: decoded.slice(idx + 1) };
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// src/capture/pagination.ts
|
|
2
|
+
import type { PaginationInfo } from '../types.js';
|
|
3
|
+
|
|
4
|
+
const OFFSET_PARAMS = new Set(['offset', 'skip']);
|
|
5
|
+
const CURSOR_PARAMS = new Set(['cursor', 'after', 'before', 'next_cursor', 'starting_after']);
|
|
6
|
+
const PAGE_PARAMS = new Set(['page', 'p', 'page_number']);
|
|
7
|
+
const LIMIT_PARAMS = new Set(['limit', 'per_page', 'page_size', 'count', 'size']);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Detect pagination patterns from query parameters.
|
|
11
|
+
* Returns null if no pagination pattern is detected.
|
|
12
|
+
*/
|
|
13
|
+
export function detectPagination(
|
|
14
|
+
queryParams: Record<string, { type: string; example: string }>,
|
|
15
|
+
): PaginationInfo | null {
|
|
16
|
+
const paramNames = Object.keys(queryParams);
|
|
17
|
+
const limitParam = paramNames.find(p => LIMIT_PARAMS.has(p.toLowerCase()));
|
|
18
|
+
|
|
19
|
+
// Check offset-based (offset/skip + optional limit)
|
|
20
|
+
for (const name of paramNames) {
|
|
21
|
+
if (OFFSET_PARAMS.has(name.toLowerCase())) {
|
|
22
|
+
return {
|
|
23
|
+
type: 'offset',
|
|
24
|
+
paramName: name,
|
|
25
|
+
...(limitParam ? { limitParam } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check cursor-based
|
|
31
|
+
for (const name of paramNames) {
|
|
32
|
+
if (CURSOR_PARAMS.has(name.toLowerCase())) {
|
|
33
|
+
return { type: 'cursor', paramName: name };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check page-based
|
|
38
|
+
for (const name of paramNames) {
|
|
39
|
+
if (PAGE_PARAMS.has(name.toLowerCase())) {
|
|
40
|
+
return {
|
|
41
|
+
type: 'page',
|
|
42
|
+
paramName: name,
|
|
43
|
+
...(limitParam && limitParam !== name ? { limitParam } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// src/capture/parameterize.ts
|
|
2
|
+
|
|
3
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4
|
+
const PURE_NUMERIC_RE = /^\d+$/;
|
|
5
|
+
const LONG_DIGITS_RE = /\d{8,}/;
|
|
6
|
+
const NEXT_DATA_PREFIX_RE = /^\/_next\/data\/[^/]+\//;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if a path segment is a dynamic value that should be parameterized.
|
|
10
|
+
* Returns the parameter name (:id, :hash, :slug) or null if static.
|
|
11
|
+
*/
|
|
12
|
+
function classifySegment(segment: string): string | null {
|
|
13
|
+
// Pure numeric → :id
|
|
14
|
+
if (PURE_NUMERIC_RE.test(segment)) return ':id';
|
|
15
|
+
|
|
16
|
+
// UUID → :id
|
|
17
|
+
if (UUID_RE.test(segment)) return ':id';
|
|
18
|
+
|
|
19
|
+
// Slug with embedded long number (8+ consecutive digits) — check before hash
|
|
20
|
+
// because slugs like "btc-updown-15m-1770254100" would also match the hash rule
|
|
21
|
+
if (LONG_DIGITS_RE.test(segment)) {
|
|
22
|
+
return ':slug';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Strip hyphens/underscores for character analysis
|
|
26
|
+
const stripped = segment.replace(/[-_]/g, '');
|
|
27
|
+
|
|
28
|
+
// Hash-like: 12+ alphanumeric chars with both letters and digits
|
|
29
|
+
if (stripped.length >= 12 && /[a-zA-Z]/.test(stripped) && /\d/.test(stripped)) {
|
|
30
|
+
return ':hash';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Replace dynamic path segments with :param placeholders.
|
|
38
|
+
*
|
|
39
|
+
* Rules:
|
|
40
|
+
* - Pure numeric → :id
|
|
41
|
+
* - UUID → :id
|
|
42
|
+
* - 12+ alphanum with mixed letters+digits → :hash
|
|
43
|
+
* - Contains 8+ consecutive digits → :slug
|
|
44
|
+
*/
|
|
45
|
+
export function parameterizePath(path: string): string {
|
|
46
|
+
const segments = path.split('/');
|
|
47
|
+
const result = segments.map(seg => {
|
|
48
|
+
if (seg === '') return seg;
|
|
49
|
+
return classifySegment(seg) ?? seg;
|
|
50
|
+
});
|
|
51
|
+
return result.join('/');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Strip framework-specific path noise for clean endpoint IDs.
|
|
56
|
+
*
|
|
57
|
+
* - Strips /_next/data/<hash>/ prefix (Next.js data routes)
|
|
58
|
+
* - Strips .json suffix
|
|
59
|
+
*/
|
|
60
|
+
export function cleanFrameworkPath(path: string): string {
|
|
61
|
+
let cleaned = path;
|
|
62
|
+
// Strip _next/data/<hash>/ prefix
|
|
63
|
+
cleaned = cleaned.replace(NEXT_DATA_PREFIX_RE, '/');
|
|
64
|
+
// Strip .json suffix
|
|
65
|
+
cleaned = cleaned.replace(/\.json$/, '');
|
|
66
|
+
// Ensure we have at least /
|
|
67
|
+
return cleaned || '/';
|
|
68
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// src/capture/scrubber.ts
|
|
2
|
+
|
|
3
|
+
// Email: standard pattern
|
|
4
|
+
const EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
|
|
5
|
+
|
|
6
|
+
// Phone (international): requires + prefix
|
|
7
|
+
const PHONE_INTL_RE = /\+[1-9]\d{7,14}/g;
|
|
8
|
+
|
|
9
|
+
// Phone (US): requires separators — (123) 456-7890 or 123-456-7890 or 123.456.7890
|
|
10
|
+
const PHONE_US_RE = /\(\d{3}\)[-.\s]\d{3}[-.\s]\d{4}|\d{3}[-.\s]\d{3}[-.\s]\d{4}/g;
|
|
11
|
+
|
|
12
|
+
// IPv4: four octets, each 0-255, validated programmatically
|
|
13
|
+
const IPV4_RE = /\b(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\b/g;
|
|
14
|
+
|
|
15
|
+
// Credit card: 16 digits with optional dashes or spaces every 4
|
|
16
|
+
const CARD_RE = /\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g;
|
|
17
|
+
|
|
18
|
+
// US SSN: 123-45-6789
|
|
19
|
+
const SSN_RE = /\b\d{3}-\d{2}-\d{4}\b/g;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Scrub PII from a string. Returns the string with PII replaced by placeholders.
|
|
23
|
+
* Order matters: SSN before phone (SSN is more specific).
|
|
24
|
+
*/
|
|
25
|
+
export function scrubPII(input: string): string {
|
|
26
|
+
let result = input;
|
|
27
|
+
|
|
28
|
+
// Email first (most distinctive pattern)
|
|
29
|
+
result = result.replace(EMAIL_RE, '[email]');
|
|
30
|
+
|
|
31
|
+
// SSN before phone (SSN pattern 123-45-6789 could be confused)
|
|
32
|
+
result = result.replace(SSN_RE, '[ssn]');
|
|
33
|
+
|
|
34
|
+
// Credit cards
|
|
35
|
+
result = result.replace(CARD_RE, '[card]');
|
|
36
|
+
|
|
37
|
+
// IPv4 with octet validation
|
|
38
|
+
result = result.replace(IPV4_RE, (_match, o1, o2, o3, o4) => {
|
|
39
|
+
const octets = [o1, o2, o3, o4].map(Number);
|
|
40
|
+
if (octets.every(o => o <= 255)) return '[ip]';
|
|
41
|
+
return _match;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Phone (international, then US)
|
|
45
|
+
result = result.replace(PHONE_INTL_RE, '[phone]');
|
|
46
|
+
result = result.replace(PHONE_US_RE, '[phone]');
|
|
47
|
+
|
|
48
|
+
return result;
|
|
49
|
+
}
|