@apifuse/provider-sdk 2.2.0-beta.10 → 2.2.0-beta.12

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 (83) hide show
  1. package/AUTHORING.md +37 -0
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +18 -0
  4. package/bin/apifuse-pack-smoke.ts +14 -0
  5. package/bin/apifuse-pack-types.ts +11 -1
  6. package/dist/config/loader.d.ts +9 -1
  7. package/dist/config/loader.js +9 -0
  8. package/dist/errors.d.ts +5 -0
  9. package/dist/errors.js +15 -0
  10. package/dist/index.d.ts +3 -3
  11. package/dist/index.js +1 -1
  12. package/dist/runtime/stealth.js +113 -47
  13. package/dist/server/index.d.ts +1 -1
  14. package/dist/server/index.js +1 -1
  15. package/dist/server/serve.d.ts +98 -2
  16. package/dist/server/serve.js +485 -23
  17. package/dist/stateful/errors.d.ts +14 -0
  18. package/dist/stateful/errors.js +14 -0
  19. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  20. package/dist/stateful/http-provider-event-emitter.js +237 -0
  21. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  22. package/dist/stateful/http-session-owner-registry.js +210 -0
  23. package/dist/stateful/index.d.ts +18 -0
  24. package/dist/stateful/index.js +18 -0
  25. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  26. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  27. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  28. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  29. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  30. package/dist/stateful/provider-event-pipeline.js +1 -0
  31. package/dist/stateful/provider-events.d.ts +101 -0
  32. package/dist/stateful/provider-events.js +289 -0
  33. package/dist/stateful/session-key.d.ts +15 -0
  34. package/dist/stateful/session-key.js +86 -0
  35. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  36. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  37. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  38. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  39. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  40. package/dist/stateful/stateful-provider-adapter.js +287 -0
  41. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  42. package/dist/stateful/stateful-provider-observability.js +161 -0
  43. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  44. package/dist/stateful/stateful-provider-owner-forwarder.js +207 -0
  45. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  46. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  47. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  48. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  49. package/dist/stateful/stateful-provider-session-routing.d.ts +71 -0
  50. package/dist/stateful/stateful-provider-session-routing.js +353 -0
  51. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  52. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  53. package/dist/stateful-signing.d.ts +18 -0
  54. package/dist/stateful-signing.js +27 -0
  55. package/dist/types.d.ts +39 -7
  56. package/package.json +7 -1
  57. package/src/config/loader.ts +22 -1
  58. package/src/errors.ts +15 -0
  59. package/src/index.ts +8 -1
  60. package/src/runtime/stealth.ts +127 -48
  61. package/src/server/index.ts +13 -1
  62. package/src/server/serve.ts +691 -25
  63. package/src/stateful/README.md +146 -0
  64. package/src/stateful/errors.ts +23 -0
  65. package/src/stateful/http-provider-event-emitter.ts +314 -0
  66. package/src/stateful/http-session-owner-registry.ts +306 -0
  67. package/src/stateful/index.ts +18 -0
  68. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  69. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  70. package/src/stateful/provider-event-pipeline.ts +61 -0
  71. package/src/stateful/provider-events.ts +462 -0
  72. package/src/stateful/session-key.ts +111 -0
  73. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  74. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  75. package/src/stateful/stateful-provider-adapter.ts +562 -0
  76. package/src/stateful/stateful-provider-observability.ts +261 -0
  77. package/src/stateful/stateful-provider-owner-forwarder.ts +279 -0
  78. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  79. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  80. package/src/stateful/stateful-provider-session-routing.ts +555 -0
  81. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  82. package/src/stateful-signing.ts +46 -0
  83. package/src/types.ts +41 -7
