@apifuse/provider-sdk 2.1.0-beta.8 → 2.2.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 (133) 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-perf.ts +18 -9
  7. package/bin/apifuse-submit-check.ts +1747 -388
  8. package/dist/auth-turn/auth-turn.v1.schema.json +89 -0
  9. package/dist/auth-turn/fixtures/invalid/empty-kind.json +4 -0
  10. package/dist/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
  11. package/dist/auth-turn/fixtures/invalid/missing-kind.json +3 -0
  12. package/dist/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
  13. package/dist/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
  14. package/dist/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
  15. package/dist/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
  16. package/dist/auth-turn/fixtures/valid/abort.json +8 -0
  17. package/dist/auth-turn/fixtures/valid/challenge.json +17 -0
  18. package/dist/auth-turn/fixtures/valid/complete.json +13 -0
  19. package/dist/auth-turn/fixtures/valid/form.json +14 -0
  20. package/dist/auth-turn/fixtures/valid/message.json +13 -0
  21. package/dist/auth-turn/fixtures/valid/multi_choice.json +15 -0
  22. package/dist/auth-turn/fixtures/valid/pending.json +5 -0
  23. package/dist/auth-turn/fixtures/valid/poll.json +9 -0
  24. package/dist/auth-turn/fixtures/valid/redirect.json +16 -0
  25. package/dist/auth-turn/fixtures/valid/retry.json +8 -0
  26. package/dist/auth-turn/fixtures/valid/unknown-kind.json +7 -0
  27. package/dist/auth-turn/index.d.ts +195 -0
  28. package/dist/auth-turn/index.js +133 -0
  29. package/dist/auth.d.ts +76 -0
  30. package/dist/auth.js +436 -0
  31. package/dist/ceremonies/index.js +7 -31
  32. package/dist/cli/create.js +45 -30
  33. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  34. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  35. package/dist/cli/templates/provider/AGENTS.md.tpl +87 -0
  36. package/dist/cli/templates/provider/CLAUDE.md.tpl +1 -0
  37. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  38. package/dist/cli/templates/provider/README.md.tpl +163 -0
  39. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  40. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  41. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  42. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  43. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  44. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  45. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  46. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  47. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  48. package/dist/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  49. package/dist/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  50. package/dist/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
  51. package/dist/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  52. package/dist/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  53. package/dist/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
  54. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  55. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  56. package/dist/contract.js +1 -0
  57. package/dist/define.d.ts +6 -1
  58. package/dist/define.js +140 -70
  59. package/dist/index.d.ts +3 -2
  60. package/dist/index.js +2 -1
  61. package/dist/lint.d.ts +1 -0
  62. package/dist/lint.js +27 -0
  63. package/dist/provider.d.ts +4 -2
  64. package/dist/provider.js +2 -1
  65. package/dist/runtime/auth-flow.js +2 -0
  66. package/dist/runtime/browser.js +203 -0
  67. package/dist/runtime/http.js +28 -8
  68. package/dist/runtime/stealth.d.ts +5 -2
  69. package/dist/runtime/stealth.js +157 -4
  70. package/dist/server/index.d.ts +4 -0
  71. package/dist/server/index.js +4 -0
  72. package/dist/server/self-test-input-tokens.d.ts +1 -0
  73. package/dist/server/self-test-input-tokens.js +37 -0
  74. package/dist/server/self-test-redaction.d.ts +20 -0
  75. package/dist/server/self-test-redaction.js +70 -0
  76. package/dist/server/self-test-token.d.ts +30 -0
  77. package/dist/server/self-test-token.js +50 -0
  78. package/dist/server/self-test.d.ts +98 -0
  79. package/dist/server/self-test.js +555 -0
  80. package/dist/server/serve.d.ts +6 -0
  81. package/dist/server/serve.js +33 -8
  82. package/dist/testing/run.js +5 -1
  83. package/dist/types.d.ts +152 -0
  84. package/package.json +10 -3
  85. package/src/auth-turn/auth-turn.v1.schema.json +89 -0
  86. package/src/auth-turn/fixtures/invalid/empty-kind.json +4 -0
  87. package/src/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
  88. package/src/auth-turn/fixtures/invalid/missing-kind.json +3 -0
  89. package/src/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
  90. package/src/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
  91. package/src/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
  92. package/src/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
  93. package/src/auth-turn/fixtures/valid/abort.json +8 -0
  94. package/src/auth-turn/fixtures/valid/challenge.json +17 -0
  95. package/src/auth-turn/fixtures/valid/complete.json +13 -0
  96. package/src/auth-turn/fixtures/valid/form.json +14 -0
  97. package/src/auth-turn/fixtures/valid/message.json +13 -0
  98. package/src/auth-turn/fixtures/valid/multi_choice.json +15 -0
  99. package/src/auth-turn/fixtures/valid/pending.json +5 -0
  100. package/src/auth-turn/fixtures/valid/poll.json +9 -0
  101. package/src/auth-turn/fixtures/valid/redirect.json +16 -0
  102. package/src/auth-turn/fixtures/valid/retry.json +8 -0
  103. package/src/auth-turn/fixtures/valid/unknown-kind.json +7 -0
  104. package/src/auth-turn/index.ts +177 -0
  105. package/src/auth.ts +786 -0
  106. package/src/ceremonies/index.ts +9 -43
  107. package/src/cli/create.ts +60 -97
  108. package/src/cli/templates/provider/AGENTS.md.tpl +87 -0
  109. package/src/cli/templates/provider/CLAUDE.md.tpl +1 -0
  110. package/src/cli/templates/provider/README.md.tpl +7 -4
  111. package/src/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  112. package/src/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  113. package/src/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
  114. package/src/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  115. package/src/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  116. package/src/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
  117. package/src/contract.ts +1 -0
  118. package/src/define.ts +198 -71
  119. package/src/index.ts +16 -0
  120. package/src/lint.ts +33 -0
  121. package/src/provider.ts +27 -0
  122. package/src/runtime/auth-flow.ts +2 -0
  123. package/src/runtime/browser.ts +293 -1
  124. package/src/runtime/http.ts +48 -7
  125. package/src/runtime/stealth.ts +190 -6
  126. package/src/server/index.ts +36 -0
  127. package/src/server/self-test-input-tokens.ts +46 -0
  128. package/src/server/self-test-redaction.ts +97 -0
  129. package/src/server/self-test-token.ts +70 -0
  130. package/src/server/self-test.ts +725 -0
  131. package/src/server/serve.ts +67 -4
  132. package/src/testing/run.ts +9 -1
  133. package/src/types.ts +188 -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;
