@apifuse/provider-sdk 2.2.0-beta.20 → 2.2.0-beta.21

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.2.0-beta.21
4
+
5
+ - Release candidate for main commit 00f61024fb18db39711dc5076c4621508baa49f5.
6
+
3
7
  ## 2.2.0-beta.20
4
8
 
5
9
  - Release candidate for main commit 8043d2ef0047e431aace750d8abca2e1149ec1d6.
@@ -110,6 +114,7 @@
110
114
 
111
115
  ## Unreleased
112
116
 
117
+ - Upstream the platform monorepo's beta.16 dist patch: OAuth2-proxied auth ceremony (`createOAuth2ProxiedStart`, `OAUTH2_PROXIED_PKCE_VERIFIER_KEY`, `APIFUSE__AUTH_PROXY__URL` origin key, proxied redirect/callback turns) and the provider runtime state upgrades it depends on (in-memory compareAndSet with quota-aware write policy, Redis CAS/set Lua scripts with entry quotas and legacy index migration). The monorepo drops `patchedDependencies` once it pins this release.
113
118
  - **honest-provider-error-contract (phase 2):** `UPSTREAM_REJECTED` is a registered code family serving HTTP 409 with `retryable: false` — deterministic upstream business refusals are no longer 502s. Operation-declared `docs.errorCodes` may now use 409/410/422. Public error envelopes carry a `source` field (`client` | `upstream_rule` | `upstream_failure` | `apifuse`) derived from the observability category. Taxonomy version bumps to `2026-08-07` with the `upstream_rejected`, `dependency_unavailable`, `unsupported_transport`, and `client_cancelled` categories (409/410/422 map to `upstream_rejected`), matching the platform monorepo SoT. The `unregistered_provider_error_code` signal now carries a `signalFix` pointing at `docs.errorCodes` declaration.
114
119
  - **Breaking for custom native gateway adapters:** `NativeGatewayProxySynthesisInput` now includes an injected `credentials` resolver and selected `protocol`; synthesizers may return promises and structured skip reasons, and `resolveNativeGatewayProxy` is async. Default callers retain env-backed behavior. Native transport now supports both HTTP CONNECT and SOCKS5, defaults per vendor with an explicit runtime override, registers smartproxy allocation ahead of nodemaven when declared in that order, and reports every exhausted vendor reason without exposing proxy credentials.
115
120
  - Honor operation `docs.errorCodes` at runtime: declared provider-owned statuses and retryability now drive the HTTP envelope, observability header, and structured log; invalid statuses fail `defineProvider`, declared codes no longer emit the unregistered-code signal, and `TransportError` status-preservation workarounds are obsolete.
@@ -1,5 +1,7 @@
1
1
  import type { AuthFlowDefinition, AuthTurn } from "../types.js";
2
2
  type JsonObject = Record<string, unknown>;
3
+ export declare const OAUTH2_PROXIED_AUTH_PROXY_ORIGIN_ENV_KEY = "APIFUSE__AUTH_PROXY__URL";
4
+ export declare const OAUTH2_PROXIED_PKCE_VERIFIER_KEY = "__oauth2_proxied_pkce_verifier";
3
5
  export declare function validateCeremonyOutput(turn: unknown): AuthTurn;
