@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,446 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
|
|
4
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
5
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
6
|
+
import { basename, dirname, relative, resolve } from "node:path";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
type ConnectorContext,
|
|
11
|
+
type ConnectorDefinition,
|
|
12
|
+
createHttpClient,
|
|
13
|
+
createSessionStore,
|
|
14
|
+
createStateContext,
|
|
15
|
+
createTlsClient,
|
|
16
|
+
executeOperation,
|
|
17
|
+
type HttpClient,
|
|
18
|
+
type TlsClient,
|
|
19
|
+
} from "../src";
|
|
20
|
+
|
|
21
|
+
type CliArgs = {
|
|
22
|
+
append: boolean;
|
|
23
|
+
connectorPath?: string;
|
|
24
|
+
operation?: string;
|
|
25
|
+
params: string;
|
|
26
|
+
sanitize: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type ConnectorRuntime = ConnectorDefinition;
|
|
30
|
+
|
|
31
|
+
type MutableRecord = Record<string, unknown>;
|
|
32
|
+
|
|
33
|
+
async function main() {
|
|
34
|
+
const args = parseArgs(process.argv.slice(2));
|
|
35
|
+
const location = resolveConnectorLocation(args.connectorPath);
|
|
36
|
+
const connector = await loadConnector(location.rootDir);
|
|
37
|
+
const operationName = resolveOperationName(connector, args.operation);
|
|
38
|
+
const operation = connector.operations[operationName];
|
|
39
|
+
const parsedParams = parseParams(operation, args.params);
|
|
40
|
+
|
|
41
|
+
const capture = createCaptureContext(
|
|
42
|
+
resolveOperationBaseUrl(connector, operationName),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
console.log(
|
|
46
|
+
`[apifuse record] Calling ${operationName} on ${connector.id}...`,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const result = await executeOperation(
|
|
50
|
+
connector,
|
|
51
|
+
operationName,
|
|
52
|
+
capture.ctx,
|
|
53
|
+
parsedParams,
|
|
54
|
+
);
|
|
55
|
+
const captured = capture.getCapturedRaw();
|
|
56
|
+
|
|
57
|
+
if (captured === undefined) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`No upstream response was captured for ${connector.id}.${operationName}.`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const rawPayload = args.sanitize ? sanitizeFixture(captured) : captured;
|
|
64
|
+
const fixturePath = resolve(location.rootDir, "__fixtures__", "raw.json");
|
|
65
|
+
const nextPayload = await prepareFixturePayload(
|
|
66
|
+
fixturePath,
|
|
67
|
+
rawPayload,
|
|
68
|
+
args.append,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
await mkdir(dirname(fixturePath), { recursive: true });
|
|
72
|
+
await writeFile(fixturePath, `${JSON.stringify(nextPayload, null, 2)}\n`);
|
|
73
|
+
|
|
74
|
+
console.log(
|
|
75
|
+
`[apifuse record] Captured response (${formatBytes(
|
|
76
|
+
Buffer.byteLength(JSON.stringify(rawPayload)),
|
|
77
|
+
)})`,
|
|
78
|
+
);
|
|
79
|
+
console.log(
|
|
80
|
+
`[apifuse record] Saved to ${relative(process.cwd(), fixturePath)}`,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
void result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseArgs(argv: string[]): CliArgs {
|
|
87
|
+
let connectorPath: string | undefined;
|
|
88
|
+
let operation: string | undefined;
|
|
89
|
+
let params = "{}";
|
|
90
|
+
let sanitize = true;
|
|
91
|
+
let append = false;
|
|
92
|
+
|
|
93
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
94
|
+
const arg = argv[index];
|
|
95
|
+
|
|
96
|
+
if (arg === "--operation" || arg === "-o") {
|
|
97
|
+
const value = argv[index + 1];
|
|
98
|
+
if (!value) {
|
|
99
|
+
throw new Error("Missing value for --operation.");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
operation = value;
|
|
103
|
+
index += 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (arg.startsWith("--operation=")) {
|
|
108
|
+
operation = arg.slice("--operation=".length);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (arg === "--params" || arg === "-p") {
|
|
113
|
+
const value = argv[index + 1];
|
|
114
|
+
if (!value) {
|
|
115
|
+
throw new Error("Missing value for --params.");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
params = value;
|
|
119
|
+
index += 1;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (arg.startsWith("--params=")) {
|
|
124
|
+
params = arg.slice("--params=".length);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (arg === "--sanitize") {
|
|
129
|
+
sanitize = true;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (arg === "--no-sanitize") {
|
|
134
|
+
sanitize = false;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (arg === "--append") {
|
|
139
|
+
append = true;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (arg.startsWith("-")) {
|
|
144
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!connectorPath) {
|
|
148
|
+
connectorPath = arg;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { append, connectorPath, operation, params, sanitize };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function resolveConnectorLocation(inputPath?: string) {
|
|
159
|
+
const originalInput = inputPath ?? process.cwd();
|
|
160
|
+
const resolvedInput = resolve(process.cwd(), originalInput);
|
|
161
|
+
|
|
162
|
+
if (!existsSync(resolvedInput)) {
|
|
163
|
+
throw new Error(`Connector path not found: ${originalInput}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const initialDirectory = statSync(resolvedInput).isDirectory()
|
|
167
|
+
? resolvedInput
|
|
168
|
+
: dirname(resolvedInput);
|
|
169
|
+
const connectorRoot = findConnectorRoot(initialDirectory);
|
|
170
|
+
|
|
171
|
+
if (!connectorRoot) {
|
|
172
|
+
throw new Error(`Could not find connector root under: ${originalInput}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
inputPath: originalInput,
|
|
177
|
+
label: basename(connectorRoot),
|
|
178
|
+
rootDir: connectorRoot,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function findConnectorRoot(startDirectory: string): string | undefined {
|
|
183
|
+
let currentDirectory = startDirectory;
|
|
184
|
+
|
|
185
|
+
while (true) {
|
|
186
|
+
if (looksLikeConnectorRoot(currentDirectory)) {
|
|
187
|
+
return currentDirectory;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const parentDirectory = dirname(currentDirectory);
|
|
191
|
+
if (parentDirectory === currentDirectory) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
currentDirectory = parentDirectory;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function looksLikeConnectorRoot(directory: string): boolean {
|
|
200
|
+
return (
|
|
201
|
+
existsSync(resolve(directory, "index.ts")) &&
|
|
202
|
+
existsSync(resolve(directory, "manifest.json"))
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function loadConnector(rootDir: string): Promise<ConnectorRuntime> {
|
|
207
|
+
const entryPath = resolve(rootDir, "index.ts");
|
|
208
|
+
const module = (await import(pathToFileURL(entryPath).href)) as {
|
|
209
|
+
default?: ConnectorRuntime;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
if (!module.default) {
|
|
213
|
+
throw new Error(`Connector must default-export a definition: ${entryPath}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return module.default;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function resolveOperationName(
|
|
220
|
+
connector: ConnectorRuntime,
|
|
221
|
+
operationName?: string,
|
|
222
|
+
): string {
|
|
223
|
+
if (operationName) {
|
|
224
|
+
if (!(operationName in connector.operations)) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`Unknown operation "${operationName}" for connector "${connector.id}".`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return operationName;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const [firstOperation] = Object.keys(connector.operations);
|
|
234
|
+
if (!firstOperation) {
|
|
235
|
+
throw new Error(`Connector "${connector.id}" has no operations.`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return firstOperation;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function parseParams(
|
|
242
|
+
operation: ConnectorRuntime["operations"][string],
|
|
243
|
+
value: string,
|
|
244
|
+
): unknown {
|
|
245
|
+
let parsed: unknown;
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
parsed = JSON.parse(value);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`Failed to parse --params JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return operation.input ? operation.input.parse(parsed) : parsed;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function resolveOperationBaseUrl(
|
|
259
|
+
connector: ConnectorRuntime,
|
|
260
|
+
operationName: string,
|
|
261
|
+
): string {
|
|
262
|
+
const baseUrl = connector.operations[operationName]?.upstream?.baseUrl;
|
|
263
|
+
if (!baseUrl) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
`Operation "${operationName}" for connector "${connector.id}" must define upstream.baseUrl.`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return baseUrl;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function createCaptureContext(baseUrl: string) {
|
|
273
|
+
let capturedRaw: unknown;
|
|
274
|
+
|
|
275
|
+
const http = proxyHttpClient(createHttpClient(baseUrl), (response) => {
|
|
276
|
+
capturedRaw = response.data;
|
|
277
|
+
});
|
|
278
|
+
const tls = proxyTlsClient(createTlsClient(baseUrl), (response) => {
|
|
279
|
+
capturedRaw = normalizeCapturedTlsResponse(response);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const ctx: ConnectorContext = {
|
|
283
|
+
http,
|
|
284
|
+
tls,
|
|
285
|
+
browser: {
|
|
286
|
+
engine: "playwright-stealth",
|
|
287
|
+
newPage: async () => {
|
|
288
|
+
throw new Error("Browser client is not available in apifuse record.");
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
session: createSessionStore(),
|
|
292
|
+
state: createStateContext(),
|
|
293
|
+
trace: {
|
|
294
|
+
span: async (_name, fn) => fn(),
|
|
295
|
+
},
|
|
296
|
+
auth: {
|
|
297
|
+
requestField: async () => {
|
|
298
|
+
throw new Error("Auth prompts are not available in apifuse record.");
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
ctx,
|
|
305
|
+
getCapturedRaw: () => capturedRaw,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function proxyHttpClient(
|
|
310
|
+
client: HttpClient,
|
|
311
|
+
onResponse: (response: Awaited<ReturnType<HttpClient["get"]>>) => void,
|
|
312
|
+
): HttpClient {
|
|
313
|
+
return new Proxy(client, {
|
|
314
|
+
get(target, prop, receiver) {
|
|
315
|
+
const value = Reflect.get(target, prop, receiver);
|
|
316
|
+
|
|
317
|
+
if (typeof value !== "function") {
|
|
318
|
+
return value;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return async (...args: unknown[]) => {
|
|
322
|
+
const response = await value.apply(target, args);
|
|
323
|
+
onResponse(response);
|
|
324
|
+
return response;
|
|
325
|
+
};
|
|
326
|
+
},
|
|
327
|
+
}) as HttpClient;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function proxyTlsClient(
|
|
331
|
+
client: TlsClient,
|
|
332
|
+
onResponse: (response: Awaited<ReturnType<TlsClient["fetch"]>>) => void,
|
|
333
|
+
): TlsClient {
|
|
334
|
+
return new Proxy(client, {
|
|
335
|
+
get(target, prop, receiver) {
|
|
336
|
+
const value = Reflect.get(target, prop, receiver);
|
|
337
|
+
|
|
338
|
+
if (prop === "fetch" && typeof value === "function") {
|
|
339
|
+
return async (...args: unknown[]) => {
|
|
340
|
+
const response = await value.apply(target, args);
|
|
341
|
+
onResponse(response);
|
|
342
|
+
return response;
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (prop === "createSession" && typeof value === "function") {
|
|
347
|
+
return (...args: unknown[]) => {
|
|
348
|
+
const session = value.apply(target, args) as ReturnType<
|
|
349
|
+
TlsClient["createSession"]
|
|
350
|
+
>;
|
|
351
|
+
return proxyTlsSession(session, onResponse);
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return value;
|
|
356
|
+
},
|
|
357
|
+
}) as TlsClient;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function proxyTlsSession(
|
|
361
|
+
session: ReturnType<TlsClient["createSession"]>,
|
|
362
|
+
onResponse: (response: Awaited<ReturnType<TlsClient["fetch"]>>) => void,
|
|
363
|
+
) {
|
|
364
|
+
return new Proxy(session, {
|
|
365
|
+
get(target, prop, receiver) {
|
|
366
|
+
const value = Reflect.get(target, prop, receiver);
|
|
367
|
+
|
|
368
|
+
if (prop === "fetch" && typeof value === "function") {
|
|
369
|
+
return async (...args: unknown[]) => {
|
|
370
|
+
const response = await value.apply(target, args);
|
|
371
|
+
onResponse(response);
|
|
372
|
+
return response;
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return value;
|
|
377
|
+
},
|
|
378
|
+
}) as ReturnType<TlsClient["createSession"]>;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function normalizeCapturedTlsResponse(
|
|
382
|
+
response: Awaited<ReturnType<TlsClient["fetch"]>>,
|
|
383
|
+
) {
|
|
384
|
+
try {
|
|
385
|
+
return JSON.parse(response.body);
|
|
386
|
+
} catch {
|
|
387
|
+
return response.body;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function sanitizeFixture(value: unknown): unknown {
|
|
392
|
+
if (Array.isArray(value)) {
|
|
393
|
+
return value.map((item) => sanitizeFixture(item));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (!value || typeof value !== "object") {
|
|
397
|
+
return value;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const entries = Object.entries(value as MutableRecord).map(
|
|
401
|
+
([key, entryValue]) => {
|
|
402
|
+
if (isSensitiveKey(key)) {
|
|
403
|
+
return [key, "[REDACTED]"] as const;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return [key, sanitizeFixture(entryValue)] as const;
|
|
407
|
+
},
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
return Object.fromEntries(entries);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function isSensitiveKey(key: string): boolean {
|
|
414
|
+
return /authorization|token|api[-_]?key/i.test(key);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function prepareFixturePayload(
|
|
418
|
+
fixturePath: string,
|
|
419
|
+
payload: unknown,
|
|
420
|
+
append: boolean,
|
|
421
|
+
): Promise<unknown> {
|
|
422
|
+
if (!append || !existsSync(fixturePath)) {
|
|
423
|
+
return payload;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const existing = JSON.parse(readFileSync(fixturePath, "utf8")) as unknown;
|
|
428
|
+
if (Array.isArray(existing)) {
|
|
429
|
+
return [...existing, payload];
|
|
430
|
+
}
|
|
431
|
+
} catch {
|
|
432
|
+
// Fall through to overwrite with the new payload.
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return payload;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function formatBytes(bytes: number): string {
|
|
439
|
+
if (bytes < 1024) {
|
|
440
|
+
return `${bytes} B`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
await main();
|