@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
@@ -1,5 +1,5 @@
1
- import { providerStateRedisUrlFromEnv } from "../config/loader";
2
- import { ProviderError } from "../errors";
1
+ import { providerStateRedisUrlFromEnv } from "../config/loader.js";
2
+ import { ProviderError } from "../errors.js";
3
3
  import type {
4
4
  ProviderRuntimeState,
5
5
  ProviderStateNamespace,
@@ -7,16 +7,103 @@ import type {
7
7
  StateNamespaceOptions,
8
8
  StateValue,
9
9
  StateWriteOptions,
10
- } from "../types";
10
+ } from "../types.js";
11
11
  import {
12
12
  createProviderRedisClient,
13
13
  ensureRedisReady,
14
14
  type ProviderRedisClient,
15
15
  withRedisTimeout,
16
- } from "./redis";
16
+ } from "./redis.js";
17
17
 
18
18
  const DEFAULT_REDIS_TIMEOUT_MS = 250;
19
19
  const REDIS_STATE_PREFIX = "apifuse:provider-state:v1";
20
+ const LEGACY_INDEX_SCAN_COUNT = 256;
21
+ const LEGACY_INDEX_SCAN_MAX_PAGES = 8;
22
+ const SET_WITH_QUOTA_SCRIPT = `
23
+ local now = tonumber(ARGV[1])
24
+ local max_entries = tonumber(ARGV[2])
25
+ local expires_at = tonumber(ARGV[3])
26
+ local index_ttl = tonumber(ARGV[4])
27
+ local envelope = ARGV[5]
28
+
29
+ redis.call("ZREMRANGEBYSCORE", KEYS[2], "-inf", now)
30
+ local exists = redis.call("EXISTS", KEYS[1])
31
+ local indexed = redis.call("ZSCORE", KEYS[2], KEYS[1])
32
+ if exists == 0 and 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
+ local current_version = 0
45
+ if current then
46
+ local ok, decoded = pcall(cjson.decode, current)
47
+ if not ok or type(decoded) ~= "table" or type(decoded.version) ~= "number" then
48
+ return {-2, current}
49
+ end
50
+ current_version = decoded.version
51
+ end
52
+ if current_version ~= tonumber(ARGV[1]) then
53
+ return {-1, current or false}
54
+ end
55
+
56
+ local now = tonumber(ARGV[2])
57
+ local max_entries = tonumber(ARGV[3])
58
+ local expires_at = tonumber(ARGV[4])
59
+ local index_ttl = tonumber(ARGV[5])
60
+ local envelope = ARGV[6]
61
+ redis.call("ZREMRANGEBYSCORE", KEYS[2], "-inf", now)
62
+ local exists = current and 1 or 0
63
+ local indexed = redis.call("ZSCORE", KEYS[2], KEYS[1])
64
+ if exists == 0 and not indexed and redis.call("ZCARD", KEYS[2]) >= max_entries then
65
+ return {0, false}
66
+ end
67
+
68
+ redis.call("SET", KEYS[1], envelope, "PXAT", expires_at)
69
+ redis.call("ZADD", KEYS[2], expires_at, KEYS[1])
70
+ redis.call("PEXPIRE", KEYS[2], index_ttl)
71
+ return {1, envelope}
72
+ `;
73
+
74
+ const DELETE_WITH_INDEX_SCRIPT = `
75
+ redis.call("DEL", KEYS[1])
76
+ redis.call("ZREM", KEYS[2], KEYS[1])
77
+ return 1
78
+ `;
79
+
80
+ // Older SDKs wrote only the value key. Every operation that depends on the
81
+ // namespace index advances a bounded SCAN cursor and lazily imports active
82
+ // legacy envelopes into the new ZSET. The cursor is
83
+ // deliberately cyclic rather than permanently "complete": an old pod may
84
+ // still write an unindexed key during a rolling deploy. Each list/write call
85
+ // does a fixed amount of migration work; Redis KEYS and unbounded scans remain
86
+ // forbidden.
87
+ const BACKFILL_LEGACY_INDEX_SCRIPT = `
88
+ local now = tonumber(ARGV[1])
89
+ local index_ttl = tonumber(ARGV[2])
90
+ local next_cursor = ARGV[3]
91
+
92
+ redis.call("ZREMRANGEBYSCORE", KEYS[1], "-inf", now)
93
+ for i = 4, #ARGV, 3 do
94
+ local key = ARGV[i]
95
+ local expected = ARGV[i + 1]
96
+ local expires_at = tonumber(ARGV[i + 2])
97
+ if redis.call("GET", key) == expected then
98
+ redis.call("ZADD", KEYS[1], "NX", expires_at, key)
99
+ end
100
+ end
101
+ if redis.call("EXISTS", KEYS[1]) == 1 then
102
+ redis.call("PEXPIRE", KEYS[1], index_ttl)
103
+ end
104
+ redis.call("SET", KEYS[2], next_cursor, "PX", index_ttl)
105
+ return redis.call("ZCARD", KEYS[1])
106
+ `;
20
107
 
