@apifuse/provider-sdk 2.2.0-beta.3 → 2.2.0-beta.30

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 (342) hide show
  1. package/AUTHORING.md +493 -5
  2. package/CHANGELOG.md +127 -1
  3. package/README.md +52 -6
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-check.ts +106 -62
  6. package/bin/apifuse-create.ts +1 -1
  7. package/bin/apifuse-dev.ts +63 -55
  8. package/bin/apifuse-pack-check.ts +22 -2
  9. package/bin/apifuse-pack-smoke.ts +78 -82
  10. package/bin/apifuse-pack-types.ts +583 -0
  11. package/bin/apifuse-perf.ts +59 -140
  12. package/bin/apifuse-record.ts +698 -113
  13. package/bin/apifuse-submit-check.ts +517 -44
  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.ts +1 -1
  18. package/dist/auth-turn/index.d.ts +4 -4
  19. package/dist/auth-turn/index.js +1 -1
  20. package/dist/auth.d.ts +16 -2
  21. package/dist/auth.js +76 -18
  22. package/dist/ceremonies/index.d.ts +9 -1
  23. package/dist/ceremonies/index.js +117 -29
  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/cli/templates/provider/README.md.tpl +4 -4
  32. package/dist/config/loader.d.ts +176 -17
  33. package/dist/config/loader.js +434 -161
  34. package/dist/contract-serialization.d.ts +2 -2
  35. package/dist/contract-serialization.js +7 -14
  36. package/dist/contract-types.d.ts +3 -2
  37. package/dist/contract.d.ts +3 -3
  38. package/dist/contract.js +6 -6
  39. package/dist/declaration-validation.d.ts +23 -0
  40. package/dist/declaration-validation.js +159 -0
  41. package/dist/define.d.ts +13 -1
  42. package/dist/define.js +391 -122
  43. package/dist/dev.d.ts +1 -1
  44. package/dist/dev.js +1 -1
  45. package/dist/error-resolution.d.ts +4 -0
  46. package/dist/error-resolution.js +122 -0
  47. package/dist/errors.d.ts +19 -1
  48. package/dist/errors.js +41 -3
  49. package/dist/fixture-sanitization.d.ts +26 -0
  50. package/dist/fixture-sanitization.js +216 -0
  51. package/dist/i18n/catalog.d.ts +2 -2
  52. package/dist/i18n/catalog.js +4 -10
  53. package/dist/i18n/index.d.ts +2 -2
  54. package/dist/i18n/index.js +2 -2
  55. package/dist/i18n/keys.d.ts +2 -2
  56. package/dist/index.d.ts +50 -42
  57. package/dist/index.js +41 -37
  58. package/dist/lint.d.ts +6 -1
  59. package/dist/lint.js +370 -18
  60. package/dist/native-address.d.ts +43 -0
  61. package/dist/native-address.js +281 -0
  62. package/dist/native-egress-policy.d.ts +31 -0
  63. package/dist/native-egress-policy.js +288 -0
  64. package/dist/observability.d.ts +5 -2
  65. package/dist/observability.js +48 -1
  66. package/dist/provider.d.ts +13 -11
  67. package/dist/provider.js +10 -9
  68. package/dist/public-schema-field-lint.d.ts +1 -1
  69. package/dist/recipes/gov-api.js +1 -1
  70. package/dist/runtime/auth-flow.d.ts +3 -1
  71. package/dist/runtime/auth-flow.js +8 -3
  72. package/dist/runtime/browser.d.ts +1 -1
  73. package/dist/runtime/browser.js +138 -40
  74. package/dist/runtime/cache.d.ts +2 -1
  75. package/dist/runtime/cache.js +173 -23
  76. package/dist/runtime/choice-wordlist.d.ts +9 -0
  77. package/dist/runtime/choice-wordlist.js +138 -0
  78. package/dist/runtime/choice.d.ts +13 -1
  79. package/dist/runtime/choice.js +514 -124
  80. package/dist/runtime/credential.d.ts +1 -1
  81. package/dist/runtime/credential.js +1 -1
  82. package/dist/runtime/env.d.ts +1 -1
  83. package/dist/runtime/executor.d.ts +1 -1
  84. package/dist/runtime/executor.js +25 -3
  85. package/dist/runtime/http.d.ts +3 -2
  86. package/dist/runtime/http.js +517 -55
  87. package/dist/runtime/insights.d.ts +1 -1
  88. package/dist/runtime/insights.js +6 -13
  89. package/dist/runtime/instrumentation.d.ts +2 -2
  90. package/dist/runtime/instrumentation.js +371 -23
  91. package/dist/runtime/keyring.js +1 -1
  92. package/dist/runtime/namespace.js +1 -1
  93. package/dist/runtime/native-network-errors.d.ts +33 -0
  94. package/dist/runtime/native-network-errors.js +69 -0
  95. package/dist/runtime/native-network.d.ts +96 -0
  96. package/dist/runtime/native-network.js +1232 -0
  97. package/dist/runtime/ocr.d.ts +29 -0
  98. package/dist/runtime/ocr.js +440 -0
  99. package/dist/runtime/otlp.d.ts +1 -1
  100. package/dist/runtime/perf.d.ts +1 -1
  101. package/dist/runtime/provider.d.ts +1 -1
  102. package/dist/runtime/provider.js +1 -2
  103. package/dist/runtime/proxy-errors.d.ts +1 -1
  104. package/dist/runtime/proxy-errors.js +9 -7
  105. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  106. package/dist/runtime/proxy-nodemaven.js +146 -0
  107. package/dist/runtime/proxy-retry-policy.d.ts +2 -2
  108. package/dist/runtime/proxy-retry-policy.js +2 -2
  109. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  110. package/dist/runtime/proxy-telemetry.js +58 -52
  111. package/dist/runtime/redirects.d.ts +29 -0
  112. package/dist/runtime/redirects.js +36 -0
  113. package/dist/runtime/redis.d.ts +1 -1
  114. package/dist/runtime/redis.js +5 -5
  115. package/dist/runtime/request-options.d.ts +68 -1
  116. package/dist/runtime/request-options.js +548 -0
  117. package/dist/runtime/resolver-config.d.ts +6 -0
  118. package/dist/runtime/resolver-config.js +6 -0
  119. package/dist/runtime/resolver-public.d.ts +1 -0
  120. package/dist/runtime/resolver-public.js +1 -0
  121. package/dist/runtime/resolver-shared.d.ts +3 -0
  122. package/dist/runtime/resolver-shared.js +12 -0
  123. package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
  124. package/dist/runtime/resolver-vendors/bindings.js +40 -0
  125. package/dist/runtime/resolver-vendors/browser.d.ts +20 -0
  126. package/dist/runtime/resolver-vendors/browser.js +282 -0
  127. package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
  128. package/dist/runtime/resolver-vendors/hosts.js +33 -0
  129. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
  130. package/dist/runtime/resolver-vendors/twocaptcha.js +368 -0
  131. package/dist/runtime/resolver-vendors/types.d.ts +83 -0
  132. package/dist/runtime/resolver-vendors/types.js +69 -0
  133. package/dist/runtime/resolver.d.ts +59 -0
  134. package/dist/runtime/resolver.js +705 -0
  135. package/dist/runtime/secrets.d.ts +27 -0
  136. package/dist/runtime/secrets.js +51 -0
  137. package/dist/runtime/state.d.ts +5 -2
  138. package/dist/runtime/state.js +280 -74
  139. package/dist/runtime/stealth-cookies.d.ts +20 -0
  140. package/dist/runtime/stealth-cookies.js +111 -0
  141. package/dist/runtime/stealth.d.ts +30 -5
  142. package/dist/runtime/stealth.js +523 -259
  143. package/dist/runtime/stt.d.ts +1 -1
  144. package/dist/runtime/stt.js +12 -27
  145. package/dist/runtime/timeout.d.ts +5 -0
  146. package/dist/runtime/timeout.js +12 -0
  147. package/dist/runtime/trace.d.ts +2 -2
  148. package/dist/runtime/trace.js +2 -4
  149. package/dist/runtime/waterfall.d.ts +1 -1
  150. package/dist/schema.d.ts +1 -1
  151. package/dist/schema.js +7 -15
  152. package/dist/serve.d.ts +1 -1
  153. package/dist/serve.js +1 -1
  154. package/dist/server/index.d.ts +7 -7
  155. package/dist/server/index.js +6 -6
  156. package/dist/server/self-test-input-tokens.d.ts +2 -1
  157. package/dist/server/self-test-input-tokens.js +18 -14
  158. package/dist/server/self-test-redaction.d.ts +1 -1
  159. package/dist/server/self-test-redaction.js +1 -1
  160. package/dist/server/self-test.d.ts +117 -3
  161. package/dist/server/self-test.js +787 -151
  162. package/dist/server/serve-implementation.d.ts +210 -0
  163. package/dist/server/serve-implementation.js +2077 -0
  164. package/dist/server/serve.d.ts +1 -70
  165. package/dist/server/serve.js +1 -1143
  166. package/dist/server/types.d.ts +34 -9
  167. package/dist/server/types.js +8 -1
  168. package/dist/stateful/errors.d.ts +19 -0
  169. package/dist/stateful/errors.js +24 -0
  170. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  171. package/dist/stateful/http-provider-event-emitter.js +237 -0
  172. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  173. package/dist/stateful/http-session-owner-registry.js +210 -0
  174. package/dist/stateful/index.d.ts +18 -0
  175. package/dist/stateful/index.js +18 -0
  176. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  177. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  178. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  179. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  180. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  181. package/dist/stateful/provider-event-pipeline.js +1 -0
  182. package/dist/stateful/provider-events.d.ts +101 -0
  183. package/dist/stateful/provider-events.js +289 -0
  184. package/dist/stateful/session-key.d.ts +15 -0
  185. package/dist/stateful/session-key.js +86 -0
  186. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  187. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  188. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  189. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  190. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  191. package/dist/stateful/stateful-provider-adapter.js +287 -0
  192. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  193. package/dist/stateful/stateful-provider-observability.js +161 -0
  194. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  195. package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
  196. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  197. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  198. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  199. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  200. package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
  201. package/dist/stateful/stateful-provider-session-routing.js +345 -0
  202. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  203. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  204. package/dist/stateful-signing.d.ts +18 -0
  205. package/dist/stateful-signing.js +27 -0
  206. package/dist/stealth/profiles.d.ts +1 -1
  207. package/dist/stealth/profiles.js +21 -21
  208. package/dist/stream-evidence.d.ts +74 -0
  209. package/dist/stream-evidence.js +785 -0
  210. package/dist/stream.d.ts +1 -1
  211. package/dist/stream.js +7 -1
  212. package/dist/testing/index.d.ts +3 -2
  213. package/dist/testing/index.js +3 -2
  214. package/dist/testing/run.d.ts +32 -2
  215. package/dist/testing/run.js +488 -28
  216. package/dist/types.d.ts +566 -19
  217. package/dist/types.js +1 -0
  218. package/dist/user-input.d.ts +30 -0
  219. package/dist/user-input.js +66 -0
  220. package/package.json +42 -7
  221. package/src/auth-turn/index.ts +2 -2
  222. package/src/auth.ts +146 -86
  223. package/src/ceremonies/index.ts +167 -92
  224. package/src/cli/commands.ts +10 -0
  225. package/src/cli/create.ts +42 -35
  226. package/src/cli/prompt-assets.ts +865 -0
  227. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  228. package/src/cli/templates/provider/README.md.tpl +4 -4
  229. package/src/config/loader.ts +667 -289
  230. package/src/contract-serialization.ts +10 -18
  231. package/src/contract-types.ts +3 -2
  232. package/src/contract.ts +14 -28
  233. package/src/declaration-validation.ts +202 -0
  234. package/src/define.ts +631 -495
  235. package/src/dev.ts +4 -9
  236. package/src/error-resolution.ts +127 -0
  237. package/src/errors.ts +56 -11
  238. package/src/fixture-sanitization.ts +247 -0
  239. package/src/i18n/catalog.ts +10 -32
  240. package/src/i18n/index.ts +2 -2
  241. package/src/i18n/keys.ts +5 -11
  242. package/src/index.ts +157 -44
  243. package/src/lint.ts +488 -154
  244. package/src/native-address.ts +340 -0
  245. package/src/native-egress-policy.ts +358 -0
  246. package/src/observability.ts +51 -1
  247. package/src/provider.ts +65 -11
  248. package/src/public-schema-field-lint.ts +7 -33
  249. package/src/recipes/gov-api.ts +2 -5
  250. package/src/runtime/auth-flow.ts +13 -7
  251. package/src/runtime/browser.ts +252 -207
  252. package/src/runtime/cache.ts +209 -81
  253. package/src/runtime/choice-wordlist.ts +145 -0
  254. package/src/runtime/choice.ts +685 -216
  255. package/src/runtime/credential.ts +2 -2
  256. package/src/runtime/env.ts +1 -1
  257. package/src/runtime/executor.ts +37 -19
  258. package/src/runtime/http.ts +645 -65
  259. package/src/runtime/insights.ts +15 -53
  260. package/src/runtime/instrumentation.ts +530 -67
  261. package/src/runtime/keyring.ts +7 -19
  262. package/src/runtime/namespace.ts +2 -7
  263. package/src/runtime/native-network-errors.ts +99 -0
  264. package/src/runtime/native-network.ts +1605 -0
  265. package/src/runtime/ocr.ts +523 -0
  266. package/src/runtime/otlp.ts +12 -23
  267. package/src/runtime/perf.ts +1 -1
  268. package/src/runtime/provider.ts +4 -9
  269. package/src/runtime/proxy-errors.ts +29 -42
  270. package/src/runtime/proxy-nodemaven.ts +221 -0
  271. package/src/runtime/proxy-retry-policy.ts +3 -3
  272. package/src/runtime/proxy-telemetry.ts +84 -77
  273. package/src/runtime/redirects.ts +66 -0
  274. package/src/runtime/redis.ts +10 -13
  275. package/src/runtime/request-options.ts +679 -9
  276. package/src/runtime/resolver-config.ts +6 -0
  277. package/src/runtime/resolver-public.ts +18 -0
  278. package/src/runtime/resolver-shared.ts +17 -0
  279. package/src/runtime/resolver-vendors/bindings.ts +56 -0
  280. package/src/runtime/resolver-vendors/browser.ts +408 -0
  281. package/src/runtime/resolver-vendors/hosts.ts +38 -0
  282. package/src/runtime/resolver-vendors/twocaptcha.ts +500 -0
  283. package/src/runtime/resolver-vendors/types.ts +173 -0
  284. package/src/runtime/resolver.ts +1060 -0
  285. package/src/runtime/secrets.ts +64 -0
  286. package/src/runtime/state.ts +399 -161
  287. package/src/runtime/stealth-cookies.ts +132 -0
  288. package/src/runtime/stealth.ts +681 -295
  289. package/src/runtime/stt.ts +39 -113
  290. package/src/runtime/timeout.ts +18 -0
  291. package/src/runtime/trace.ts +14 -44
  292. package/src/runtime/waterfall.ts +5 -18
  293. package/src/schema.ts +23 -84
  294. package/src/serve.ts +6 -1
  295. package/src/server/index.ts +30 -7
  296. package/src/server/self-test-input-tokens.ts +29 -14
  297. package/src/server/self-test-redaction.ts +2 -2
  298. package/src/server/self-test.ts +1030 -180
  299. package/src/server/serve-implementation.ts +3060 -0
  300. package/src/server/serve.ts +1 -1781
  301. package/src/server/types.ts +12 -13
  302. package/src/stateful/README.md +146 -0
  303. package/src/stateful/errors.ts +35 -0
  304. package/src/stateful/http-provider-event-emitter.ts +314 -0
  305. package/src/stateful/http-session-owner-registry.ts +306 -0
  306. package/src/stateful/index.ts +18 -0
  307. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  308. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  309. package/src/stateful/provider-event-pipeline.ts +61 -0
  310. package/src/stateful/provider-events.ts +462 -0
  311. package/src/stateful/session-key.ts +111 -0
  312. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  313. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  314. package/src/stateful/stateful-provider-adapter.ts +562 -0
  315. package/src/stateful/stateful-provider-observability.ts +261 -0
  316. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  317. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  318. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  319. package/src/stateful/stateful-provider-session-routing.ts +546 -0
  320. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  321. package/src/stateful-signing.ts +46 -0
  322. package/src/stealth/profiles.ts +27 -33
  323. package/src/stream-evidence.ts +988 -0
  324. package/src/stream.ts +16 -20
  325. package/src/testing/index.ts +11 -2
  326. package/src/testing/run.ts +668 -74
  327. package/src/types.ts +665 -35
  328. package/src/user-input.ts +118 -0
  329. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  330. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  331. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  332. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  333. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  334. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  335. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  336. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  337. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  338. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  339. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  340. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  341. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  342. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
