@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.
Files changed (67) hide show
  1. package/README.md +44 -0
  2. package/bin/apifuse-check.ts +408 -0
  3. package/bin/apifuse-dev.ts +222 -0
  4. package/bin/apifuse-init.ts +390 -0
  5. package/bin/apifuse-perf.ts +1101 -0
  6. package/bin/apifuse-record.ts +446 -0
  7. package/bin/apifuse-test.ts +688 -0
  8. package/bin/apifuse.ts +51 -0
  9. package/package.json +64 -0
  10. package/src/__tests__/auth.test.ts +396 -0
  11. package/src/__tests__/browser-auth.test.ts +180 -0
  12. package/src/__tests__/browser.test.ts +632 -0
  13. package/src/__tests__/connectors-yaml.test.ts +135 -0
  14. package/src/__tests__/define.test.ts +225 -0
  15. package/src/__tests__/errors.test.ts +69 -0
  16. package/src/__tests__/executor.test.ts +214 -0
  17. package/src/__tests__/http.test.ts +238 -0
  18. package/src/__tests__/insights.test.ts +210 -0
  19. package/src/__tests__/instrumentation.test.ts +290 -0
  20. package/src/__tests__/otlp.test.ts +141 -0
  21. package/src/__tests__/perf.test.ts +60 -0
  22. package/src/__tests__/proxy.test.ts +359 -0
  23. package/src/__tests__/recipes.test.ts +36 -0
  24. package/src/__tests__/serve.test.ts +233 -0
  25. package/src/__tests__/session.test.ts +231 -0
  26. package/src/__tests__/state.test.ts +100 -0
  27. package/src/__tests__/stealth.test.ts +57 -0
  28. package/src/__tests__/testing.test.ts +97 -0
  29. package/src/__tests__/tls.test.ts +345 -0
  30. package/src/__tests__/types.test.ts +142 -0
  31. package/src/__tests__/utils.test.ts +62 -0
  32. package/src/__tests__/waterfall.test.ts +270 -0
  33. package/src/config/connectors-yaml.ts +373 -0
  34. package/src/config/loader.ts +122 -0
  35. package/src/define.ts +137 -0
  36. package/src/dev.ts +38 -0
  37. package/src/errors.ts +68 -0
  38. package/src/index.test.ts +1 -0
  39. package/src/index.ts +100 -0
  40. package/src/protocol.ts +183 -0
  41. package/src/recipes/gov-api.ts +97 -0
  42. package/src/recipes/rest-api.ts +152 -0
  43. package/src/runtime/auth.ts +245 -0
  44. package/src/runtime/browser.ts +724 -0
  45. package/src/runtime/connector.ts +20 -0
  46. package/src/runtime/executor.ts +51 -0
  47. package/src/runtime/http.ts +248 -0
  48. package/src/runtime/insights.ts +456 -0
  49. package/src/runtime/instrumentation.ts +424 -0
  50. package/src/runtime/otlp.ts +171 -0
  51. package/src/runtime/perf.ts +73 -0
  52. package/src/runtime/session.ts +573 -0
  53. package/src/runtime/state.ts +124 -0
  54. package/src/runtime/tls.ts +410 -0
  55. package/src/runtime/trace.ts +261 -0
  56. package/src/runtime/waterfall.ts +245 -0
  57. package/src/serve.ts +665 -0
  58. package/src/stealth/profiles.ts +391 -0
  59. package/src/testing/helpers.ts +144 -0
  60. package/src/testing/index.ts +2 -0
  61. package/src/testing/run.ts +88 -0
  62. package/src/types/playwright-stealth.d.ts +9 -0
  63. package/src/types.ts +243 -0
  64. package/src/utils/date.ts +163 -0
  65. package/src/utils/parse.ts +66 -0
  66. package/src/utils/text.ts +20 -0
  67. package/src/utils/transform.ts +62 -0
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @apifuse/connector-sdk
2
+
3
+ ApiFuse Connector SDK — Build private API automation connectors.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @apifuse/connector-sdk
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { defineConnector } from '@apifuse/connector-sdk'
15
+ import { z } from 'zod'
16
+
17
+ export default defineConnector({
18
+ meta: {
19
+ id: 'my-api-prices',
20
+ displayName: 'My API Prices',
21
+ category: 'finance',
22
+ version: '1.0.0',
23
+ runtime: 'standard',
24
+ upstream: { baseUrl: 'https://api.example.com' },
25
+ },
26
+ input: z.object({ id: z.string() }),
27
+ output: z.object({ price: z.number() }),
28
+ operations: {
29
+ prices: {
30
+ execute: async (ctx, input) => {
31
+ const res = await ctx.http.get('/prices', { params: { id: input.id } })
32
+ return res.data as { price: number }
33
+ },
34
+ },
35
+ },
36
+ })
37
+ ```
38
+
39
+ ## Testing
40
+
41
+ ```typescript
42
+ import { runStandardTests } from '@apifuse/connector-sdk/testing'
43
+ runStandardTests(myConnector)
44
+ ```
@@ -0,0 +1,408 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { existsSync, readFileSync, statSync } from "node:fs";
4
+ import { dirname, resolve } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ import { z } from "zod";
8
+
9
+ import type { ConnectorDefinition } from "../src";
10
+
11
+ const HELP_TEXT = `Usage: apifuse check [path]
12
+ Example: apifuse check connectors/upbit-crypto
13
+ Default: apifuse check .`;
14
+
15
+ const manifestSchema = z.object({
16
+ auth: z.enum(["none", "credentials", "api-key", "oauth2"]),
17
+ category: z.string().min(1),
18
+ displayName: z.string().min(1),
19
+ id: z.string().min(1),
20
+ language: z.literal("typescript"),
21
+ runtime: z.enum(["standard", "browser"]),
22
+ sdkVersion: z.number().int().positive(),
23
+ version: z.string().min(1),
24
+ });
25
+
26
+ type CheckResult = {
27
+ message: string;
28
+ passed: boolean;
29
+ details?: string[];
30
+ };
31
+
32
+ type SafeParseResult =
33
+ | { success: true; data: unknown }
34
+ | { success: false; error: z.ZodError };
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
+ const inputPath = args[0] ?? ".";
45
+ const connectorRoot = resolveConnectorRoot(inputPath);
46
+ const results = await runChecks(connectorRoot);
47
+ const failed = results.filter((result) => !result.passed);
48
+
49
+ console.log(`Checking connector: ${connectorRoot}\n`);
50
+
51
+ for (const result of results) {
52
+ const prefix = result.passed ? "✓" : "✗";
53
+ console.log(`${prefix} ${result.message}`);
54
+
55
+ for (const detail of result.details ?? []) {
56
+ console.log(` - ${detail}`);
57
+ }
58
+ }
59
+
60
+ if (failed.length > 0) {
61
+ process.exit(1);
62
+ }
63
+
64
+ console.log("\nAll checks passed.");
65
+ }
66
+
67
+ function normalizeArgs(argv: string[]): string[] {
68
+ return argv[0] === "check" ? argv.slice(1) : argv;
69
+ }
70
+
71
+ function resolveConnectorRoot(inputPath: string): string {
72
+ const resolvedInput = resolveFromParents(inputPath);
73
+
74
+ if (!existsSync(resolvedInput)) {
75
+ throw new Error(`Connector path not found: ${inputPath}`);
76
+ }
77
+
78
+ const startDirectory = statSync(resolvedInput).isDirectory()
79
+ ? resolvedInput
80
+ : dirname(resolvedInput);
81
+
82
+ for (let currentDirectory = startDirectory; ; ) {
83
+ if (existsSync(resolve(currentDirectory, "index.ts"))) {
84
+ return currentDirectory;
85
+ }
86
+
87
+ const parentDirectory = dirname(currentDirectory);
88
+ if (parentDirectory === currentDirectory) {
89
+ break;
90
+ }
91
+
92
+ currentDirectory = parentDirectory;
93
+ }
94
+
95
+ throw new Error(`Could not find connector root for: ${inputPath}`);
96
+ }
97
+
98
+ function resolveFromParents(inputPath: string): string {
99
+ let currentDirectory = process.cwd();
100
+
101
+ while (true) {
102
+ const candidate = resolve(currentDirectory, inputPath);
103
+ if (existsSync(candidate)) {
104
+ return candidate;
105
+ }
106
+
107
+ const parentDirectory = dirname(currentDirectory);
108
+ if (parentDirectory === currentDirectory) {
109
+ return resolve(process.cwd(), inputPath);
110
+ }
111
+
112
+ currentDirectory = parentDirectory;
113
+ }
114
+ }
115
+
116
+ async function runChecks(connectorRoot: string): Promise<CheckResult[]> {
117
+ const indexPath = resolve(connectorRoot, "index.ts");
118
+ const manifestPath = resolve(connectorRoot, "manifest.json");
119
+ const dockerfilePath = resolve(connectorRoot, "Dockerfile");
120
+ const packageJsonPath = resolve(connectorRoot, "package.json");
121
+
122
+ const connectorModule = existsSync(indexPath)
123
+ ? await import(pathToFileURL(indexPath).href)
124
+ : undefined;
125
+ const connector = assertConnectorDefinition(connectorModule?.default);
126
+
127
+ return [
128
+ checkIndex(indexPath, connector),
129
+ checkOperations(connector),
130
+ checkFixtures(connector),
131
+ checkSchemas(connector),
132
+ checkManifest(manifestPath, connector),
133
+ checkDockerfile(dockerfilePath),
134
+ checkPackageJson(packageJsonPath),
135
+ ];
136
+ }
137
+
138
+ function checkIndex(
139
+ indexPath: string,
140
+ connector: ConnectorDefinition | undefined,
141
+ ): CheckResult {
142
+ if (!existsSync(indexPath)) {
143
+ return {
144
+ message: "index.ts exists and exports default defineConnector",
145
+ passed: false,
146
+ };
147
+ }
148
+
149
+ if (!connector) {
150
+ return {
151
+ message: "index.ts exists and exports default defineConnector",
152
+ passed: false,
153
+ };
154
+ }
155
+
156
+ return {
157
+ message: "index.ts exists and exports default defineConnector",
158
+ passed: true,
159
+ details: [`connector id: ${connector.id}`],
160
+ };
161
+ }
162
+
163
+ function checkOperations(
164
+ connector: ConnectorDefinition | undefined,
165
+ ): CheckResult {
166
+ if (!connector) {
167
+ return {
168
+ message: "All operations have handler, input, output",
169
+ passed: false,
170
+ };
171
+ }
172
+
173
+ const failures: string[] = [];
174
+
175
+ for (const [operationId, operation] of Object.entries(connector.operations)) {
176
+ if (typeof operation.handler !== "function") {
177
+ failures.push(`${operationId}: missing handler`);
178
+ }
179
+
180
+ if (!hasSafeParse(operation.input)) {
181
+ failures.push(`${operationId}: missing input schema`);
182
+ }
183
+
184
+ if (!hasSafeParse(operation.output)) {
185
+ failures.push(`${operationId}: missing output schema`);
186
+ }
187
+ }
188
+
189
+ return {
190
+ message: "All operations have handler, input, output",
191
+ passed: failures.length === 0,
192
+ details: failures.length > 0 ? failures : Object.keys(connector.operations),
193
+ };
194
+ }
195
+
196
+ function checkFixtures(
197
+ connector: ConnectorDefinition | undefined,
198
+ ): CheckResult {
199
+ if (!connector) {
200
+ return { message: "All operations have fixtures", passed: false };
201
+ }
202
+
203
+ const failures: string[] = [];
204
+
205
+ for (const [operationId, operation] of Object.entries(connector.operations)) {
206
+ if (!operation.fixtures) {
207
+ failures.push(`${operationId}: missing fixtures`);
208
+ continue;
209
+ }
210
+
211
+ if (operation.fixtures.request === undefined) {
212
+ failures.push(`${operationId}: missing fixtures.request`);
213
+ }
214
+
215
+ if (operation.fixtures.response === undefined) {
216
+ failures.push(`${operationId}: missing fixtures.response`);
217
+ }
218
+ }
219
+
220
+ return {
221
+ message: "All operations have fixtures",
222
+ passed: failures.length === 0,
223
+ details: failures,
224
+ };
225
+ }
226
+
227
+ function checkSchemas(connector: ConnectorDefinition | undefined): CheckResult {
228
+ if (!connector) {
229
+ return {
230
+ message: "Zod schemas parse fixtures without error",
231
+ passed: false,
232
+ };
233
+ }
234
+
235
+ const failures: string[] = [];
236
+
237
+ for (const [operationId, operation] of Object.entries(connector.operations)) {
238
+ if (!operation.fixtures) {
239
+ continue;
240
+ }
241
+
242
+ const requestResult = parseFixture(
243
+ operation.input,
244
+ operation.fixtures.request,
245
+ );
246
+ if (!requestResult.success) {
247
+ failures.push(
248
+ `${operationId}: request fixture invalid (${requestResult.error.issues.map((issue: z.ZodIssue) => issue.message).join(", ")})`,
249
+ );
250
+ }
251
+
252
+ const responseResult = parseFixture(
253
+ operation.output,
254
+ operation.fixtures.response,
255
+ );
256
+ if (!responseResult.success) {
257
+ failures.push(
258
+ `${operationId}: response fixture invalid (${responseResult.error.issues.map((issue: z.ZodIssue) => issue.message).join(", ")})`,
259
+ );
260
+ }
261
+ }
262
+
263
+ return {
264
+ message: "Zod schemas parse fixtures without error",
265
+ passed: failures.length === 0,
266
+ details: failures,
267
+ };
268
+ }
269
+
270
+ function checkManifest(
271
+ manifestPath: string,
272
+ connector: ConnectorDefinition | undefined,
273
+ ): CheckResult {
274
+ if (!existsSync(manifestPath)) {
275
+ return { message: "manifest.json exists and is valid", passed: false };
276
+ }
277
+
278
+ try {
279
+ const manifest = manifestSchema.parse(
280
+ JSON.parse(readFileSync(manifestPath, "utf-8")) as unknown,
281
+ );
282
+ const details: string[] = [];
283
+
284
+ if (connector) {
285
+ if (manifest.id !== connector.id) {
286
+ details.push(
287
+ `id mismatch: manifest=${manifest.id} connector=${connector.id}`,
288
+ );
289
+ }
290
+
291
+ if (manifest.displayName !== connector.meta.displayName) {
292
+ details.push(
293
+ `displayName mismatch: manifest=${manifest.displayName} connector=${connector.meta.displayName}`,
294
+ );
295
+ }
296
+
297
+ if (manifest.category !== connector.meta.category) {
298
+ details.push(
299
+ `category mismatch: manifest=${manifest.category} connector=${connector.meta.category}`,
300
+ );
301
+ }
302
+
303
+ if (manifest.runtime !== connector.runtime) {
304
+ details.push(
305
+ `runtime mismatch: manifest=${manifest.runtime} connector=${connector.runtime}`,
306
+ );
307
+ }
308
+ }
309
+
310
+ return {
311
+ message: "manifest.json exists and is valid",
312
+ passed: details.length === 0,
313
+ details,
314
+ };
315
+ } catch (error) {
316
+ return {
317
+ message: "manifest.json exists and is valid",
318
+ passed: false,
319
+ details: [error instanceof Error ? error.message : String(error)],
320
+ };
321
+ }
322
+ }
323
+
324
+ function checkDockerfile(dockerfilePath: string): CheckResult {
325
+ return {
326
+ message: "Dockerfile exists",
327
+ passed: existsSync(dockerfilePath),
328
+ };
329
+ }
330
+
331
+ function checkPackageJson(packageJsonPath: string): CheckResult {
332
+ if (!existsSync(packageJsonPath)) {
333
+ return {
334
+ message: "package.json exists with @apifuse/connector-sdk dependency",
335
+ passed: false,
336
+ };
337
+ }
338
+
339
+ try {
340
+ const packageJson = z
341
+ .object({
342
+ dependencies: z.record(z.string(), z.string()).optional(),
343
+ })
344
+ .parse(JSON.parse(readFileSync(packageJsonPath, "utf-8")) as unknown);
345
+
346
+ const dependency = packageJson.dependencies?.["@apifuse/connector-sdk"];
347
+
348
+ return {
349
+ message: "package.json exists with @apifuse/connector-sdk dependency",
350
+ passed: typeof dependency === "string" && dependency.length > 0,
351
+ details:
352
+ typeof dependency === "string"
353
+ ? [`@apifuse/connector-sdk: ${dependency}`]
354
+ : ["Missing dependencies.@apifuse/connector-sdk"],
355
+ };
356
+ } catch (error) {
357
+ return {
358
+ message: "package.json exists with @apifuse/connector-sdk dependency",
359
+ passed: false,
360
+ details: [error instanceof Error ? error.message : String(error)],
361
+ };
362
+ }
363
+ }
364
+
365
+ function assertConnectorDefinition(
366
+ value: unknown,
367
+ ): ConnectorDefinition | undefined {
368
+ return isConnectorDefinition(value) ? value : undefined;
369
+ }
370
+
371
+ function isConnectorDefinition(value: unknown): value is ConnectorDefinition {
372
+ if (
373
+ !isRecord(value) ||
374
+ !isRecord(value.meta) ||
375
+ !isRecord(value.operations)
376
+ ) {
377
+ return false;
378
+ }
379
+
380
+ return (
381
+ typeof value.id === "string" &&
382
+ typeof value.version === "string" &&
383
+ (value.runtime === "standard" || value.runtime === "browser") &&
384
+ typeof value.meta.displayName === "string" &&
385
+ typeof value.meta.category === "string"
386
+ );
387
+ }
388
+
389
+ function isRecord(value: unknown): value is Record<string, unknown> {
390
+ return typeof value === "object" && value !== null;
391
+ }
392
+
393
+ function hasSafeParse(value: unknown): value is {
394
+ safeParse: (input: unknown) => SafeParseResult;
395
+ } {
396
+ return isRecord(value) && typeof value.safeParse === "function";
397
+ }
398
+
399
+ function parseFixture(schema: z.ZodType, fixture: unknown): SafeParseResult {
400
+ return schema.safeParse(fixture);
401
+ }
402
+
403
+ if (import.meta.main) {
404
+ await main().catch((error: unknown) => {
405
+ console.error(error instanceof Error ? error.message : String(error));
406
+ process.exit(1);
407
+ });
408
+ }
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { existsSync } from "node:fs";
4
+ import { dirname, resolve } from "node:path";
5
+ import * as readline from "node:readline";
6
+
7
+ import type { ConnectorDefinition } from "../src";
8
+ import {
9
+ ConnectorError,
10
+ createBrowserClient,
11
+ createHttpClient,
12
+ createStateContext,
13
+ createTlsClient,
14
+ } from "../src";
15
+ import type { AuthManager } from "../src/runtime/auth";
16
+ import { createTraceContext } from "../src/runtime/trace";
17
+ import type { ConnectorContext, SessionStore } from "../src/types";
18
+
19
+ const HELP_TEXT = `Usage: apifuse dev [path]
20
+ Example: apifuse dev connectors/upbit-crypto
21
+ Default: apifuse dev .`;
22
+
23
+ export async function main() {
24
+ const args = normalizeArgs(process.argv.slice(2));
25
+
26
+ if (args.includes("--help") || args.includes("-h")) {
27
+ console.log(HELP_TEXT);
28
+ return;
29
+ }
30
+
31
+ const connectorPath = resolveConnectorPath(args[0] ?? ".");
32
+ const connectorModule = await import(resolve(connectorPath, "index.ts"));
33
+ const connector = assertConnectorDefinition(
34
+ connectorModule.default,
35
+ connectorPath,
36
+ );
37
+
38
+ const { startDevServer } = await import("../src/dev");
39
+ const port = Number(process.env.PORT) || 3900;
40
+
41
+ startDevServer(connector, { port });
42
+
43
+ console.log("\nEndpoints:");
44
+ console.log(` GET http://localhost:${port}/health`);
45
+
46
+ for (const operationId of Object.keys(connector.operations)) {
47
+ console.log(` POST http://localhost:${port}/execute/${operationId}`);
48
+ console.log(` GET http://localhost:${port}/schema/${operationId}`);
49
+ }
50
+
51
+ console.log("\nHot reload:");
52
+ console.log(
53
+ ` bun --hot ${resolveImportPath("apifuse-dev.ts")} ${args[0] ?? "."}`,
54
+ );
55
+ }
56
+
57
+ export function createConnectorContext(
58
+ connector: ConnectorDefinition,
59
+ session: SessionStore,
60
+ authManager: AuthManager,
61
+ ): { ctx: ConnectorContext } {
62
+ const ctx: ConnectorContext = {
63
+ auth: authManager.createAuthContext(),
64
+ browser:
65
+ connector.runtime === "browser"
66
+ ? createBrowserClient({
67
+ engine: connector.browser?.engine ?? "playwright-stealth",
68
+ })
69
+ : createUnsupportedBrowserStub(),
70
+ http: createHttpClient(),
71
+ session,
72
+ state: createStateContext("dev-secret"),
73
+ trace: createTraceContext(),
74
+ tls: createTlsClient("http://localhost"),
75
+ };
76
+
77
+ return { ctx };
78
+ }
79
+
80
+ export async function runExchangeWithDeferredFieldPrompting(
81
+ authManager: AuthManager,
82
+ ctx: ConnectorContext,
83
+ credentials: Record<string, string>,
84
+ options: { pollIntervalMs?: number } = {},
85
+ ): Promise<void> {
86
+ const promptedFields = new Set<string>();
87
+ const pollIntervalMs = options.pollIntervalMs ?? 100;
88
+ let isSettled = false;
89
+
90
+ const exchangePromise = authManager.exchange(ctx, credentials).finally(() => {
91
+ isSettled = true;
92
+ });
93
+
94
+ while (!isSettled) {
95
+ for (const fieldName of authManager.getPendingFields()) {
96
+ if (promptedFields.has(fieldName)) {
97
+ continue;
98
+ }
99
+
100
+ promptedFields.add(fieldName);
101
+ const value = await promptForField(fieldName);
102
+ authManager.resolveField(fieldName, value.trim());
103
+ }
104
+
105
+ if (!isSettled) {
106
+ await Bun.sleep(pollIntervalMs);
107
+ }
108
+ }
109
+
110
+ await exchangePromise;
111
+ }
112
+
113
+ function normalizeArgs(argv: string[]): string[] {
114
+ return argv[0] === "dev" ? argv.slice(1) : argv;
115
+ }
116
+
117
+ function resolveConnectorPath(inputPath: string): string {
118
+ const resolvedInput = resolveFromParents(inputPath);
119
+ const entryPath = resolve(resolvedInput, "index.ts");
120
+
121
+ if (!existsSync(entryPath)) {
122
+ throw new Error(`Could not find index.ts in connector path: ${inputPath}`);
123
+ }
124
+
125
+ return resolvedInput;
126
+ }
127
+
128
+ function resolveFromParents(inputPath: string): string {
129
+ let currentDirectory = process.cwd();
130
+
131
+ while (true) {
132
+ const candidate = resolve(currentDirectory, inputPath);
133
+ if (existsSync(candidate)) {
134
+ return candidate;
135
+ }
136
+
137
+ const parentDirectory = dirname(currentDirectory);
138
+ if (parentDirectory === currentDirectory) {
139
+ return resolve(process.cwd(), inputPath);
140
+ }
141
+
142
+ currentDirectory = parentDirectory;
143
+ }
144
+ }
145
+
146
+ function resolveImportPath(fileName: string): string {
147
+ return resolve(process.cwd(), "bin", fileName);
148
+ }
149
+
150
+ function createUnsupportedBrowserStub(): ConnectorContext["browser"] {
151
+ return {
152
+ engine: "playwright-stealth",
153
+ async newPage() {
154
+ throw new ConnectorError(
155
+ "Browser runtime is not enabled for this connector",
156
+ {
157
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
158
+ fix: 'Set connector runtime to "browser" to use ctx.browser',
159
+ },
160
+ );
161
+ },
162
+ };
163
+ }
164
+
165
+ async function promptForField(fieldName: string): Promise<string> {
166
+ const rl = readline.createInterface({
167
+ input: process.stdin,
168
+ output: process.stdout,
169
+ });
170
+
171
+ try {
172
+ return await new Promise<string>((resolvePrompt) => {
173
+ rl.question(`\n[apifuse dev] Enter ${fieldName}: `, (answer) => {
174
+ resolvePrompt(answer);
175
+ });
176
+ });
177
+ } finally {
178
+ rl.close();
179
+ }
180
+ }
181
+
182
+ function assertConnectorDefinition(
183
+ value: unknown,
184
+ connectorPath: string,
185
+ ): ConnectorDefinition {
186
+ if (!isConnectorDefinition(value)) {
187
+ throw new Error(
188
+ `Expected ${resolve(connectorPath, "index.ts")} to export default defineConnector(...)`,
189
+ );
190
+ }
191
+
192
+ return value;
193
+ }
194
+
195
+ function isConnectorDefinition(value: unknown): value is ConnectorDefinition {
196
+ if (
197
+ !isRecord(value) ||
198
+ !isRecord(value.meta) ||
199
+ !isRecord(value.operations)
200
+ ) {
201
+ return false;
202
+ }
203
+
204
+ return (
205
+ typeof value.id === "string" &&
206
+ typeof value.version === "string" &&
207
+ (value.runtime === "standard" || value.runtime === "browser") &&
208
+ typeof value.meta.displayName === "string" &&
209
+ typeof value.meta.category === "string"
210
+ );
211
+ }
212
+
213
+ function isRecord(value: unknown): value is Record<string, unknown> {
214
+ return typeof value === "object" && value !== null;
215
+ }
216
+
217
+ if (import.meta.main) {
218
+ await main().catch((error: unknown) => {
219
+ console.error(error instanceof Error ? error.message : String(error));
220
+ process.exit(1);
221
+ });
222
+ }