@@ -209,7 +212,7 @@ function createFlowContextStore(allowedKeys, initialContext = {}) {
209
212
  },
210
213
  };
211
214
  }
212
- function createAuthFlowContext(provider, request, options = {}) {
215
+ function createAuthFlowContext(provider, request, options = {}, signal) {
213
216
  const baseUrl = getProviderBaseUrl(provider);
214
217
  const stealthBaseUrl = getProviderStealthBaseUrl(provider);
215
218
  const stealthProfile = getProviderStealthProfile(provider);
@@ -251,6 +254,7 @@ function createAuthFlowContext(provider, request, options = {}) {
251
254
  credential,
252
255
  context: flowContextStore.context,
253
256
  stt: options.stt ?? createSttClientFromEnv(provider.stt),
257
+ auth: createAuthFlowHelpers({ signal }),
254
258
  },
255
259
  getPatch: flowContextStore.getPatch,
256
260
  };
@@ -879,14 +883,14 @@ function responseWithProviderTelemetry(response, proxyTelemetry) {
879
883
  statusText: response.statusText,
880
884
  });
881
885
  }
882
- async function handleAuthFlow(provider, request, route, options = {}) {
886
+ async function handleAuthFlow(provider, request, route, options = {}, signal) {
883
887
  const flow = provider.auth?.flow;
884
888
  if (!flow) {
885
889
  throw new ProviderError("Auth flow is not configured", {
886
890
  code: "AUTH_FLOW_NOT_CONFIGURED",
887
891
  });
888
892
  }
889
- const { context, getPatch } = createAuthFlowContext(provider, request, options);
893
+ const { context, getPatch } = createAuthFlowContext(provider, request, options, signal);
890
894
  try {
891
895
  const result = route === "start"
892
896
  ? await flow.start(context)
@@ -916,6 +920,12 @@ async function handleAuthFlow(provider, request, route, options = {}) {
916
920
  : result;
917
921
  return toAuthFlowResponse(materializedResult, getPatch());
918
922
  }
923
+ catch (error) {
924
+ if (error instanceof AuthAbortError) {
925
+ return toAuthFlowResponse(error.turn, getPatch());
926
+ }
927
+ throw error;
928
+ }
919
929
  finally {
920
930
  context.stealth.close?.();
921
931
  }
@@ -982,7 +992,7 @@ export function createServerApp(provider, options = {}) {
982
992
  .json()
983
993
  .catch(() => undefined);
984
994
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
985
- const response = await handleAuthFlow(provider, body, "start", options);
995
+ const response = await handleAuthFlow(provider, body, "start", options, c.req.raw.signal);
986
996
  logProviderSuccess(logger, provider, "auth", "start", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
987
997
  return response instanceof Response ? response : c.json(response);
988
998
  }
@@ -1002,7 +1012,7 @@ export function createServerApp(provider, options = {}) {
1002
1012
  .json()
1003
1013
  .catch(() => undefined);
1004
1014
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
1005
- const response = await handleAuthFlow(provider, body, "continue", options);
1015
+ const response = await handleAuthFlow(provider, body, "continue", options, c.req.raw.signal);
1006
1016
  logProviderSuccess(logger, provider, "auth", "continue", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
1007
1017
  return response instanceof Response ? response : c.json(response);
1008
1018
  }
@@ -1022,7 +1032,7 @@ export function createServerApp(provider, options = {}) {
1022
1032
  .json()
1023
1033
  .catch(() => undefined);
1024
1034
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
1025
- const response = await handleAuthFlow(provider, body, "poll", options);
1035
+ const response = await handleAuthFlow(provider, body, "poll", options, c.req.raw.signal);
1026
1036
  logProviderSuccess(logger, provider, "auth", "poll", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
1027
1037
  return response instanceof Response ? response : c.json(response);
1028
1038
  }
@@ -1042,7 +1052,7 @@ export function createServerApp(provider, options = {}) {
1042
1052
  .json()
1043
1053
  .catch(() => undefined);
1044
1054
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
1045
- const response = await handleAuthFlow(provider, body, "refresh", options);
1055
+ const response = await handleAuthFlow(provider, body, "refresh", options, c.req.raw.signal);
1046
1056
  logProviderSuccess(logger, provider, "auth", "refresh", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
1047
1057
  return response instanceof Response ? response : c.json(response);
1048
1058
  }
@@ -1062,7 +1072,7 @@ export function createServerApp(provider, options = {}) {
1062
1072
  .json()
1063
1073
  .catch(() => undefined);
1064
1074
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
1065
- const response = await handleAuthFlow(provider, body, "abort", options);
1075
+ const response = await handleAuthFlow(provider, body, "abort", options, c.req.raw.signal);
1066
1076
  logProviderSuccess(logger, provider, "auth", "disconnect", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
1067
1077
  return response instanceof Response ? response : c.json(response);
1068
1078
  }
@@ -1106,5 +1116,20 @@ export async function serve(provider, options = {}) {
1106
1116
  hostname: options.host ?? DEFAULT_HOST,
1107
1117
  fetch: app.fetch,
1108
1118
  });
1119
+ // Internal self-test listener (health dependency inversion): a SEPARATE
1120
+ // socket the tenant-facing gateway never dials. Off by default — it only
1121
+ // starts when the shared self-test master secret env is present.
1122
+ const selfTestSecrets = resolveSelfTestMasterSecrets();
1123
+ if (selfTestSecrets) {
1124
+ const selfTestApp = createSelfTestApp(provider, {
1125
+ secrets: selfTestSecrets,
1126
+ invoke: createSelfTestInvoke(app),
1127
+ });
1128
+ bunRuntime.serve({
1129
+ port: options.selfTestPort ?? resolveSelfTestPort(),
1130
+ hostname: options.host ?? DEFAULT_HOST,
1131
+ fetch: selfTestApp.fetch,
1132
+ });
1133
+ }
1109
1134
  await Promise.resolve();
1110
1135
  }
@@ -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;
@@ -843,14 +872,38 @@ export interface DeclarativeStealthResponse {
843
872
  };
844
873
  cookies: CookieJar;
845
874
  json<T>(): Promise<T>;
875
+ arrayBuffer(): Promise<ArrayBuffer>;
876
+ bytes(): Promise<Uint8Array>;
846
877
  }
847
878
  export type StealthResponse = DeclarativeStealthResponse;
848
879
  export type RequestWithMethodOptions = RequestOptions & {
849
880
  method?: string;
850
881
  body?: unknown;
851
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
+ }
852
901
  export interface StealthSession {
853
902
  fetch(url: string, options?: StealthFetchOptions): Promise<StealthResponse>;
903
+ cookies: StealthSessionCookies;
904
+ redirects: {
905
+ run(options: StealthRedirectRunOptions): Promise<StealthRedirectRunResult>;
906
+ };
854
907
  close(): void;
855
908
  }
856
909
  export interface ApiFuseResponse<T> {
@@ -871,6 +924,8 @@ export interface HttpResponse<T = unknown> {
871
924
  data: T;
872
925
  json<U = T>(): Promise<U>;
873
926
  text(): Promise<string>;
927
+ arrayBuffer(): Promise<ArrayBuffer>;
928
+ bytes(): Promise<Uint8Array>;
874
929
  }
875
930
  export interface HttpStreamResponse {
876
931
  status: number;
@@ -981,6 +1036,32 @@ export interface BrowserFrame {
981
1036
  evaluate<T>(fn: string | (() => T)): Promise<T>;
982
1037
  locator(selector: string): BrowserLocator;
983
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
+ };
984
1065
  export interface BrowserPage extends BrowserFrame {
985
1066
  close(): Promise<void>;
986
1067
  fill(selector: string, text: string): Promise<void>;
@@ -995,6 +1076,7 @@ export interface BrowserPage extends BrowserFrame {
995
1076
  timeout?: number;
996
1077
  }): Promise<void>;
997
1078
  frames(): Promise<BrowserFrame[]>;
1079
+ withResourcePolicy<T>(policy: BrowserResourcePolicy, run: () => Promise<T>): Promise<T>;
998
1080
  }
999
1081
  export type BrowserChallengeRequest = {
1000
1082
  type: "recaptcha";
@@ -1135,6 +1217,73 @@ export interface ContextScratchpad {
1135
1217
  toJSON(): Record<string, unknown>;
1136
1218
  }
1137
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
+ }
1138
1287
  export interface FlowContext {
1139
1288
  connectionId?: string;
1140
1289
  externalRef?: string;
@@ -1146,6 +1295,7 @@ export interface FlowContext {
1146
1295
  credential?: CredentialContext;
1147
1296
  context: ContextScratchpad;
1148
1297
  stt: SttContext;
1298
+ auth: AuthFlowTerminalContext;
1149
1299
  }
1150
1300
  export interface AuthTurn {
1151
1301
  kind: string;
@@ -1320,5 +1470,7 @@ export interface ProviderDefinition {
1320
1470
  meta: ProviderMeta;
1321
1471
  operations: Record<string, OperationDefinition<SchemaLike, SchemaLike>>;
1322
1472
  healthMonitor?: ProviderHealthMonitorConfig;
1473
+ /** Transitional alias for `healthMonitor`; `defineProvider` mirrors both. */
1474
+ healthProbe?: ProviderHealthProbeConfig;
1323
1475
  healthJourneys?: readonly HealthJourneyDefinition[];
1324
1476
  }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
+ "version": "2.2.0-beta.1",
2
3
  "name": "@apifuse/provider-sdk",
3
- "version": "2.1.0-beta.8",
4
4
  "private": false,
5
5
  "type": "module",
6
- "description": "APIFuse Provider SDK \u2014 Build providers with zero architectural constraints",
6
+ "description": "APIFuse Provider SDK Build providers with zero architectural constraints",
7
7
  "license": "MIT",
8
8
  "main": "./dist/index.js",
9
9
  "types": "./dist/index.d.ts",
@@ -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
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "kind": "multi_choice",
3
+ "turnId": "fixture.multi_choice",
4
+ "hint": "Choose an authentication method.",
5
+ "data": {
6
+ "choices": ["oauth", "form"]
7
+ },
8
+ "expectedInput": {
9
+ "type": "object",
10
+ "required": ["choice"],
11
+ "properties": {
12
+ "choice": { "type": "string", "enum": ["oauth", "form"] }
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "kind": "pending",
3
+ "turnId": "credentials.noPendingChallenge",
4
+ "hintKey": "auth.challenge.pending"
5
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "kind": "poll",
3
+ "turnId": "fixture.poll",
4
+ "hint": "Continue polling until the device flow completes.",
5
+ "timing": {
6
+ "suggestedPollIntervalMs": 5000,
7
+ "maxWaitMs": 120000
8
+ }
9
+ }