@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.
Files changed (291) hide show
  1. package/AUTHORING.md +487 -0
  2. package/CHANGELOG.md +90 -0
  3. package/README.md +49 -3
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-check.ts +44 -59
  6. package/bin/apifuse-create.ts +1 -1
  7. package/bin/apifuse-dev.ts +27 -52
  8. package/bin/apifuse-pack-check.ts +14 -0
  9. package/bin/apifuse-pack-smoke.ts +36 -81
  10. package/bin/apifuse-pack-types.ts +305 -0
  11. package/bin/apifuse-perf.ts +45 -127
  12. package/bin/apifuse-record.ts +659 -111
  13. package/bin/apifuse-submit-check.ts +546 -48
  14. package/bin/apifuse-sync-assets.ts +117 -0
  15. package/bin/apifuse.ts +1 -1
  16. package/bin/submit-check-delimited-text.ts +50 -0
  17. package/bin/submit-check-xml-semantics.ts +204 -0
  18. package/bin/submit-check-xml.ts +134 -0
  19. package/dist/auth-turn/index.d.ts +3 -3
  20. package/dist/auth.d.ts +2 -2
  21. package/dist/auth.js +9 -18
  22. package/dist/ceremonies/index.d.ts +9 -1
  23. package/dist/ceremonies/index.js +65 -18
  24. package/dist/cli/commands.d.ts +1 -1
  25. package/dist/cli/commands.js +8 -0
  26. package/dist/cli/create.d.ts +3 -0
  27. package/dist/cli/create.js +34 -35
  28. package/dist/cli/prompt-assets.d.ts +80 -0
  29. package/dist/cli/prompt-assets.js +743 -0
  30. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  31. package/dist/config/loader.d.ts +176 -8
  32. package/dist/config/loader.js +424 -95
  33. package/dist/contract-serialization.d.ts +2 -2
  34. package/dist/contract-serialization.js +3 -6
  35. package/dist/contract-types.d.ts +2 -2
  36. package/dist/contract.d.ts +3 -3
  37. package/dist/contract.js +4 -6
  38. package/dist/define.d.ts +9 -1
  39. package/dist/define.js +304 -119
  40. package/dist/dev.d.ts +1 -1
  41. package/dist/dev.js +1 -1
  42. package/dist/error-resolution.d.ts +2 -0
  43. package/dist/error-resolution.js +90 -0
  44. package/dist/errors.d.ts +22 -1
  45. package/dist/errors.js +88 -0
  46. package/dist/fixture-sanitization.d.ts +26 -0
  47. package/dist/fixture-sanitization.js +216 -0
  48. package/dist/i18n/catalog.d.ts +2 -2
  49. package/dist/i18n/catalog.js +4 -10
  50. package/dist/i18n/index.d.ts +2 -2
  51. package/dist/i18n/index.js +2 -2
  52. package/dist/i18n/keys.d.ts +2 -2
  53. package/dist/index.d.ts +46 -42
  54. package/dist/index.js +41 -37
  55. package/dist/lint.d.ts +1 -1
  56. package/dist/lint.js +8 -15
  57. package/dist/native-address.d.ts +43 -0
  58. package/dist/native-address.js +281 -0
  59. package/dist/native-egress-policy.d.ts +31 -0
  60. package/dist/native-egress-policy.js +288 -0
  61. package/dist/observability.d.ts +5 -2
  62. package/dist/observability.js +48 -1
  63. package/dist/provider.d.ts +12 -11
  64. package/dist/provider.js +10 -9
  65. package/dist/public-schema-field-lint.d.ts +1 -1
  66. package/dist/recipes/gov-api.js +1 -1
  67. package/dist/runtime/auth-flow.d.ts +2 -1
  68. package/dist/runtime/auth-flow.js +4 -3
  69. package/dist/runtime/browser.d.ts +1 -1
  70. package/dist/runtime/browser.js +15 -29
  71. package/dist/runtime/cache.d.ts +1 -1
  72. package/dist/runtime/cache.js +4 -8
  73. package/dist/runtime/choice.d.ts +1 -1
  74. package/dist/runtime/choice.js +31 -35
  75. package/dist/runtime/credential.d.ts +1 -1
  76. package/dist/runtime/credential.js +1 -1
  77. package/dist/runtime/env.d.ts +1 -1
  78. package/dist/runtime/executor.d.ts +1 -1
  79. package/dist/runtime/executor.js +31 -4
  80. package/dist/runtime/http.d.ts +2 -2
  81. package/dist/runtime/http.js +387 -47
  82. package/dist/runtime/insights.d.ts +1 -1
  83. package/dist/runtime/insights.js +6 -13
  84. package/dist/runtime/instrumentation.d.ts +2 -2
  85. package/dist/runtime/instrumentation.js +345 -22
  86. package/dist/runtime/keyring.js +1 -1
  87. package/dist/runtime/namespace.js +1 -1
  88. package/dist/runtime/native-network.d.ts +127 -0
  89. package/dist/runtime/native-network.js +1298 -0
  90. package/dist/runtime/otlp.d.ts +1 -1
  91. package/dist/runtime/perf.d.ts +1 -1
  92. package/dist/runtime/provider.d.ts +1 -1
  93. package/dist/runtime/provider.js +1 -2
  94. package/dist/runtime/proxy-errors.d.ts +1 -1
  95. package/dist/runtime/proxy-errors.js +9 -7
  96. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  97. package/dist/runtime/proxy-nodemaven.js +146 -0
  98. package/dist/runtime/proxy-retry-policy.d.ts +2 -2
  99. package/dist/runtime/proxy-retry-policy.js +2 -2
  100. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  101. package/dist/runtime/proxy-telemetry.js +55 -52
  102. package/dist/runtime/redirects.d.ts +29 -0
  103. package/dist/runtime/redirects.js +36 -0
  104. package/dist/runtime/redis.d.ts +1 -1
  105. package/dist/runtime/redis.js +2 -4
  106. package/dist/runtime/request-options.d.ts +68 -1
  107. package/dist/runtime/request-options.js +548 -0
  108. package/dist/runtime/secrets.d.ts +27 -0
  109. package/dist/runtime/secrets.js +51 -0
  110. package/dist/runtime/state.d.ts +2 -2
  111. package/dist/runtime/state.js +238 -26
  112. package/dist/runtime/stealth.d.ts +5 -3
  113. package/dist/runtime/stealth.js +423 -143
  114. package/dist/runtime/stt.d.ts +1 -1
  115. package/dist/runtime/stt.js +11 -15
  116. package/dist/runtime/trace.d.ts +2 -2
  117. package/dist/runtime/trace.js +2 -4
  118. package/dist/runtime/waterfall.d.ts +1 -1
  119. package/dist/schema.d.ts +1 -1
  120. package/dist/schema.js +7 -15
  121. package/dist/serve.d.ts +1 -1
  122. package/dist/serve.js +1 -1
  123. package/dist/server/index.d.ts +7 -7
  124. package/dist/server/index.js +6 -6
  125. package/dist/server/self-test-input-tokens.d.ts +2 -1
  126. package/dist/server/self-test-input-tokens.js +18 -14
  127. package/dist/server/self-test-redaction.d.ts +1 -1
  128. package/dist/server/self-test-redaction.js +1 -1
  129. package/dist/server/self-test.d.ts +104 -3
  130. package/dist/server/self-test.js +673 -115
  131. package/dist/server/serve.d.ts +116 -4
  132. package/dist/server/serve.js +799 -128
  133. package/dist/server/types.d.ts +34 -9
  134. package/dist/server/types.js +8 -1
  135. package/dist/stateful/errors.d.ts +14 -0
  136. package/dist/stateful/errors.js +14 -0
  137. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  138. package/dist/stateful/http-provider-event-emitter.js +237 -0
  139. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  140. package/dist/stateful/http-session-owner-registry.js +210 -0
  141. package/dist/stateful/index.d.ts +18 -0
  142. package/dist/stateful/index.js +18 -0
  143. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  144. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  145. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  146. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  147. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  148. package/dist/stateful/provider-event-pipeline.js +1 -0
  149. package/dist/stateful/provider-events.d.ts +101 -0
  150. package/dist/stateful/provider-events.js +289 -0
  151. package/dist/stateful/session-key.d.ts +15 -0
  152. package/dist/stateful/session-key.js +86 -0
  153. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  154. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  155. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  156. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  157. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  158. package/dist/stateful/stateful-provider-adapter.js +287 -0
  159. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  160. package/dist/stateful/stateful-provider-observability.js +161 -0
  161. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  162. package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
  163. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  164. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  165. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  166. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  167. package/dist/stateful/stateful-provider-session-routing.d.ts +71 -0
  168. package/dist/stateful/stateful-provider-session-routing.js +353 -0
  169. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  170. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  171. package/dist/stateful-signing.d.ts +18 -0
  172. package/dist/stateful-signing.js +27 -0
  173. package/dist/stealth/profiles.d.ts +1 -1
  174. package/dist/stealth/profiles.js +5 -14
  175. package/dist/stream-evidence.d.ts +74 -0
  176. package/dist/stream-evidence.js +785 -0
  177. package/dist/stream.d.ts +1 -1
  178. package/dist/testing/index.d.ts +2 -2
  179. package/dist/testing/index.js +2 -2
  180. package/dist/testing/run.d.ts +32 -2
  181. package/dist/testing/run.js +478 -28
  182. package/dist/types.d.ts +342 -14
  183. package/dist/types.js +1 -0
  184. package/dist/user-input.d.ts +30 -0
  185. package/dist/user-input.js +66 -0
  186. package/package.json +16 -5
  187. package/src/auth-turn/index.ts +1 -1
  188. package/src/auth.ts +28 -86
  189. package/src/ceremonies/index.ts +103 -78
  190. package/src/cli/commands.ts +10 -0
  191. package/src/cli/create.ts +42 -35
  192. package/src/cli/prompt-assets.ts +865 -0
  193. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  194. package/src/config/loader.ts +652 -204
  195. package/src/contract-serialization.ts +5 -11
  196. package/src/contract-types.ts +2 -2
  197. package/src/contract.ts +12 -28
  198. package/src/define.ts +499 -498
  199. package/src/dev.ts +4 -9
  200. package/src/error-resolution.ts +91 -0
  201. package/src/errors.ts +110 -5
  202. package/src/fixture-sanitization.ts +247 -0
  203. package/src/i18n/catalog.ts +10 -32
  204. package/src/i18n/index.ts +2 -2
  205. package/src/i18n/keys.ts +5 -11
  206. package/src/index.ts +112 -42
  207. package/src/lint.ts +88 -152
  208. package/src/native-address.ts +340 -0
  209. package/src/native-egress-policy.ts +358 -0
  210. package/src/observability.ts +51 -1
  211. package/src/provider.ts +66 -11
  212. package/src/public-schema-field-lint.ts +7 -33
  213. package/src/recipes/gov-api.ts +2 -5
  214. package/src/runtime/auth-flow.ts +7 -7
  215. package/src/runtime/browser.ts +64 -187
  216. package/src/runtime/cache.ts +20 -67
  217. package/src/runtime/choice.ts +79 -132
  218. package/src/runtime/credential.ts +2 -2
  219. package/src/runtime/env.ts +1 -1
  220. package/src/runtime/executor.ts +43 -20
  221. package/src/runtime/http.ts +494 -57
  222. package/src/runtime/insights.ts +15 -53
  223. package/src/runtime/instrumentation.ts +495 -66
  224. package/src/runtime/keyring.ts +7 -19
  225. package/src/runtime/namespace.ts +2 -7
  226. package/src/runtime/native-network.ts +1686 -0
  227. package/src/runtime/otlp.ts +12 -23
  228. package/src/runtime/perf.ts +1 -1
  229. package/src/runtime/provider.ts +4 -9
  230. package/src/runtime/proxy-errors.ts +29 -42
  231. package/src/runtime/proxy-nodemaven.ts +221 -0
  232. package/src/runtime/proxy-retry-policy.ts +3 -3
  233. package/src/runtime/proxy-telemetry.ts +79 -77
  234. package/src/runtime/redirects.ts +66 -0
  235. package/src/runtime/redis.ts +4 -12
  236. package/src/runtime/request-options.ts +679 -9
  237. package/src/runtime/secrets.ts +64 -0
  238. package/src/runtime/state.ts +353 -133
  239. package/src/runtime/stealth.ts +505 -154
  240. package/src/runtime/stt.ts +38 -94
  241. package/src/runtime/trace.ts +14 -44
  242. package/src/runtime/waterfall.ts +5 -18
  243. package/src/schema.ts +23 -84
  244. package/src/serve.ts +1 -1
  245. package/src/server/index.ts +29 -7
  246. package/src/server/self-test-input-tokens.ts +29 -14
  247. package/src/server/self-test-redaction.ts +2 -2
  248. package/src/server/self-test.ts +857 -132
  249. package/src/server/serve.ts +1151 -328
  250. package/src/server/types.ts +12 -13
  251. package/src/stateful/README.md +146 -0
  252. package/src/stateful/errors.ts +23 -0
  253. package/src/stateful/http-provider-event-emitter.ts +314 -0
  254. package/src/stateful/http-session-owner-registry.ts +306 -0
  255. package/src/stateful/index.ts +18 -0
  256. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  257. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  258. package/src/stateful/provider-event-pipeline.ts +61 -0
  259. package/src/stateful/provider-events.ts +462 -0
  260. package/src/stateful/session-key.ts +111 -0
  261. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  262. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  263. package/src/stateful/stateful-provider-adapter.ts +562 -0
  264. package/src/stateful/stateful-provider-observability.ts +261 -0
  265. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  266. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  267. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  268. package/src/stateful/stateful-provider-session-routing.ts +555 -0
  269. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  270. package/src/stateful-signing.ts +46 -0
  271. package/src/stealth/profiles.ts +10 -26
  272. package/src/stream-evidence.ts +988 -0
  273. package/src/stream.ts +8 -19
  274. package/src/testing/index.ts +10 -2
  275. package/src/testing/run.ts +653 -74
  276. package/src/types.ts +408 -28
  277. package/src/user-input.ts +118 -0
  278. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  279. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  280. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  281. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  282. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  283. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  284. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  285. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  286. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  287. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  288. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  289. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  290. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  291. /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
