@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/types.ts CHANGED
@@ -1,5 +1,271 @@
1
1
  import type { infer as ZodInfer, ZodType } from "zod";
2
2
 
3
+ /** Minimal Standard Schema v1 shape accepted by provider operations. */
4
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
5
+ readonly "~standard": {
6
+ readonly version: 1;
7
+ readonly vendor: string;
8
+ readonly validate: (
9
+ value: unknown,
10
+ ) =>
11
+ | StandardSchemaV1.Result<Output>
12
+ | Promise<StandardSchemaV1.Result<Output>>;
13
+ readonly types?: { readonly input: Input; readonly output: Output };
14
+ };
15
+ }
16
+ export namespace StandardSchemaV1 {
17
+ export interface Issue {
18
+ readonly message: string;
19
+ readonly path?: readonly (PropertyKey | PathSegment)[];
20
+ }
21
+ export interface PathSegment {
22
+ readonly key: PropertyKey;
23
+ }
24
+ export interface SuccessResult<Output> {
25
+ readonly value: Output;
26
+ }
27
+ export interface FailureResult {
28
+ readonly issues: readonly Issue[];
29
+ }
30
+ export type Result<Output> = SuccessResult<Output> | FailureResult;
31
+ }
32
+ /** Schema formats supported by provider operations. */
33
+ export type SchemaLike = ZodType | StandardSchemaV1;
34
+ /** Infer the validated output type produced by a Zod or Standard Schema. */
35
+ export type InferSchemaOutput<TSchema extends SchemaLike> =
36
+ TSchema extends ZodType
37
+ ? ZodInfer<TSchema>
38
+ : TSchema extends StandardSchemaV1<unknown, infer Output>
39
+ ? Output
40
+ : unknown;
41
+
42
+ export interface OperationInputExample {
43
+ scenario: string;
44
+ input: unknown;
45
+ rationale?: string;
46
+ }
47
+
48
+ export interface OperationAnnotations {
49
+ readOnly?: boolean;
50
+ destructive?: boolean;
51
+ idempotent?: boolean;
52
+ /**
53
+ * Marks the operation as callable without provider-level authentication.
54
+ *
55
+ * Provider-level `auth.mode` describes the **majority** auth model of a
56
+ * provider; individual operations can still opt out via `openWorld: true`
57
+ * when their handler does not consume `ctx.credential`. This is the
58
+ * canonical way to declare "this operation is public, even though the
59
+ * provider is `credentials`-mode" without splitting the provider into two.
60
+ *
61
+ * Health-check projections treat `openWorld: true` operations as
62
+ * connection-free probes (no `requiresConnection` required, no SA token
63
+ * lookup). Future gateway work MAY extend this annotation to bypass
64
+ * `X-ApiFuse-Connection-Id` enforcement at proxy time.
65
+ *
66
+ * Example: Naver Map's `search`, `geocode`, and directions operations
67
+ * call public Naver endpoints with no cookies, while `collections` and
68
+ * `export` consume the user's session cookie — the provider declares
69
+ * `auth.mode: "credentials"` (for the latter) and the former mark
70
+ * `openWorld: true`.
71
+ */
72
+ openWorld?: boolean;
73
+ rateLimit?: {
74
+ calls: number;
75
+ window: "minute" | "hour" | "day";
76
+ };
77
+ timeoutMs?: number;
78
+ }
79
+
80
+ export const OPERATION_TIMEOUT_MS_MIN = 1;
81
+ export const OPERATION_TIMEOUT_MS_MAX = 60_000;
82
+
83
+ export interface OperationRelationships {
84
+ alternatives?: string[];
85
+ }
86
+
87
+ /**
88
+ * Health-check authoring surface owned by `@apifuse/provider-sdk`.
89
+ *
90
+ * IMPORTANT (architectural invariant): These types are PURE DATA + assertion
91
+ * lambdas. They MUST NOT import or reference any health-monitor runtime
92
+ * surface (scheduler, recorder, gateway client, registry projection types).
93
+ * Provider declarations remain runtime-agnostic at build time.
94
+ *
95
+ * See `openspec/changes/enforce-sdk-operation-health-suite/design.md` §D1.
96
+ */
97
+
98
+ /** Polling intervals supported by the health-monitor runtime. */
99
+ export type ProbeInterval =
100
+ | "30s"
101
+ | "1m"
102
+ | "3m"
103
+ | "5m"
104
+ | "15m"
105
+ | "30m"
106
+ | "1h"
107
+ | "24h";
108
+
109
+ export const PROBE_INTERVALS: readonly ProbeInterval[] = [
110
+ "30s",
111
+ "1m",
112
+ "3m",
113
+ "5m",
114
+ "15m",
115
+ "30m",
116
+ "1h",
117
+ "24h",
118
+ ] as const;
119
+
120
+ /**
121
+ * Context passed to a `HealthCheckCase.assertions` lambda.
122
+ * `data` is typed against the operation's declared output schema (TOutput).
123
+ */
124
+ export interface HealthCheckAssertionContext<TOutput = unknown> {
125
+ /** Parsed response body (already validated against operation.output). */
126
+ readonly data: TOutput;
127
+ /** HTTP status code returned by the gateway. */
128
+ readonly status: number;
129
+ /** Wall-clock duration of the operation invocation, in milliseconds. */
130
+ readonly durationMs: number;
131
+ }
132
+
133
+ /**
134
+ * Optional return value from an assertions lambda. Allows the case to
135
+ * downgrade to "degraded" without throwing, and to attach a human-friendly
136
+ * label that surfaces on the status page (e.g., "BTC 95,000,000원").
137
+ */
138
+ export interface HealthCheckCaseResult {
139
+ /** Override final status; if omitted, "ok" unless assertion threw. */
140
+ status?: "ok" | "degraded";
141
+ /** Optional human-readable label surfaced on the status page. */
142
+ label?: string;
143
+ }
144
+
145
+ /**
146
+ * A single test-case-style verification scenario for an operation.
147
+ *
148
+ * Type parameters flow from the OperationDefinition's input/output schemas
149
+ * so authors get IntelliSense and compile-time errors when accessing fields
150
+ * that do not exist on the operation's declared output schema.
151
+ */
152
+ export interface HealthCheckCase<TInput = unknown, TOutput = unknown> {
153
+ /** Human-readable case name; unique within the suite. */
154
+ name: string;
155
+ /** Optional longer description shown on ops dashboards. */
156
+ description?: string;
157
+ /** Input passed to the operation handler for this case. */
158
+ input: TInput;
159
+ /**
160
+ * Assertion executed against the operation's response and timing.
161
+ *
162
+ * - Throw to fail the case (recorded as `down`).
163
+ * - Return `{ status: "degraded", label }` to flag without failing.
164
+ * - Return `void` (implicit) for `ok`.
165
+ *
166
+ * MUST NOT access scheduler, recorder, or any runtime type — pure data
167
+ * + lambda only.
168
+ */
169
+ assertions: (
170
+ ctx: HealthCheckAssertionContext<TOutput>,
171
+ ) =>
172
+ | void
173
+ | Promise<void>
174
+ | HealthCheckCaseResult
175
+ | Promise<HealthCheckCaseResult>;
176
+ /** Override per-case degradation threshold (ms); falls back to the suite default. */
177
+ degradedThresholdMs?: number;
178
+ /** Expected outcome for "negative" cases (e.g., expecting a degraded baseline). Default: `"ok"`. */
179
+ expectedStatus?: "ok" | "degraded";
180
+ /** Runtime gate (env-driven); if returns false the case is skipped & logged. */
181
+ enabled?: () => boolean;
182
+ }
183
+
184
+ /**
185
+ * Operation-level health-check suite. At least one case is required when
186
+ * present. All cases share the suite's interval and default timeout.
187
+ */
188
+ export interface HealthCheckSuite<TInput = unknown, TOutput = unknown> {
189
+ /** Polling interval for the suite. All cases share this cadence. */
190
+ interval: ProbeInterval;
191
+ /** Per-case timeout in milliseconds. Default: 30000. */
192
+ timeoutMs?: number;
193
+ /** Non-empty list of cases. Empty arrays are rejected at definition time. */
194
+ cases: [
195
+ HealthCheckCase<TInput, TOutput>,
196
+ ...HealthCheckCase<TInput, TOutput>[],
197
+ ];
198
+ /**
199
+ * If true, the runtime SHALL invoke `connect()` before `execute()` and
200
+ * `disconnect()` after, using the service-account token. The provider
201
+ * MUST also declare `healthMonitor.requiredSecrets` for the env values
202
+ * the connect ceremony will consume.
203
+ */
204
+ requiresConnection?: boolean;
205
+ }
206
+
207
+ /**
208
+ * Explicit, audited opt-out for operations that genuinely cannot be probed
209
+ * (e.g., destructive mutations, paid-per-call flows). Either `healthCheck`
210
+ * or `healthCheckUnsupported` SHALL be present on every operation; missing
211
+ * both is a registry-build error.
212
+ */
213
+ export interface HealthCheckUnsupported {
214
+ /** Human-readable explanation. Required, non-empty. */
215
+ reason: string;
216
+ /** Optional issue/PR url that revisits the decision. */
217
+ trackedIn?: string;
218
+ }
219
+
220
+ /**
221
+ * Provider-level monitoring metadata for credential-bearing health checks.
222
+ * Describes ONLY env-secret keys the runtime needs and the service account
223
+ * to use; SHALL NOT carry probe schedules, sample inputs, or assertion
224
+ * logic (those remain on `OperationDefinition.healthCheck`).
225
+ */
226
+ export interface ProviderHealthMonitorConfig {
227
+ /**
228
+ * Env-secret key names (e.g., "HEALTH_MONITOR_CATCHTABLE_PHONE") the
229
+ * synthetic-monitor runtime needs to execute probes that declare
230
+ * `requiresConnection: true`.
231
+ */
232
+ requiredSecrets?: string[];
233
+ /**
234
+ * Runtime probe overrides keyed by probe id (for example
235
+ * "catchtable/auth-flow" or "catchtable/waiting-lifecycle"). Use this for
236
+ * health-monitor probes that are provider-scoped or cross-operation and
237
+ * therefore cannot declare an `OperationDefinition.healthCheck.interval`.
238
+ */
239
+ probeOverrides?: Record<string, HealthMonitorProbeOverride>;
240
+ /**
241
+ * Override the default service account ID for this provider's probes.
242
+ * Defaults to the runtime's `APIFUSE_SERVICE_ACCOUNT_ID` env var.
243
+ */
244
+ serviceAccount?: string;
245
+ }
246
+
247
+ export interface HealthMonitorProbeOverride {
248
+ /** Optional runtime interval override. Must be one of PROBE_INTERVALS. */
249
+ interval?: ProbeInterval;
250
+ }
251
+
252
+ export interface OperationErrorCode {
253
+ code: string;
254
+ status?: number;
255
+ description: string;
256
+ retryable?: boolean;
257
+ }
258
+
259
+ export interface OperationDocMeta {
260
+ title?: string;
261
+ description?: string;
262
+ summary?: string;
263
+ normalizationNotes?: string[];
264
+ requestExample?: Record<string, unknown>;
265
+ responseExample?: unknown;
266
+ errorCodes?: OperationErrorCode[];
267
+ }
268
+
3
269
  export type StealthPlatform = "macos" | "windows" | "linux" | "android" | "ios";