@@ -1,5 +1,6 @@
1
- import { providerStateRedisUrlFromEnv } from "../config/loader";
2
- import { ProviderError } from "../errors";
1
+ import { createHash } from "node:crypto";
2
+ import { providerStateRedisUrlFromEnv } from "../config/loader.js";
3
+ import { ProviderError } from "../errors.js";
3
4
  import type {
4
5
  ProviderRuntimeState,
5
6
  ProviderStateNamespace,
@@ -7,20 +8,94 @@ import type {
7
8
  StateNamespaceOptions,
8
9
  StateValue,
9
10
  StateWriteOptions,
10
- } from "../types";
11
+ } from "../types.js";
11
12
  import {
12
13
  createProviderRedisClient,
13
14
  ensureRedisReady,
14
15
  type ProviderRedisClient,
15
16
  withRedisTimeout,
16
- } from "./redis";
17
+ } from "./redis.js";
17
18
 
18
19
  const DEFAULT_REDIS_TIMEOUT_MS = 250;
19
- const REDIS_STATE_PREFIX = "apifuse:provider-state:v1";
20
+ const REDIS_STATE_PREFIX = "apifuse:provider-state:v2";
21
+ const PROVIDER_SCOPE_DISCRIMINATOR = "scope:provider";
22
+ const MISSING_CONNECTION_SCOPE_DISCRIMINATOR = "scope:connection:missing";
23
+ const SET_WITH_QUOTA_SCRIPT = `
24
+ local now = tonumber(ARGV[1])
25
+ local max_entries = tonumber(ARGV[2])
26
+ local expires_at = tonumber(ARGV[3])
27
+ local index_ttl = tonumber(ARGV[4])
28
+ local envelope = ARGV[5]
29
+
30
+ redis.call("ZREMRANGEBYSCORE", KEYS[2], "-inf", now)
31
+ local indexed = redis.call("ZSCORE", KEYS[2], KEYS[1])
32
+ if not indexed and redis.call("ZCARD", KEYS[2]) >= max_entries then
33
+ return {0, false}
34
+ end
35
+
36
+ redis.call("SET", KEYS[1], envelope, "PXAT", expires_at)
37
+ redis.call("ZADD", KEYS[2], expires_at, KEYS[1])
38
+ redis.call("PEXPIRE", KEYS[2], index_ttl)
39
+ return {1, envelope}
40
+ `;
41
+
42
+ const COMPARE_AND_SET_WITH_QUOTA_SCRIPT = `
43
+ local current = redis.call("GET", KEYS[1])
44
+ if current then
45
+ local ok, decoded = pcall(cjson.decode, current)
46
+ if ok and type(decoded) == "table" and decoded.deleted == true then
47
+ current = false
48
+ end
49
+ end
50
+
51
+ local now = tonumber(ARGV[2])
52
+ local max_entries = tonumber(ARGV[3])
53
+ local expires_at = tonumber(ARGV[4])
54
+ local index_ttl = tonumber(ARGV[5])
55
+ local envelope = ARGV[6]
56
+
57
+ local current_version = 0
58
+ local current_decoded = false
59
+ if current then
60
+ local ok, decoded = pcall(cjson.decode, current)
61
+ if not ok or type(decoded) ~= "table" or type(decoded.version) ~= "number" then
62
+ return {-2, current}
63
+ end
64
+ current_version = decoded.version
65
+ current_decoded = decoded
66
+ end
67
+ if current_version ~= tonumber(ARGV[1]) then
68
+ return {-1, current or false}
69
+ end
70
+
71
+ redis.call("ZREMRANGEBYSCORE", KEYS[2], "-inf", now)
72
+ local indexed = redis.call("ZSCORE", KEYS[2], KEYS[1])
73
+ if not indexed and redis.call("ZCARD", KEYS[2]) >= max_entries then
74
+ return {0, false}
75
+ end
76
+
77
+ if current_decoded and type(current_decoded.createdAt) == "string" then
78
+ local next_decoded = cjson.decode(envelope)
79
+ next_decoded.createdAt = current_decoded.createdAt
80
+ envelope = cjson.encode(next_decoded)
81
+ end
82
+ redis.call("SET", KEYS[1], envelope, "PXAT", expires_at)
83
+ redis.call("ZADD", KEYS[2], expires_at, KEYS[1])
84
+ redis.call("PEXPIRE", KEYS[2], index_ttl)
85
+ return {1, envelope}
86
+ `;
87
+
88
+ const DELETE_WITH_INDEX_SCRIPT = `
89
+ redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2])
90
+ redis.call("ZREM", KEYS[2], KEYS[1])
91
+ return 1
92
+ `;
20
93
 