+ }
@@ -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;
@@ -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
- const parsed = JSON.parse(raw);
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
- 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() {
112
220
  return `${providerStatePrefix(this.providerId, this.namespaceName)}:`;
113
221
  }
114
- async activeKeys() {
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
- return await withRequiredRedis(() => this.backend.redis.keys(`${this.prefix()}*`));
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
- async enforceMaxEntries(key) {
125
- const keys = await this.activeKeys();
126
- const redisKey = this.redisKey(key);
127
- const otherKeys = keys.filter((candidate) => candidate !== redisKey);
128
- if (otherKeys.length >= this.options.maxEntries) {
129
- 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})`);
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 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) => {
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, Math.max(0, options?.limit ?? keys.length));
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.enforceMaxEntries(key);
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 ttl = options?.ttl ?? this.options.defaultTtl;
161
- const ttlMs = parseStateDurationMs(ttl);
162
- const expiresAt = resolveExpiresAt(ttl);
163
- const envelope = redisEnvelope(value, version, createdAt, expiresAt);
164
- 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
+ }
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
- 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
+ };
187
365
  }
188
366
  async delete(key) {
189
367
  await requireRedisReady(this.backend.redis);
190
- 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()));
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(_key, _expectedVersion, _value, _options) {
286
- 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 };
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 {};