@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/types.ts CHANGED
@@ -1,5 +1,251 @@
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
+ chainsWith?: string[];
86
+ }
87
+
88
+ /**
89
+ * Health-check authoring surface owned by `@apifuse/provider-sdk`.
90
+ *
91
+ * IMPORTANT (architectural invariant): These types are PURE DATA + assertion
92
+ * lambdas. They MUST NOT import or reference any health-monitor runtime
93
+ * surface (scheduler, recorder, gateway client, registry projection types).
94
+ * Provider declarations remain runtime-agnostic at build time.
95
+ *
96
+ * See `openspec/changes/enforce-sdk-operation-health-suite/design.md` §D1.
97
+ */
98
+
99
+ /** Polling intervals supported by the health-monitor runtime. */
100
+ export type ProbeInterval = "30s" | "1m" | "3m" | "5m" | "15m" | "30m" | "1h";
101
+
102
+ export const PROBE_INTERVALS: readonly ProbeInterval[] = [
103
+ "30s",
104
+ "1m",
105
+ "3m",
106
+ "5m",
107
+ "15m",
108
+ "30m",
109
+ "1h",
110
+ ] as const;
111
+
112
+ /**
113
+ * Context passed to a `HealthCheckCase.assertions` lambda.
114
+ * `data` is typed against the operation's declared output schema (TOutput).
115
+ */
116
+ export interface HealthCheckAssertionContext<TOutput = unknown> {
117
+ /** Parsed response body (already validated against operation.output). */
118
+ readonly data: TOutput;
119
+ /** HTTP status code returned by the gateway. */
120
+ readonly status: number;
121
+ /** Wall-clock duration of the operation invocation, in milliseconds. */
122
+ readonly durationMs: number;
123
+ }
124
+
125
+ /**
126
+ * Optional return value from an assertions lambda. Allows the case to
127
+ * downgrade to "degraded" without throwing, and to attach a human-friendly
128
+ * label that surfaces on the status page (e.g., "BTC 95,000,000원").
129
+ */
130
+ export interface HealthCheckCaseResult {
131
+ /** Override final status; if omitted, "ok" unless assertion threw. */
132
+ status?: "ok" | "degraded";
133
+ /** Optional human-readable label surfaced on the status page. */
134
+ label?: string;
135
+ }
136
+
137
+ /**
138
+ * A single test-case-style verification scenario for an operation.
139
+ *
140
+ * Type parameters flow from the OperationDefinition's input/output schemas
141
+ * so authors get IntelliSense and compile-time errors when accessing fields
142
+ * that do not exist on the operation's declared output schema.
143
+ */
144
+ export interface HealthCheckCase<TInput = unknown, TOutput = unknown> {
145
+ /** Human-readable case name; unique within the suite. */
146
+ name: string;
147
+ /** Optional longer description shown on ops dashboards. */
148
+ description?: string;
149
+ /** Input passed to the operation handler for this case. */
150
+ input: TInput;
151
+ /**
152
+ * Assertion executed against the operation's response and timing.
153
+ *
154
+ * - Throw to fail the case (recorded as `down`).
155
+ * - Return `{ status: "degraded", label }` to flag without failing.
156
+ * - Return `void` (implicit) for `ok`.
157
+ *
158
+ * MUST NOT access scheduler, recorder, or any runtime type — pure data
159
+ * + lambda only.
160
+ */
161
+ assertions: (
162
+ ctx: HealthCheckAssertionContext<TOutput>,
163
+ ) =>
164
+ | void
165
+ | Promise<void>
166
+ | HealthCheckCaseResult
167
+ | Promise<HealthCheckCaseResult>;
168
+ /** Override per-case degradation threshold (ms); falls back to the suite default. */
169
+ degradedThresholdMs?: number;
170
+ /** Expected outcome for "negative" cases (e.g., expecting a degraded baseline). Default: `"ok"`. */
171
+ expectedStatus?: "ok" | "degraded";
172
+ /** Runtime gate (env-driven); if returns false the case is skipped & logged. */
173
+ enabled?: () => boolean;
174
+ }
175
+
176
+ /**
177
+ * Operation-level health-check suite. At least one case is required when
178
+ * present. All cases share the suite's interval and default timeout.
179
+ */
180
+ export interface HealthCheckSuite<TInput = unknown, TOutput = unknown> {
181
+ /** Polling interval for the suite. All cases share this cadence. */
182
+ interval: ProbeInterval;
183
+ /** Per-case timeout in milliseconds. Default: 30000. */
184
+ timeoutMs?: number;
185
+ /** Non-empty list of cases. Empty arrays are rejected at definition time. */
186
+ cases: [
187
+ HealthCheckCase<TInput, TOutput>,
188
+ ...HealthCheckCase<TInput, TOutput>[],
189
+ ];
190
+ /**
191
+ * If true, the runtime SHALL invoke `connect()` before `execute()` and
192
+ * `disconnect()` after, using the service-account token. The provider
193
+ * MUST also declare `healthMonitor.requiredSecrets` for the env values
194
+ * the connect ceremony will consume.
195
+ */
196
+ requiresConnection?: boolean;
197
+ }
198
+
199
+ /**
200
+ * Explicit, audited opt-out for operations that genuinely cannot be probed
201
+ * (e.g., destructive mutations, paid-per-call flows). Either `healthCheck`
202
+ * or `healthCheckUnsupported` SHALL be present on every operation; missing
203
+ * both is a registry-build error.
204
+ */
205
+ export interface HealthCheckUnsupported {
206
+ /** Human-readable explanation. Required, non-empty. */
207
+ reason: string;
208
+ /** Optional issue/PR url that revisits the decision. */
209
+ trackedIn?: string;
210
+ }
211
+
212
+ /**
213
+ * Provider-level monitoring metadata for credential-bearing health checks.
214
+ * Describes ONLY env-secret keys the runtime needs and the service account
215
+ * to use; SHALL NOT carry probe schedules, sample inputs, or assertion
216
+ * logic (those remain on `OperationDefinition.healthCheck`).
217
+ */
218
+ export interface ProviderHealthMonitorConfig {
219
+ /**
220
+ * Env-secret key names (e.g., "HEALTH_MONITOR_CATCHTABLE_PHONE") the
221
+ * synthetic-monitor runtime needs to execute probes that declare
222
+ * `requiresConnection: true`.
223
+ */
224
+ requiredSecrets?: string[];
225
+ /**
226
+ * Override the default service account ID for this provider's probes.
227
+ * Defaults to the runtime's `APIFUSE_SERVICE_ACCOUNT_ID` env var.
228
+ */
229
+ serviceAccount?: string;
230
+ }
231
+
232
+ export interface OperationErrorCode {
233
+ code: string;
234
+ status?: number;
235
+ description: string;
236
+ retryable?: boolean;
237
+ }
238
+
239
+ export interface OperationDocMeta {
240
+ title?: string;
241
+ description?: string;
242
+ summary?: string;
243
+ normalizationNotes?: string[];
244
+ requestExample?: Record<string, unknown>;
245
+ responseExample?: unknown;
246
+ errorCodes?: OperationErrorCode[];
247
+ }
248
+
3
249
  export type StealthPlatform = "macos" | "windows" | "linux" | "android" | "ios";
