@apifuse/provider-sdk 2.1.0-beta.9 → 2.2.0-beta.2

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 (138) hide show
  1. package/AUTHORING.md +134 -0
  2. package/CHANGELOG.md +61 -0
  3. package/README.md +21 -9
  4. package/SUBMISSION.md +10 -11
  5. package/bin/apifuse-pack-check.ts +22 -0
  6. package/bin/apifuse-submit-check.ts +1747 -388
  7. package/dist/auth-turn/auth-turn.v1.schema.json +89 -0
  8. package/dist/auth-turn/fixtures/invalid/empty-kind.json +4 -0
  9. package/dist/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
  10. package/dist/auth-turn/fixtures/invalid/missing-kind.json +3 -0
  11. package/dist/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
  12. package/dist/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
  13. package/dist/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
  14. package/dist/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
  15. package/dist/auth-turn/fixtures/valid/abort.json +8 -0
  16. package/dist/auth-turn/fixtures/valid/challenge.json +17 -0
  17. package/dist/auth-turn/fixtures/valid/complete.json +13 -0
  18. package/dist/auth-turn/fixtures/valid/form.json +14 -0
  19. package/dist/auth-turn/fixtures/valid/message.json +13 -0
  20. package/dist/auth-turn/fixtures/valid/multi_choice.json +15 -0
  21. package/dist/auth-turn/fixtures/valid/pending.json +5 -0
  22. package/dist/auth-turn/fixtures/valid/poll.json +9 -0
  23. package/dist/auth-turn/fixtures/valid/redirect.json +16 -0
  24. package/dist/auth-turn/fixtures/valid/retry.json +8 -0
  25. package/dist/auth-turn/fixtures/valid/unknown-kind.json +7 -0
  26. package/dist/auth-turn/index.d.ts +195 -0
  27. package/dist/auth-turn/index.js +133 -0
  28. package/dist/auth.d.ts +76 -0
  29. package/dist/auth.js +436 -0
  30. package/dist/ceremonies/index.js +7 -31
  31. package/dist/cli/create.js +45 -30
  32. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  33. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  34. package/dist/cli/templates/provider/AGENTS.md.tpl +87 -0
  35. package/dist/cli/templates/provider/CLAUDE.md.tpl +1 -0
  36. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  37. package/dist/cli/templates/provider/README.md.tpl +163 -0
  38. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  39. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  40. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  41. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  42. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  43. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  44. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  45. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  46. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  47. package/dist/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  48. package/dist/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  49. package/dist/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
  50. package/dist/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  51. package/dist/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  52. package/dist/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
  53. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  54. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  55. package/dist/contract.js +1 -0
  56. package/dist/define.d.ts +6 -1
  57. package/dist/define.js +140 -70
  58. package/dist/index.d.ts +3 -2
  59. package/dist/index.js +2 -1
  60. package/dist/lint.d.ts +1 -0
  61. package/dist/lint.js +27 -0
  62. package/dist/provider.d.ts +4 -2
  63. package/dist/provider.js +2 -1
  64. package/dist/runtime/auth-flow.js +2 -0
  65. package/dist/runtime/browser.js +203 -0
  66. package/dist/runtime/http.js +46 -336
  67. package/dist/runtime/proxy-retry-policy.d.ts +40 -0
  68. package/dist/runtime/proxy-retry-policy.js +326 -0
  69. package/dist/runtime/stealth.d.ts +5 -2
  70. package/dist/runtime/stealth.js +206 -206
  71. package/dist/server/index.d.ts +4 -0
  72. package/dist/server/index.js +4 -0
  73. package/dist/server/self-test-input-tokens.d.ts +1 -0
  74. package/dist/server/self-test-input-tokens.js +37 -0
  75. package/dist/server/self-test-redaction.d.ts +20 -0
  76. package/dist/server/self-test-redaction.js +70 -0
  77. package/dist/server/self-test-token.d.ts +30 -0
  78. package/dist/server/self-test-token.js +50 -0
  79. package/dist/server/self-test.d.ts +98 -0
  80. package/dist/server/self-test.js +555 -0
  81. package/dist/server/serve.d.ts +6 -0
  82. package/dist/server/serve.js +38 -10
  83. package/dist/server/types.d.ts +1 -0
  84. package/dist/server/types.js +1 -0
  85. package/dist/testing/run.js +5 -1
  86. package/dist/types.d.ts +150 -0
  87. package/package.json +9 -2
  88. package/src/auth-turn/auth-turn.v1.schema.json +89 -0
  89. package/src/auth-turn/fixtures/invalid/empty-kind.json +4 -0
  90. package/src/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
  91. package/src/auth-turn/fixtures/invalid/missing-kind.json +3 -0
  92. package/src/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
  93. package/src/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
  94. package/src/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
  95. package/src/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
  96. package/src/auth-turn/fixtures/valid/abort.json +8 -0
  97. package/src/auth-turn/fixtures/valid/challenge.json +17 -0
  98. package/src/auth-turn/fixtures/valid/complete.json +13 -0
  99. package/src/auth-turn/fixtures/valid/form.json +14 -0
  100. package/src/auth-turn/fixtures/valid/message.json +13 -0
  101. package/src/auth-turn/fixtures/valid/multi_choice.json +15 -0
  102. package/src/auth-turn/fixtures/valid/pending.json +5 -0
  103. package/src/auth-turn/fixtures/valid/poll.json +9 -0
  104. package/src/auth-turn/fixtures/valid/redirect.json +16 -0
  105. package/src/auth-turn/fixtures/valid/retry.json +8 -0
  106. package/src/auth-turn/fixtures/valid/unknown-kind.json +7 -0
  107. package/src/auth-turn/index.ts +177 -0
  108. package/src/auth.ts +786 -0
  109. package/src/ceremonies/index.ts +9 -43
  110. package/src/cli/create.ts +60 -97
  111. package/src/cli/templates/provider/AGENTS.md.tpl +87 -0
  112. package/src/cli/templates/provider/CLAUDE.md.tpl +1 -0
  113. package/src/cli/templates/provider/README.md.tpl +7 -4
  114. package/src/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  115. package/src/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  116. package/src/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
  117. package/src/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  118. package/src/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  119. package/src/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
  120. package/src/contract.ts +1 -0
  121. package/src/define.ts +198 -71
  122. package/src/index.ts +16 -0
  123. package/src/lint.ts +33 -0
  124. package/src/provider.ts +27 -0
  125. package/src/runtime/auth-flow.ts +2 -0
  126. package/src/runtime/browser.ts +293 -1
  127. package/src/runtime/http.ts +84 -530
  128. package/src/runtime/proxy-retry-policy.ts +469 -0
  129. package/src/runtime/stealth.ts +269 -353
  130. package/src/server/index.ts +36 -0
  131. package/src/server/self-test-input-tokens.ts +46 -0
  132. package/src/server/self-test-redaction.ts +97 -0
  133. package/src/server/self-test-token.ts +70 -0
  134. package/src/server/self-test.ts +725 -0
  135. package/src/server/serve.ts +75 -6
  136. package/src/server/types.ts +1 -0
  137. package/src/testing/run.ts +9 -1
  138. package/src/types.ts +186 -0
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { Hono } from "hono";
4
4
  import { z } from "zod";
