@apifuse/provider-sdk 2.1.0-beta.11 → 2.1.0-beta.13

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.
@@ -544,8 +544,8 @@ function renderAuthBlock(authMode) {
544
544
  return `{
545
545
  mode: "credentials",
546
546
  flow: {
547
- start: async () => ({
548
- kind: "input",
547
+ start: async (_ctx) => ({
548
+ kind: "form",
549
549
  turnId: crypto.randomUUID(),
550
550
  expectedInput: {
551
551
  schema: {
@@ -588,7 +588,7 @@ function renderAuthBlock(authMode) {
588
588
  return `{
589
589
  mode: "oauth2",
590
590
  flow: {
591
- start: async () => ({
591
+ start: async (_ctx) => ({
592
592
  kind: "redirect",
593
593
  turnId: crypto.randomUUID(),
594
594
  data: {
package/dist/define.js CHANGED
@@ -141,6 +141,11 @@ function validateProviderShape(config) {
141
141
  "mode" in auth &&
142
142
  typeof auth.mode === "string")
143
143
  assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
144
+ if (auth && typeof auth === "object" && "exchange" in auth) {
145
+ throw new ProviderError(`Provider "${String(config.id)}" auth.exchange is not part of the Provider SDK auth contract`, {
146
+ 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.",
147
+ });
148
+ }
144
149
  if (auth &&
145
150
  typeof auth === "object" &&
146
151
  "flow" in auth &&
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
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";
@@ -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, 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,4 +1,5 @@
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";
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,3 +1,5 @@
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
5
  export { defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, every, } from "./define";
@@ -5,5 +7,5 @@ export { AuthError, ProviderError, SessionExpiredError, TransportError, Validati
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, 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,3 +1,4 @@
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
4
  export { defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, every, } from "./define";
@@ -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;
@@ -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";
@@ -209,7 +210,7 @@ function createFlowContextStore(allowedKeys, initialContext = {}) {
209
210
  },
210
211
  };
211
212
  }
212
- function createAuthFlowContext(provider, request, options = {}) {
213
+ function createAuthFlowContext(provider, request, options = {}, signal) {
213
214
  const baseUrl = getProviderBaseUrl(provider);
214
215
  const stealthBaseUrl = getProviderStealthBaseUrl(provider);
215
216
  const stealthProfile = getProviderStealthProfile(provider);
@@ -251,6 +252,7 @@ function createAuthFlowContext(provider, request, options = {}) {
251
252
  credential,
252
253
  context: flowContextStore.context,
253
254
  stt: options.stt ?? createSttClientFromEnv(provider.stt),
255
+ auth: createAuthFlowHelpers({ signal }),
254
256
  },
255
257
  getPatch: flowContextStore.getPatch,
256
258
  };
@@ -879,14 +881,14 @@ function responseWithProviderTelemetry(response, proxyTelemetry) {
879
881
  statusText: response.statusText,
880
882
  });
881
883
  }
882
- async function handleAuthFlow(provider, request, route, options = {}) {
884
+ async function handleAuthFlow(provider, request, route, options = {}, signal) {
883
885
  const flow = provider.auth?.flow;
884
886
  if (!flow) {
885
887
  throw new ProviderError("Auth flow is not configured", {
886
888
  code: "AUTH_FLOW_NOT_CONFIGURED",
887
889
  });
888
890
  }
889
- const { context, getPatch } = createAuthFlowContext(provider, request, options);
891
+ const { context, getPatch } = createAuthFlowContext(provider, request, options, signal);
890
892
  try {
891
893
  const result = route === "start"
892
894
  ? await flow.start(context)
@@ -916,6 +918,12 @@ async function handleAuthFlow(provider, request, route, options = {}) {
916
918
  : result;
917
919
  return toAuthFlowResponse(materializedResult, getPatch());
918
920
  }
921
+ catch (error) {
922
+ if (error instanceof AuthAbortError) {
923
+ return toAuthFlowResponse(error.turn, getPatch());
924
+ }
925
+ throw error;
926
+ }
919
927
  finally {
920
928
  context.stealth.close?.();
921
929
  }
@@ -982,7 +990,7 @@ export function createServerApp(provider, options = {}) {
982
990
  .json()
983
991
  .catch(() => undefined);
984
992
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
985
- const response = await handleAuthFlow(provider, body, "start", options);
993
+ const response = await handleAuthFlow(provider, body, "start", options, c.req.raw.signal);
986
994
  logProviderSuccess(logger, provider, "auth", "start", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
987
995
  return response instanceof Response ? response : c.json(response);
988
996
  }
@@ -1002,7 +1010,7 @@ export function createServerApp(provider, options = {}) {
1002
1010
  .json()
1003
1011
  .catch(() => undefined);
1004
1012
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
1005
- const response = await handleAuthFlow(provider, body, "continue", options);
1013
+ const response = await handleAuthFlow(provider, body, "continue", options, c.req.raw.signal);
1006
1014
  logProviderSuccess(logger, provider, "auth", "continue", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
1007
1015
  return response instanceof Response ? response : c.json(response);
1008
1016
  }
@@ -1022,7 +1030,7 @@ export function createServerApp(provider, options = {}) {
1022
1030
  .json()
1023
1031
  .catch(() => undefined);
1024
1032
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
1025
- const response = await handleAuthFlow(provider, body, "poll", options);
1033
+ const response = await handleAuthFlow(provider, body, "poll", options, c.req.raw.signal);
1026
1034
  logProviderSuccess(logger, provider, "auth", "poll", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
1027
1035
  return response instanceof Response ? response : c.json(response);
1028
1036
  }
@@ -1042,7 +1050,7 @@ export function createServerApp(provider, options = {}) {
1042
1050
  .json()
1043
1051
  .catch(() => undefined);
1044
1052
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
1045
- const response = await handleAuthFlow(provider, body, "refresh", options);
1053
+ const response = await handleAuthFlow(provider, body, "refresh", options, c.req.raw.signal);
1046
1054
  logProviderSuccess(logger, provider, "auth", "refresh", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
1047
1055
  return response instanceof Response ? response : c.json(response);
1048
1056
  }
@@ -1062,7 +1070,7 @@ export function createServerApp(provider, options = {}) {
1062
1070
  .json()
1063
1071
  .catch(() => undefined);
1064
1072
  const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
1065
- const response = await handleAuthFlow(provider, body, "abort", options);
1073
+ const response = await handleAuthFlow(provider, body, "abort", options, c.req.raw.signal);
1066
1074
  logProviderSuccess(logger, provider, "auth", "disconnect", body.requestId, response instanceof Response ? response.status : 200, finishRequestCost(requestCost));
1067
1075
  return response instanceof Response ? response : c.json(response);
1068
1076
  }
package/dist/types.d.ts CHANGED
@@ -1016,6 +1016,32 @@ export interface BrowserFrame {
1016
1016
  evaluate<T>(fn: string | (() => T)): Promise<T>;
1017
1017
  locator(selector: string): BrowserLocator;
1018
1018
  }
1019
+ export type BrowserResourceMethod = "GET" | "HEAD";
1020
+ export type BrowserResourceRequest = {
1021
+ readonly url: string;
1022
+ readonly method: BrowserResourceMethod;
1023
+ readonly resourceType?: string;
1024
+ readonly headers: Readonly<Record<string, string>>;
1025
+ };
1026
+ export type BrowserResourceBody = Buffer | Uint8Array | ArrayBuffer | string;
1027
+ export type BrowserResourceDecision = {
1028
+ readonly action: "fulfill";
1029
+ readonly status?: number;
1030
+ readonly headers?: Readonly<Record<string, string>>;
1031
+ readonly body?: BrowserResourceBody;
1032
+ } | {
1033
+ readonly action: "block";
1034
+ readonly reason?: string;
1035
+ };
1036
+ export type BrowserResourceRoute = {
1037
+ readonly match: string | RegExp | ((request: BrowserResourceRequest) => boolean);
1038
+ readonly handle: (request: BrowserResourceRequest) => Promise<BrowserResourceDecision> | BrowserResourceDecision;
1039
+ };
1040
+ export type BrowserResourcePolicy = {
1041
+ readonly defaultAction?: "block";
1042
+ readonly allowedMethods?: readonly BrowserResourceMethod[];
1043
+ readonly routes: readonly BrowserResourceRoute[];
1044
+ };
1019
1045
  export interface BrowserPage extends BrowserFrame {
1020
1046
  close(): Promise<void>;
1021
1047
  fill(selector: string, text: string): Promise<void>;
@@ -1030,6 +1056,7 @@ export interface BrowserPage extends BrowserFrame {
1030
1056
  timeout?: number;
1031
1057
  }): Promise<void>;
1032
1058
  frames(): Promise<BrowserFrame[]>;
1059
+ withResourcePolicy<T>(policy: BrowserResourcePolicy, run: () => Promise<T>): Promise<T>;
1033
1060
  }
1034
1061
  export type BrowserChallengeRequest = {
1035
1062
  type: "recaptcha";
@@ -1170,6 +1197,73 @@ export interface ContextScratchpad {
1170
1197
  toJSON(): Record<string, unknown>;
1171
1198
  }
1172
1199
  export type FlowContextStore = ContextScratchpad;
1200
+ export type AuthSafeJson = string | number | boolean | null | readonly AuthSafeJson[] | {
1201
+ readonly [key: string]: AuthSafeJson;
1202
+ };
1203
+ export type AuthSafeData = {
1204
+ readonly [key: string]: AuthSafeJson;
1205
+ };
1206
+ export type AuthAbortRetry = "never" | "retry" | "after_user_action";
1207
+ export type AuthAbortData = Record<string, unknown> & {
1208
+ readonly code: string;
1209
+ readonly message?: string;
1210
+ readonly retry?: AuthAbortRetry;
1211
+ readonly actionHint?: AuthSafeJson;
1212
+ readonly fieldErrors?: {
1213
+ readonly [field: string]: string;
1214
+ };
1215
+ readonly details?: AuthSafeData;
1216
+ };
1217
+ export interface AuthFlowTerminalContext {
1218
+ readonly signal?: AbortSignal;
1219
+ readonly deadline?: string;
1220
+ complete<TCredential extends Record<string, string>>(options: {
1221
+ readonly credential: TCredential;
1222
+ readonly metadata?: AuthSafeData;
1223
+ readonly data?: AuthSafeData;
1224
+ readonly turnId?: string;
1225
+ readonly expiresAt?: string;
1226
+ }): AuthTurn;
1227
+ abort(options: {
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 data?: AuthSafeData;
1236
+ readonly turnId?: string;
1237
+ readonly expiresAt?: string;
1238
+ }): AuthTurn;
1239
+ nextForm(options: {
1240
+ readonly hintKey?: ProviderLocaleKeyInput;
1241
+ readonly data?: AuthSafeData;
1242
+ readonly turnId?: string;
1243
+ readonly expiresAt?: string;
1244
+ readonly timing?: AuthTurn["timing"];
1245
+ } & ({
1246
+ readonly fields: Record<string, {
1247
+ readonly type?: "string" | "email" | "password" | "otp";
1248
+ readonly labelKey?: ProviderLocaleKeyInput;
1249
+ readonly descriptionKey?: ProviderLocaleKeyInput;
1250
+ readonly placeholderKey?: ProviderLocaleKeyInput;
1251
+ readonly required?: boolean;
1252
+ readonly sensitive?: boolean;
1253
+ }>;
1254
+ readonly expectedInput?: never;
1255
+ } | {
1256
+ readonly expectedInput: Record<string, unknown>;
1257
+ readonly fields?: never;
1258
+ })): AuthTurn;
1259
+ nextPoll(options?: {
1260
+ readonly hintKey?: ProviderLocaleKeyInput;
1261
+ readonly data?: AuthSafeData;
1262
+ readonly turnId?: string;
1263
+ readonly expiresAt?: string;
1264
+ readonly timing?: AuthTurn["timing"];
1265
+ }): AuthTurn;
1266
+ }
1173
1267
  export interface FlowContext {
1174
1268
  connectionId?: string;
1175
1269
  externalRef?: string;
@@ -1181,6 +1275,7 @@ export interface FlowContext {
1181
1275
  credential?: CredentialContext;
1182
1276
  context: ContextScratchpad;
1183
1277
  stt: SttContext;
1278
+ auth: AuthFlowTerminalContext;
1184
1279
  }
1185
1280
  export interface AuthTurn {
1186
1281
  kind: string;
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.1.0-beta.11",
2
+ "version": "2.1.0-beta.13",
3
3
  "name": "@apifuse/provider-sdk",
4
4
  "private": false,
5
5
  "type": "module",