@apifuse/provider-sdk 2.1.0-beta.12 → 2.1.0-beta.14

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.
package/dist/contract.js CHANGED
@@ -122,6 +122,7 @@ function extractHealthCheck(value) {
122
122
  return undefined;
123
123
  return compactObject({
124
124
  interval: value.interval,
125
+ schedule: toJsonValue(value.schedule),
125
126
  timeoutMs: value.timeoutMs,
126
127
  degradedThresholdMs: value.degradedThresholdMs,
127
128
  requiresConnection: value.requiresConnection,
package/dist/define.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AuthConfig, BrowserEngine, ContextDeclaration, CredentialDeclaration, HealthJourneyDefinition, HealthJourneySchedule, InferSchemaOutput, OperationDefinition, OperationHandlerResult, OperationHttpStreamTransport, OperationSseTransport, OperationWebSocketTransport, ProviderAccessConfig, ProviderDefinition, ProviderHealthMonitorConfig, ProviderProxyConfig, ProviderPublicProfile, ProviderReviewed, ProviderSecretDeclaration, ProviderStreamEvent, ProviderSttConfig, SchemaLike, SmsOtpMatcherDefinition, StealthPlatform } from "./types";
1
+ import type { AuthConfig, BrowserEngine, ContextDeclaration, CredentialDeclaration, HealthJourneyDefinition, HealthJourneySchedule, HealthScheduleRandomization, InferSchemaOutput, OperationDefinition, OperationHandlerResult, OperationHttpStreamTransport, OperationSseTransport, OperationWebSocketTransport, ProviderAccessConfig, ProviderDefinition, ProviderHealthMonitorConfig, ProviderProxyConfig, ProviderPublicProfile, ProviderReviewed, ProviderSecretDeclaration, ProviderStreamEvent, ProviderSttConfig, SchemaLike, SmsOtpMatcherDefinition, StealthPlatform } from "./types";
2
2
  type ProviderImplementationSourceAccess = "official_api" | "private_api" | "browser_flow" | "hybrid";
3
3
  type ProviderImplementationCredentialStrategy = "apifuse_managed" | "workspace_secret" | "user_oauth" | "user_session" | "none";
4
4
  interface ProviderImplementationProfile {
@@ -88,7 +88,10 @@ export declare function defineOperation<TInput extends SchemaLike, TOutput exten
88
88
  export declare function defineStreamOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(operation: StreamOperationConfig<TInput, TOutput>): OperationDefinition<TInput, TOutput>;
89
89
  export declare function every(interval: string, options?: {
90
90
  jitter?: string;
91
+ randomize?: HealthScheduleRandomization;
91
92
  }): HealthJourneySchedule;
93
+ export declare function centered(maxOffset: string): HealthScheduleRandomization;
94
+ export declare function delayed(maxDelay: string): HealthScheduleRandomization;
92
95
  export declare function defineSmsOtpMatcher(config: Omit<SmsOtpMatcherDefinition, "extractOtp">): SmsOtpMatcherDefinition;
93
96
  export declare function defineHealthJourney(config: HealthJourneyDefinition): HealthJourneyDefinition;
94
97
  export declare function defineProvider<TOperations extends Record<string, ProviderOperation>, TConfig extends ProviderConfig<TOperations>>(config: TConfig & AuthStartNoInputGuard<TConfig>): ProviderDefinition & {
package/dist/define.js CHANGED
@@ -1,3 +1,4 @@
1
+ import ms from "ms";
1
2
  import { ProviderError, ValidationError } from "./errors";
2
3
  import { safeParseSchemaSync } from "./schema";
3
4
  import { HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX, HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN, HEALTH_CHECK_TIMEOUT_MS_MAX, HEALTH_CHECK_TIMEOUT_MS_MIN, OPERATION_TIMEOUT_MS_MAX, OPERATION_TIMEOUT_MS_MIN, STREAM_CHUNK_BYTES_MAX, STREAM_CHUNK_BYTES_MIN, STREAM_HEARTBEAT_MS_MAX, STREAM_HEARTBEAT_MS_MIN, STREAM_IDLE_TIMEOUT_MS_MAX, STREAM_IDLE_TIMEOUT_MS_MIN, STREAM_MAX_DURATION_MS_MAX, STREAM_MAX_DURATION_MS_MIN, } from "./types";
@@ -50,51 +51,23 @@ const VALID_OPERATION_TRANSPORT_KINDS = [
50
51
  ];
51
52
  const SSE_EVENT_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
52
53
  const WEBSOCKET_SUBPROTOCOL_REGEX = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
53
- const MS_DURATION_UNITS = new Set([
54
- "years",
55
- "year",
56
- "yrs",
57
- "yr",
58
- "y",
59
- "weeks",
60
- "week",
61
- "w",
62
- "days",
63
- "day",
64
- "d",
65
- "hours",
66
- "hour",
67
- "hrs",
68
- "hr",
69
- "h",
70
- "minutes",
71
- "minute",
72
- "mins",
73
- "min",
74
- "m",
75
- "seconds",
76
- "second",
77
- "secs",
78
- "sec",
79
- "s",
80
- "milliseconds",
81
- "millisecond",
82
- "msecs",
83
- "msec",
84
- "ms",
85
- ]);
86
54
  const MS_DURATION_PATTERN = /^([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*([a-zA-Z]+)?$/;
87
55
  function isPositiveMsDurationString(value) {
88
56
  if (typeof value !== "string")
89
57
  return false;
90
- const match = value.trim().match(MS_DURATION_PATTERN);
91
- if (!match)
92
- return false;
93
- const amount = Number(match[1]);
94
- if (!Number.isFinite(amount) || amount <= 0)
95
- return false;
96
- const unit = match[2]?.toLowerCase();
97
- return unit === undefined || MS_DURATION_UNITS.has(unit);
58
+ return parsePositiveMsDuration(value) !== undefined;
59
+ }
60
+ function msDurationMs(value) {
61
+ return parsePositiveMsDuration(value) ?? 0;
62
+ }
63
+ function parsePositiveMsDuration(value) {
64
+ const trimmed = value.trim();
65
+ if (!MS_DURATION_PATTERN.test(trimmed))
66
+ return undefined;
67
+ const parsed = ms((trimmed.startsWith("+") ? trimmed.slice(1) : trimmed));
68
+ if (!Number.isFinite(parsed) || parsed <= 0)
69
+ return undefined;
70
+ return parsed;
98
71
  }
99
72
  /** Define one provider operation with schema-driven handler inference. */
100
73
  export function defineOperation(operation) {
@@ -141,6 +114,11 @@ function validateProviderShape(config) {
141
114
  "mode" in auth &&
142
115
  typeof auth.mode === "string")
143
116
  assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
117
+ if (auth && typeof auth === "object" && "exchange" in auth) {
118
+ throw new ProviderError(`Provider "${String(config.id)}" auth.exchange is not part of the Provider SDK auth contract`, {
119
+ fix: "Use the single canonical auth interface: auth.flow. Gateway calls auth.flow.start/continue/poll/abort/refresh only and persists complete turn data.credential as-is, so put login/token/session exchange inside auth.flow.continue.",
120
+ });
121
+ }
144
122
  if (auth &&
145
123
  typeof auth === "object" &&
146
124
  "flow" in auth &&
@@ -555,6 +533,7 @@ function validateOperationTransports(providerId, operations) {
555
533
  }
556
534
  const HEALTH_CHECK_SUITE_FIELDS = new Set([
557
535
  "interval",
536
+ "schedule",
558
537
  "timeoutMs",
559
538
  "degradedThresholdMs",
560
539
  "cases",
@@ -776,6 +755,21 @@ function validateHealthCheckSuite(providerId, operationName, suite) {
776
755
  throw new ValidationError(`Provider "${providerId}" ${fieldPath}.interval must be a positive ms-style duration string such as 30s, 5m, 8h, or 1 day.`, {
777
756
  fix: `Set ${fieldPath}.interval to a positive ms-style duration string.`,
778
757
  });
758
+ if (s.schedule !== undefined) {
759
+ if (!s.schedule ||
760
+ typeof s.schedule !== "object" ||
761
+ Array.isArray(s.schedule)) {
762
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath}.schedule must be an object.`);
763
+ }
764
+ if (Reflect.get(s.schedule, "jitter") !== undefined) {
765
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath}.schedule.jitter is not supported for operation healthCheck schedules. Use schedule.randomize instead.`);
766
+ }
767
+ rejectUnknownFields(s.schedule, new Set(["randomize"]), `${fieldPath}.schedule`);
768
+ const randomize = Reflect.get(s.schedule, "randomize");
769
+ if (randomize !== undefined) {
770
+ validateScheduleRandomization(randomize, `Provider "${providerId}" ${fieldPath}.schedule.randomize`, msDurationMs(s.interval));
771
+ }
772
+ }
779
773
  if (s.timeoutMs !== undefined) {
780
774
  assertBoundedIntegerMs(s.timeoutMs, `Provider "${providerId}" ${fieldPath}.timeoutMs`, {
781
775
  min: HEALTH_CHECK_TIMEOUT_MS_MIN,
@@ -837,7 +831,12 @@ const HEALTH_JOURNEY_FIELDS = new Set([
837
831
  "steps",
838
832
  "run",
839
833
  ]);
840
- const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter"]);
834
+ const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set([
835
+ "kind",
836
+ "interval",
837
+ "jitter",
838
+ "randomize",
839
+ ]);
841
840
  const HEALTH_JOURNEY_STEP_FIELDS = new Set([
842
841
  "id",
843
842
  "description",
@@ -953,6 +952,40 @@ function isoDurationMs(value) {
953
952
  const seconds = Number(/(\d+(?:\.\d+)?)S/.exec(value)?.[1] ?? 0);
954
953
  return (days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000);
955
954
  }
955
+ function scheduleRandomizationMs(randomize, fieldPath) {
956
+ const mode = Reflect.get(randomize, "mode");
957
+ switch (mode) {
958
+ case "centered": {
959
+ const maxOffset = Reflect.get(randomize, "maxOffset");
960
+ assertIsoDuration(maxOffset, `${fieldPath}.maxOffset`);
961
+ return isoDurationMs(maxOffset);
962
+ }
963
+ case "delayed": {
964
+ const maxDelay = Reflect.get(randomize, "maxDelay");
965
+ assertIsoDuration(maxDelay, `${fieldPath}.maxDelay`);
966
+ return isoDurationMs(maxDelay);
967
+ }
968
+ default:
969
+ throw new ValidationError(`${fieldPath}.mode must be "centered" or "delayed".`);
970
+ }
971
+ }
972
+ function validateScheduleRandomization(randomize, fieldPath, intervalMs) {
973
+ if (!randomize || typeof randomize !== "object" || Array.isArray(randomize)) {
974
+ throw new ValidationError(`${fieldPath} must be an object.`);
975
+ }
976
+ const mode = Reflect.get(randomize, "mode");
977
+ const allowedFields = mode === "centered"
978
+ ? new Set(["mode", "maxOffset"])
979
+ : new Set(["mode", "maxDelay"]);
980
+ rejectUnknownFields(randomize, allowedFields, fieldPath);
981
+ const offsetMs = scheduleRandomizationMs(randomize, fieldPath);
982
+ if (offsetMs <= 0) {
983
+ throw new ValidationError(`${fieldPath} duration must be positive.`);
984
+ }
985
+ if (offsetMs >= intervalMs) {
986
+ throw new ValidationError(`${fieldPath} duration must be shorter than schedule interval.`);
987
+ }
988
+ }
956
989
  function assertIsoCountry(value, fieldPath) {
957
990
  if (typeof value !== "string" || !ISO_COUNTRY_RE.test(value)) {
958
991
  throw new ValidationError(`${fieldPath} must be an ISO 3166-1 alpha-2 country code for example KR.`);
@@ -962,11 +995,18 @@ function normalizeIntervalDuration(input) {
962
995
  const trimmed = input.trim();
963
996
  const shorthand = /^(\d+)(s|m|h|d)$/i.exec(trimmed);
964
997
  if (shorthand) {
965
- const amount = Number(shorthand[1]);
998
+ const durationMs = msDurationMs(trimmed);
999
+ const unit = shorthand[2]?.toLowerCase();
1000
+ const amount = unit === "s"
1001
+ ? durationMs / 1_000
1002
+ : unit === "m"
1003
+ ? durationMs / 60_000
1004
+ : unit === "h"
1005
+ ? durationMs / 3_600_000
1006
+ : durationMs / 86_400_000;
966
1007
  if (!Number.isInteger(amount) || amount <= 0) {
967
1008
  throw new ValidationError(`Journey schedule interval must be a positive duration.`);
968
1009
  }
969
- const unit = shorthand[2]?.toLowerCase();
970
1010
  if (unit === "s")
971
1011
  return `PT${amount}S`;
972
1012
  if (unit === "m")
@@ -980,15 +1020,27 @@ function normalizeIntervalDuration(input) {
980
1020
  return trimmed;
981
1021
  }
982
1022
  export function every(interval, options = {}) {
1023
+ if (options.jitter !== undefined && options.randomize !== undefined) {
1024
+ throw new ValidationError(`Schedule cannot define both jitter and randomize. Use randomize instead.`);
1025
+ }
983
1026
  const schedule = {
984
1027
  kind: "interval",
985
1028
  interval: normalizeIntervalDuration(interval),
986
1029
  };
1030
+ if (options.randomize !== undefined) {
1031
+ schedule.randomize = options.randomize;
1032
+ }
987
1033
  if (options.jitter !== undefined) {
988
1034
  schedule.jitter = normalizeIntervalDuration(options.jitter);
989
1035
  }
990
1036
  return schedule;
991
1037
  }
1038
+ export function centered(maxOffset) {
1039
+ return { mode: "centered", maxOffset: normalizeIntervalDuration(maxOffset) };
1040
+ }
1041
+ export function delayed(maxDelay) {
1042
+ return { mode: "delayed", maxDelay: normalizeIntervalDuration(maxDelay) };
1043
+ }
992
1044
  function countCapturingGroups(pattern) {
993
1045
  let count = 0;
994
1046
  const source = pattern.source;
@@ -1140,9 +1192,18 @@ function validateHealthJourneySchedule(providerId, journeyId, schedule) {
1140
1192
  rejectUnknownFields(schedule, HEALTH_JOURNEY_SCHEDULE_FIELDS, fieldPath);
1141
1193
  if (Reflect.get(schedule, "kind") !== "interval")
1142
1194
  throw new ValidationError(`Provider "${providerId}" ${fieldPath}.kind must be "interval".`);
1143
- assertIsoDuration(Reflect.get(schedule, "interval"), `Provider "${providerId}" ${fieldPath}.interval`);
1195
+ const interval = Reflect.get(schedule, "interval");
1196
+ assertIsoDuration(interval, `Provider "${providerId}" ${fieldPath}.interval`);
1197
+ const randomize = Reflect.get(schedule, "randomize");
1198
+ if (Reflect.get(schedule, "jitter") !== undefined &&
1199
+ randomize !== undefined) {
1200
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath} cannot define both jitter and randomize.`);
1201
+ }
1144
1202
  if (Reflect.get(schedule, "jitter") !== undefined)
1145
1203
  assertIsoDuration(Reflect.get(schedule, "jitter"), `Provider "${providerId}" ${fieldPath}.jitter`);
1204
+ if (randomize !== undefined) {
1205
+ validateScheduleRandomization(randomize, `Provider "${providerId}" ${fieldPath}.randomize`, isoDurationMs(interval));
1206
+ }
1146
1207
  }
1147
1208
  function validateHealthJourneys(providerId, operations, healthJourneys) {
1148
1209
  const covered = new Set();
@@ -1278,7 +1339,9 @@ export function defineProvider(config) {
1278
1339
  fix: 'Use lowercase alphanumeric with dashes, e.g., "korea-air-quality"',
1279
1340
  });
1280
1341
  if (Object.keys(config.operations).length === 0)
1281
- throw new ProviderError(`Provider "${config.id}" must define at least one operation`, { fix: "Add at least one operation to the operations object" });
1342
+ throw new ProviderError(`Provider "${config.id}" must define at least one operation`, {
1343
+ fix: "Add at least one operation to the operations object",
1344
+ });
1282
1345
  validateOperationIds(config.id, config.operations);
1283
1346
  validateOperationAnnotations(config.id, config.operations);
1284
1347
  validateOperationObservability(config.id, config.operations);
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
+ export * from "./auth";
1
2
  export * from "./ceremonies";
2
3
  export * from "./choice-token";
3
4
  export type { ApiFuseConfig, BrowserConfig, ProxyConfig, SessionConfig, } from "./config/loader";
4
5
  export { defineConfig, loadApiFuseConfig } from "./config/loader";
5
6
  export { canonicalJson, digestProviderContract, extractProviderContract, type JsonPrimitive, type JsonValue, PROVIDER_CONTRACT_SCHEMA_VERSION, type ProviderContractOperation, type ProviderContractSnapshot, } from "./contract";
6
- export { defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, defineStreamOperation, every, type ProviderConfig, } from "./define";
7
+ export { centered, delayed, defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, defineStreamOperation, every, type ProviderConfig, } from "./define";
7
8
  export type { DevServerOptions } from "./dev";
8
9
  export { createDevServer, startDevServer } from "./dev";
9
10
  export * from "./errors";
@@ -33,7 +34,7 @@ export { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SEN
33
34
  export { createServerApp, type ServeOptions, serve } from "./server";
34
35
  export { getStealthProfile, listStealthProfiles } from "./stealth/profiles";
35
36
  export * from "./stream";
36
- export type { ApiFuseResponse, AuthConfig, AuthContext, AuthFlowDefinition, AuthFlowInputHandler, AuthFlowStartHandler, AuthMode, AuthTurn, Bcp47Locale, BrowserEngine, BrowserOptions, ConnectionMode, ContextDeclaration, CookieJar, ContextScratchpad, CredentialContext, CredentialDeclaration, E164PhoneNumber, EnvContext, FlowContext, FlowContextStore, HealthCheckAssertionContext, HealthCheckCase, HealthCheckCaseResult, HealthCheckSuite, HealthCheckUnsupported, HealthJourneyDefinition, HealthJourneyEventContext, HealthJourneyGatewayContext, HealthJourneyJournalContext, HealthJourneyManualTriggerPolicy, HealthJourneyRunContext, HealthJourneyRunResult, HealthJourneySchedule, HealthJourneySmsContext, HealthJourneyStep, HttpClient, HttpMethod, HttpResponse, HttpRetryOptions, HttpRetrySummary, HttpStreamResponse, IanaTimeZone, InferSchemaOutput, Iso3166Alpha2CountryCode, Iso4217CurrencyCode, Iso8601Duration, OperationAnnotations, OperationApprovalPolicy, OperationContractMetadata, OperationDefinition, OperationDeprecationMetadata, OperationDocMeta, OperationErrorCode, OperationHandlerResult, OperationInputExample, OperationLifecycle, OperationObservabilityConfig, OperationObservabilitySensitiveConfig, OperationRelationships, OperationRiskClass, OperationSensitivePath, OperationToolRouterMetadata, OperationTransport, OperationTransportKind, ProbeInterval, ProviderAccessConfig, ProviderAccessVisibility, ProviderCache, ProviderCacheGetOrSetOptions, ProviderCacheKeyOptions, ProviderCacheLookupMeta, ProviderCacheResponseMeta, ProviderCacheResult, ProviderChoiceBindingOptions, ProviderChoiceContext, ProviderChoiceIssueOptions, ProviderChoiceParseOptions, ProviderContext, ProviderDefinition, ProviderHealthMonitorConfig, ProviderLocale, ProviderLocaleKey, ProviderLocaleKeyInput, ProviderLogoProfile, ProviderLogoSource, ProviderMeta, ProviderProxyConfig, ProviderProxyMode, ProviderProxyPolicy, ProviderProxyProvider, ProviderProxySessionAffinity, ProviderPublicConnectionMode, ProviderPublicProfile, ProviderReviewed, ProviderRuntimeState, ProviderSecretDeclaration, ProviderStateDurationString, ProviderStateNamespace, ProviderStreamEvent, ProviderSttConfig, ProviderSttMode, ProviderSupportLevel, RequestOptions, Rfc3339Instant, SchemaLike, SmsOrigin, SmsOtpExtractionPattern, SmsOtpMatcherDefinition, SseMessage, StandardSchemaV1, StateCasResult, StateNamespaceOptions, StateValue, StateWriteOptions, StealthClient, StealthFetchOptions, StealthPlatform, StealthProfile, StealthRedirectHop, StealthRedirectRunOptions, StealthRedirectRunResult, StealthResponse, StealthSession, StealthSessionCookies, SttAudioInput, SttContext, SttPromptPolicy, SttSegment, SttTranscribeMode, SttTranscribeRequest, SttTranscript, SttUnsupportedOptionPolicy, SttUsage, SttVerificationCodeOptions, SttWarning, TraceConfig, TraceSpan, VerificationCodeCandidate, VerificationCodeCandidateSource, VerificationCodeExtractionResult, } from "./types";
37
+ export type { ApiFuseResponse, AuthConfig, AuthContext, AuthFlowDefinition, AuthFlowInputHandler, AuthFlowStartHandler, AuthMode, AuthTurn, Bcp47Locale, BrowserEngine, BrowserOptions, BrowserResourceBody, BrowserResourceDecision, BrowserResourceMethod, BrowserResourcePolicy, BrowserResourceRequest, BrowserResourceRoute, ConnectionMode, ContextDeclaration, CookieJar, ContextScratchpad, CredentialContext, CredentialDeclaration, E164PhoneNumber, EnvContext, FlowContext, FlowContextStore, HealthCheckAssertionContext, HealthCheckCase, HealthCheckCaseResult, HealthCheckSuite, HealthCheckUnsupported, HealthJourneyDefinition, HealthJourneyEventContext, HealthJourneyGatewayContext, HealthJourneyJournalContext, HealthJourneyManualTriggerPolicy, HealthJourneyRunContext, HealthJourneyRunResult, HealthJourneySchedule, HealthScheduleRandomization, HealthJourneySmsContext, HealthJourneyStep, HttpClient, HttpMethod, HttpResponse, HttpRetryOptions, HttpRetrySummary, HttpStreamResponse, IanaTimeZone, InferSchemaOutput, Iso3166Alpha2CountryCode, Iso4217CurrencyCode, Iso8601Duration, OperationAnnotations, OperationApprovalPolicy, OperationContractMetadata, OperationDefinition, OperationDeprecationMetadata, OperationDocMeta, OperationErrorCode, OperationHandlerResult, OperationInputExample, OperationLifecycle, OperationObservabilityConfig, OperationObservabilitySensitiveConfig, OperationRelationships, OperationRiskClass, OperationSensitivePath, OperationToolRouterMetadata, OperationTransport, OperationTransportKind, ProbeInterval, ProviderAccessConfig, ProviderAccessVisibility, ProviderCache, ProviderCacheGetOrSetOptions, ProviderCacheKeyOptions, ProviderCacheLookupMeta, ProviderCacheResponseMeta, ProviderCacheResult, ProviderChoiceBindingOptions, ProviderChoiceContext, ProviderChoiceIssueOptions, ProviderChoiceParseOptions, ProviderContext, ProviderDefinition, ProviderHealthMonitorConfig, ProviderLocale, ProviderLocaleKey, ProviderLocaleKeyInput, ProviderLogoProfile, ProviderLogoSource, ProviderMeta, ProviderProxyConfig, ProviderProxyMode, ProviderProxyPolicy, ProviderProxyProvider, ProviderProxySessionAffinity, ProviderPublicConnectionMode, ProviderPublicProfile, ProviderReviewed, ProviderRuntimeState, ProviderSecretDeclaration, ProviderStateDurationString, ProviderStateNamespace, ProviderStreamEvent, ProviderSttConfig, ProviderSttMode, ProviderSupportLevel, RequestOptions, Rfc3339Instant, SchemaLike, SmsOrigin, SmsOtpExtractionPattern, SmsOtpMatcherDefinition, SseMessage, StandardSchemaV1, StateCasResult, StateNamespaceOptions, StateValue, StateWriteOptions, StealthClient, StealthFetchOptions, StealthPlatform, StealthProfile, StealthRedirectHop, StealthRedirectRunOptions, StealthRedirectRunResult, StealthResponse, StealthSession, StealthSessionCookies, SttAudioInput, SttContext, SttPromptPolicy, SttSegment, SttTranscribeMode, SttTranscribeRequest, SttTranscript, SttUnsupportedOptionPolicy, SttUsage, SttVerificationCodeOptions, SttWarning, TraceConfig, TraceSpan, VerificationCodeCandidate, VerificationCodeCandidateSource, VerificationCodeExtractionResult, } from "./types";
37
38
  export { DEFAULT_OPERATION_TRANSPORT, HttpRetryAfterPolicy, HttpRetryDelayStrategy, HttpRetryJitter, HttpRetryPreset, HttpRetryUnsafeMethodPolicy, PROBE_INTERVALS, STREAM_CHUNK_BYTES_MAX, STREAM_CHUNK_BYTES_MIN, STREAM_HEARTBEAT_MS_MAX, STREAM_HEARTBEAT_MS_MIN, STREAM_IDLE_TIMEOUT_MS_MAX, STREAM_IDLE_TIMEOUT_MS_MIN, STREAM_MAX_DURATION_MS_MAX, STREAM_MAX_DURATION_MS_MIN, } from "./types";
38
39
  export * from "./utils/date";
39
40
  export * from "./utils/parse";
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  // @apifuse/provider-sdk
2
+ export * from "./auth";
2
3
  export * from "./ceremonies";
3
4
  export * from "./choice-token";
4
5
  export { defineConfig, loadApiFuseConfig } from "./config/loader";
5
6
  export { canonicalJson, digestProviderContract, extractProviderContract, PROVIDER_CONTRACT_SCHEMA_VERSION, } from "./contract";
6
- export { defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, defineStreamOperation, every, } from "./define";
7
+ export { centered, delayed, defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, defineStreamOperation, every, } from "./define";
7
8
  export { createDevServer, startDevServer } from "./dev";
8
9
  export * from "./errors";
9
10
  export * from "./i18n";
package/dist/lint.d.ts CHANGED
@@ -8,6 +8,7 @@ type ProviderAuthLike = {
8
8
  abort?: unknown;
9
9
  refresh?: unknown;
10
10
  };
11
+ exchange?: unknown;
11
12
  };
12
13
  type ProviderContractMetaLike = {
13
14
  publicSchemaFieldNames?: "normalized";
package/dist/lint.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { lintPublicSchemaFieldNames } from "./public-schema-field-lint";
2
2
  import { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_SENSITIVE_META_KEY, } from "./schema";
3
+ const AUTH_OPERATION_ID_PATTERN = /^(?:auth[-_])?(?:login|exchange|continue|refresh|callback)(?:[-_]|$)/i;
3
4
  function lintAllowedHosts(providerId, allowedHosts) {
4
5
  const prefix = providerId ? `Provider "${providerId}"` : "Provider";
5
6
  if (!allowedHosts) {
@@ -49,6 +50,9 @@ function lintReviewed(providerId, reviewed) {
49
50
  },
50
51
  ];
51
52
  }
53
+ function isProviderAuthLike(value) {
54
+ return !!value && typeof value === "object" && !Array.isArray(value);
55
+ }
52
56
  function hasReusableSecretKeys(keys) {
53
57
  if (!keys) {
54
58
  return false;
@@ -99,6 +103,14 @@ function lintAuthModel(provider) {
99
103
  message: `${providerLabel} must define auth.flow.continue for ${authMode} auth mode.`,
100
104
  });
101
105
  }
106
+ if (isProviderAuthLike(provider.auth) && "exchange" in provider.auth) {
107
+ diagnostics.push({
108
+ rule: "auth-exchange-unsupported",
109
+ level: "error",
110
+ field: "auth.exchange",
111
+ message: `${providerLabel} must not define auth.exchange. The Provider SDK has one auth interface: auth.flow. Gateway only calls auth.flow.start/continue/poll/abort/refresh and persists complete turn data.credential as-is; put login/token/session exchange inside auth.flow.continue.`,
112
+ });
113
+ }
102
114
  if (authMode === "credentials" && credentialKeys.length === 0) {
103
115
  diagnostics.push({
104
116
  rule: "credential-keys-required-when-credentials-mode",
@@ -673,6 +685,21 @@ export function lintProvider(provider, options = {}) {
673
685
  ...lintPlaywrightDirectImports(provider),
674
686
  ...lintSelfHostedBrowserPatterns(provider, options),
675
687
  ];
688
+ if (provider.operations) {
689
+ const authMode = provider.auth?.mode;
690
+ if (authMode === "credentials" || authMode === "oauth2") {
691
+ for (const operationKey of Object.keys(provider.operations)) {
692
+ if (AUTH_OPERATION_ID_PATTERN.test(operationKey)) {
693
+ diagnostics.push({
694
+ rule: "auth-operation-unsupported",
695
+ level: "error",
696
+ field: `operations.${operationKey}`,
697
+ message: `Provider "${provider.id ?? "unknown"}" operation "${operationKey}" looks like a login/token/session exchange endpoint. Authenticated providers must expose login through the single auth.flow interface because Gateway persists only auth.flow complete turn data.credential as the connection credential. Move this logic into auth.flow.continue instead of a provider operation.`,
698
+ });
699
+ }
700
+ }
701
+ }
702
+ }
676
703
  if (!provider.operations) {
677
704
  return diagnostics;
678
705
  }
@@ -1,9 +1,11 @@
1
+ export { AuthAbortError, credentialsAuthChallenge, createAuthFlowHelpers, defineCredentialsAuth, } from "./auth";
2
+ export type { CredentialsAuthChallengeDefinition, CredentialsAuthChallengeRequest, CredentialsAuthCompleteResult, CredentialsAuthCredential, CredentialsAuthField, CredentialsAuthFields, CredentialsAuthFieldType, CredentialsAuthInput, CredentialsAuthLoginResult, DefineCredentialsAuthOptions, DefinedCredentialsAuth, } from "./auth";
1
3
  export { createFormCeremony } from "./ceremonies";
2
4
  export { assertFreshProviderChoiceIssuedAt, createProviderChoiceToken, ProviderChoiceTokenError, type ProviderChoiceTokenErrorReason, type ProviderChoiceTokenPayload, parseProviderChoiceToken, } from "./choice-token";
3
- export { defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, every, } from "./define";
5
+ export { centered, delayed, defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, every, } from "./define";
4
6
  export { AuthError, ProviderError, SessionExpiredError, TransportError, ValidationError, } from "./errors";
5
7
  export { getProviderLocalePath, providerLocaleKey, qualifyProviderLocaleKey, } from "./i18n";
6
8
  export { type CreateProviderChoiceContextOptions, createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "./runtime/choice";
7
9
  export { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SENSITIVE_KIND_META_KEY, APIFUSE_SENSITIVE_META_KEY, collectSensitivePaths, describeKey, field, fields, isSensitiveSchema, redactPayload, type SensitiveFieldKind, type SensitiveFieldOptions, type SensitivePath, sensitive, z, } from "./schema";
8
- export type { AuthMode, FlowContext, HealthCheckAssertionContext, HealthCheckCase, HealthCheckSuite, HealthCheckUnsupported, HealthJourneyDefinition, HealthJourneyEventContext, HealthJourneyManualTriggerPolicy, HealthJourneyRunContext, HealthJourneyRunResult, HttpRetryOptions, HttpRetrySummary, InferSchemaOutput, OperationApprovalPolicy, OperationContractMetadata, OperationDefinition, OperationDocMeta, OperationErrorCode, OperationInputExample, OperationLifecycle, OperationObservabilityConfig, OperationObservabilitySensitiveConfig, OperationRelationships, OperationRiskClass, OperationSensitivePath, OperationToolRouterMetadata, OperationTransport, ProviderAccessVisibility, ProviderChoiceBindingOptions, ProviderChoiceContext, ProviderChoiceIssueOptions, ProviderChoiceParseOptions, ProviderContext, ProviderDefinition, ProviderLocale, ProviderLocaleKey, ProviderLocaleKeyInput, ProviderLogoProfile, ProviderProxyPolicy, ProviderPublicConnectionMode, ProviderPublicProfile, ProviderRuntimeState, ProviderStateDurationString, ProviderStateNamespace, ProviderSupportLevel, SchemaLike, SmsOtpMatcherDefinition, StandardSchemaV1, StateCasResult, StateNamespaceOptions, StateValue, StateWriteOptions, } from "./types";
10
+ export type { AuthAbortData, AuthAbortRetry, AuthFlowTerminalContext, AuthMode, AuthSafeData, AuthSafeJson, FlowContext, HealthCheckAssertionContext, HealthCheckCase, HealthCheckSuite, HealthCheckUnsupported, HealthJourneyDefinition, HealthJourneyEventContext, HealthJourneyManualTriggerPolicy, HealthJourneyRunContext, HealthJourneyRunResult, HealthScheduleRandomization, HttpRetryOptions, HttpRetrySummary, InferSchemaOutput, OperationApprovalPolicy, OperationContractMetadata, OperationDefinition, OperationDocMeta, OperationErrorCode, OperationInputExample, OperationLifecycle, OperationObservabilityConfig, OperationObservabilitySensitiveConfig, OperationRelationships, OperationRiskClass, OperationSensitivePath, OperationToolRouterMetadata, OperationTransport, ProviderAccessVisibility, ProviderChoiceBindingOptions, ProviderChoiceContext, ProviderChoiceIssueOptions, ProviderChoiceParseOptions, ProviderContext, ProviderDefinition, ProviderLocale, ProviderLocaleKey, ProviderLocaleKeyInput, ProviderLogoProfile, ProviderProxyPolicy, ProviderPublicConnectionMode, ProviderPublicProfile, ProviderRuntimeState, ProviderStateDurationString, ProviderStateNamespace, ProviderSupportLevel, SchemaLike, SmsOtpMatcherDefinition, StandardSchemaV1, StateCasResult, StateNamespaceOptions, StateValue, StateWriteOptions, } from "./types";
9
11
  export { HttpRetryAfterPolicy, HttpRetryDelayStrategy, HttpRetryJitter, HttpRetryPreset, HttpRetryUnsafeMethodPolicy, } from "./types";
package/dist/provider.js CHANGED
@@ -1,6 +1,7 @@
1
+ export { AuthAbortError, credentialsAuthChallenge, createAuthFlowHelpers, defineCredentialsAuth, } from "./auth";
1
2
  export { createFormCeremony } from "./ceremonies";
2
3
  export { assertFreshProviderChoiceIssuedAt, createProviderChoiceToken, ProviderChoiceTokenError, parseProviderChoiceToken, } from "./choice-token";
3
- export { defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, every, } from "./define";
4
+ export { centered, delayed, defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, every, } from "./define";
4
5
  export { AuthError, ProviderError, SessionExpiredError, TransportError, ValidationError, } from "./errors";
5
6
  export { getProviderLocalePath, providerLocaleKey, qualifyProviderLocaleKey, } from "./i18n";
6
7
  export { createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "./runtime/choice";
@@ -1,4 +1,5 @@
1
1
  import { ContextAccessError } from "../errors";
2
+ import { createAuthFlowHelpers } from "../auth";
2
3
  import { createUnsupportedSttClient } from "./stt";
3
4
  function normalizeAllowedKeys(allowedKeys) {
4
5
  return new Set(allowedKeys.filter((key) => key.trim().length > 0));
@@ -40,5 +41,6 @@ export function createFlowContext(options) {
40
41
  env: options.env,
41
42
  context: createScratchpad(options.allowedKeys, options.initialContext),
42
43
  stt: options.stt ?? createUnsupportedSttClient(),
44
+ auth: createAuthFlowHelpers(),
43
45
  };
44
46
  }
@@ -3,6 +3,106 @@ import { ProviderError } from "../errors";
3
3
  const require = createRequire(import.meta.url);
4
4
  const DEFAULT_WAIT_TIMEOUT_MS = 30_000;
5
5
  const SELECTOR_POLL_INTERVAL_MS = 100;
6
+ const RESOURCE_POLICY_ROUTE_PATTERN = "**/*";
7
+ const DEFAULT_RESOURCE_METHODS = ["GET", "HEAD"];
8
+ function toResourceBody(body) {
9
+ if (body === undefined || typeof body === "string" || Buffer.isBuffer(body)) {
10
+ return body;
11
+ }
12
+ if (body instanceof ArrayBuffer) {
13
+ return Buffer.from(new Uint8Array(body));
14
+ }
15
+ return Buffer.from(body);
16
+ }
17
+ function isResourceMethod(method) {
18
+ return method === "GET" || method === "HEAD";
19
+ }
20
+ async function toResourceRequest(request) {
21
+ const method = request.method().toUpperCase();
22
+ if (!isResourceMethod(method)) {
23
+ return null;
24
+ }
25
+ return {
26
+ headers: await request.allHeaders(),
27
+ method,
28
+ resourceType: request.resourceType(),
29
+ url: request.url(),
30
+ };
31
+ }
32
+ function toCdpResourceRequest(params) {
33
+ if (!isRecord(params)) {
34
+ return null;
35
+ }
36
+ const requestId = params.requestId;
37
+ const rawRequest = params.request;
38
+ if (typeof requestId !== "string" || !isRecord(rawRequest)) {
39
+ return null;
40
+ }
41
+ const url = rawRequest.url;
42
+ const method = String(rawRequest.method ?? "").toUpperCase();
43
+ if (typeof url !== "string" || !isResourceMethod(method)) {
44
+ return null;
45
+ }
46
+ return {
47
+ requestId,
48
+ request: {
49
+ headers: toCdpResourceHeaders(rawRequest.headers),
50
+ method,
51
+ resourceType: typeof params.resourceType === "string" ? params.resourceType : undefined,
52
+ url,
53
+ },
54
+ };
55
+ }
56
+ function getCdpPausedRequestId(params) {
57
+ if (!isRecord(params) || typeof params.requestId !== "string") {
58
+ return null;
59
+ }
60
+ return params.requestId;
61
+ }
62
+ function toCdpResourceHeaders(value) {
63
+ if (!isRecord(value)) {
64
+ return {};
65
+ }
66
+ const headers = {};
67
+ for (const [name, headerValue] of Object.entries(value)) {
68
+ if (typeof headerValue === "string") {
69
+ headers[name] = headerValue;
70
+ }
71
+ }
72
+ return headers;
73
+ }
74
+ function matchesResourceRoute(match, request) {
75
+ if (typeof match === "string") {
76
+ return request.url === match;
77
+ }
78
+ if (match instanceof RegExp) {
79
+ return match.test(request.url);
80
+ }
81
+ return match(request);
82
+ }
83
+ function toCdpFulfillParams(requestId, decision) {
84
+ const body = toResourceBody(decision.body);
85
+ return {
86
+ ...(body === undefined
87
+ ? {}
88
+ : { body: Buffer.from(body).toString("base64") }),
89
+ ...(decision.headers === undefined
90
+ ? {}
91
+ : {
92
+ responseHeaders: Object.entries(decision.headers).map(([name, value]) => ({ name, value })),
93
+ }),
94
+ requestId,
95
+ responseCode: decision.status ?? 200,
96
+ };
97
+ }
98
+ async function fulfillResourceRoute(route, decision) {
99
+ const body = toResourceBody(decision.body);
100
+ await route.fulfill({
101
+ ...(body === undefined ? {} : { body }),
102
+ ...(decision.headers === undefined ? {} : { headers: decision.headers }),
103
+ status: decision.status ?? 200,
104
+ });
105
+ }
6
106
  function getDefaultCdpPoolUrl(env = process.env) {
7
107
  return env.APIFUSE__CDP_POOL__URL;
8
108
  }
@@ -256,6 +356,38 @@ class PlaywrightBrowserPage {
256
356
  async close() {
257
357
  await this.page.close();
258
358
  }
359
+ async withResourcePolicy(policy, run) {
360
+ const allowedMethods = new Set(policy.allowedMethods ?? DEFAULT_RESOURCE_METHODS);
361
+ const handler = async (route) => {
362
+ const request = await toResourceRequest(route.request());
363
+ if (!request || !allowedMethods.has(request.method)) {
364
+ await route.abort("blockedbyclient");
365
+ return;
366
+ }
367
+ for (const resourceRoute of policy.routes) {
368
+ if (!matchesResourceRoute(resourceRoute.match, request)) {
369
+ continue;
370
+ }
371
+ const decision = await resourceRoute.handle(request);
372
+ switch (decision.action) {
373
+ case "fulfill":
374
+ await fulfillResourceRoute(route, decision);
375
+ return;
376
+ case "block":
377
+ await route.abort("blockedbyclient");
378
+ return;
379
+ }
380
+ }
381
+ await route.abort("blockedbyclient");
382
+ };
383
+ await this.page.route(RESOURCE_POLICY_ROUTE_PATTERN, handler);
384
+ try {
385
+ return await run();
386
+ }
387
+ finally {
388
+ await this.page.unroute(RESOURCE_POLICY_ROUTE_PATTERN, handler);
389
+ }
390
+ }
259
391
  }
260
392
  class PlaywrightBrowserClient {
261
393
  options;
@@ -737,6 +869,77 @@ class CdpPoolBrowserPage {
737
869
  await this.pageClient.close();
738
870
  }
739
871
  }
872
+ async withResourcePolicy(policy, run) {
873
+ const allowedMethods = new Set(policy.allowedMethods ?? DEFAULT_RESOURCE_METHODS);
874
+ const handlePausedRequest = (params) => {
875
+ void this.handleResourcePolicyPausedRequest(params, policy, allowedMethods);
876
+ };
877
+ const unsubscribe = this.pageClient.on("Fetch.requestPaused", handlePausedRequest);
878
+ try {
879
+ await this.pageClient.send("Fetch.enable", {
880
+ patterns: [{ requestStage: "Request", urlPattern: "*" }],
881
+ });
882
+ }
883
+ catch (error) {
884
+ unsubscribe();
885
+ throw new ProviderError("CDP browser target does not support BrowserPage.withResourcePolicy()", {
886
+ cause: error instanceof Error ? error : undefined,
887
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
888
+ fix: "Use a Chromium CDP target with the Fetch domain enabled, or use the local Playwright browser runtime.",
889
+ });
890
+ }
891
+ try {
892
+ return await run();
893
+ }
894
+ finally {
895
+ unsubscribe();
896
+ await this.pageClient.send("Fetch.disable");
897
+ }
898
+ }
899
+ async handleResourcePolicyPausedRequest(params, policy, allowedMethods) {
900
+ const requestId = getCdpPausedRequestId(params);
901
+ if (requestId === null) {
902
+ return;
903
+ }
904
+ try {
905
+ const parsed = toCdpResourceRequest(params);
906
+ if (!parsed || !allowedMethods.has(parsed.request.method)) {
907
+ await this.failCdpResourceRequest(requestId);
908
+ return;
909
+ }
910
+ for (const resourceRoute of policy.routes) {
911
+ if (!matchesResourceRoute(resourceRoute.match, parsed.request)) {
912
+ continue;
913
+ }
914
+ const decision = await resourceRoute.handle(parsed.request);
915
+ switch (decision.action) {
916
+ case "fulfill":
917
+ await this.pageClient.send("Fetch.fulfillRequest", toCdpFulfillParams(parsed.requestId, decision));
918
+ return;
919
+ case "block":
920
+ await this.failCdpResourceRequest(parsed.requestId);
921
+ return;
922
+ }
923
+ }
924
+ await this.failCdpResourceRequest(parsed.requestId);
925
+ }
926
+ catch {
927
+ await this.failCdpResourceRequest(requestId);
928
+ }
929
+ }
930
+ async failCdpResourceRequest(requestId) {
931
+ try {
932
+ await this.pageClient.send("Fetch.failRequest", {
933
+ errorReason: "BlockedByClient",
934
+ requestId,
935
+ });
936
+ }
937
+ catch (error) {
938
+ if (error instanceof Error) {
939
+ return;
940
+ }
941
+ }
942
+ }
740
943
  async initialize() {
741
944
  if (this.initialized) {
742
945
  return;