@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.
Files changed (79) hide show
  1. package/AUTHORING.md +93 -0
  2. package/CHANGELOG.md +21 -0
  3. package/README.md +133 -28
  4. package/bin/apifuse-check.ts +78 -71
  5. package/bin/apifuse-create.ts +12 -0
  6. package/bin/apifuse-dev.ts +24 -61
  7. package/bin/apifuse-pack-check.ts +87 -0
  8. package/bin/apifuse-pack-smoke.ts +122 -0
  9. package/bin/apifuse-perf.ts +33 -32
  10. package/bin/apifuse-record.ts +17 -7
  11. package/bin/apifuse-test.ts +6 -4
  12. package/bin/apifuse.ts +36 -35
  13. package/package.json +29 -9
  14. package/src/ceremonies/index.ts +768 -0
  15. package/src/cli/commands.ts +87 -0
  16. package/src/cli/create.ts +845 -0
  17. package/src/cli/templates/provider/Dockerfile.tpl +7 -0
  18. package/src/cli/templates/provider/README.md.tpl +41 -0
  19. package/src/cli/templates/provider/dev.ts.tpl +5 -0
  20. package/src/cli/templates/provider/index.test.ts.tpl +13 -0
  21. package/src/cli/templates/provider/index.ts.tpl +58 -0
  22. package/src/cli/templates/provider/start.ts.tpl +5 -0
  23. package/src/config/loader.ts +61 -1
  24. package/src/define.ts +565 -41
  25. package/src/dev.ts +2 -6
  26. package/src/errors.ts +42 -0
  27. package/src/index.ts +44 -38
  28. package/src/lint.ts +574 -0
  29. package/src/provider.ts +13 -0
  30. package/src/runtime/auth-flow.ts +67 -0
  31. package/src/runtime/credential.ts +95 -0
  32. package/src/runtime/env.ts +13 -0
  33. package/src/runtime/executor.ts +13 -14
  34. package/src/runtime/http.ts +36 -12
  35. package/src/runtime/insights.ts +3 -3
  36. package/src/runtime/key-derivation.ts +122 -0
  37. package/src/runtime/keyring.ts +148 -0
  38. package/src/runtime/namespace.ts +33 -0
  39. package/src/runtime/prevalidate.ts +252 -0
  40. package/src/runtime/tls.ts +41 -17
  41. package/src/runtime/waterfall.ts +0 -1
  42. package/src/schema.ts +77 -0
  43. package/src/serve.ts +1 -664
  44. package/src/server/index.ts +22 -0
  45. package/src/server/serve.ts +624 -0
  46. package/src/server/types.ts +78 -0
  47. package/src/stealth/profiles.ts +10 -93
  48. package/src/testing/run.ts +391 -32
  49. package/src/types.ts +390 -41
  50. package/bin/apifuse-init.ts +0 -387
  51. package/src/__tests__/auth.test.ts +0 -396
  52. package/src/__tests__/browser-auth.test.ts +0 -180
  53. package/src/__tests__/browser.test.ts +0 -632
  54. package/src/__tests__/define.test.ts +0 -225
  55. package/src/__tests__/errors.test.ts +0 -69
  56. package/src/__tests__/executor.test.ts +0 -214
  57. package/src/__tests__/http.test.ts +0 -238
  58. package/src/__tests__/insights.test.ts +0 -210
  59. package/src/__tests__/instrumentation.test.ts +0 -290
  60. package/src/__tests__/otlp.test.ts +0 -141
  61. package/src/__tests__/perf.test.ts +0 -60
  62. package/src/__tests__/providers-yaml.test.ts +0 -135
  63. package/src/__tests__/proxy.test.ts +0 -359
  64. package/src/__tests__/recipes.test.ts +0 -36
  65. package/src/__tests__/serve.test.ts +0 -233
  66. package/src/__tests__/session.test.ts +0 -231
  67. package/src/__tests__/state.test.ts +0 -100
  68. package/src/__tests__/stealth.test.ts +0 -57
  69. package/src/__tests__/testing.test.ts +0 -97
  70. package/src/__tests__/tls.test.ts +0 -345
  71. package/src/__tests__/types.test.ts +0 -142
  72. package/src/__tests__/utils.test.ts +0 -62
  73. package/src/__tests__/waterfall.test.ts +0 -270
  74. package/src/config/providers-yaml.ts +0 -370
  75. package/src/index.test.ts +0 -1
  76. package/src/protocol.ts +0 -183
  77. package/src/runtime/auth.ts +0 -245
  78. package/src/runtime/session.ts +0 -573
  79. package/src/runtime/state.ts +0 -124