21
108
  type RedisProviderRuntimeStateOptions = {
22
109
  readonly redisUrl: string;
@@ -57,37 +144,24 @@ async function withRequiredRedis<T>(operation: () => Promise<T>): Promise<T> {
57
144
  return await withRedisTimeout(operation, {
58
145
  timeoutMs: DEFAULT_REDIS_TIMEOUT_MS,
59
146
  onTimeout: () => {
60
- throw new UnsupportedProviderStateError(
61
- "Provider runtime state Redis timed out",
62
- );
147
+ throw new UnsupportedProviderStateError("Provider runtime state Redis timed out");
63
148
  },
64
149
  onError: () => {
65
- throw new UnsupportedProviderStateError(
66
- "Provider runtime state Redis is unavailable",
67
- );
150
+ throw new UnsupportedProviderStateError("Provider runtime state Redis is unavailable");
68
151
  },
69
152
  });
70
153
  }
71
154
 
72
155
  async function requireRedisReady(redis: ProviderRedisClient): Promise<void> {
73
156
  if (await ensureRedisReady(redis, DEFAULT_REDIS_TIMEOUT_MS)) return;
74
- throw new UnsupportedProviderStateError(
75
- "Provider runtime state Redis is unavailable",
76
- );
157
+ throw new UnsupportedProviderStateError("Provider runtime state Redis is unavailable");
77
158
  }
78
159
 
79
- function providerStatePrefix(
80
- providerId: string | undefined,
81
- namespace: string,
82
- ): string {
160
+ function providerStatePrefix(providerId: string | undefined, namespace: string): string {
83
161
  return `${REDIS_STATE_PREFIX}:${providerId ?? "default"}:${namespace}`;
84
162
  }
85
163
 
86
- function providerStateKey(
87
- providerId: string | undefined,
88
- namespace: string,
89
- key: string,
90
- ): string {
164
+ function providerStateKey(providerId: string | undefined, namespace: string, key: string): string {
91
165
  return `${providerStatePrefix(providerId, namespace)}:${key}`;
92
166
  }
93
167
 
@@ -100,6 +174,10 @@ function publicStateKey(
100
174
  return redisKey.startsWith(prefix) ? redisKey.slice(prefix.length) : redisKey;
101
175
  }
102
176
 
177
+ function redisGlobLiteral(value: string): string {
178
+ return value.replace(/[\\*?\[\]]/g, "\\$&");
179
+ }
180
+
103
181
  function parseStateDurationMs(ttl: StateWriteOptions["ttl"]): number {
104
182
  const match = /^(\d+)(ms|s|m|h|d)$/.exec(ttl ?? "1h");
105
183
  if (!match) return 3_600_000;
@@ -128,13 +206,21 @@ function envelopeFromJson(
128
206
  // biome-ignore lint/suspicious/noExplicitAny: state envelopes deserialize caller-owned generic values.
129
207
  ): StateValue<any> | null {
130
208
  if (!raw) return null;
131
- const parsed: unknown = JSON.parse(raw);
209
+ // A corrupt/undecodable persisted envelope must be treated as absent rather
210
+ // than throwing a raw JSON.parse SyntaxError: an uncaught SyntaxError escapes
211
+ // the provider error taxonomy, is masked as internal_error 500, and is then
212
+ // retried by the hub (2026-07-22 catchtable reserve RCA, candidate A). Returning
213
+ // null also keeps list() from aborting the whole scan on a single bad entry.
214
+ let parsed: unknown;
215
+ try {
216
+ parsed = JSON.parse(raw);
217
+ } catch {
218
+ return null;
219
+ }
132
220
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
133
221
  return null;
134
222
  }
135
- const record: Record<string, unknown> = Object.fromEntries(
136
- Object.entries(parsed),
137
- );
223
+ const record: Record<string, unknown> = Object.fromEntries(Object.entries(parsed));
138
224
  if (
139
225
  typeof record.version !== "number" ||
140
226
  typeof record.expiresAt !== "string" ||
@@ -176,15 +262,91 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
176
262
  return providerStateKey(this.providerId, this.namespaceName, key);
177
263
  }
178
264
 
179
- private prefix(): string {
265
+ private indexKey(): string {
266
+ // Keep bookkeeping outside the caller-owned keyspace. A provider may use
267
+ // any state key (including "__index"), so a suffix inside the namespace
268
+ // could turn the ZSET into a string and break every subsequent write.
269
+ const namespaceIdentity = Buffer.from(
270
+ providerStatePrefix(this.providerId, this.namespaceName),
271
+ "utf8",
272
+ ).toString("base64url");
273
+ return `${REDIS_STATE_PREFIX}:index:${namespaceIdentity}`;
274
+ }
275
+
276
+ private legacyScanCursorKey(): string {
277
+ return `${this.indexKey()}:legacy-scan-cursor`;
278
+ }
279
+
280
+ private legacyPrefix(): string {
180
281
  return `${providerStatePrefix(this.providerId, this.namespaceName)}:`;
181
282
  }
182
283
 
183
- private async activeKeys(): Promise<string[]> {
284
+ private async backfillLegacyIndex(): Promise<void> {
184
285
  await requireRedisReady(this.backend.redis);
185
- return await withRequiredRedis(() =>
186
- this.backend.redis.keys(`${this.prefix()}*`),
187
- );
286
+ const cursorKey = this.legacyScanCursorKey();
287
+ let cursor =
288
+ (await withRequiredRedis(() => this.backend.redis.get(cursorKey))) ?? "0";
289
+ const pattern = `${redisGlobLiteral(this.legacyPrefix())}*`;
290
+ const indexTtlMs = parseStateDurationMs(this.options.maxTtl);
291
+
292
+ for (let page = 0; page < LEGACY_INDEX_SCAN_MAX_PAGES; page += 1) {
293
+ const [nextCursor, keys] = await withRequiredRedis(() =>
294
+ this.backend.redis.scan(
295
+ cursor,
296
+ "MATCH",
297
+ pattern,
298
+ "COUNT",
299
+ LEGACY_INDEX_SCAN_COUNT,
300
+ ),
301
+ );
302
+ const rawValues =
303
+ keys.length > 0
304
+ ? await withRequiredRedis(() => this.backend.redis.mget(keys))
305
+ : [];
306
+ const now = Date.now();
307
+ const activeLegacyArgs: string[] = [];
308
+ for (const [index, raw] of rawValues.entries()) {
309
+ const key = keys[index];
310
+ if (!key || !raw) continue;
311
+ const envelope = envelopeFromJson(
312
+ publicStateKey(this.providerId, this.namespaceName, key),
313
+ raw,
314
+ );
315
+ const expiresAtMs = envelope ? Date.parse(envelope.expiresAt) : Number.NaN;
316
+ if (!Number.isFinite(expiresAtMs) || expiresAtMs <= now) continue;
317
+ activeLegacyArgs.push(key, raw, String(expiresAtMs));
318
+ }
319
+ await withRequiredRedis(() =>
320
+ this.backend.redis.eval(
321
+ BACKFILL_LEGACY_INDEX_SCRIPT,
322
+ 2,
323
+ this.indexKey(),
324
+ cursorKey,
325
+ String(now),
326
+ String(indexTtlMs),
327
+ nextCursor,
328
+ ...activeLegacyArgs,
329
+ ),
330
+ );
331
+ cursor = nextCursor;
332
+ if (cursor === "0") break;
333
+ }
334
+ }
335
+
336
+ private async indexedKeys(limit: number): Promise<string[]> {
337
+ await requireRedisReady(this.backend.redis);
338
+ const now = Date.now();
339
+ return await withRequiredRedis(async () => {
340
+ await this.backend.redis.zremrangebyscore(this.indexKey(), "-inf", now);
341
+ return await this.backend.redis.zrangebyscore(
342
+ this.indexKey(),
343
+ now + 1,
344
+ "+inf",
345
+ "LIMIT",
346
+ 0,
347
+ limit,
348
+ );
349
+ });
188
350
  }
189
351
 
190
352
  private enforceValueSize(value: unknown): void {
@@ -196,80 +358,87 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
196
358
  }
197
359
  }
198
360
 
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) {
361
+ private quotaExceeded(): UnsupportedProviderStateError {
362
+ return new UnsupportedProviderStateError(
363
+ `Provider runtime state namespace quota exceeded (${this.options.maxEntries + 1} > ${this.options.maxEntries})`,
364
+ );
365
+ }
366
+
367
+ private writeTiming(ttl: StateWriteOptions["ttl"]): {
368
+ expiresAt: string;
369
+ expiresAtMs: number;
370
+ indexTtlMs: number;
371
+ } {
372
+ const ttlMs = parseStateDurationMs(ttl ?? this.options.defaultTtl);
373
+ const maxTtlMs = parseStateDurationMs(this.options.maxTtl);
374
+ if (ttlMs > maxTtlMs) {
204
375
  throw new UnsupportedProviderStateError(
205
- `Provider runtime state namespace quota exceeded (${otherKeys.length + 1} > ${this.options.maxEntries})`,
376
+ `Provider runtime state ttl exceeds maxTtl (${ttlMs} > ${maxTtlMs})`,
206
377
  );
207
378
  }
379
+ const expiresAtMs = Date.now() + ttlMs;
380
+ return {
381
+ expiresAt: new Date(expiresAtMs).toISOString(),
382
+ expiresAtMs,
383
+ indexTtlMs: maxTtlMs,
384
+ };
208
385
  }
209
386
 
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
- );
387
+ async list<T>(options?: { limit?: number; prefix?: string }): Promise<StateValue<T>[]> {
388
+ const requestedLimit = Math.max(0, options?.limit ?? this.options.maxEntries);
389
+ if (requestedLimit === 0) return [];
390
+ await this.backfillLegacyIndex();
391
+ const keys = (await this.indexedKeys(this.options.maxEntries)).filter((key) => {
392
+ const publicKey = publicStateKey(this.providerId, this.namespaceName, key);
220
393
  return options?.prefix ? publicKey.startsWith(options.prefix) : true;
221
394
  });
222
- const limited = keys.slice(0, Math.max(0, options?.limit ?? keys.length));
395
+ const limited = keys.slice(0, requestedLimit);
223
396
  if (limited.length === 0) return [];
224
- const values = await withRequiredRedis(() =>
225
- this.backend.redis.mget(limited),
226
- );
397
+ const values = await withRequiredRedis(() => this.backend.redis.mget(limited));
227
398
  return values.flatMap((raw, index) => {
228
399
  const key = limited[index];
229
400
  if (!key) return [];
230
- const value = envelopeFromJson(
231
- publicStateKey(this.providerId, this.namespaceName, key),
232
- raw,
233
- );
401
+ const value = envelopeFromJson(publicStateKey(this.providerId, this.namespaceName, key), raw);
234
402
  return value ? [value] : [];
235
403
  });
236
404
  }
237
405
 
238
406
  async get<T>(key: string): Promise<StateValue<T> | null> {
239
407
  await requireRedisReady(this.backend.redis);
240
- const raw = await withRequiredRedis(() =>
241
- this.backend.redis.get(this.redisKey(key)),
242
- );
408
+ const raw = await withRequiredRedis(() => this.backend.redis.get(this.redisKey(key)));
243
409
  return envelopeFromJson(key, raw);
244
410
  }
245
411
 
246
- async set<T>(
247
- key: string,
248
- value: T,
249
- options?: StateWriteOptions,
250
- ): Promise<StateValue<T>> {
412
+ async set<T>(key: string, value: T, options?: StateWriteOptions): Promise<StateValue<T>> {
251
413
  this.enforceValueSize(value);
252
- await this.enforceMaxEntries(key);
414
+ await this.backfillLegacyIndex();
253
415
  const current = await this.get<T>(key);
254
416
  const createdAt = current?.createdAt ?? new Date().toISOString();
255
417
  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(
418
+ const timing = this.writeTiming(options?.ttl);
419
+ const envelope = redisEnvelope(value, version, createdAt, timing.expiresAt);
420
+ await requireRedisReady(this.backend.redis);
421
+ const result = await withRequiredRedis(() =>
422
+ this.backend.redis.eval(
423
+ SET_WITH_QUOTA_SCRIPT,
424
+ 2,
262
425
  this.redisKey(key),
426
+ this.indexKey(),
427
+ String(Date.now()),
428
+ String(this.options.maxEntries),
429
+ String(timing.expiresAtMs),
430
+ String(timing.indexTtlMs),
263
431
  JSON.stringify(envelope),
264
- "PX",
265
- ttlMs,
266
432
  ),
267
433
  );
434
+ if (!Array.isArray(result) || Number(result[0]) !== 1) {
435
+ throw this.quotaExceeded();
436
+ }
268
437
  return {
269
438
  key,
270
439
  value,
271
440
  version,
272
- expiresAt,
441
+ expiresAt: timing.expiresAt,
273
442
  createdAt,
274
443
  updatedAt: envelope.updatedAt,
275
444
  };
@@ -293,16 +462,64 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
293
462
  options?: StateWriteOptions,
294
463
  ): Promise<StateCasResult<T>> {
295
464
  this.enforceValueSize(value);
465
+ await this.backfillLegacyIndex();
296
466
  const current = await this.get<T>(key);
297
467
  if ((current?.version ?? 0) !== expectedVersion) {
298
468
  return { ok: false, current };
299
469
  }
300
- return { ok: true, value: await this.set(key, value, options) };
470
+ const createdAt = current?.createdAt ?? new Date().toISOString();
471
+ const timing = this.writeTiming(options?.ttl);
472
+ const envelope = redisEnvelope(
473
+ value,
474
+ expectedVersion + 1,
475
+ createdAt,
476
+ timing.expiresAt,
477
+ );
478
+ await requireRedisReady(this.backend.redis);
479
+ const result = await withRequiredRedis(() =>
480
+ this.backend.redis.eval(
481
+ COMPARE_AND_SET_WITH_QUOTA_SCRIPT,
482
+ 2,
483
+ this.redisKey(key),
484
+ this.indexKey(),
485
+ String(expectedVersion),
486
+ String(Date.now()),
487
+ String(this.options.maxEntries),
488
+ String(timing.expiresAtMs),
489
+ String(timing.indexTtlMs),
490
+ JSON.stringify(envelope),
491
+ ),
492
+ );
493
+ if (Array.isArray(result) && Number(result[0]) === 0) {
494
+ throw this.quotaExceeded();
495
+ }
496
+ if (!Array.isArray(result) || Number(result[0]) !== 1) {
497
+ const rawCurrent = Array.isArray(result) && typeof result[1] === "string" ? result[1] : null;
498
+ return { ok: false, current: envelopeFromJson(key, rawCurrent) };
499
+ }
500
+ return {
501
+ ok: true,
502
+ value: {
503
+ key,
504
+ value,
505
+ version: envelope.version,
506
+ expiresAt: timing.expiresAt,
507
+ createdAt,
508
+ updatedAt: envelope.updatedAt,
509
+ },
510
+ };
301
511
  }
302
512
 
303
513
  async delete(key: string): Promise<void> {
304
514
  await requireRedisReady(this.backend.redis);
305
- await withRequiredRedis(() => this.backend.redis.del(this.redisKey(key)));
515
+ await withRequiredRedis(() =>
516
+ this.backend.redis.eval(
517
+ DELETE_WITH_INDEX_SCRIPT,
518
+ 2,
519
+ this.redisKey(key),
520
+ this.indexKey(),
521
+ ),
522
+ );
306
523
  }
307
524
 
308
525
  async increment(
@@ -313,11 +530,7 @@ class RedisProviderStateNamespace implements ProviderStateNamespace {
313
530
  ): Promise<StateValue<Record<string, unknown>>> {
314
531
  const current = (await this.get<Record<string, unknown>>(key))?.value ?? {};
315
532
  const previous = typeof current[field] === "number" ? current[field] : 0;
316
- return await this.set(
317
- key,
318
- { ...current, [field]: previous + delta },
319
- options,
320
- );
533
+ return await this.set(key, { ...current, [field]: previous + delta }, options);
321
534
  }
322
535
  }
323
536
 
@@ -330,43 +543,26 @@ class RedisProviderRuntimeState implements ProviderRuntimeState {
330
543
  this.providerId = options.providerId;
331
544
  }
332
545
 
333
- namespace(
334
- name: string,
335
- options: StateNamespaceOptions,
336
- ): ProviderStateNamespace {
337
- return new RedisProviderStateNamespace(
338
- this.backend,
339
- this.providerId,
340
- name,
341
- options,
342
- );
546
+ namespace(name: string, options: StateNamespaceOptions): ProviderStateNamespace {
547
+ return new RedisProviderStateNamespace(this.backend, this.providerId, name, options);
343
548
  }
344
549
  }
345
550
 
346
551
  export class UnsupportedProviderStateError extends ProviderError {
347
- constructor(
348
- message = "Provider runtime state is not available in this runtime",
349
- ) {
552
+ constructor(message = "Provider runtime state is not available in this runtime") {
350
553
  super(message, { code: "PROVIDER_STATE_UNSUPPORTED" });
351
554
  this.name = "UnsupportedProviderStateError";
352
555
  }
353
556
  }
354
557
 
355
558
  class UnsupportedProviderStateNamespace implements ProviderStateNamespace {
356
- async list<T>(_options?: {
357
- limit?: number;
358
- prefix?: string;
359
- }): Promise<StateValue<T>[]> {
559
+ async list<T>(_options?: { limit?: number; prefix?: string }): Promise<StateValue<T>[]> {
360
560
  throw new UnsupportedProviderStateError();
361
561
  }
362
562
  async get<T>(_key: string): Promise<StateValue<T> | null> {
363
563
  throw new UnsupportedProviderStateError();
364
564
  }
365
- async set<T>(
366
- _key: string,
367
- _value: T,
368
- _options?: StateWriteOptions,
369
- ): Promise<StateValue<T>> {
565
+ async set<T>(_key: string, _value: T, _options?: StateWriteOptions): Promise<StateValue<T>> {
370
566
  throw new UnsupportedProviderStateError();
371
567
  }
372
568
  async patch<T extends Record<string, unknown>>(
@@ -398,10 +594,7 @@ class UnsupportedProviderStateNamespace implements ProviderStateNamespace {
398
594
  }
399
595
 
400
596
  class UnsupportedProviderRuntimeState implements ProviderRuntimeState {
401
- namespace(
402
- _name: string,
403
- _options: StateNamespaceOptions,
404
- ): ProviderStateNamespace {
597
+ namespace(_name: string, _options: StateNamespaceOptions): ProviderStateNamespace {
405
598
  return new UnsupportedProviderStateNamespace();
406
599
  }
407
600
  }
@@ -412,6 +605,31 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
412
605
 
413
606
  constructor(private readonly options: StateNamespaceOptions) {}
414
607
 
608
+ private enforceValueSize(value: unknown): void {
609
+ const bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
610
+ if (bytes > this.options.maxValueBytes) {
611
+ throw new UnsupportedProviderStateError(
612
+ `Provider runtime state value exceeds maxValueBytes (${bytes} > ${this.options.maxValueBytes})`,
613
+ );
614
+ }
615
+ }
616
+
617
+ private enforceWritePolicy(key: string, value: unknown, ttl: StateWriteOptions["ttl"]): void {
618
+ this.enforceValueSize(value);
619
+ const ttlMs = parseStateDurationMs(ttl ?? this.options.defaultTtl);
620
+ const maxTtlMs = parseStateDurationMs(this.options.maxTtl);
621
+ if (ttlMs > maxTtlMs) {
622
+ throw new UnsupportedProviderStateError(
623
+ `Provider runtime state ttl exceeds maxTtl (${ttlMs} > ${maxTtlMs})`,
624
+ );
625
+ }
626
+ if (!this.values.has(key) && this.values.size >= this.options.maxEntries) {
627
+ throw new UnsupportedProviderStateError(
628
+ `Provider runtime state namespace quota exceeded (${this.options.maxEntries + 1} > ${this.options.maxEntries})`,
629
+ );
630
+ }
631
+ }
632
+
415
633
  private pruneExpired(nowMs = Date.now()): void {
416
634
  for (const [key, row] of this.values.entries()) {
417
635
  if (row.expiresAt && Date.parse(row.expiresAt) <= nowMs) {
@@ -420,10 +638,7 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
420
638
  }
421
639
  }
422
640
 
423
- async list<T>(_options?: {
424
- limit?: number;
425
- prefix?: string;
426
- }): Promise<StateValue<T>[]> {
641
+ async list<T>(_options?: { limit?: number; prefix?: string }): Promise<StateValue<T>[]> {
427
642
  this.pruneExpired();
428
643
  const rows = Array.from(this.values.values()).filter((value) =>
429
644
  _options?.prefix ? value.key.startsWith(_options.prefix) : true,
@@ -436,17 +651,12 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
436
651
  return this.values.get(key) ?? null;
437
652
  }
438
653
 
439
- async set<T>(
440
- key: string,
441
- value: T,
442
- options?: StateWriteOptions,
443
- ): Promise<StateValue<T>> {
654
+ async set<T>(key: string, value: T, options?: StateWriteOptions): Promise<StateValue<T>> {
444
655
  this.pruneExpired();
656
+ this.enforceWritePolicy(key, value, options?.ttl);
445
657
  const now = new Date().toISOString();
446
658
  const current = this.values.get(key);
447
- const expiresAt = resolveMemoryStateExpiresAt(
448
- options?.ttl ?? this.options.defaultTtl,
449
- );
659
+ const expiresAt = resolveMemoryStateExpiresAt(options?.ttl ?? this.options.defaultTtl);
450
660
  const row = {
451
661
  key,
452
662
  value,
@@ -470,14 +680,30 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
470
680
  }
471
681
 
472
682
  async compareAndSet<T>(
473
- _key: string,
474
- _expectedVersion: number,
475
- _value: T,
476
- _options?: StateWriteOptions,
683
+ key: string,
684
+ expectedVersion: number,
685
+ value: T,
686
+ options?: StateWriteOptions,
477
687
  ): Promise<StateCasResult<T>> {
478
- throw new UnsupportedProviderStateError(
479
- "In-memory provider runtime state does not support compareAndSet",
480
- );
688
+ this.pruneExpired();
689
+ const current = this.values.get(key) as StateValue<T> | undefined;
690
+ if ((current?.version ?? 0) !== expectedVersion) {
691
+ return { ok: false, current: current ?? null };
692
+ }
693
+ this.enforceWritePolicy(key, value, options?.ttl);
694
+ const now = new Date().toISOString();
695
+ const stored = {
696
+ key,
697
+ value,
698
+ version: expectedVersion + 1,
699
+ expiresAt: resolveMemoryStateExpiresAt(
700
+ options?.ttl ?? this.options.defaultTtl,
701
+ ),
702
+ createdAt: current?.createdAt ?? now,
703
+ updatedAt: now,
704
+ } satisfies StateValue<T>;
705
+ this.values.set(key, stored);
706
+ return { ok: true, value: stored };
481
707
  }
482
708
 
483
709
  async delete(key: string): Promise<void> {
@@ -499,10 +725,7 @@ class MemoryProviderStateNamespace implements ProviderStateNamespace {
499
725
  class MemoryProviderRuntimeState implements ProviderRuntimeState {
500
726
  readonly namespaces = new Map<string, MemoryProviderStateNamespace>();
501
727
 
502
- namespace(
503
- name: string,
504
- _options: StateNamespaceOptions,
505
- ): ProviderStateNamespace {
728
+ namespace(name: string, _options: StateNamespaceOptions): ProviderStateNamespace {
506
729
  const existing = this.namespaces.get(name);
507
730
  if (existing) return existing;
508
731
  const created = new MemoryProviderStateNamespace(_options);
@@ -536,10 +759,7 @@ export function createRedisProviderRuntimeState(
536
759
  }
537
760
 
538
761
  export function createProviderRuntimeStateFromEnv(
539
- options: {
540
- readonly providerId?: string;
541
- readonly allowMemoryFallback?: boolean;
542
- } = {},
762
+ options: { readonly providerId?: string; readonly allowMemoryFallback?: boolean } = {},
543
763
  ): ProviderRuntimeState {
544
764
  const redisUrl = providerStateRedisUrlFromEnv();
545
765
  if (redisUrl) {