@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
package/AUTHORING.md
CHANGED
|
@@ -130,6 +130,12 @@ not list `APIFUSE__PROXY__SMARTPROXY_APP_KEY`,
|
|
|
130
130
|
`APIFUSE__PROXY__NODEMAVEN_PASSWORD` in provider `secrets`; `proxy` contains
|
|
131
131
|
policy intent only.
|
|
132
132
|
|
|
133
|
+
Hosted runtime settings are engine-owned as well: every `APIFUSE__CDP_POOL__*`
|
|
134
|
+
variable, the Cloudflare OCR/STT tokens and account identifier,
|
|
135
|
+
`APIFUSE__OCR__API_KEY`, `APIFUSE__CACHE__KEY_PEPPER`, and
|
|
136
|
+
`APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET`. `defineProvider`
|
|
137
|
+
rejects them in `secrets`; the engine reads them from its own environment.
|
|
138
|
+
|
|
133
139
|
### Factored operations
|
|
134
140
|
|
|
135
141
|
`defineProvider(declaration)` returns the builder that accepts `operations`, so
|
|
@@ -501,6 +507,9 @@ SDK observability is emitted separately in the
|
|
|
501
507
|
```
|
|
502
508
|
|
|
503
509
|
Treat this header as telemetry, not as provider-controlled public error detail.
|
|
510
|
+
It is SDK-owned even when a handler returns a raw `Response`: a caller-set
|
|
511
|
+
header is dropped, and a raw status of 400 or above gets a status-derived one
|
|
512
|
+
(retryable only for 408, 429, and 5xx) while the body is left untouched.
|
|
504
513
|
Its category, taxonomy version, retryability, and optional upstream status match
|
|
505
514
|
the structured `provider_request_failed` log event.
|
|
506
515
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @apifuse/provider-sdk Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.0-beta.59
|
|
4
|
+
|
|
5
|
+
- Release candidate for main commit 55ae556fcf33e28a228c2d54ce76e7afa41e5dc7.
|
|
6
|
+
|
|
7
|
+
## 2.2.0-beta.58
|
|
8
|
+
|
|
9
|
+
- Release candidate for main commit e137940caa8bc32ce768571a6197651e581c2270.
|
|
10
|
+
|
|
3
11
|
## 2.2.0-beta.57
|
|
4
12
|
|
|
5
13
|
- Release candidate for main commit 4087dafdeab771eb91691884ce7315b8c399f863.
|
package/README.md
CHANGED
|
@@ -76,7 +76,14 @@ The operation request body is the same envelope used by the APIFuse gateway:
|
|
|
76
76
|
| `requestId` | yes | Any unique string is fine for local debugging; it is echoed in structured errors. |
|
|
77
77
|
| `input` | yes | The operation input after schema validation. |
|
|
78
78
|
| `headers` | no | Extra caller headers to expose through `ctx.request.headers`. |
|
|
79
|
-
| `
|
|
79
|
+
| `connectionId` | no | Connection identity only; it does not include credentials. The gateway sends it for `optional` connection mode, and only when the caller supplied a connection that passed authorization. Exposed as `ctx.request.connectionId`. |
|
|
80
|
+
| `connection` | no | Credential-bearing connection data. The gateway sends it for `required` connection mode; omit it for no-auth/public (`none` mode) operations. For local debugging, pass an object with `id`, `mode`, `secrets`, `metadata`, and `externalRef`. Do not pass `null`. |
|
|
81
|
+
|
|
82
|
+
The gateway sends only `connection` for `required` mode, only `connectionId`
|
|
83
|
+
for `optional` mode (and nothing when the caller passed no connection), and
|
|
84
|
+
neither field for `none` mode. If a malformed or manually constructed envelope
|
|
85
|
+
contains both, a non-empty nested `connection.id` takes precedence over the
|
|
86
|
+
top-level `connectionId`; an empty string is treated as absent.
|
|
80
87
|
|
|
81
88
|
Credential-bearing local smoke example:
|
|
82
89
|
|
|
@@ -115,9 +122,10 @@ the bad request path; provider/runtime failures include `code`, `message`, and
|
|
|
115
122
|
- **`invalid_request` on `/v1/{operation}`**: confirm the request body includes
|
|
116
123
|
`requestId` and `input`. Omit `connection` for public/no-auth operations;
|
|
117
124
|
never send `connection: null`.
|
|
118
|
-
- **Credential-backed operations**: declare `credential.keys`,
|
|
119
|
-
|
|
120
|
-
|
|
125
|
+
- **Credential-backed operations**: declare `credential.keys`, except when
|
|
126
|
+
`auth.mode` is `platform-managed`; that auth declaration implies an unfiltered
|
|
127
|
+
credential capability. Pass local-only values through `connection.secrets` and
|
|
128
|
+
read them with `ctx.credential.get("key")` or `ctx.credential.getAccessToken()`.
|
|
121
129
|
- **Provider env secrets**: declare `secrets[]`, set values in your shell or
|
|
122
130
|
`.env`, and read only those names through `ctx.env.get("NAME")`. The SDK
|
|
123
131
|
enforces presence of `required: true` declarations before handlers and auth
|
|
@@ -145,7 +153,11 @@ the bad request path; provider/runtime failures include `code`, `message`, and
|
|
|
145
153
|
a session with `ctx.stealth.createSession()` and use `session.redirects.run()`;
|
|
146
154
|
inspect accumulated cookies through `session.cookies`. Select an SDK stealth
|
|
147
155
|
identity with structured `stealth: { browser, os }` options; omitted `os`
|
|
148
|
-
explicitly defaults to `macos`.
|
|
156
|
+
explicitly defaults to `macos`. The transport owns `User-Agent`,
|
|
157
|
+
`Accept-Encoding`, `sec-ch-ua*`, and every `Sec-Fetch-*` header and throws
|
|
158
|
+
`STEALTH_HEADER_OVERRIDE_UNSUPPORTED` if you set one; declare
|
|
159
|
+
`stealth.requestClass: "navigation" | "xhr" | "post"` instead. Do not pin a
|
|
160
|
+
browser version
|
|
149
161
|
or tune JA3, HTTP/2 SETTINGS, or
|
|
150
162
|
pseudo-header order in provider code. Chrome, Firefox, and Safari profiles
|
|
151
163
|
are supported; use `ctx.browser` when the provider needs browser execution.
|
|
@@ -324,3 +336,8 @@ External bounty contributors should submit standalone Provider source plus
|
|
|
324
336
|
`bun run check` / `bun run test` evidence. APIFuse maintainers own monorepo
|
|
325
337
|
import, registry generation, deployment projection checks, and release
|
|
326
338
|
publishing.
|
|
339
|
+
|
|
340
|
+
Relayed user-generated text (listing bodies, reviews, posts) is contained by the
|
|
341
|
+
platform, not by the provider. Wrap such output fields in `untrustedContent(...)`
|
|
342
|
+
so the schema carries `x-apifuse-content-trust: untrusted` and
|
|
343
|
+
`x-apifuse-content-provenance: external`; do not strip instruction-like prose.
|
package/SUBMISSION.md
CHANGED
|
@@ -46,7 +46,8 @@ Fix all blockers before submitting:
|
|
|
46
46
|
- Missing fixture request or response.
|
|
47
47
|
- Fixture data does not parse against schemas.
|
|
48
48
|
- Missing `healthCheck` or `healthCheckUnsupported` on any Operation.
|
|
49
|
-
-
|
|
49
|
+
- User-managed credential auth mode without declared credential keys
|
|
50
|
+
(`platform-managed` auth instead implies the credential capability and forbids keys).
|
|
50
51
|
- High-confidence secret or token material in source, README, package metadata, or fixtures.
|
|
51
52
|
- SDK-native source blockers: prefixed Provider ids, `vendor/` SDK shims or imports, raw `.describe()` prose instead of `describeKey`, raw global `fetch()` calls, and excessive `as Type` assertions.
|
|
52
53
|
|
|
@@ -401,6 +401,55 @@ const NEGATIVE_CONTROLS = [
|
|
|
401
401
|
"",
|
|
402
402
|
].join("\n"),
|
|
403
403
|
},
|
|
404
|
+
{
|
|
405
|
+
filename: "negative-control-resolver-telemetry-description.ts",
|
|
406
|
+
expectedCode: "TS2353",
|
|
407
|
+
description: "resolver gateway telemetry rejects vendor error descriptions",
|
|
408
|
+
source: [
|
|
409
|
+
'import type { ResolverTelemetryHeaderPayload } from "@apifuse/provider-sdk";',
|
|
410
|
+
"",
|
|
411
|
+
"export const mustNotCompile: ResolverTelemetryHeaderPayload = {",
|
|
412
|
+
"\tattempts: 0,",
|
|
413
|
+
"\tfailovers: 0,",
|
|
414
|
+
"\tvendorChain: [],",
|
|
415
|
+
"\tpollCount: 0,",
|
|
416
|
+
'\tvendorErrorDescription: "free text must stay log-only",',
|
|
417
|
+
"};",
|
|
418
|
+
"",
|
|
419
|
+
].join("\n"),
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
filename: "negative-control-resolver-telemetry-phase.ts",
|
|
423
|
+
expectedCode: "TS2322",
|
|
424
|
+
description: "resolver gateway telemetry rejects phases outside its closed vocabulary",
|
|
425
|
+
source: [
|
|
426
|
+
'import { closedEnum, type ResolverTelemetryHeaderPayload } from "@apifuse/provider-sdk";',
|
|
427
|
+
"",
|
|
428
|
+
"export const mustNotCompile: ResolverTelemetryHeaderPayload = {",
|
|
429
|
+
"\tattempts: 1,",
|
|
430
|
+
"\tfailovers: 0,",
|
|
431
|
+
'\tvendorChain: [closedEnum("hypersolutions")],',
|
|
432
|
+
"\tpollCount: 0,",
|
|
433
|
+
'\tattemptSamples: [{ v: closedEnum("hypersolutions"), p: closedEnum("solve_exchange"), o: closedEnum("error"), ms: 1 }],',
|
|
434
|
+
"};",
|
|
435
|
+
"",
|
|
436
|
+
].join("\n"),
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
filename: "positive-control-resolver-telemetry-contributor.ts",
|
|
440
|
+
expectedCode: "",
|
|
441
|
+
description: "resolver telemetry contributor satisfies the gateway-ingestible contract",
|
|
442
|
+
source: [
|
|
443
|
+
'import { closedEnum, ResolverTelemetryCollector, type GatewayIngestible, type ResolverTelemetryHeaderPayload, type ResolverTelemetryLogPayload, type ResolverTelemetryPhase, type TelemetryContributor } from "@apifuse/provider-sdk";',
|
|
444
|
+
"",
|
|
445
|
+
"export const resolver: TelemetryContributor<ResolverTelemetryLogPayload, ResolverTelemetryHeaderPayload> = new ResolverTelemetryCollector();",
|
|
446
|
+
'const hyperPhases: ResolverTelemetryPhase[] = ["measure_ip", "fetch_script", "generate_payload", "post_payload"];',
|
|
447
|
+
'export const hyperPhaseHeader: ResolverTelemetryHeaderPayload = { attempts: 1, failovers: 0, vendorChain: [closedEnum("hypersolutions")], pollCount: 0, attemptSamples: hyperPhases.map((phase) => ({ v: closedEnum("hypersolutions"), p: closedEnum(phase), o: closedEnum("ok"), ms: 1 })) };',
|
|
448
|
+
"declare const header: ResolverTelemetryHeaderPayload;",
|
|
449
|
+
"export const ingestible: GatewayIngestible<ResolverTelemetryHeaderPayload> = header;",
|
|
450
|
+
"",
|
|
451
|
+
].join("\n"),
|
|
452
|
+
},
|
|
404
453
|
] as const;
|
|
405
454
|
|
|
406
455
|
const tempRoot = mkdtempSync(join(tmpdir(), "apifuse-provider-sdk-pack-types-"));
|
package/bin/apifuse-record.ts
CHANGED
|
@@ -832,6 +832,12 @@ function proxyStealthSession(
|
|
|
832
832
|
onResponse(order, response);
|
|
833
833
|
return response;
|
|
834
834
|
},
|
|
835
|
+
replayChallenged: async (...args: Parameters<StealthSession["replayChallenged"]>) => {
|
|
836
|
+
const order = reserveOrder();
|
|
837
|
+
const response = await session.replayChallenged(...args);
|
|
838
|
+
onResponse(order, response);
|
|
839
|
+
return response;
|
|
840
|
+
},
|
|
835
841
|
cookies: session.cookies,
|
|
836
842
|
redirects: {
|
|
837
843
|
run: async (...args: Parameters<StealthSession["redirects"]["run"]>) => {
|
|
@@ -97,9 +97,21 @@ The `POST /v1/{operation}` body is a request envelope:
|
|
|
97
97
|
- `requestId` is required and can be any unique local debugging string.
|
|
98
98
|
- `input` contains the operation input shape.
|
|
99
99
|
- `headers` is optional.
|
|
100
|
-
- `
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
- `connectionId` is optional connection identity only and does not include
|
|
101
|
+
credentials. The gateway sends it for `optional` connection mode, and only
|
|
102
|
+
when the caller supplied a connection that passed authorization. It is
|
|
103
|
+
exposed as `ctx.request.connectionId`.
|
|
104
|
+
- `connection` is optional credential-bearing connection data. The gateway
|
|
105
|
+
sends it for `required` connection mode; omit it for no-auth/public (`none`
|
|
106
|
+
mode) operations. For local debugging, pass
|
|
107
|
+
`{ "id", "mode", "secrets", "metadata", "externalRef" }` with local-only
|
|
108
|
+
secret values.
|
|
109
|
+
|
|
110
|
+
The gateway sends only `connection` for `required` mode, only `connectionId`
|
|
111
|
+
for `optional` mode (and nothing when the caller passed no connection), and
|
|
112
|
+
neither field for `none` mode. If a malformed or manually constructed envelope
|
|
113
|
+
contains both, a non-empty nested `connection.id` takes precedence over the
|
|
114
|
+
top-level `connectionId`; an empty string is treated as absent.
|
|
103
115
|
|
|
104
116
|
Structured errors return an `error` object with `code`, `message`,
|
|
105
117
|
`requestId`, and optional `details`; validation failures include field paths in
|
|
@@ -109,8 +121,10 @@ Structured errors return an `error` object with `code`, `message`,
|
|
|
109
121
|
|
|
110
122
|
- `invalid_request`: include `requestId` and `input`; omit `connection` for
|
|
111
123
|
public/no-auth operations and never send `connection: null`.
|
|
112
|
-
- Credentials: declare `credential.keys
|
|
113
|
-
|
|
124
|
+
- Credentials: declare `credential.keys` unless `auth.mode` is `platform-managed`;
|
|
125
|
+
that mode implies an unfiltered credential capability and forbids key declarations.
|
|
126
|
+
Pass local-only values through `connection.secrets` and read them with
|
|
127
|
+
`ctx.credential`.
|
|
114
128
|
- Auth flow: call `/auth/start`, then `/auth/continue` with the same `flowId`;
|
|
115
129
|
carry returned `contextPatch` values into the next request's `context`.
|
|
116
130
|
- Stealth/browser runtime: keep access-sensitive operations on `ctx.stealth.fetch()` with
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -140,6 +140,13 @@ export declare function providerStateRedisUrlFromEnv(): string | undefined;
|
|
|
140
140
|
/** @internal Test-only hook for exercising shared proxy-cache behavior. */
|
|
141
141
|
export declare function __setProxyRedisForTests(redis: ProxyRedisClient | undefined): void;
|
|
142
142
|
export declare function __setSmartproxyAllocatorDeadlineMsForTests(deadlineMs: number | undefined): void;
|
|
143
|
+
/** @internal Test-only hook for exercising the pool-cache capacity bound at a small size. */
|
|
144
|
+
export declare function __setSmartproxyPoolCacheMaxEntriesForTests(maxEntries: number | undefined): void;
|
|
145
|
+
/** @internal Test-only view of the in-memory Smartproxy cache sizes. */
|
|
146
|
+
export declare function __getSmartproxyCacheSizesForTests(): {
|
|
147
|
+
pools: number;
|
|
148
|
+
tombstones: number;
|
|
149
|
+
};
|
|
143
150
|
export declare function resolveProxyConfig(options?: ProxyResolutionOptions): ResolvedProxyConfig;
|
|
144
151
|
export declare function resolveProxyConfigAsync(options?: ProxyResolutionOptions): Promise<ResolvedProxyConfig>;
|
|
145
152
|
/**
|
package/dist/config/loader.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { BoundedExpiringMap } from "../runtime/bounded-expiring-map.js";
|
|
5
6
|
import { NODEMAVEN_DEFAULT_PROTOCOL, NODEMAVEN_FILTER_ENV, NODEMAVEN_MAX_POOL_SIZE, NODEMAVEN_PASSWORD_ENV, NODEMAVEN_USERNAME_ENV, nodemavenPoolSize, synthesizeNodemavenProxy, } from "../runtime/proxy-nodemaven.js";
|
|
6
7
|
// "smartproxy" here is api.smartproxy.org — a residential proxy with an IP
|
|
7
8
|
// extraction API (app_key → raw ip:port pool). It is NOT the company formerly
|
|
@@ -33,7 +34,13 @@ export class ProxyResolutionError extends Error {
|
|
|
33
34
|
this.protocol = options?.protocol;
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
+
// Keyed per (policy, affinity). Connection affinities are minted by callers, so
|
|
38
|
+
// without a cap a long-lived process accumulates one pool per affinity it ever
|
|
39
|
+
// saw. Evicting a still-fresh pool forces a fresh allocation (a different
|
|
40
|
+
// egress endpoint) for that affinity, so the cap is generous and every
|
|
41
|
+
// capacity eviction is reported as `diagnostics.poolCacheEvictions`.
|
|
42
|
+
const SMARTPROXY_POOL_CACHE_MAX_ENTRIES = 10_000;
|
|
43
|
+
let proxyCache = createSmartproxyPoolCache(SMARTPROXY_POOL_CACHE_MAX_ENTRIES);
|
|
37
44
|
const require = createRequire(import.meta.url);
|
|
38
45
|
const proxyInflight = new Map();
|
|
39
46
|
const invalidatedProxyKeys = new Map();
|
|
@@ -76,6 +83,17 @@ export function __setProxyRedisForTests(redis) {
|
|
|
76
83
|
export function __setSmartproxyAllocatorDeadlineMsForTests(deadlineMs) {
|
|
77
84
|
smartproxyAllocatorDeadlineMsForTests = deadlineMs;
|
|
78
85
|
}
|
|
86
|
+
/** @internal Test-only hook for exercising the pool-cache capacity bound at a small size. */
|
|
87
|
+
export function __setSmartproxyPoolCacheMaxEntriesForTests(maxEntries) {
|
|
88
|
+
proxyCache = createSmartproxyPoolCache(maxEntries ?? SMARTPROXY_POOL_CACHE_MAX_ENTRIES);
|
|
89
|
+
}
|
|
90
|
+
/** @internal Test-only view of the in-memory Smartproxy cache sizes. */
|
|
91
|
+
export function __getSmartproxyCacheSizesForTests() {
|
|
92
|
+
return { pools: proxyCache.size, tombstones: invalidatedProxyKeys.size };
|
|
93
|
+
}
|
|
94
|
+
function createSmartproxyPoolCache(maxEntries) {
|
|
95
|
+
return new BoundedExpiringMap(maxEntries, (pool) => pool.expiresAt);
|
|
96
|
+
}
|
|
79
97
|
function getProxyRedis() {
|
|
80
98
|
if (proxyRedisForTests)
|
|
81
99
|
return proxyRedisForTests;
|
|
@@ -457,6 +475,7 @@ export async function resolveWithVendor(vendor, policy, options, context) {
|
|
|
457
475
|
...allocated.pool.diagnostics,
|
|
458
476
|
poolSize: allocated.pool.urls.length,
|
|
459
477
|
poolIndex,
|
|
478
|
+
poolCacheEvictions: proxyCache.capacityEvictions,
|
|
460
479
|
},
|
|
461
480
|
};
|
|
462
481
|
}
|
|
@@ -695,8 +714,8 @@ async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey,
|
|
|
695
714
|
const now = startedAt;
|
|
696
715
|
const invalidatedUntil = invalidatedProxyKeys.get(cacheKey) ?? 0;
|
|
697
716
|
const skipCached = invalidatedUntil > now;
|
|
698
|
-
const cached = proxyCache.get(cacheKey);
|
|
699
|
-
if (
|
|
717
|
+
const cached = skipCached ? undefined : proxyCache.get(cacheKey, now);
|
|
718
|
+
if (cached) {
|
|
700
719
|
if (shouldSoftRefresh(cached, now)) {
|
|
701
720
|
void refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, protocol, ambientDefaults, sharedCache);
|
|
702
721
|
return {
|
|
@@ -751,7 +770,7 @@ async function readSmartproxyRedisPool(cacheKey, startedAt) {
|
|
|
751
770
|
const now = Date.now();
|
|
752
771
|
if (!isFresh(pool, now))
|
|
753
772
|
return null;
|
|
754
|
-
proxyCache.set(cacheKey, pool);
|
|
773
|
+
proxyCache.set(cacheKey, pool, now);
|
|
755
774
|
return {
|
|
756
775
|
pool,
|
|
757
776
|
telemetry: telemetryForPool(pool, "redis_hit", startedAt, { redisReadMs }),
|
|
@@ -962,7 +981,7 @@ async function allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetime
|
|
|
962
981
|
rawConnect: true,
|
|
963
982
|
},
|
|
964
983
|
};
|
|
965
|
-
proxyCache.set(cacheKey, result);
|
|
984
|
+
proxyCache.set(cacheKey, result, allocatedAt);
|
|
966
985
|
if (options.redis && options.poolKey) {
|
|
967
986
|
const redis = options.redis;
|
|
968
987
|
const poolKey = options.poolKey;
|
|
@@ -1164,7 +1183,12 @@ function markSmartproxyCacheInvalidated(options = {}) {
|
|
|
1164
1183
|
if (!appKey)
|
|
1165
1184
|
return undefined;
|
|
1166
1185
|
const cacheKey = buildSmartproxyCacheKey(policy, appKey, options.affinityKey, lifetimeMinutes, options.protocol ?? VENDOR_DEFAULT_PROTOCOL.smartproxy);
|
|
1167
|
-
|
|
1186
|
+
const now = Date.now();
|
|
1187
|
+
for (const [key, invalidatedUntil] of invalidatedProxyKeys) {
|
|
1188
|
+
if (invalidatedUntil <= now)
|
|
1189
|
+
invalidatedProxyKeys.delete(key);
|
|
1190
|
+
}
|
|
1191
|
+
invalidatedProxyKeys.set(cacheKey, now + SMARTPROXY_INVALIDATION_SKIP_REDIS_MS);
|
|
1168
1192
|
proxyCache.delete(cacheKey);
|
|
1169
1193
|
proxyInflight.delete(cacheKey);
|
|
1170
1194
|
return cacheKey;
|
|
@@ -13,6 +13,7 @@ export declare const DECLARATION_RULE_IDS: {
|
|
|
13
13
|
readonly proxySmartproxyGeo: "proxy-smartproxy-country-only";
|
|
14
14
|
readonly operationUpstreamProxy: "operation-upstream-proxy-unsupported";
|
|
15
15
|
readonly pinnedWireFieldPathValid: "public-schema-pinned-wire-field-valid";
|
|
16
|
+
readonly resolverClientProfileFamily: "resolver-client-profile-family";
|
|
16
17
|
};
|
|
17
18
|
export type DeclarationRuleId = (typeof DECLARATION_RULE_IDS)[keyof typeof DECLARATION_RULE_IDS];
|
|
18
19
|
export type DeclarationViolation = {
|
|
@@ -24,7 +25,7 @@ export type DeclarationViolation = {
|
|
|
24
25
|
export declare function declarationInvalidError(violations: readonly DeclarationViolation[]): ProviderError;
|
|
25
26
|
/** Enforces declaration rules whose runtime behavior would otherwise fail open. */
|
|
26
27
|
export declare function validateFailClosedDeclaration(provider: ProviderDefinition): void;
|
|
27
|
-
type ProviderDeclarationRulesInput = Pick<ProviderDefinition, "healthJourneys" | "meta" | "proxy">;
|
|
28
|
+
type ProviderDeclarationRulesInput = Pick<ProviderDefinition, "healthJourneys" | "meta" | "proxy" | "resolver" | "stealth">;
|
|
28
29
|
type OperationDeclarationRulesInput = Pick<ProviderDefinition, "operations">;
|
|
29
30
|
/** Enforces fail-closed rules that only depend on the provider declaration. */
|
|
30
31
|
export declare function validateFailClosedProviderDeclaration(provider: ProviderDeclarationRulesInput): void;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describeSchema } from "./contract-serialization.js";
|
|
2
2
|
import { ProviderError } from "./errors.js";
|
|
3
3
|
import { HealthScenarioSchema } from "./health-scenario.js";
|
|
4
|
+
import { kindRequiresClientProfile } from "./runtime/resolver-config.js";
|
|
5
|
+
import { resolverClientProfileFamily, resolveStealthProfileSelection } from "./stealth/profiles.js";
|
|
4
6
|
export const DECLARATION_INVALID_CODE = "DECLARATION_INVALID";
|
|
5
7
|
export const DECLARATION_RULE_IDS = {
|
|
6
8
|
challengeShape: "credentials-challenge-shape",
|
|
@@ -14,6 +16,7 @@ export const DECLARATION_RULE_IDS = {
|
|
|
14
16
|
proxySmartproxyGeo: "proxy-smartproxy-country-only",
|
|
15
17
|
operationUpstreamProxy: "operation-upstream-proxy-unsupported",
|
|
16
18
|
pinnedWireFieldPathValid: "public-schema-pinned-wire-field-valid",
|
|
19
|
+
resolverClientProfileFamily: "resolver-client-profile-family",
|
|
17
20
|
};
|
|
18
21
|
export function declarationInvalidError(violations) {
|
|
19
22
|
const summary = violations
|
|
@@ -32,6 +35,7 @@ export function validateFailClosedDeclaration(provider) {
|
|
|
32
35
|
validatePinnedWireFieldPaths(provider, violations);
|
|
33
36
|
validateSchemaDeclaration(provider, violations);
|
|
34
37
|
validateProxyDeclaration(provider, violations);
|
|
38
|
+
validateResolverClientProfileDeclaration(provider, violations);
|
|
35
39
|
validateOperationDeclaration(provider, violations);
|
|
36
40
|
if (violations.length > 0)
|
|
37
41
|
throw declarationInvalidError(violations);
|
|
@@ -54,6 +58,7 @@ function collectProviderDeclarationViolations(provider, violations) {
|
|
|
54
58
|
validateHealthDeclaration(provider, violations);
|
|
55
59
|
validatePinnedWireFieldPaths(provider, violations);
|
|
56
60
|
validateProxyDeclaration(provider, violations);
|
|
61
|
+
validateResolverClientProfileDeclaration(provider, violations);
|
|
57
62
|
}
|
|
58
63
|
function validatePinnedWireFieldPaths(provider, violations) {
|
|
59
64
|
const pins = provider.meta?.contract?.pinnedWireFieldPaths;
|
|
@@ -245,6 +250,40 @@ function validateProxyDeclaration(provider, violations) {
|
|
|
245
250
|
}
|
|
246
251
|
}
|
|
247
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* An akamai_sbsd solve runs on the provider's own stealth session, so the declared
|
|
255
|
+
* resolver.clientProfile must name the browser family that session emulates. The
|
|
256
|
+
* stealth runtime re-checks per request (a fetch may override the browser); this
|
|
257
|
+
* rule fails the declaration instead of the first challenged request.
|
|
258
|
+
*/
|
|
259
|
+
function validateResolverClientProfileDeclaration(provider, violations) {
|
|
260
|
+
const resolver = provider.resolver;
|
|
261
|
+
if (!resolver ||
|
|
262
|
+
!Array.isArray(resolver.kinds) ||
|
|
263
|
+
!resolver.kinds.some(kindRequiresClientProfile) ||
|
|
264
|
+
typeof resolver.clientProfile !== "string") {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
let sessionBrowser;
|
|
268
|
+
try {
|
|
269
|
+
sessionBrowser = resolveStealthProfileSelection(provider.stealth).browser;
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
// An unsupported stealth selection is reported by the stealth runtime itself.
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const family = resolverClientProfileFamily(resolver.clientProfile);
|
|
276
|
+
if (family === sessionBrowser)
|
|
277
|
+
return;
|
|
278
|
+
violations.push({
|
|
279
|
+
ruleId: DECLARATION_RULE_IDS.resolverClientProfileFamily,
|
|
280
|
+
path: "resolver.clientProfile",
|
|
281
|
+
message: family
|
|
282
|
+
? `"${resolver.clientProfile}" names ${family}, but the stealth session browser is ${sessionBrowser}.`
|
|
283
|
+
: `"${resolver.clientProfile}" does not name a supported browser family (chrome, firefox, safari).`,
|
|
284
|
+
fix: `Declare stealth.browser as the family resolver.clientProfile names, or set resolver.clientProfile to the ${sessionBrowser} profile the SBSD session runs on.`,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
248
287
|
function declaredProxyVendors(policy) {
|
|
249
288
|
const vendors = [...(policy.providers ?? [])];
|
|
250
289
|
if (policy.provider !== undefined)
|
package/dist/define.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AuthConfig, BrowserEngine, ContextDeclaration, CredentialDeclaration, HealthJourneyDefinition, HealthJourneySchedule, HealthScheduleRandomization, InferSchemaOutput, NativeProviderConfig, OperationDefinition, OperationHandlerResult, OperationHttpStreamTransport, OperationSseTransport, OperationWebSocketTransport, ProviderAccessConfig, ProviderContext, ProviderDefinition, ProviderDeploymentOverrides, ProviderHealthMonitorConfig, ProviderOcrConfig, ProviderProxyConfig, ProviderPublicProfile, ProviderResolverConfig, ProviderReviewed, ProviderRuntimeTarget, ProviderSecretDeclaration, ProviderStreamEvent, ProviderSttConfig, SchemaLike, SmsOtpMatcherDefinition
|
|
1
|
+
import type { AuthConfig, BrowserEngine, ContextDeclaration, CredentialDeclaration, HealthJourneyDefinition, HealthJourneySchedule, HealthScheduleRandomization, InferSchemaOutput, NativeProviderConfig, OperationDefinition, OperationHandlerResult, OperationHttpStreamTransport, OperationSseTransport, OperationWebSocketTransport, ProviderAccessConfig, ProviderContext, ProviderDefinition, ProviderDeploymentOverrides, ProviderHealthMonitorConfig, ProviderOcrConfig, ProviderProxyConfig, ProviderPublicProfile, ProviderResolverConfig, ProviderReviewed, ProviderRuntimeTarget, ProviderSecretDeclaration, ProviderStreamEvent, ProviderSttConfig, ProviderStealthConfig, SchemaLike, SmsOtpMatcherDefinition } from "./types.js";
|
|
2
2
|
type ProviderImplementationSourceAccess = "official_api" | "private_api" | "browser_flow" | "hybrid";
|
|
3
3
|
type ProviderImplementationCredentialStrategy = "apifuse_managed" | "workspace_secret" | "user_oauth" | "user_session" | "none";
|
|
4
4
|
interface ProviderImplementationProfile {
|
|
@@ -8,8 +8,8 @@ interface ProviderImplementationProfile {
|
|
|
8
8
|
operatorNotes?: string;
|
|
9
9
|
visibility: "internal" | "operator";
|
|
10
10
|
}
|
|
11
|
-
export declare const VALID_PROVIDER_RESOLVER_VENDORS: readonly ["browser", "capsolver", "capmonster", "2captcha", "custom"];
|
|
12
|
-
export declare const VALID_PROVIDER_CHALLENGE_KINDS: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf", "akamai_sec_cpt", "akamai_sensor"];
|
|
11
|
+
export declare const VALID_PROVIDER_RESOLVER_VENDORS: readonly ["browser", "capsolver", "capmonster", "2captcha", "hypersolutions", "custom"];
|
|
12
|
+
export declare const VALID_PROVIDER_CHALLENGE_KINDS: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf", "akamai_sec_cpt", "akamai_sensor", "akamai_sbsd"];
|
|
13
13
|
type ProviderOperation = OperationDefinition<any, any, any>;
|
|
14
14
|
type OperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike, TContext = ProviderContext> = Omit<OperationDefinition<TInput, TOutput, TContext>, "handler"> & {
|
|
15
15
|
handler(ctx: TContext, input: InferSchemaOutput<TInput>): OperationHandlerResult<InferSchemaOutput<TOutput>> | Promise<OperationHandlerResult<InferSchemaOutput<TOutput>>>;
|
|
@@ -60,7 +60,7 @@ export interface ProviderDeclaration {
|
|
|
60
60
|
/** Declares upstream host policy; this does not add a `ctx.allowedHosts` member. */
|
|
61
61
|
allowedHosts?: string[];
|
|
62
62
|
native?: NativeProviderConfig;
|
|
63
|
-
stealth?:
|
|
63
|
+
stealth?: ProviderStealthConfig;
|
|
64
64
|
/** Declares proxy policy; this is provider intent and does not add a `ctx.proxy` member. */
|
|
65
65
|
proxy?: ProviderProxyConfig;
|
|
66
66
|
ocr?: ProviderOcrConfig;
|
|
@@ -78,6 +78,7 @@ export interface ProviderDeclaration {
|
|
|
78
78
|
secrets?: ProviderSecretDeclaration[];
|
|
79
79
|
/** Declares the environment capability binding. A bare object states use without configuration. */
|
|
80
80
|
env?: Record<string, never> | true;
|
|
81
|
+
/** Declares credential keys; platform-managed auth declares this capability without key filtering. */
|
|
81
82
|
credential?: CredentialDeclaration;
|
|
82
83
|
/** Declares provider context metadata; this does not add a `ctx.context` member. */
|
|
83
84
|
context?: ContextDeclaration;
|
package/dist/define.js
CHANGED
|
@@ -2,8 +2,9 @@ import ms from "ms";
|
|
|
2
2
|
import { validateFailClosedOperationDeclaration, validateFailClosedProviderDeclaration, } from "./declaration-validation.js";
|
|
3
3
|
import { SDK_RUNTIME_OWNED_ERROR_CODES } from "./error-resolution.js";
|
|
4
4
|
import { ProviderError, ValidationError } from "./errors.js";
|
|
5
|
-
import { isEngineOwnedProxyCredentialName, isEngineOwnedTelemetryEnvName } from "./engine.js";
|
|
5
|
+
import { engineOwnedResolverCredentialTarget, isEngineOwnedEnvName, isEngineOwnedProxyCredentialName, isEngineOwnedRuntimeEnvName, isEngineOwnedTelemetryEnvName, } from "./engine.js";
|
|
6
6
|
import { HealthScenarioSchema } from "./health-scenario.js";
|
|
7
|
+
import { kindRequiresClientProfile } from "./runtime/resolver-config.js";
|
|
7
8
|
import { NativeEgressPolicyValidationError, validateNativeProviderConfig, } from "./native-egress-policy.js";
|
|
8
9
|
import { safeParseSchemaSync } from "./schema.js";
|
|
9
10
|
import { resolveHealthCheckInputDateTokens } from "./server/self-test-input-tokens.js";
|
|
@@ -61,6 +62,7 @@ export const VALID_PROVIDER_RESOLVER_VENDORS = exhaustiveLiteralArray()([
|
|
|
61
62
|
"capsolver",
|
|
62
63
|
"capmonster",
|
|
63
64
|
"2captcha",
|
|
65
|
+
"hypersolutions",
|
|
64
66
|
"custom",
|
|
65
67
|
]);
|
|
66
68
|
export const VALID_PROVIDER_CHALLENGE_KINDS = exhaustiveLiteralArray()([
|
|
@@ -72,6 +74,7 @@ export const VALID_PROVIDER_CHALLENGE_KINDS = exhaustiveLiteralArray()([
|
|
|
72
74
|
"aws_waf",
|
|
73
75
|
"akamai_sec_cpt",
|
|
74
76
|
"akamai_sensor",
|
|
77
|
+
"akamai_sbsd",
|
|
75
78
|
]);
|
|
76
79
|
const RESERVED_OPERATION_IDS = new Set(["auth", "health"]);
|
|
77
80
|
const VALID_OPERATION_RISK_CLASSES = ["read", "write", "destructive", "external-send"];
|
|
@@ -633,11 +636,27 @@ function validateProviderImplementationShape(config) {
|
|
|
633
636
|
}
|
|
634
637
|
function validateProviderProxy(config) {
|
|
635
638
|
for (const secret of config.secrets ?? []) {
|
|
639
|
+
const engineName = engineOwnedResolverCredentialTarget(secret.name);
|
|
640
|
+
if (engineName !== undefined) {
|
|
641
|
+
throw new ValidationError(`Provider "${config.id}" cannot declare engine-owned resolver credential "${secret.name}"`, {
|
|
642
|
+
fix: `Remove "${secret.name}" from provider secrets; configure the solver key only on the provider engine as ${engineName}.`,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
636
645
|
if (isEngineOwnedTelemetryEnvName(secret.name)) {
|
|
637
646
|
throw new ValidationError(`Provider "${config.id}" cannot declare engine-owned telemetry variable "${secret.name}"`, {
|
|
638
647
|
fix: `Remove "${secret.name}" from provider secrets; trace export is configured only on the provider engine.`,
|
|
639
648
|
});
|
|
640
649
|
}
|
|
650
|
+
if (isEngineOwnedRuntimeEnvName(secret.name)) {
|
|
651
|
+
throw new ValidationError(`Provider "${config.id}" cannot declare engine-owned runtime variable "${secret.name}"`, {
|
|
652
|
+
fix: `Remove "${secret.name}" from provider secrets; the provider engine reads it from its own environment.`,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
if (isEngineOwnedEnvName(secret.name) && !isEngineOwnedProxyCredentialName(secret.name)) {
|
|
656
|
+
throw new ValidationError(`Provider "${config.id}" cannot declare engine-owned control variable "${secret.name}"`, {
|
|
657
|
+
fix: `Remove "${secret.name}" from provider secrets; configure it only on the provider engine.`,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
641
660
|
if (!isEngineOwnedProxyCredentialName(secret.name))
|
|
642
661
|
continue;
|
|
643
662
|
throw new ValidationError(`Provider "${config.id}" cannot declare engine-owned proxy credential "${secret.name}"`, {
|
|
@@ -785,6 +804,11 @@ function validateProviderResolver(config) {
|
|
|
785
804
|
fix: `Set resolver.clientProfile for provider "${config.id}" to a transport-owned profile name.`,
|
|
786
805
|
});
|
|
787
806
|
}
|
|
807
|
+
if (resolver.kinds.some(kindRequiresClientProfile) && resolver.clientProfile === undefined) {
|
|
808
|
+
throw new ValidationError(`Provider "${config.id}" must declare resolver.clientProfile for akamai_sbsd.`, {
|
|
809
|
+
fix: `Set resolver.clientProfile for provider "${config.id}" to the transport-owned profile used by its Akamai session.`,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
788
812
|
}
|
|
789
813
|
function validateResolverLiteralArray(value, field, validValues, providerId) {
|
|
790
814
|
if (!Array.isArray(value)) {
|
package/dist/engine.d.ts
CHANGED
|
@@ -4,6 +4,15 @@ export declare const PROVIDER_ENGINE_PROTOCOL_VERSION: "provider-engine.v1";
|
|
|
4
4
|
/** Credential names owned by the engine and forbidden in provider declarations. */
|
|
5
5
|
export declare const ENGINE_OWNED_PROXY_CREDENTIAL_ENV_NAMES: readonly ["APIFUSE__PROXY__SMARTPROXY_APP_KEY", "APIFUSE__PROXY__NODEMAVEN_USERNAME", "APIFUSE__PROXY__NODEMAVEN_PASSWORD"];
|
|
6
6
|
export declare function isEngineOwnedProxyCredentialName(name: string): boolean;
|
|
7
|
+
/** Hosted resolver credentials owned by the engine and never projected to providers. */
|
|
8
|
+
export declare const ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAMES: readonly ["APIFUSE__RESOLVER__2CAPTCHA__API_KEY", "APIFUSE__RESOLVER__CAPSOLVER__API_KEY", "APIFUSE__RESOLVER__CAPMONSTER__API_KEY", "APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY"];
|
|
9
|
+
/**
|
|
10
|
+
* Engine variable a provider-declared secret name resolves to, or `undefined` when the
|
|
11
|
+
* name is not an engine-owned resolver credential. Provider-scoped Hyper aliases
|
|
12
|
+
* (`APIFUSE__PROVIDER__<ID>__HYPER_API_KEY`, measured on zozotown) map to the shared key.
|
|
13
|
+
*/
|
|
14
|
+
export declare function engineOwnedResolverCredentialTarget(name: string): string | undefined;
|
|
15
|
+
export declare function isEngineOwnedResolverCredentialName(name: string): boolean;
|
|
7
16
|
/**
|
|
8
17
|
* Trace-export configuration owned by the engine and forbidden in provider
|
|
9
18
|
* declarations. The header variables carry collector credentials; the rest are
|
|
@@ -11,6 +20,13 @@ export declare function isEngineOwnedProxyCredentialName(name: string): boolean;
|
|
|
11
20
|
*/
|
|
12
21
|
export declare const ENGINE_OWNED_TELEMETRY_ENV_NAMES: readonly ["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "OTEL_EXPORTER_OTLP_ENDPOINT", "OTEL_EXPORTER_OTLP_TRACES_HEADERS", "OTEL_EXPORTER_OTLP_HEADERS", "OTEL_SERVICE_NAME", "OTEL_RESOURCE_ATTRIBUTES"];
|
|
13
22
|
export declare function isEngineOwnedTelemetryEnvName(name: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Hosted runtime settings the engine reads for OCR, STT, cache keying, choice
|
|
25
|
+
* tokens, and the CDP pool. Providers never own them: declarations are rejected
|
|
26
|
+
* and projections drop them, like the vendor credentials above.
|
|
27
|
+
*/
|
|
28
|
+
export declare const ENGINE_OWNED_RUNTIME_ENV_NAMES: readonly ["APIFUSE__STT__CLOUDFLARE_API_TOKEN", "APIFUSE__OCR__CLOUDFLARE_API_TOKEN", "APIFUSE__OCR__API_KEY", "APIFUSE__CLOUDFLARE__ACCOUNT_ID", "APIFUSE__CACHE__KEY_PEPPER", "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET"];
|
|
29
|
+
export declare function isEngineOwnedRuntimeEnvName(name: string): boolean;
|
|
14
30
|
/** Every environment name the engine owns: rejected in declarations and filtered from provider projections. */
|
|
15
31
|
export declare function isEngineOwnedEnvName(name: string): boolean;
|
|
16
32
|
/** Capture credentials in the engine host before constructing provider bindings. */
|
package/dist/engine.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ProviderError } from "./errors.js";
|
|
2
|
+
import { APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY, } from "./runtime/resolver-config.js";
|
|
2
3
|
import { OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_HEADERS, OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME, } from "./runtime/otlp.js";
|
|
3
4
|
/** Versioned envelope protocol used by out-of-process engine transports. */
|
|
4
5
|
export const PROVIDER_ENGINE_PROTOCOL_VERSION = "provider-engine.v1";
|
|
@@ -20,6 +21,31 @@ function canonicalEnvName(name) {
|
|
|
20
21
|
export function isEngineOwnedProxyCredentialName(name) {
|
|
21
22
|
return ENGINE_OWNED_PROXY_CREDENTIAL_ENV_NAME_SET.has(canonicalEnvName(name));
|
|
22
23
|
}
|
|
24
|
+
/** Hosted resolver credentials owned by the engine and never projected to providers. */
|
|
25
|
+
export const ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAMES = [
|
|
26
|
+
APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
|
|
27
|
+
APIFUSE__RESOLVER__CAPSOLVER__API_KEY,
|
|
28
|
+
APIFUSE__RESOLVER__CAPMONSTER__API_KEY,
|
|
29
|
+
APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY,
|
|
30
|
+
];
|
|
31
|
+
const ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAME_SET = new Set(ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAMES);
|
|
32
|
+
/**
|
|
33
|
+
* Engine variable a provider-declared secret name resolves to, or `undefined` when the
|
|
34
|
+
* name is not an engine-owned resolver credential. Provider-scoped Hyper aliases
|
|
35
|
+
* (`APIFUSE__PROVIDER__<ID>__HYPER_API_KEY`, measured on zozotown) map to the shared key.
|
|
36
|
+
*/
|
|
37
|
+
export function engineOwnedResolverCredentialTarget(name) {
|
|
38
|
+
const canonical = canonicalEnvName(name);
|
|
39
|
+
if (ENGINE_OWNED_RESOLVER_CREDENTIAL_ENV_NAME_SET.has(canonical))
|
|
40
|
+
return canonical;
|
|
41
|
+
if (/^APIFUSE__PROVIDER__.+__HYPER_API_KEY$/u.test(canonical)) {
|
|
42
|
+
return APIFUSE__RESOLVER__HYPERSOLUTIONS__API_KEY;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
export function isEngineOwnedResolverCredentialName(name) {
|
|
47
|
+
return engineOwnedResolverCredentialTarget(name) !== undefined;
|
|
48
|
+
}
|
|
23
49
|
/**
|
|
24
50
|
* Trace-export configuration owned by the engine and forbidden in provider
|
|
25
51
|
* declarations. The header variables carry collector credentials; the rest are
|
|
@@ -37,9 +63,38 @@ const ENGINE_OWNED_TELEMETRY_ENV_NAME_SET = new Set(ENGINE_OWNED_TELEMETRY_ENV_N
|
|
|
37
63
|
export function isEngineOwnedTelemetryEnvName(name) {
|
|
38
64
|
return ENGINE_OWNED_TELEMETRY_ENV_NAME_SET.has(canonicalEnvName(name));
|
|
39
65
|
}
|
|
66
|
+
/** Engine control-plane configuration is never declarable or visible to provider code. */
|
|
67
|
+
function isEngineOwnedEngineEnvName(name) {
|
|
68
|
+
return canonicalEnvName(name).startsWith("APIFUSE__ENGINE__");
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Hosted runtime settings the engine reads for OCR, STT, cache keying, choice
|
|
72
|
+
* tokens, and the CDP pool. Providers never own them: declarations are rejected
|
|
73
|
+
* and projections drop them, like the vendor credentials above.
|
|
74
|
+
*/
|
|
75
|
+
export const ENGINE_OWNED_RUNTIME_ENV_NAMES = [
|
|
76
|
+
"APIFUSE__STT__CLOUDFLARE_API_TOKEN",
|
|
77
|
+
"APIFUSE__OCR__CLOUDFLARE_API_TOKEN",
|
|
78
|
+
"APIFUSE__OCR__API_KEY",
|
|
79
|
+
"APIFUSE__CLOUDFLARE__ACCOUNT_ID",
|
|
80
|
+
"APIFUSE__CACHE__KEY_PEPPER",
|
|
81
|
+
"APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET",
|
|
82
|
+
];
|
|
83
|
+
const ENGINE_OWNED_RUNTIME_ENV_NAME_SET = new Set(ENGINE_OWNED_RUNTIME_ENV_NAMES);
|
|
84
|
+
/** CDP pool settings form a prefix family (`APIFUSE__CDP_POOL__URL` today); the whole prefix is engine-owned. */
|
|
85
|
+
const ENGINE_OWNED_CDP_POOL_ENV_PREFIX = "APIFUSE__CDP_POOL__";
|
|
86
|
+
export function isEngineOwnedRuntimeEnvName(name) {
|
|
87
|
+
const canonical = canonicalEnvName(name);
|
|
88
|
+
return (ENGINE_OWNED_RUNTIME_ENV_NAME_SET.has(canonical) ||
|
|
89
|
+
canonical.startsWith(ENGINE_OWNED_CDP_POOL_ENV_PREFIX));
|
|
90
|
+
}
|
|
40
91
|
/** Every environment name the engine owns: rejected in declarations and filtered from provider projections. */
|
|
41
92
|
export function isEngineOwnedEnvName(name) {
|
|
42
|
-
return
|
|
93
|
+
return (isEngineOwnedEngineEnvName(name) ||
|
|
94
|
+
isEngineOwnedProxyCredentialName(name) ||
|
|
95
|
+
isEngineOwnedResolverCredentialName(name) ||
|
|
96
|
+
isEngineOwnedRuntimeEnvName(name) ||
|
|
97
|
+
isEngineOwnedTelemetryEnvName(name));
|
|
43
98
|
}
|
|
44
99
|
/** Capture credentials in the engine host before constructing provider bindings. */
|
|
45
100
|
export function readEngineProxyCredentials(environment = process.env) {
|
|
@@ -75,6 +130,8 @@ export const PROVIDER_CAPABILITY_KEYS = [
|
|
|
75
130
|
];
|
|
76
131
|
const CAPABILITY_KEY_SET = new Set(PROVIDER_CAPABILITY_KEYS);
|
|
77
132
|
function declaresCapability(provider, capability) {
|
|
133
|
+
if (capability === "credential" && provider.auth?.mode === "platform-managed")
|
|
134
|
+
return true;
|
|
78
135
|
return Object.hasOwn(provider, capability) && provider[capability] !== undefined;
|
|
79
136
|
}
|
|
80
137
|
function attachmentError(provider, capability) {
|