package/src/protocol.ts DELETED
@@ -1,183 +0,0 @@
1
- /**
2
- * Container Protocol — Communication contract between Route Server and Provider Containers.
3
- *
4
- * All provider containers expose HTTP endpoints. Route Server sends requests
5
- * and receives responses in these formats.
6
- */
7
-
8
- // ═══════════════════════════════════════════════════════════════
9
- // Execute Operation
10
- // POST /execute/:operationId
11
- // ═══════════════════════════════════════════════════════════════
12
-
13
- export interface ExecuteRequest {
14
- /** Validated input (already passed Zod validation by Route Server) */
15
- input: unknown;
16
- /** Decrypted session key-value pairs (loaded by Route Server from DB) */
17
- session?: Record<string, string>;
18
- /** Trace context for distributed tracing */
19
- trace?: { traceId: string; spanId: string };
20
- }
21
-
22
- export interface ExecuteResponse {
23
- /** Operation result (will be Zod-validated by Route Server) */
24
- data: unknown;
25
- /** Session mutations to persist back to DB (key→value for set, key→null for delete) */
26
- sessionPatch?: Record<string, string | null>;
27
- /** Trace spans collected during execution */
28
- trace?: { spans: unknown[] };
29
- }
30
-
31
- // ═══════════════════════════════════════════════════════════════
32
- // Connect (Auth Exchange)
33
- // POST /connect
34
- // ═══════════════════════════════════════════════════════════════
35
-
36
- export interface ConnectRequest {
37
- /** User-provided credentials */
38
- credentials: Record<string, string>;
39
- /** Existing session (for re-auth scenarios) */
40
- session?: Record<string, string>;
41
- }
42
-
43
- /** Successful connection */
44
- export interface ConnectSuccessResponse {
45
- status: "success";
46
- /** Display name for the connected upstream account */
47
- accountRef?: string;
48
- /** Session data to persist (full session state after exchange) */
49
- sessionPatch: Record<string, string>;
50
- }
51
-
52
- /** Connection requires additional field (e.g., OTP) */
53
- export interface ConnectPendingFieldResponse {
54
- status: "pending_field";
55
- /** Name of the field to request from user */
56
- field: string;
57
- /** Label to display to user */
58
- fieldLabel?: string;
59
- /** Opaque token to resume the connection flow */
60
- resumeToken: string;
61
- /** Partial session state to persist (intermediate state) */
62
- sessionPatch?: Record<string, string>;
63
- }
64
-
65
- /** Connection failed */
66
- export interface ConnectFailedResponse {
67
- status: "failed";
68
- /** Error code for programmatic handling */
69
- error: string;
70
- /** Human-readable error message */
71
- message?: string;
72
- }
73
-
74
- export type ConnectResponse =
75
- | ConnectSuccessResponse
76
- | ConnectPendingFieldResponse
77
- | ConnectFailedResponse;
78
-
79
- // ═══════════════════════════════════════════════════════════════
80
- // Resume (Continue deferred auth)
81
- // POST /connect/resume
82
- // ═══════════════════════════════════════════════════════════════
83
-
84
- export interface ResumeRequest {
85
- /** Resume token from ConnectPendingFieldResponse */
86
- resumeToken: string;
87
- /** User-provided value for the requested field */
88
- fieldValue: string;
89
- /** Current session state */
90
- session?: Record<string, string>;
91
- }
92
-
93
- export type ResumeResponse = ConnectResponse; // same as ConnectResponse
94
-
95
- // ═══════════════════════════════════════════════════════════════
96
- // Refresh (Auth token refresh)
97
- // POST /refresh
98
- // ═══════════════════════════════════════════════════════════════
99
-
100
- export interface RefreshRequest {
101
- /** Current session to refresh */
102
- session: Record<string, string>;
103
- }
104
-
105
- export interface RefreshSuccessResponse {
106
- status: "success";
107
- /** Updated session data */
108
- sessionPatch: Record<string, string>;
109
- }
110
-
111
- export interface RefreshFailedResponse {
112
- status: "failed";
113
- error: string;
114
- message?: string;
115
- }
116
-
117
- export type RefreshResponse = RefreshSuccessResponse | RefreshFailedResponse;
118
-
119
- // ═══════════════════════════════════════════════════════════════
120
- // Disconnect
121
- // POST /disconnect
122
- // ═══════════════════════════════════════════════════════════════
123
-
124
- export interface DisconnectRequest {
125
- /** Session to clean up */
126
- session: Record<string, string>;
127
- }
128
-
129
- export interface DisconnectResponse {
130
- status: "success" | "failed";
131
- error?: string;
132
- }
133
-
134
- // ═══════════════════════════════════════════════════════════════
135
- // Health
136
- // GET /health
137
- // ═══════════════════════════════════════════════════════════════
138
-
139
- export interface HealthResponse {
140
- status: "ok" | "degraded" | "error";
141
- provider: string;
142
- version: string;
143
- uptime?: number;
144
- }
145
-
146
- // ═══════════════════════════════════════════════════════════════
147
- // Schema
148
- // GET /schema/:operationId
149
- // ═══════════════════════════════════════════════════════════════
150
-
151
- export interface SchemaResponse {
152
- operationId: string;
153
- description?: string;
154
- input: Record<string, unknown>; // JSON Schema
155
- output: Record<string, unknown>; // JSON Schema
156
- hints?: Record<string, string>;
157
- }
158
-
159
- // ═══════════════════════════════════════════════════════════════
160
- // Error envelope (used for any error response)
161
- // ═══════════════════════════════════════════════════════════════
162
-
163
- export interface ContainerErrorResponse {
164
- error: {
165
- code: string;
166
- message: string;
167
- details?: unknown;
168
- };
169
- }
170
-
171
- // ═══════════════════════════════════════════════════════════════
172
- // Standard endpoint paths
173
- // ═══════════════════════════════════════════════════════════════
174
-
175
- export const CONTAINER_ENDPOINTS = {
176
- execute: "/execute/:operationId",
177
- connect: "/connect",
178
- resume: "/connect/resume",
179
- refresh: "/refresh",
180
- disconnect: "/disconnect",
181
- health: "/health",
182
- schema: "/schema/:operationId",
183
- } as const;
@@ -1,245 +0,0 @@
1
- import { AuthError, TransportError } from "../errors";
2
- import type {
3
- AuthConfig,
4
- AuthContext,
5
- AuthField,
6
- ProviderContext,
7
- SessionStore,
8
- } from "../types";
9
-
10
- const AUTH_SESSION_KEY = "__auth__";
11
- const REQUEST_FIELD_TIMEOUT_MS = 60_000;
12
-
13
- function createAuthSessionMarker(
14
- marker: Record<string, boolean | number>,
15
- ): string {
16
- return JSON.stringify(marker);
17
- }
18
-
19
- type PendingFieldRequest = {
20
- promise: Promise<string>;
21
- resolve: (value: string) => void;
22
- reject: (error: AuthError) => void;
23
- timeoutId: ReturnType<typeof setTimeout>;
24
- };
25
-
26
- export interface AuthManager {
27
- exchange(
28
- ctx: ProviderContext,
29
- credentials: Record<string, string>,
30
- ): Promise<void>;
31
- refresh(ctx: ProviderContext): Promise<void>;
32
- disconnect(ctx: ProviderContext): Promise<void>;
33
- wrapWithAutoRefresh<T>(
34
- ctx: ProviderContext,
35
- operation: () => Promise<T>,
36
- ): Promise<T>;
37
- getFields(): AuthField[];
38
- createAuthContext(): AuthContext;
39
- resolveField(name: string, value: string): void;
40
- getPendingFields(): string[];
41
- isAuthNone(): boolean;
42
- }
43
-
44
- function toAuthError(error: unknown): AuthError {
45
- if (error instanceof AuthError) {
46
- return error;
47
- }
48
-
49
- if (error instanceof Error) {
50
- return new AuthError(error.message, { cause: error });
51
- }
52
-
53
- return new AuthError("Auth exchange failed");
54
- }
55
-
56
- function is401or403(error: unknown): error is TransportError {
57
- return (
58
- error instanceof TransportError &&
59
- (error.status === 401 || error.status === 403)
60
- );
61
- }
62
-
63
- export function createAuthManager(
64
- config: AuthConfig | undefined,
65
- session: SessionStore,
66
- ): AuthManager {
67
- const pendingFieldRequests = new Map<string, PendingFieldRequest>();
68
- const resolvedFieldValues = new Map<string, string>();
69
- let refreshPromise: Promise<void> | null = null;
70
-
71
- function createTimedFieldRequest(name: string): Promise<string> {
72
- const existingRequest = pendingFieldRequests.get(name);
73
- if (existingRequest) {
74
- return existingRequest.promise;
75
- }
76
-
77
- const preResolvedValue = resolvedFieldValues.get(name);
78
- if (preResolvedValue !== undefined) {
79
- resolvedFieldValues.delete(name);
80
- return Promise.resolve(preResolvedValue);
81
- }
82
-
83
- let resolvePromise!: (value: string) => void;
84
- let rejectPromise!: (error: AuthError) => void;
85
-
86
- const promise = new Promise<string>((resolve, reject) => {
87
- resolvePromise = resolve;
88
- rejectPromise = reject;
89
- });
90
-
91
- const timeoutId = setTimeout(() => {
92
- pendingFieldRequests.delete(name);
93
- rejectPromise(new AuthError(`Auth field request timed out: ${name}`));
94
- }, REQUEST_FIELD_TIMEOUT_MS);
95
-
96
- pendingFieldRequests.set(name, {
97
- promise,
98
- resolve: (value) => {
99
- clearTimeout(timeoutId);
100
- pendingFieldRequests.delete(name);
101
- resolvePromise(value);
102
- },
103
- reject: (error) => {
104
- clearTimeout(timeoutId);
105
- pendingFieldRequests.delete(name);
106
- rejectPromise(error);
107
- },
108
- timeoutId,
109
- });
110
-
111
- return promise;
112
- }
113
-
114
- const authContext: AuthContext = {
115
- requestField(name) {
116
- return createTimedFieldRequest(name);
117
- },
118
- };
119
-
120
- const manager: AuthManager = {
121
- async exchange(ctx, credentials) {
122
- if (!config || config.mode === "none") {
123
- throw new AuthError("No auth configured");
124
- }
125
-
126
- if (!config.exchange) {
127
- throw new AuthError("Auth exchange is not configured");
128
- }
129
-
130
- ctx.auth = authContext;
131
-
132
- await ctx.trace.span("auth.exchange", async () => {
133
- try {
134
- await config.exchange?.(ctx, credentials);
135
- } catch (error) {
136
- throw toAuthError(error);
137
- }
138
-
139
- await session.set(
140
- AUTH_SESSION_KEY,
141
- createAuthSessionMarker({
142
- authenticated: true,
143
- timestamp: Date.now(),
144
- }),
145
- );
146
- });
147
- },
148
- async refresh(ctx) {
149
- if (refreshPromise) {
150
- return refreshPromise;
151
- }
152
-
153
- if (!config?.refresh) {
154
- throw new AuthError("No refresh configured");
155
- }
156
-
157
- const doRefresh = async () => {
158
- await ctx.trace.span("auth.refresh", async () => {
159
- try {
160
- await config.refresh?.(ctx);
161
- } catch (error) {
162
- throw toAuthError(error);
163
- }
164
-
165
- await session.set(
166
- AUTH_SESSION_KEY,
167
- createAuthSessionMarker({
168
- authenticated: true,
169
- refreshed: true,
170
- timestamp: Date.now(),
171
- }),
172
- );
173
- });
174
- };
175
-
176
- refreshPromise = doRefresh().finally(() => {
177
- refreshPromise = null;
178
- });
179
-
180
- return refreshPromise;
181
- },
182
- async disconnect(ctx) {
183
- await ctx.trace.span("auth.disconnect", async () => {
184
- let disconnectError: AuthError | null = null;
185
-
186
- try {
187
- if (config?.disconnect) {
188
- await config.disconnect(ctx);
189
- }
190
- } catch (error) {
191
- disconnectError = toAuthError(error);
192
- } finally {
193
- await session.delete(AUTH_SESSION_KEY);
194
- }
195
-
196
- if (disconnectError) {
197
- throw disconnectError;
198
- }
199
- });
200
- },
201
- async wrapWithAutoRefresh(ctx, operation) {
202
- try {
203
- return await operation();
204
- } catch (error) {
205
- if (is401or403(error) && config?.refresh) {
206
- const originalError = error;
207
- await manager.refresh(ctx);
208
-
209
- try {
210
- return await operation();
211
- } catch {
212
- throw originalError;
213
- }
214
- }
215
-
216
- throw error;
217
- }
218
- },
219
- getFields() {
220
- return config?.fields ?? [];
221
- },
222
- createAuthContext() {
223
- return authContext;
224
- },
225
- resolveField(name, value) {
226
- const pendingRequest = pendingFieldRequests.get(name);
227
-
228
- if (!pendingRequest) {
229
- resolvedFieldValues.set(name, value);
230
- return;
231
- }
232
-
233
- clearTimeout(pendingRequest.timeoutId);
234
- pendingRequest.resolve(value);
235
- },
236
- getPendingFields() {
237
- return Array.from(pendingFieldRequests.keys());
238
- },
239
- isAuthNone() {
240
- return !config || config.mode === "none";
241
- },
242
- };
243
-
244
- return manager;
245
- }