@apifuse/provider-sdk 2.0.0-beta.1 → 2.1.0-beta.0

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 (78) hide show
  1. package/AUTHORING.md +102 -0
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +100 -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 +47 -0
  8. package/bin/apifuse-perf.ts +33 -32
  9. package/bin/apifuse-record.ts +17 -7
  10. package/bin/apifuse-test.ts +6 -4
  11. package/bin/apifuse.ts +36 -35
  12. package/package.json +28 -9
  13. package/src/ceremonies/index.ts +747 -0
  14. package/src/cli/commands.ts +87 -0
  15. package/src/cli/create.ts +845 -0
  16. package/src/cli/templates/provider/Dockerfile.tpl +7 -0
  17. package/src/cli/templates/provider/README.md.tpl +28 -0
  18. package/src/cli/templates/provider/dev.ts.tpl +5 -0
  19. package/src/cli/templates/provider/index.test.ts.tpl +13 -0
  20. package/src/cli/templates/provider/index.ts.tpl +54 -0
  21. package/src/cli/templates/provider/start.ts.tpl +5 -0
  22. package/src/composite.ts +43 -0
  23. package/src/define.ts +527 -41
  24. package/src/dev.ts +2 -6
  25. package/src/errors.ts +42 -0
  26. package/src/index.ts +50 -38
  27. package/src/lint.ts +574 -0
  28. package/src/provider.ts +14 -0
  29. package/src/runtime/auth-flow.ts +67 -0
  30. package/src/runtime/credential.ts +95 -0
  31. package/src/runtime/env.ts +13 -0
  32. package/src/runtime/executor.ts +13 -14
  33. package/src/runtime/http.ts +10 -2
  34. package/src/runtime/insights.ts +3 -3
  35. package/src/runtime/key-derivation.ts +122 -0
  36. package/src/runtime/keyring.ts +148 -0
  37. package/src/runtime/namespace.ts +33 -0
  38. package/src/runtime/prevalidate.ts +252 -0
  39. package/src/runtime/tls.ts +20 -5
  40. package/src/runtime/waterfall.ts +0 -1
  41. package/src/schema.ts +77 -0
  42. package/src/serve.ts +1 -664
  43. package/src/server/index.ts +22 -0
  44. package/src/server/serve.ts +610 -0
  45. package/src/server/types.ts +78 -0
  46. package/src/stealth/profiles.ts +10 -93
  47. package/src/testing/run.ts +391 -32
  48. package/src/types.ts +364 -41
  49. package/bin/apifuse-init.ts +0 -387
  50. package/src/__tests__/auth.test.ts +0 -396
  51. package/src/__tests__/browser-auth.test.ts +0 -180
  52. package/src/__tests__/browser.test.ts +0 -632
  53. package/src/__tests__/define.test.ts +0 -225
  54. package/src/__tests__/errors.test.ts +0 -69
  55. package/src/__tests__/executor.test.ts +0 -214
  56. package/src/__tests__/http.test.ts +0 -238
  57. package/src/__tests__/insights.test.ts +0 -210
  58. package/src/__tests__/instrumentation.test.ts +0 -290
  59. package/src/__tests__/otlp.test.ts +0 -141
  60. package/src/__tests__/perf.test.ts +0 -60
  61. package/src/__tests__/providers-yaml.test.ts +0 -135
  62. package/src/__tests__/proxy.test.ts +0 -359
  63. package/src/__tests__/recipes.test.ts +0 -36
  64. package/src/__tests__/serve.test.ts +0 -233
  65. package/src/__tests__/session.test.ts +0 -231
  66. package/src/__tests__/state.test.ts +0 -100
  67. package/src/__tests__/stealth.test.ts +0 -57
  68. package/src/__tests__/testing.test.ts +0 -97
  69. package/src/__tests__/tls.test.ts +0 -345
  70. package/src/__tests__/types.test.ts +0 -142
  71. package/src/__tests__/utils.test.ts +0 -62
  72. package/src/__tests__/waterfall.test.ts +0 -270
  73. package/src/config/providers-yaml.ts +0 -370
  74. package/src/index.test.ts +0 -1
  75. package/src/protocol.ts +0 -183
  76. package/src/runtime/auth.ts +0 -245
  77. package/src/runtime/session.ts +0 -573
  78. 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
- }