@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 +5 -0
- package/dist/ceremonies/index.d.ts +8 -0
- package/dist/ceremonies/index.js +57 -0
- package/dist/define.js +103 -1
- package/dist/index.d.ts +1 -1
- package/dist/lint.d.ts +1 -1
- package/dist/provider.d.ts +1 -1
- package/dist/runtime/auth-flow.d.ts +1 -0
- package/dist/runtime/auth-flow.js +1 -0
- package/dist/runtime/state.js +223 -22
- package/dist/server/serve.d.ts +9 -8
- package/dist/server/serve.js +7 -1
- package/dist/server/types.d.ts +4 -0
- package/dist/server/types.js +7 -1
- package/dist/testing/run.js +8 -2
- package/dist/types.d.ts +28 -12
- package/package.json +1 -1
- package/src/ceremonies/index.ts +79 -0
- package/src/define.ts +138 -1
- package/src/index.ts +1 -0
- package/src/lint.ts +7 -1
- package/src/provider.ts +1 -0
- package/src/runtime/auth-flow.ts +2 -0
- package/src/runtime/state.ts +317 -28
- package/src/server/serve.ts +20 -14
- package/src/server/types.ts +7 -1
- package/src/testing/run.ts +8 -2
- package/src/types.ts +48 -26
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;
|
package/dist/ceremonies/index.js
CHANGED
|
@@ -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 = [
|
|
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
package/dist/provider.d.ts
CHANGED
|
@@ -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;
|
package/dist/runtime/state.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
222
|
+
async backfillLegacyIndex() {
|
|
126
223
|
await requireRedisReady(this.backend.redis);
|
|
127
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
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,
|
|
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.
|
|
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
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
297
|
-
|
|
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);
|