@apifuse/provider-sdk 2.1.0-beta.1 → 2.1.0-beta.10

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 (212) hide show
  1. package/AUTHORING.md +208 -2
  2. package/CHANGELOG.md +47 -0
  3. package/README.md +114 -10
  4. package/SUBMISSION.md +87 -0
  5. package/bin/apifuse-check.ts +86 -4
  6. package/bin/apifuse-dev.ts +87 -13
  7. package/bin/apifuse-pack-check.ts +80 -0
  8. package/bin/apifuse-pack-smoke.ts +303 -2
  9. package/bin/apifuse-perf.ts +142 -49
  10. package/bin/apifuse-record.ts +182 -104
  11. package/bin/apifuse-submit-check.ts +2538 -0
  12. package/bin/apifuse.ts +1 -1
  13. package/dist/ceremonies/index.d.ts +41 -0
  14. package/dist/ceremonies/index.js +490 -0
  15. package/dist/choice-token.d.ts +24 -0
  16. package/dist/choice-token.js +74 -0
  17. package/dist/cli/commands.d.ts +10 -0
  18. package/dist/cli/commands.js +80 -0
  19. package/dist/cli/create.d.ts +47 -0
  20. package/dist/cli/create.js +762 -0
  21. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  22. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  23. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  24. package/dist/cli/templates/provider/README.md.tpl +160 -0
  25. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  26. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  27. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  28. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  29. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  30. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  31. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  32. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  33. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  34. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  35. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  36. package/dist/config/loader.d.ts +107 -0
  37. package/dist/config/loader.js +935 -0
  38. package/dist/contract-json.d.ts +9 -0
  39. package/dist/contract-json.js +51 -0
  40. package/dist/contract-serialization.d.ts +4 -0
  41. package/dist/contract-serialization.js +78 -0
  42. package/dist/contract-types.d.ts +49 -0
  43. package/dist/contract-types.js +1 -0
  44. package/dist/contract.d.ts +6 -0
  45. package/dist/contract.js +155 -0
  46. package/dist/define.d.ts +97 -0
  47. package/dist/define.js +1320 -0
  48. package/dist/dev.d.ts +9 -0
  49. package/dist/dev.js +15 -0
  50. package/dist/errors.d.ts +59 -0
  51. package/dist/errors.js +97 -0
  52. package/dist/i18n/catalog.d.ts +29 -0
  53. package/dist/i18n/catalog.js +159 -0
  54. package/dist/i18n/index.d.ts +2 -0
  55. package/dist/i18n/index.js +2 -0
  56. package/dist/i18n/keys.d.ts +10 -0
  57. package/dist/i18n/keys.js +34 -0
  58. package/dist/index.d.ts +41 -0
  59. package/dist/index.js +37 -0
  60. package/dist/lint.d.ts +73 -0
  61. package/dist/lint.js +702 -0
  62. package/dist/observability.d.ts +5 -0
  63. package/dist/observability.js +39 -0
  64. package/dist/provider.d.ts +9 -0
  65. package/dist/provider.js +8 -0
  66. package/dist/public-schema-field-lint.d.ts +2 -0
  67. package/dist/public-schema-field-lint.js +158 -0
  68. package/dist/recipes/gov-api.d.ts +19 -0
  69. package/dist/recipes/gov-api.js +72 -0
  70. package/dist/recipes/rest-api.d.ts +21 -0
  71. package/dist/recipes/rest-api.js +115 -0
  72. package/dist/runtime/auth-flow.d.ts +14 -0
  73. package/dist/runtime/auth-flow.js +44 -0
  74. package/dist/runtime/browser.d.ts +25 -0
  75. package/dist/runtime/browser.js +1034 -0
  76. package/dist/runtime/cache.d.ts +10 -0
  77. package/dist/runtime/cache.js +372 -0
  78. package/dist/runtime/choice.d.ts +15 -0
  79. package/dist/runtime/choice.js +435 -0
  80. package/dist/runtime/credential.d.ts +8 -0
  81. package/dist/runtime/credential.js +61 -0
  82. package/dist/runtime/env.d.ts +2 -0
  83. package/dist/runtime/env.js +10 -0
  84. package/dist/runtime/executor.d.ts +16 -0
  85. package/dist/runtime/executor.js +51 -0
  86. package/dist/runtime/http.d.ts +8 -0
  87. package/dist/runtime/http.js +706 -0
  88. package/dist/runtime/insights.d.ts +9 -0
  89. package/dist/runtime/insights.js +324 -0
  90. package/dist/runtime/instrumentation.d.ts +8 -0
  91. package/dist/runtime/instrumentation.js +269 -0
  92. package/dist/runtime/key-derivation.d.ts +24 -0
  93. package/dist/runtime/key-derivation.js +73 -0
  94. package/dist/runtime/keyring.d.ts +25 -0
  95. package/dist/runtime/keyring.js +93 -0
  96. package/dist/runtime/namespace.d.ts +9 -0
  97. package/dist/runtime/namespace.js +19 -0
  98. package/dist/runtime/otlp.d.ts +39 -0
  99. package/dist/runtime/otlp.js +103 -0
  100. package/dist/runtime/perf.d.ts +12 -0
  101. package/dist/runtime/perf.js +52 -0
  102. package/dist/runtime/prevalidate.d.ts +12 -0
  103. package/dist/runtime/prevalidate.js +173 -0
  104. package/dist/runtime/provider.d.ts +2 -0
  105. package/dist/runtime/provider.js +11 -0
  106. package/dist/runtime/proxy-errors.d.ts +21 -0
  107. package/dist/runtime/proxy-errors.js +83 -0
  108. package/dist/runtime/proxy-telemetry.d.ts +8 -0
  109. package/dist/runtime/proxy-telemetry.js +174 -0
  110. package/dist/runtime/redis.d.ts +17 -0
  111. package/dist/runtime/redis.js +82 -0
  112. package/dist/runtime/request-options.d.ts +3 -0
  113. package/dist/runtime/request-options.js +42 -0
  114. package/dist/runtime/state.d.ts +17 -0
  115. package/dist/runtime/state.js +344 -0
  116. package/dist/runtime/stealth.d.ts +18 -0
  117. package/dist/runtime/stealth.js +834 -0
  118. package/dist/runtime/stt.d.ts +22 -0
  119. package/dist/runtime/stt.js +480 -0
  120. package/dist/runtime/trace.d.ts +26 -0
  121. package/dist/runtime/trace.js +142 -0
  122. package/dist/runtime/waterfall.d.ts +12 -0
  123. package/dist/runtime/waterfall.js +147 -0
  124. package/dist/schema.d.ts +74 -0
  125. package/dist/schema.js +243 -0
  126. package/dist/serve.d.ts +1 -0
  127. package/dist/serve.js +1 -0
  128. package/dist/server/index.d.ts +3 -0
  129. package/dist/server/index.js +2 -0
  130. package/dist/server/serve.d.ts +64 -0
  131. package/dist/server/serve.js +1110 -0
  132. package/dist/server/types.d.ts +136 -0
  133. package/dist/server/types.js +86 -0
  134. package/dist/stealth/profiles.d.ts +4 -0
  135. package/dist/stealth/profiles.js +259 -0
  136. package/dist/stream.d.ts +44 -0
  137. package/dist/stream.js +151 -0
  138. package/dist/testing/helpers.d.ts +23 -0
  139. package/dist/testing/helpers.js +95 -0
  140. package/dist/testing/index.d.ts +2 -0
  141. package/dist/testing/index.js +2 -0
  142. package/dist/testing/run.d.ts +34 -0
  143. package/dist/testing/run.js +303 -0
  144. package/dist/types.d.ts +1326 -0
  145. package/dist/types.js +61 -0
  146. package/dist/utils/date.d.ts +6 -0
  147. package/dist/utils/date.js +101 -0
  148. package/dist/utils/parse.d.ts +16 -0
  149. package/dist/utils/parse.js +51 -0
  150. package/dist/utils/text.d.ts +4 -0
  151. package/dist/utils/text.js +14 -0
  152. package/dist/utils/transform.d.ts +8 -0
  153. package/dist/utils/transform.js +48 -0
  154. package/package.json +57 -30
  155. package/src/ceremonies/index.ts +8 -2
  156. package/src/choice-token.ts +165 -0
  157. package/src/cli/commands.ts +34 -11
  158. package/src/cli/create.ts +214 -52
  159. package/src/cli/templates/provider/.dockerignore.tpl +22 -0
  160. package/src/cli/templates/provider/.gitignore.tpl +22 -0
  161. package/src/cli/templates/provider/README.md.tpl +120 -1
  162. package/src/cli/templates/provider/dev.ts.tpl +1 -1
  163. package/src/cli/templates/provider/domain/README.md.tpl +3 -0
  164. package/src/cli/templates/provider/index.ts.tpl +5 -48
  165. package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
  166. package/src/cli/templates/provider/meta.ts.tpl +7 -0
  167. package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
  168. package/src/cli/templates/provider/operations/ping.ts.tpl +24 -0
  169. package/src/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  170. package/src/cli/templates/provider/start.ts.tpl +1 -1
  171. package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
  172. package/src/config/loader.ts +1224 -9
  173. package/src/contract-json.ts +75 -0
  174. package/src/contract-serialization.ts +89 -0
  175. package/src/contract-types.ts +52 -0
  176. package/src/contract.ts +215 -0
  177. package/src/define.ts +1688 -48
  178. package/src/errors.ts +27 -0
  179. package/src/i18n/catalog.ts +277 -0
  180. package/src/i18n/index.ts +2 -0
  181. package/src/i18n/keys.ts +64 -0
  182. package/src/index.ts +174 -9
  183. package/src/lint.ts +547 -73
  184. package/src/observability.ts +41 -0
  185. package/src/provider.ts +104 -4
  186. package/src/public-schema-field-lint.ts +237 -0
  187. package/src/runtime/auth-flow.ts +7 -0
  188. package/src/runtime/browser.ts +762 -51
  189. package/src/runtime/cache.ts +528 -0
  190. package/src/runtime/choice.ts +760 -0
  191. package/src/runtime/executor.ts +32 -3
  192. package/src/runtime/http.ts +939 -195
  193. package/src/runtime/insights.ts +11 -11
  194. package/src/runtime/instrumentation.ts +12 -4
  195. package/src/runtime/key-derivation.ts +1 -1
  196. package/src/runtime/keyring.ts +4 -3
  197. package/src/runtime/proxy-errors.ts +132 -0
  198. package/src/runtime/proxy-telemetry.ts +253 -0
  199. package/src/runtime/redis.ts +116 -0
  200. package/src/runtime/request-options.ts +66 -0
  201. package/src/runtime/state.ts +563 -0
  202. package/src/runtime/stealth.ts +1159 -0
  203. package/src/runtime/stt.ts +629 -0
  204. package/src/runtime/trace.ts +1 -1
  205. package/src/schema.ts +363 -1
  206. package/src/server/serve.ts +1157 -75
  207. package/src/server/types.ts +37 -0
  208. package/src/stream.ts +210 -0
  209. package/src/testing/run.ts +31 -5
  210. package/src/types.ts +1107 -59
  211. package/src/runtime/tls.ts +0 -434
  212. package/src/types/playwright-stealth.d.ts +0 -9
