@apifuse/provider-sdk 2.2.0-beta.57 → 2.2.0-beta.59
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.
- package/AUTHORING.md +9 -0
- package/CHANGELOG.md +8 -0
- package/README.md +22 -5
- package/SUBMISSION.md +2 -1
- package/bin/apifuse-pack-types.ts +49 -0
- package/bin/apifuse-record.ts +6 -0
- package/dist/cli/templates/provider/README.md.tpl +19 -5
- package/dist/config/loader.d.ts +7 -0
- package/dist/config/loader.js +30 -6
- package/dist/declaration-validation.d.ts +2 -1
- package/dist/declaration-validation.js +39 -0
- package/dist/define.d.ts +5 -4
- package/dist/define.js +25 -1
- package/dist/engine.d.ts +16 -0
- package/dist/engine.js +58 -1
- package/dist/error-resolution.js +13 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -2
- package/dist/lint.js +3 -3
- package/dist/provider.d.ts +3 -3
- package/dist/provider.js +2 -2
- package/dist/runtime/bounded-expiring-map.d.ts +21 -0
- package/dist/runtime/bounded-expiring-map.js +64 -0
- package/dist/runtime/cache.d.ts +2 -0
- package/dist/runtime/cache.js +8 -1
- package/dist/runtime/egress-lease.d.ts +63 -0
- package/dist/runtime/egress-lease.js +233 -0
- package/dist/runtime/http.js +1 -0
- package/dist/runtime/proxy-retry-policy.d.ts +4 -1
- package/dist/runtime/proxy-retry-policy.js +11 -2
- package/dist/runtime/request-telemetry.d.ts +7 -9
- package/dist/runtime/resolver-config.d.ts +7 -0
- package/dist/runtime/resolver-config.js +8 -0
- package/dist/runtime/resolver-public.d.ts +3 -1
- package/dist/runtime/resolver-public.js +1 -1
- package/dist/runtime/resolver-shared.d.ts +9 -0
- package/dist/runtime/resolver-shared.js +57 -0
- package/dist/runtime/resolver-telemetry.d.ts +134 -0
- package/dist/runtime/resolver-telemetry.js +204 -0
- package/dist/runtime/resolver-usage.d.ts +24 -0
- package/dist/runtime/resolver-usage.js +60 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +5 -0
- package/dist/runtime/resolver-vendors/bindings.js +5 -0
- package/dist/runtime/resolver-vendors/capsolver.d.ts +2 -2
- package/dist/runtime/resolver-vendors/capsolver.js +21 -9
- package/dist/runtime/resolver-vendors/hypersolutions.d.ts +27 -0
- package/dist/runtime/resolver-vendors/hypersolutions.js +369 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +2 -2
- package/dist/runtime/resolver-vendors/twocaptcha.js +26 -10
- package/dist/runtime/resolver-vendors/types.d.ts +35 -5
- package/dist/runtime/resolver-vendors/types.js +20 -10
- package/dist/runtime/resolver.d.ts +5 -1
- package/dist/runtime/resolver.js +397 -109
- package/dist/runtime/stealth-akamai-sbsd.d.ts +54 -0
- package/dist/runtime/stealth-akamai-sbsd.js +142 -0
- package/dist/runtime/stealth.js +547 -53
- package/dist/schema.d.ts +9 -0
- package/dist/schema.js +16 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +1 -0
- package/dist/server/self-test.d.ts +4 -0
- package/dist/server/self-test.js +10 -1
- package/dist/server/serve-implementation.d.ts +7 -1
- package/dist/server/serve-implementation.js +181 -53
- package/dist/server/types.d.ts +6 -0
- package/dist/server/types.js +14 -0
- package/dist/stealth/profiles.d.ts +7 -1
- package/dist/stealth/profiles.js +9 -0
- package/dist/testing/run.js +6 -0
- package/dist/types.d.ts +115 -12
- package/package.json +1 -1
- package/src/cli/templates/provider/README.md.tpl +19 -5
- package/src/config/loader.ts +34 -6
- package/src/declaration-validation.ts +47 -1
- package/src/define.ts +46 -3
- package/src/engine.ts +76 -1
- package/src/error-resolution.ts +13 -0
- package/src/index.ts +32 -1
- package/src/lint.ts +3 -3
- package/src/provider.ts +9 -0
- package/src/runtime/bounded-expiring-map.ts +67 -0
- package/src/runtime/cache.ts +10 -1
- package/src/runtime/egress-lease.ts +336 -0
- package/src/runtime/http.ts +1 -0
- package/src/runtime/proxy-retry-policy.ts +12 -1
- package/src/runtime/request-telemetry.ts +5 -7
- package/src/runtime/resolver-config.ts +12 -0
- package/src/runtime/resolver-public.ts +21 -0
- package/src/runtime/resolver-shared.ts +75 -0
- package/src/runtime/resolver-telemetry.ts +362 -0
- package/src/runtime/resolver-usage.ts +96 -0
- package/src/runtime/resolver-vendors/bindings.ts +5 -0
- package/src/runtime/resolver-vendors/capsolver.ts +22 -3
- package/src/runtime/resolver-vendors/hypersolutions.ts +480 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +26 -12
- package/src/runtime/resolver-vendors/types.ts +60 -12
- package/src/runtime/resolver.ts +465 -124
- package/src/runtime/stealth-akamai-sbsd.ts +207 -0
- package/src/runtime/stealth.ts +727 -95
- package/src/schema.ts +17 -0
- package/src/server/index.ts +21 -0
- package/src/server/self-test.ts +13 -0
- package/src/server/serve-implementation.ts +268 -48
- package/src/server/types.ts +14 -0
- package/src/stealth/profiles.ts +10 -0
- package/src/testing/run.ts +6 -0
- package/src/types.ts +130 -22
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import type { ProxyVendorName } from "../config/loader.js";
|
|
4
|
+
import { SDKError } from "../errors.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* AES-256-GCM key for the engine ceremony egress lease handle (ADR-0010 v1.1).
|
|
8
|
+
* Engine host only: the `APIFUSE__ENGINE__` family is rejected from provider
|
|
9
|
+
* secrets and filtered from provider env projections. Supply at least 32 random
|
|
10
|
+
* bytes (for example `openssl rand -base64 32`). Rotating the key fails
|
|
11
|
+
* verification of every outstanding handle (`EGRESS_LEASE_INVALID`, 409): the
|
|
12
|
+
* affected ceremonies restart; they do not expire.
|
|
13
|
+
*/
|
|
14
|
+
export const APIFUSE__ENGINE__CEREMONY_LEASE_KEY = "APIFUSE__ENGINE__CEREMONY_LEASE_KEY";
|
|
15
|
+
|
|
16
|
+
/** Internal option key shared only by the server assembler and stealth runtime. */
|
|
17
|
+
export const ENGINE_CEREMONY_EGRESS_LEASE = Symbol.for(
|
|
18
|
+
"@apifuse/provider-sdk/engine-ceremony-egress-lease",
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const HANDLE_VERSION = 1;
|
|
22
|
+
const HANDLE_MAX_BYTES = 16_384;
|
|
23
|
+
const AEAD_NONCE_BYTES = 12;
|
|
24
|
+
const AEAD_TAG_BYTES = 16;
|
|
25
|
+
const AEAD_ALGORITHM = "aes-256-gcm";
|
|
26
|
+
const AEAD_KEY_DOMAIN = "apifuse:ceremony-egress-lease:v1\0";
|
|
27
|
+
const LEASE_KEY_MIN_BYTES = 32;
|
|
28
|
+
|
|
29
|
+
export type CeremonyEgressBinding = {
|
|
30
|
+
readonly vendor: ProxyVendorName;
|
|
31
|
+
readonly proxyUrl: string;
|
|
32
|
+
readonly poolIndex: number;
|
|
33
|
+
readonly affinityKey: string;
|
|
34
|
+
readonly refreshEpoch: number;
|
|
35
|
+
/**
|
|
36
|
+
* Vendor session lifetime the endpoint was allocated for (`proxy.session.lifetimeMinutes`,
|
|
37
|
+
* Smartproxy `life`, NodeMaven SID window). It is the upper bound of the lease, counted
|
|
38
|
+
* from the mint (the first successful attempt), so a Smartproxy lease can outlive the
|
|
39
|
+
* vendor session by the extraction-cache reuse window; expiry then drops and rebinds.
|
|
40
|
+
* The bound is not a vendor guarantee (ADR-0010: a raw Smartproxy endpoint is not a hard
|
|
41
|
+
* lease): the stealth runtime releases the binding as soon as the endpoint fails at the
|
|
42
|
+
* transport, so a dead endpoint is left before the lifetime elapses.
|
|
43
|
+
*/
|
|
44
|
+
readonly lifetimeMinutes: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type CeremonyEgressLeasePayloadV1 = CeremonyEgressBinding & {
|
|
48
|
+
readonly version: typeof HANDLE_VERSION;
|
|
49
|
+
readonly tenantId: string;
|
|
50
|
+
readonly providerId: string;
|
|
51
|
+
readonly flowId: string;
|
|
52
|
+
readonly mintedAtMs: number;
|
|
53
|
+
readonly expiresAtMs: number;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type CeremonyEgressLeaseScopeV1 = Pick<
|
|
57
|
+
CeremonyEgressLeasePayloadV1,
|
|
58
|
+
"tenantId" | "providerId" | "flowId" | "affinityKey"
|
|
59
|
+
>;
|
|
60
|
+
|
|
61
|
+
export type CeremonyEgressLeaseRuntime = {
|
|
62
|
+
/** Exact egress bound to this ceremony; undefined until the first successful attempt binds one. */
|
|
63
|
+
readonly binding: CeremonyEgressBinding | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Drops the binding once its vendor session lifetime has elapsed so the next attempt
|
|
66
|
+
* selects and binds a fresh endpoint. Returns true when a binding was dropped: the
|
|
67
|
+
* caller must also discard challenge state that was only valid for that endpoint.
|
|
68
|
+
*/
|
|
69
|
+
dropExpiredBinding(): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Drops the binding unconditionally; the stealth runtime calls this when the bound
|
|
72
|
+
* endpoint fails at the transport. Same return and caller obligation as
|
|
73
|
+
* `dropExpiredBinding`.
|
|
74
|
+
*/
|
|
75
|
+
dropBinding(): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Number of bindings dropped so far (expiry or release). Every stealth session created
|
|
78
|
+
* from one client shares the lease; a session compares this to what it last observed to
|
|
79
|
+
* retire endpoint-bound challenge state and replay records after another session moved
|
|
80
|
+
* the ceremony.
|
|
81
|
+
*/
|
|
82
|
+
readonly generation: number;
|
|
83
|
+
bind(binding: CeremonyEgressBinding): void;
|
|
84
|
+
handle(): string | undefined;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** The caller presented a handle this engine cannot verify for this ceremony (409). */
|
|
88
|
+
function invalidLease(): never {
|
|
89
|
+
throw new SDKError("The engine ceremony egress lease is invalid", {
|
|
90
|
+
code: "EGRESS_LEASE_INVALID",
|
|
91
|
+
fix: "Restart the authentication ceremony to obtain a new engine-owned egress lease.",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The engine violated its own binding invariant (500); never attributable to the caller. */
|
|
96
|
+
function bindingFault(message: string): never {
|
|
97
|
+
throw new SDKError(message, {
|
|
98
|
+
code: "EGRESS_LEASE_BINDING_INVALID",
|
|
99
|
+
fix: "This is an SDK or provider-runtime fault; the ceremony lease cannot bind the selected egress.",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function encryptionKey(key: string): Buffer {
|
|
104
|
+
return createHash("sha256").update(AEAD_KEY_DOMAIN, "utf8").update(key, "utf8").digest();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Fixed field order and JSON.stringify's compact form are the canonical AAD encoding. */
|
|
108
|
+
function scopeAad(scope: CeremonyEgressLeaseScopeV1): Buffer {
|
|
109
|
+
return Buffer.from(
|
|
110
|
+
JSON.stringify({
|
|
111
|
+
version: HANDLE_VERSION,
|
|
112
|
+
tenantId: scope.tenantId,
|
|
113
|
+
providerId: scope.providerId,
|
|
114
|
+
flowId: scope.flowId,
|
|
115
|
+
affinityKey: scope.affinityKey,
|
|
116
|
+
}),
|
|
117
|
+
"utf8",
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function encodeHandle(
|
|
122
|
+
payload: CeremonyEgressLeasePayloadV1,
|
|
123
|
+
key: string,
|
|
124
|
+
scope: CeremonyEgressLeaseScopeV1,
|
|
125
|
+
): string {
|
|
126
|
+
const nonce = randomBytes(AEAD_NONCE_BYTES);
|
|
127
|
+
const cipher = createCipheriv(AEAD_ALGORITHM, encryptionKey(key), nonce, {
|
|
128
|
+
authTagLength: AEAD_TAG_BYTES,
|
|
129
|
+
});
|
|
130
|
+
cipher.setAAD(scopeAad(scope));
|
|
131
|
+
const ciphertext = Buffer.concat([
|
|
132
|
+
cipher.update(JSON.stringify(payload), "utf8"),
|
|
133
|
+
cipher.final(),
|
|
134
|
+
]);
|
|
135
|
+
return [
|
|
136
|
+
`v${HANDLE_VERSION}`,
|
|
137
|
+
nonce.toString("base64url"),
|
|
138
|
+
ciphertext.toString("base64url"),
|
|
139
|
+
cipher.getAuthTag().toString("base64url"),
|
|
140
|
+
].join(".");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isFiniteInteger(value: unknown): value is number {
|
|
144
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isPositiveFinite(value: unknown): value is number {
|
|
148
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function decodeHandle(
|
|
152
|
+
handle: string,
|
|
153
|
+
key: string,
|
|
154
|
+
scope: CeremonyEgressLeaseScopeV1,
|
|
155
|
+
): CeremonyEgressLeasePayloadV1 {
|
|
156
|
+
if (Buffer.byteLength(handle, "utf8") > HANDLE_MAX_BYTES) return invalidLease();
|
|
157
|
+
const parts = handle.split(".");
|
|
158
|
+
if (
|
|
159
|
+
parts.length !== 4 ||
|
|
160
|
+
parts[0] !== `v${HANDLE_VERSION}` ||
|
|
161
|
+
parts.slice(1).some((part) => !/^[A-Za-z0-9_-]+$/u.test(part))
|
|
162
|
+
) {
|
|
163
|
+
return invalidLease();
|
|
164
|
+
}
|
|
165
|
+
let parsed: unknown;
|
|
166
|
+
try {
|
|
167
|
+
const nonce = Buffer.from(parts[1]!, "base64url");
|
|
168
|
+
const ciphertext = Buffer.from(parts[2]!, "base64url");
|
|
169
|
+
const tag = Buffer.from(parts[3]!, "base64url");
|
|
170
|
+
if (
|
|
171
|
+
nonce.byteLength !== AEAD_NONCE_BYTES ||
|
|
172
|
+
tag.byteLength !== AEAD_TAG_BYTES ||
|
|
173
|
+
ciphertext.byteLength === 0
|
|
174
|
+
) {
|
|
175
|
+
return invalidLease();
|
|
176
|
+
}
|
|
177
|
+
const decipher = createDecipheriv(AEAD_ALGORITHM, encryptionKey(key), nonce, {
|
|
178
|
+
authTagLength: AEAD_TAG_BYTES,
|
|
179
|
+
});
|
|
180
|
+
decipher.setAAD(scopeAad(scope));
|
|
181
|
+
decipher.setAuthTag(tag);
|
|
182
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
183
|
+
parsed = JSON.parse(plaintext.toString("utf8"));
|
|
184
|
+
} catch {
|
|
185
|
+
return invalidLease();
|
|
186
|
+
}
|
|
187
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return invalidLease();
|
|
188
|
+
const candidate = parsed as Partial<CeremonyEgressLeasePayloadV1>;
|
|
189
|
+
if (
|
|
190
|
+
candidate.version !== HANDLE_VERSION ||
|
|
191
|
+
(candidate.vendor !== "smartproxy" && candidate.vendor !== "nodemaven") ||
|
|
192
|
+
typeof candidate.proxyUrl !== "string" ||
|
|
193
|
+
!candidate.proxyUrl ||
|
|
194
|
+
!isFiniteInteger(candidate.poolIndex) ||
|
|
195
|
+
typeof candidate.affinityKey !== "string" ||
|
|
196
|
+
!candidate.affinityKey ||
|
|
197
|
+
!isFiniteInteger(candidate.refreshEpoch) ||
|
|
198
|
+
typeof candidate.tenantId !== "string" ||
|
|
199
|
+
!candidate.tenantId ||
|
|
200
|
+
typeof candidate.providerId !== "string" ||
|
|
201
|
+
!candidate.providerId ||
|
|
202
|
+
typeof candidate.flowId !== "string" ||
|
|
203
|
+
!candidate.flowId ||
|
|
204
|
+
!isFiniteInteger(candidate.mintedAtMs) ||
|
|
205
|
+
!isFiniteInteger(candidate.expiresAtMs) ||
|
|
206
|
+
candidate.expiresAtMs <= candidate.mintedAtMs ||
|
|
207
|
+
!isPositiveFinite(candidate.lifetimeMinutes)
|
|
208
|
+
) {
|
|
209
|
+
return invalidLease();
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const proxy = new URL(candidate.proxyUrl);
|
|
213
|
+
if (proxy.protocol !== "http:" && proxy.protocol !== "socks5:") return invalidLease();
|
|
214
|
+
} catch {
|
|
215
|
+
return invalidLease();
|
|
216
|
+
}
|
|
217
|
+
return candidate as CeremonyEgressLeasePayloadV1;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function bindingOf(payload: CeremonyEgressLeasePayloadV1): CeremonyEgressBinding {
|
|
221
|
+
return {
|
|
222
|
+
vendor: payload.vendor,
|
|
223
|
+
proxyUrl: payload.proxyUrl,
|
|
224
|
+
poolIndex: payload.poolIndex,
|
|
225
|
+
affinityKey: payload.affinityKey,
|
|
226
|
+
refreshEpoch: payload.refreshEpoch,
|
|
227
|
+
lifetimeMinutes: payload.lifetimeMinutes,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function createCeremonyEgressLeaseRuntime(options: {
|
|
232
|
+
readonly tenantId?: string;
|
|
233
|
+
readonly providerId: string;
|
|
234
|
+
readonly flowId: string;
|
|
235
|
+
readonly affinityKey: string;
|
|
236
|
+
readonly handle?: string;
|
|
237
|
+
readonly environment?: Readonly<Record<string, string | undefined>>;
|
|
238
|
+
readonly now?: () => number;
|
|
239
|
+
}): CeremonyEgressLeaseRuntime {
|
|
240
|
+
const environment = options.environment ?? process.env;
|
|
241
|
+
const now = options.now ?? Date.now;
|
|
242
|
+
if (typeof options.tenantId !== "string" || !options.tenantId.trim()) {
|
|
243
|
+
throw new SDKError("A tenant is required for an engine ceremony egress lease", {
|
|
244
|
+
code: "EGRESS_LEASE_INVALID",
|
|
245
|
+
fix: "Restart the authentication ceremony with its tenant identity.",
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const scope: CeremonyEgressLeaseScopeV1 = {
|
|
249
|
+
tenantId: options.tenantId,
|
|
250
|
+
providerId: options.providerId,
|
|
251
|
+
flowId: options.flowId,
|
|
252
|
+
affinityKey: options.affinityKey,
|
|
253
|
+
};
|
|
254
|
+
const key = environment[APIFUSE__ENGINE__CEREMONY_LEASE_KEY]?.trim();
|
|
255
|
+
if (!key) {
|
|
256
|
+
throw new SDKError("The engine ceremony egress lease key is not configured", {
|
|
257
|
+
code: "EGRESS_LEASE_KEY_MISSING",
|
|
258
|
+
fix: `Configure ${APIFUSE__ENGINE__CEREMONY_LEASE_KEY} in the engine host.`,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
// The key is the whole authenticity boundary of a handle (a verified handle's proxyUrl
|
|
262
|
+
// becomes the ceremony egress), so the documented minimum is enforced, not advisory.
|
|
263
|
+
if (Buffer.byteLength(key, "utf8") < LEASE_KEY_MIN_BYTES) {
|
|
264
|
+
throw new SDKError(
|
|
265
|
+
`The engine ceremony egress lease key is shorter than ${LEASE_KEY_MIN_BYTES} bytes`,
|
|
266
|
+
{
|
|
267
|
+
code: "EGRESS_LEASE_KEY_WEAK",
|
|
268
|
+
fix: `Set ${APIFUSE__ENGINE__CEREMONY_LEASE_KEY} to at least ${LEASE_KEY_MIN_BYTES} random bytes (for example \`openssl rand -base64 32\`).`,
|
|
269
|
+
},
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
let payload: CeremonyEgressLeasePayloadV1 | undefined;
|
|
273
|
+
let currentHandle: string | undefined;
|
|
274
|
+
let generation = 0;
|
|
275
|
+
|
|
276
|
+
const dropBinding = (): boolean => {
|
|
277
|
+
if (!payload) return false;
|
|
278
|
+
payload = undefined;
|
|
279
|
+
currentHandle = undefined;
|
|
280
|
+
generation += 1;
|
|
281
|
+
return true;
|
|
282
|
+
};
|
|
283
|
+
const dropExpiredBinding = (): boolean =>
|
|
284
|
+
payload !== undefined && now() >= payload.expiresAtMs ? dropBinding() : false;
|
|
285
|
+
|
|
286
|
+
if (options.handle !== undefined) {
|
|
287
|
+
// Scope mismatch fails GCM authentication because scope is AAD; there is
|
|
288
|
+
// deliberately no post-decrypt scope comparison fallback.
|
|
289
|
+
payload = decodeHandle(options.handle, key, scope);
|
|
290
|
+
currentHandle = options.handle;
|
|
291
|
+
// An expired but authentic handle is not a fault: the ceremony continues on a
|
|
292
|
+
// freshly selected endpoint and the next turn receives the new handle.
|
|
293
|
+
dropExpiredBinding();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
get binding() {
|
|
298
|
+
return payload ? bindingOf(payload) : undefined;
|
|
299
|
+
},
|
|
300
|
+
dropExpiredBinding,
|
|
301
|
+
dropBinding,
|
|
302
|
+
get generation() {
|
|
303
|
+
return generation;
|
|
304
|
+
},
|
|
305
|
+
bind(binding) {
|
|
306
|
+
if (payload) {
|
|
307
|
+
// The stealth runtime reuses the bound endpoint while a binding exists and
|
|
308
|
+
// never rebinds it; a second bind is an engine invariant break.
|
|
309
|
+
return bindingFault("The ceremony egress lease is already bound");
|
|
310
|
+
}
|
|
311
|
+
if (
|
|
312
|
+
binding.affinityKey !== options.affinityKey ||
|
|
313
|
+
!binding.proxyUrl ||
|
|
314
|
+
(binding.vendor !== "smartproxy" && binding.vendor !== "nodemaven") ||
|
|
315
|
+
!isFiniteInteger(binding.poolIndex) ||
|
|
316
|
+
!isPositiveFinite(binding.lifetimeMinutes)
|
|
317
|
+
) {
|
|
318
|
+
return bindingFault("The selected egress cannot be bound to the ceremony lease");
|
|
319
|
+
}
|
|
320
|
+
const mintedAtMs = now();
|
|
321
|
+
payload = {
|
|
322
|
+
version: HANDLE_VERSION,
|
|
323
|
+
tenantId: scope.tenantId,
|
|
324
|
+
providerId: options.providerId,
|
|
325
|
+
flowId: options.flowId,
|
|
326
|
+
...binding,
|
|
327
|
+
mintedAtMs,
|
|
328
|
+
expiresAtMs: mintedAtMs + binding.lifetimeMinutes * 60_000,
|
|
329
|
+
};
|
|
330
|
+
currentHandle = encodeHandle(payload, key, scope);
|
|
331
|
+
},
|
|
332
|
+
handle() {
|
|
333
|
+
return currentHandle;
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|
package/src/runtime/http.ts
CHANGED
|
@@ -654,6 +654,7 @@ async function resolveNativeProxy(
|
|
|
654
654
|
// supported here, so a socks5 policy fails loudly rather than downgrading.
|
|
655
655
|
transportProtocols: ["http"],
|
|
656
656
|
telemetry: clientOptions.telemetry,
|
|
657
|
+
engineCredentials: clientOptions.engineCredentials,
|
|
657
658
|
});
|
|
658
659
|
if (resolvedProxy.shouldWarn) {
|
|
659
660
|
warn(
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
HttpRetryPreset,
|
|
8
8
|
HttpRetryUnsafeMethodPolicy,
|
|
9
9
|
} from "../types.js";
|
|
10
|
+
import { isSensitiveKey } from "./request-options.js";
|
|
10
11
|
|
|
11
12
|
export type NormalizedProxyTransportRetryOptions = Required<
|
|
12
13
|
Pick<
|
|
@@ -364,10 +365,20 @@ export function validateUnsafeProxyTransportRetryMethods(
|
|
|
364
365
|
export function isProxyTransportRetryMethod(
|
|
365
366
|
method: HttpMethod | string,
|
|
366
367
|
options: NormalizedProxyTransportRetryOptions,
|
|
368
|
+
request?: {
|
|
369
|
+
readonly body?: unknown;
|
|
370
|
+
readonly headers?: Readonly<Record<string, string | readonly string[] | undefined>>;
|
|
371
|
+
},
|
|
367
372
|
): boolean {
|
|
368
|
-
|
|
373
|
+
const methodAllowed = options.methods
|
|
369
374
|
.map((allowedMethod) => allowedMethod.toUpperCase())
|
|
370
375
|
.includes(method.toUpperCase());
|
|
376
|
+
if (!methodAllowed || request === undefined) return methodAllowed;
|
|
377
|
+
if (request.body !== undefined) return false;
|
|
378
|
+
return !Object.keys(request.headers ?? {}).some((name) => {
|
|
379
|
+
const normalized = name.toLowerCase();
|
|
380
|
+
return normalized === "cookie" || normalized === "proxy-authorization" || isSensitiveKey(name);
|
|
381
|
+
});
|
|
371
382
|
}
|
|
372
383
|
|
|
373
384
|
export function shouldRetryProxyTransportError(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PROVIDER_OBSERVABILITY_TAXONOMY_VERSION } from "../observability.js";
|
|
2
2
|
import type { ProxyTelemetrySink } from "../config/loader.js";
|
|
3
3
|
import type { ProxyTelemetryLogPayload } from "./proxy-telemetry.js";
|
|
4
|
+
import type { ResolverTelemetryLogPayload } from "./resolver-telemetry.js";
|
|
4
5
|
import type { Span, TraceContext } from "./trace.js";
|
|
5
6
|
|
|
6
7
|
export const PROVIDER_TELEMETRY_HEADER = "X-ApiFuse-Provider-Telemetry";
|
|
@@ -57,9 +58,7 @@ export type GatewayIngestible<T> = 0 extends 1 & T
|
|
|
57
58
|
? never
|
|
58
59
|
: unknown extends U
|
|
59
60
|
? never
|
|
60
|
-
: [U] extends [
|
|
61
|
-
number | boolean | ClosedEnum<string> | GatewayIngestible<U>,
|
|
62
|
-
]
|
|
61
|
+
: [U] extends [number | boolean | ClosedEnum<string> | GatewayIngestible<U>]
|
|
63
62
|
? T[K]
|
|
64
63
|
: never
|
|
65
64
|
: [NonNullable<T[K]>] extends [object]
|
|
@@ -86,9 +85,7 @@ export type TenantNeutral<T> = 0 extends 1 & T
|
|
|
86
85
|
? never
|
|
87
86
|
: unknown extends T[K]
|
|
88
87
|
? never
|
|
89
|
-
: [NonNullable<T[K]>] extends [
|
|
90
|
-
string[] & { readonly __tenantOpaqueCacheKeys: true },
|
|
91
|
-
]
|
|
88
|
+
: [NonNullable<T[K]>] extends [string[] & { readonly __tenantOpaqueCacheKeys: true }]
|
|
92
89
|
? K extends "keys"
|
|
93
90
|
? T[K]
|
|
94
91
|
: never
|
|
@@ -123,7 +120,8 @@ export interface TelemetryContributor<Log extends object, Header extends object>
|
|
|
123
120
|
|
|
124
121
|
export type RequestTelemetryLogPayload = {
|
|
125
122
|
proxy?: ProxyTelemetryLogPayload;
|
|
126
|
-
|
|
123
|
+
resolver?: ResolverTelemetryLogPayload;
|
|
124
|
+
} & Partial<Record<Exclude<TelemetryKey, "proxy" | "resolver">, object>>;
|
|
127
125
|
|
|
128
126
|
type RegisteredTelemetryContributor = {
|
|
129
127
|
readonly key: TelemetryKey;
|
|
@@ -1,6 +1,18 @@
|
|
|
1
|
+
import type { ProviderChallengeKind } from "../types.js";
|
|
2
|
+
|
|
1
3
|
export const APIFUSE__RESOLVER__2CAPTCHA__API_KEY = "APIFUSE__RESOLVER__2CAPTCHA__API_KEY";
|
|
2
4
|
export const APIFUSE__RESOLVER__CAPSOLVER__API_KEY = "APIFUSE__RESOLVER__CAPSOLVER__API_KEY";
|
|
3
5
|
export const APIFUSE__RESOLVER__CAPMONSTER__API_KEY = "APIFUSE__RESOLVER__CAPMONSTER__API_KEY";
|
|
6
|
+
export const APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY =
|
|
7
|
+
"APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY";
|
|
4
8
|
export const APIFUSE__RESOLVER__TIMEOUT_MS = "APIFUSE__RESOLVER__TIMEOUT_MS";
|
|
5
9
|
export const APIFUSE__CDP_POOL__URL = "APIFUSE__CDP_POOL__URL";
|
|
6
10
|
export const DEFAULT_RESOLVER_TIMEOUT_MS = 180_000;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* SBSD is solved on the provider's own admitted session, so the transport profile must be
|
|
14
|
+
* declared up front. Other kinds, including `akamai_sensor`, keep the profile optional.
|
|
15
|
+
*/
|
|
16
|
+
export function kindRequiresClientProfile(kind: ProviderChallengeKind): boolean {
|
|
17
|
+
return kind === "akamai_sbsd";
|
|
18
|
+
}
|
|
@@ -3,6 +3,7 @@ export {
|
|
|
3
3
|
APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
|
|
4
4
|
APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
|
|
5
5
|
APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
|
|
6
|
+
APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY,
|
|
6
7
|
APIFUSE__RESOLVER__TIMEOUT_MS,
|
|
7
8
|
bindResolverSignal,
|
|
8
9
|
createResolverClient,
|
|
@@ -21,3 +22,23 @@ export {
|
|
|
21
22
|
type ResolverRuntimeOptions,
|
|
22
23
|
type ResolverSolutionSource,
|
|
23
24
|
} from "./resolver.js";
|
|
25
|
+
export {
|
|
26
|
+
type ResolverCacheReadTelemetryEvent,
|
|
27
|
+
type ResolverCacheWriteTelemetryEvent,
|
|
28
|
+
type ResolverFailoverTelemetryEvent,
|
|
29
|
+
type ResolverIdentityTelemetryEvent,
|
|
30
|
+
type ResolverOutcomeTelemetryEvent,
|
|
31
|
+
type ResolverTelemetryAttemptOutcome,
|
|
32
|
+
type ResolverTelemetryCacheStatus,
|
|
33
|
+
type ResolverTelemetryCacheWriteReason,
|
|
34
|
+
type ResolverTelemetryErrorClass,
|
|
35
|
+
type ResolverTelemetryIdentitySource,
|
|
36
|
+
type ResolverTelemetryOutcome,
|
|
37
|
+
type ResolverTelemetryPhase,
|
|
38
|
+
type ResolverTelemetrySink,
|
|
39
|
+
type ResolverVendorAttemptTelemetryEvent,
|
|
40
|
+
} from "./resolver-telemetry.js";
|
|
41
|
+
export type {
|
|
42
|
+
ResolverPaidUsageContext,
|
|
43
|
+
ResolverVendorUnavailableReason,
|
|
44
|
+
} from "./resolver-vendors/types.js";
|
|
@@ -1,6 +1,81 @@
|
|
|
1
1
|
import { ProviderError } from "../errors.js";
|
|
2
2
|
import type { ChallengeSolution, ProviderChallenge, ResolverContext } from "../types.js";
|
|
3
3
|
import type { TraceRecorder } from "./trace.js";
|
|
4
|
+
import type { ResolverTelemetryOutcome, ResolverTelemetrySink } from "./resolver-telemetry.js";
|
|
5
|
+
|
|
6
|
+
type ResolverTelemetryBinding = (sink: ResolverTelemetrySink) => ResolverContext;
|
|
7
|
+
const telemetryBindings = new WeakMap<ResolverContext, ResolverTelemetryBinding>();
|
|
8
|
+
|
|
9
|
+
/** Internal contract: reconstruct a chain with a new request sink, without mutating the host chain. */
|
|
10
|
+
export function registerResolverTelemetryBinding(
|
|
11
|
+
resolver: ResolverContext,
|
|
12
|
+
bind: ResolverTelemetryBinding,
|
|
13
|
+
): void {
|
|
14
|
+
telemetryBindings.set(resolver, bind);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Preserve reconstruction across SDK wrappers without capturing their current request sink. */
|
|
18
|
+
export function inheritResolverTelemetryBinding(
|
|
19
|
+
resolver: ResolverContext,
|
|
20
|
+
wrapper: ResolverContext,
|
|
21
|
+
): void {
|
|
22
|
+
const bind = telemetryBindings.get(resolver);
|
|
23
|
+
if (bind) telemetryBindings.set(wrapper, bind);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** SDK chains expose each vendor invocation; opaque host resolvers expose one custom invocation. */
|
|
27
|
+
export function bindResolverTelemetry(
|
|
28
|
+
resolver: ResolverContext,
|
|
29
|
+
sink: ResolverTelemetrySink | undefined,
|
|
30
|
+
): ResolverContext {
|
|
31
|
+
if (!sink) return resolver;
|
|
32
|
+
const metadata = (
|
|
33
|
+
resolver as ResolverContext & {
|
|
34
|
+
readonly [RESOLVER_INSTRUMENTATION_METADATA]?: { readonly target: ResolverContext };
|
|
35
|
+
}
|
|
36
|
+
)[RESOLVER_INSTRUMENTATION_METADATA];
|
|
37
|
+
if (metadata) return bindResolverTelemetry(metadata.target, sink);
|
|
38
|
+
const bind = telemetryBindings.get(resolver);
|
|
39
|
+
if (bind) return bind(sink);
|
|
40
|
+
const wrapper: ResolverSolveWithRecorder = {
|
|
41
|
+
async solve(challenge, signal, recorder) {
|
|
42
|
+
const startedAt = Date.now();
|
|
43
|
+
let outcome: ResolverTelemetryOutcome = "error";
|
|
44
|
+
try {
|
|
45
|
+
const solution = await (resolver as ResolverSolveWithRecorder).solve(
|
|
46
|
+
challenge,
|
|
47
|
+
signal,
|
|
48
|
+
recorder,
|
|
49
|
+
);
|
|
50
|
+
outcome = "solved";
|
|
51
|
+
return solution;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
outcome = signal?.aborted
|
|
54
|
+
? "aborted"
|
|
55
|
+
: error instanceof ProviderError && error.code === "RESOLVER_CHAIN_EXHAUSTED"
|
|
56
|
+
? "exhausted"
|
|
57
|
+
: "error";
|
|
58
|
+
throw error;
|
|
59
|
+
} finally {
|
|
60
|
+
const ms = Date.now() - startedAt;
|
|
61
|
+
sink.recordVendorAttempt({
|
|
62
|
+
vendor: "custom",
|
|
63
|
+
phase: outcome === "solved" ? "poll_result" : "create_task",
|
|
64
|
+
outcome: outcome === "solved" ? "ok" : "error",
|
|
65
|
+
ms,
|
|
66
|
+
});
|
|
67
|
+
sink.recordOutcome({ outcome, solveMs: ms, challengeKind: challenge.kind });
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
// Rebinding this wrapper returns to the original host, never to its previous sink.
|
|
72
|
+
registerResolverTelemetryBinding(wrapper, createOpaqueResolverTelemetryBinding(resolver));
|
|
73
|
+
return wrapper;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function createOpaqueResolverTelemetryBinding(resolver: ResolverContext): ResolverTelemetryBinding {
|
|
77
|
+
return (sink) => bindResolverTelemetry(resolver, sink);
|
|
78
|
+
}
|
|
4
79
|
|
|
5
80
|
export const RESOLVER_INSTRUMENTATION_METADATA = Symbol.for(
|
|
6
81
|
"@apifuse/provider-sdk/runtime/resolver-instrumentation-metadata",
|