@apifuse/provider-sdk 2.2.0-beta.2 → 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/AUTHORING.md +487 -0
- package/CHANGELOG.md +90 -0
- package/README.md +49 -3
- package/SUBMISSION.md +1 -1
- package/bin/apifuse-check.ts +44 -59
- package/bin/apifuse-create.ts +1 -1
- package/bin/apifuse-dev.ts +27 -52
- package/bin/apifuse-pack-check.ts +14 -0
- package/bin/apifuse-pack-smoke.ts +36 -81
- package/bin/apifuse-pack-types.ts +305 -0
- package/bin/apifuse-perf.ts +45 -127
- package/bin/apifuse-record.ts +659 -111
- package/bin/apifuse-submit-check.ts +546 -48
- package/bin/apifuse-sync-assets.ts +117 -0
- package/bin/apifuse.ts +1 -1
- package/bin/submit-check-delimited-text.ts +50 -0
- package/bin/submit-check-xml-semantics.ts +204 -0
- package/bin/submit-check-xml.ts +134 -0
- package/dist/auth-turn/index.d.ts +3 -3
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +9 -18
- package/dist/ceremonies/index.d.ts +9 -1
- package/dist/ceremonies/index.js +65 -18
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +8 -0
- package/dist/cli/create.d.ts +3 -0
- package/dist/cli/create.js +34 -35
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/dist/config/loader.d.ts +176 -8
- package/dist/config/loader.js +424 -95
- package/dist/contract-serialization.d.ts +2 -2
- package/dist/contract-serialization.js +3 -6
- package/dist/contract-types.d.ts +2 -2
- package/dist/contract.d.ts +3 -3
- package/dist/contract.js +4 -6
- package/dist/define.d.ts +9 -1
- package/dist/define.js +304 -119
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +1 -1
- package/dist/error-resolution.d.ts +2 -0
- package/dist/error-resolution.js +90 -0
- package/dist/errors.d.ts +22 -1
- package/dist/errors.js +88 -0
- package/dist/fixture-sanitization.d.ts +26 -0
- package/dist/fixture-sanitization.js +216 -0
- package/dist/i18n/catalog.d.ts +2 -2
- package/dist/i18n/catalog.js +4 -10
- package/dist/i18n/index.d.ts +2 -2
- package/dist/i18n/index.js +2 -2
- package/dist/i18n/keys.d.ts +2 -2
- package/dist/index.d.ts +46 -42
- package/dist/index.js +41 -37
- package/dist/lint.d.ts +1 -1
- package/dist/lint.js +8 -15
- package/dist/native-address.d.ts +43 -0
- package/dist/native-address.js +281 -0
- package/dist/native-egress-policy.d.ts +31 -0
- package/dist/native-egress-policy.js +288 -0
- package/dist/observability.d.ts +5 -2
- package/dist/observability.js +48 -1
- package/dist/provider.d.ts +12 -11
- package/dist/provider.js +10 -9
- package/dist/public-schema-field-lint.d.ts +1 -1
- package/dist/recipes/gov-api.js +1 -1
- package/dist/runtime/auth-flow.d.ts +2 -1
- package/dist/runtime/auth-flow.js +4 -3
- package/dist/runtime/browser.d.ts +1 -1
- package/dist/runtime/browser.js +15 -29
- package/dist/runtime/cache.d.ts +1 -1
- package/dist/runtime/cache.js +4 -8
- package/dist/runtime/choice.d.ts +1 -1
- package/dist/runtime/choice.js +31 -35
- package/dist/runtime/credential.d.ts +1 -1
- package/dist/runtime/credential.js +1 -1
- package/dist/runtime/env.d.ts +1 -1
- package/dist/runtime/executor.d.ts +1 -1
- package/dist/runtime/executor.js +31 -4
- package/dist/runtime/http.d.ts +2 -2
- package/dist/runtime/http.js +387 -47
- package/dist/runtime/insights.d.ts +1 -1
- package/dist/runtime/insights.js +6 -13
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +345 -22
- package/dist/runtime/keyring.js +1 -1
- package/dist/runtime/namespace.js +1 -1
- package/dist/runtime/native-network.d.ts +127 -0
- package/dist/runtime/native-network.js +1298 -0
- package/dist/runtime/otlp.d.ts +1 -1
- package/dist/runtime/perf.d.ts +1 -1
- package/dist/runtime/provider.d.ts +1 -1
- package/dist/runtime/provider.js +1 -2
- package/dist/runtime/proxy-errors.d.ts +1 -1
- package/dist/runtime/proxy-errors.js +9 -7
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +146 -0
- package/dist/runtime/proxy-retry-policy.d.ts +2 -2
- package/dist/runtime/proxy-retry-policy.js +2 -2
- package/dist/runtime/proxy-telemetry.d.ts +2 -1
- package/dist/runtime/proxy-telemetry.js +55 -52
- package/dist/runtime/redirects.d.ts +29 -0
- package/dist/runtime/redirects.js +36 -0
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +2 -4
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +2 -2
- package/dist/runtime/state.js +238 -26
- package/dist/runtime/stealth.d.ts +5 -3
- package/dist/runtime/stealth.js +423 -143
- package/dist/runtime/stt.d.ts +1 -1
- package/dist/runtime/stt.js +11 -15
- package/dist/runtime/trace.d.ts +2 -2
- package/dist/runtime/trace.js +2 -4
- package/dist/runtime/waterfall.d.ts +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +7 -15
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/index.d.ts +7 -7
- package/dist/server/index.js +6 -6
- package/dist/server/self-test-input-tokens.d.ts +2 -1
- package/dist/server/self-test-input-tokens.js +18 -14
- package/dist/server/self-test-redaction.d.ts +1 -1
- package/dist/server/self-test-redaction.js +1 -1
- package/dist/server/self-test.d.ts +104 -3
- package/dist/server/self-test.js +673 -115
- package/dist/server/serve.d.ts +116 -4
- package/dist/server/serve.js +799 -128
- package/dist/server/types.d.ts +34 -9
- package/dist/server/types.js +8 -1
- package/dist/stateful/errors.d.ts +14 -0
- package/dist/stateful/errors.js +14 -0
- package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
- package/dist/stateful/http-provider-event-emitter.js +237 -0
- package/dist/stateful/http-session-owner-registry.d.ts +44 -0
- package/dist/stateful/http-session-owner-registry.js +210 -0
- package/dist/stateful/index.d.ts +18 -0
- package/dist/stateful/index.js +18 -0
- package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
- package/dist/stateful/provider-event-delivery-failures.js +43 -0
- package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
- package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
- package/dist/stateful/provider-event-pipeline.d.ts +50 -0
- package/dist/stateful/provider-event-pipeline.js +1 -0
- package/dist/stateful/provider-events.d.ts +101 -0
- package/dist/stateful/provider-events.js +289 -0
- package/dist/stateful/session-key.d.ts +15 -0
- package/dist/stateful/session-key.js +86 -0
- package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
- package/dist/stateful/stateful-provider-adapter-context.js +42 -0
- package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
- package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
- package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
- package/dist/stateful/stateful-provider-adapter.js +287 -0
- package/dist/stateful/stateful-provider-observability.d.ts +62 -0
- package/dist/stateful/stateful-provider-observability.js +161 -0
- package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
- package/dist/stateful/stateful-provider-runtime-context.js +60 -0
- package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
- package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +71 -0
- package/dist/stateful/stateful-provider-session-routing.js +353 -0
- package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
- package/dist/stateful/stateful-provider-session-runtime.js +245 -0
- package/dist/stateful-signing.d.ts +18 -0
- package/dist/stateful-signing.js +27 -0
- package/dist/stealth/profiles.d.ts +1 -1
- package/dist/stealth/profiles.js +5 -14
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.d.ts +1 -1
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +478 -28
- package/dist/types.d.ts +342 -14
- package/dist/types.js +1 -0
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +16 -5
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +28 -86
- package/src/ceremonies/index.ts +103 -78
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +42 -35
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/config/loader.ts +652 -204
- package/src/contract-serialization.ts +5 -11
- package/src/contract-types.ts +2 -2
- package/src/contract.ts +12 -28
- package/src/define.ts +499 -498
- package/src/dev.ts +4 -9
- package/src/error-resolution.ts +91 -0
- package/src/errors.ts +110 -5
- package/src/fixture-sanitization.ts +247 -0
- package/src/i18n/catalog.ts +10 -32
- package/src/i18n/index.ts +2 -2
- package/src/i18n/keys.ts +5 -11
- package/src/index.ts +112 -42
- package/src/lint.ts +88 -152
- package/src/native-address.ts +340 -0
- package/src/native-egress-policy.ts +358 -0
- package/src/observability.ts +51 -1
- package/src/provider.ts +66 -11
- package/src/public-schema-field-lint.ts +7 -33
- package/src/recipes/gov-api.ts +2 -5
- package/src/runtime/auth-flow.ts +7 -7
- package/src/runtime/browser.ts +64 -187
- package/src/runtime/cache.ts +20 -67
- package/src/runtime/choice.ts +79 -132
- package/src/runtime/credential.ts +2 -2
- package/src/runtime/env.ts +1 -1
- package/src/runtime/executor.ts +43 -20
- package/src/runtime/http.ts +494 -57
- package/src/runtime/insights.ts +15 -53
- package/src/runtime/instrumentation.ts +495 -66
- package/src/runtime/keyring.ts +7 -19
- package/src/runtime/namespace.ts +2 -7
- package/src/runtime/native-network.ts +1686 -0
- package/src/runtime/otlp.ts +12 -23
- package/src/runtime/perf.ts +1 -1
- package/src/runtime/provider.ts +4 -9
- package/src/runtime/proxy-errors.ts +29 -42
- package/src/runtime/proxy-nodemaven.ts +221 -0
- package/src/runtime/proxy-retry-policy.ts +3 -3
- package/src/runtime/proxy-telemetry.ts +79 -77
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +4 -12
- package/src/runtime/request-options.ts +679 -9
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +353 -133
- package/src/runtime/stealth.ts +505 -154
- package/src/runtime/stt.ts +38 -94
- package/src/runtime/trace.ts +14 -44
- package/src/runtime/waterfall.ts +5 -18
- package/src/schema.ts +23 -84
- package/src/serve.ts +1 -1
- package/src/server/index.ts +29 -7
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test-redaction.ts +2 -2
- package/src/server/self-test.ts +857 -132
- package/src/server/serve.ts +1151 -328
- package/src/server/types.ts +12 -13
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +23 -0
- package/src/stateful/http-provider-event-emitter.ts +314 -0
- package/src/stateful/http-session-owner-registry.ts +306 -0
- package/src/stateful/index.ts +18 -0
- package/src/stateful/provider-event-delivery-failures.ts +80 -0
- package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
- package/src/stateful/provider-event-pipeline.ts +61 -0
- package/src/stateful/provider-events.ts +462 -0
- package/src/stateful/session-key.ts +111 -0
- package/src/stateful/stateful-provider-adapter-context.ts +59 -0
- package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
- package/src/stateful/stateful-provider-adapter.ts +562 -0
- package/src/stateful/stateful-provider-observability.ts +261 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
- package/src/stateful/stateful-provider-runtime-context.ts +92 -0
- package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
- package/src/stateful/stateful-provider-session-routing.ts +555 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +10 -26
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +8 -19
- package/src/testing/index.ts +10 -2
- package/src/testing/run.ts +653 -74
- package/src/types.ts +408 -28
- package/src/user-input.ts +118 -0
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ProviderSecretError } from "../errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Canonical error code for a declared-but-unprovisioned provider secret.
|
|
4
|
+
*
|
|
5
|
+
* The SDK is the single source of truth for env/secret presence validation:
|
|
6
|
+
* providers declare secrets in `defineProvider({ secrets: [...] })` and the
|
|
7
|
+
* runtime enforces presence before any handler or auth-flow code runs.
|
|
8
|
+
* Provider-local presence guards (requireServiceKey/requireApiKey style) are a
|
|
9
|
+
* deprecated antipattern — see the `sdk-owned-secret-presence` submit-check
|
|
10
|
+
* rule.
|
|
11
|
+
*/
|
|
12
|
+
export const MISSING_SECRET_CODE = "MISSING_SECRET";
|
|
13
|
+
/**
|
|
14
|
+
* Names of declared `required: true` secrets whose env values are unset or
|
|
15
|
+
* whitespace-only. Whitespace-only values count as missing for parity with the
|
|
16
|
+
* `.trim()` guards well-built providers used before the SDK owned this check —
|
|
17
|
+
* a blank value provisioned by a broken secret pipeline must not pass the gate.
|
|
18
|
+
*/
|
|
19
|
+
export function listMissingRequiredSecrets(provider, env) {
|
|
20
|
+
const missing = [];
|
|
21
|
+
for (const secret of provider.secrets ?? []) {
|
|
22
|
+
if (secret.required !== true) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const value = env.get(secret.name);
|
|
26
|
+
if (value === undefined || value.trim() === "") {
|
|
27
|
+
missing.push(secret.name);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return missing;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Throws the canonical structured missing-secret error when any declared
|
|
34
|
+
* `required: true` secret is absent. All missing names are reported in a
|
|
35
|
+
* single error so operators can provision the full set in one pass instead of
|
|
36
|
+
* discovering them one deploy at a time (the 2026-07-22 unprovisioned-secret
|
|
37
|
+
* incident failure mode).
|
|
38
|
+
*/
|
|
39
|
+
export function assertRequiredSecretsPresent(provider, env) {
|
|
40
|
+
const missing = listMissingRequiredSecrets(provider, env);
|
|
41
|
+
if (missing.length === 0) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const names = missing.join(", ");
|
|
45
|
+
throw new ProviderSecretError(`Missing required provider secret${missing.length > 1 ? "s" : ""}: ${names}`, {
|
|
46
|
+
code: MISSING_SECRET_CODE,
|
|
47
|
+
category: "credential_unavailable",
|
|
48
|
+
retryable: false,
|
|
49
|
+
fix: `Provision ${names} in the provider environment (e.g. Doppler). Declared in defineProvider({ secrets: [...] }).`,
|
|
50
|
+
});
|
|
51
|
+
}
|
package/dist/runtime/state.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ProviderError } from "../errors";
|
|
2
|
-
import type { ProviderRuntimeState } from "../types";
|
|
1
|
+
import { ProviderError } from "../errors.js";
|
|
2
|
+
import type { ProviderRuntimeState } from "../types.js";
|
|
3
3
|
type RedisProviderRuntimeStateOptions = {
|
|
4
4
|
readonly redisUrl: string;
|
|
5
5
|
readonly providerId?: string;
|
package/dist/runtime/state.js
CHANGED
|
@@ -1,8 +1,92 @@
|
|
|
1
|
-
import { providerStateRedisUrlFromEnv } from "../config/loader";
|
|
2
|
-
import { ProviderError } from "../errors";
|
|
3
|
-
import { createProviderRedisClient, ensureRedisReady, withRedisTimeout, } from "./redis";
|
|
1
|
+
import { providerStateRedisUrlFromEnv } from "../config/loader.js";
|
|
2
|
+
import { ProviderError } from "../errors.js";
|
|
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)
|
|
@@ -69,7 +156,18 @@ function resolveExpiresAt(ttl) {
|
|
|
69
156
|
function envelopeFromJson(key, raw) {
|
|
70
157
|
if (!raw)
|
|
71
158
|
return null;
|
|
72
|
-
|
|
159
|
+
// A corrupt/undecodable persisted envelope must be treated as absent rather
|
|
160
|
+
// than throwing a raw JSON.parse SyntaxError: an uncaught SyntaxError escapes
|
|
161
|
+
// the provider error taxonomy, is masked as internal_error 500, and is then
|
|
162
|
+
// retried by the hub (2026-07-22 catchtable reserve RCA, candidate A). Returning
|
|
163
|
+
// null also keeps list() from aborting the whole scan on a single bad entry.
|
|
164
|
+
let parsed;
|
|
165
|
+
try {
|
|
166
|
+
parsed = JSON.parse(raw);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
73
171
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
74
172
|
return null;
|
|
75
173
|
}
|
|
@@ -108,12 +206,55 @@ class RedisProviderStateNamespace {
|
|
|
108
206
|
redisKey(key) {
|
|
109
207
|
return providerStateKey(this.providerId, this.namespaceName, key);
|
|
110
208
|
}
|
|
111
|
-
|
|
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() {
|
|
112
220
|
return `${providerStatePrefix(this.providerId, this.namespaceName)}:`;
|
|
113
221
|
}
|
|
114
|
-
async
|
|
222
|
+
async backfillLegacyIndex() {
|
|
223
|
+
await requireRedisReady(this.backend.redis);
|
|
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) {
|
|
115
252
|
await requireRedisReady(this.backend.redis);
|
|
116
|
-
|
|
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
|
+
});
|
|
117
258
|
}
|
|
118
259
|
enforceValueSize(value) {
|
|
119
260
|
const bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
@@ -121,20 +262,32 @@ class RedisProviderStateNamespace {
|
|
|
121
262
|
throw new UnsupportedProviderStateError(`Provider runtime state value exceeds maxValueBytes (${bytes} > ${this.options.maxValueBytes})`);
|
|
122
263
|
}
|
|
123
264
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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})`);
|
|
130
273
|
}
|
|
274
|
+
const expiresAtMs = Date.now() + ttlMs;
|
|
275
|
+
return {
|
|
276
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
277
|
+
expiresAtMs,
|
|
278
|
+
indexTtlMs: maxTtlMs,
|
|
279
|
+
};
|
|
131
280
|
}
|
|
132
281
|
async list(options) {
|
|
133
|
-
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) => {
|
|
134
287
|
const publicKey = publicStateKey(this.providerId, this.namespaceName, key);
|
|
135
288
|
return options?.prefix ? publicKey.startsWith(options.prefix) : true;
|
|
136
289
|
});
|
|
137
|
-
const limited = keys.slice(0,
|
|
290
|
+
const limited = keys.slice(0, requestedLimit);
|
|
138
291
|
if (limited.length === 0)
|
|
139
292
|
return [];
|
|
140
293
|
const values = await withRequiredRedis(() => this.backend.redis.mget(limited));
|
|
@@ -153,20 +306,22 @@ class RedisProviderStateNamespace {
|
|
|
153
306
|
}
|
|
154
307
|
async set(key, value, options) {
|
|
155
308
|
this.enforceValueSize(value);
|
|
156
|
-
await this.
|
|
309
|
+
await this.backfillLegacyIndex();
|
|
157
310
|
const current = await this.get(key);
|
|
158
311
|
const createdAt = current?.createdAt ?? new Date().toISOString();
|
|
159
312
|
const version = (current?.version ?? 0) + 1;
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
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
|
+
}
|
|
165
320
|
return {
|
|
166
321
|
key,
|
|
167
322
|
value,
|
|
168
323
|
version,
|
|
169
|
-
expiresAt,
|
|
324
|
+
expiresAt: timing.expiresAt,
|
|
170
325
|
createdAt,
|
|
171
326
|
updatedAt: envelope.updatedAt,
|
|
172
327
|
};
|
|
@@ -179,15 +334,38 @@ class RedisProviderStateNamespace {
|
|
|
179
334
|
}
|
|
180
335
|
async compareAndSet(key, expectedVersion, value, options) {
|
|
181
336
|
this.enforceValueSize(value);
|
|
337
|
+
await this.backfillLegacyIndex();
|
|
182
338
|
const current = await this.get(key);
|
|
183
339
|
if ((current?.version ?? 0) !== expectedVersion) {
|
|
184
340
|
return { ok: false, current };
|
|
185
341
|
}
|
|
186
|
-
|
|
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
|
+
};
|
|
187
365
|
}
|
|
188
366
|
async delete(key) {
|
|
189
367
|
await requireRedisReady(this.backend.redis);
|
|
190
|
-
await withRequiredRedis(() => this.backend.redis.
|
|
368
|
+
await withRequiredRedis(() => this.backend.redis.eval(DELETE_WITH_INDEX_SCRIPT, 2, this.redisKey(key), this.indexKey()));
|
|
191
369
|
}
|
|
192
370
|
async increment(key, field, delta = 1, options) {
|
|
193
371
|
const current = (await this.get(key))?.value ?? {};
|
|
@@ -247,6 +425,23 @@ class MemoryProviderStateNamespace {
|
|
|
247
425
|
constructor(options) {
|
|
248
426
|
this.options = options;
|
|
249
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
|
+
}
|
|
250
445
|
pruneExpired(nowMs = Date.now()) {
|
|
251
446
|
for (const [key, row] of this.values.entries()) {
|
|
252
447
|
if (row.expiresAt && Date.parse(row.expiresAt) <= nowMs) {
|
|
@@ -265,6 +460,7 @@ class MemoryProviderStateNamespace {
|
|
|
265
460
|
}
|
|
266
461
|
async set(key, value, options) {
|
|
267
462
|
this.pruneExpired();
|
|
463
|
+
this.enforceWritePolicy(key, value, options?.ttl);
|
|
268
464
|
const now = new Date().toISOString();
|
|
269
465
|
const current = this.values.get(key);
|
|
270
466
|
const expiresAt = resolveMemoryStateExpiresAt(options?.ttl ?? this.options.defaultTtl);
|
|
@@ -282,8 +478,24 @@ class MemoryProviderStateNamespace {
|
|
|
282
478
|
async patch(_key, _partial, _options) {
|
|
283
479
|
throw new UnsupportedProviderStateError("In-memory provider runtime state does not support patch");
|
|
284
480
|
}
|
|
285
|
-
async compareAndSet(
|
|
286
|
-
|
|
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 };
|
|
287
499
|
}
|
|
288
500
|
async delete(key) {
|
|
289
501
|
this.values.delete(key);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ImpitResponse } from "impit";
|
|
2
|
-
import type { ProxyResolutionOptions } from "../config/loader";
|
|
3
|
-
import type { StealthClient, StealthResponse } from "../types";
|
|
2
|
+
import type { ProxyResolutionOptions } from "../config/loader.js";
|
|
3
|
+
import type { StealthClient, StealthResponse } from "../types.js";
|
|
4
4
|
export type StealthClientOptions = ProxyResolutionOptions & {
|
|
5
5
|
warn?: (message: string) => void;
|
|
6
6
|
/**
|
|
@@ -13,9 +13,11 @@ export type StealthClientOptions = ProxyResolutionOptions & {
|
|
|
13
13
|
};
|
|
14
14
|
};
|
|
15
15
|
type StealthTransportResponse = Pick<ImpitResponse, "arrayBuffer" | "headers" | "json" | "ok" | "status" | "text"> & {
|
|
16
|
+
body?: ReadableStream<Uint8Array>;
|
|
17
|
+
abort?: () => void;
|
|
16
18
|
url?: string;
|
|
17
19
|
redirected?: boolean;
|
|
18
20
|
};
|
|
19
|
-
export declare function normalizeResponse(response: StealthTransportResponse, requestUrl?: string): Promise<StealthResponse>;
|
|
21
|
+
export declare function normalizeResponse(response: StealthTransportResponse, requestUrl?: string, maxBodyBytes?: number): Promise<StealthResponse>;
|
|
20
22
|
export declare function createStealthClient(baseUrl: string, defaultProfileOrOptions?: string | StealthClientOptions, clientOptions?: StealthClientOptions): StealthClient;
|
|
21
23
|
export {};
|