21
94
  type RedisProviderRuntimeStateOptions = {
22
95
  readonly redisUrl: string;
23
96
  readonly providerId?: string;
97
+ /** Test seam; production callers use redisUrl-backed client sharing. */
98
+ readonly __redisClient?: ProviderRedisClient;
24
99
  };
25
100
 
26
101
  type RedisStateEnvelope = {
@@ -31,23 +106,32 @@ type RedisStateEnvelope = {
31
106
  readonly updatedAt: string;
32
107
  };
33
108
 
109
+ type RedisStateTombstone = {
110
+ readonly deleted: true;
111
+ };
112
+
34
113
  type RedisBackend = {
35
114
  readonly redis: ProviderRedisClient;
36
115
  };
37
116
 
38
117
  const redisBackends = new Map<string, RedisBackend>();
39
118
 
40
- function getRedisBackend(redisUrl: string): RedisBackend {
119
+ function getRedisBackend(
120
+ redisUrl: string,
121
+ injectedRedis?: ProviderRedisClient,
122
+ ): RedisBackend {
41
123
  const existing = redisBackends.get(redisUrl);
42
124
  if (existing) return existing;
43
- const redis = createProviderRedisClient({
44
- redisUrl,
45
- timeoutMs: DEFAULT_REDIS_TIMEOUT_MS,
46
- onError: () => {
47
- // Runtime state operations fail closed at their call sites. Avoid noisy
48
- // unhandled Redis errors from background reconnect attempts.
49
- },
50
- });
125
+ const redis =
126
+ injectedRedis ??
127
+ createProviderRedisClient({
128
+ redisUrl,
129
+ timeoutMs: DEFAULT_REDIS_TIMEOUT_MS,
130
+ onError: () => {
131
+ // Runtime state operations fail closed at their call sites. Avoid noisy
132
+ // unhandled Redis errors from background reconnect attempts.
133
+ },
134
+ });
51
135
  const backend = { redis };
52
136
  redisBackends.set(redisUrl, backend);
53
137
  return backend;
@@ -57,47 +141,50 @@ async function withRequiredRedis<T>(operation: () => Promise<T>): Promise<T> {
57
141
  return await withRedisTimeout(operation, {
58
142
  timeoutMs: DEFAULT_REDIS_TIMEOUT_MS,
59
143
  onTimeout: () => {
60
- throw new UnsupportedProviderStateError(
61
- "Provider runtime state Redis timed out",
62
- );
144
+ throw new UnsupportedProviderStateError("Provider runtime state Redis timed out");
63
145
  },
64
146
  onError: () => {
65
- throw new UnsupportedProviderStateError(
66
- "Provider runtime state Redis is unavailable",
67
- );
147
+ throw new UnsupportedProviderStateError("Provider runtime state Redis is unavailable");
68
148
  },
69
149
  });
70
150
  }
71
151
 
72
152
  async function requireRedisReady(redis: ProviderRedisClient): Promise<void> {
73
153
  if (await ensureRedisReady(redis, DEFAULT_REDIS_TIMEOUT_MS)) return;
74
- throw new UnsupportedProviderStateError(
75
- "Provider runtime state Redis is unavailable",
76
- );
154
+ throw new UnsupportedProviderStateError("Provider runtime state Redis is unavailable");
155
+ }
156
+
157
+ function connectionScopeDiscriminator(connectionId: string | undefined): string {
158
+ if (connectionId === undefined) return MISSING_CONNECTION_SCOPE_DISCRIMINATOR;
159
+ const digest = createHash("sha256").update(connectionId, "utf8").digest("hex");
160
+ return `scope:connection:sha256:${digest}`;
77
161
  }
78
162
 
79
163
  function providerStatePrefix(
80
164
  providerId: string | undefined,
81
165
  namespace: string,
166
+ scopeDiscriminator: string,
82
167
  ): string {
83
- return `${REDIS_STATE_PREFIX}:${providerId ?? "default"}:${namespace}`;
168
+ return `${REDIS_STATE_PREFIX}:${providerId ?? "default"}:${namespace}:${scopeDiscriminator}`;
84
169
  }
85
170
 
86
171
  function providerStateKey(
87
172
  providerId: string | undefined,
88
173
  namespace: string,
174
+ scopeDiscriminator: string,
89
175
  key: string,
90
176
  ): string {
91
- return `${providerStatePrefix(providerId, namespace)}:${key}`;
177
+ return `${providerStatePrefix(providerId, namespace, scopeDiscriminator)}:${key}`;
92
178
  }
93
179
 
94
- function publicStateKey(
95
- providerId: string | undefined,
96
- namespace: string,
97
- redisKey: string,
98
- ): string {
99
- const prefix = `${providerStatePrefix(providerId, namespace)}:`;
100
- return redisKey.startsWith(prefix) ? redisKey.slice(prefix.length) : redisKey;
180
+ function publicStateKey(redisKey: string, prefixes: readonly string[]): string {
181
+ for (const prefix of prefixes) {
182
+ const prefixWithSeparator = `${prefix}:`;
183
+ if (redisKey.startsWith(prefixWithSeparator)) {
184
+ return redisKey.slice(prefixWithSeparator.length);
185
+ }
186
+ }
187
+ return redisKey;
101
188
  }
102
189
 
103
190
  function parseStateDurationMs(ttl: StateWriteOptions["ttl"]): number {
@@ -118,23 +205,27 @@ function parseStateDurationMs(ttl: StateWriteOptions["ttl"]): number {
118
205
  return Math.max(1, amount * multiplier);
119
206
  }
120
207
 
121
- function resolveExpiresAt(ttl: StateWriteOptions["ttl"]): string {
122
- return new Date(Date.now() + parseStateDurationMs(ttl)).toISOString();
123
- }
124
-
125
208
  function envelopeFromJson(
126
209
  key: string,
127
210
  raw: string | null,
128
211
  // biome-ignore lint/suspicious/noExplicitAny: state envelopes deserialize caller-owned generic values.
129
212
  ): StateValue<any> | null {
130
213
  if (!raw) return null;
131
- const parsed: unknown = JSON.parse(raw);
214
+ // A corrupt/undecodable persisted envelope must be treated as absent rather
215
+ // than throwing a raw JSON.parse SyntaxError: an uncaught SyntaxError escapes
216
+ // the provider error taxonomy, is masked as internal_error 500, and is then
217
+ // retried by the hub (2026-07-22 catchtable reserve RCA, candidate A). Returning
218
+ // null also keeps list() from aborting the whole scan on a single bad entry.
219
+ let parsed: unknown;
220
+ try {
221
+ parsed = JSON.parse(raw);
222
+ } catch {
223
+ return null;
224
+ }
132
225
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
133
226
  return null;
134
227
  }
135
- const record: Record<string, unknown> = Object.fromEntries(
136
- Object.entries(parsed),
137
- );
228
+ const record: Record<string, unknown> = Object.fromEntries(Object.entries(parsed));
138
229
  if (
139
230
  typeof record.version !== "number" ||
140
231
  typeof record.expiresAt !== "string" ||
@@ -154,6 +245,21 @@ function envelopeFromJson(
154
245
  };
155
246
  }
156
247
 
248
+ function isStateTombstone(raw: string | null): boolean {
249
+ if (!raw) return false;
250
+ try {
251
+ const parsed: unknown = JSON.parse(raw);
252
+ return (
253
+ parsed !== null &&
254
+ typeof parsed === "object" &&
255
+ !Array.isArray(parsed) &&
256
+ (parsed as { deleted?: unknown }).deleted === true
257
+ );
258
+ } catch {
259
+ return false;
260
+ }
261
+ }
262
+
157
263
  function redisEnvelope(
158
264
  value: unknown,
159
265
  version: number,
@@ -170,21 +276,60 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
170
276
  private readonly providerId: string | undefined,
171
277
  private readonly namespaceName: string,
172
278
  private readonly options: StateNamespaceOptions,
279
+ private readonly scopeDiscriminator: string,
173
280
  ) {}
174
281
 
175
282
  private redisKey(key: string): string {
176
- return providerStateKey(this.providerId, this.namespaceName, key);
283
+ return providerStateKey(
284
+ this.providerId,
285
+ this.namespaceName,
286
+ this.scopeDiscriminator,
287
+ key,
288
+ );
177
289
  }
178
290
 
179
- private prefix(): string {
180
- return `${providerStatePrefix(this.providerId, this.namespaceName)}:`;
291
+ private statePrefix(): string {
292
+ return providerStatePrefix(
293
+ this.providerId,
294
+ this.namespaceName,
295
+ this.scopeDiscriminator,
296
+ );
181
297
  }
182
298
 
183
- private async activeKeys(): Promise<string[]> {
299
+ private indexKey(): string {
300
+ // Keep bookkeeping outside the caller-owned keyspace. A provider may use
301
+ // any state key (including "__index"), so a suffix inside the namespace
302
+ // could turn the ZSET into a string and break every subsequent write.
303
+ const namespaceIdentity = Buffer.from(
304
+ providerStatePrefix(
305
+ this.providerId,
306
+ this.namespaceName,
307
+ this.scopeDiscriminator,
308
+ ),
309
+ "utf8",
310
+ ).toString("base64url");
311
+ return `${REDIS_STATE_PREFIX}:index:${namespaceIdentity}`;
312
+ }
313
+
314
+ private async indexedKeys(limit: number): Promise<string[]> {
184
315
  await requireRedisReady(this.backend.redis);
185
- return await withRequiredRedis(() =>
186
- this.backend.redis.keys(`${this.prefix()}*`),
187
- );
316
+ const now = Date.now();
317
+ return await withRequiredRedis(async () => {
318
+ await this.backend.redis.zremrangebyscore(this.indexKey(), "-inf", now);
319
+ return await this.backend.redis.zrangebyscore(
320
+ this.indexKey(),
321
+ now + 1,
322
+ "+inf",
323
+ "LIMIT",
324
+ 0,
325
+ limit,
326
+ );
327
+ });
328
+ }
329
+
330
+ private async readRaw(key: string): Promise<string | null> {
331
+ await requireRedisReady(this.backend.redis);
332
+ return await withRequiredRedis(() => this.backend.redis.get(this.redisKey(key)));
188
333
  }
189
334
 
190
335
  private enforceValueSize(value: unknown): void {
@@ -196,80 +341,86 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
196
341
  }
197
342
  }
198
343
 
199
- private async enforceMaxEntries(key: string): Promise<void> {
200
- const keys = await this.activeKeys();
201
- const redisKey = this.redisKey(key);
202
- const otherKeys = keys.filter((candidate) => candidate !== redisKey);
203
- if (otherKeys.length >= this.options.maxEntries) {
344
+ private quotaExceeded(): UnsupportedProviderStateError {
345
+ return new UnsupportedProviderStateError(
346
+ `Provider runtime state namespace quota exceeded (${this.options.maxEntries + 1} > ${this.options.maxEntries})`,
347
+ );
348
+ }
349
+
350
+ private writeTiming(ttl: StateWriteOptions["ttl"]): {
351
+ expiresAt: string;
352
+ expiresAtMs: number;
353
+ indexTtlMs: number;
354
+ } {
355
+ const ttlMs = parseStateDurationMs(ttl ?? this.options.defaultTtl);
356
+ const maxTtlMs = parseStateDurationMs(this.options.maxTtl);
357
+ if (ttlMs > maxTtlMs) {
204
358
  throw new UnsupportedProviderStateError(
205
- `Provider runtime state namespace quota exceeded (${otherKeys.length + 1} > ${this.options.maxEntries})`,
359
+ `Provider runtime state ttl exceeds maxTtl (${ttlMs} > ${maxTtlMs})`,
206
360
  );
207
361
  }
362
+ const expiresAtMs = Date.now() + ttlMs;
363
+ return {
364
+ expiresAt: new Date(expiresAtMs).toISOString(),
365
+ expiresAtMs,
366
+ indexTtlMs: maxTtlMs,
367
+ };
208
368
  }
209
369
 
210
- async list<T>(options?: {
211
- limit?: number;
212
- prefix?: string;
213
- }): Promise<StateValue<T>[]> {
214
- const keys = (await this.activeKeys()).filter((key) => {
215
- const publicKey = publicStateKey(
216
- this.providerId,
217
- this.namespaceName,
218
- key,
219
- );
220
- return options?.prefix ? publicKey.startsWith(options.prefix) : true;
221
- });
222
- const limited = keys.slice(0, Math.max(0, options?.limit ?? keys.length));
223
- if (limited.length === 0) return [];
224
- const values = await withRequiredRedis(() =>
225
- this.backend.redis.mget(limited),
226
- );
227
- return values.flatMap((raw, index) => {
228
- const key = limited[index];
229
- if (!key) return [];
230
- const value = envelopeFromJson(
231
- publicStateKey(this.providerId, this.namespaceName, key),
232
- raw,
233
- );
234
- return value ? [value] : [];
235
- });
370
+ async list<T>(options?: { limit?: number; prefix?: string }): Promise<StateValue<T>[]> {
371
+ const requestedLimit = Math.max(0, options?.limit ?? this.options.maxEntries);
372
+ if (requestedLimit === 0) return [];
373
+ const keys = await this.indexedKeys(this.options.maxEntries);
374
+ if (keys.length === 0) return [];
375
+ const values = await withRequiredRedis(() => this.backend.redis.mget(keys));
376
+ const rows: StateValue<T>[] = [];
377
+ for (const [index, raw] of values.entries()) {
378
+ const redisKey = keys[index];
379
+ if (!redisKey) continue;
380
+ const publicKey = publicStateKey(redisKey, [this.statePrefix()]);
381
+ if (options?.prefix && !publicKey.startsWith(options.prefix)) continue;
382
+ const value = envelopeFromJson(publicKey, raw);
383
+ if (!value) continue;
384
+ rows.push(value);
385
+ }
386
+ return rows.slice(0, requestedLimit);
236
387
  }
237
388
 
238
389
  async get<T>(key: string): Promise<StateValue<T> | null> {
239
- await requireRedisReady(this.backend.redis);
240
- const raw = await withRequiredRedis(() =>
241
- this.backend.redis.get(this.redisKey(key)),
242
- );
390
+ const raw = await this.readRaw(key);
391
+ if (isStateTombstone(raw)) return null;
243
392
  return envelopeFromJson(key, raw);
244
393
  }
245
394
 
246
- async set<T>(
247
- key: string,
248
- value: T,
249
- options?: StateWriteOptions,
250
- ): Promise<StateValue<T>> {
395
+ async set<T>(key: string, value: T, options?: StateWriteOptions): Promise<StateValue<T>> {
251
396
  this.enforceValueSize(value);
252
- await this.enforceMaxEntries(key);
253
397
  const current = await this.get<T>(key);
254
398
  const createdAt = current?.createdAt ?? new Date().toISOString();
255
399
  const version = (current?.version ?? 0) + 1;
256
- const ttl = options?.ttl ?? this.options.defaultTtl;
257
- const ttlMs = parseStateDurationMs(ttl);
258
- const expiresAt = resolveExpiresAt(ttl);
259
- const envelope = redisEnvelope(value, version, createdAt, expiresAt);
260
- await withRequiredRedis(() =>
261
- this.backend.redis.set(
400
+ const timing = this.writeTiming(options?.ttl);
401
+ const envelope = redisEnvelope(value, version, createdAt, timing.expiresAt);
402
+ await requireRedisReady(this.backend.redis);
403
+ const result = await withRequiredRedis(() =>
404
+ this.backend.redis.eval(
405
+ SET_WITH_QUOTA_SCRIPT,
406
+ 2,
262
407
  this.redisKey(key),
408
+ this.indexKey(),
409
+ String(Date.now()),
410
+ String(this.options.maxEntries),
411
+ String(timing.expiresAtMs),
412
+ String(timing.indexTtlMs),
263
413
  JSON.stringify(envelope),
264
- "PX",
265
- ttlMs,
266
414
  ),
267
415
  );
416
+ if (!Array.isArray(result) || Number(result[0]) !== 1) {
417
+ throw this.quotaExceeded();
418
+ }
268
419
  return {
269
420
  key,
270
421
  value,
271
422
  version,
272
- expiresAt,
423
+ expiresAt: timing.expiresAt,
273
424
  createdAt,
274
425
  updatedAt: envelope.updatedAt,
275
426
  };
@@ -293,16 +444,59 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
293
444
  options?: StateWriteOptions,
294
445
  ): Promise<StateCasResult<T>> {
295
446
  this.enforceValueSize(value);
296
- const current = await this.get<T>(key);
297
- if ((current?.version ?? 0) !== expectedVersion) {
298
- return { ok: false, current };
447
+ const createdAt = new Date().toISOString();
448
+ const timing = this.writeTiming(options?.ttl);
449
+ const envelope = redisEnvelope(
450
+ value,
451
+ expectedVersion + 1,
452
+ createdAt,
453
+ timing.expiresAt,
454
+ );
455
+ await requireRedisReady(this.backend.redis);
456
+ const result = await withRequiredRedis(() =>
457
+ this.backend.redis.eval(
458
+ COMPARE_AND_SET_WITH_QUOTA_SCRIPT,
459
+ 2,
460
+ this.redisKey(key),
461
+ this.indexKey(),
462
+ String(expectedVersion),
463
+ String(Date.now()),
464
+ String(this.options.maxEntries),
465
+ String(timing.expiresAtMs),
466
+ String(timing.indexTtlMs),
467
+ JSON.stringify(envelope),
468
+ ),
469
+ );
470
+ if (Array.isArray(result) && Number(result[0]) === 0) {
471
+ throw this.quotaExceeded();
299
472
  }
300
- return { ok: true, value: await this.set(key, value, options) };
473
+ if (!Array.isArray(result) || Number(result[0]) !== 1) {
474
+ const rawCurrent = Array.isArray(result) && typeof result[1] === "string" ? result[1] : null;
475
+ return { ok: false, current: envelopeFromJson(key, rawCurrent) };
476
+ }
477
+ const rawWritten = typeof result[1] === "string" ? result[1] : null;
478
+ const written = envelopeFromJson(key, rawWritten) as StateValue<T> | null;
479
+ if (!written) {
480
+ throw new UnsupportedProviderStateError(
481
+ "Provider runtime state CAS returned an invalid value",
482
+ );
483
+ }
484
+ return { ok: true, value: written };
301
485
  }
302
486
 
303
487
  async delete(key: string): Promise<void> {
304
488
  await requireRedisReady(this.backend.redis);
305
- await withRequiredRedis(() => this.backend.redis.del(this.redisKey(key)));
489
+ const tombstone = JSON.stringify({ deleted: true } satisfies RedisStateTombstone);
490
+ await withRequiredRedis(() =>
491
+ this.backend.redis.eval(
492
+ DELETE_WITH_INDEX_SCRIPT,
493
+ 2,
494
+ this.redisKey(key),
495
+ this.indexKey(),
496
+ tombstone,
497
+ String(parseStateDurationMs(this.options.maxTtl)),
498
+ ),
499
+ );
306
500
  }
307
501
 
308
502
  async increment(
@@ -313,60 +507,59 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
313
507
  ): Promise<StateValue<Record<string, unknown>>> {
314
508
  const current = (await this.get<Record<string, unknown>>(key))?.value ?? {};
315
509
  const previous = typeof current[field] === "number" ? current[field] : 0;
316
- return await this.set(
317
- key,
318
- { ...current, [field]: previous + delta },
319
- options,
320
- );
510
+ return await this.set(key, { ...current, [field]: previous + delta }, options);
321
511
  }
322
512
  }
323
513
 
324
514
  class RedisProviderRuntimeState implements ProviderRuntimeState {
325
515
  readonly backend: RedisBackend;
326
516
  readonly providerId?: string;
517
+ readonly redisUrl: string;
518
+ readonly scopeDiscriminator: string;
327
519
 
328
- constructor(options: RedisProviderRuntimeStateOptions) {
329
- this.backend = getRedisBackend(options.redisUrl);
520
+ constructor(
521
+ options: RedisProviderRuntimeStateOptions,
522
+ scopeDiscriminator = MISSING_CONNECTION_SCOPE_DISCRIMINATOR,
523
+ ) {
524
+ this.backend = getRedisBackend(options.redisUrl, options.__redisClient);
330
525
  this.providerId = options.providerId;
526
+ this.redisUrl = options.redisUrl;
527
+ this.scopeDiscriminator = scopeDiscriminator;
331
528
  }
332
529
 
333
- namespace(
334
- name: string,
335
- options: StateNamespaceOptions,
336
- ): ProviderStateNamespace {
530
+ forConnection(connectionId: string | undefined): ProviderRuntimeState {
531
+ return new RedisProviderRuntimeState(
532
+ { redisUrl: this.redisUrl, providerId: this.providerId },
533
+ connectionScopeDiscriminator(connectionId),
534
+ );
535
+ }
536
+
537
+ namespace(name: string, options: StateNamespaceOptions): ProviderStateNamespace {
337
538
  return new RedisProviderStateNamespace(
338
539
  this.backend,
339
540
  this.providerId,
340
541
  name,
341
542
  options,
543
+ options.scope === "provider" ? PROVIDER_SCOPE_DISCRIMINATOR : this.scopeDiscriminator,
342
544
  );
343
545
  }
344
546
  }
345
547
 
346
548
  export class UnsupportedProviderStateError extends ProviderError {
347
- constructor(
348
- message = "Provider runtime state is not available in this runtime",
349
- ) {
549
+ constructor(message = "Provider runtime state is not available in this runtime") {
350
550
  super(message, { code: "PROVIDER_STATE_UNSUPPORTED" });
351
551
  this.name = "UnsupportedProviderStateError";
352
552
  }
353
553
  }
354
554
 
355
555
  class UnsupportedProviderStateNamespace implements ProviderStateNamespace {
356
- async list<T>(_options?: {
357
- limit?: number;
358
- prefix?: string;
359
- }): Promise<StateValue<T>[]> {
556
+ async list<T>(_options?: { limit?: number; prefix?: string }): Promise<StateValue<T>[]> {
360
557
  throw new UnsupportedProviderStateError();
361
558
  }
362
559
  async get<T>(_key: string): Promise<StateValue<T> | null> {
363
560
  throw new UnsupportedProviderStateError();
364
561
  }
365
- async set<T>(
366
- _key: string,
367
- _value: T,
368
- _options?: StateWriteOptions,
369
- ): Promise<StateValue<T>> {
562
+ async set<T>(_key: string, _value: T, _options?: StateWriteOptions): Promise<StateValue<T>> {
370
563
  throw new UnsupportedProviderStateError();
371
564
  }
372
565
  async patch<T extends Record<string, unknown>>(
@@ -398,10 +591,11 @@ class UnsupportedProviderStateNamespace implements ProviderStateNamespace {
398
591
  }
399
592
 
400
593
  class UnsupportedProviderRuntimeState implements ProviderRuntimeState {
401
- namespace(
402
- _name: string,
403
- _options: StateNamespaceOptions,
404
- ): ProviderStateNamespace {
594
+ forConnection(_connectionId: string | undefined): ProviderRuntimeState {
595
+ return new UnsupportedProviderRuntimeState();
596
+ }
597
+
598
+ namespace(_name: string, _options: StateNamespaceOptions): ProviderStateNamespace {
405
599
  return new UnsupportedProviderStateNamespace();
406
600
  }
407
601
  }
@@ -412,6 +606,31 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
412
606
 
413
607
  constructor(private readonly options: StateNamespaceOptions) {}
414
608
 
609
+ private enforceValueSize(value: unknown): void {
610
+ const bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
611
+ if (bytes > this.options.maxValueBytes) {
612
+ throw new UnsupportedProviderStateError(
613
+ `Provider runtime state value exceeds maxValueBytes (${bytes} > ${this.options.maxValueBytes})`,
614
+ );
615
+ }
616
+ }
617
+
618
+ private enforceWritePolicy(key: string, value: unknown, ttl: StateWriteOptions["ttl"]): void {
619
+ this.enforceValueSize(value);
620
+ const ttlMs = parseStateDurationMs(ttl ?? this.options.defaultTtl);
621
+ const maxTtlMs = parseStateDurationMs(this.options.maxTtl);
622
+ if (ttlMs > maxTtlMs) {
623
+ throw new UnsupportedProviderStateError(
624
+ `Provider runtime state ttl exceeds maxTtl (${ttlMs} > ${maxTtlMs})`,
625
+ );
626
+ }
627
+ if (!this.values.has(key) && this.values.size >= this.options.maxEntries) {
628
+ throw new UnsupportedProviderStateError(
629
+ `Provider runtime state namespace quota exceeded (${this.options.maxEntries + 1} > ${this.options.maxEntries})`,
630
+ );
631
+ }
632
+ }
633
+
415
634
  private pruneExpired(nowMs = Date.now()): void {
416
635
  for (const [key, row] of this.values.entries()) {
417
636
  if (row.expiresAt && Date.parse(row.expiresAt) <= nowMs) {
@@ -420,10 +639,7 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
420
639
  }
421
640
  }
422
641
 
423
- async list<T>(_options?: {
424
- limit?: number;
425
- prefix?: string;
426
- }): Promise<StateValue<T>[]> {
642
+ async list<T>(_options?: { limit?: number; prefix?: string }): Promise<StateValue<T>[]> {
427
643
  this.pruneExpired();
428
644
  const rows = Array.from(this.values.values()).filter((value) =>
429
645
  _options?.prefix ? value.key.startsWith(_options.prefix) : true,
@@ -436,17 +652,12 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
436
652
  return this.values.get(key) ?? null;
437
653
  }
438
654
 
439
- async set<T>(
440
- key: string,
441
- value: T,
442
- options?: StateWriteOptions,
443
- ): Promise<StateValue<T>> {
655
+ async set<T>(key: string, value: T, options?: StateWriteOptions): Promise<StateValue<T>> {
444
656
  this.pruneExpired();
657
+ this.enforceWritePolicy(key, value, options?.ttl);
445
658
  const now = new Date().toISOString();
446
659
  const current = this.values.get(key);
447
- const expiresAt = resolveMemoryStateExpiresAt(
448
- options?.ttl ?? this.options.defaultTtl,
449
- );
660
+ const expiresAt = resolveMemoryStateExpiresAt(options?.ttl ?? this.options.defaultTtl);
450
661
  const row = {
451
662
  key,
452
663
  value,
@@ -470,14 +681,30 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
470
681
  }
471
682
 
472
683
  async compareAndSet<T>(
473
- _key: string,
474
- _expectedVersion: number,
475
- _value: T,
476
- _options?: StateWriteOptions,
684
+ key: string,
685
+ expectedVersion: number,
686
+ value: T,
687
+ options?: StateWriteOptions,
477
688
  ): Promise<StateCasResult<T>> {
478
- throw new UnsupportedProviderStateError(
479
- "In-memory provider runtime state does not support compareAndSet",
480
- );
689
+ this.pruneExpired();
690
+ const current = this.values.get(key) as StateValue<T> | undefined;
691
+ if ((current?.version ?? 0) !== expectedVersion) {
692
+ return { ok: false, current: current ?? null };
693
+ }
694
+ this.enforceWritePolicy(key, value, options?.ttl);
695
+ const now = new Date().toISOString();
696
+ const stored = {
697
+ key,
698
+ value,
699
+ version: expectedVersion + 1,
700
+ expiresAt: resolveMemoryStateExpiresAt(
701
+ options?.ttl ?? this.options.defaultTtl,
702
+ ),
703
+ createdAt: current?.createdAt ?? now,
704
+ updatedAt: now,
705
+ } satisfies StateValue<T>;
706
+ this.values.set(key, stored);
707
+ return { ok: true, value: stored };
481
708
  }
482
709
 
483
710
  async delete(key: string): Promise<void> {
@@ -496,17 +723,31 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
496
723
  }
497
724
  }
498
725
 
726
+ type MemoryProviderStateBackend = {
727
+ readonly namespaces: Map<string, MemoryProviderStateNamespace>;
728
+ };
729
+
499
730
  class MemoryProviderRuntimeState implements ProviderRuntimeState {
500
- readonly namespaces = new Map<string, MemoryProviderStateNamespace>();
731
+ constructor(
732
+ private readonly backend: MemoryProviderStateBackend = { namespaces: new Map() },
733
+ private readonly scopeDiscriminator = MISSING_CONNECTION_SCOPE_DISCRIMINATOR,
734
+ ) {}
735
+
736
+ forConnection(connectionId: string | undefined): ProviderRuntimeState {
737
+ return new MemoryProviderRuntimeState(
738
+ this.backend,
739
+ connectionScopeDiscriminator(connectionId),
740
+ );
741
+ }
501
742
 
502
- namespace(
503
- name: string,
504
- _options: StateNamespaceOptions,
505
- ): ProviderStateNamespace {
506
- const existing = this.namespaces.get(name);
743
+ namespace(name: string, options: StateNamespaceOptions): ProviderStateNamespace {
744
+ const scopeDiscriminator =
745
+ options.scope === "provider" ? PROVIDER_SCOPE_DISCRIMINATOR : this.scopeDiscriminator;
746
+ const namespaceIdentity = `${scopeDiscriminator}\0${name}`;
747
+ const existing = this.backend.namespaces.get(namespaceIdentity);
507
748
  if (existing) return existing;
508
- const created = new MemoryProviderStateNamespace(_options);
509
- this.namespaces.set(name, created);
749
+ const created = new MemoryProviderStateNamespace(options);
750
+ this.backend.namespaces.set(namespaceIdentity, created);
510
751
  return created;
511
752
  }
512
753
  }
@@ -536,10 +777,7 @@ export function createRedisProviderRuntimeState(
536
777
  }
537
778
 
538
779
  export function createProviderRuntimeStateFromEnv(
539
- options: {
540
- readonly providerId?: string;
541
- readonly allowMemoryFallback?: boolean;
542
- } = {},
780
+ options: { readonly providerId?: string; readonly allowMemoryFallback?: boolean } = {},
543
781
  ): ProviderRuntimeState {
544
782
  const redisUrl = providerStateRedisUrlFromEnv();
545
783
  if (redisUrl) {