4
250
 
5
251
  export type BrowserEngine = "playwright-stealth" | "nodriver" | "selenium-uc";
@@ -23,15 +269,11 @@ export interface StealthProfile {
23
269
  headerOrder?: string[];
24
270
  }
25
271
 
26
- export type AuthMode = "none" | "credentials" | "oauth2" | "api-key";
272
+ export type AuthMode = "none" | "platform-managed" | "credentials" | "oauth2";
27
273
 
28
- export interface AuthField {
29
- name: string;
30
- label: string;
31
- type: "text" | "password" | "otp";
32
- required?: boolean;
33
- deferred?: boolean;
34
- }
274
+ export type ConnectionMode = AuthMode;
275
+
276
+ export type ProviderReviewed = "first-party" | "community" | "staging";
35
277
 
36
278
  export interface ProviderMeta {
37
279
  displayName: string;
@@ -39,6 +281,12 @@ export interface ProviderMeta {
39
281
  category: string;
40
282
  tags?: string[];
41
283
  icon?: string;
284
+ docTitle?: string;
285
+ docDescription?: string;
286
+ docSummary?: string;
287
+ normalizationNotes?: string[];
288
+ environment?: "staging";
289
+ purpose?: string;
42
290
  }
43
291
 
44
292
  export interface RequestOptions {
@@ -133,17 +381,6 @@ export interface BrowserClient {
133
381
  newPage(): Promise<unknown>;
134
382
  }
135
383
 
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
384
  export type TraceAttributeValue = string | number | boolean;
148
385
 
149
386
  export interface TraceSpan {
@@ -182,53 +419,134 @@ export interface AuthContext {
182
419
  ): Promise<string>;
183
420
  }
184
421
 
422
+ export interface EnvContext {
423
+ get(key: string): string | undefined;
424
+ }
425
+
426
+ export interface CredentialContext {
427
+ mode: AuthMode;
428
+ get(key: string): string | undefined;
429
+ getAll(): Record<string, string>;
430
+ getAccessToken(): string | undefined;
431
+ getScopes(): string[];
432
+ }
433
+
434
+ export interface ProviderRequestContext {
435
+ connectionId?: string;
436
+ headers: Record<string, string>;
437
+ }
438
+
439
+ export interface ContextScratchpad {
440
+ get(key: string): unknown;
441
+ set(key: string, value: unknown): void;
442
+ toJSON(): Record<string, unknown>;
443
+ }
444
+
445
+ export type FlowContextStore = ContextScratchpad;
446
+
447
+ export interface FlowContext {
448
+ connectionId?: string;
449
+ externalRef?: string;
450
+ tenantId: string;
451
+ providerId: string;
452
+ http: HttpClient;
453
+ env: EnvContext;
454
+ context: ContextScratchpad;
455
+ }
456
+
457
+ export interface AuthTurn {
458
+ kind: string;
459
+ turnId: string;
460
+ expiresAt?: string;
461
+ data?: Record<string, unknown>;
462
+ expectedInput?: Record<string, unknown>;
463
+ hint?: string;
464
+ timing?: {
465
+ suggestedPollIntervalMs?: number;
466
+ maxWaitMs?: number;
467
+ };
468
+ }
469
+
470
+ export type AuthFlowHandler = (
471
+ ctx: FlowContext,
472
+ input?: Record<string, unknown>,
473
+ ) => Promise<AuthTurn>;
474
+
475
+ export interface AuthFlowDefinition {
476
+ start: AuthFlowHandler;
477
+ continue: AuthFlowHandler;
478
+ poll?: AuthFlowHandler;
479
+ abort?: AuthFlowHandler;
480
+ }
481
+
185
482
  export interface ProviderContext {
483
+ env: EnvContext;
484
+ credential: CredentialContext;
485
+ request?: ProviderRequestContext;
186
486
  http: HttpClient;
187
487
  tls: TlsClient;
188
488
  browser: BrowserClient;
189
- session: SessionStore;
190
- state: StateContext;
191
489
  trace: TraceContext;
192
490
  auth: AuthContext;
193
491
  }
194
492
 
195
493
  export interface AuthConfig {
196
494
  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>;
495
+ flow?: AuthFlowDefinition;
496
+ }
497
+
498
+ export interface ProviderSecretDeclaration {
499
+ name: string;
500
+ description?: string;
501
+ required?: boolean;
502
+ }
503
+
504
+ export interface CredentialDeclaration {
505
+ keys: string[];
506
+ storesReusableSecret?: boolean;
507
+ justification?: string;
508
+ }
509
+
510
+ export interface ContextDeclaration {
511
+ keys: string[];
208
512
  }
209
513
 
210
514
  export interface OperationDefinition<
211
- TInput extends ZodType = ZodType,
212
- TOutput extends ZodType = ZodType,
515
+ TInput extends SchemaLike = SchemaLike,
516
+ TOutput extends SchemaLike = SchemaLike,
213
517
  > {
214
518
  description?: string;
519
+ docs?: OperationDocMeta;
520
+ whenToUse?: string[];
521
+ whenNotToUse?: string[];
522
+ derivations?: Record<string, string>;
523
+ inputExamples?: OperationInputExample[];
524
+ annotations?: OperationAnnotations;
525
+ tags?: string[];
526
+ relatedOperations?: OperationRelationships;
215
527
  input: TInput;
216
528
  output: TOutput;
217
- handler: (
529
+ handler(
218
530
  ctx: ProviderContext,
219
- input: ZodInfer<TInput>,
220
- ) => Promise<ZodInfer<TOutput>>;
531
+ input: InferSchemaOutput<TInput>,
532
+ ): Promise<InferSchemaOutput<TOutput>>;
221
533
  fixtures?: {
222
- request: ZodInfer<TInput>;
223
- response: ZodInfer<TOutput>;
534
+ request: InferSchemaOutput<TInput>;
535
+ response: InferSchemaOutput<TOutput>;
224
536
  };
225
537
  hints?: Record<string, string>;
538
+ healthCheck?: HealthCheckSuite<
539
+ InferSchemaOutput<TInput>,
540
+ InferSchemaOutput<TOutput>
541
+ >;
542
+ healthCheckUnsupported?: HealthCheckUnsupported;
226
543
  }
227
544
 
228
545
  export interface ProviderDefinition {
229
546
  id: string;
230
547
  version: string;
231
- runtime: "standard" | "browser";
548
+ runtime: "standard" | "shared" | "browser";
549
+ allowedHosts?: string[];
232
550
  stealth?: {
233
551
  profile: string;
234
552
  platform: StealthPlatform;
@@ -238,6 +556,11 @@ export interface ProviderDefinition {
238
556
  engine: BrowserEngine;
239
557
  };
240
558
  auth?: AuthConfig;
559
+ reviewed?: ProviderReviewed;
560
+ secrets?: ProviderSecretDeclaration[];
561
+ credential?: CredentialDeclaration;
562
+ context?: ContextDeclaration;
241
563
  meta: ProviderMeta;
242
- operations: Record<string, OperationDefinition<ZodType, ZodType>>;
564
+ operations: Record<string, OperationDefinition<SchemaLike, SchemaLike>>;
565
+ healthMonitor?: ProviderHealthMonitorConfig;
243
566
  }