4
270
 
5
271
  export type BrowserEngine = "playwright-stealth" | "nodriver" | "selenium-uc";
@@ -23,15 +289,11 @@ export interface StealthProfile {
23
289
  headerOrder?: string[];
24
290
  }
25
291
 
26
- export type AuthMode = "none" | "credentials" | "oauth2" | "api-key";
292
+ export type AuthMode = "none" | "platform-managed" | "credentials" | "oauth2";
27
293
 
28
- export interface AuthField {
29
- name: string;
30
- label: string;
31
- type: "text" | "password" | "otp";
32
- required?: boolean;
33
- deferred?: boolean;
34
- }
294
+ export type ConnectionMode = AuthMode;
295
+
296
+ export type ProviderReviewed = "first-party" | "community" | "staging";
35
297
 
36
298
  export interface ProviderMeta {
37
299
  displayName: string;
@@ -39,6 +301,12 @@ export interface ProviderMeta {
39
301
  category: string;
40
302
  tags?: string[];
41
303
  icon?: string;
304
+ docTitle?: string;
305
+ docDescription?: string;
306
+ docSummary?: string;
307
+ normalizationNotes?: string[];
308
+ environment?: "staging";
309
+ purpose?: string;
42
310
  }
43
311
 
44
312
  export interface RequestOptions {
@@ -52,9 +320,15 @@ export interface TlsFetchOptions extends RequestOptions {
52
320
  method?: string;
53
321
  body?: string | Buffer;
54
322
  profile?: string;
323
+ /**
324
+ * Defaults to true. Set to false when callers need to inspect upstream
325
+ * non-2xx bodies themselves instead of converting them to TransportError.
326
+ */
327
+ throwOnHttpError?: boolean;
55
328
  tls?: {
56
329
  ja3?: string;
57
330
  h2?: Record<string, unknown>;
331
+ insecureSkipVerify?: boolean;
58
332
  };
59
333
  headerOrder?: string[];
60
334
  }
@@ -133,17 +407,6 @@ export interface BrowserClient {
133
407
  newPage(): Promise<unknown>;
134
408
  }
135
409
 
136
- export interface SessionStore {
137
- get(key: string): Promise<string | null>;
138
- set(key: string, value: string, ttl?: string): Promise<void>;
139
- delete(key: string): Promise<void>;
140
- }
141
-
142
- export interface StateContext {
143
- seal(data: unknown, options?: { ttl?: string }): Promise<string>;
144
- unseal<T = unknown>(token: string): Promise<T | null>;
145
- }
146
-
147
410
  export type TraceAttributeValue = string | number | boolean;
148
411
 
149
412
  export interface TraceSpan {
@@ -182,53 +445,134 @@ export interface AuthContext {
182
445
  ): Promise<string>;
183
446
  }
184
447
 
448
+ export interface EnvContext {
449
+ get(key: string): string | undefined;
450
+ }
451
+
452
+ export interface CredentialContext {
453
+ mode: AuthMode;
454
+ get(key: string): string | undefined;
455
+ getAll(): Record<string, string>;
456
+ getAccessToken(): string | undefined;
457
+ getScopes(): string[];
458
+ }
459
+
460
+ export interface ProviderRequestContext {
461
+ connectionId?: string;
462
+ headers: Record<string, string>;
463
+ }
464
+
465
+ export interface ContextScratchpad {
466
+ get(key: string): unknown;
467
+ set(key: string, value: unknown): void;
468
+ toJSON(): Record<string, unknown>;
469
+ }
470
+
471
+ export type FlowContextStore = ContextScratchpad;
472
+
473
+ export interface FlowContext {
474
+ connectionId?: string;
475
+ externalRef?: string;
476
+ tenantId: string;
477
+ providerId: string;
478
+ http: HttpClient;
479
+ env: EnvContext;
480
+ context: ContextScratchpad;
481
+ }
482
+
483
+ export interface AuthTurn {
484
+ kind: string;
485
+ turnId: string;
486
+ expiresAt?: string;
487
+ data?: Record<string, unknown>;
488
+ expectedInput?: Record<string, unknown>;
489
+ hint?: string;
490
+ timing?: {
491
+ suggestedPollIntervalMs?: number;
492
+ maxWaitMs?: number;
493
+ };
494
+ }
495
+
496
+ export type AuthFlowHandler = (
497
+ ctx: FlowContext,
498
+ input?: Record<string, unknown>,
499
+ ) => Promise<AuthTurn>;
500
+
501
+ export interface AuthFlowDefinition {
502
+ start: AuthFlowHandler;
503
+ continue: AuthFlowHandler;
504
+ poll?: AuthFlowHandler;
505
+ abort?: AuthFlowHandler;
506
+ }
507
+
185
508
  export interface ProviderContext {
509
+ env: EnvContext;
510
+ credential: CredentialContext;
511
+ request?: ProviderRequestContext;
186
512
  http: HttpClient;
187
513
  tls: TlsClient;
188
514
  browser: BrowserClient;
189
- session: SessionStore;
190
- state: StateContext;
191
515
  trace: TraceContext;
192
516
  auth: AuthContext;
193
517
  }
194
518
 
195
519
  export interface AuthConfig {
196
520
  mode: AuthMode;
197
- fields?: AuthField[];
198
- exchange?: (
199
- ctx: ProviderContext,
200
- credentials: Record<string, string>,
201
- ) => Promise<void>;
202
- refresh?: (ctx: ProviderContext) => Promise<void>;
203
- refreshPolicy?: {
204
- strategy: "reactive" | "proactive" | "both";
205
- interval?: string;
206
- };
207
- disconnect?: (ctx: ProviderContext) => Promise<void>;
521
+ flow?: AuthFlowDefinition;
522
+ }
523
+
524
+ export interface ProviderSecretDeclaration {
525
+ name: string;
526
+ description?: string;
527
+ required?: boolean;
528
+ }
529
+
530
+ export interface CredentialDeclaration {
531
+ keys: string[];
532
+ storesReusableSecret?: boolean;
533
+ justification?: string;
534
+ }
535
+
536
+ export interface ContextDeclaration {
537
+ keys: string[];
208
538
  }
209
539
 
210
540
  export interface OperationDefinition<
211
- TInput extends ZodType = ZodType,
212
- TOutput extends ZodType = ZodType,
541
+ TInput extends SchemaLike = SchemaLike,
542
+ TOutput extends SchemaLike = SchemaLike,
213
543
  > {
214
544
  description?: string;
545
+ docs?: OperationDocMeta;
546
+ whenToUse?: string[];
547
+ whenNotToUse?: string[];
548
+ derivations?: Record<string, string>;
549
+ inputExamples?: OperationInputExample[];
550
+ annotations?: OperationAnnotations;
551
+ tags?: string[];
552
+ relatedOperations?: OperationRelationships;
215
553
  input: TInput;
216
554
  output: TOutput;
217
- handler: (
555
+ handler(
218
556
  ctx: ProviderContext,
219
- input: ZodInfer<TInput>,
220
- ) => Promise<ZodInfer<TOutput>>;
557
+ input: InferSchemaOutput<TInput>,
558
+ ): Promise<InferSchemaOutput<TOutput>>;
221
559
  fixtures?: {
222
- request: ZodInfer<TInput>;
223
- response: ZodInfer<TOutput>;
560
+ request: InferSchemaOutput<TInput>;
561
+ response: InferSchemaOutput<TOutput>;
224
562
  };
225
563
  hints?: Record<string, string>;
564
+ healthCheck?: HealthCheckSuite<
565
+ InferSchemaOutput<TInput>,
566
+ InferSchemaOutput<TOutput>
567
+ >;
568
+ healthCheckUnsupported?: HealthCheckUnsupported;
226
569
  }
227
570
 
228
571
  export interface ProviderDefinition {
229
572
  id: string;
230
573
  version: string;
231
- runtime: "standard" | "browser";
574
+ runtime: "standard" | "shared" | "browser";
575
+ allowedHosts?: string[];
232
576
  stealth?: {
233
577
  profile: string;
234
578
  platform: StealthPlatform;
@@ -238,6 +582,11 @@ export interface ProviderDefinition {
238
582
  engine: BrowserEngine;
239
583
  };
240
584
  auth?: AuthConfig;
585
+ reviewed?: ProviderReviewed;
586
+ secrets?: ProviderSecretDeclaration[];
587
+ credential?: CredentialDeclaration;
588
+ context?: ContextDeclaration;
241
589
  meta: ProviderMeta;
242
- operations: Record<string, OperationDefinition<ZodType, ZodType>>;
590
+ operations: Record<string, OperationDefinition<SchemaLike, SchemaLike>>;
591
+ healthMonitor?: ProviderHealthMonitorConfig;
243
592
  }