5
+ import { AuthAbortError, createAuthFlowHelpers } from "../auth";
5
6
  import { AuthError, ProviderError, SessionExpiredError, TransportError, } from "../errors";
6
7
  import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog";
7
8
  import { categoryForStatus, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability";
@@ -24,6 +25,8 @@ import { createTraceContext } from "../runtime/trace";
24
25
  import { parseSchema } from "../schema";
25
26
  import { getStealthProfile } from "../stealth/profiles";
26
27
  import { APIFUSE_STREAM_DONE_EVENT, APIFUSE_STREAM_ERROR_EVENT, encodeSseEvent, error as streamError, } from "../stream";
28
+ import { createSelfTestApp, createSelfTestInvoke, resolveSelfTestPort, } from "./self-test";
29
+ import { resolveSelfTestMasterSecrets } from "./self-test-token";
27
30
  import { AuthFlowRequestSchema, OperationRequestSchema, } from "./types";
28
31
  const DEFAULT_HOST = "0.0.0.0";
29
32
  const DEFAULT_PORT = 3000;
@@ -104,7 +107,7 @@ function isProductionProviderBrowserMode(provider, env = process.env) {
104
107
  return (env.NODE_ENV === "production" && env.APIFUSE__PROVIDER__ID === provider.id);
105
108
  }
106
109
  export function resolveProviderProxyAffinityKey(provider, request, operationId) {
107
- const connectionKey = request.connection?.id ?? request.connection?.externalRef;
110
+ const connectionKey = resolveOperationConnectionId(request) ?? request.connection?.externalRef;
108
111
  const affinity = typeof provider.proxy === "object"
109
112
  ? provider.proxy.session?.affinity
110
113
  : undefined;
@@ -113,6 +116,9 @@ export function resolveProviderProxyAffinityKey(provider, request, operationId)
113
116
  }
114
117
  return connectionKey ?? provider.id;
115
118
  }
