@apifuse/provider-sdk 2.0.0-beta.1 → 2.1.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/AUTHORING.md +93 -0
- package/CHANGELOG.md +21 -0
- package/README.md +133 -28
- package/bin/apifuse-check.ts +78 -71
- package/bin/apifuse-create.ts +12 -0
- package/bin/apifuse-dev.ts +24 -61
- package/bin/apifuse-pack-check.ts +87 -0
- package/bin/apifuse-pack-smoke.ts +122 -0
- package/bin/apifuse-perf.ts +33 -32
- package/bin/apifuse-record.ts +17 -7
- package/bin/apifuse-test.ts +6 -4
- package/bin/apifuse.ts +36 -35
- package/package.json +29 -9
- package/src/ceremonies/index.ts +768 -0
- package/src/cli/commands.ts +87 -0
- package/src/cli/create.ts +845 -0
- package/src/cli/templates/provider/Dockerfile.tpl +7 -0
- package/src/cli/templates/provider/README.md.tpl +41 -0
- package/src/cli/templates/provider/dev.ts.tpl +5 -0
- package/src/cli/templates/provider/index.test.ts.tpl +13 -0
- package/src/cli/templates/provider/index.ts.tpl +58 -0
- package/src/cli/templates/provider/start.ts.tpl +5 -0
- package/src/config/loader.ts +61 -1
- package/src/define.ts +565 -41
- package/src/dev.ts +2 -6
- package/src/errors.ts +42 -0
- package/src/index.ts +44 -38
- package/src/lint.ts +574 -0
- package/src/provider.ts +13 -0
- package/src/runtime/auth-flow.ts +67 -0
- package/src/runtime/credential.ts +95 -0
- package/src/runtime/env.ts +13 -0
- package/src/runtime/executor.ts +13 -14
- package/src/runtime/http.ts +36 -12
- package/src/runtime/insights.ts +3 -3
- package/src/runtime/key-derivation.ts +122 -0
- package/src/runtime/keyring.ts +148 -0
- package/src/runtime/namespace.ts +33 -0
- package/src/runtime/prevalidate.ts +252 -0
- package/src/runtime/tls.ts +41 -17
- package/src/runtime/waterfall.ts +0 -1
- package/src/schema.ts +77 -0
- package/src/serve.ts +1 -664
- package/src/server/index.ts +22 -0
- package/src/server/serve.ts +624 -0
- package/src/server/types.ts +78 -0
- package/src/stealth/profiles.ts +10 -93
- package/src/testing/run.ts +391 -32
- package/src/types.ts +390 -41
- package/bin/apifuse-init.ts +0 -387
- package/src/__tests__/auth.test.ts +0 -396
- package/src/__tests__/browser-auth.test.ts +0 -180
- package/src/__tests__/browser.test.ts +0 -632
- package/src/__tests__/define.test.ts +0 -225
- package/src/__tests__/errors.test.ts +0 -69
- package/src/__tests__/executor.test.ts +0 -214
- package/src/__tests__/http.test.ts +0 -238
- package/src/__tests__/insights.test.ts +0 -210
- package/src/__tests__/instrumentation.test.ts +0 -290
- package/src/__tests__/otlp.test.ts +0 -141
- package/src/__tests__/perf.test.ts +0 -60
- package/src/__tests__/providers-yaml.test.ts +0 -135
- package/src/__tests__/proxy.test.ts +0 -359
- package/src/__tests__/recipes.test.ts +0 -36
- package/src/__tests__/serve.test.ts +0 -233
- package/src/__tests__/session.test.ts +0 -231
- package/src/__tests__/state.test.ts +0 -100
- package/src/__tests__/stealth.test.ts +0 -57
- package/src/__tests__/testing.test.ts +0 -97
- package/src/__tests__/tls.test.ts +0 -345
- package/src/__tests__/types.test.ts +0 -142
- package/src/__tests__/utils.test.ts +0 -62
- package/src/__tests__/waterfall.test.ts +0 -270
- package/src/config/providers-yaml.ts +0 -370
- package/src/index.test.ts +0 -1
- package/src/protocol.ts +0 -183
- package/src/runtime/auth.ts +0 -245
- package/src/runtime/session.ts +0 -573
- package/src/runtime/state.ts +0 -124
package/src/serve.ts
CHANGED
|
@@ -1,664 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { type ZodType, z } from "zod";
|
|
3
|
-
|
|
4
|
-
import { ProviderError } from "./errors";
|
|
5
|
-
import type {
|
|
6
|
-
ConnectRequest,
|
|
7
|
-
ConnectResponse,
|
|
8
|
-
ContainerErrorResponse,
|
|
9
|
-
DisconnectRequest,
|
|
10
|
-
DisconnectResponse,
|
|
11
|
-
ExecuteRequest,
|
|
12
|
-
ExecuteResponse,
|
|
13
|
-
HealthResponse,
|
|
14
|
-
RefreshRequest,
|
|
15
|
-
RefreshResponse,
|
|
16
|
-
ResumeRequest,
|
|
17
|
-
ResumeResponse,
|
|
18
|
-
SchemaResponse,
|
|
19
|
-
} from "./protocol";
|
|
20
|
-
import { createBrowserClient } from "./runtime/browser";
|
|
21
|
-
import { executeOperation } from "./runtime/executor";
|
|
22
|
-
import { createHttpClient } from "./runtime/http";
|
|
23
|
-
import { getProviderBaseUrl } from "./runtime/provider";
|
|
24
|
-
import { createStateContext } from "./runtime/state";
|
|
25
|
-
import { createTlsClient } from "./runtime/tls";
|
|
26
|
-
import { createTraceContext, type TraceContext } from "./runtime/trace";
|
|
27
|
-
import type {
|
|
28
|
-
AuthContext,
|
|
29
|
-
BrowserClient,
|
|
30
|
-
ProviderContext,
|
|
31
|
-
ProviderDefinition,
|
|
32
|
-
SessionStore,
|
|
33
|
-
TlsClient,
|
|
34
|
-
} from "./types";
|
|
35
|
-
|
|
36
|
-
const AUTH_SESSION_KEY = "__auth__";
|
|
37
|
-
const DEFAULT_PORT = 3900;
|
|
38
|
-
|
|
39
|
-
type ErrorStatusCode = 400 | 404 | 500;
|
|
40
|
-
|
|
41
|
-
type SessionPatch = Record<string, string | null>;
|
|
42
|
-
|
|
43
|
-
type PendingAuthState = {
|
|
44
|
-
credentials: Record<string, string>;
|
|
45
|
-
resolvedFields: Record<string, string>;
|
|
46
|
-
requestedField: string;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
class PendingFieldError extends Error {
|
|
50
|
-
constructor(readonly field: string) {
|
|
51
|
-
super(`Pending auth field: ${field}`);
|
|
52
|
-
this.name = "PendingFieldError";
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function createSessionOverlay(snapshot: Record<string, string> = {}): {
|
|
57
|
-
store: SessionStore;
|
|
58
|
-
getPatch: () => SessionPatch;
|
|
59
|
-
} {
|
|
60
|
-
const data = new Map<string, string>(Object.entries(snapshot));
|
|
61
|
-
const patch: SessionPatch = {};
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
store: {
|
|
65
|
-
async get(key: string): Promise<string | null> {
|
|
66
|
-
return data.get(key) ?? null;
|
|
67
|
-
},
|
|
68
|
-
async set(key: string, value: string): Promise<void> {
|
|
69
|
-
data.set(key, value);
|
|
70
|
-
patch[key] = value;
|
|
71
|
-
},
|
|
72
|
-
async delete(key: string): Promise<void> {
|
|
73
|
-
data.delete(key);
|
|
74
|
-
patch[key] = null;
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
getPatch: () => ({ ...patch }),
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function createBrowserStub(): BrowserClient {
|
|
82
|
-
return {
|
|
83
|
-
engine: "playwright-stealth",
|
|
84
|
-
async newPage() {
|
|
85
|
-
throw new ProviderError("Browser runtime is not available", {
|
|
86
|
-
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
87
|
-
});
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function closeBrowserRuntime(browser: BrowserClient): Promise<void> {
|
|
93
|
-
if (!Reflect.has(browser as object, "close")) {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
await (browser as BrowserClient & { close(): Promise<void> }).close();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function createTlsStub(): TlsClient {
|
|
101
|
-
return {
|
|
102
|
-
async fetch() {
|
|
103
|
-
throw new ProviderError("TLS runtime is not available", {
|
|
104
|
-
code: "TLS_RUNTIME_UNSUPPORTED",
|
|
105
|
-
});
|
|
106
|
-
},
|
|
107
|
-
createSession() {
|
|
108
|
-
throw new ProviderError("TLS runtime is not available", {
|
|
109
|
-
code: "TLS_RUNTIME_UNSUPPORTED",
|
|
110
|
-
});
|
|
111
|
-
},
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function createAuthSessionMarker(
|
|
116
|
-
marker: Record<string, boolean | number>,
|
|
117
|
-
): string {
|
|
118
|
-
return JSON.stringify(marker);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function toSessionSnapshot(
|
|
122
|
-
snapshot: Record<string, string> | undefined,
|
|
123
|
-
): Record<string, string> {
|
|
124
|
-
return snapshot ?? {};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function toStringRecord(value: unknown): Record<string, string> {
|
|
128
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
129
|
-
return {};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return Object.fromEntries(
|
|
133
|
-
Object.entries(value).map(([key, entryValue]) => [key, String(entryValue)]),
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function toErrorEnvelope(
|
|
138
|
-
error: unknown,
|
|
139
|
-
overrides?: Partial<ContainerErrorResponse["error"]>,
|
|
140
|
-
): ContainerErrorResponse {
|
|
141
|
-
const providerError = error instanceof ProviderError ? error : null;
|
|
142
|
-
const fallbackMessage =
|
|
143
|
-
error instanceof Error ? error.message : "Unknown error";
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
error: {
|
|
147
|
-
code: overrides?.code ?? providerError?.code ?? "INTERNAL_ERROR",
|
|
148
|
-
message: overrides?.message ?? fallbackMessage,
|
|
149
|
-
...(overrides?.details !== undefined
|
|
150
|
-
? { details: overrides.details }
|
|
151
|
-
: providerError?.options?.fix
|
|
152
|
-
? { details: { fix: providerError.options.fix } }
|
|
153
|
-
: {}),
|
|
154
|
-
},
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function getStatusCode(error: unknown): ErrorStatusCode {
|
|
159
|
-
if (error instanceof ProviderError && error.code === "NOT_FOUND") {
|
|
160
|
-
return 404;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (error instanceof z.ZodError) {
|
|
164
|
-
return 400;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return 500;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function getFieldLabel(
|
|
171
|
-
provider: ProviderDefinition,
|
|
172
|
-
fieldName: string,
|
|
173
|
-
): string | undefined {
|
|
174
|
-
return provider.auth?.fields?.find((field) => field.name === fieldName)
|
|
175
|
-
?.label;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function toConnectFailedResponse(error: unknown): ConnectResponse {
|
|
179
|
-
return {
|
|
180
|
-
status: "failed",
|
|
181
|
-
error:
|
|
182
|
-
error instanceof ProviderError
|
|
183
|
-
? (error.code ?? "AUTH_FAILED")
|
|
184
|
-
: "AUTH_FAILED",
|
|
185
|
-
message: error instanceof Error ? error.message : "Authentication failed",
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function toRefreshFailedResponse(error: unknown): RefreshResponse {
|
|
190
|
-
return {
|
|
191
|
-
status: "failed",
|
|
192
|
-
error:
|
|
193
|
-
error instanceof ProviderError
|
|
194
|
-
? (error.code ?? "REFRESH_FAILED")
|
|
195
|
-
: "REFRESH_FAILED",
|
|
196
|
-
message: error instanceof Error ? error.message : "Refresh failed",
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function toDisconnectFailedResponse(error: unknown): DisconnectResponse {
|
|
201
|
-
return {
|
|
202
|
-
status: "failed",
|
|
203
|
-
error:
|
|
204
|
-
error instanceof ProviderError
|
|
205
|
-
? (error.code ?? "DISCONNECT_FAILED")
|
|
206
|
-
: error instanceof Error
|
|
207
|
-
? error.message
|
|
208
|
-
: "DISCONNECT_FAILED",
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function toSchema(schema: ZodType): Record<string, unknown> {
|
|
213
|
-
const zodModule = z as typeof z & {
|
|
214
|
-
toJSONSchema?: (schema: ZodType) => Record<string, unknown>;
|
|
215
|
-
};
|
|
216
|
-
const schemaWithMethod = schema as ZodType & {
|
|
217
|
-
toJSONSchema?: () => Record<string, unknown>;
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
if (typeof zodModule.toJSONSchema === "function") {
|
|
221
|
-
return zodModule.toJSONSchema(schema);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (typeof schemaWithMethod.toJSONSchema === "function") {
|
|
225
|
-
return schemaWithMethod.toJSONSchema();
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
throw new ProviderError("Zod JSON Schema conversion is unavailable", {
|
|
229
|
-
code: "SCHEMA_CONVERSION_UNAVAILABLE",
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function getTracePayload(
|
|
234
|
-
trace: TraceContext,
|
|
235
|
-
): ExecuteResponse["trace"] | undefined {
|
|
236
|
-
const spans = trace.getSpans();
|
|
237
|
-
return spans.length > 0 ? { spans } : undefined;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function toStringSessionPatch(
|
|
241
|
-
sessionPatch: SessionPatch,
|
|
242
|
-
): Record<string, string> {
|
|
243
|
-
return Object.fromEntries(
|
|
244
|
-
Object.entries(sessionPatch).filter(
|
|
245
|
-
(entry): entry is [string, string] => entry[1] !== null,
|
|
246
|
-
),
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function assertAuthConfigured(
|
|
251
|
-
provider: ProviderDefinition,
|
|
252
|
-
): asserts provider is ProviderDefinition & {
|
|
253
|
-
auth: NonNullable<ProviderDefinition["auth"]>;
|
|
254
|
-
} {
|
|
255
|
-
if (!provider.auth || provider.auth.mode === "none") {
|
|
256
|
-
throw new ProviderError("Auth is not configured", {
|
|
257
|
-
code: "AUTH_NOT_CONFIGURED",
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (!provider.auth.exchange) {
|
|
262
|
-
throw new ProviderError("Auth exchange is not configured", {
|
|
263
|
-
code: "AUTH_EXCHANGE_NOT_CONFIGURED",
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function createContainerContext(
|
|
269
|
-
provider: ProviderDefinition,
|
|
270
|
-
sessionSnapshot: Record<string, string> = {},
|
|
271
|
-
options: {
|
|
272
|
-
auth?: AuthContext;
|
|
273
|
-
baseUrl?: string;
|
|
274
|
-
stateSecret?: string;
|
|
275
|
-
} = {},
|
|
276
|
-
): {
|
|
277
|
-
ctx: ProviderContext;
|
|
278
|
-
getSessionPatch: () => SessionPatch;
|
|
279
|
-
close: () => Promise<void>;
|
|
280
|
-
trace: TraceContext;
|
|
281
|
-
} {
|
|
282
|
-
const overlay = createSessionOverlay(sessionSnapshot);
|
|
283
|
-
const trace = createTraceContext({ onSpan: () => {} });
|
|
284
|
-
const resolvedBaseUrl = options.baseUrl ?? getProviderBaseUrl(provider);
|
|
285
|
-
const browser =
|
|
286
|
-
provider.runtime === "browser"
|
|
287
|
-
? createBrowserClient({
|
|
288
|
-
cdpUrl: process.env.CDP_POOL_URL ?? process.env.APIFUSE_CDP_POOL_URL,
|
|
289
|
-
headless: true,
|
|
290
|
-
stealth: true,
|
|
291
|
-
engine: provider.browser?.engine,
|
|
292
|
-
})
|
|
293
|
-
: createBrowserStub();
|
|
294
|
-
const tls = resolvedBaseUrl
|
|
295
|
-
? createTlsClient(resolvedBaseUrl)
|
|
296
|
-
: createTlsStub();
|
|
297
|
-
|
|
298
|
-
const ctx: ProviderContext = {
|
|
299
|
-
http: createHttpClient(resolvedBaseUrl),
|
|
300
|
-
tls,
|
|
301
|
-
browser,
|
|
302
|
-
session: overlay.store,
|
|
303
|
-
state: createStateContext(options.stateSecret),
|
|
304
|
-
trace,
|
|
305
|
-
auth:
|
|
306
|
-
options.auth ??
|
|
307
|
-
({
|
|
308
|
-
async requestField(name: string): Promise<string> {
|
|
309
|
-
throw new ProviderError(`Deferred auth field requested: ${name}`, {
|
|
310
|
-
code: "AUTH_FIELD_REQUESTED",
|
|
311
|
-
});
|
|
312
|
-
},
|
|
313
|
-
} satisfies AuthContext),
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
return {
|
|
317
|
-
ctx,
|
|
318
|
-
getSessionPatch: overlay.getPatch,
|
|
319
|
-
close: async () => await closeBrowserRuntime(browser),
|
|
320
|
-
trace,
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
async function runAuthExchange(
|
|
325
|
-
provider: ProviderDefinition,
|
|
326
|
-
credentials: Record<string, string>,
|
|
327
|
-
sessionSnapshot: Record<string, string>,
|
|
328
|
-
resolvedFields: Record<string, string>,
|
|
329
|
-
options: {
|
|
330
|
-
baseUrl?: string;
|
|
331
|
-
stateSecret?: string;
|
|
332
|
-
},
|
|
333
|
-
): Promise<
|
|
334
|
-
| { type: "success"; sessionPatch: Record<string, string> }
|
|
335
|
-
| {
|
|
336
|
-
type: "pending_field";
|
|
337
|
-
field: string;
|
|
338
|
-
fieldLabel?: string;
|
|
339
|
-
resumeToken: string;
|
|
340
|
-
sessionPatch?: Record<string, string>;
|
|
341
|
-
}
|
|
342
|
-
> {
|
|
343
|
-
assertAuthConfigured(provider);
|
|
344
|
-
const auth = provider.auth as NonNullable<ProviderDefinition["auth"]> & {
|
|
345
|
-
exchange: NonNullable<NonNullable<ProviderDefinition["auth"]>["exchange"]>;
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
let requestedField: string | null = null;
|
|
349
|
-
const { ctx, getSessionPatch, close } = createContainerContext(
|
|
350
|
-
provider,
|
|
351
|
-
sessionSnapshot,
|
|
352
|
-
{
|
|
353
|
-
baseUrl: options.baseUrl,
|
|
354
|
-
stateSecret: options.stateSecret,
|
|
355
|
-
auth: {
|
|
356
|
-
async requestField(name: string): Promise<string> {
|
|
357
|
-
const resolvedValue = resolvedFields[name];
|
|
358
|
-
if (resolvedValue !== undefined) {
|
|
359
|
-
return resolvedValue;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
requestedField = name;
|
|
363
|
-
throw new PendingFieldError(name);
|
|
364
|
-
},
|
|
365
|
-
},
|
|
366
|
-
},
|
|
367
|
-
);
|
|
368
|
-
|
|
369
|
-
try {
|
|
370
|
-
await auth.exchange(ctx, credentials);
|
|
371
|
-
await ctx.session.set(
|
|
372
|
-
AUTH_SESSION_KEY,
|
|
373
|
-
createAuthSessionMarker({ authenticated: true, timestamp: Date.now() }),
|
|
374
|
-
);
|
|
375
|
-
return {
|
|
376
|
-
type: "success",
|
|
377
|
-
sessionPatch: toStringSessionPatch(getSessionPatch()),
|
|
378
|
-
};
|
|
379
|
-
} catch (error) {
|
|
380
|
-
if (error instanceof PendingFieldError && requestedField) {
|
|
381
|
-
const resumeToken = await ctx.state.seal(
|
|
382
|
-
{
|
|
383
|
-
credentials,
|
|
384
|
-
resolvedFields,
|
|
385
|
-
requestedField,
|
|
386
|
-
} satisfies PendingAuthState,
|
|
387
|
-
{ ttl: "15m" },
|
|
388
|
-
);
|
|
389
|
-
|
|
390
|
-
const sessionPatch = toStringSessionPatch(getSessionPatch());
|
|
391
|
-
return {
|
|
392
|
-
type: "pending_field",
|
|
393
|
-
field: requestedField,
|
|
394
|
-
fieldLabel: getFieldLabel(provider, requestedField),
|
|
395
|
-
resumeToken,
|
|
396
|
-
...(Object.keys(sessionPatch).length > 0 ? { sessionPatch } : {}),
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
throw error;
|
|
401
|
-
} finally {
|
|
402
|
-
await close();
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
async function parseJsonBody<T>(request: Request): Promise<T> {
|
|
407
|
-
try {
|
|
408
|
-
return (await request.json()) as T;
|
|
409
|
-
} catch (error) {
|
|
410
|
-
throw new ProviderError("Invalid JSON body", {
|
|
411
|
-
code: "INVALID_JSON",
|
|
412
|
-
cause: error instanceof Error ? error : undefined,
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
export function createProviderServer(
|
|
418
|
-
provider: ProviderDefinition,
|
|
419
|
-
options: {
|
|
420
|
-
port?: number;
|
|
421
|
-
baseUrl?: string;
|
|
422
|
-
stateSecret?: string;
|
|
423
|
-
sessionDbPath?: string;
|
|
424
|
-
} = {},
|
|
425
|
-
): { app: Hono; start: () => void } {
|
|
426
|
-
const startedAt = Date.now();
|
|
427
|
-
const app = new Hono();
|
|
428
|
-
|
|
429
|
-
app.post("/execute/:operationId", async (c) => {
|
|
430
|
-
try {
|
|
431
|
-
const operationId = c.req.param("operationId");
|
|
432
|
-
const body = await parseJsonBody<ExecuteRequest>(c.req.raw);
|
|
433
|
-
const { ctx, getSessionPatch, trace, close } = createContainerContext(
|
|
434
|
-
provider,
|
|
435
|
-
toSessionSnapshot(body.session),
|
|
436
|
-
{ baseUrl: options.baseUrl, stateSecret: options.stateSecret },
|
|
437
|
-
);
|
|
438
|
-
|
|
439
|
-
try {
|
|
440
|
-
const data = await executeOperation(
|
|
441
|
-
provider,
|
|
442
|
-
operationId,
|
|
443
|
-
ctx,
|
|
444
|
-
body.input,
|
|
445
|
-
);
|
|
446
|
-
const response: ExecuteResponse = {
|
|
447
|
-
data,
|
|
448
|
-
sessionPatch: getSessionPatch(),
|
|
449
|
-
...(getTracePayload(trace) ? { trace: getTracePayload(trace) } : {}),
|
|
450
|
-
};
|
|
451
|
-
|
|
452
|
-
return c.json(response);
|
|
453
|
-
} finally {
|
|
454
|
-
await close();
|
|
455
|
-
}
|
|
456
|
-
} catch (error) {
|
|
457
|
-
return c.json(toErrorEnvelope(error), getStatusCode(error));
|
|
458
|
-
}
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
app.post("/connect", async (c) => {
|
|
462
|
-
try {
|
|
463
|
-
const body = await parseJsonBody<ConnectRequest>(c.req.raw);
|
|
464
|
-
const result = await runAuthExchange(
|
|
465
|
-
provider,
|
|
466
|
-
toStringRecord(body.credentials),
|
|
467
|
-
toSessionSnapshot(body.session),
|
|
468
|
-
{},
|
|
469
|
-
{ baseUrl: options.baseUrl, stateSecret: options.stateSecret },
|
|
470
|
-
);
|
|
471
|
-
|
|
472
|
-
const response: ConnectResponse =
|
|
473
|
-
result.type === "success"
|
|
474
|
-
? {
|
|
475
|
-
status: "success",
|
|
476
|
-
sessionPatch: result.sessionPatch,
|
|
477
|
-
}
|
|
478
|
-
: {
|
|
479
|
-
status: "pending_field",
|
|
480
|
-
field: result.field,
|
|
481
|
-
resumeToken: result.resumeToken,
|
|
482
|
-
...(result.fieldLabel ? { fieldLabel: result.fieldLabel } : {}),
|
|
483
|
-
...(result.sessionPatch
|
|
484
|
-
? { sessionPatch: result.sessionPatch }
|
|
485
|
-
: {}),
|
|
486
|
-
};
|
|
487
|
-
|
|
488
|
-
return c.json(response);
|
|
489
|
-
} catch (error) {
|
|
490
|
-
return c.json(toConnectFailedResponse(error));
|
|
491
|
-
}
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
app.post("/connect/resume", async (c) => {
|
|
495
|
-
try {
|
|
496
|
-
const body = await parseJsonBody<ResumeRequest>(c.req.raw);
|
|
497
|
-
const state = createStateContext(options.stateSecret);
|
|
498
|
-
const pendingState = await state.unseal<PendingAuthState>(
|
|
499
|
-
body.resumeToken,
|
|
500
|
-
);
|
|
501
|
-
|
|
502
|
-
if (!pendingState) {
|
|
503
|
-
const response: ResumeResponse = {
|
|
504
|
-
status: "failed",
|
|
505
|
-
error: "INVALID_RESUME_TOKEN",
|
|
506
|
-
message: "Resume token is invalid or expired",
|
|
507
|
-
};
|
|
508
|
-
return c.json(response);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
const result = await runAuthExchange(
|
|
512
|
-
provider,
|
|
513
|
-
pendingState.credentials,
|
|
514
|
-
toSessionSnapshot(body.session),
|
|
515
|
-
{
|
|
516
|
-
...pendingState.resolvedFields,
|
|
517
|
-
[pendingState.requestedField]: body.fieldValue,
|
|
518
|
-
},
|
|
519
|
-
{ baseUrl: options.baseUrl, stateSecret: options.stateSecret },
|
|
520
|
-
);
|
|
521
|
-
|
|
522
|
-
const response: ResumeResponse =
|
|
523
|
-
result.type === "success"
|
|
524
|
-
? {
|
|
525
|
-
status: "success",
|
|
526
|
-
sessionPatch: result.sessionPatch,
|
|
527
|
-
}
|
|
528
|
-
: {
|
|
529
|
-
status: "pending_field",
|
|
530
|
-
field: result.field,
|
|
531
|
-
resumeToken: result.resumeToken,
|
|
532
|
-
...(result.fieldLabel ? { fieldLabel: result.fieldLabel } : {}),
|
|
533
|
-
...(result.sessionPatch
|
|
534
|
-
? { sessionPatch: result.sessionPatch }
|
|
535
|
-
: {}),
|
|
536
|
-
};
|
|
537
|
-
|
|
538
|
-
return c.json(response);
|
|
539
|
-
} catch (error) {
|
|
540
|
-
return c.json(toConnectFailedResponse(error));
|
|
541
|
-
}
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
app.post("/refresh", async (c) => {
|
|
545
|
-
try {
|
|
546
|
-
if (!provider.auth?.refresh) {
|
|
547
|
-
throw new ProviderError("Auth refresh is not configured", {
|
|
548
|
-
code: "AUTH_REFRESH_NOT_CONFIGURED",
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
const body = await parseJsonBody<RefreshRequest>(c.req.raw);
|
|
553
|
-
const { ctx, getSessionPatch, close } = createContainerContext(
|
|
554
|
-
provider,
|
|
555
|
-
body.session,
|
|
556
|
-
{ baseUrl: options.baseUrl, stateSecret: options.stateSecret },
|
|
557
|
-
);
|
|
558
|
-
|
|
559
|
-
try {
|
|
560
|
-
await provider.auth.refresh(ctx);
|
|
561
|
-
await ctx.session.set(
|
|
562
|
-
AUTH_SESSION_KEY,
|
|
563
|
-
createAuthSessionMarker({
|
|
564
|
-
authenticated: true,
|
|
565
|
-
refreshed: true,
|
|
566
|
-
timestamp: Date.now(),
|
|
567
|
-
}),
|
|
568
|
-
);
|
|
569
|
-
const response: RefreshResponse = {
|
|
570
|
-
status: "success",
|
|
571
|
-
sessionPatch: toStringSessionPatch(getSessionPatch()),
|
|
572
|
-
};
|
|
573
|
-
|
|
574
|
-
return c.json(response);
|
|
575
|
-
} finally {
|
|
576
|
-
await close();
|
|
577
|
-
}
|
|
578
|
-
} catch (error) {
|
|
579
|
-
return c.json(toRefreshFailedResponse(error));
|
|
580
|
-
}
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
app.post("/disconnect", async (c) => {
|
|
584
|
-
try {
|
|
585
|
-
const body = await parseJsonBody<DisconnectRequest>(c.req.raw);
|
|
586
|
-
const { ctx, close } = createContainerContext(provider, body.session, {
|
|
587
|
-
baseUrl: options.baseUrl,
|
|
588
|
-
stateSecret: options.stateSecret,
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
try {
|
|
592
|
-
if (provider.auth?.disconnect) {
|
|
593
|
-
await provider.auth.disconnect(ctx);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
await ctx.session.delete(AUTH_SESSION_KEY);
|
|
597
|
-
const response: DisconnectResponse = { status: "success" };
|
|
598
|
-
return c.json(response);
|
|
599
|
-
} finally {
|
|
600
|
-
await close();
|
|
601
|
-
}
|
|
602
|
-
} catch (error) {
|
|
603
|
-
return c.json(toDisconnectFailedResponse(error));
|
|
604
|
-
}
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
app.get("/health", (c) => {
|
|
608
|
-
const response: HealthResponse = {
|
|
609
|
-
status: "ok",
|
|
610
|
-
provider: provider.id,
|
|
611
|
-
version: provider.version,
|
|
612
|
-
uptime: Date.now() - startedAt,
|
|
613
|
-
};
|
|
614
|
-
|
|
615
|
-
return c.json(response);
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
app.get("/schema/:operationId", (c) => {
|
|
619
|
-
try {
|
|
620
|
-
const operationId = c.req.param("operationId");
|
|
621
|
-
const operation = provider.operations[operationId];
|
|
622
|
-
|
|
623
|
-
if (!operation) {
|
|
624
|
-
throw new ProviderError(
|
|
625
|
-
`Unknown operation: ${provider.id}/${operationId}`,
|
|
626
|
-
{
|
|
627
|
-
code: "NOT_FOUND",
|
|
628
|
-
fix: `Valid operations: ${Object.keys(provider.operations).join(", ")}`,
|
|
629
|
-
},
|
|
630
|
-
);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
const response: SchemaResponse = {
|
|
634
|
-
operationId,
|
|
635
|
-
...(operation.description
|
|
636
|
-
? { description: operation.description }
|
|
637
|
-
: {}),
|
|
638
|
-
input: toSchema(operation.input),
|
|
639
|
-
output: toSchema(operation.output),
|
|
640
|
-
...(operation.hints ? { hints: operation.hints } : {}),
|
|
641
|
-
};
|
|
642
|
-
|
|
643
|
-
return c.json(response);
|
|
644
|
-
} catch (error) {
|
|
645
|
-
return c.json(toErrorEnvelope(error), getStatusCode(error));
|
|
646
|
-
}
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
return {
|
|
650
|
-
app,
|
|
651
|
-
start() {
|
|
652
|
-
if (typeof Bun === "undefined") {
|
|
653
|
-
throw new ProviderError("Bun runtime is required to start the server", {
|
|
654
|
-
code: "RUNTIME_UNSUPPORTED",
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
Bun.serve({
|
|
659
|
-
port: options.port ?? DEFAULT_PORT,
|
|
660
|
-
fetch: app.fetch,
|
|
661
|
-
});
|
|
662
|
-
},
|
|
663
|
-
};
|
|
664
|
-
}
|
|
1
|
+
export { createServerApp, type ServeOptions, serve } from "./server/serve";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { createServerApp, type ServeOptions, serve } from "./serve";
|
|
2
|
+
export type {
|
|
3
|
+
AuthFlowRequest,
|
|
4
|
+
AuthFlowResponse,
|
|
5
|
+
AuthFlowSuccessResponse,
|
|
6
|
+
ConnectionMode,
|
|
7
|
+
OperationConnection,
|
|
8
|
+
OperationErrorResponse,
|
|
9
|
+
OperationRequest,
|
|
10
|
+
OperationResponse,
|
|
11
|
+
OperationSuccessResponse,
|
|
12
|
+
} from "./types";
|
|
13
|
+
export {
|
|
14
|
+
AuthFlowRequestSchema,
|
|
15
|
+
AuthFlowSuccessResponseSchema,
|
|
16
|
+
ConnectionModeSchema,
|
|
17
|
+
ErrorEnvelopeSchema,
|
|
18
|
+
OperationConnectionSchema,
|
|
19
|
+
OperationErrorResponseSchema,
|
|
20
|
+
OperationRequestSchema,
|
|
21
|
+
OperationSuccessResponseSchema,
|
|
22
|
+
} from "./types";
|