@apifuse/connector-sdk 2.0.0-beta.1
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 +44 -0
- package/bin/apifuse-check.ts +408 -0
- package/bin/apifuse-dev.ts +222 -0
- package/bin/apifuse-init.ts +390 -0
- package/bin/apifuse-perf.ts +1101 -0
- package/bin/apifuse-record.ts +446 -0
- package/bin/apifuse-test.ts +688 -0
- package/bin/apifuse.ts +51 -0
- package/package.json +64 -0
- package/src/__tests__/auth.test.ts +396 -0
- package/src/__tests__/browser-auth.test.ts +180 -0
- package/src/__tests__/browser.test.ts +632 -0
- package/src/__tests__/connectors-yaml.test.ts +135 -0
- package/src/__tests__/define.test.ts +225 -0
- package/src/__tests__/errors.test.ts +69 -0
- package/src/__tests__/executor.test.ts +214 -0
- package/src/__tests__/http.test.ts +238 -0
- package/src/__tests__/insights.test.ts +210 -0
- package/src/__tests__/instrumentation.test.ts +290 -0
- package/src/__tests__/otlp.test.ts +141 -0
- package/src/__tests__/perf.test.ts +60 -0
- package/src/__tests__/proxy.test.ts +359 -0
- package/src/__tests__/recipes.test.ts +36 -0
- package/src/__tests__/serve.test.ts +233 -0
- package/src/__tests__/session.test.ts +231 -0
- package/src/__tests__/state.test.ts +100 -0
- package/src/__tests__/stealth.test.ts +57 -0
- package/src/__tests__/testing.test.ts +97 -0
- package/src/__tests__/tls.test.ts +345 -0
- package/src/__tests__/types.test.ts +142 -0
- package/src/__tests__/utils.test.ts +62 -0
- package/src/__tests__/waterfall.test.ts +270 -0
- package/src/config/connectors-yaml.ts +373 -0
- package/src/config/loader.ts +122 -0
- package/src/define.ts +137 -0
- package/src/dev.ts +38 -0
- package/src/errors.ts +68 -0
- package/src/index.test.ts +1 -0
- package/src/index.ts +100 -0
- package/src/protocol.ts +183 -0
- package/src/recipes/gov-api.ts +97 -0
- package/src/recipes/rest-api.ts +152 -0
- package/src/runtime/auth.ts +245 -0
- package/src/runtime/browser.ts +724 -0
- package/src/runtime/connector.ts +20 -0
- package/src/runtime/executor.ts +51 -0
- package/src/runtime/http.ts +248 -0
- package/src/runtime/insights.ts +456 -0
- package/src/runtime/instrumentation.ts +424 -0
- package/src/runtime/otlp.ts +171 -0
- package/src/runtime/perf.ts +73 -0
- package/src/runtime/session.ts +573 -0
- package/src/runtime/state.ts +124 -0
- package/src/runtime/tls.ts +410 -0
- package/src/runtime/trace.ts +261 -0
- package/src/runtime/waterfall.ts +245 -0
- package/src/serve.ts +665 -0
- package/src/stealth/profiles.ts +391 -0
- package/src/testing/helpers.ts +144 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/run.ts +88 -0
- package/src/types/playwright-stealth.d.ts +9 -0
- package/src/types.ts +243 -0
- package/src/utils/date.ts +163 -0
- package/src/utils/parse.ts +66 -0
- package/src/utils/text.ts +20 -0
- package/src/utils/transform.ts +62 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { SDKError } from "../errors";
|
|
2
|
+
import type { StealthPlatform, StealthProfile } from "../types";
|
|
3
|
+
|
|
4
|
+
type StealthProfileDefinition = Omit<StealthProfile, "name" | "platform"> & {
|
|
5
|
+
platform: StealthPlatform;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const CHROMIUM_HEADER_ORDER = [
|
|
9
|
+
":method",
|
|
10
|
+
":authority",
|
|
11
|
+
":scheme",
|
|
12
|
+
":path",
|
|
13
|
+
"user-agent",
|
|
14
|
+
"accept",
|
|
15
|
+
"accept-encoding",
|
|
16
|
+
"accept-language",
|
|
17
|
+
"cache-control",
|
|
18
|
+
"pragma",
|
|
19
|
+
"cookie",
|
|
20
|
+
"sec-ch-ua",
|
|
21
|
+
"sec-ch-ua-mobile",
|
|
22
|
+
"sec-ch-ua-platform",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const FIREFOX_HEADER_ORDER = [
|
|
26
|
+
":method",
|
|
27
|
+
":path",
|
|
28
|
+
":authority",
|
|
29
|
+
":scheme",
|
|
30
|
+
"user-agent",
|
|
31
|
+
"accept",
|
|
32
|
+
"accept-language",
|
|
33
|
+
"accept-encoding",
|
|
34
|
+
"referer",
|
|
35
|
+
"cookie",
|
|
36
|
+
"upgrade-insecure-requests",
|
|
37
|
+
"sec-fetch-dest",
|
|
38
|
+
"sec-fetch-mode",
|
|
39
|
+
"sec-fetch-site",
|
|
40
|
+
"sec-fetch-user",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const SAFARI_HEADER_ORDER = [
|
|
44
|
+
":method",
|
|
45
|
+
":scheme",
|
|
46
|
+
":path",
|
|
47
|
+
":authority",
|
|
48
|
+
"accept",
|
|
49
|
+
"user-agent",
|
|
50
|
+
"accept-language",
|
|
51
|
+
"accept-encoding",
|
|
52
|
+
"cookie",
|
|
53
|
+
"upgrade-insecure-requests",
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const CHROMIUM_H2_SETTINGS = {
|
|
57
|
+
HEADER_TABLE_SIZE: 65536,
|
|
58
|
+
ENABLE_PUSH: 0,
|
|
59
|
+
INITIAL_WINDOW_SIZE: 6291456,
|
|
60
|
+
MAX_HEADER_LIST_SIZE: 262144,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const FIREFOX_H2_SETTINGS = {
|
|
64
|
+
HEADER_TABLE_SIZE: 65536,
|
|
65
|
+
INITIAL_WINDOW_SIZE: 131072,
|
|
66
|
+
MAX_FRAME_SIZE: 16384,
|
|
67
|
+
MAX_HEADER_LIST_SIZE: 65536,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const SAFARI_H2_SETTINGS = {
|
|
71
|
+
HEADER_TABLE_SIZE: 4096,
|
|
72
|
+
ENABLE_PUSH: 0,
|
|
73
|
+
INITIAL_WINDOW_SIZE: 4194304,
|
|
74
|
+
MAX_CONCURRENT_STREAMS: 100,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const CHROMIUM_JA3 =
|
|
78
|
+
"771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513-65037,29-23-24,0";
|
|
79
|
+
const FIREFOX_JA3 =
|
|
80
|
+
"771,4865-4867-4866-49195-49199-52393-52392-49196-49200-49162-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-28-27-43-45-51,29-23-24-25,0";
|
|
81
|
+
const SAFARI_JA3 =
|
|
82
|
+
"771,4865-4866-4867-49196-49195-52393-49200-49199-49188-49192-159-158-107-103-57-51-157-156-61-60-53-47-255,0-23-65281-10-11-16-5-13-18-51-45-43-27,29-23-24-25,0";
|
|
83
|
+
|
|
84
|
+
function createProfile(
|
|
85
|
+
name: string,
|
|
86
|
+
definition: StealthProfileDefinition,
|
|
87
|
+
): StealthProfile {
|
|
88
|
+
return {
|
|
89
|
+
name,
|
|
90
|
+
platform: definition.platform,
|
|
91
|
+
version: definition.version,
|
|
92
|
+
userAgent: definition.userAgent,
|
|
93
|
+
tlsClientIdentifier: definition.tlsClientIdentifier,
|
|
94
|
+
ja3: definition.ja3,
|
|
95
|
+
ja4: definition.ja4,
|
|
96
|
+
h2Settings: definition.h2Settings
|
|
97
|
+
? { ...definition.h2Settings }
|
|
98
|
+
: undefined,
|
|
99
|
+
headerOrder: definition.headerOrder
|
|
100
|
+
? [...definition.headerOrder]
|
|
101
|
+
: undefined,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function extractBrowserMajorVersion(profile: StealthProfile): string {
|
|
106
|
+
const chromeVersion = profile.userAgent.match(/Chrome\/(\d+)/)?.[1];
|
|
107
|
+
if (chromeVersion) {
|
|
108
|
+
return chromeVersion;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const identifierVersion =
|
|
112
|
+
profile.tlsClientIdentifier?.match(/(\d+)(?!.*\d)/)?.[1];
|
|
113
|
+
if (identifierVersion) {
|
|
114
|
+
return identifierVersion;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return profile.version.split(".")[0] ?? profile.version;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function toPlatformHeaderValue(platform: StealthPlatform): string {
|
|
121
|
+
switch (platform) {
|
|
122
|
+
case "macos":
|
|
123
|
+
return '"macOS"';
|
|
124
|
+
case "windows":
|
|
125
|
+
return '"Windows"';
|
|
126
|
+
case "linux":
|
|
127
|
+
return '"Linux"';
|
|
128
|
+
case "android":
|
|
129
|
+
return '"Android"';
|
|
130
|
+
case "ios":
|
|
131
|
+
return '"iOS"';
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function generateLayer2Headers(
|
|
136
|
+
profile: StealthProfile,
|
|
137
|
+
): Record<string, string> {
|
|
138
|
+
const headers: Record<string, string> = {
|
|
139
|
+
"Accept-Language": "ko-KR,ko;q=0.9,en-US;q=0.8",
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const identifier = profile.tlsClientIdentifier?.toLowerCase() ?? "";
|
|
143
|
+
const majorVersion = extractBrowserMajorVersion(profile);
|
|
144
|
+
|
|
145
|
+
if (identifier.startsWith("chrome_") || identifier.startsWith("edge_")) {
|
|
146
|
+
const isEdge =
|
|
147
|
+
identifier.startsWith("edge_") || /\bEdg\//.test(profile.userAgent);
|
|
148
|
+
headers["Sec-Ch-Ua"] = isEdge
|
|
149
|
+
? `"Chromium";v="${majorVersion}", "Microsoft Edge";v="${majorVersion}", "Not)A;Brand";v="99"`
|
|
150
|
+
: `"Chromium";v="${majorVersion}", "Google Chrome";v="${majorVersion}", "Not)A;Brand";v="99"`;
|
|
151
|
+
headers["Sec-Ch-Ua-Platform"] = toPlatformHeaderValue(profile.platform);
|
|
152
|
+
headers["Sec-Ch-Ua-Mobile"] =
|
|
153
|
+
profile.platform === "android" || profile.platform === "ios"
|
|
154
|
+
? "?1"
|
|
155
|
+
: "?0";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return headers;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const STEALTH_PROFILES: Record<string, StealthProfile> = {
|
|
162
|
+
"chrome-146": createProfile("chrome-146", {
|
|
163
|
+
platform: "macos",
|
|
164
|
+
version: "146.0.0.0",
|
|
165
|
+
userAgent:
|
|
166
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
|
|
167
|
+
tlsClientIdentifier: "chrome_146",
|
|
168
|
+
ja3: CHROMIUM_JA3,
|
|
169
|
+
h2Settings: CHROMIUM_H2_SETTINGS,
|
|
170
|
+
headerOrder: CHROMIUM_HEADER_ORDER,
|
|
171
|
+
}),
|
|
172
|
+
"chrome-144": createProfile("chrome-144", {
|
|
173
|
+
platform: "macos",
|
|
174
|
+
version: "144.0.0.0",
|
|
175
|
+
userAgent:
|
|
176
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36",
|
|
177
|
+
tlsClientIdentifier: "chrome_144",
|
|
178
|
+
ja3: CHROMIUM_JA3,
|
|
179
|
+
h2Settings: CHROMIUM_H2_SETTINGS,
|
|
180
|
+
headerOrder: CHROMIUM_HEADER_ORDER,
|
|
181
|
+
}),
|
|
182
|
+
"chrome-133": createProfile("chrome-133", {
|
|
183
|
+
platform: "macos",
|
|
184
|
+
version: "133.0.0.0",
|
|
185
|
+
userAgent:
|
|
186
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
|
|
187
|
+
tlsClientIdentifier: "chrome_133",
|
|
188
|
+
ja3: CHROMIUM_JA3,
|
|
189
|
+
h2Settings: CHROMIUM_H2_SETTINGS,
|
|
190
|
+
headerOrder: CHROMIUM_HEADER_ORDER,
|
|
191
|
+
}),
|
|
192
|
+
"chrome-131": createProfile("chrome-131", {
|
|
193
|
+
platform: "macos",
|
|
194
|
+
version: "131.0.0.0",
|
|
195
|
+
userAgent:
|
|
196
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
197
|
+
tlsClientIdentifier: "chrome_131",
|
|
198
|
+
ja3: CHROMIUM_JA3,
|
|
199
|
+
ja4: "t13d1516h2_8daaf6152771_02713d6af862",
|
|
200
|
+
h2Settings: CHROMIUM_H2_SETTINGS,
|
|
201
|
+
headerOrder: CHROMIUM_HEADER_ORDER,
|
|
202
|
+
}),
|
|
203
|
+
"chrome-124": createProfile("chrome-124", {
|
|
204
|
+
platform: "macos",
|
|
205
|
+
version: "124.0.0.0",
|
|
206
|
+
userAgent:
|
|
207
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
208
|
+
tlsClientIdentifier: "chrome_124",
|
|
209
|
+
ja3: CHROMIUM_JA3,
|
|
210
|
+
h2Settings: CHROMIUM_H2_SETTINGS,
|
|
211
|
+
headerOrder: CHROMIUM_HEADER_ORDER,
|
|
212
|
+
}),
|
|
213
|
+
"chrome-120": createProfile("chrome-120", {
|
|
214
|
+
platform: "macos",
|
|
215
|
+
version: "120.0.0.0",
|
|
216
|
+
userAgent:
|
|
217
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
218
|
+
tlsClientIdentifier: "chrome_120",
|
|
219
|
+
ja3: CHROMIUM_JA3,
|
|
220
|
+
h2Settings: CHROMIUM_H2_SETTINGS,
|
|
221
|
+
headerOrder: CHROMIUM_HEADER_ORDER,
|
|
222
|
+
}),
|
|
223
|
+
"chrome-146-psk": createProfile("chrome-146-psk", {
|
|
224
|
+
platform: "macos",
|
|
225
|
+
version: "146.0.0.0",
|
|
226
|
+
userAgent:
|
|
227
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
|
|
228
|
+
tlsClientIdentifier: "chrome_146_PSK",
|
|
229
|
+
ja3: CHROMIUM_JA3,
|
|
230
|
+
h2Settings: CHROMIUM_H2_SETTINGS,
|
|
231
|
+
headerOrder: CHROMIUM_HEADER_ORDER,
|
|
232
|
+
}),
|
|
233
|
+
"chrome-131-psk": createProfile("chrome-131-psk", {
|
|
234
|
+
platform: "macos",
|
|
235
|
+
version: "131.0.0.0",
|
|
236
|
+
userAgent:
|
|
237
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
238
|
+
tlsClientIdentifier: "chrome_131_PSK",
|
|
239
|
+
ja3: CHROMIUM_JA3,
|
|
240
|
+
h2Settings: CHROMIUM_H2_SETTINGS,
|
|
241
|
+
headerOrder: CHROMIUM_HEADER_ORDER,
|
|
242
|
+
}),
|
|
243
|
+
"chrome-130-psk": createProfile("chrome-130-psk", {
|
|
244
|
+
platform: "macos",
|
|
245
|
+
version: "130.0.0.0",
|
|
246
|
+
userAgent:
|
|
247
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
|
|
248
|
+
tlsClientIdentifier: "chrome_130_PSK",
|
|
249
|
+
ja3: CHROMIUM_JA3,
|
|
250
|
+
h2Settings: CHROMIUM_H2_SETTINGS,
|
|
251
|
+
headerOrder: CHROMIUM_HEADER_ORDER,
|
|
252
|
+
}),
|
|
253
|
+
"firefox-147": createProfile("firefox-147", {
|
|
254
|
+
platform: "macos",
|
|
255
|
+
version: "147.0",
|
|
256
|
+
userAgent:
|
|
257
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:147.0) Gecko/20100101 Firefox/147.0",
|
|
258
|
+
tlsClientIdentifier: "firefox_147",
|
|
259
|
+
ja3: FIREFOX_JA3,
|
|
260
|
+
h2Settings: FIREFOX_H2_SETTINGS,
|
|
261
|
+
headerOrder: FIREFOX_HEADER_ORDER,
|
|
262
|
+
}),
|
|
263
|
+
"firefox-135": createProfile("firefox-135", {
|
|
264
|
+
platform: "macos",
|
|
265
|
+
version: "135.0",
|
|
266
|
+
userAgent:
|
|
267
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:135.0) Gecko/20100101 Firefox/135.0",
|
|
268
|
+
tlsClientIdentifier: "firefox_135",
|
|
269
|
+
ja3: FIREFOX_JA3,
|
|
270
|
+
h2Settings: FIREFOX_H2_SETTINGS,
|
|
271
|
+
headerOrder: FIREFOX_HEADER_ORDER,
|
|
272
|
+
}),
|
|
273
|
+
"firefox-133": createProfile("firefox-133", {
|
|
274
|
+
platform: "macos",
|
|
275
|
+
version: "133.0",
|
|
276
|
+
userAgent:
|
|
277
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:133.0) Gecko/20100101 Firefox/133.0",
|
|
278
|
+
tlsClientIdentifier: "firefox_133",
|
|
279
|
+
ja3: FIREFOX_JA3,
|
|
280
|
+
h2Settings: FIREFOX_H2_SETTINGS,
|
|
281
|
+
headerOrder: FIREFOX_HEADER_ORDER,
|
|
282
|
+
}),
|
|
283
|
+
"firefox-132": createProfile("firefox-132", {
|
|
284
|
+
platform: "macos",
|
|
285
|
+
version: "132.0",
|
|
286
|
+
userAgent:
|
|
287
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0",
|
|
288
|
+
tlsClientIdentifier: "firefox_132",
|
|
289
|
+
ja3: FIREFOX_JA3,
|
|
290
|
+
h2Settings: FIREFOX_H2_SETTINGS,
|
|
291
|
+
headerOrder: FIREFOX_HEADER_ORDER,
|
|
292
|
+
}),
|
|
293
|
+
"safari-16": createProfile("safari-16", {
|
|
294
|
+
platform: "macos",
|
|
295
|
+
version: "16.0",
|
|
296
|
+
userAgent:
|
|
297
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15",
|
|
298
|
+
tlsClientIdentifier: "safari_16_0",
|
|
299
|
+
ja3: SAFARI_JA3,
|
|
300
|
+
h2Settings: SAFARI_H2_SETTINGS,
|
|
301
|
+
headerOrder: SAFARI_HEADER_ORDER,
|
|
302
|
+
}),
|
|
303
|
+
"safari-15": createProfile("safari-15", {
|
|
304
|
+
platform: "macos",
|
|
305
|
+
version: "15.6.1",
|
|
306
|
+
userAgent:
|
|
307
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6.1 Safari/605.1.15",
|
|
308
|
+
tlsClientIdentifier: "safari_15_6_1",
|
|
309
|
+
ja3: SAFARI_JA3,
|
|
310
|
+
h2Settings: SAFARI_H2_SETTINGS,
|
|
311
|
+
headerOrder: SAFARI_HEADER_ORDER,
|
|
312
|
+
}),
|
|
313
|
+
"edge-131": createProfile("edge-131", {
|
|
314
|
+
platform: "windows",
|
|
315
|
+
version: "131.0.0.0",
|
|
316
|
+
userAgent:
|
|
317
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0",
|
|
318
|
+
tlsClientIdentifier: "chrome_131",
|
|
319
|
+
ja3: CHROMIUM_JA3,
|
|
320
|
+
h2Settings: CHROMIUM_H2_SETTINGS,
|
|
321
|
+
headerOrder: CHROMIUM_HEADER_ORDER,
|
|
322
|
+
}),
|
|
323
|
+
"ios-safari-26": createProfile("ios-safari-26", {
|
|
324
|
+
platform: "ios",
|
|
325
|
+
version: "26.0",
|
|
326
|
+
userAgent:
|
|
327
|
+
"Mozilla/5.0 (iPhone; CPU iPhone OS 26_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Mobile/15E148 Safari/604.1",
|
|
328
|
+
tlsClientIdentifier: "safari_ios_26_0",
|
|
329
|
+
ja3: SAFARI_JA3,
|
|
330
|
+
h2Settings: SAFARI_H2_SETTINGS,
|
|
331
|
+
headerOrder: SAFARI_HEADER_ORDER,
|
|
332
|
+
}),
|
|
333
|
+
"ios-safari-18": createProfile("ios-safari-18", {
|
|
334
|
+
platform: "ios",
|
|
335
|
+
version: "18.0",
|
|
336
|
+
userAgent:
|
|
337
|
+
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",
|
|
338
|
+
tlsClientIdentifier: "safari_ios_18_0",
|
|
339
|
+
ja3: SAFARI_JA3,
|
|
340
|
+
h2Settings: SAFARI_H2_SETTINGS,
|
|
341
|
+
headerOrder: SAFARI_HEADER_ORDER,
|
|
342
|
+
}),
|
|
343
|
+
"ios-safari-17": createProfile("ios-safari-17", {
|
|
344
|
+
platform: "ios",
|
|
345
|
+
version: "17.0",
|
|
346
|
+
userAgent:
|
|
347
|
+
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
|
|
348
|
+
tlsClientIdentifier: "safari_ios_17_0",
|
|
349
|
+
ja3: SAFARI_JA3,
|
|
350
|
+
h2Settings: SAFARI_H2_SETTINGS,
|
|
351
|
+
headerOrder: SAFARI_HEADER_ORDER,
|
|
352
|
+
}),
|
|
353
|
+
"generic-desktop": createProfile("generic-desktop", {
|
|
354
|
+
platform: "macos",
|
|
355
|
+
version: "146.0.0.0",
|
|
356
|
+
userAgent:
|
|
357
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
|
|
358
|
+
tlsClientIdentifier: "chrome_146",
|
|
359
|
+
ja3: CHROMIUM_JA3,
|
|
360
|
+
h2Settings: CHROMIUM_H2_SETTINGS,
|
|
361
|
+
headerOrder: CHROMIUM_HEADER_ORDER,
|
|
362
|
+
}),
|
|
363
|
+
"generic-mobile": createProfile("generic-mobile", {
|
|
364
|
+
platform: "ios",
|
|
365
|
+
version: "26.0",
|
|
366
|
+
userAgent:
|
|
367
|
+
"Mozilla/5.0 (iPhone; CPU iPhone OS 26_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Mobile/15E148 Safari/604.1",
|
|
368
|
+
tlsClientIdentifier: "safari_ios_26_0",
|
|
369
|
+
ja3: SAFARI_JA3,
|
|
370
|
+
h2Settings: SAFARI_H2_SETTINGS,
|
|
371
|
+
headerOrder: SAFARI_HEADER_ORDER,
|
|
372
|
+
}),
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
export function getStealthProfile(name: string): StealthProfile {
|
|
376
|
+
const profile = STEALTH_PROFILES[name];
|
|
377
|
+
|
|
378
|
+
if (!profile) {
|
|
379
|
+
throw new SDKError(`Unknown stealth profile: ${name}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
...profile,
|
|
384
|
+
h2Settings: profile.h2Settings ? { ...profile.h2Settings } : undefined,
|
|
385
|
+
headerOrder: profile.headerOrder ? [...profile.headerOrder] : undefined,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function listStealthProfiles(): string[] {
|
|
390
|
+
return Object.keys(STEALTH_PROFILES);
|
|
391
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
type ShapeMatcher =
|
|
4
|
+
| "array"
|
|
5
|
+
| "boolean"
|
|
6
|
+
| "null"
|
|
7
|
+
| "number"
|
|
8
|
+
| "object"
|
|
9
|
+
| "string"
|
|
10
|
+
| "undefined";
|
|
11
|
+
|
|
12
|
+
interface ShapeObject {
|
|
13
|
+
[key: string]: ShapeValue;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ShapeArray extends Array<ShapeValue> {}
|
|
17
|
+
|
|
18
|
+
type ShapeValue = ShapeMatcher | ShapeObject | ShapeArray | unknown;
|
|
19
|
+
|
|
20
|
+
const SHAPE_MATCHERS = new Set<ShapeMatcher>([
|
|
21
|
+
"array",
|
|
22
|
+
"boolean",
|
|
23
|
+
"null",
|
|
24
|
+
"number",
|
|
25
|
+
"object",
|
|
26
|
+
"string",
|
|
27
|
+
"undefined",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Describe a transform function with before/after snapshot testing.
|
|
32
|
+
* Usage: describeTransform("normalizeUser", rawInput, expectedOutput, transformFn)
|
|
33
|
+
*/
|
|
34
|
+
export function describeTransform<TRaw, TOutput>(
|
|
35
|
+
name: string,
|
|
36
|
+
raw: TRaw,
|
|
37
|
+
expected: TOutput,
|
|
38
|
+
transformFn: (raw: TRaw) => TOutput,
|
|
39
|
+
): void {
|
|
40
|
+
describe(`transform: ${name}`, () => {
|
|
41
|
+
it("produces expected output", () => {
|
|
42
|
+
const result = transformFn(raw);
|
|
43
|
+
expect(result).toEqual(expected);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("is pure (same input → same output)", () => {
|
|
47
|
+
const result1 = transformFn(raw);
|
|
48
|
+
const result2 = transformFn(raw);
|
|
49
|
+
expect(result1).toEqual(result2);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function assertShape(received: unknown, shape: ShapeValue, path: string): void {
|
|
55
|
+
if (typeof shape === "string" && SHAPE_MATCHERS.has(shape as ShapeMatcher)) {
|
|
56
|
+
switch (shape) {
|
|
57
|
+
case "array":
|
|
58
|
+
expect(Array.isArray(received), `${path} should be an array`).toBe(
|
|
59
|
+
true,
|
|
60
|
+
);
|
|
61
|
+
return;
|
|
62
|
+
case "null":
|
|
63
|
+
expect(received, `${path} should be null`).toBeNull();
|
|
64
|
+
return;
|
|
65
|
+
case "object":
|
|
66
|
+
expect(received, `${path} should be an object`).not.toBeNull();
|
|
67
|
+
expect(typeof received, `${path} should be an object`).toBe("object");
|
|
68
|
+
expect(Array.isArray(received), `${path} should not be an array`).toBe(
|
|
69
|
+
false,
|
|
70
|
+
);
|
|
71
|
+
return;
|
|
72
|
+
case "undefined":
|
|
73
|
+
expect(received, `${path} should be undefined`).toBeUndefined();
|
|
74
|
+
return;
|
|
75
|
+
default: {
|
|
76
|
+
const receivedType = typeof received as ShapeMatcher;
|
|
77
|
+
expect(receivedType, `${path} should be ${shape}`).toBe(
|
|
78
|
+
shape as ShapeMatcher,
|
|
79
|
+
);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (Array.isArray(shape)) {
|
|
86
|
+
expect(Array.isArray(received), `${path} should be an array`).toBe(true);
|
|
87
|
+
|
|
88
|
+
if (shape.length > 0) {
|
|
89
|
+
const [itemShape] = shape;
|
|
90
|
+
for (const [index, item] of (received as Array<unknown>).entries()) {
|
|
91
|
+
assertShape(item, itemShape, `${path}[${index}]`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (shape && typeof shape === "object") {
|
|
99
|
+
const receivedRecord = received as Record<string, unknown>;
|
|
100
|
+
|
|
101
|
+
expect(received, `${path} should be an object`).toBeDefined();
|
|
102
|
+
expect(received, `${path} should be an object`).not.toBeNull();
|
|
103
|
+
expect(typeof received, `${path} should be an object`).toBe("object");
|
|
104
|
+
|
|
105
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
106
|
+
assertShape(receivedRecord[key], value, `${path}.${key}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
expect(received).toEqual(shape);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Assert that an object matches a shape (partial deep match).
|
|
117
|
+
* Checks that all expected keys exist with expected types/values.
|
|
118
|
+
*/
|
|
119
|
+
export function toMatchShape<T extends Record<string, unknown>>(
|
|
120
|
+
received: unknown,
|
|
121
|
+
shape: Partial<Record<keyof T, ShapeValue>>,
|
|
122
|
+
): void {
|
|
123
|
+
expect(received).toBeDefined();
|
|
124
|
+
expect(received).not.toBeNull();
|
|
125
|
+
expect(typeof received).toBe("object");
|
|
126
|
+
assertShape(received, shape, "received");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Create a snapshot test for a transform function.
|
|
131
|
+
* Records the output of transform(raw) and compares on future runs.
|
|
132
|
+
*/
|
|
133
|
+
export function snapshotTransform<TRaw, TOutput>(
|
|
134
|
+
name: string,
|
|
135
|
+
raw: TRaw,
|
|
136
|
+
transformFn: (raw: TRaw) => TOutput,
|
|
137
|
+
): void {
|
|
138
|
+
describe(`snapshot: ${name}`, () => {
|
|
139
|
+
it("matches snapshot", () => {
|
|
140
|
+
const result = transformFn(raw);
|
|
141
|
+
expect(result).toMatchSnapshot();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { ConnectorDefinition } from "../types";
|
|
4
|
+
|
|
5
|
+
const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)+$/;
|
|
6
|
+
|
|
7
|
+
type ConnectorManifest = {
|
|
8
|
+
id?: unknown;
|
|
9
|
+
displayName?: unknown;
|
|
10
|
+
category?: unknown;
|
|
11
|
+
version?: unknown;
|
|
12
|
+
sdkVersion?: unknown;
|
|
13
|
+
runtime?: unknown;
|
|
14
|
+
auth?: unknown;
|
|
15
|
+
stealthProfile?: unknown;
|
|
16
|
+
language?: unknown;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Run standard SDK tests for a connector in one line.
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* import { myConnector } from "../index";
|
|
24
|
+
* import { runStandardTests } from "@apifuse/connector-sdk/testing";
|
|
25
|
+
* runStandardTests(myConnector);
|
|
26
|
+
*/
|
|
27
|
+
export function runStandardTests(
|
|
28
|
+
connector: ConnectorDefinition,
|
|
29
|
+
_rawFixture?: unknown,
|
|
30
|
+
manifest?: ConnectorManifest,
|
|
31
|
+
): void {
|
|
32
|
+
const operations = Object.entries(connector.operations);
|
|
33
|
+
|
|
34
|
+
describe(`[SDK Standard Tests] ${connector.id}`, () => {
|
|
35
|
+
it("id follows kebab-case format", () => {
|
|
36
|
+
expect(CONNECTOR_ID_REGEX.test(connector.id)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("has required meta fields", () => {
|
|
40
|
+
expect(connector.meta.displayName).toBeTruthy();
|
|
41
|
+
expect(connector.meta.category).toBeTruthy();
|
|
42
|
+
expect(connector.version).toBeTruthy();
|
|
43
|
+
expect(["standard", "browser"]).toContain(connector.runtime);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("has at least one operation", () => {
|
|
47
|
+
expect(Object.keys(connector.operations).length).toBeGreaterThan(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("all operations have handler, input, and output", () => {
|
|
51
|
+
for (const [, op] of operations) {
|
|
52
|
+
expect(op.input).toBeTruthy();
|
|
53
|
+
expect(op.output).toBeTruthy();
|
|
54
|
+
expect(typeof op.handler).toBe("function");
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("operation schemas can parse fixture data", () => {
|
|
59
|
+
for (const [operationName, op] of operations) {
|
|
60
|
+
if (op.fixtures?.request !== undefined && op.input) {
|
|
61
|
+
const requestResult = op.input.safeParse(op.fixtures.request);
|
|
62
|
+
expect(requestResult.success).toBe(true);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (op.fixtures?.response !== undefined && op.output) {
|
|
66
|
+
const responseResult = op.output.safeParse(op.fixtures.response);
|
|
67
|
+
expect(responseResult.success).toBe(true);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
expect(operationName).toBeTruthy();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("manifest matches connector metadata", () => {
|
|
75
|
+
if (!manifest) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
expect(manifest.id).toBe(connector.id);
|
|
80
|
+
expect(manifest.displayName).toBe(connector.meta.displayName);
|
|
81
|
+
expect(manifest.category).toBe(connector.meta.category);
|
|
82
|
+
expect(manifest.version).toBe(connector.version);
|
|
83
|
+
expect(manifest.runtime).toBe(connector.runtime);
|
|
84
|
+
expect(manifest.sdkVersion).toBe(1);
|
|
85
|
+
expect(manifest.language).toBe("typescript");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|