119
+ function resolveOperationConnectionId(request) {
120
+ return request.connection?.id ?? request.connectionId;
121
+ }
116
122
  function createProviderContext(provider, request, operationId, options = {}, state = createUnsupportedProviderRuntimeState(), proxyTelemetry) {
117
123
  const baseUrl = getProviderBaseUrl(provider);
118
124
  const stealthBaseUrl = getProviderStealthBaseUrl(provider);
@@ -139,7 +145,7 @@ function createProviderContext(provider, request, operationId, options = {}, sta
139
145
  values: request.connection?.secrets,
140
146
  });
141
147
  const requestContext = {
142
- connectionId: request.connection?.id,
148
+ connectionId: resolveOperationConnectionId(request),
143
149
  headers: request.headers ?? {},
144
150
  };
145
151
  const context = wrapWithInstrumentation({
@@ -209,7 +215,7 @@ function createFlowContextStore(allowedKeys, initialContext = {}) {
209
215
  },
210
216
  };
211
217
  }
212
- function createAuthFlowContext(provider, request, options = {}) {
218
+ function createAuthFlowContext(provider, request, options = {}, signal) {
213
219
  const baseUrl = getProviderBaseUrl(provider);
214
220
  const stealthBaseUrl = getProviderStealthBaseUrl(provider);
215
221
  const stealthProfile = getProviderStealthProfile(provider);
@@ -251,6 +257,7 @@ function createAuthFlowContext(provider, request, options = {}) {
251
257
  credential,
252
258
  context: flowContextStore.context,
253
259
  stt: options.stt ?? createSttClientFromEnv(provider.stt),
260
+ auth: createAuthFlowHelpers({ signal }),
254
261
  },
255
262
  getPatch: flowContextStore.getPatch,
256
263
  };
@@ -879,14 +886,14 @@ function responseWithProviderTelemetry(response, proxyTelemetry) {
879
886
  statusText: response.statusText,
880
887
  });
881
888
  }
