@apifuse/provider-sdk 2.2.0-beta.2 → 2.2.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/AUTHORING.md +487 -0
  2. package/CHANGELOG.md +90 -0
  3. package/README.md +49 -3
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-check.ts +44 -59
  6. package/bin/apifuse-create.ts +1 -1
  7. package/bin/apifuse-dev.ts +27 -52
  8. package/bin/apifuse-pack-check.ts +14 -0
  9. package/bin/apifuse-pack-smoke.ts +36 -81
  10. package/bin/apifuse-pack-types.ts +305 -0
  11. package/bin/apifuse-perf.ts +45 -127
  12. package/bin/apifuse-record.ts +659 -111
  13. package/bin/apifuse-submit-check.ts +546 -48
  14. package/bin/apifuse-sync-assets.ts +117 -0
  15. package/bin/apifuse.ts +1 -1
  16. package/bin/submit-check-delimited-text.ts +50 -0
  17. package/bin/submit-check-xml-semantics.ts +204 -0
  18. package/bin/submit-check-xml.ts +134 -0
  19. package/dist/auth-turn/index.d.ts +3 -3
  20. package/dist/auth.d.ts +2 -2
  21. package/dist/auth.js +9 -18
  22. package/dist/ceremonies/index.d.ts +9 -1
  23. package/dist/ceremonies/index.js +65 -18
  24. package/dist/cli/commands.d.ts +1 -1
  25. package/dist/cli/commands.js +8 -0
  26. package/dist/cli/create.d.ts +3 -0
  27. package/dist/cli/create.js +34 -35
  28. package/dist/cli/prompt-assets.d.ts +80 -0
  29. package/dist/cli/prompt-assets.js +743 -0
  30. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  31. package/dist/config/loader.d.ts +176 -8
  32. package/dist/config/loader.js +424 -95
  33. package/dist/contract-serialization.d.ts +2 -2
  34. package/dist/contract-serialization.js +3 -6
  35. package/dist/contract-types.d.ts +2 -2
  36. package/dist/contract.d.ts +3 -3
  37. package/dist/contract.js +4 -6
  38. package/dist/define.d.ts +9 -1
  39. package/dist/define.js +304 -119
  40. package/dist/dev.d.ts +1 -1
  41. package/dist/dev.js +1 -1
  42. package/dist/error-resolution.d.ts +2 -0
  43. package/dist/error-resolution.js +90 -0
  44. package/dist/errors.d.ts +22 -1
  45. package/dist/errors.js +88 -0
  46. package/dist/fixture-sanitization.d.ts +26 -0
  47. package/dist/fixture-sanitization.js +216 -0
  48. package/dist/i18n/catalog.d.ts +2 -2
  49. package/dist/i18n/catalog.js +4 -10
  50. package/dist/i18n/index.d.ts +2 -2
  51. package/dist/i18n/index.js +2 -2
  52. package/dist/i18n/keys.d.ts +2 -2
  53. package/dist/index.d.ts +46 -42
  54. package/dist/index.js +41 -37
  55. package/dist/lint.d.ts +1 -1
  56. package/dist/lint.js +8 -15
  57. package/dist/native-address.d.ts +43 -0
  58. package/dist/native-address.js +281 -0
  59. package/dist/native-egress-policy.d.ts +31 -0
  60. package/dist/native-egress-policy.js +288 -0
  61. package/dist/observability.d.ts +5 -2
  62. package/dist/observability.js +48 -1
  63. package/dist/provider.d.ts +12 -11
  64. package/dist/provider.js +10 -9
  65. package/dist/public-schema-field-lint.d.ts +1 -1
  66. package/dist/recipes/gov-api.js +1 -1
  67. package/dist/runtime/auth-flow.d.ts +2 -1
  68. package/dist/runtime/auth-flow.js +4 -3
  69. package/dist/runtime/browser.d.ts +1 -1
  70. package/dist/runtime/browser.js +15 -29
  71. package/dist/runtime/cache.d.ts +1 -1
  72. package/dist/runtime/cache.js +4 -8
  73. package/dist/runtime/choice.d.ts +1 -1
  74. package/dist/runtime/choice.js +31 -35
  75. package/dist/runtime/credential.d.ts +1 -1
  76. package/dist/runtime/credential.js +1 -1
  77. package/dist/runtime/env.d.ts +1 -1
  78. package/dist/runtime/executor.d.ts +1 -1
  79. package/dist/runtime/executor.js +31 -4
  80. package/dist/runtime/http.d.ts +2 -2
  81. package/dist/runtime/http.js +387 -47
  82. package/dist/runtime/insights.d.ts +1 -1
  83. package/dist/runtime/insights.js +6 -13
  84. package/dist/runtime/instrumentation.d.ts +2 -2
  85. package/dist/runtime/instrumentation.js +345 -22
  86. package/dist/runtime/keyring.js +1 -1
  87. package/dist/runtime/namespace.js +1 -1
  88. package/dist/runtime/native-network.d.ts +127 -0
  89. package/dist/runtime/native-network.js +1298 -0
  90. package/dist/runtime/otlp.d.ts +1 -1
  91. package/dist/runtime/perf.d.ts +1 -1
  92. package/dist/runtime/provider.d.ts +1 -1
  93. package/dist/runtime/provider.js +1 -2
  94. package/dist/runtime/proxy-errors.d.ts +1 -1
  95. package/dist/runtime/proxy-errors.js +9 -7
  96. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  97. package/dist/runtime/proxy-nodemaven.js +146 -0
  98. package/dist/runtime/proxy-retry-policy.d.ts +2 -2
  99. package/dist/runtime/proxy-retry-policy.js +2 -2
  100. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  101. package/dist/runtime/proxy-telemetry.js +55 -52
  102. package/dist/runtime/redirects.d.ts +29 -0
  103. package/dist/runtime/redirects.js +36 -0
  104. package/dist/runtime/redis.d.ts +1 -1
  105. package/dist/runtime/redis.js +2 -4
  106. package/dist/runtime/request-options.d.ts +68 -1
  107. package/dist/runtime/request-options.js +548 -0
  108. package/dist/runtime/secrets.d.ts +27 -0
  109. package/dist/runtime/secrets.js +51 -0
  110. package/dist/runtime/state.d.ts +2 -2
  111. package/dist/runtime/state.js +238 -26
  112. package/dist/runtime/stealth.d.ts +5 -3
  113. package/dist/runtime/stealth.js +423 -143
  114. package/dist/runtime/stt.d.ts +1 -1
  115. package/dist/runtime/stt.js +11 -15
  116. package/dist/runtime/trace.d.ts +2 -2
  117. package/dist/runtime/trace.js +2 -4
  118. package/dist/runtime/waterfall.d.ts +1 -1
  119. package/dist/schema.d.ts +1 -1
  120. package/dist/schema.js +7 -15
  121. package/dist/serve.d.ts +1 -1
  122. package/dist/serve.js +1 -1
  123. package/dist/server/index.d.ts +7 -7
  124. package/dist/server/index.js +6 -6
  125. package/dist/server/self-test-input-tokens.d.ts +2 -1
  126. package/dist/server/self-test-input-tokens.js +18 -14
  127. package/dist/server/self-test-redaction.d.ts +1 -1
  128. package/dist/server/self-test-redaction.js +1 -1
  129. package/dist/server/self-test.d.ts +104 -3
  130. package/dist/server/self-test.js +673 -115
  131. package/dist/server/serve.d.ts +116 -4
  132. package/dist/server/serve.js +799 -128
  133. package/dist/server/types.d.ts +34 -9
  134. package/dist/server/types.js +8 -1
  135. package/dist/stateful/errors.d.ts +14 -0
  136. package/dist/stateful/errors.js +14 -0
  137. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  138. package/dist/stateful/http-provider-event-emitter.js +237 -0
  139. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  140. package/dist/stateful/http-session-owner-registry.js +210 -0
  141. package/dist/stateful/index.d.ts +18 -0
  142. package/dist/stateful/index.js +18 -0
  143. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  144. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  145. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  146. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  147. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  148. package/dist/stateful/provider-event-pipeline.js +1 -0
  149. package/dist/stateful/provider-events.d.ts +101 -0
  150. package/dist/stateful/provider-events.js +289 -0
  151. package/dist/stateful/session-key.d.ts +15 -0
  152. package/dist/stateful/session-key.js +86 -0
  153. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  154. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  155. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  156. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  157. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  158. package/dist/stateful/stateful-provider-adapter.js +287 -0
  159. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  160. package/dist/stateful/stateful-provider-observability.js +161 -0
  161. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  162. package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
  163. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  164. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  165. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  166. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  167. package/dist/stateful/stateful-provider-session-routing.d.ts +71 -0
  168. package/dist/stateful/stateful-provider-session-routing.js +353 -0
  169. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  170. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  171. package/dist/stateful-signing.d.ts +18 -0
  172. package/dist/stateful-signing.js +27 -0
  173. package/dist/stealth/profiles.d.ts +1 -1
  174. package/dist/stealth/profiles.js +5 -14
  175. package/dist/stream-evidence.d.ts +74 -0
  176. package/dist/stream-evidence.js +785 -0
  177. package/dist/stream.d.ts +1 -1
  178. package/dist/testing/index.d.ts +2 -2
  179. package/dist/testing/index.js +2 -2
  180. package/dist/testing/run.d.ts +32 -2
  181. package/dist/testing/run.js +478 -28
  182. package/dist/types.d.ts +342 -14
  183. package/dist/types.js +1 -0
  184. package/dist/user-input.d.ts +30 -0
  185. package/dist/user-input.js +66 -0
  186. package/package.json +16 -5
  187. package/src/auth-turn/index.ts +1 -1
  188. package/src/auth.ts +28 -86
  189. package/src/ceremonies/index.ts +103 -78
  190. package/src/cli/commands.ts +10 -0
  191. package/src/cli/create.ts +42 -35
  192. package/src/cli/prompt-assets.ts +865 -0
  193. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  194. package/src/config/loader.ts +652 -204
  195. package/src/contract-serialization.ts +5 -11
  196. package/src/contract-types.ts +2 -2
  197. package/src/contract.ts +12 -28
  198. package/src/define.ts +499 -498
  199. package/src/dev.ts +4 -9
  200. package/src/error-resolution.ts +91 -0
  201. package/src/errors.ts +110 -5
  202. package/src/fixture-sanitization.ts +247 -0
  203. package/src/i18n/catalog.ts +10 -32
  204. package/src/i18n/index.ts +2 -2
  205. package/src/i18n/keys.ts +5 -11
  206. package/src/index.ts +112 -42
  207. package/src/lint.ts +88 -152
  208. package/src/native-address.ts +340 -0
  209. package/src/native-egress-policy.ts +358 -0
  210. package/src/observability.ts +51 -1
  211. package/src/provider.ts +66 -11
  212. package/src/public-schema-field-lint.ts +7 -33
  213. package/src/recipes/gov-api.ts +2 -5
  214. package/src/runtime/auth-flow.ts +7 -7
  215. package/src/runtime/browser.ts +64 -187
  216. package/src/runtime/cache.ts +20 -67
  217. package/src/runtime/choice.ts +79 -132
  218. package/src/runtime/credential.ts +2 -2
  219. package/src/runtime/env.ts +1 -1
  220. package/src/runtime/executor.ts +43 -20
  221. package/src/runtime/http.ts +494 -57
  222. package/src/runtime/insights.ts +15 -53
  223. package/src/runtime/instrumentation.ts +495 -66
  224. package/src/runtime/keyring.ts +7 -19
  225. package/src/runtime/namespace.ts +2 -7
  226. package/src/runtime/native-network.ts +1686 -0
  227. package/src/runtime/otlp.ts +12 -23
  228. package/src/runtime/perf.ts +1 -1
  229. package/src/runtime/provider.ts +4 -9
  230. package/src/runtime/proxy-errors.ts +29 -42
  231. package/src/runtime/proxy-nodemaven.ts +221 -0
  232. package/src/runtime/proxy-retry-policy.ts +3 -3
  233. package/src/runtime/proxy-telemetry.ts +79 -77
  234. package/src/runtime/redirects.ts +66 -0
  235. package/src/runtime/redis.ts +4 -12
  236. package/src/runtime/request-options.ts +679 -9
  237. package/src/runtime/secrets.ts +64 -0
  238. package/src/runtime/state.ts +353 -133
  239. package/src/runtime/stealth.ts +505 -154
  240. package/src/runtime/stt.ts +38 -94
  241. package/src/runtime/trace.ts +14 -44
  242. package/src/runtime/waterfall.ts +5 -18
  243. package/src/schema.ts +23 -84
  244. package/src/serve.ts +1 -1
  245. package/src/server/index.ts +29 -7
  246. package/src/server/self-test-input-tokens.ts +29 -14
  247. package/src/server/self-test-redaction.ts +2 -2
  248. package/src/server/self-test.ts +857 -132
  249. package/src/server/serve.ts +1151 -328
  250. package/src/server/types.ts +12 -13
  251. package/src/stateful/README.md +146 -0
  252. package/src/stateful/errors.ts +23 -0
  253. package/src/stateful/http-provider-event-emitter.ts +314 -0
  254. package/src/stateful/http-session-owner-registry.ts +306 -0
  255. package/src/stateful/index.ts +18 -0
  256. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  257. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  258. package/src/stateful/provider-event-pipeline.ts +61 -0
  259. package/src/stateful/provider-events.ts +462 -0
  260. package/src/stateful/session-key.ts +111 -0
  261. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  262. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  263. package/src/stateful/stateful-provider-adapter.ts +562 -0
  264. package/src/stateful/stateful-provider-observability.ts +261 -0
  265. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  266. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  267. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  268. package/src/stateful/stateful-provider-session-routing.ts +555 -0
  269. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  270. package/src/stateful-signing.ts +46 -0
  271. package/src/stealth/profiles.ts +10 -26
  272. package/src/stream-evidence.ts +988 -0
  273. package/src/stream.ts +8 -19
  274. package/src/testing/index.ts +10 -2
  275. package/src/testing/run.ts +653 -74
  276. package/src/types.ts +408 -28
  277. package/src/user-input.ts +118 -0
  278. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  279. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  280. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  281. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  282. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  283. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  284. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  285. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  286. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  287. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  288. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  289. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  290. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  291. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