4
6
  export declare function createOAuth2Ceremony(options: {
5
7
  authorizeUrl: string;
@@ -9,6 +11,12 @@ export declare function createOAuth2Ceremony(options: {
9
11
  scopes: string[];
10
12
  usePKCE?: boolean;
11
13
  }): AuthFlowDefinition;
14
+ /**
15
+ * Builds the start handler for a custom-scheme OAuth provider. The shared
16
+ * auth-proxy owns state minting and callback capture, while the provider owns
17
+ * token exchange and any following authentication turns.
18
+ */
19
+ export declare function createOAuth2ProxiedStart(options: import("../types.js").ProxiedOAuthConfig): import("../types.js").AuthFlowStartHandler;
12
20
  export declare function createDeviceFlowCeremony(options: {
13
21
  deviceCodeUrl: string;
14
22
  tokenUrl: string;
@@ -9,6 +9,15 @@ const ajv = new Ajv2020({ allErrors: true, strict: true, strictSchema: true });
9
9
  const validateAuthTurn = ajv.compile(AUTH_TURN_SCHEMA);
10
10
  const OAUTH2_STATE_KEY = "__oauth2_state";
11
11
  const OAUTH2_PKCE_VERIFIER_KEY = "__oauth2_pkce_verifier";
12
+ export const OAUTH2_PROXIED_AUTH_PROXY_ORIGIN_ENV_KEY = "APIFUSE__AUTH_PROXY__URL";
13
+ export const OAUTH2_PROXIED_PKCE_VERIFIER_KEY = "__oauth2_proxied_pkce_verifier";
14
+ const OAUTH2_PROXIED_RESERVED_AUTHORIZE_PARAMS = new Set([
15
+ "client_id",
16
+ "response_type",
17
+ "state",
18
+ "code_challenge",
19
+ "code_challenge_method",
20
+ ]);
12
21
  const DEVICE_FLOW_KEY = "__device_flow";
13
22
  const MAGIC_LINK_KEY = "__magic_link";
14
23
  const COMBINED_STAGE_KEY = "__combined_stage";
@@ -173,6 +182,54 @@ export function createOAuth2Ceremony(options) {
173
182
  abort: async () => validateCeremonyOutput(createTurn("abort", { hint: "OAuth flow aborted." })),
174
183
  };
175
184
  }
185
+ /**
186
+ * Builds the start handler for a custom-scheme OAuth provider. The shared
187
+ * auth-proxy owns state minting and callback capture, while the provider owns
188
+ * token exchange and any following authentication turns.
189
+ */
190
+ export function createOAuth2ProxiedStart(options) {
191
+ const pkce = options.pkce ?? "S256";
192
+ return (ctx) => runCeremonyHandler(async () => {
193
+ for (const key of Object.keys(options.authorizeParams ?? {})) {
194
+ if (OAUTH2_PROXIED_RESERVED_AUTHORIZE_PARAMS.has(key.toLowerCase())) {
195
+ throw new ValidationError(`OAuth2 proxied authorizeParams cannot override reserved parameter "${key}".`);
196
+ }
197
+ }
198
+ const proxyOrigin = ctx.env.get(OAUTH2_PROXIED_AUTH_PROXY_ORIGIN_ENV_KEY);
199
+ if (!proxyOrigin) {
200
+ throw new ProviderSecretError(`Missing required platform environment: ${OAUTH2_PROXIED_AUTH_PROXY_ORIGIN_ENV_KEY}`);
201
+ }
202
+ const clientId = getRequiredEnv(ctx, options.clientIdEnvKey);
203
+ if (!ctx.flowId) {
204
+ throw new ValidationError("OAuth2 proxied start requires the gateway flow id.");
205
+ }
206
+ const authorizePath = new URL(options.authorizeUrl).pathname;
207
+ const url = new URL(`/p/${encodeURIComponent(ctx.providerId)}/f/${encodeURIComponent(ctx.flowId)}${authorizePath}`, proxyOrigin);
208
+ url.searchParams.set("client_id", clientId);
209
+ url.searchParams.set("response_type", "code");
210
+ if (pkce === "S256") {
211
+ const verifier = createCodeVerifier();
212
+ ctx.context.set(OAUTH2_PROXIED_PKCE_VERIFIER_KEY, verifier);
213
+ url.searchParams.set("code_challenge", createCodeChallenge(verifier));
214
+ url.searchParams.set("code_challenge_method", "S256");
215
+ }
216
+ for (const [key, value] of Object.entries(options.authorizeParams ?? {})) {
217
+ url.searchParams.set(key, value);
218
+ }
219
+ return createTurn("redirect", {
220
+ data: { url: url.toString() },
221
+ hint: "Open the provider authorization page to continue.",
222
+ expectedInput: {
223
+ type: "object",
224
+ required: ["code", "state"],
225
+ properties: {
226
+ code: { type: "string" },
227
+ state: { type: "string" },
228
+ },
229
+ },
230
+ });
231
+ }, "OAuth2 proxied start failed", ctx);
232
+ }
176
233
  export function createDeviceFlowCeremony(options) {
177
234
  return {
178
235
  start: (ctx) => runCeremonyHandler(async () => {
package/dist/define.js CHANGED
@@ -8,7 +8,36 @@ import { HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX, HEALTH_CHECK_DEGRADED_THRESHOLD
8
8
  const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
9
9
  const OPERATION_ID_REGEX = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
10
10
  const VALID_RUNTIMES = ["standard", "shared", "browser"];
11
- const VALID_AUTH_MODES = ["none", "platform-managed", "credentials", "oauth2"];
11
+ const VALID_AUTH_MODES = [
12
+ "none",
13
+ "platform-managed",
14
+ "credentials",
15
+ "oauth2",
16
+ "oauth2_proxied",
17
+ ];
18
+ const PROXIED_OAUTH_REQUIRED_FIELDS = [
19
+ "authorizeUrl",
20
+ "tokenUrl",
21
+ "customScheme",
22
+ "rewriteProfile",
23
+ "clientIdEnvKey",
24
+ ];
25
+ const PROXIED_OAUTH_ALLOWED_FIELDS = new Set([
26
+ ...PROXIED_OAUTH_REQUIRED_FIELDS,
27
+ "pkce",
28
+ "authorizeParams",
29
+ "tokenParams",
30
+ ]);
31
+ const PROXIED_OAUTH_RESERVED_AUTHORIZE_PARAMS = new Set([
32
+ "client_id",
33
+ "response_type",
34
+ "state",
35
+ "code_challenge",
36
+ "code_challenge_method",
37
+ ]);
38
+ const PROXIED_OAUTH_PROFILE_REGEX = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
39
+ const PROXIED_OAUTH_ENV_KEY_REGEX = /^[A-Z][A-Z0-9_]*__[A-Z0-9_]+$/;
40
+ const CUSTOM_SCHEME_REGEX = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/\S+$/;
12
41
  const VALID_PROVIDER_ACCESS_VISIBILITIES = ["public", "early_access"];
13
42
  const VALID_PROVIDER_PROXY_MODES = ["disabled", "optional", "required"];
14
43
  const VALID_PROVIDER_PROXY_PROVIDERS = ["smartproxy", "nodemaven", "decodo", "custom"];
@@ -89,6 +118,76 @@ function assertLiteralField(value, field, validValues, providerId) {
89
118
  });
90
119
  }
91
120
  }
121
+ function validateProxiedOAuthParams(value, field, providerId) {
122
+ if (value === undefined)
123
+ return;
124
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
125
+ throw new ValidationError(`Provider "${providerId}" auth.proxied.${field} must be an object of string values.`);
126
+ }
127
+ for (const [key, paramValue] of Object.entries(value)) {
128
+ if (!key.trim() || typeof paramValue !== "string") {
129
+ throw new ValidationError(`Provider "${providerId}" auth.proxied.${field} must contain non-empty keys and string values.`);
130
+ }
131
+ if (field === "authorizeParams" &&
132
+ PROXIED_OAUTH_RESERVED_AUTHORIZE_PARAMS.has(key.toLowerCase())) {
133
+ throw new ValidationError(`Provider "${providerId}" auth.proxied.authorizeParams cannot override reserved parameter "${key}".`);
134
+ }
135
+ }
136
+ }
137
+ function validateProxiedOAuthAuth(auth, providerId) {
138
+ const proxied = auth.proxied;
139
+ if (auth.mode !== "oauth2_proxied") {
140
+ if (proxied !== undefined) {
141
+ throw new ValidationError(`Provider "${providerId}" auth.proxied is only valid when auth.mode is "oauth2_proxied".`);
142
+ }
143
+ return;
144
+ }
145
+ if (!proxied || typeof proxied !== "object" || Array.isArray(proxied)) {
146
+ throw new ValidationError(`Provider "${providerId}" with auth.mode "oauth2_proxied" must declare auth.proxied.`);
147
+ }
148
+ const config = Object.fromEntries(Object.entries(proxied));
149
+ for (const key of Object.keys(config)) {
150
+ if (!PROXIED_OAUTH_ALLOWED_FIELDS.has(key)) {
151
+ throw new ValidationError(`Provider "${providerId}" has unknown auth.proxied field "${key}".`);
152
+ }
153
+ }
154
+ for (const field of PROXIED_OAUTH_REQUIRED_FIELDS) {
155
+ if (typeof config[field] !== "string" || !config[field].trim()) {
156
+ throw new ValidationError(`Provider "${providerId}" auth.proxied.${field} must be a non-empty string.`);
157
+ }
158
+ }
159
+ for (const field of ["authorizeUrl", "tokenUrl"]) {
160
+ try {
161
+ const endpoint = new URL(String(config[field]));
162
+ if (endpoint.protocol !== "https:" ||
163
+ endpoint.username ||
164
+ endpoint.password ||
165
+ endpoint.hash) {
166
+ throw new Error("invalid endpoint");
167
+ }
168
+ }
169
+ catch {
170
+ throw new ValidationError(`Provider "${providerId}" auth.proxied.${field} must be an absolute HTTPS URL without credentials or a fragment.`);
171
+ }
172
+ }
173
+ const customScheme = String(config.customScheme);
174
+ if (!CUSTOM_SCHEME_REGEX.test(customScheme) ||
175
+ customScheme.toLowerCase().startsWith("http://") ||
176
+ customScheme.toLowerCase().startsWith("https://")) {
177
+ throw new ValidationError(`Provider "${providerId}" auth.proxied.customScheme must be a non-HTTP custom-scheme URL prefix.`);
178
+ }
179
+ if (!PROXIED_OAUTH_PROFILE_REGEX.test(String(config.rewriteProfile))) {
180
+ throw new ValidationError(`Provider "${providerId}" auth.proxied.rewriteProfile must be a kebab-case profile name.`);
181
+ }
182
+ if (!PROXIED_OAUTH_ENV_KEY_REGEX.test(String(config.clientIdEnvKey))) {
183
+ throw new ValidationError(`Provider "${providerId}" auth.proxied.clientIdEnvKey must be an APIFuse-style uppercase environment key.`);
184
+ }
185
+ if (config.pkce !== undefined && config.pkce !== "S256" && config.pkce !== "none") {
186
+ throw new ValidationError(`Provider "${providerId}" auth.proxied.pkce must be "S256" or "none".`);
187
+ }
188
+ validateProxiedOAuthParams(config.authorizeParams, "authorizeParams", providerId);
189
+ validateProxiedOAuthParams(config.tokenParams, "tokenParams", providerId);
190
+ }
92
191
  function validateProviderShape(config) {
93
192
  assertObjectConfig(config);
94
193
  assertRequiredField(config, "id");
@@ -101,6 +200,9 @@ function validateProviderShape(config) {
101
200
  const auth = config.auth;
102
201
  if (auth && typeof auth === "object" && "mode" in auth && typeof auth.mode === "string")
103
202
  assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
203
+ if (auth && typeof auth === "object" && !Array.isArray(auth)) {
204
+ validateProxiedOAuthAuth(Object.fromEntries(Object.entries(auth)), String(config.id));
205
+ }
104
206
  if (auth && typeof auth === "object" && "exchange" in auth) {
105
207
  throw new ProviderError(`Provider "${String(config.id)}" auth.exchange is not part of the Provider SDK auth contract`, {
106
208
  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.",
package/dist/index.d.ts CHANGED
@@ -38,7 +38,7 @@ export { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SEN
38
38
  export { createServerApp, ERROR_OBSERVABILITY_HEADER, type ServeOptions, serve, } from "./server/index.js";
39
39
  export { getStealthProfile, listStealthProfiles } from "./stealth/profiles.js";
40
40
  export * from "./stream.js";
41
- 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, HttpRedirectFailureReason, HttpRedirectPolicy, HttpRedirectPolicyMode, HttpResponse, HttpRetryOptions, HttpRetrySummary, HttpStreamResponse, IanaTimeZone, InferSchemaOutput, Iso3166Alpha2CountryCode, Iso4217CurrencyCode, Iso8601Duration, NativeContext, NativeNetworkClient, NativeNetworkCloseReason, NativeNetworkConnection, NativeNetworkConnectInput, NativeNetworkConnectOptions, NativeNetworkDynamicGrantOptions, NativeNetworkEgressGrant, NativeProviderConfig, NativeProviderContext, NativeProxyDrainHandler, NativeProxyEgressInfo, NativeProxyExpiringEvent, NativeProxyExpiringReason, NativeTcpDynamicEgressRule, NativeTcpEgressGrant, NativeTcpEgressRule, NativeTcpPortRange, NativeTcpTlsMode, NativeTlsConnectOptions, OperationAnnotations, OperationApprovalPolicy, OperationContractMetadata, OperationDefinition, OperationDeprecationMetadata, OperationDocMeta, OperationErrorCode, ProviderErrorStatus, 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, ProviderDeploymentOverrides, ProviderFileRef, ProviderFilesContext, ProviderHealthMonitorConfig, ProviderHealthProbeConfig, ProviderLocale, ProviderLocaleKey, ProviderLocaleKeyInput, ProviderLogoProfile, ProviderLogoSource, ProviderMeta, ProviderProxyConfig, ProviderProxyMode, ProviderProxyPolicy, ProviderProxyProvider, ProviderProxySessionAffinity, ProviderPublicConnectionMode, ProviderPublicProfile, ProviderReviewed, ProviderResolvedFile, ProviderRuntimeState, ProviderSecretDeclaration, ProviderStateDurationString, ProviderStateNamespace, ProviderStreamEvent, ProviderSttConfig, ProviderSttMode, ProviderSupportLevel, RequestOptions, RedirectRunReason, Rfc3339Instant, SchemaLike, SmsOrigin, SmsOtpExtractionPattern, SmsOtpMatcherDefinition, SseMessage, StandardSchemaV1, StateCasResult, StateNamespaceOptions, StateValue, StateWriteOptions, StealthClient, StealthCookieStore, StealthCookieStoreV1, 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.js";
41
+ 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, HttpRedirectFailureReason, HttpRedirectPolicy, HttpRedirectPolicyMode, HttpResponse, HttpRetryOptions, HttpRetrySummary, HttpStreamResponse, IanaTimeZone, InferSchemaOutput, Iso3166Alpha2CountryCode, Iso4217CurrencyCode, Iso8601Duration, NativeContext, NativeNetworkClient, NativeNetworkCloseReason, NativeNetworkConnection, NativeNetworkConnectInput, NativeNetworkConnectOptions, NativeNetworkDynamicGrantOptions, NativeNetworkEgressGrant, NativeProviderConfig, NativeProviderContext, NativeProxyDrainHandler, NativeProxyEgressInfo, NativeProxyExpiringEvent, NativeProxyExpiringReason, NativeTcpDynamicEgressRule, NativeTcpEgressGrant, NativeTcpEgressRule, NativeTcpPortRange, NativeTcpTlsMode, NativeTlsConnectOptions, OperationAnnotations, OperationApprovalPolicy, OperationContractMetadata, OperationDefinition, OperationDeprecationMetadata, OperationDocMeta, OperationErrorCode, ProviderErrorStatus, 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, ProviderDeploymentOverrides, ProviderFileRef, ProviderFilesContext, ProviderHealthMonitorConfig, ProviderHealthProbeConfig, ProviderLocale, ProviderLocaleKey, ProviderLocaleKeyInput, ProviderLogoProfile, ProviderLogoSource, ProviderMeta, ProviderProxyConfig, ProviderProxyMode, ProviderProxyPolicy, ProxiedOAuthConfig, ProviderProxyProvider, ProviderProxySessionAffinity, ProviderPublicConnectionMode, ProviderPublicProfile, ProviderReviewed, ProviderResolvedFile, ProviderRuntimeState, ProviderSecretDeclaration, ProviderStateDurationString, ProviderStateNamespace, ProviderStreamEvent, ProviderSttConfig, ProviderSttMode, ProviderSupportLevel, RequestOptions, RedirectRunReason, Rfc3339Instant, SchemaLike, SmsOrigin, SmsOtpExtractionPattern, SmsOtpMatcherDefinition, SseMessage, StandardSchemaV1, StateCasResult, StateNamespaceOptions, StateValue, StateWriteOptions, StealthClient, StealthCookieStore, StealthCookieStoreV1, 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.js";
42
42
  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.js";
43
43
  export * from "./utils/date.js";
44
44
  export * from "./utils/parse.js";
package/dist/lint.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- type AuthModeLike = "none" | "platform-managed" | "credentials" | "oauth2" | "api-key";
1
+ type AuthModeLike = "none" | "platform-managed" | "credentials" | "oauth2" | "oauth2_proxied" | "api-key";
2
2
  type ProviderAuthLike = {
3
3
  mode?: AuthModeLike;
4
4
  flow?: {
@@ -7,6 +7,6 @@ export { AuthError, HttpRedirectError, isProviderError, isSessionExpiredError, i
7
7
  export { getProviderLocalePath, providerLocaleKey, qualifyProviderLocaleKey, } from "./i18n/index.js";
8
8
  export { type CreateProviderChoiceContextOptions, createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "./runtime/choice.js";
9
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.js";
10
- export type { AuthAbortData, AuthAbortRetry, AuthFlowTerminalContext, AuthMode, AuthSafeData, AuthSafeJson, FlowContext, HealthCheckAssertionContext, HealthCheckCase, HealthCheckSuite, HealthCheckUnsupported, HealthJourneyDefinition, HealthJourneyEventContext, HealthJourneyManualTriggerPolicy, HealthJourneyRunContext, HealthJourneyRunResult, HealthScheduleRandomization, HttpRedirectFailureReason, HttpRedirectPolicy, HttpRedirectPolicyMode, HttpRetryOptions, HttpRetrySummary, InferSchemaOutput, NativeContext, NativeNetworkClient, NativeNetworkCloseReason, NativeNetworkConnection, NativeNetworkConnectInput, NativeNetworkConnectOptions, NativeNetworkDynamicGrantOptions, NativeNetworkEgressGrant, NativeProviderConfig, NativeProviderContext, NativeProxyDrainHandler, NativeProxyEgressInfo, NativeProxyExpiringEvent, NativeProxyExpiringReason, NativeTcpDynamicEgressRule, NativeTcpEgressGrant, NativeTcpEgressRule, NativeTcpPortRange, NativeTcpTlsMode, NativeTlsConnectOptions, OperationApprovalPolicy, OperationContractMetadata, OperationDefinition, OperationDocMeta, OperationErrorCode, ProviderErrorStatus, OperationInputExample, OperationLifecycle, OperationObservabilityConfig, OperationObservabilitySensitiveConfig, OperationRelationships, OperationRiskClass, OperationSensitivePath, OperationToolRouterMetadata, OperationTransport, ProviderAccessVisibility, ProviderChoiceBindingOptions, ProviderChoiceContext, ProviderChoiceIssueOptions, ProviderChoiceParseOptions, ProviderContext, ProviderDefinition, ProviderDeploymentOverrides, ProviderFileRef, ProviderFilesContext, ProviderLocale, ProviderLocaleKey, ProviderLocaleKeyInput, ProviderLogoProfile, ProviderProxyPolicy, ProviderPublicConnectionMode, ProviderPublicProfile, ProviderResolvedFile, ProviderRuntimeState, ProviderStateDurationString, ProviderStateNamespace, ProviderSupportLevel, RedirectRunReason, SchemaLike, SmsOtpMatcherDefinition, StandardSchemaV1, StateCasResult, StateNamespaceOptions, StateValue, StateWriteOptions, } from "./types.js";
10
+ export type { AuthAbortData, AuthAbortRetry, AuthFlowTerminalContext, AuthMode, AuthSafeData, AuthSafeJson, FlowContext, HealthCheckAssertionContext, HealthCheckCase, HealthCheckSuite, HealthCheckUnsupported, HealthJourneyDefinition, HealthJourneyEventContext, HealthJourneyManualTriggerPolicy, HealthJourneyRunContext, HealthJourneyRunResult, HealthScheduleRandomization, HttpRedirectFailureReason, HttpRedirectPolicy, HttpRedirectPolicyMode, HttpRetryOptions, HttpRetrySummary, InferSchemaOutput, NativeContext, NativeNetworkClient, NativeNetworkCloseReason, NativeNetworkConnection, NativeNetworkConnectInput, NativeNetworkConnectOptions, NativeNetworkDynamicGrantOptions, NativeNetworkEgressGrant, NativeProviderConfig, NativeProviderContext, NativeProxyDrainHandler, NativeProxyEgressInfo, NativeProxyExpiringEvent, NativeProxyExpiringReason, NativeTcpDynamicEgressRule, NativeTcpEgressGrant, NativeTcpEgressRule, NativeTcpPortRange, NativeTcpTlsMode, NativeTlsConnectOptions, OperationApprovalPolicy, OperationContractMetadata, OperationDefinition, OperationDocMeta, OperationErrorCode, ProviderErrorStatus, OperationInputExample, OperationLifecycle, OperationObservabilityConfig, OperationObservabilitySensitiveConfig, OperationRelationships, OperationRiskClass, OperationSensitivePath, OperationToolRouterMetadata, OperationTransport, ProviderAccessVisibility, ProviderChoiceBindingOptions, ProviderChoiceContext, ProviderChoiceIssueOptions, ProviderChoiceParseOptions, ProviderContext, ProviderDefinition, ProviderDeploymentOverrides, ProviderFileRef, ProviderFilesContext, ProviderLocale, ProviderLocaleKey, ProviderLocaleKeyInput, ProviderLogoProfile, ProviderProxyPolicy, ProxiedOAuthConfig, ProviderPublicConnectionMode, ProviderPublicProfile, ProviderResolvedFile, ProviderRuntimeState, ProviderStateDurationString, ProviderStateNamespace, ProviderSupportLevel, RedirectRunReason, SchemaLike, SmsOtpMatcherDefinition, StandardSchemaV1, StateCasResult, StateNamespaceOptions, StateValue, StateWriteOptions, } from "./types.js";
11
11
  export { createNativeNetworkClient, createEnvVendorCredentialResolver, deriveNativeCredentialAffinityKey, NativeEgressGrantExpiredError, NativeEgressNotDeclaredError, NativeIdleTimeoutError, NativeNetworkError, NativeProxyExpiredError, resolveNativeGatewayProxy, type NativeGatewayProxy, type NativeGatewayProxyResolutionInput, type NativeGatewayProxySkipReason, type NativeGatewayProxySynthesizer, type NativeGatewayProxySynthesisResult, type NativeGatewayProxySynthesisInput, type NativeNetworkClientOptions, type NativeNetworkErrorCode, type VendorCredentialLookup, type VendorCredentialResolver, } from "./runtime/native-network.js";
12
12
  export { HttpRetryAfterPolicy, HttpRetryDelayStrategy, HttpRetryJitter, HttpRetryPreset, HttpRetryUnsafeMethodPolicy, } from "./types.js";
@@ -1,6 +1,7 @@
1
1
  import type { ContextScratchpad, EnvContext, FlowContext, HttpClient, StealthClient, SttContext } from "../types.js";
2
2
  export declare function createScratchpad(allowedKeys: string[], initial?: Record<string, unknown>): ContextScratchpad;
3
3
  export declare function createFlowContext(options: {
4
+ flowId?: string;
4
5
  http: HttpClient;
5
6
  stealth: StealthClient;
6
7
  env: EnvContext;
@@ -32,6 +32,7 @@ export function createScratchpad(allowedKeys, initial = {}) {
32
32
  }
33
33
  export function createFlowContext(options) {
34
34
  return {
35
+ flowId: options.flowId,
35
36
  connectionId: options.connectionId,
36
37
  externalRef: options.externalRef,
37
38
  tenantId: options.tenantId,
@@ -3,6 +3,90 @@ import { ProviderError } from "../errors.js";
3
3
  import { createProviderRedisClient, ensureRedisReady, withRedisTimeout, } from "./redis.js";
4
4
  const DEFAULT_REDIS_TIMEOUT_MS = 250;
5
5
  const REDIS_STATE_PREFIX = "apifuse:provider-state:v1";
6
+ const LEGACY_INDEX_SCAN_COUNT = 256;
7
+ const LEGACY_INDEX_SCAN_MAX_PAGES = 8;
8
+ const SET_WITH_QUOTA_SCRIPT = `
9
+ local now = tonumber(ARGV[1])
10
+ local max_entries = tonumber(ARGV[2])
11
+ local expires_at = tonumber(ARGV[3])
12
+ local index_ttl = tonumber(ARGV[4])
13
+ local envelope = ARGV[5]
14
+
15
+ redis.call("ZREMRANGEBYSCORE", KEYS[2], "-inf", now)
16
+ local exists = redis.call("EXISTS", KEYS[1])
17
+ local indexed = redis.call("ZSCORE", KEYS[2], KEYS[1])
18
+ if exists == 0 and not indexed and redis.call("ZCARD", KEYS[2]) >= max_entries then
19
+ return {0, false}
20
+ end
21
+
22
+ redis.call("SET", KEYS[1], envelope, "PXAT", expires_at)
23
+ redis.call("ZADD", KEYS[2], expires_at, KEYS[1])
24
+ redis.call("PEXPIRE", KEYS[2], index_ttl)
25
+ return {1, envelope}
26
+ `;
27
+ const COMPARE_AND_SET_WITH_QUOTA_SCRIPT = `
28
+ local current = redis.call("GET", KEYS[1])
29
+ local current_version = 0
30
+ if current then
31
+ local ok, decoded = pcall(cjson.decode, current)
32
+ if not ok or type(decoded) ~= "table" or type(decoded.version) ~= "number" then
33
+ return {-2, current}
34
+ end
35
+ current_version = decoded.version
36
+ end
37
+ if current_version ~= tonumber(ARGV[1]) then
38
+ return {-1, current or false}
39
+ end
40
+
41
+ local now = tonumber(ARGV[2])
42
+ local max_entries = tonumber(ARGV[3])
43
+ local expires_at = tonumber(ARGV[4])
44
+ local index_ttl = tonumber(ARGV[5])
45
+ local envelope = ARGV[6]
46
+ redis.call("ZREMRANGEBYSCORE", KEYS[2], "-inf", now)
47
+ local exists = current and 1 or 0
48
+ local indexed = redis.call("ZSCORE", KEYS[2], KEYS[1])
49
+ if exists == 0 and not indexed and redis.call("ZCARD", KEYS[2]) >= max_entries then
50
+ return {0, false}
51
+ end
52
+
53
+ redis.call("SET", KEYS[1], envelope, "PXAT", expires_at)
54
+ redis.call("ZADD", KEYS[2], expires_at, KEYS[1])
55
+ redis.call("PEXPIRE", KEYS[2], index_ttl)
56
+ return {1, envelope}
57
+ `;
58
+ const DELETE_WITH_INDEX_SCRIPT = `
59
+ redis.call("DEL", KEYS[1])
60
+ redis.call("ZREM", KEYS[2], KEYS[1])
61
+ return 1
62
+ `;
63
+ // Older SDKs wrote only the value key. Every operation that depends on the
64
+ // namespace index advances a bounded SCAN cursor and lazily imports active
65
+ // legacy envelopes into the new ZSET. The cursor is
66
+ // deliberately cyclic rather than permanently "complete": an old pod may
67
+ // still write an unindexed key during a rolling deploy. Each list/write call
68
+ // does a fixed amount of migration work; Redis KEYS and unbounded scans remain
69
+ // forbidden.
70
+ const BACKFILL_LEGACY_INDEX_SCRIPT = `
71
+ local now = tonumber(ARGV[1])
72
+ local index_ttl = tonumber(ARGV[2])
73
+ local next_cursor = ARGV[3]
74
+
75
+ redis.call("ZREMRANGEBYSCORE", KEYS[1], "-inf", now)
76
+ for i = 4, #ARGV, 3 do
77
+ local key = ARGV[i]
78
+ local expected = ARGV[i + 1]
79
+ local expires_at = tonumber(ARGV[i + 2])
80
+ if redis.call("GET", key) == expected then
81
+ redis.call("ZADD", KEYS[1], "NX", expires_at, key)
82
+ end
83
+ end
84
+ if redis.call("EXISTS", KEYS[1]) == 1 then
85
+ redis.call("PEXPIRE", KEYS[1], index_ttl)
86
+ end
87
+ redis.call("SET", KEYS[2], next_cursor, "PX", index_ttl)
88
+ return redis.call("ZCARD", KEYS[1])
89
+ `;
6
90
  const redisBackends = new Map();
7
91
  function getRedisBackend(redisUrl) {
8
92
  const existing = redisBackends.get(redisUrl);
@@ -46,6 +130,9 @@ function publicStateKey(providerId, namespace, redisKey) {
46
130
  const prefix = `${providerStatePrefix(providerId, namespace)}:`;
47
131
  return redisKey.startsWith(prefix) ? redisKey.slice(prefix.length) : redisKey;
48
132
  }
133
+ function redisGlobLiteral(value) {
134
+ return value.replace(/[\\*?\[\]]/g, "\\$&");
135
+ }
49
136
  function parseStateDurationMs(ttl) {
50
137
  const match = /^(\d+)(ms|s|m|h|d)$/.exec(ttl ?? "1h");
51
138
  if (!match)
@@ -119,12 +206,55 @@ class RedisProviderStateNamespace {
119
206
  redisKey(key) {
120
207
  return providerStateKey(this.providerId, this.namespaceName, key);
121
208
  }
122
- prefix() {
209
+ indexKey() {
210
+ // Keep bookkeeping outside the caller-owned keyspace. A provider may use
211
+ // any state key (including "__index"), so a suffix inside the namespace
212
+ // could turn the ZSET into a string and break every subsequent write.
213
+ const namespaceIdentity = Buffer.from(providerStatePrefix(this.providerId, this.namespaceName), "utf8").toString("base64url");
214
+ return `${REDIS_STATE_PREFIX}:index:${namespaceIdentity}`;
215
+ }
216
+ legacyScanCursorKey() {
217
+ return `${this.indexKey()}:legacy-scan-cursor`;
218
+ }
219
+ legacyPrefix() {
123
220
  return `${providerStatePrefix(this.providerId, this.namespaceName)}:`;
124
221
  }
125
- async activeKeys() {
222
+ async backfillLegacyIndex() {
126
223
  await requireRedisReady(this.backend.redis);
127
- return await withRequiredRedis(() => this.backend.redis.keys(`${this.prefix()}*`));
224
+ const cursorKey = this.legacyScanCursorKey();
225
+ let cursor = (await withRequiredRedis(() => this.backend.redis.get(cursorKey))) ?? "0";
226
+ const pattern = `${redisGlobLiteral(this.legacyPrefix())}*`;
227
+ const indexTtlMs = parseStateDurationMs(this.options.maxTtl);
228
+ for (let page = 0; page < LEGACY_INDEX_SCAN_MAX_PAGES; page += 1) {
229
+ const [nextCursor, keys] = await withRequiredRedis(() => this.backend.redis.scan(cursor, "MATCH", pattern, "COUNT", LEGACY_INDEX_SCAN_COUNT));
230
+ const rawValues = keys.length > 0
231
+ ? await withRequiredRedis(() => this.backend.redis.mget(keys))
232
+ : [];
233
+ const now = Date.now();
234
+ const activeLegacyArgs = [];
235
+ for (const [index, raw] of rawValues.entries()) {
236
+ const key = keys[index];
237
+ if (!key || !raw)
238
+ continue;
239
+ const envelope = envelopeFromJson(publicStateKey(this.providerId, this.namespaceName, key), raw);
240
+ const expiresAtMs = envelope ? Date.parse(envelope.expiresAt) : Number.NaN;
241
+ if (!Number.isFinite(expiresAtMs) || expiresAtMs <= now)
242
+ continue;
243
+ activeLegacyArgs.push(key, raw, String(expiresAtMs));
244
+ }
245
+ await withRequiredRedis(() => this.backend.redis.eval(BACKFILL_LEGACY_INDEX_SCRIPT, 2, this.indexKey(), cursorKey, String(now), String(indexTtlMs), nextCursor, ...activeLegacyArgs));
246
+ cursor = nextCursor;
247
+ if (cursor === "0")
248
+ break;
249
+ }
250
+ }
251
+ async indexedKeys(limit) {
252
+ await requireRedisReady(this.backend.redis);
253
+ const now = Date.now();
254
+ return await withRequiredRedis(async () => {
255
+ await this.backend.redis.zremrangebyscore(this.indexKey(), "-inf", now);
256
+ return await this.backend.redis.zrangebyscore(this.indexKey(), now + 1, "+inf", "LIMIT", 0, limit);
257
+ });
128
258
  }
129
259
  enforceValueSize(value) {
130
260
  const bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
@@ -132,20 +262,32 @@ class RedisProviderStateNamespace {
132
262
  throw new UnsupportedProviderStateError(`Provider runtime state value exceeds maxValueBytes (${bytes} > ${this.options.maxValueBytes})`);
133
263
  }
134
264
  }
135
- async enforceMaxEntries(key) {
136
- const keys = await this.activeKeys();
137
- const redisKey = this.redisKey(key);
138
- const otherKeys = keys.filter((candidate) => candidate !== redisKey);
139
- if (otherKeys.length >= this.options.maxEntries) {
140
- throw new UnsupportedProviderStateError(`Provider runtime state namespace quota exceeded (${otherKeys.length + 1} > ${this.options.maxEntries})`);
265
+ quotaExceeded() {
266
+ return new UnsupportedProviderStateError(`Provider runtime state namespace quota exceeded (${this.options.maxEntries + 1} > ${this.options.maxEntries})`);
267
+ }
268
+ writeTiming(ttl) {
269
+ const ttlMs = parseStateDurationMs(ttl ?? this.options.defaultTtl);
270
+ const maxTtlMs = parseStateDurationMs(this.options.maxTtl);
271
+ if (ttlMs > maxTtlMs) {
272
+ throw new UnsupportedProviderStateError(`Provider runtime state ttl exceeds maxTtl (${ttlMs} > ${maxTtlMs})`);
141
273
  }
274
+ const expiresAtMs = Date.now() + ttlMs;
275
+ return {
276
+ expiresAt: new Date(expiresAtMs).toISOString(),
277
+ expiresAtMs,
278
+ indexTtlMs: maxTtlMs,
279
+ };
142
280
  }
143
281
  async list(options) {
144
- const keys = (await this.activeKeys()).filter((key) => {
282
+ const requestedLimit = Math.max(0, options?.limit ?? this.options.maxEntries);
283
+ if (requestedLimit === 0)
284
+ return [];
285
+ await this.backfillLegacyIndex();
286
+ const keys = (await this.indexedKeys(this.options.maxEntries)).filter((key) => {
145
287
  const publicKey = publicStateKey(this.providerId, this.namespaceName, key);
146
288
  return options?.prefix ? publicKey.startsWith(options.prefix) : true;
147
289
  });
148
- const limited = keys.slice(0, Math.max(0, options?.limit ?? keys.length));
290
+ const limited = keys.slice(0, requestedLimit);
149
291
  if (limited.length === 0)
150
292
  return [];
151
293
  const values = await withRequiredRedis(() => this.backend.redis.mget(limited));
@@ -164,20 +306,22 @@ class RedisProviderStateNamespace {
164
306
  }
165
307
  async set(key, value, options) {
166
308
  this.enforceValueSize(value);
167
- await this.enforceMaxEntries(key);
309
+ await this.backfillLegacyIndex();
168
310
  const current = await this.get(key);
169
311
  const createdAt = current?.createdAt ?? new Date().toISOString();
170
312
  const version = (current?.version ?? 0) + 1;
171
- const ttl = options?.ttl ?? this.options.defaultTtl;
172
- const ttlMs = parseStateDurationMs(ttl);
173
- const expiresAt = resolveExpiresAt(ttl);
174
- const envelope = redisEnvelope(value, version, createdAt, expiresAt);
175
- await withRequiredRedis(() => this.backend.redis.set(this.redisKey(key), JSON.stringify(envelope), "PX", ttlMs));
313
+ const timing = this.writeTiming(options?.ttl);
314
+ const envelope = redisEnvelope(value, version, createdAt, timing.expiresAt);
315
+ await requireRedisReady(this.backend.redis);
316
+ const result = await withRequiredRedis(() => this.backend.redis.eval(SET_WITH_QUOTA_SCRIPT, 2, this.redisKey(key), this.indexKey(), String(Date.now()), String(this.options.maxEntries), String(timing.expiresAtMs), String(timing.indexTtlMs), JSON.stringify(envelope)));
317
+ if (!Array.isArray(result) || Number(result[0]) !== 1) {
318
+ throw this.quotaExceeded();
319
+ }
176
320
  return {
177
321
  key,
178
322
  value,
179
323
  version,
180
- expiresAt,
324
+ expiresAt: timing.expiresAt,
181
325
  createdAt,
182
326
  updatedAt: envelope.updatedAt,
183
327
  };
@@ -190,15 +334,38 @@ class RedisProviderStateNamespace {
190
334
  }
191
335
  async compareAndSet(key, expectedVersion, value, options) {
192
336
  this.enforceValueSize(value);
337
+ await this.backfillLegacyIndex();
193
338
  const current = await this.get(key);
194
339
  if ((current?.version ?? 0) !== expectedVersion) {
195
340
  return { ok: false, current };
196
341
  }
197
- return { ok: true, value: await this.set(key, value, options) };
342
+ const createdAt = current?.createdAt ?? new Date().toISOString();
343
+ const timing = this.writeTiming(options?.ttl);
344
+ const envelope = redisEnvelope(value, expectedVersion + 1, createdAt, timing.expiresAt);
345
+ await requireRedisReady(this.backend.redis);
346
+ const result = await withRequiredRedis(() => this.backend.redis.eval(COMPARE_AND_SET_WITH_QUOTA_SCRIPT, 2, this.redisKey(key), this.indexKey(), String(expectedVersion), String(Date.now()), String(this.options.maxEntries), String(timing.expiresAtMs), String(timing.indexTtlMs), JSON.stringify(envelope)));
347
+ if (Array.isArray(result) && Number(result[0]) === 0) {
348
+ throw this.quotaExceeded();
349
+ }
350
+ if (!Array.isArray(result) || Number(result[0]) !== 1) {
351
+ const rawCurrent = Array.isArray(result) && typeof result[1] === "string" ? result[1] : null;
352
+ return { ok: false, current: envelopeFromJson(key, rawCurrent) };
353
+ }
354
+ return {
355
+ ok: true,
356
+ value: {
357
+ key,
358
+ value,
359
+ version: envelope.version,
360
+ expiresAt: timing.expiresAt,
361
+ createdAt,
362
+ updatedAt: envelope.updatedAt,
363
+ },
364
+ };
198
365
  }
199
366
  async delete(key) {
200
367
  await requireRedisReady(this.backend.redis);
201
- await withRequiredRedis(() => this.backend.redis.del(this.redisKey(key)));
368
+ await withRequiredRedis(() => this.backend.redis.eval(DELETE_WITH_INDEX_SCRIPT, 2, this.redisKey(key), this.indexKey()));
202
369
  }
203
370
  async increment(key, field, delta = 1, options) {
204
371
  const current = (await this.get(key))?.value ?? {};
@@ -258,6 +425,23 @@ class MemoryProviderStateNamespace {
258
425
  constructor(options) {
259
426
  this.options = options;
260
427
  }
428
+ enforceValueSize(value) {
429
+ const bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
430
+ if (bytes > this.options.maxValueBytes) {
431
+ throw new UnsupportedProviderStateError(`Provider runtime state value exceeds maxValueBytes (${bytes} > ${this.options.maxValueBytes})`);
432
+ }
433
+ }
434
+ enforceWritePolicy(key, value, ttl) {
435
+ this.enforceValueSize(value);
436
+ const ttlMs = parseStateDurationMs(ttl ?? this.options.defaultTtl);
437
+ const maxTtlMs = parseStateDurationMs(this.options.maxTtl);
438
+ if (ttlMs > maxTtlMs) {
439
+ throw new UnsupportedProviderStateError(`Provider runtime state ttl exceeds maxTtl (${ttlMs} > ${maxTtlMs})`);
440
+ }
441
+ if (!this.values.has(key) && this.values.size >= this.options.maxEntries) {
442
+ throw new UnsupportedProviderStateError(`Provider runtime state namespace quota exceeded (${this.options.maxEntries + 1} > ${this.options.maxEntries})`);
443
+ }
444
+ }
261
445
  pruneExpired(nowMs = Date.now()) {
262
446
  for (const [key, row] of this.values.entries()) {
263
447
  if (row.expiresAt && Date.parse(row.expiresAt) <= nowMs) {
@@ -276,6 +460,7 @@ class MemoryProviderStateNamespace {
276
460
  }
277
461
  async set(key, value, options) {
278
462
  this.pruneExpired();
463
+ this.enforceWritePolicy(key, value, options?.ttl);
279
464
  const now = new Date().toISOString();
280
465
  const current = this.values.get(key);
281
466
  const expiresAt = resolveMemoryStateExpiresAt(options?.ttl ?? this.options.defaultTtl);
@@ -293,8 +478,24 @@ class MemoryProviderStateNamespace {
293
478
  async patch(_key, _partial, _options) {
294
479
  throw new UnsupportedProviderStateError("In-memory provider runtime state does not support patch");
295
480
  }
296
- async compareAndSet(_key, _expectedVersion, _value, _options) {
297
- throw new UnsupportedProviderStateError("In-memory provider runtime state does not support compareAndSet");
481
+ async compareAndSet(key, expectedVersion, value, options) {
482
+ this.pruneExpired();
483
+ const current = this.values.get(key);
484
+ if ((current?.version ?? 0) !== expectedVersion) {
485
+ return { ok: false, current: current ?? null };
486
+ }
487
+ this.enforceWritePolicy(key, value, options?.ttl);
488
+ const now = new Date().toISOString();
489
+ const stored = {
490
+ key,
491
+ value,
492
+ version: expectedVersion + 1,
493
+ expiresAt: resolveMemoryStateExpiresAt(options?.ttl ?? this.options.defaultTtl),
494
+ createdAt: current?.createdAt ?? now,
495
+ updatedAt: now,
496
+ };
497
+ this.values.set(key, stored);
498
+ return { ok: true, value: stored };
298
499
  }
299
500
  async delete(key) {
300
501
  this.values.delete(key);