@@ -0,0 +1,18 @@
1
+ export declare const STATEFUL_SIGNATURE_HEADER = "x-apifuse-stateful-signature";
2
+ export declare const STATEFUL_TIMESTAMP_HEADER = "x-apifuse-stateful-timestamp";
3
+ export declare const STATEFUL_NONCE_HEADER = "x-apifuse-stateful-nonce";
4
+ export type StatefulSigningInput = {
5
+ readonly secret: string;
6
+ readonly timestamp: string;
7
+ readonly rawBody: string;
8
+ readonly method: string;
9
+ readonly path: string;
10
+ readonly nonce: string;
11
+ };
12
+ export declare function signStatefulRequestBody(input: StatefulSigningInput): string;
13
+ export declare function verifyStatefulRequestSignature(input: StatefulSigningInput & {
14
+ readonly signature: string;
15
+ }): boolean;
16
+ export declare function statefulSignedHeaders(input: Omit<StatefulSigningInput, "nonce"> & {
17
+ readonly nonce?: string;
18
+ }): Record<string, string>;
@@ -0,0 +1,27 @@
1
+ import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
2
+ export const STATEFUL_SIGNATURE_HEADER = "x-apifuse-stateful-signature";
3
+ export const STATEFUL_TIMESTAMP_HEADER = "x-apifuse-stateful-timestamp";
4
+ export const STATEFUL_NONCE_HEADER = "x-apifuse-stateful-nonce";
5
+ export function signStatefulRequestBody(input) {
6
+ return `v1=${createHmac("sha256", input.secret)
7
+ .update(`v1:${input.method.toUpperCase()}:${input.path}:${input.timestamp}:${input.nonce}.${input.rawBody}`)
8
+ .digest("hex")}`;
9
+ }
10
+ export function verifyStatefulRequestSignature(input) {
11
+ return safeEqualAscii(input.signature, signStatefulRequestBody(input));
12
+ }
13
+ export function statefulSignedHeaders(input) {
14
+ const nonce = input.nonce ?? randomUUID();
15
+ return {
16
+ [STATEFUL_SIGNATURE_HEADER]: signStatefulRequestBody({ ...input, nonce }),
17
+ [STATEFUL_TIMESTAMP_HEADER]: input.timestamp,
18
+ [STATEFUL_NONCE_HEADER]: nonce,
19
+ };
20
+ }
21
+ function safeEqualAscii(actual, expected) {
22
+ const actualBytes = Buffer.from(actual);
23
+ const expectedBytes = Buffer.from(expected);
24
+ if (actualBytes.byteLength !== expectedBytes.byteLength)
25
+ return false;
26
+ return timingSafeEqual(actualBytes, expectedBytes);
27
+ }
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type ms from "ms";
2
+ import type { SerializedCookieJar } from "tough-cookie";
2
3
  import type { infer as ZodInfer, ZodType } from "zod";
3
4
  /** Minimal Standard Schema v1 shape accepted by provider operations. */
4
5
  export interface StandardSchemaV1<Input = unknown, Output = Input> {
@@ -872,17 +873,42 @@ export interface StealthFetchOptions extends RequestOptions {
872
873
  };
873
874
  }
874
875
  export interface CookieJar {
875
- get(name: string): string | undefined;
876
- getAll(): Record<string, string>;
877
- toString(): string;
878
- find?(predicate: (cookie: string) => boolean): string | undefined;
876
+ /** URL-less reads use the jar's response URL or session base URL. */
877
+ get(name: string, url?: string): string | undefined;
878
+ getAll(url?: string): Record<string, string>;
879
+ toString(url?: string): string;
880
+ find?(predicate: (cookie: string) => boolean, url?: string): string | undefined;
879
881
  }
882
+ /**
883
+ * Version 1 of the JSON-safe, attribute-preserving stealth cookie store.
884
+ * The nested jar is tough-cookie's serialized form and retains cookie origin,
885
+ * Path, Secure, expiry, host-only, and other RFC attributes.
886
+ */
887
+ export interface StealthCookieStoreV1 {
888
+ readonly version: 1;
889
+ readonly jar: SerializedCookieJar;
890
+ }
891
+ /** Cookie persistence formats understood by this SDK version. */
892
+ export type StealthCookieStore = StealthCookieStoreV1;
880
893
  export interface StealthSessionCookies extends CookieJar {
881
- has(name: string): boolean;
882
- setFromCookieStrings(cookieStrings: readonly string[]): void;
883
- toHeader(): string;
894
+ has(name: string, url?: string): boolean;
895
+ /** URL-less writes are scoped to the session base URL. */
896
+ setFromCookieStrings(cookieStrings: readonly string[], url?: string): void;
897
+ toHeader(url?: string): string;
898
+ /**
899
+ * Returns every cookie as a flat name/value map, collapsing duplicate names.
900
+ * @deprecated Use serialize() for lossless, attribute-preserving persistence.
901
+ */
884
902
  snapshot(): Record<string, string>;
903
+ /**
904
+ * Restores flat values as host-only, Path=/ cookies on the session base URL.
905
+ * @deprecated Use deserialize() with state produced by serialize().
906
+ */
885
907
  restore(cookies: Record<string, string>): void;
908
+ /** Returns a versioned, JSON-safe, attribute-preserving representation of every cookie. */
909
+ serialize(): StealthCookieStoreV1;
910
+ /** Replaces the jar with a previously serialized, attribute-preserving cookie store. */
911
+ deserialize(state: StealthCookieStore): void;
886
912
  clear(): void;
887
913
  }