@@ -0,0 +1,403 @@
1
+ import type { SessionKey } from "./session-key.js";
2
+
3
+ /** @deprecated Use SessionKey from session-key.ts. */
4
+ export type StatefulProviderSessionKey = SessionKey;
5
+ export type SessionOwnerStatus = "acquiring" | "connected" | "draining" | "expired";
6
+
7
+ export interface SessionOwnerRecord {
8
+ /** Runtime-decoded key; registry inputs use the opaque SessionKey type. */
9
+ readonly sessionKey: string;
10
+ readonly ownerPodId: string;
11
+ readonly ownerEndpoint: string;
12
+ readonly generation: number;
13
+ readonly leaseExpiresAt: string;
14
+ readonly status: SessionOwnerStatus;
15
+ readonly lastUsedAt: string;
16
+ }
17
+
18
+ export interface AcquireSessionOwnerInput {
19
+ readonly sessionKey: SessionKey;
20
+ readonly ownerPodId: string;
21
+ readonly ownerEndpoint: string;
22
+ readonly leaseDurationMs: number;
23
+ readonly status?: Exclude<SessionOwnerStatus, "expired">;
24
+ readonly now?: Date;
25
+ }
26
+
27
+ export interface AcquireSessionOwnerResult {
28
+ readonly record: SessionOwnerRecord;
29
+ readonly acquired: boolean;
30
+ }
31
+
32
+ export interface RenewSessionOwnerInput {
33
+ readonly sessionKey: SessionKey;
34
+ readonly ownerPodId: string;
35
+ readonly generation: number;
36
+ readonly leaseDurationMs: number;
37
+ readonly status?: Exclude<SessionOwnerStatus, "expired">;
38
+ readonly now?: Date;
39
+ }
40
+
41
+ export interface ReleaseSessionOwnerInput {
42
+ readonly sessionKey: SessionKey;
43
+ readonly ownerPodId: string;
44
+ readonly generation: number;
45
+ }
46
+
47
+ /**
48
+ * A session-owner registry is a fencing authority. Generations MUST be positive integers and
49
+ * strictly increase for each successful takeover of a session key, including after expiry or
50
+ * release. Implementations must retain a high-water mark (a tombstone) for at least the registry's
51
+ * lifetime; generation values must never be reused.
52
+ */
53
+ export interface SessionOwnerRegistry {
54
+ resolve(
55
+ sessionKey: SessionKey,
56
+ now?: Date,
57
+ signal?: AbortSignal,
58
+ ): Promise<SessionOwnerRecord | null>;
59
+ acquire(
60
+ input: AcquireSessionOwnerInput,
61
+ signal?: AbortSignal,
62
+ ): Promise<AcquireSessionOwnerResult>;
63
+ renew(input: RenewSessionOwnerInput, signal?: AbortSignal): Promise<SessionOwnerRecord | null>;
64
+ release(input: ReleaseSessionOwnerInput, signal?: AbortSignal): Promise<boolean>;
65
+ }
66
+
67
+ export interface SessionPoolPolicy {
68
+ readonly maxSessions: number;
69
+ /** Disable idle eviction for connection-owned listeners with `"unlimited"`. */
70
+ readonly idleTimeoutMs: number | "unlimited";
71
+ /** Disable age-based recycling for expensive healthy sessions with `"unlimited"`. */
72
+ readonly maxLifetimeMs: number | "unlimited";
73
+ }
74
+
75
+ export interface ManagedSessionIdentity {
76
+ readonly connectionId: string;
77
+ readonly serviceAccountId: string;
78
+ readonly ownerPodId: string;
79
+ readonly ownerEndpoint: string;
80
+ readonly ownerStatus: SessionOwnerStatus;
81
+ }
82
+
83
+ export interface ManagedSession<T> {
84
+ readonly sessionKey: string;
85
+ readonly generation: number;
86
+ readonly value: T;
87
+ readonly createdAt: string;
88
+ readonly lastUsedAt: string;
89
+ readonly identity?: ManagedSessionIdentity;
90
+ }
91
+
92
+ type SessionFactory<T> = () => T | Promise<T>;
93
+ type SessionCloseHook<T> = (session: ManagedSession<T>, reason: string) => void | Promise<void>;
94
+ export class InMemorySessionOwnerRegistry implements SessionOwnerRegistry {
95
+ readonly #owners = new Map<SessionKey, SessionOwnerRecord>();
96
+ readonly #generationHighWater = new Map<SessionKey, number>();
97
+
98
+ async resolve(
99
+ sessionKey: SessionKey,
100
+ now: Date = new Date(),
101
+ signal?: AbortSignal,
102
+ ): Promise<SessionOwnerRecord | null> {
103
+ signal?.throwIfAborted();
104
+ const current = this.#owners.get(sessionKey);
105
+ if (!current || isLeaseExpired(current, now)) return null;
106
+ validateGeneration(current.generation);
107
+ return current;
108
+ }
109
+
110
+ async acquire(
111
+ input: AcquireSessionOwnerInput,
112
+ signal?: AbortSignal,
113
+ ): Promise<AcquireSessionOwnerResult> {
114
+ signal?.throwIfAborted();
115
+ validateLeaseDuration(input.leaseDurationMs);
116
+ const now = input.now ?? new Date();
117
+ const current = this.#owners.get(input.sessionKey);
118
+ if (current && !isLeaseExpired(current, now)) {
119
+ validateGeneration(current.generation);
120
+ if (current.ownerPodId !== input.ownerPodId) {
121
+ return { record: current, acquired: false };
122
+ }
123
+ const record = makeOwnerRecord(input, current.generation, now);
124
+ this.#owners.set(input.sessionKey, record);
125
+ return { record, acquired: true };
126
+ }
127
+
128
+ const generation =
129
+ Math.max(current?.generation ?? 0, this.#generationHighWater.get(input.sessionKey) ?? 0) + 1;
130
+ const record = makeOwnerRecord(input, generation, now);
131
+ this.#owners.set(input.sessionKey, record);
132
+ this.#generationHighWater.set(input.sessionKey, generation);
133
+ return { record, acquired: true };
134
+ }
135
+
136
+ async renew(
137
+ input: RenewSessionOwnerInput,
138
+ signal?: AbortSignal,
139
+ ): Promise<SessionOwnerRecord | null> {
140
+ signal?.throwIfAborted();
141
+ validateGeneration(input.generation);
142
+ validateLeaseDuration(input.leaseDurationMs);
143
+ const now = input.now ?? new Date();
144
+ const current = this.#owners.get(input.sessionKey);
145
+ if (
146
+ !current ||
147
+ current.ownerPodId !== input.ownerPodId ||
148
+ current.generation !== input.generation ||
149
+ isLeaseExpired(current, now)
150
+ ) {
151
+ return null;
152
+ }
153
+
154
+ const record: SessionOwnerRecord = {
155
+ ...current,
156
+ leaseExpiresAt: addMs(now, input.leaseDurationMs).toISOString(),
157
+ status: input.status ?? current.status,
158
+ lastUsedAt: now.toISOString(),
159
+ };
160
+ this.#owners.set(input.sessionKey, record);
161
+ return record;
162
+ }
163
+
164
+ async release(input: ReleaseSessionOwnerInput, signal?: AbortSignal): Promise<boolean> {
165
+ signal?.throwIfAborted();
166
+ validateGeneration(input.generation);
167
+ const current = this.#owners.get(input.sessionKey);
168
+ if (
169
+ !current ||
170
+ current.ownerPodId !== input.ownerPodId ||
171
+ current.generation !== input.generation
172
+ ) {
173
+ return false;
174
+ }
175
+ this.#generationHighWater.set(
176
+ input.sessionKey,
177
+ Math.max(this.#generationHighWater.get(input.sessionKey) ?? 0, current.generation),
178
+ );
179
+ this.#owners.delete(input.sessionKey);
180
+ return true;
181
+ }
182
+ }
183
+
184
+ export class PodLocalSessionPool<T> {
185
+ readonly #sessions = new Map<string, ManagedSession<T>>();
186
+ readonly #queues = new Map<string, Promise<void>>();
187
+ readonly #creates = new Map<string, Promise<ManagedSession<T>>>();
188
+ #closed = false;
189
+
190
+ constructor(
191
+ private readonly policy: SessionPoolPolicy,
192
+ private readonly closeSession: SessionCloseHook<T>,
193
+ ) {
194
+ validatePoolPolicy(policy);
195
+ }
196
+
197
+ async getOrCreate(
198
+ sessionKey: string,
199
+ generation: number,
200
+ factory: SessionFactory<T>,
201
+ now: Date = new Date(),
202
+ identity?: ManagedSessionIdentity,
203
+ ): Promise<ManagedSession<T>> {
204
+ validateGeneration(generation);
205
+ this.assertOpen(sessionKey);
206
+ const existingCreate = this.#creates.get(sessionKey);
207
+ if (existingCreate) {
208
+ try {
209
+ await existingCreate;
210
+ } catch {}
211
+ this.assertOpen(sessionKey);
212
+ }
213
+ const create = this.getOrCreateUnlocked(sessionKey, generation, factory, now, identity);
214
+ this.#creates.set(sessionKey, create);
215
+ try {
216
+ const session = await create;
217
+ this.assertOpen(sessionKey);
218
+ return session;
219
+ } finally {
220
+ if (this.#creates.get(sessionKey) === create) this.#creates.delete(sessionKey);
221
+ }
222
+ }
223
+
224
+ private async getOrCreateUnlocked(
225
+ sessionKey: string,
226
+ generation: number,
227
+ factory: SessionFactory<T>,
228
+ now: Date,
229
+ identity?: ManagedSessionIdentity,
230
+ ): Promise<ManagedSession<T>> {
231
+ await this.evictExpired(now);
232
+
233
+ const current = this.#sessions.get(sessionKey);
234
+ if (current && current.generation === generation) {
235
+ const touched = { ...current, lastUsedAt: now.toISOString() };
236
+ this.#sessions.delete(sessionKey);
237
+ this.#sessions.set(sessionKey, touched);
238
+ return touched;
239
+ }
240
+ if (current) await this.closeOne(sessionKey, "generation-changed");
241
+
242
+ const session: ManagedSession<T> = {
243
+ sessionKey,
244
+ generation,
245
+ value: await factory(),
246
+ createdAt: now.toISOString(),
247
+ lastUsedAt: now.toISOString(),
248
+ ...(identity ? { identity } : {}),
249
+ };
250
+ this.#sessions.set(sessionKey, session);
251
+ await this.evictOverCapacity();
252
+ return session;
253
+ }
254
+
255
+ async closeAll(reason: string): Promise<void> {
256
+ this.#closed = true;
257
+ const errors: unknown[] = [];
258
+ await Promise.all(
259
+ [...this.#creates.values()].map(async (create) => {
260
+ let session: ManagedSession<T>;
261
+ try {
262
+ session = await create;
263
+ } catch {
264
+ return;
265
+ }
266
+ try {
267
+ await this.closeOne(session.sessionKey, reason);
268
+ } catch (error) {
269
+ errors.push(error);
270
+ }
271
+ }),
272
+ );
273
+ for (const sessionKey of [...this.#sessions.keys()]) {
274
+ try {
275
+ await this.closeOne(sessionKey, reason);
276
+ } catch (error) {
277
+ errors.push(error);
278
+ }
279
+ }
280
+ if (errors.length > 0) {
281
+ throw new AggregateError(errors, `Failed to close ${errors.length} stateful session(s).`);
282
+ }
283
+ }
284
+
285
+ async invalidate(sessionKey: string, reason: string): Promise<void> {
286
+ await this.closeOne(sessionKey, reason);
287
+ }
288
+
289
+ async runExclusive<R>(sessionKey: string, task: () => R | Promise<R>): Promise<R> {
290
+ const previous = this.#queues.get(sessionKey) ?? Promise.resolve();
291
+ const next = previous.then(task, task);
292
+ const settled = next.then(
293
+ () => undefined,
294
+ () => undefined,
295
+ );
296
+ this.#queues.set(sessionKey, settled);
297
+ await settled.finally(() => {
298
+ if (this.#queues.get(sessionKey) === settled) {
299
+ this.#queues.delete(sessionKey);
300
+ }
301
+ });
302
+ return next;
303
+ }
304
+
305
+ private async evictExpired(now: Date): Promise<void> {
306
+ for (const [sessionKey, session] of this.#sessions) {
307
+ if (isSessionExpired(session, this.policy, now)) {
308
+ await this.closeOne(sessionKey, "expired");
309
+ }
310
+ }
311
+ }
312
+
313
+ private async evictOverCapacity(): Promise<void> {
314
+ while (this.#sessions.size > this.policy.maxSessions) {
315
+ const lruKey = this.#sessions.keys().next().value;
316
+ if (lruKey === undefined) return;
317
+ await this.closeOne(lruKey, "capacity");
318
+ }
319
+ }
320
+
321
+ private async closeOne(sessionKey: string, reason: string): Promise<void> {
322
+ const session = this.#sessions.get(sessionKey);
323
+ if (!session) return;
324
+ this.#sessions.delete(sessionKey);
325
+ await this.closeSession(session, reason);
326
+ }
327
+
328
+ private assertOpen(sessionKey: string): void {
329
+ if (!this.#closed) return;
330
+ throw new Error(
331
+ `Pod-local session pool is closed; cannot get or create session "${sessionKey}".`,
332
+ );
333
+ }
334
+ }
335
+
336
+ function makeOwnerRecord(
337
+ input: AcquireSessionOwnerInput,
338
+ generation: number,
339
+ now: Date,
340
+ ): SessionOwnerRecord {
341
+ validateGeneration(generation);
342
+ return {
343
+ sessionKey: input.sessionKey,
344
+ ownerPodId: input.ownerPodId,
345
+ ownerEndpoint: input.ownerEndpoint,
346
+ generation,
347
+ leaseExpiresAt: addMs(now, input.leaseDurationMs).toISOString(),
348
+ status: input.status ?? "acquiring",
349
+ lastUsedAt: now.toISOString(),
350
+ };
351
+ }
352
+
353
+ function isLeaseExpired(record: SessionOwnerRecord, now: Date): boolean {
354
+ return Date.parse(record.leaseExpiresAt) <= now.getTime();
355
+ }
356
+
357
+ function isSessionExpired<T>(
358
+ session: ManagedSession<T>,
359
+ policy: SessionPoolPolicy,
360
+ now: Date,
361
+ ): boolean {
362
+ const nowMs = now.getTime();
363
+ return (
364
+ (policy.idleTimeoutMs !== "unlimited" &&
365
+ nowMs - Date.parse(session.lastUsedAt) >= policy.idleTimeoutMs) ||
366
+ (policy.maxLifetimeMs !== "unlimited" &&
367
+ nowMs - Date.parse(session.createdAt) >= policy.maxLifetimeMs)
368
+ );
369
+ }
370
+
371
+ function validateLeaseDuration(leaseDurationMs: number): void {
372
+ if (!Number.isFinite(leaseDurationMs) || leaseDurationMs <= 0) {
373
+ throw new Error("Session owner leaseDurationMs must be a positive finite number.");
374
+ }
375
+ }
376
+
377
+ function validateGeneration(generation: number): void {
378
+ if (!Number.isInteger(generation) || generation <= 0) {
379
+ throw new Error("Session owner generation must be a positive integer.");
380
+ }
381
+ }
382
+
383
+ function validatePoolPolicy(policy: SessionPoolPolicy): void {
384
+ if (!Number.isInteger(policy.maxSessions) || policy.maxSessions <= 0) {
385
+ throw new Error("Session pool maxSessions must be a positive integer.");
386
+ }
387
+ if (
388
+ policy.idleTimeoutMs !== "unlimited" &&
389
+ (!Number.isFinite(policy.idleTimeoutMs) || policy.idleTimeoutMs <= 0)
390
+ ) {
391
+ throw new Error('Session pool idleTimeoutMs must be a positive finite number or "unlimited".');
392
+ }
393
+ if (
394
+ policy.maxLifetimeMs !== "unlimited" &&
395
+ (!Number.isFinite(policy.maxLifetimeMs) || policy.maxLifetimeMs <= 0)
396
+ ) {
397
+ throw new Error('Session pool maxLifetimeMs must be a positive finite number or "unlimited".');
398
+ }
399
+ }
400
+
401
+ function addMs(date: Date, ms: number): Date {
402
+ return new Date(date.getTime() + ms);
403
+ }
@@ -0,0 +1,46 @@
1
+ import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
2
+
3
+ export const STATEFUL_SIGNATURE_HEADER = "x-apifuse-stateful-signature";
4
+ export const STATEFUL_TIMESTAMP_HEADER = "x-apifuse-stateful-timestamp";
5
+ export const STATEFUL_NONCE_HEADER = "x-apifuse-stateful-nonce";
6
+
7
+ export type StatefulSigningInput = {
8
+ readonly secret: string;
9
+ readonly timestamp: string;
10
+ readonly rawBody: string;
11
+ readonly method: string;
12
+ readonly path: string;
13
+ readonly nonce: string;
14
+ };
15
+
16
+ export function signStatefulRequestBody(input: StatefulSigningInput): string {
17
+ return `v1=${createHmac("sha256", input.secret)
18
+ .update(
19
+ `v1:${input.method.toUpperCase()}:${input.path}:${input.timestamp}:${input.nonce}.${input.rawBody}`,
20
+ )
21
+ .digest("hex")}`;
22
+ }
23
+
24
+ export function verifyStatefulRequestSignature(
25
+ input: StatefulSigningInput & { readonly signature: string },
26
+ ): boolean {
27
+ return safeEqualAscii(input.signature, signStatefulRequestBody(input));
28
+ }
29
+
30
+ export function statefulSignedHeaders(
31
+ input: Omit<StatefulSigningInput, "nonce"> & { readonly nonce?: string },
32
+ ): Record<string, string> {
33
+ const nonce = input.nonce ?? randomUUID();
34
+ return {
35
+ [STATEFUL_SIGNATURE_HEADER]: signStatefulRequestBody({ ...input, nonce }),
36
+ [STATEFUL_TIMESTAMP_HEADER]: input.timestamp,
37
+ [STATEFUL_NONCE_HEADER]: nonce,
38
+ };
39
+ }
40
+
41
+ function safeEqualAscii(actual: string, expected: string): boolean {
42
+ const actualBytes = Buffer.from(actual);
43
+ const expectedBytes = Buffer.from(expected);
44
+ if (actualBytes.byteLength !== expectedBytes.byteLength) return false;
45
+ return timingSafeEqual(actualBytes, expectedBytes);
46
+ }
@@ -1,5 +1,5 @@
1
- import { SDKError } from "../errors";
2
- import type { StealthPlatform, StealthProfile } from "../types";
1
+ import { SDKError } from "../errors.js";
2
+ import type { StealthPlatform, StealthProfile } from "../types.js";
3
3
 
4
4
  type StealthProfileDefinition = Omit<StealthProfile, "name" | "platform"> & {
5
5
  platform: StealthPlatform;
@@ -81,10 +81,7 @@ const FIREFOX_JA3 =
81
81
  const SAFARI_JA3 =
82
82
  "771,4865-4866-4867-49196-49195-52393-49200-49199-49188-49192-159-158-107-103-57-51-157-156-61-60-53-47-255,0-23-65281-10-11-16-5-13-18-51-45-43-27,29-23-24-25,0";
83
83
 
84
- function createProfile(
85
- name: string,
86
- definition: StealthProfileDefinition,
87
- ): StealthProfile {
84
+ function createProfile(name: string, definition: StealthProfileDefinition): StealthProfile {
88
85
  return {
89
86
  name,
90
87
  platform: definition.platform,
@@ -93,12 +90,8 @@ function createProfile(
93
90
  tlsClientIdentifier: definition.tlsClientIdentifier,
94
91
  ja3: definition.ja3,
95
92
  ja4: definition.ja4,
96
- h2Settings: definition.h2Settings
97
- ? { ...definition.h2Settings }
98
- : undefined,
99
- headerOrder: definition.headerOrder
100
- ? [...definition.headerOrder]
101
- : undefined,
93
+ h2Settings: definition.h2Settings ? { ...definition.h2Settings } : undefined,
94
+ headerOrder: definition.headerOrder ? [...definition.headerOrder] : undefined,
102
95
  };
103
96
  }
104
97
 
@@ -108,8 +101,7 @@ function extractBrowserMajorVersion(profile: StealthProfile): string {
108
101
  return chromeVersion;
109
102
  }
110
103
 
111
- const identifierVersion =
112
- profile.tlsClientIdentifier?.match(/(\d+)(?!.*\d)/)?.[1];
104
+ const identifierVersion = profile.tlsClientIdentifier?.match(/(\d+)(?!.*\d)/)?.[1];
113
105
  if (identifierVersion) {
114
106
  return identifierVersion;
115
107
  }
@@ -132,9 +124,7 @@ function toPlatformHeaderValue(platform: StealthPlatform): string {
132
124
  }
133
125
  }
134
126
 
135
- export function generateLayer2Headers(
136
- profile: StealthProfile,
137
- ): Record<string, string> {
127
+ export function generateLayer2Headers(profile: StealthProfile): Record<string, string> {
138
128
  const headers: Record<string, string> = {
139
129
  "Accept-Language": "ko-KR,ko;q=0.9,en-US;q=0.8",
140
130
  };
@@ -143,16 +133,13 @@ export function generateLayer2Headers(
143
133
  const majorVersion = extractBrowserMajorVersion(profile);
144
134
 
145
135
  if (identifier.startsWith("chrome_") || identifier.startsWith("edge_")) {
146
- const isEdge =
147
- identifier.startsWith("edge_") || /\bEdg\//.test(profile.userAgent);
136
+ const isEdge = identifier.startsWith("edge_") || /\bEdg\//.test(profile.userAgent);
148
137
  headers["Sec-Ch-Ua"] = isEdge
149
138
  ? `"Chromium";v="${majorVersion}", "Microsoft Edge";v="${majorVersion}", "Not)A;Brand";v="99"`
150
139
  : `"Chromium";v="${majorVersion}", "Google Chrome";v="${majorVersion}", "Not)A;Brand";v="99"`;
151
140
  headers["Sec-Ch-Ua-Platform"] = toPlatformHeaderValue(profile.platform);
152
141
  headers["Sec-Ch-Ua-Mobile"] =
153
- profile.platform === "android" || profile.platform === "ios"
154
- ? "?1"
155
- : "?0";
142
+ profile.platform === "android" || profile.platform === "ios" ? "?1" : "?0";
156
143
  }
157
144
 
158
145
  return headers;
@@ -301,8 +288,5 @@ export function getStealthProfile(name: string): StealthProfile {
301
288
  }
302
289
 
303
290
  export function listStealthProfiles(): string[] {
304
- return [
305
- ...Object.keys(STEALTH_PROFILES),
306
- ...Object.keys(STEALTH_PROFILE_ALIASES),
307
- ];
291
+ return [...Object.keys(STEALTH_PROFILES), ...Object.keys(STEALTH_PROFILE_ALIASES)];
308
292
  }