@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
@@ -0,0 +1,20 @@
1
+ import type { ConnectorDefinition } from "../types";
2
+
3
+ export function getConnectorBaseUrl(
4
+ connector: ConnectorDefinition,
5
+ ): string | undefined {
6
+ const operations = Object.values(
7
+ connector.operations as Record<string, unknown>,
8
+ );
9
+
10
+ for (const operation of operations) {
11
+ const baseUrl = (operation as { upstream?: { baseUrl?: unknown } }).upstream
12
+ ?.baseUrl;
13
+
14
+ if (typeof baseUrl === "string" && baseUrl.length > 0) {
15
+ return baseUrl;
16
+ }
17
+ }
18
+
19
+ return undefined;
20
+ }
@@ -0,0 +1,51 @@
1
+ import { ConnectorError } from "../errors";
2
+ import type { ConnectorContext, ConnectorDefinition } from "../types";
3
+
4
+ import { createAuthManager } from "./auth";
5
+
6
+ /**
7
+ * Execute a connector operation by calling its handler.
8
+ *
9
+ * SDK auto-wraps every handler call with:
10
+ * 1. Input Zod validation
11
+ * 2. Auth auto-refresh (if auth configured)
12
+ * 3. Trace span
13
+ * 4. Output Zod validation
14
+ *
15
+ * @see openspec/connector-sdk/03-sdk-core.md §3.6
16
+ */
17
+ export async function executeOperation(
18
+ connector: ConnectorDefinition,
19
+ operationId: string,
20
+ ctx: ConnectorContext,
21
+ input: unknown,
22
+ ): Promise<unknown> {
23
+ const operation = connector.operations[operationId];
24
+
25
+ if (!operation) {
26
+ throw new ConnectorError(
27
+ `Unknown operation: ${connector.id}/${operationId}`,
28
+ {
29
+ code: "NOT_FOUND",
30
+ fix: `Valid operations: ${Object.keys(connector.operations).join(", ")}`,
31
+ },
32
+ );
33
+ }
34
+
35
+ const validatedInput = operation.input.parse(input);
36
+
37
+ const execute = () =>
38
+ ctx.trace.span(`handler:${operationId}`, () =>
39
+ operation.handler(ctx, validatedInput),
40
+ );
41
+
42
+ const result =
43
+ connector.auth && connector.auth.mode !== "none"
44
+ ? await createAuthManager(
45
+ connector.auth,
46
+ ctx.session,
47
+ ).wrapWithAutoRefresh(ctx, execute)
48
+ : await execute();
49
+
50
+ return operation.output.parse(result);
51
+ }
@@ -0,0 +1,248 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ import type { ProxyResolutionOptions } from "../config/loader";
4
+ import { resolveProxyConfig } from "../config/loader";
5
+ import { TransportError } from "../errors";
6
+ import { generateLayer2Headers } from "../stealth/profiles";
7
+ import type {
8
+ HttpClient,
9
+ HttpResponse,
10
+ RequestOptions,
11
+ RequestWithMethodOptions,
12
+ StealthProfile,
13
+ } from "../types";
14
+
15
+ const require = createRequire(import.meta.url);
16
+
17
+ const MISSING_PROXY_WARNING =
18
+ "[connector-sdk] Connector requested proxy routing, but no proxy URL was configured. Continuing without proxy.";
19
+
20
+ type FetchProxyInit = RequestInit & {
21
+ dispatcher?: unknown;
22
+ proxy?: string;
23
+ };
24
+
25
+ type UndiciModule = {
26
+ ProxyAgent?: new (proxyUrl: string) => unknown;
27
+ };
28
+
29
+ export type HttpClientOptions = ProxyResolutionOptions & {
30
+ warn?: (message: string) => void;
31
+ stealthProfile?: StealthProfile;
32
+ userAgent?: string;
33
+ };
34
+
35
+ function buildUrl(url: string, params?: Record<string, string>): string {
36
+ if (!params || Object.keys(params).length === 0) {
37
+ return url;
38
+ }
39
+
40
+ const searchParams = new URLSearchParams(params);
41
+ return `${url}?${searchParams.toString()}`;
42
+ }
43
+
44
+ function resolveUrl(baseUrl: string | undefined, url: string): string {
45
+ if (!baseUrl) {
46
+ return url;
47
+ }
48
+
49
+ return new URL(url, baseUrl).toString();
50
+ }
51
+
52
+ async function doRequest(
53
+ baseUrl: string | undefined,
54
+ url: string,
55
+ method: string,
56
+ proxy: string | undefined,
57
+ options: RequestOptions & { body?: unknown } = {},
58
+ ): Promise<HttpResponse> {
59
+ const { headers, params, timeout, body } = options;
60
+
61
+ const controller = new AbortController();
62
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
63
+
64
+ if (timeout) {
65
+ timeoutId = setTimeout(() => controller.abort(), timeout);
66
+ }
67
+
68
+ try {
69
+ const requestUrl = buildUrl(resolveUrl(baseUrl, url), params);
70
+ const proxyInit = createProxyInit(proxy);
71
+ const response = await fetch(requestUrl, {
72
+ ...proxyInit,
73
+ method,
74
+ headers: {
75
+ "Content-Type": "application/json",
76
+ ...headers,
77
+ },
78
+ body: body !== undefined ? JSON.stringify(body) : undefined,
79
+ signal: controller.signal,
80
+ });
81
+
82
+ if (!response.ok) {
83
+ const text = await response.text().catch(() => "");
84
+ throw new TransportError(
85
+ `HTTP ${response.status} ${response.statusText}: ${requestUrl}`,
86
+ {
87
+ status: response.status,
88
+ fix: `Check the endpoint URL and request parameters. Response: ${text.slice(0, 200)}`,
89
+ },
90
+ );
91
+ }
92
+
93
+ const contentType = response.headers.get("content-type") ?? "";
94
+ const rawText = await response.text();
95
+ let data: unknown;
96
+
97
+ if (contentType.includes("application/json")) {
98
+ data = rawText ? (JSON.parse(rawText) as unknown) : null;
99
+ } else {
100
+ data = rawText;
101
+ }
102
+
103
+ return {
104
+ data,
105
+ headers: Object.fromEntries(response.headers.entries()),
106
+ json: async <T = unknown>() =>
107
+ contentType.includes("application/json")
108
+ ? (data as T)
109
+ : (JSON.parse(rawText) as T),
110
+ ok: response.ok,
111
+ status: response.status,
112
+ text: async () => rawText,
113
+ };
114
+ } catch (error) {
115
+ if (error instanceof TransportError) {
116
+ throw error;
117
+ }
118
+
119
+ if (error instanceof Error && error.name === "AbortError") {
120
+ throw new TransportError(
121
+ `Request timed out: ${resolveUrl(baseUrl, url)}`,
122
+ {
123
+ fix: `Increase timeout option (current: ${timeout}ms)`,
124
+ },
125
+ );
126
+ }
127
+
128
+ throw new TransportError(`Network error: ${String(error)}`, {
129
+ cause: error instanceof Error ? error : undefined,
130
+ });
131
+ } finally {
132
+ if (timeoutId) {
133
+ clearTimeout(timeoutId);
134
+ }
135
+ }
136
+ }
137
+
138
+ function createProxyInit(
139
+ proxy?: string,
140
+ ): Pick<FetchProxyInit, "dispatcher" | "proxy"> {
141
+ if (!proxy) {
142
+ return {};
143
+ }
144
+
145
+ if (typeof Bun !== "undefined") {
146
+ return { proxy };
147
+ }
148
+
149
+ try {
150
+ const undici = require("undici") as UndiciModule;
151
+ if (typeof undici.ProxyAgent === "function") {
152
+ return {
153
+ dispatcher: new undici.ProxyAgent(
154
+ proxy,
155
+ ) as FetchProxyInit["dispatcher"],
156
+ };
157
+ }
158
+ } catch {
159
+ // Fall through to a generic fetch init extension when undici is unavailable.
160
+ }
161
+
162
+ return { proxy };
163
+ }
164
+
165
+ export function createHttpClient(
166
+ baseUrl?: string,
167
+ clientOptions: HttpClientOptions = {},
168
+ ): HttpClient {
169
+ let hasWarnedMissingProxy = false;
170
+ const warn = clientOptions.warn ?? console.warn;
171
+ const stealthProfile = clientOptions.stealthProfile;
172
+ const userAgent = clientOptions.userAgent ?? stealthProfile?.userAgent;
173
+
174
+ function withClientHeaders(
175
+ options?: RequestOptions,
176
+ ): RequestOptions | undefined {
177
+ const layer2Headers = stealthProfile
178
+ ? generateLayer2Headers(stealthProfile)
179
+ : undefined;
180
+
181
+ if (!userAgent && !layer2Headers) {
182
+ return options;
183
+ }
184
+
185
+ return {
186
+ ...options,
187
+ headers: {
188
+ ...(userAgent ? { "User-Agent": userAgent } : {}),
189
+ ...layer2Headers,
190
+ ...options?.headers,
191
+ },
192
+ };
193
+ }
194
+
195
+ function resolveRequestProxy(
196
+ requestOptions?: RequestOptions,
197
+ ): string | undefined {
198
+ const resolvedProxy = resolveProxyConfig({
199
+ proxy: requestOptions?.proxy ?? clientOptions.proxy,
200
+ upstream: clientOptions.upstream,
201
+ apifuseConfig: clientOptions.apifuseConfig,
202
+ });
203
+
204
+ if (resolvedProxy.shouldWarn && !hasWarnedMissingProxy) {
205
+ hasWarnedMissingProxy = true;
206
+ warn(MISSING_PROXY_WARNING);
207
+ }
208
+
209
+ return resolvedProxy.url;
210
+ }
211
+
212
+ return {
213
+ request: (url, options: RequestWithMethodOptions = {}) =>
214
+ doRequest(
215
+ baseUrl,
216
+ url,
217
+ options.method ?? "GET",
218
+ resolveRequestProxy(options),
219
+ withClientHeaders(options),
220
+ ),
221
+ get: (url, options) =>
222
+ doRequest(
223
+ baseUrl,
224
+ url,
225
+ "GET",
226
+ resolveRequestProxy(options),
227
+ withClientHeaders(options),
228
+ ),
229
+ post: (url, body, options) =>
230
+ doRequest(baseUrl, url, "POST", resolveRequestProxy(options), {
231
+ ...withClientHeaders(options),
232
+ body,
233
+ }),
234
+ put: (url, body, options) =>
235
+ doRequest(baseUrl, url, "PUT", resolveRequestProxy(options), {
236
+ ...withClientHeaders(options),
237
+ body,
238
+ }),
239
+ delete: (url, options) =>
240
+ doRequest(
241
+ baseUrl,
242
+ url,
243
+ "DELETE",
244
+ resolveRequestProxy(options),
245
+ withClientHeaders(options),
246
+ ),
247
+ };
248
+ }