888
914
  export interface DeclarativeStealthResponse {
@@ -925,7 +951,13 @@ export interface StealthRedirectRunResult {
925
951
  final: StealthResponse;
926
952
  hops: StealthRedirectHop[];
927
953
  reason: "completed" | "stopped" | "max_hops" | "missing_location" | "loop";
954
+ /**
955
+ * Complete flat view across all redirect hosts. Attributes and duplicate names are lost.
956
+ * @deprecated Use cookieStore for lossless persistence.
957
+ */
928
958
  cookies: Record<string, string>;
959
+ /** Versioned, attribute-preserving cookie state accumulated across the redirect chain. */
960
+ cookieStore: StealthCookieStoreV1;
929
961
  }
930
962
  export interface StealthSession {
931
963
  fetch(url: string, options?: StealthFetchOptions): Promise<StealthResponse>;
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.2.0-beta.10",
2
+ "version": "2.2.0-beta.12",
3
3
  "name": "@apifuse/provider-sdk",
4
4
  "private": false,
5
5
  "type": "module",
@@ -61,6 +61,11 @@
61
61
  "import": "./dist/server/index.js",
62
62
  "default": "./dist/server/index.js"
63
63
  },
64
+ "./stateful": {
65
+ "types": "./dist/stateful/index.d.ts",
66
+ "import": "./dist/stateful/index.js",
67
+ "default": "./dist/stateful/index.js"
68
+ },
64
69
  "./testing": {
65
70
  "types": "./dist/testing/index.d.ts",
66
71
  "import": "./dist/testing/index.js",
@@ -109,6 +114,7 @@
109
114
  "puppeteer-extra-plugin-stealth": "^2.11.2",
110
115
  "re2-wasm": "^1.0",
111
116
  "safe-regex": "^2.1",
117
+ "tough-cookie": "^6.0.2",
112
118
  "zod": "^4.4.3"
113
119
  },
114
120
  "repository": {
@@ -156,10 +156,19 @@ export type ProxyTelemetrySink = {
156
156
  recordProxyVendorFailover?(event: ProxyVendorFailoverTelemetryEvent): void;
157
157
  };
158
158
 
159
+ export type ProxyResolutionSource =
160
+ | "explicit"
161
+ | "env"
162
+ | "config"
163
+ | "smartproxy-allocator"
164
+ | "nodemaven-gateway";
165
+
159
166
  export type ResolvedProxyConfig = {
160
167
  shouldWarn: boolean;
161
168
  url?: string;
162
- source?: "explicit" | "env" | "config" | "smartproxy-allocator" | "nodemaven-gateway";
169
+ /** SDK-native vendor that supplied the URL, when applicable. */
170
+ vendor?: ProxyVendorName;
171
+ source?: ProxyResolutionSource;
163
172
  protocol?: ProxyProtocol;
164
173
  diagnostics?: Record<string, string | number | boolean>;
165
174
  };
@@ -656,6 +665,18 @@ export async function resolveProxyConfigAsync(
656
665
  return { shouldWarn: true };
657
666
  }
658
667
 
668
+ /**
669
+ * Resolve the proxy URL for a provider-owned consumer such as a CAPTCHA solver.
670
+ * Vendor allocation and failover remain owned by the SDK.
671
+ */
672
+ export async function resolveProxy(
673
+ options: ProxyResolutionOptions = {},
674
+ ): Promise<ResolvedProxyConfig> {
675
+ const resolved = await resolveProxyConfigAsync(options);
676
+ const vendor = vendorFromResolvedSource(resolved.source);
677
+ return vendor ? { ...resolved, vendor } : resolved;
678
+ }
679
+
659
680
  /**
660
681
  * Each vendor's default egress protocol, chosen from live KR benchmarks. HTTP
661
682
  * CONNECT wins for nodemaven (socks5 adds ~500ms through the gateway) and ties
package/src/errors.ts CHANGED
@@ -79,6 +79,21 @@ export class SDKError extends ProviderError {
79
79
  }
80
80
  }
81
81
 
82
+ /** Raised when persisted stealth cookies use a store version this SDK cannot read. */
83
+ export class StealthCookieStoreVersionError extends SDKError {
84
+ constructor(public readonly version: unknown) {
85
+ const displayedVersion =
86
+ typeof version === "string" || typeof version === "number"
87
+ ? String(version)
88
+ : "missing or invalid";
89
+ super(`Unsupported stealth cookie store version: ${displayedVersion}`, {
90
+ code: "unsupported_stealth_cookie_store_version",
91
+ details: { receivedVersion: version, supportedVersions: [1] },
92
+ });
93
+ this.name = "StealthCookieStoreVersionError";
94
+ }
95
+ }
96
+
82
97
  export class AuthError extends ProviderError {
83
98
  constructor(message: string, options?: ProviderErrorOptions) {
84
99
  super(message, options);
package/src/index.ts CHANGED
@@ -7,9 +7,14 @@ export type {
7
7
  ApiFuseConfig,
8
8
  BrowserConfig,
9
9
  ProxyConfig,
10
+ ProxyProtocol,
11
+ ProxyResolutionOptions,
12
+ ProxyResolutionSource,
13
+ ProxyVendorName,
14
+ ResolvedProxyConfig,
10
15
  SessionConfig,
11
16
  } from "./config/loader.js";
12
- export { defineConfig, loadApiFuseConfig } from "./config/loader.js";
17
+ export { defineConfig, loadApiFuseConfig, resolveProxy } from "./config/loader.js";
13
18
  export {
14
19
  canonicalJson,
15
20
  digestProviderContract,
@@ -245,6 +250,8 @@ export type {
245
250
  StateValue,
246
251
  StateWriteOptions,
247
252
  StealthClient,
253
+ StealthCookieStore,
254
+ StealthCookieStoreV1,
248
255
  StealthFetchOptions,
249
256
  StealthPlatform,
250
257
  StealthProfile,
@@ -1,24 +1,27 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import type { Browser, ImpitOptions, ImpitResponse, RequestInit } from "impit";
3
3
  import { Impit } from "impit";
4
+ import { Cookie, CookieJar as ToughCookieJar } from "tough-cookie";
4
5
 
5
6
  import type { ProxyResolutionOptions, ProxyVendorName } from "../config/loader.js";
6
7
  import {
7
8
  DEFAULT_SMARTPROXY_POOL_SIZE,
8
9
  invalidateProxyResolutionCacheAsync,
9
- policyResolvesRegistryVendorChain,
10
10
  ProxyResolutionError,
11
+ policyResolvesRegistryVendorChain,
11
12
  resolvePolicyProxyPoolSpan,
12
13
  resolvePolicyTransportAttemptCap,
13
14
  resolveProxyConfigAsync,
14
15
  vendorFromResolvedSource,
15
16
  } from "../config/loader.js";
16
- import { SDKError, TransportError } from "../errors.js";
17
+ import { SDKError, StealthCookieStoreVersionError, TransportError } from "../errors.js";
17
18
  import { getStealthProfile } from "../stealth/profiles.js";
18
19
  import type {
19
20
  CookieJar,
20
21
  HttpMethod,
21
22
  StealthClient,
23
+ StealthCookieStore,
24
+ StealthCookieStoreV1,
22
25
  StealthFetchOptions,
23
26
  StealthRedirectHop,
24
27
  StealthResponse,
@@ -127,81 +130,145 @@ function isRecord(value: unknown): value is Record<PropertyKey, unknown> {
127
130
  return typeof value === "object" && value !== null;
128
131
  }
129
132
 
133
+ const LEGACY_COOKIE_ORIGIN = "https://legacy-cookie.invalid/";
134
+
130
135
  class CookieJarImpl implements CookieJar {
131
- private readonly cookies: Record<string, string>;
136
+ private cookies: ToughCookieJar;
137
+ private readonly defaultUrl: string;
132
138
 
133
- constructor(cookieStrings: string[]) {
134
- this.cookies = {};
139
+ constructor(cookieStrings: readonly string[], defaultUrl = LEGACY_COOKIE_ORIGIN) {
140
+ this.cookies = new ToughCookieJar(undefined, {
141
+ allowSecureOnLocal: false,
142
+ rejectPublicSuffixes: true,
143
+ });
144
+ this.defaultUrl = this.normalizeUrl(defaultUrl) ?? LEGACY_COOKIE_ORIGIN;
135
145
  this.setFromCookieStrings(cookieStrings);
136
146
  }
137
147
 
138
- setFromCookieStrings(cookieStrings: readonly string[]): void {
139
- for (const cookieString of cookieStrings) {
140
- const [nameValue] = cookieString.split(";");
141
- if (!nameValue) {
142
- continue;
143
- }
144
-
145
- const separatorIndex = nameValue.indexOf("=");
146
- if (separatorIndex === -1) {
147
- continue;
148
- }
148
+ /**
149
+ * URL-less legacy operations are scoped to this jar's default URL. Session
150
+ * jars use the client's base URL and response jars use the response URL. A
151
+ * flat restore has no attributes to recover, so it creates host-only Path=/
152
+ * cookies for that default URL instead of making them visible to every host.
153
+ */
154
+ setFromCookieStrings(cookieStrings: readonly string[], url = this.defaultUrl): void {
155
+ const cookieUrl = this.normalizeUrl(url);
156
+ if (!cookieUrl) return;
149
157
 
150
- const name = nameValue.slice(0, separatorIndex).trim();
151
- const value = nameValue.slice(separatorIndex + 1).trim();
152
- if (name) this.cookies[name] = value;
158
+ for (const cookieString of cookieStrings) {
159
+ this.cookies.setCookieSync(cookieString, cookieUrl, { ignoreError: true });
153
160
  }
154
161
  }
155
162
 
156
- get(name: string): string | undefined {
157
- return this.cookies[name];
163
+ get(name: string, url?: string): string | undefined {
164
+ return this.getAll(url)[name];
158
165
  }
159
166
 
160
- getAll(): Record<string, string> {
161
- return { ...this.cookies };
167
+ getAll(url?: string): Record<string, string> {
168
+ return Object.fromEntries(
169
+ this.getUniqueCookies(url ?? this.defaultUrl).map((cookie) => [cookie.key, cookie.value]),
170
+ );
162
171
  }
163
172
 
164
- has(name: string): boolean {
165
- return Object.hasOwn(this.cookies, name);
173
+ has(name: string, url?: string): boolean {
174
+ return Object.hasOwn(this.getAll(url), name);
166
175
  }
167
176
 
168
- toString(): string {
169
- return Object.entries(this.cookies)
170
- .map(([name, value]) => `${name}=${value}`)
177
+ toString(url?: string): string {
178
+ return this.getUniqueCookies(url ?? this.defaultUrl)
179
+ .map((cookie) => cookie.cookieString())
171
180
  .join("; ");
172
181
  }
173
182
 
174
- toHeader(): string {
175
- return this.toString();
183
+ toHeader(url?: string): string {
184
+ return this.toString(url);
176
185
  }
177
186
 
178
187
  snapshot(): Record<string, string> {
179
- return this.getAll();
188
+ // This compatibility view deliberately enumerates the serialized store,
189
+ // not getAll(defaultUrl): persistence must include sibling hosts and paths.
190
+ // Duplicate names still collapse because a flat map cannot represent them.
191
+ const entries: [string, string][] = [];
192
+ for (const cookie of this.serialize().jar.cookies) {
193
+ if (typeof cookie.key === "string" && typeof cookie.value === "string" && cookie.key) {
194
+ entries.push([cookie.key, cookie.value]);
195
+ }
196
+ }
197
+ return Object.fromEntries(entries);
180
198
  }
181
199
 
182
200
  restore(cookies: Record<string, string>): void {
183
201
  this.clear();
184
202
  for (const [name, value] of Object.entries(cookies)) {
185
- if (name) this.cookies[name] = value;
203
+ if (!name) continue;
204
+ this.cookies.setCookieSync(new Cookie({ key: name, path: "/", value }), this.defaultUrl, {
205
+ ignoreError: true,
206
+ });
186
207
  }
187
208
  }
188
209
 
189
- clear(): void {
190
- for (const name of Object.keys(this.cookies)) {
191
- delete this.cookies[name];
210
+ serialize(): StealthCookieStoreV1 {
211
+ const jar = this.cookies.serializeSync();
212
+ if (!jar) {
213
+ throw new SDKError("Stealth cookie store could not be serialized", {
214
+ code: "stealth_cookie_store_serialize_failed",
215
+ });
192
216
  }
217
+ return { version: 1, jar };
193
218
  }
194
219
 
195
- find(predicate: (cookie: string) => boolean): string | undefined {
196
- for (const [name, value] of Object.entries(this.cookies)) {
197
- const cookie = `${name}=${value}`;
198
- if (predicate(cookie)) {
199
- return cookie;
220
+ deserialize(state: StealthCookieStore): void {
221
+ const version = isRecord(state) ? state.version : undefined;
222
+ if (version !== 1) {
223
+ throw new StealthCookieStoreVersionError(version);
224
+ }
225
+
226
+ // Deserialize into a new jar first so invalid state cannot partially clear
227
+ // or replace a live session. tough-cookie restores the cookie attributes and
228
+ // matching semantics represented in its own serialized format.
229
+ const restored = ToughCookieJar.deserializeSync(state.jar);
230
+ // tough-cookie 6 does not include this option in serializeSync(). Preserve
231
+ // the SDK's stricter setting across restoration.
232
+ Reflect.set(restored, "allowSecureOnLocal", false);
233
+ this.cookies = restored;
234
+ }
235
+
236
+ clear(): void {
237
+ this.cookies.removeAllCookiesSync();
238
+ }
239
+
240
+ find(predicate: (cookie: string) => boolean, url?: string): string | undefined {
241
+ for (const cookie of this.getUniqueCookies(url ?? this.defaultUrl)) {
242
+ const cookieString = cookie.cookieString();
243
+ if (predicate(cookieString)) {
244
+ return cookieString;
200
245
  }
201
246
  }
202
247
 
203
248
  return undefined;
204
249
  }
250
+
251
+ private normalizeUrl(url: string): string | undefined {
252
+ try {
253
+ return new URL(url).toString();
254
+ } catch {
255
+ return undefined;
256
+ }
257
+ }
258
+
259
+ private getUniqueCookies(url: string): Cookie[] {
260
+ const cookieUrl = this.normalizeUrl(url);
261
+ if (!cookieUrl) return [];
262
+
263
+ // tough-cookie returns longer (more-specific) paths first. Keeping the
264
+ // first cookie for each name prevents ambiguous duplicate-name headers.
265
+ const names = new Set<string>();
266
+ return this.cookies.getCookiesSync(cookieUrl).filter((cookie) => {
267
+ if (names.has(cookie.key)) return false;
268
+ names.add(cookie.key);
269
+ return true;
270
+ });
271
+ }
205
272
  }
206
273
 
207
274
  function closestImpitBrowser(
@@ -282,12 +349,12 @@ function hasOwn(object: object, key: string): boolean {
282
349
  }
283
350
  function toImpitCookieJar(cookieJar: CookieJarImpl): NonNullable<ImpitOptions["cookieJar"]> {
284
351
  return {
285
- setCookie(cookie: string, _url: string, cb?: (error?: unknown) => void) {
286
- cookieJar.setFromCookieStrings([cookie]);
352
+ setCookie(cookie: string, url: string, cb?: (error?: unknown) => void) {
353
+ cookieJar.setFromCookieStrings([cookie], url);
287
354
  if (typeof cb === "function") cb();
288
355
  },
289
- getCookieString(_url: string) {
290
- return cookieJar.toString();
356
+ getCookieString(url: string) {
357
+ return cookieJar.toHeader(url);
291
358
  },
292
359
  };
293
360
  }
@@ -341,7 +408,10 @@ export async function normalizeResponse(
341
408
  requestUrl?: string,
342
409
  ): Promise<StealthResponse> {
343
410
  const headers = Object.fromEntries(response.headers.entries());
344
- const cookies = new CookieJarImpl(setCookieHeadersFromResponse(response.headers));
411
+ const cookies = new CookieJarImpl(
412
+ setCookieHeadersFromResponse(response.headers),
413
+ response.url ?? requestUrl,
414
+ );
345
415
  const bodyBytes = await response.arrayBuffer();
346
416
  const body = new TextDecoder().decode(bodyBytes);
347
417
 
@@ -588,7 +658,7 @@ function createSessionFetcher(
588
658
  let closed = false;
589
659
  let hasWarnedMissingProxy = false;
590
660
  const warn = clientOptions.warn ?? console.warn;
591
- const cookieJar = new CookieJarImpl([]);
661
+ const cookieJar = new CookieJarImpl([], baseUrl);
592
662
  const impitCookieJar = toImpitCookieJar(cookieJar);
593
663
 
594
664
  function getClient(
@@ -757,7 +827,7 @@ function createSessionFetcher(
757
827
  const requestUrl = appendQueryParams(resolveUrl(baseUrl, url), options.params);
758
828
  const headers = { ...(options.headers ?? {}) };
759
829
  if (!hasHeader(headers, "Cookie")) {
760
- const cookieHeader = cookieJar.toString();
830
+ const cookieHeader = cookieJar.toHeader(requestUrl);
761
831
  if (cookieHeader) headers.Cookie = cookieHeader;
762
832
  }
763
833
  const requestInit: StealthRequestInit = {
@@ -774,7 +844,10 @@ function createSessionFetcher(
774
844
  requestInit,
775
845
  );
776
846
  const normalized = await normalizeResponse(response, requestUrl);
777
- cookieJar.setFromCookieStrings(setCookieHeadersFromResponse(response.headers));
847
+ cookieJar.setFromCookieStrings(
848
+ setCookieHeadersFromResponse(response.headers),
849
+ response.url ?? requestUrl,
850
+ );
778
851
 
779
852
  if (proxy && isProxyConnectFailureResponse(response, normalized.body)) {
780
853
  throw createProxyConnectFailureError(normalized.body);
@@ -954,6 +1027,7 @@ function createSessionFetcher(
954
1027
  hops,
955
1028
  reason: "completed",
956
1029
  cookies: cookieJar.snapshot(),
1030
+ cookieStore: cookieJar.serialize(),
957
1031
  };
958
1032
  }
959
1033
 
@@ -976,6 +1050,7 @@ function createSessionFetcher(
976
1050
  hops,
977
1051
  reason: "stopped",
978
1052
  cookies: cookieJar.snapshot(),
1053
+ cookieStore: cookieJar.serialize(),
979
1054
  };
980
1055
  }
981
1056
 
@@ -985,6 +1060,7 @@ function createSessionFetcher(
985
1060
  hops,
986
1061
  reason: "missing_location",
987
1062
  cookies: cookieJar.snapshot(),
1063
+ cookieStore: cookieJar.serialize(),
988
1064
  };
989
1065
  }
990
1066
 
@@ -994,6 +1070,7 @@ function createSessionFetcher(
994
1070
  hops,
995
1071
  reason: "max_hops",
996
1072
  cookies: cookieJar.snapshot(),
1073
+ cookieStore: cookieJar.serialize(),
997
1074
  };
998
1075
  }
999
1076
 
@@ -1007,6 +1084,7 @@ function createSessionFetcher(
1007
1084
  hops,
1008
1085
  reason: "loop",
1009
1086
  cookies: cookieJar.snapshot(),
1087
+ cookieStore: cookieJar.serialize(),
1010
1088
  };
1011
1089
  }
1012
1090
  method = nextMethod;
@@ -1028,6 +1106,7 @@ function createSessionFetcher(
1028
1106
  hops,
1029
1107
  reason: "max_hops",
1030
1108
  cookies: cookieJar.snapshot(),
1109
+ cookieStore: cookieJar.serialize(),
1031
1110
  };
1032
1111
  },
1033
1112
  },
@@ -1,4 +1,16 @@
1
- export { createServerApp, type ServeOptions, serve } from "./serve.js";
1
+ export {
2
+ createServerApp,
3
+ type ProviderServerCloseOptions,
4
+ type ProviderServerHandle,
5
+ type ProviderServerLogEvent,
6
+ type ProviderServerLogger,
7
+ type ProviderServerOperationExecutor,
8
+ type ProviderServerOperationExecutorInput,
9
+ type ProviderServerOptions,
10
+ type ProviderServerStatefulForwardEnvelope,
11
+ type ServeOptions,
12
+ serve,
13
+ } from "./serve.js";
2
14
  export {
3
15
  computeSelfTestPlanDigest,
4
16
  createSelfTestApp,