@apifuse/provider-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 +406 -0
- package/bin/apifuse-dev.ts +222 -0
- package/bin/apifuse-init.ts +387 -0
- package/bin/apifuse-perf.ts +1099 -0
- package/bin/apifuse-record.ts +444 -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__/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__/providers-yaml.test.ts +135 -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/loader.ts +122 -0
- package/src/config/providers-yaml.ts +370 -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/executor.ts +54 -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/provider.ts +20 -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 +664 -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
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# @apifuse/provider-sdk
|
|
2
|
+
|
|
3
|
+
ApiFuse Provider SDK — Build private API automation providers.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @apifuse/provider-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { defineProvider } from '@apifuse/provider-sdk'
|
|
15
|
+
import { z } from 'zod'
|
|
16
|
+
|
|
17
|
+
export default defineProvider({
|
|
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/provider-sdk/testing'
|
|
43
|
+
runStandardTests(myProvider)
|
|
44
|
+
```
|
|
@@ -0,0 +1,406 @@
|
|
|
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 { ProviderDefinition } from "../src";
|
|
10
|
+
|
|
11
|
+
const HELP_TEXT = `Usage: apifuse check [path]
|
|
12
|
+
Example: apifuse check providers/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 providerRoot = resolveProviderRoot(inputPath);
|
|
46
|
+
const results = await runChecks(providerRoot);
|
|
47
|
+
const failed = results.filter((result) => !result.passed);
|
|
48
|
+
|
|
49
|
+
console.log(`Checking provider: ${providerRoot}\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 resolveProviderRoot(inputPath: string): string {
|
|
72
|
+
const resolvedInput = resolveFromParents(inputPath);
|
|
73
|
+
|
|
74
|
+
if (!existsSync(resolvedInput)) {
|
|
75
|
+
throw new Error(`Provider 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 provider 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(providerRoot: string): Promise<CheckResult[]> {
|
|
117
|
+
const indexPath = resolve(providerRoot, "index.ts");
|
|
118
|
+
const manifestPath = resolve(providerRoot, "manifest.json");
|
|
119
|
+
const dockerfilePath = resolve(providerRoot, "Dockerfile");
|
|
120
|
+
const packageJsonPath = resolve(providerRoot, "package.json");
|
|
121
|
+
|
|
122
|
+
const providerModule = existsSync(indexPath)
|
|
123
|
+
? await import(pathToFileURL(indexPath).href)
|
|
124
|
+
: undefined;
|
|
125
|
+
const provider = assertProviderDefinition(providerModule?.default);
|
|
126
|
+
|
|
127
|
+
return [
|
|
128
|
+
checkIndex(indexPath, provider),
|
|
129
|
+
checkOperations(provider),
|
|
130
|
+
checkFixtures(provider),
|
|
131
|
+
checkSchemas(provider),
|
|
132
|
+
checkManifest(manifestPath, provider),
|
|
133
|
+
checkDockerfile(dockerfilePath),
|
|
134
|
+
checkPackageJson(packageJsonPath),
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function checkIndex(
|
|
139
|
+
indexPath: string,
|
|
140
|
+
provider: ProviderDefinition | undefined,
|
|
141
|
+
): CheckResult {
|
|
142
|
+
if (!existsSync(indexPath)) {
|
|
143
|
+
return {
|
|
144
|
+
message: "index.ts exists and exports default defineProvider",
|
|
145
|
+
passed: false,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!provider) {
|
|
150
|
+
return {
|
|
151
|
+
message: "index.ts exists and exports default defineProvider",
|
|
152
|
+
passed: false,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
message: "index.ts exists and exports default defineProvider",
|
|
158
|
+
passed: true,
|
|
159
|
+
details: [`provider id: ${provider.id}`],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function checkOperations(
|
|
164
|
+
provider: ProviderDefinition | undefined,
|
|
165
|
+
): CheckResult {
|
|
166
|
+
if (!provider) {
|
|
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(provider.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(provider.operations),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function checkFixtures(provider: ProviderDefinition | undefined): CheckResult {
|
|
197
|
+
if (!provider) {
|
|
198
|
+
return { message: "All operations have fixtures", passed: false };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const failures: string[] = [];
|
|
202
|
+
|
|
203
|
+
for (const [operationId, operation] of Object.entries(provider.operations)) {
|
|
204
|
+
if (!operation.fixtures) {
|
|
205
|
+
failures.push(`${operationId}: missing fixtures`);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (operation.fixtures.request === undefined) {
|
|
210
|
+
failures.push(`${operationId}: missing fixtures.request`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (operation.fixtures.response === undefined) {
|
|
214
|
+
failures.push(`${operationId}: missing fixtures.response`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
message: "All operations have fixtures",
|
|
220
|
+
passed: failures.length === 0,
|
|
221
|
+
details: failures,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function checkSchemas(provider: ProviderDefinition | undefined): CheckResult {
|
|
226
|
+
if (!provider) {
|
|
227
|
+
return {
|
|
228
|
+
message: "Zod schemas parse fixtures without error",
|
|
229
|
+
passed: false,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const failures: string[] = [];
|
|
234
|
+
|
|
235
|
+
for (const [operationId, operation] of Object.entries(provider.operations)) {
|
|
236
|
+
if (!operation.fixtures) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const requestResult = parseFixture(
|
|
241
|
+
operation.input,
|
|
242
|
+
operation.fixtures.request,
|
|
243
|
+
);
|
|
244
|
+
if (!requestResult.success) {
|
|
245
|
+
failures.push(
|
|
246
|
+
`${operationId}: request fixture invalid (${requestResult.error.issues.map((issue: z.ZodIssue) => issue.message).join(", ")})`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const responseResult = parseFixture(
|
|
251
|
+
operation.output,
|
|
252
|
+
operation.fixtures.response,
|
|
253
|
+
);
|
|
254
|
+
if (!responseResult.success) {
|
|
255
|
+
failures.push(
|
|
256
|
+
`${operationId}: response fixture invalid (${responseResult.error.issues.map((issue: z.ZodIssue) => issue.message).join(", ")})`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
message: "Zod schemas parse fixtures without error",
|
|
263
|
+
passed: failures.length === 0,
|
|
264
|
+
details: failures,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function checkManifest(
|
|
269
|
+
manifestPath: string,
|
|
270
|
+
provider: ProviderDefinition | undefined,
|
|
271
|
+
): CheckResult {
|
|
272
|
+
if (!existsSync(manifestPath)) {
|
|
273
|
+
return { message: "manifest.json exists and is valid", passed: false };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const manifest = manifestSchema.parse(
|
|
278
|
+
JSON.parse(readFileSync(manifestPath, "utf-8")) as unknown,
|
|
279
|
+
);
|
|
280
|
+
const details: string[] = [];
|
|
281
|
+
|
|
282
|
+
if (provider) {
|
|
283
|
+
if (manifest.id !== provider.id) {
|
|
284
|
+
details.push(
|
|
285
|
+
`id mismatch: manifest=${manifest.id} provider=${provider.id}`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (manifest.displayName !== provider.meta.displayName) {
|
|
290
|
+
details.push(
|
|
291
|
+
`displayName mismatch: manifest=${manifest.displayName} provider=${provider.meta.displayName}`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (manifest.category !== provider.meta.category) {
|
|
296
|
+
details.push(
|
|
297
|
+
`category mismatch: manifest=${manifest.category} provider=${provider.meta.category}`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (manifest.runtime !== provider.runtime) {
|
|
302
|
+
details.push(
|
|
303
|
+
`runtime mismatch: manifest=${manifest.runtime} provider=${provider.runtime}`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
message: "manifest.json exists and is valid",
|
|
310
|
+
passed: details.length === 0,
|
|
311
|
+
details,
|
|
312
|
+
};
|
|
313
|
+
} catch (error) {
|
|
314
|
+
return {
|
|
315
|
+
message: "manifest.json exists and is valid",
|
|
316
|
+
passed: false,
|
|
317
|
+
details: [error instanceof Error ? error.message : String(error)],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function checkDockerfile(dockerfilePath: string): CheckResult {
|
|
323
|
+
return {
|
|
324
|
+
message: "Dockerfile exists",
|
|
325
|
+
passed: existsSync(dockerfilePath),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function checkPackageJson(packageJsonPath: string): CheckResult {
|
|
330
|
+
if (!existsSync(packageJsonPath)) {
|
|
331
|
+
return {
|
|
332
|
+
message: "package.json exists with @apifuse/provider-sdk dependency",
|
|
333
|
+
passed: false,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
const packageJson = z
|
|
339
|
+
.object({
|
|
340
|
+
dependencies: z.record(z.string(), z.string()).optional(),
|
|
341
|
+
})
|
|
342
|
+
.parse(JSON.parse(readFileSync(packageJsonPath, "utf-8")) as unknown);
|
|
343
|
+
|
|
344
|
+
const dependency = packageJson.dependencies?.["@apifuse/provider-sdk"];
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
message: "package.json exists with @apifuse/provider-sdk dependency",
|
|
348
|
+
passed: typeof dependency === "string" && dependency.length > 0,
|
|
349
|
+
details:
|
|
350
|
+
typeof dependency === "string"
|
|
351
|
+
? [`@apifuse/provider-sdk: ${dependency}`]
|
|
352
|
+
: ["Missing dependencies.@apifuse/provider-sdk"],
|
|
353
|
+
};
|
|
354
|
+
} catch (error) {
|
|
355
|
+
return {
|
|
356
|
+
message: "package.json exists with @apifuse/provider-sdk dependency",
|
|
357
|
+
passed: false,
|
|
358
|
+
details: [error instanceof Error ? error.message : String(error)],
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function assertProviderDefinition(
|
|
364
|
+
value: unknown,
|
|
365
|
+
): ProviderDefinition | undefined {
|
|
366
|
+
return isProviderDefinition(value) ? value : undefined;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function isProviderDefinition(value: unknown): value is ProviderDefinition {
|
|
370
|
+
if (
|
|
371
|
+
!isRecord(value) ||
|
|
372
|
+
!isRecord(value.meta) ||
|
|
373
|
+
!isRecord(value.operations)
|
|
374
|
+
) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return (
|
|
379
|
+
typeof value.id === "string" &&
|
|
380
|
+
typeof value.version === "string" &&
|
|
381
|
+
(value.runtime === "standard" || value.runtime === "browser") &&
|
|
382
|
+
typeof value.meta.displayName === "string" &&
|
|
383
|
+
typeof value.meta.category === "string"
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
388
|
+
return typeof value === "object" && value !== null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function hasSafeParse(value: unknown): value is {
|
|
392
|
+
safeParse: (input: unknown) => SafeParseResult;
|
|
393
|
+
} {
|
|
394
|
+
return isRecord(value) && typeof value.safeParse === "function";
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function parseFixture(schema: z.ZodType, fixture: unknown): SafeParseResult {
|
|
398
|
+
return schema.safeParse(fixture);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (import.meta.main) {
|
|
402
|
+
await main().catch((error: unknown) => {
|
|
403
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
404
|
+
process.exit(1);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
@@ -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 { ProviderDefinition } from "../src";
|
|
8
|
+
import {
|
|
9
|
+
createBrowserClient,
|
|
10
|
+
createHttpClient,
|
|
11
|
+
createStateContext,
|
|
12
|
+
createTlsClient,
|
|
13
|
+
ProviderError,
|
|
14
|
+
} from "../src";
|
|
15
|
+
import type { AuthManager } from "../src/runtime/auth";
|
|
16
|
+
import { createTraceContext } from "../src/runtime/trace";
|
|
17
|
+
import type { ProviderContext, SessionStore } from "../src/types";
|
|
18
|
+
|
|
19
|
+
const HELP_TEXT = `Usage: apifuse dev [path]
|
|
20
|
+
Example: apifuse dev providers/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 providerPath = resolveProviderPath(args[0] ?? ".");
|
|
32
|
+
const providerModule = await import(resolve(providerPath, "index.ts"));
|
|
33
|
+
const provider = assertProviderDefinition(
|
|
34
|
+
providerModule.default,
|
|
35
|
+
providerPath,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const { startDevServer } = await import("../src/dev");
|
|
39
|
+
const port = Number(process.env.PORT) || 3900;
|
|
40
|
+
|
|
41
|
+
startDevServer(provider, { port });
|
|
42
|
+
|
|
43
|
+
console.log("\nEndpoints:");
|
|
44
|
+
console.log(` GET http://localhost:${port}/health`);
|
|
45
|
+
|
|
46
|
+
for (const operationId of Object.keys(provider.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 createProviderContext(
|
|
58
|
+
provider: ProviderDefinition,
|
|
59
|
+
session: SessionStore,
|
|
60
|
+
authManager: AuthManager,
|
|
61
|
+
): { ctx: ProviderContext } {
|
|
62
|
+
const ctx: ProviderContext = {
|
|
63
|
+
auth: authManager.createAuthContext(),
|
|
64
|
+
browser:
|
|
65
|
+
provider.runtime === "browser"
|
|
66
|
+
? createBrowserClient({
|
|
67
|
+
engine: provider.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: ProviderContext,
|
|
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 resolveProviderPath(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 provider 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(): ProviderContext["browser"] {
|
|
151
|
+
return {
|
|
152
|
+
engine: "playwright-stealth",
|
|
153
|
+
async newPage() {
|
|
154
|
+
throw new ProviderError(
|
|
155
|
+
"Browser runtime is not enabled for this provider",
|
|
156
|
+
{
|
|
157
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
158
|
+
fix: 'Set provider 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 assertProviderDefinition(
|
|
183
|
+
value: unknown,
|
|
184
|
+
providerPath: string,
|
|
185
|
+
): ProviderDefinition {
|
|
186
|
+
if (!isProviderDefinition(value)) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Expected ${resolve(providerPath, "index.ts")} to export default defineProvider(...)`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return value;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function isProviderDefinition(value: unknown): value is ProviderDefinition {
|
|
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
|
+
}
|