882
- async function handleAuthFlow(provider, request, route, options = {}) {
889
+ async function handleAuthFlow(provider, request, route, options = {}, signal) {
883
890
  const flow = provider.auth?.flow;
884
891
  if (!flow) {
885
892
  throw new ProviderError("Auth flow is not configured", {
886
893
  code: "AUTH_FLOW_NOT_CONFIGURED",
887
894
  });
888
895
  }
889
- const { context, getPatch } = createAuthFlowContext(provider, request, options);
896
+ const { context, getPatch } = createAuthFlowContext(provider, request, options, signal);
890
897
  try {
891
898
  const result = route === "start"
892
899
  ? await flow.start(context)
@@ -916,6 +923,12 @@ async function handleAuthFlow(provider, request, route, options = {}) {
916
923
  : result;
917
924
  return toAuthFlowResponse(materializedResult, getPatch());
918
925
  }
926
+ catch (error) {
927
+ if (error instanceof AuthAbortError) {
928
+ return toAuthFlowResponse(error.turn, getPatch());
929
+ }
930
+ throw error;
931
+ }
919
932
  finally {
920
933
  context.stealth.close?.();
921
934
  }
@@ -982,7 +995,7 @@ export function createServerApp(provider, options = {}) {
982
995
  .json()
983
996
  .catch(() => undefined);
984
997
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
985
- const response = await handleAuthFlow(provider, body, "start", options);
998
+ const response = await handleAuthFlow(provider, body, "start", options, c.req.raw.signal);
986
999
  logProviderSuccess(logger, provider, "auth", "start", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
987
1000
  return response instanceof Response ? response : c.json(response);
988
1001
  }
@@ -1002,7 +1015,7 @@ export function createServerApp(provider, options = {}) {
1002
1015
  .json()
1003
1016
  .catch(() => undefined);
1004
1017
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
1005
- const response = await handleAuthFlow(provider, body, "continue", options);
1018
+ const response = await handleAuthFlow(provider, body, "continue", options, c.req.raw.signal);
1006
1019
  logProviderSuccess(logger, provider, "auth", "continue", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
1007
1020
  return response instanceof Response ? response : c.json(response);
1008
1021
  }
@@ -1022,7 +1035,7 @@ export function createServerApp(provider, options = {}) {
1022
1035
  .json()
1023
1036
  .catch(() => undefined);
1024
1037
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
1025
- const response = await handleAuthFlow(provider, body, "poll", options);
1038
+ const response = await handleAuthFlow(provider, body, "poll", options, c.req.raw.signal);
1026
1039
  logProviderSuccess(logger, provider, "auth", "poll", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
1027
1040
  return response instanceof Response ? response : c.json(response);
1028
1041
  }
@@ -1042,7 +1055,7 @@ export function createServerApp(provider, options = {}) {
1042
1055
  .json()
1043
1056
  .catch(() => undefined);
1044
1057
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
1045
- const response = await handleAuthFlow(provider, body, "refresh", options);
1058
+ const response = await handleAuthFlow(provider, body, "refresh", options, c.req.raw.signal);
1046
1059
  logProviderSuccess(logger, provider, "auth", "refresh", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
1047
1060
  return response instanceof Response ? response : c.json(response);
1048
1061
  }
@@ -1062,7 +1075,7 @@ export function createServerApp(provider, options = {}) {
1062
1075
  .json()
1063
1076
  .catch(() => undefined);
1064
1077
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
1065
- const response = await handleAuthFlow(provider, body, "abort", options);
1078
+ const response = await handleAuthFlow(provider, body, "abort", options, c.req.raw.signal);
1066
1079
  logProviderSuccess(logger, provider, "auth", "disconnect", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
1067
1080
  return response instanceof Response ? response : c.json(response);
1068
1081
  }
@@ -1106,5 +1119,20 @@ export async function serve(provider, options = {}) {
1106
1119
  hostname: options.host ?? DEFAULT_HOST,
1107
1120
  fetch: app.fetch,
1108
1121
  });
1122
+ // Internal self-test listener (health dependency inversion): a SEPARATE
1123
+ // socket the tenant-facing gateway never dials. Off by default — it only
1124
+ // starts when the shared self-test master secret env is present.
1125
+ const selfTestSecrets = resolveSelfTestMasterSecrets();
1126
+ if (selfTestSecrets) {
1127
+ const selfTestApp = createSelfTestApp(provider, {
1128
+ secrets: selfTestSecrets,
1129
+ invoke: createSelfTestInvoke(app),
1130
+ });
1131
+ bunRuntime.serve({
1132
+ port: options.selfTestPort ?? resolveSelfTestPort(),
1133
+ hostname: options.host ?? DEFAULT_HOST,
1134
+ fetch: selfTestApp.fetch,
1135
+ });
1136
+ }
1109
1137
  await Promise.resolve();
1110
1138
  }
@@ -21,6 +21,7 @@ export declare const OperationConnectionSchema: z.ZodObject<{
21
21
  export declare const OperationRequestSchema: z.ZodObject<{
22
22
  requestId: z.ZodString;
23
23
  input: z.ZodRecord<z.ZodString, z.ZodUnknown>;
24
+ connectionId: z.ZodOptional<z.ZodString>;
24
25
  connection: z.ZodOptional<z.ZodObject<{
25
26
  id: z.ZodString;
26
27
  mode: z.ZodEnum<{
@@ -17,6 +17,7 @@ export const OperationConnectionSchema = z.object({
17
17
  export const OperationRequestSchema = z.object({
18
18
  requestId: z.string(),
19
19
  input: z.record(z.string(), z.unknown()),
20
+ connectionId: z.string().optional(),
20
21
  connection: OperationConnectionSchema.optional(),
21
22
  headers: z.record(z.string(), z.string()).optional(),
22
23
  trace: z.record(z.string(), z.string()).optional(),
@@ -68,13 +68,17 @@ function inferFixtureDir(providerId) {
68
68
  return `providers/${providerId}/__fixtures__`;
69
69
  }
70
70
  function jsonResponse(data) {
71
+ const body = JSON.stringify(data);
72
+ const bodyBytes = new TextEncoder().encode(body);
71
73
  return {
72
74
  status: 200,
73
75
  ok: true,
74
76
  headers: {},
75
77
  data,
76
78
  json: async () => JSON.parse(JSON.stringify(data)),
77
- text: async () => JSON.stringify(data),
79
+ text: async () => body,
80
+ arrayBuffer: async () => bodyBytes.buffer.slice(bodyBytes.byteOffset, bodyBytes.byteOffset + bodyBytes.byteLength),
81
+ bytes: async () => bodyBytes.slice(0),
78
82
  };
79
83
  }
80
84
  function unsupported(name) {
package/dist/types.d.ts CHANGED
@@ -268,8 +268,16 @@ export interface HealthJourneySchedule {
268
268
  kind: "interval";
269
269
  /** ISO 8601 duration, for example PT8H. */
270
270
  interval: Iso8601Duration;
271
+ randomize?: HealthScheduleRandomization;
271
272
  jitter?: Iso8601Duration;
272
273
  }
274
+ export type HealthScheduleRandomization = {
275
+ mode: "centered";
276
+ maxOffset: Iso8601Duration;
277
+ } | {
278
+ mode: "delayed";
279
+ maxDelay: Iso8601Duration;
280
+ };
273
281
  export interface HealthJourneyStep {
274
282
  id: string;
275
283
  description?: string;
@@ -511,6 +519,9 @@ export interface HealthCheckCase<TInput = unknown, TOutput = unknown> {
511
519
  export interface HealthCheckSuite<TInput = unknown, TOutput = unknown> {
512
520
  /** Polling interval for the suite. All cases share this cadence. */
513
521
  interval: ProbeInterval;
522
+ schedule?: {
523
+ randomize?: HealthScheduleRandomization;
524
+ };
514
525
  /** Per-case timeout in milliseconds. Default: 30000. */
515
526
  timeoutMs?: number;
516
527
  /** Default degradation threshold for cases in this suite. Default: runtime threshold. */
@@ -583,6 +594,13 @@ export interface ProviderHealthMonitorConfig {
583
594
  */
584
595
  serviceAccount?: string;
585
596
  }
597
+ /**
598
+ * New name for `ProviderHealthMonitorConfig` as part of the health dependency
599
+ * inversion (`healthMonitor` → `healthProbe`). Both provider-level fields are
600
+ * accepted transitionally with identical validation; declaring both is a
601
+ * `ValidationError`.
602
+ */
603
+ export type ProviderHealthProbeConfig = ProviderHealthMonitorConfig;
586
604
  export interface HealthMonitorProbeOverride {
587
605
  /** Optional runtime interval override as a positive `ms`-style duration string. */
588
606
  interval?: ProbeInterval;
@@ -805,6 +823,7 @@ export type HttpMethod = "HEAD" | "head" | "GET" | "get" | "POST" | "post" | "PU
805
823
  export interface StealthFetchOptions extends RequestOptions {
806
824
  method?: HttpMethod;
807
825
  body?: string | Buffer;
826
+ redirect?: "follow" | "manual" | "error";
808
827
  /**
809
828
  * Offsets policy-managed proxy pool selection for caller-managed retries.
810
829
  * Use when a request receives an upstream challenge page rather than a
@@ -829,9 +848,19 @@ export interface CookieJar {
829
848
  toString(): string;
830
849
  find?(predicate: (cookie: string) => boolean): string | undefined;
831
850
  }
851
+ export interface StealthSessionCookies extends CookieJar {
852
+ has(name: string): boolean;
853
+ setFromCookieStrings(cookieStrings: readonly string[]): void;
854
+ toHeader(): string;
855
+ snapshot(): Record<string, string>;
856
+ restore(cookies: Record<string, string>): void;
857
+ clear(): void;
858
+ }
832
859
  export interface DeclarativeStealthResponse {
833
860
  status: number;
834
861
  ok: boolean;
862
+ url?: string;
863
+ redirected?: boolean;
835
864
  headers: Record<string, string>;
836
865
  rawHeaders: [string, string][];
837
866
  body: string;
@@ -851,8 +880,30 @@ export type RequestWithMethodOptions = RequestOptions & {
851
880
  method?: string;
852
881
  body?: unknown;
853
882
  };
883
+ export interface StealthRedirectHop {
884
+ url: string;
885
+ status: number;
886
+ method: string;
887
+ location?: string;
888
+ nextUrl?: string;
889
+ }
890
+ export interface StealthRedirectRunOptions extends Omit<StealthFetchOptions, "redirect"> {
891
+ url: string;
892
+ maxHops?: number;
893
+ stopWhen?: (hop: StealthRedirectHop) => boolean | Promise<boolean>;
894
+ }
895
+ export interface StealthRedirectRunResult {
896
+ final: StealthResponse;
897
+ hops: StealthRedirectHop[];
898
+ reason: "completed" | "stopped" | "max_hops" | "missing_location" | "loop";
899
+ cookies: Record<string, string>;
900
+ }
854
901
  export interface StealthSession {
855
902
  fetch(url: string, options?: StealthFetchOptions): Promise<StealthResponse>;
903
+ cookies: StealthSessionCookies;
904
+ redirects: {
905
+ run(options: StealthRedirectRunOptions): Promise<StealthRedirectRunResult>;
906
+ };
856
907
  close(): void;
857
908
  }
858
909
  export interface ApiFuseResponse<T> {
@@ -873,6 +924,8 @@ export interface HttpResponse<T = unknown> {
873
924
  data: T;
874
925
  json<U = T>(): Promise<U>;
875
926
  text(): Promise<string>;
927
+ arrayBuffer(): Promise<ArrayBuffer>;
928
+ bytes(): Promise<Uint8Array>;
876
929
  }
877
930
  export interface HttpStreamResponse {
878
931
  status: number;
@@ -983,6 +1036,32 @@ export interface BrowserFrame {
983
1036
  evaluate<T>(fn: string | (() => T)): Promise<T>;
984
1037
  locator(selector: string): BrowserLocator;
985
1038
  }
1039
+ export type BrowserResourceMethod = "GET" | "HEAD";
1040
+ export type BrowserResourceRequest = {
1041
+ readonly url: string;
1042
+ readonly method: BrowserResourceMethod;
1043
+ readonly resourceType?: string;
1044
+ readonly headers: Readonly<Record<string, string>>;
1045
+ };
1046
+ export type BrowserResourceBody = Buffer | Uint8Array | ArrayBuffer | string;
1047
+ export type BrowserResourceDecision = {
1048
+ readonly action: "fulfill";
1049
+ readonly status?: number;
1050
+ readonly headers?: Readonly<Record<string, string>>;
1051
+ readonly body?: BrowserResourceBody;
1052
+ } | {
1053
+ readonly action: "block";
1054
+ readonly reason?: string;
1055
+ };
1056
+ export type BrowserResourceRoute = {
1057
+ readonly match: string | RegExp | ((request: BrowserResourceRequest) => boolean);
1058
+ readonly handle: (request: BrowserResourceRequest) => Promise<BrowserResourceDecision> | BrowserResourceDecision;
1059
+ };
1060
+ export type BrowserResourcePolicy = {
1061
+ readonly defaultAction?: "block";
1062
+ readonly allowedMethods?: readonly BrowserResourceMethod[];
1063
+ readonly routes: readonly BrowserResourceRoute[];
1064
+ };
986
1065
  export interface BrowserPage extends BrowserFrame {
987
1066
  close(): Promise<void>;
988
1067
  fill(selector: string, text: string): Promise<void>;
@@ -997,6 +1076,7 @@ export interface BrowserPage extends BrowserFrame {
997
1076
  timeout?: number;
998
1077
  }): Promise<void>;
999
1078
  frames(): Promise<BrowserFrame[]>;
1079
+ withResourcePolicy<T>(policy: BrowserResourcePolicy, run: () => Promise<T>): Promise<T>;
1000
1080
  }
1001
1081
  export type BrowserChallengeRequest = {
1002
1082
  type: "recaptcha";
@@ -1137,6 +1217,73 @@ export interface ContextScratchpad {
1137
1217
  toJSON(): Record<string, unknown>;
1138
1218
  }
1139
1219
  export type FlowContextStore = ContextScratchpad;
1220
+ export type AuthSafeJson = string | number | boolean | null | readonly AuthSafeJson[] | {
1221
+ readonly [key: string]: AuthSafeJson;
1222
+ };
1223
+ export type AuthSafeData = {
1224
+ readonly [key: string]: AuthSafeJson;
1225
+ };
1226
+ export type AuthAbortRetry = "never" | "retry" | "after_user_action";
1227
+ export type AuthAbortData = Record<string, unknown> & {
1228
+ readonly code: string;
1229
+ readonly message?: string;
1230
+ readonly retry?: AuthAbortRetry;
1231
+ readonly actionHint?: AuthSafeJson;
1232
+ readonly fieldErrors?: {
1233
+ readonly [field: string]: string;
1234
+ };
1235
+ readonly details?: AuthSafeData;
1236
+ };
1237
+ export interface AuthFlowTerminalContext {
1238
+ readonly signal?: AbortSignal;
1239
+ readonly deadline?: string;
1240
+ complete<TCredential extends Record<string, string>>(options: {
1241
+ readonly credential: TCredential;
1242
+ readonly metadata?: AuthSafeData;
1243
+ readonly data?: AuthSafeData;
1244
+ readonly turnId?: string;
1245
+ readonly expiresAt?: string;
1246
+ }): AuthTurn;
1247
+ abort(options: {
1248
+ readonly code: string;
1249
+ readonly message?: string;
1250
+ readonly retry?: AuthAbortRetry;
1251
+ readonly actionHint?: AuthSafeJson;
1252
+ readonly fieldErrors?: {
1253
+ readonly [field: string]: string;
1254
+ };
1255
+ readonly data?: AuthSafeData;
1256
+ readonly turnId?: string;
1257
+ readonly expiresAt?: string;
1258
+ }): AuthTurn;
1259
+ nextForm(options: {
1260
+ readonly hintKey?: ProviderLocaleKeyInput;
1261
+ readonly data?: AuthSafeData;
1262
+ readonly turnId?: string;
1263
+ readonly expiresAt?: string;
1264
+ readonly timing?: AuthTurn["timing"];
1265
+ } & ({
1266
+ readonly fields: Record<string, {
1267
+ readonly type?: "string" | "email" | "password" | "otp";
1268
+ readonly labelKey?: ProviderLocaleKeyInput;
1269
+ readonly descriptionKey?: ProviderLocaleKeyInput;
1270
+ readonly placeholderKey?: ProviderLocaleKeyInput;
1271
+ readonly required?: boolean;
1272
+ readonly sensitive?: boolean;
1273
+ }>;
1274
+ readonly expectedInput?: never;
1275
+ } | {
1276
+ readonly expectedInput: Record<string, unknown>;
1277
+ readonly fields?: never;
1278
+ })): AuthTurn;
1279
+ nextPoll(options?: {
1280
+ readonly hintKey?: ProviderLocaleKeyInput;
1281
+ readonly data?: AuthSafeData;
1282
+ readonly turnId?: string;
1283
+ readonly expiresAt?: string;
1284
+ readonly timing?: AuthTurn["timing"];
1285
+ }): AuthTurn;
1286
+ }
1140
1287
  export interface FlowContext {
1141
1288
  connectionId?: string;
1142
1289
  externalRef?: string;
@@ -1148,6 +1295,7 @@ export interface FlowContext {
1148
1295
  credential?: CredentialContext;
1149
1296
  context: ContextScratchpad;
1150
1297
  stt: SttContext;
1298
+ auth: AuthFlowTerminalContext;
1151
1299
  }
1152
1300
  export interface AuthTurn {
1153
1301
  kind: string;
@@ -1322,5 +1470,7 @@ export interface ProviderDefinition {
1322
1470
  meta: ProviderMeta;
1323
1471
  operations: Record<string, OperationDefinition<SchemaLike, SchemaLike>>;
1324
1472
  healthMonitor?: ProviderHealthMonitorConfig;
1473
+ /** Transitional alias for `healthMonitor`; `defineProvider` mirrors both. */
1474
+ healthProbe?: ProviderHealthProbeConfig;
1325
1475
  healthJourneys?: readonly HealthJourneyDefinition[];
1326
1476
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
+ "version": "2.2.0-beta.2",
2
3
  "name": "@apifuse/provider-sdk",
3
- "version": "2.1.0-beta.9",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "APIFuse Provider SDK — Build providers with zero architectural constraints",
@@ -50,6 +50,12 @@
50
50
  "import": "./dist/contract.js",
51
51
  "default": "./dist/contract.js"
52
52
  },
53
+ "./auth-turn": {
54
+ "types": "./dist/auth-turn/index.d.ts",
55
+ "import": "./dist/auth-turn/index.js",
56
+ "default": "./dist/auth-turn/index.js"
57
+ },
58
+ "./auth-turn/auth-turn.v1.schema.json": "./dist/auth-turn/auth-turn.v1.schema.json",
53
59
  "./server": {
54
60
  "types": "./dist/server/index.d.ts",
55
61
  "import": "./dist/server/index.js",
@@ -77,7 +83,7 @@
77
83
  "pack:smoke": "bun run build && bun bin/apifuse-pack-smoke.ts",
78
84
  "release:guard": "bun scripts/guard-release-pr.ts",
79
85
  "format:check": "biome format .",
80
- "build": "tsgo -p tsconfig.build.json"
86
+ "build": "tsgo -p tsconfig.build.json && rm -rf dist/cli/templates && cp -R src/cli/templates dist/cli/templates && rm -rf dist/auth-turn/fixtures && cp -R src/auth-turn/fixtures dist/auth-turn/fixtures && cp src/auth-turn/auth-turn.v1.schema.json dist/auth-turn/auth-turn.v1.schema.json"
81
87
  },
82
88
  "devDependencies": {
83
89
  "@biomejs/biome": "^2.5.0",
@@ -88,6 +94,7 @@
88
94
  "dependencies": {
89
95
  "@clack/prompts": "^1.5.1",
90
96
  "@types/ms": "^2.1.0",
97
+ "acorn": "^8.17.0",
91
98
  "ajv": "^8.17",
92
99
  "hono": "^4.12.25",
93
100
  "impit": "0.14.1",
@@ -0,0 +1,89 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://apifuse.com/contracts/auth-turn/v1",
4
+ "title": "APIFuse AuthTurn envelope v1",
5
+ "description": "Protocol message exchanged during auth.flow ceremonies. kind is an open string; known kinds are tooling metadata, not a wire constraint.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["kind", "turnId"],
9
+ "properties": {
10
+ "kind": {
11
+ "type": "string",
12
+ "minLength": 1,
13
+ "description": "Open turn kind. Known kinds are listed in the TURN_KINDS registry; unknown kinds remain valid on the wire."
14
+ },
15
+ "turnId": {
16
+ "type": "string",
17
+ "minLength": 1,
18
+ "description": "Provider-scoped identifier of this turn."
19
+ },
20
+ "expiresAt": {
21
+ "type": "string",
22
+ "minLength": 1,
23
+ "description": "Turn expiry timestamp as an ISO 8601 / RFC 3339 string."
24
+ },
25
+ "data": {
26
+ "type": "object",
27
+ "additionalProperties": true,
28
+ "description": "Kind-specific payload. Terminal kinds carry the payloads described in $defs."
29
+ },
30
+ "expectedInput": {
31
+ "type": "object",
32
+ "additionalProperties": true,
33
+ "description": "JSON Schema describing the input expected next."
34
+ },
35
+ "hint": {
36
+ "type": "string",
37
+ "description": "Deprecated but load-bearing human-readable hint materialized from provider locale catalogs."
38
+ },
39
+ "hintKey": {
40
+ "type": "string",
41
+ "description": "Provider locale catalog key for the turn hint."
42
+ },
43
+ "timing": {
44
+ "type": "object",
45
+ "additionalProperties": false,
46
+ "description": "Client pacing guidance for poll-style turns.",
47
+ "properties": {
48
+ "suggestedPollIntervalMs": {
49
+ "type": "number",
50
+ "minimum": 1
51
+ },
52
+ "maxWaitMs": {
53
+ "type": "number",
54
+ "minimum": 1
55
+ }
56
+ }
57
+ }
58
+ },
59
+ "$defs": {
60
+ "completeTurnData": {
61
+ "title": "Terminal payload for kind \"complete\"",
62
+ "description": "data payload of a complete turn. The gateway extracts data.credential for persistence; complete turns are never echoed to browsers.",
63
+ "type": "object",
64
+ "additionalProperties": true,
65
+ "required": ["credential"],
66
+ "properties": {
67
+ "credential": {
68
+ "type": "object",
69
+ "additionalProperties": true
70
+ },
71
+ "metadata": {
72
+ "type": "object",
73
+ "additionalProperties": true
74
+ }
75
+ }
76
+ },
77
+ "abortTurnData": {
78
+ "title": "Terminal payload for kind \"abort\"",
79
+ "description": "data payload of an abort turn. code, when present, is the machine-readable abort reason.",
80
+ "type": "object",
81
+ "additionalProperties": true,
82
+ "properties": {
83
+ "code": {
84
+ "type": "string"
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "kind": "",
3
+ "turnId": "fixture.invalid.empty-kind"
4
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "kind": "challenge",
3
+ "turnId": "fixture.invalid.expires-at-not-string",
4
+ "expiresAt": 1767225600000
5
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "turnId": "fixture.invalid.missing-kind"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "kind": "form"
3
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "kind": "poll",
3
+ "turnId": "fixture.invalid.timing-unknown-field",
4
+ "timing": {
5
+ "recommendedIntervalMs": 5000
6
+ }
7
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "kind": "form",
3
+ "turn_id": "fixture.invalid.turn-id-snake-case"
4
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "kind": "form",
3
+ "turnId": "fixture.invalid.unknown-top-level-field",
4
+ "setCookie": "session=leaked-value"
5
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "kind": "abort",
3
+ "turnId": "fixture.abort",
4
+ "hint": "OAuth flow aborted.",
5
+ "data": {
6
+ "code": "flow_expired"
7
+ }
8
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "kind": "challenge",
3
+ "turnId": "fixture.challenge",
4
+ "hint": "Complete the WebAuthn prompt in your browser.",
5
+ "expiresAt": "2026-01-01T00:00:00.000Z",
6
+ "data": {
7
+ "challenge": "fixture-challenge-not-real",
8
+ "rpId": "example.com"
9
+ },
10
+ "expectedInput": {
11
+ "type": "object",
12
+ "required": ["attestation"],
13
+ "properties": {
14
+ "attestation": { "type": "object" }
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "kind": "complete",
3
+ "turnId": "fixture.complete",
4
+ "hint": "OAuth flow completed.",
5
+ "data": {
6
+ "credential": {
7
+ "accessToken": "fixture-fake-access-token-not-a-secret"
8
+ },
9
+ "metadata": {
10
+ "scope": "read"
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "kind": "form",
3
+ "turnId": "fixture.form",
4
+ "hint": "Provide the required input to continue.",
5
+ "data": {},
6
+ "expectedInput": {
7
+ "type": "object",
8
+ "required": ["apiKey"],
9
+ "properties": {
10
+ "apiKey": { "type": "string" }
11
+ },
12
+ "x-apifuse-field-order": ["apiKey"]
13
+ }
14
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "kind": "message",
3
+ "turnId": "fixture.message",
4
+ "hint": "Enter the code on the verification page, then poll for completion.",
5
+ "data": {
6
+ "user_code": "FIXT-CODE",
7
+ "verification_uri": "https://example.com/device"
8
+ },
9
+ "timing": {
10
+ "suggestedPollIntervalMs": 5000,
11
+ "maxWaitMs": 120000
12
+ }
13
+ }