@@ -0,0 +1,66 @@
1
+ import type {
2
+ RequestParamPrimitive,
3
+ RequestParams,
4
+ RequestParamValue,
5
+ } from "../types";
6
+
7
+ function isParamArray(
8
+ value: RequestParamValue,
9
+ ): value is readonly RequestParamPrimitive[] {
10
+ return Array.isArray(value);
11
+ }
12
+
13
+ function appendQueryValue(
14
+ searchParams: URLSearchParams,
15
+ key: string,
16
+ value: string | number | boolean | null | undefined,
17
+ ): void {
18
+ if (value === null || value === undefined) {
19
+ return;
20
+ }
21
+ searchParams.append(key, String(value));
22
+ }
23
+
24
+ export function appendQueryParams(url: string, params?: RequestParams): string {
25
+ if (!params || Object.keys(params).length === 0) {
26
+ return url;
27
+ }
28
+
29
+ const parsed = new URL(url);
30
+ for (const [key, value] of Object.entries(params)) {
31
+ if (isParamArray(value)) {
32
+ for (const item of value)
33
+ appendQueryValue(parsed.searchParams, key, item);
34
+ continue;
35
+ }
36
+ appendQueryValue(parsed.searchParams, key, value);
37
+ }
38
+
39
+ return parsed.toString();
40
+ }
41
+
42
+ export function normalizeHttpRequestBody(
43
+ body: unknown,
44
+ ): string | Buffer | undefined {
45
+ if (body === undefined) {
46
+ return undefined;
47
+ }
48
+
49
+ if (typeof body === "string" || Buffer.isBuffer(body)) {
50
+ return body;
51
+ }
52
+
53
+ if (body instanceof URLSearchParams) {
54
+ return body.toString();
55
+ }
56
+
57
+ if (body instanceof ArrayBuffer) {
58
+ return Buffer.from(body);
59
+ }
60
+
61
+ if (ArrayBuffer.isView(body)) {
62
+ return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
63
+ }
64
+
65
+ return JSON.stringify(body);
66
+ }
@@ -0,0 +1,563 @@
1
+ import { providerStateRedisUrlFromEnv } from "../config/loader";
2
+ import { ProviderError } from "../errors";
3
+ import type {
4
+ ProviderRuntimeState,
5
+ ProviderStateNamespace,
6
+ StateCasResult,
7
+ StateNamespaceOptions,
8
+ StateValue,
9
+ StateWriteOptions,
10
+ } from "../types";
11
+ import {
12
+ createProviderRedisClient,
13
+ ensureRedisReady,
14
+ type ProviderRedisClient,
15
+ withRedisTimeout,
16
+ } from "./redis";
17
+
18
+ const DEFAULT_REDIS_TIMEOUT_MS = 250;
19
+ const REDIS_STATE_PREFIX = "apifuse:provider-state:v1";
20
+
21
+ type RedisProviderRuntimeStateOptions = {
22
+ readonly redisUrl: string;
23
+ readonly providerId?: string;
24
+ };
25
+
26
+ type RedisStateEnvelope = {
27
+ readonly value: unknown;
28
+ readonly version: number;
29
+ readonly expiresAt: string;
30
+ readonly createdAt: string;
31
+ readonly updatedAt: string;
32
+ };
33
+
34
+ type RedisBackend = {
35
+ readonly redis: ProviderRedisClient;
36
+ };
37
+
38
+ const redisBackends = new Map<string, RedisBackend>();
39
+
40
+ function getRedisBackend(redisUrl: string): RedisBackend {
41
+ const existing = redisBackends.get(redisUrl);
42
+ 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
+ });
51
+ const backend = { redis };
52
+ redisBackends.set(redisUrl, backend);
53
+ return backend;
54
+ }
55
+
56
+ async function withRequiredRedis<T>(operation: () => Promise<T>): Promise<T> {
57
+ return await withRedisTimeout(operation, {
58
+ timeoutMs: DEFAULT_REDIS_TIMEOUT_MS,
59
+ onTimeout: () => {
60
+ throw new UnsupportedProviderStateError(
61
+ "Provider runtime state Redis timed out",
62
+ );
63
+ },
64
+ onError: () => {
65
+ throw new UnsupportedProviderStateError(
66
+ "Provider runtime state Redis is unavailable",
67
+ );
68
+ },
69
+ });
70
+ }
71
+
72
+ async function requireRedisReady(redis: ProviderRedisClient): Promise<void> {
73
+ if (await ensureRedisReady(redis, DEFAULT_REDIS_TIMEOUT_MS)) return;
74
+ throw new UnsupportedProviderStateError(
75
+ "Provider runtime state Redis is unavailable",
76
+ );
77
+ }
78
+
79
+ function providerStatePrefix(
80
+ providerId: string | undefined,
81
+ namespace: string,
82
+ ): string {
83
+ return `${REDIS_STATE_PREFIX}:${providerId ?? "default"}:${namespace}`;
84
+ }
85
+
86
+ function providerStateKey(
87
+ providerId: string | undefined,
88
+ namespace: string,
89
+ key: string,
90
+ ): string {
91
+ return `${providerStatePrefix(providerId, namespace)}:${key}`;
92
+ }
93
+
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;
101
+ }
102
+
103
+ function parseStateDurationMs(ttl: StateWriteOptions["ttl"]): number {
104
+ const match = /^(\d+)(ms|s|m|h|d)$/.exec(ttl ?? "1h");
105
+ if (!match) return 3_600_000;
106
+ const amount = Number(match[1]);
107
+ const unit = match[2];
108
+ const multiplier =
109
+ unit === "ms"
110
+ ? 1
111
+ : unit === "s"
112
+ ? 1_000
113
+ : unit === "m"
114
+ ? 60_000
115
+ : unit === "h"
116
+ ? 3_600_000
117
+ : 86_400_000;
118
+ return Math.max(1, amount * multiplier);
119
+ }
120
+
121
+ function resolveExpiresAt(ttl: StateWriteOptions["ttl"]): string {
122
+ return new Date(Date.now() + parseStateDurationMs(ttl)).toISOString();
123
+ }
124
+
125
+ function envelopeFromJson(
126
+ key: string,
127
+ raw: string | null,
128
+ // biome-ignore lint/suspicious/noExplicitAny: state envelopes deserialize caller-owned generic values.
129
+ ): StateValue<any> | null {
130
+ if (!raw) return null;
131
+ const parsed: unknown = JSON.parse(raw);
132
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
133
+ return null;
134
+ }
135
+ const record: Record<string, unknown> = Object.fromEntries(
136
+ Object.entries(parsed),
137
+ );
138
+ if (
139
+ typeof record.version !== "number" ||
140
+ typeof record.expiresAt !== "string" ||
141
+ typeof record.createdAt !== "string" ||
142
+ typeof record.updatedAt !== "string" ||
143
+ !("value" in record)
144
+ ) {
145
+ return null;
146
+ }
147
+ return {
148
+ key,
149
+ value: record.value,
150
+ version: record.version,
151
+ expiresAt: record.expiresAt,
152
+ createdAt: record.createdAt,
153
+ updatedAt: record.updatedAt,
154
+ };
155
+ }
156
+
157
+ function redisEnvelope(
158
+ value: unknown,
159
+ version: number,
160
+ createdAt: string,
161
+ expiresAt: string,
162
+ ): RedisStateEnvelope {
163
+ const updatedAt = new Date().toISOString();
164
+ return { value, version, expiresAt, createdAt, updatedAt };
165
+ }
166
+
167
+ class RedisProviderStateNamespace implements ProviderStateNamespace {
168
+ constructor(
169
+ private readonly backend: RedisBackend,
170
+ private readonly providerId: string | undefined,
171
+ private readonly namespaceName: string,
172
+ private readonly options: StateNamespaceOptions,
173
+ ) {}
174
+
175
+ private redisKey(key: string): string {
176
+ return providerStateKey(this.providerId, this.namespaceName, key);
177
+ }
178
+
179
+ private prefix(): string {
180
+ return `${providerStatePrefix(this.providerId, this.namespaceName)}:`;
181
+ }
182
+
183
+ private async activeKeys(): Promise<string[]> {
184
+ await requireRedisReady(this.backend.redis);
185
+ return await withRequiredRedis(() =>
186
+ this.backend.redis.keys(`${this.prefix()}*`),
187
+ );
188
+ }
189
+
190
+ private enforceValueSize(value: unknown): void {
191
+ const bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
192
+ if (bytes > this.options.maxValueBytes) {
193
+ throw new UnsupportedProviderStateError(
194
+ `Provider runtime state value exceeds maxValueBytes (${bytes} > ${this.options.maxValueBytes})`,
195
+ );
196
+ }
197
+ }
198
+
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) {
204
+ throw new UnsupportedProviderStateError(
205
+ `Provider runtime state namespace quota exceeded (${otherKeys.length + 1} > ${this.options.maxEntries})`,
206
+ );
207
+ }
208
+ }
209
+
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
+ });
236
+ }
237
+
238
+ 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
+ );
243
+ return envelopeFromJson(key, raw);
244
+ }
245
+
246
+ async set<T>(
247
+ key: string,
248
+ value: T,
249
+ options?: StateWriteOptions,
250
+ ): Promise<StateValue<T>> {
251
+ this.enforceValueSize(value);
252
+ await this.enforceMaxEntries(key);
253
+ const current = await this.get<T>(key);
254
+ const createdAt = current?.createdAt ?? new Date().toISOString();
255
+ 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(
262
+ this.redisKey(key),
263
+ JSON.stringify(envelope),
264
+ "PX",
265
+ ttlMs,
266
+ ),
267
+ );
268
+ return {
269
+ key,
270
+ value,
271
+ version,
272
+ expiresAt,
273
+ createdAt,
274
+ updatedAt: envelope.updatedAt,
275
+ };
276
+ }
277
+
278
+ async patch<T extends Record<string, unknown>>(
279
+ key: string,
280
+ partial: Partial<T>,
281
+ options?: StateWriteOptions,
282
+ ): Promise<StateValue<T>> {
283
+ const current = (await this.get<Record<string, unknown>>(key))?.value ?? {};
284
+ // biome-ignore lint/suspicious/noExplicitAny: patch preserves the caller-provided generic state shape.
285
+ const merged: any = { ...current, ...partial };
286
+ return await this.set<T>(key, merged, options);
287
+ }
288
+
289
+ async compareAndSet<T>(
290
+ key: string,
291
+ expectedVersion: number,
292
+ value: T,
293
+ options?: StateWriteOptions,
294
+ ): Promise<StateCasResult<T>> {
295
+ this.enforceValueSize(value);
296
+ const current = await this.get<T>(key);
297
+ if ((current?.version ?? 0) !== expectedVersion) {
298
+ return { ok: false, current };
299
+ }
300
+ return { ok: true, value: await this.set(key, value, options) };
301
+ }
302
+
303
+ async delete(key: string): Promise<void> {
304
+ await requireRedisReady(this.backend.redis);
305
+ await withRequiredRedis(() => this.backend.redis.del(this.redisKey(key)));
306
+ }
307
+
308
+ async increment(
309
+ key: string,
310
+ field: string,
311
+ delta = 1,
312
+ options?: StateWriteOptions,
313
+ ): Promise<StateValue<Record<string, unknown>>> {
314
+ const current = (await this.get<Record<string, unknown>>(key))?.value ?? {};
315
+ const previous = typeof current[field] === "number" ? current[field] : 0;
316
+ return await this.set(
317
+ key,
318
+ { ...current, [field]: previous + delta },
319
+ options,
320
+ );
321
+ }
322
+ }
323
+
324
+ class RedisProviderRuntimeState implements ProviderRuntimeState {
325
+ readonly backend: RedisBackend;
326
+ readonly providerId?: string;
327
+
328
+ constructor(options: RedisProviderRuntimeStateOptions) {
329
+ this.backend = getRedisBackend(options.redisUrl);
330
+ this.providerId = options.providerId;
331
+ }
332
+
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
+ );
343
+ }
344
+ }
345
+
346
+ export class UnsupportedProviderStateError extends ProviderError {
347
+ constructor(
348
+ message = "Provider runtime state is not available in this runtime",
349
+ ) {
350
+ super(message, { code: "PROVIDER_STATE_UNSUPPORTED" });
351
+ this.name = "UnsupportedProviderStateError";
352
+ }
353
+ }
354
+
355
+ class UnsupportedProviderStateNamespace implements ProviderStateNamespace {
356
+ async list<T>(_options?: {
357
+ limit?: number;
358
+ prefix?: string;
359
+ }): Promise<StateValue<T>[]> {
360
+ throw new UnsupportedProviderStateError();
361
+ }
362
+ async get<T>(_key: string): Promise<StateValue<T> | null> {
363
+ throw new UnsupportedProviderStateError();
364
+ }
365
+ async set<T>(
366
+ _key: string,
367
+ _value: T,
368
+ _options?: StateWriteOptions,
369
+ ): Promise<StateValue<T>> {
370
+ throw new UnsupportedProviderStateError();
371
+ }
372
+ async patch<T extends Record<string, unknown>>(
373
+ _key: string,
374
+ _partial: Partial<T>,
375
+ _options?: StateWriteOptions,
376
+ ): Promise<StateValue<T>> {
377
+ throw new UnsupportedProviderStateError();
378
+ }
379
+ async compareAndSet<T>(
380
+ _key: string,
381
+ _expectedVersion: number,
382
+ _value: T,
383
+ _options?: StateWriteOptions,
384
+ ): Promise<StateCasResult<T>> {
385
+ throw new UnsupportedProviderStateError();
386
+ }
387
+ async delete(_key: string): Promise<void> {
388
+ throw new UnsupportedProviderStateError();
389
+ }
390
+ async increment(
391
+ _key: string,
392
+ _field: string,
393
+ _delta?: number,
394
+ _options?: StateWriteOptions,
395
+ ): Promise<StateValue<Record<string, unknown>>> {
396
+ throw new UnsupportedProviderStateError();
397
+ }
398
+ }
399
+
400
+ class UnsupportedProviderRuntimeState implements ProviderRuntimeState {
401
+ namespace(
402
+ _name: string,
403
+ _options: StateNamespaceOptions,
404
+ ): ProviderStateNamespace {
405
+ return new UnsupportedProviderStateNamespace();
406
+ }
407
+ }
408
+
409
+ class MemoryProviderStateNamespace implements ProviderStateNamespace {
410
+ // biome-ignore lint/suspicious/noExplicitAny: in-memory state stores heterogeneous generic values by key.
411
+ readonly values = new Map<string, StateValue<any>>();
412
+
413
+ constructor(private readonly options: StateNamespaceOptions) {}
414
+
415
+ private pruneExpired(nowMs = Date.now()): void {
416
+ for (const [key, row] of this.values.entries()) {
417
+ if (row.expiresAt && Date.parse(row.expiresAt) <= nowMs) {
418
+ this.values.delete(key);
419
+ }
420
+ }
421
+ }
422
+
423
+ async list<T>(_options?: {
424
+ limit?: number;
425
+ prefix?: string;
426
+ }): Promise<StateValue<T>[]> {
427
+ this.pruneExpired();
428
+ const rows = Array.from(this.values.values()).filter((value) =>
429
+ _options?.prefix ? value.key.startsWith(_options.prefix) : true,
430
+ );
431
+ return rows.slice(0, _options?.limit);
432
+ }
433
+
434
+ async get<T>(key: string): Promise<StateValue<T> | null> {
435
+ this.pruneExpired();
436
+ return this.values.get(key) ?? null;
437
+ }
438
+
439
+ async set<T>(
440
+ key: string,
441
+ value: T,
442
+ options?: StateWriteOptions,
443
+ ): Promise<StateValue<T>> {
444
+ this.pruneExpired();
445
+ const now = new Date().toISOString();
446
+ const current = this.values.get(key);
447
+ const expiresAt = resolveMemoryStateExpiresAt(
448
+ options?.ttl ?? this.options.defaultTtl,
449
+ );
450
+ const row = {
451
+ key,
452
+ value,
453
+ version: (current?.version ?? 0) + 1,
454
+ expiresAt,
455
+ createdAt: current?.createdAt ?? now,
456
+ updatedAt: now,
457
+ } satisfies StateValue<T>;
458
+ this.values.set(key, row);
459
+ return row;
460
+ }
461
+
462
+ async patch<T extends Record<string, unknown>>(
463
+ _key: string,
464
+ _partial: Partial<T>,
465
+ _options?: StateWriteOptions,
466
+ ): Promise<StateValue<T>> {
467
+ throw new UnsupportedProviderStateError(
468
+ "In-memory provider runtime state does not support patch",
469
+ );
470
+ }
471
+
472
+ async compareAndSet<T>(
473
+ _key: string,
474
+ _expectedVersion: number,
475
+ _value: T,
476
+ _options?: StateWriteOptions,
477
+ ): Promise<StateCasResult<T>> {
478
+ throw new UnsupportedProviderStateError(
479
+ "In-memory provider runtime state does not support compareAndSet",
480
+ );
481
+ }
482
+
483
+ async delete(key: string): Promise<void> {
484
+ this.values.delete(key);
485
+ }
486
+
487
+ async increment(
488
+ _key: string,
489
+ _field: string,
490
+ _delta = 1,
491
+ _options?: StateWriteOptions,
492
+ ): Promise<StateValue<Record<string, unknown>>> {
493
+ throw new UnsupportedProviderStateError(
494
+ "In-memory provider runtime state does not support increment",
495
+ );
496
+ }
497
+ }
498
+
499
+ class MemoryProviderRuntimeState implements ProviderRuntimeState {
500
+ readonly namespaces = new Map<string, MemoryProviderStateNamespace>();
501
+
502
+ namespace(
503
+ name: string,
504
+ _options: StateNamespaceOptions,
505
+ ): ProviderStateNamespace {
506
+ const existing = this.namespaces.get(name);
507
+ if (existing) return existing;
508
+ const created = new MemoryProviderStateNamespace(_options);
509
+ this.namespaces.set(name, created);
510
+ return created;
511
+ }
512
+ }
513
+
514
+ function resolveMemoryStateExpiresAt(ttl: StateWriteOptions["ttl"]): string {
515
+ const match = /^(\d+)(ms|s|m|h|d)$/.exec(ttl ?? "1h");
516
+ if (!match) return new Date(Date.now() + 3_600_000).toISOString();
517
+ const amount = Number(match[1]);
518
+ const unit = match[2];
519
+ const multiplier =
520
+ unit === "ms"
521
+ ? 1
522
+ : unit === "s"
523
+ ? 1_000
524
+ : unit === "m"
525
+ ? 60_000
526
+ : unit === "h"
527
+ ? 3_600_000
528
+ : 86_400_000;
529
+ return new Date(Date.now() + amount * multiplier).toISOString();
530
+ }
531
+
532
+ export function createRedisProviderRuntimeState(
533
+ options: RedisProviderRuntimeStateOptions,
534
+ ): ProviderRuntimeState {
535
+ return new RedisProviderRuntimeState(options);
536
+ }
537
+
538
+ export function createProviderRuntimeStateFromEnv(
539
+ options: {
540
+ readonly providerId?: string;
541
+ readonly allowMemoryFallback?: boolean;
542
+ } = {},
543
+ ): ProviderRuntimeState {
544
+ const redisUrl = providerStateRedisUrlFromEnv();
545
+ if (redisUrl) {
546
+ return createRedisProviderRuntimeState({
547
+ redisUrl,
548
+ providerId: options.providerId,
549
+ });
550
+ }
551
+ if (options.allowMemoryFallback === true) {
552
+ return new MemoryProviderRuntimeState();
553
+ }
554
+ return createUnsupportedProviderRuntimeState();
555
+ }
556
+
557
+ export function createMemoryProviderRuntimeState(): ProviderRuntimeState {
558
+ return new MemoryProviderRuntimeState();
559
+ }
560
+
561
+ export function createUnsupportedProviderRuntimeState(): ProviderRuntimeState {
562
+ return new UnsupportedProviderRuntimeState();
563
+ }