@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,390 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
|
|
7
|
+
import { cancel, intro, isCancel, outro, select, text } from "@clack/prompts";
|
|
8
|
+
|
|
9
|
+
const CONNECTOR_NAME_REGEX = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
10
|
+
const CATEGORIES = [
|
|
11
|
+
"developer-tools",
|
|
12
|
+
"finance",
|
|
13
|
+
"commerce",
|
|
14
|
+
"productivity",
|
|
15
|
+
"marketing",
|
|
16
|
+
"data",
|
|
17
|
+
"communication",
|
|
18
|
+
"other",
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
type AuthMode = "none" | "credentials" | "api-key" | "oauth2";
|
|
22
|
+
type RuntimeMode = "standard" | "browser";
|
|
23
|
+
type Category = (typeof CATEGORIES)[number];
|
|
24
|
+
|
|
25
|
+
type Answers = {
|
|
26
|
+
name: string;
|
|
27
|
+
displayName: string;
|
|
28
|
+
category: Category;
|
|
29
|
+
authMode: AuthMode;
|
|
30
|
+
runtime: RuntimeMode;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const HELP_TEXT = `Usage: apifuse init <connector-name>
|
|
34
|
+
Example: apifuse init my-new-api`;
|
|
35
|
+
|
|
36
|
+
export async function main() {
|
|
37
|
+
const args = normalizeArgs(process.argv.slice(2));
|
|
38
|
+
|
|
39
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
40
|
+
console.log(HELP_TEXT);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
intro("Create a new ApiFuse connector");
|
|
45
|
+
|
|
46
|
+
const initialName = args[0];
|
|
47
|
+
const answers = await promptForAnswers(initialName);
|
|
48
|
+
const workspaceRoot = findWorkspaceRoot();
|
|
49
|
+
const connectorRoot = resolve(workspaceRoot, "connectors", answers.name);
|
|
50
|
+
|
|
51
|
+
if (existsSync(connectorRoot)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Connector directory already exists: connectors/${answers.name}`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await mkdir(join(connectorRoot, "__tests__"), { recursive: true });
|
|
58
|
+
|
|
59
|
+
await Promise.all([
|
|
60
|
+
writeFile(join(connectorRoot, "index.ts"), renderIndexTs(answers)),
|
|
61
|
+
writeFile(join(connectorRoot, "package.json"), renderPackageJson(answers)),
|
|
62
|
+
writeFile(join(connectorRoot, "Dockerfile"), renderDockerfile()),
|
|
63
|
+
writeFile(
|
|
64
|
+
join(connectorRoot, "manifest.json"),
|
|
65
|
+
renderManifestJson(answers),
|
|
66
|
+
),
|
|
67
|
+
writeFile(join(connectorRoot, "dev.ts"), renderDevTs()),
|
|
68
|
+
writeFile(join(connectorRoot, "tsconfig.json"), renderTsconfig()),
|
|
69
|
+
writeFile(
|
|
70
|
+
join(connectorRoot, "__tests__", "index.test.ts"),
|
|
71
|
+
renderTestTs(answers),
|
|
72
|
+
),
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
outro(`Created connectors/${answers.name}`);
|
|
76
|
+
console.log(`
|
|
77
|
+
Created:
|
|
78
|
+
connectors/${answers.name}/
|
|
79
|
+
├── index.ts
|
|
80
|
+
├── package.json
|
|
81
|
+
├── Dockerfile
|
|
82
|
+
├── manifest.json
|
|
83
|
+
├── dev.ts
|
|
84
|
+
├── tsconfig.json
|
|
85
|
+
└── __tests__/
|
|
86
|
+
└── index.test.ts
|
|
87
|
+
`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeArgs(argv: string[]): string[] {
|
|
91
|
+
return argv[0] === "init" ? argv.slice(1) : argv;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function promptForAnswers(initialName?: string): Promise<Answers> {
|
|
95
|
+
const name = await promptValue(
|
|
96
|
+
text({
|
|
97
|
+
message: "Connector name",
|
|
98
|
+
initialValue: initialName,
|
|
99
|
+
placeholder: "my-new-api",
|
|
100
|
+
validate(value) {
|
|
101
|
+
const normalized = value?.trim() ?? "";
|
|
102
|
+
if (!normalized) {
|
|
103
|
+
return "Connector name is required.";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!CONNECTOR_NAME_REGEX.test(normalized)) {
|
|
107
|
+
return "Use kebab-case, e.g. my-new-api.";
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const displayName = await promptValue(
|
|
114
|
+
text({
|
|
115
|
+
message: "Display name",
|
|
116
|
+
initialValue: toDisplayName(name),
|
|
117
|
+
validate(value) {
|
|
118
|
+
if (!(value?.trim() ?? "")) {
|
|
119
|
+
return "Display name is required.";
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const category = await promptValue<Category>(
|
|
126
|
+
select({
|
|
127
|
+
message: "Category",
|
|
128
|
+
options: CATEGORIES.map((value) => ({ label: value, value })),
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const authMode = await promptValue<AuthMode>(
|
|
133
|
+
select({
|
|
134
|
+
message: "Auth mode",
|
|
135
|
+
options: [
|
|
136
|
+
{ label: "none", value: "none" },
|
|
137
|
+
{ label: "credentials", value: "credentials" },
|
|
138
|
+
{ label: "api-key", value: "api-key" },
|
|
139
|
+
{ label: "oauth2", value: "oauth2" },
|
|
140
|
+
],
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const runtime = await promptValue<RuntimeMode>(
|
|
145
|
+
select({
|
|
146
|
+
message: "Runtime",
|
|
147
|
+
options: [
|
|
148
|
+
{ label: "standard", value: "standard" },
|
|
149
|
+
{ label: "browser", value: "browser" },
|
|
150
|
+
],
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
authMode,
|
|
156
|
+
category,
|
|
157
|
+
displayName: displayName.trim(),
|
|
158
|
+
name: name.trim(),
|
|
159
|
+
runtime,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function promptValue<T>(prompt: Promise<T | symbol>): Promise<T> {
|
|
164
|
+
const result = await prompt;
|
|
165
|
+
|
|
166
|
+
if (isCancel(result)) {
|
|
167
|
+
cancel("Operation cancelled.");
|
|
168
|
+
process.exit(0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function findWorkspaceRoot(): string {
|
|
175
|
+
let currentDirectory = process.cwd();
|
|
176
|
+
|
|
177
|
+
while (true) {
|
|
178
|
+
if (existsSync(resolve(currentDirectory, "connectors"))) {
|
|
179
|
+
return currentDirectory;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const parentDirectory = dirname(currentDirectory);
|
|
183
|
+
if (parentDirectory === currentDirectory) {
|
|
184
|
+
return process.cwd();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
currentDirectory = parentDirectory;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function toDisplayName(name: string): string {
|
|
192
|
+
return name
|
|
193
|
+
.split("-")
|
|
194
|
+
.filter(Boolean)
|
|
195
|
+
.map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`)
|
|
196
|
+
.join(" ");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function renderIndexTs(answers: Answers): string {
|
|
200
|
+
const authBlock = renderAuthBlock(answers.authMode);
|
|
201
|
+
const browserBlock =
|
|
202
|
+
answers.runtime === "browser"
|
|
203
|
+
? '\n browser: {\n engine: "nodriver",\n },'
|
|
204
|
+
: "";
|
|
205
|
+
|
|
206
|
+
return `import { defineConnector, z } from "@apifuse/connector-sdk";
|
|
207
|
+
|
|
208
|
+
const InputSchema = z.object({
|
|
209
|
+
value: z.string().default("hello").describe("Sample input value"),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const OutputSchema = z.object({
|
|
213
|
+
ok: z.boolean(),
|
|
214
|
+
message: z.string(),
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
export default defineConnector({
|
|
218
|
+
id: "${answers.name}",
|
|
219
|
+
version: "1.0.0",
|
|
220
|
+
runtime: "${answers.runtime}",${browserBlock}
|
|
221
|
+
auth: ${authBlock},
|
|
222
|
+
meta: {
|
|
223
|
+
displayName: "${escapeTemplate(answers.displayName)}",
|
|
224
|
+
description: "${escapeTemplate(answers.displayName)} connector",
|
|
225
|
+
category: "${answers.category}",
|
|
226
|
+
tags: ["${answers.name}", "example"],
|
|
227
|
+
},
|
|
228
|
+
operations: {
|
|
229
|
+
ping: {
|
|
230
|
+
description: "Sample operation for local connector development",
|
|
231
|
+
input: InputSchema,
|
|
232
|
+
output: OutputSchema,
|
|
233
|
+
handler: async (_ctx, input) => {
|
|
234
|
+
return {
|
|
235
|
+
ok: true,
|
|
236
|
+
message: "${escapeTemplate(answers.displayName)} received: " + input.value,
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
fixtures: {
|
|
240
|
+
request: { value: "hello" },
|
|
241
|
+
response: { ok: true, message: "${escapeTemplate(answers.displayName)} received: hello" },
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function renderAuthBlock(authMode: AuthMode): string {
|
|
250
|
+
switch (authMode) {
|
|
251
|
+
case "none":
|
|
252
|
+
return '{ mode: "none" }';
|
|
253
|
+
case "credentials":
|
|
254
|
+
return `{
|
|
255
|
+
mode: "credentials",
|
|
256
|
+
fields: [
|
|
257
|
+
{ name: "username", label: "Username", type: "text", required: true },
|
|
258
|
+
{ name: "password", label: "Password", type: "password", required: true },
|
|
259
|
+
],
|
|
260
|
+
}`;
|
|
261
|
+
case "api-key":
|
|
262
|
+
return `{
|
|
263
|
+
mode: "api-key",
|
|
264
|
+
fields: [
|
|
265
|
+
{ name: "apiKey", label: "API Key", type: "password", required: true },
|
|
266
|
+
],
|
|
267
|
+
}`;
|
|
268
|
+
case "oauth2":
|
|
269
|
+
return `{
|
|
270
|
+
mode: "oauth2",
|
|
271
|
+
fields: [
|
|
272
|
+
{ name: "accessToken", label: "Access Token", type: "password", required: true },
|
|
273
|
+
],
|
|
274
|
+
}`;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function renderPackageJson(answers: Answers): string {
|
|
279
|
+
return `${JSON.stringify(
|
|
280
|
+
{
|
|
281
|
+
name: `@apifuse/connector-${answers.name}`,
|
|
282
|
+
version: "1.0.0",
|
|
283
|
+
private: true,
|
|
284
|
+
type: "module",
|
|
285
|
+
main: "./index.ts",
|
|
286
|
+
scripts: {
|
|
287
|
+
dev: "bun --hot dev.ts",
|
|
288
|
+
start: "bun dev.ts",
|
|
289
|
+
test: "bun test",
|
|
290
|
+
},
|
|
291
|
+
dependencies: {
|
|
292
|
+
"@apifuse/connector-sdk": "workspace:*",
|
|
293
|
+
},
|
|
294
|
+
devDependencies: {
|
|
295
|
+
"@types/bun": "latest",
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
null,
|
|
299
|
+
2,
|
|
300
|
+
)}
|
|
301
|
+
`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function renderDockerfile(): string {
|
|
305
|
+
return `FROM oven/bun:1.2-alpine
|
|
306
|
+
WORKDIR /connector
|
|
307
|
+
COPY package.json bun.lockb* ./
|
|
308
|
+
RUN bun install --frozen-lockfile || bun install
|
|
309
|
+
COPY . .
|
|
310
|
+
EXPOSE 3900
|
|
311
|
+
CMD ["bun", "run", "start"]
|
|
312
|
+
`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function renderManifestJson(answers: Answers): string {
|
|
316
|
+
return `${JSON.stringify(
|
|
317
|
+
{
|
|
318
|
+
auth: answers.authMode,
|
|
319
|
+
category: answers.category,
|
|
320
|
+
displayName: answers.displayName,
|
|
321
|
+
id: answers.name,
|
|
322
|
+
language: "typescript",
|
|
323
|
+
runtime: answers.runtime,
|
|
324
|
+
sdkVersion: 2,
|
|
325
|
+
version: "1.0.0",
|
|
326
|
+
},
|
|
327
|
+
null,
|
|
328
|
+
2,
|
|
329
|
+
)}
|
|
330
|
+
`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function renderDevTs(): string {
|
|
334
|
+
return `import { startDevServer } from "@apifuse/connector-sdk";
|
|
335
|
+
|
|
336
|
+
import connector from "./index";
|
|
337
|
+
|
|
338
|
+
startDevServer(connector, { port: Number(process.env.PORT) || 3900 });
|
|
339
|
+
`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function renderTsconfig(): string {
|
|
343
|
+
return `${JSON.stringify(
|
|
344
|
+
{
|
|
345
|
+
compilerOptions: {
|
|
346
|
+
target: "ES2022",
|
|
347
|
+
module: "ES2022",
|
|
348
|
+
moduleResolution: "bundler",
|
|
349
|
+
strict: true,
|
|
350
|
+
noEmit: true,
|
|
351
|
+
skipLibCheck: true,
|
|
352
|
+
resolveJsonModule: true,
|
|
353
|
+
},
|
|
354
|
+
include: ["**/*.ts"],
|
|
355
|
+
exclude: ["node_modules"],
|
|
356
|
+
},
|
|
357
|
+
null,
|
|
358
|
+
2,
|
|
359
|
+
)}
|
|
360
|
+
`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function renderTestTs(answers: Answers): string {
|
|
364
|
+
return `import { describe, expect, it } from "bun:test";
|
|
365
|
+
import { runStandardTests } from "@apifuse/connector-sdk/testing";
|
|
366
|
+
|
|
367
|
+
import connector from "../index";
|
|
368
|
+
import manifest from "../manifest.json";
|
|
369
|
+
|
|
370
|
+
runStandardTests(connector, undefined, { ...manifest, sdkVersion: 1 });
|
|
371
|
+
|
|
372
|
+
describe("${answers.name}", () => {
|
|
373
|
+
it("keeps manifest metadata on sdk v2", () => {
|
|
374
|
+
expect(manifest.sdkVersion).toBe(2);
|
|
375
|
+
expect(manifest.auth).toBe("${answers.authMode}");
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function escapeTemplate(value: string): string {
|
|
382
|
+
return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (import.meta.main) {
|
|
386
|
+
await main().catch((error: unknown) => {
|
|
387
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
388
|
+
process.exit(1);
|
|
389
|
+
});
|
|
390
|
+
}
|