@apifuse/provider-sdk 2.2.0-beta.25 → 2.2.0-beta.26
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 +7 -6
- package/CHANGELOG.md +5 -1
- package/README.md +3 -3
- package/bin/apifuse-check.ts +62 -3
- package/bin/apifuse-pack-check.ts +8 -2
- package/bin/apifuse-pack-smoke.ts +43 -2
- package/bin/apifuse-pack-types.ts +58 -0
- package/dist/auth.js +29 -0
- package/dist/cli/templates/provider/README.md.tpl +4 -4
- package/dist/contract-serialization.d.ts +20 -1
- package/dist/contract-serialization.js +583 -8
- package/dist/contract.d.ts +2 -0
- package/dist/contract.js +9 -5
- package/dist/declaration-validation.d.ts +23 -0
- package/dist/declaration-validation.js +159 -0
- package/dist/define.d.ts +1 -1
- package/dist/define.js +13 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -2
- package/dist/lint.js +85 -3
- package/dist/provider.d.ts +1 -1
- package/dist/provider.js +1 -1
- package/dist/runtime/resolver-vendors/bindings.d.ts +42 -2
- package/dist/runtime/resolver-vendors/bindings.js +31 -6
- package/dist/runtime/resolver-vendors/browser.d.ts +3 -7
- package/dist/runtime/resolver-vendors/browser.js +7 -22
- package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
- package/dist/runtime/resolver-vendors/hosts.js +33 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +23 -0
- package/dist/runtime/resolver-vendors/twocaptcha.js +264 -0
- package/dist/runtime/resolver-vendors/types.d.ts +44 -3
- package/dist/runtime/resolver-vendors/types.js +10 -0
- package/dist/runtime/resolver.d.ts +17 -2
- package/dist/runtime/resolver.js +237 -15
- package/dist/runtime/stealth.d.ts +26 -4
- package/dist/runtime/stealth.js +224 -114
- package/dist/schema.d.ts +63 -0
- package/dist/schema.js +808 -8
- package/dist/server/serve.js +8 -0
- package/dist/stealth/profiles.js +16 -7
- package/dist/types.d.ts +34 -1
- package/package.json +2 -2
- package/src/auth.ts +40 -0
- package/src/cli/templates/provider/README.md.tpl +4 -4
- package/src/contract-serialization.ts +857 -8
- package/src/contract.ts +16 -5
- package/src/declaration-validation.ts +202 -0
- package/src/define.ts +23 -2
- package/src/index.ts +12 -0
- package/src/lint.ts +98 -3
- package/src/provider.ts +10 -0
- package/src/runtime/resolver-vendors/bindings.ts +40 -15
- package/src/runtime/resolver-vendors/browser.ts +9 -31
- package/src/runtime/resolver-vendors/hosts.ts +38 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +366 -0
- package/src/runtime/resolver-vendors/types.ts +54 -0
- package/src/runtime/resolver.ts +304 -24
- package/src/runtime/stealth.ts +317 -136
- package/src/schema.ts +1060 -9
- package/src/server/serve.ts +8 -0
- package/src/stealth/profiles.ts +17 -7
- package/src/types.ts +36 -3
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ProviderError } from "./errors.js";
|
|
2
|
+
import type { ProviderDefinition } from "./types.js";
|
|
3
|
+
export declare const DECLARATION_INVALID_CODE = "DECLARATION_INVALID";
|
|
4
|
+
export declare const DECLARATION_RULE_IDS: {
|
|
5
|
+
readonly challengeShape: "credentials-challenge-shape";
|
|
6
|
+
readonly journeyExecutable: "health-journey-executable";
|
|
7
|
+
readonly schemaSerializable: "operation-schema-serializable";
|
|
8
|
+
readonly proxyExplicitPolicy: "proxy-explicit-policy";
|
|
9
|
+
readonly proxyVendorExclusive: "proxy-vendor-fields-exclusive";
|
|
10
|
+
readonly proxyNoMixedVendors: "proxy-no-mixed-vendors";
|
|
11
|
+
readonly proxySmartproxyGeo: "proxy-smartproxy-country-only";
|
|
12
|
+
readonly operationUpstreamProxy: "operation-upstream-proxy-unsupported";
|
|
13
|
+
};
|
|
14
|
+
export type DeclarationRuleId = (typeof DECLARATION_RULE_IDS)[keyof typeof DECLARATION_RULE_IDS];
|
|
15
|
+
export type DeclarationViolation = {
|
|
16
|
+
ruleId: DeclarationRuleId;
|
|
17
|
+
path: string;
|
|
18
|
+
message: string;
|
|
19
|
+
fix: string;
|
|
20
|
+
};
|
|
21
|
+
export declare function declarationInvalidError(violations: readonly DeclarationViolation[]): ProviderError;
|
|
22
|
+
/** Enforces declaration rules whose runtime behavior would otherwise fail open. */
|
|
23
|
+
export declare function validateFailClosedDeclaration(provider: ProviderDefinition): void;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describeSchema } from "./contract-serialization.js";
|
|
2
|
+
import { ProviderError } from "./errors.js";
|
|
3
|
+
export const DECLARATION_INVALID_CODE = "DECLARATION_INVALID";
|
|
4
|
+
export const DECLARATION_RULE_IDS = {
|
|
5
|
+
challengeShape: "credentials-challenge-shape",
|
|
6
|
+
journeyExecutable: "health-journey-executable",
|
|
7
|
+
schemaSerializable: "operation-schema-serializable",
|
|
8
|
+
proxyExplicitPolicy: "proxy-explicit-policy",
|
|
9
|
+
proxyVendorExclusive: "proxy-vendor-fields-exclusive",
|
|
10
|
+
proxyNoMixedVendors: "proxy-no-mixed-vendors",
|
|
11
|
+
proxySmartproxyGeo: "proxy-smartproxy-country-only",
|
|
12
|
+
operationUpstreamProxy: "operation-upstream-proxy-unsupported",
|
|
13
|
+
};
|
|
14
|
+
export function declarationInvalidError(violations) {
|
|
15
|
+
const summary = violations
|
|
16
|
+
.map((violation) => `${violation.path} [${violation.ruleId}]: ${violation.message}`)
|
|
17
|
+
.join("\n");
|
|
18
|
+
return new ProviderError(`Provider declaration is invalid (${violations.length} violation${violations.length === 1 ? "" : "s"}).${summary ? `\n${summary}` : ""}`, {
|
|
19
|
+
code: DECLARATION_INVALID_CODE,
|
|
20
|
+
details: { violations: [...violations] },
|
|
21
|
+
fix: "Apply every violation's fix hint, then validate the declaration again.",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** Enforces declaration rules whose runtime behavior would otherwise fail open. */
|
|
25
|
+
export function validateFailClosedDeclaration(provider) {
|
|
26
|
+
const violations = [];
|
|
27
|
+
validateHealthDeclaration(provider, violations);
|
|
28
|
+
validateSchemaDeclaration(provider, violations);
|
|
29
|
+
validateProxyDeclaration(provider, violations);
|
|
30
|
+
validateOperationDeclaration(provider, violations);
|
|
31
|
+
if (violations.length > 0)
|
|
32
|
+
throw declarationInvalidError(violations);
|
|
33
|
+
}
|
|
34
|
+
function validateHealthDeclaration(provider, violations) {
|
|
35
|
+
for (const [index, journey] of (provider.healthJourneys ?? []).entries()) {
|
|
36
|
+
if (!journey || typeof journey !== "object")
|
|
37
|
+
continue;
|
|
38
|
+
if (typeof journey.run !== "function") {
|
|
39
|
+
const journeyPath = healthJourneyPath(journey, index);
|
|
40
|
+
violations.push({
|
|
41
|
+
ruleId: DECLARATION_RULE_IDS.journeyExecutable,
|
|
42
|
+
path: `${journeyPath}.run`,
|
|
43
|
+
message: "coversOperations cannot provide health coverage without executable run logic.",
|
|
44
|
+
fix: `Add an async run(ctx) implementation to ${journeyPath}.`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// NOTE: healthCheck.cases[].enabled is intentionally NOT validated here.
|
|
49
|
+
// self-test.ts reports a gated case as status "skipped" with skipReason
|
|
50
|
+
// "disabled", so the skip is visible in results rather than silent — it is a
|
|
51
|
+
// supported conditional-execution feature, not a class-1 silent no-op.
|
|
52
|
+
}
|
|
53
|
+
function healthJourneyPath(journey, index) {
|
|
54
|
+
return typeof journey.id === "string" && journey.id.length > 0
|
|
55
|
+
? `healthJourneys.${journey.id}`
|
|
56
|
+
: `healthJourneys[${index}]`;
|
|
57
|
+
}
|
|
58
|
+
function validateSchemaDeclaration(provider, violations) {
|
|
59
|
+
for (const [operationId, operation] of Object.entries(provider.operations ?? {})) {
|
|
60
|
+
const schemaEntries = [
|
|
61
|
+
[`operations.${operationId}.input`, operation.input],
|
|
62
|
+
[`operations.${operationId}.output`, operation.output],
|
|
63
|
+
];
|
|
64
|
+
// SSE event schemas reach contract extraction the same way input/output do,
|
|
65
|
+
// so a transform-bearing event schema would abort extraction at runtime while
|
|
66
|
+
// passing declaration checks. Validate them under the same rule.
|
|
67
|
+
const transport = operation.transport;
|
|
68
|
+
if (transport?.kind === "sse") {
|
|
69
|
+
for (const [eventName, eventSchema] of Object.entries(transport.events ?? {})) {
|
|
70
|
+
schemaEntries.push([
|
|
71
|
+
`operations.${operationId}.transport.events.${eventName}`,
|
|
72
|
+
eventSchema,
|
|
73
|
+
]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const [path, schema] of schemaEntries) {
|
|
77
|
+
try {
|
|
78
|
+
describeSchema(schema);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
82
|
+
violations.push({
|
|
83
|
+
ruleId: DECLARATION_RULE_IDS.schemaSerializable,
|
|
84
|
+
path,
|
|
85
|
+
message: `schema conversion to JSON Schema failed: ${reason}`,
|
|
86
|
+
fix: `Replace unsupported constructs in ${path} so z.toJSONSchema() succeeds.`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const MANAGED_PROXY_VENDORS = new Set(["smartproxy", "nodemaven"]);
|
|
93
|
+
const STATIC_PROXY_VENDORS = new Set(["custom", "decodo"]);
|
|
94
|
+
function validateProxyDeclaration(provider, violations) {
|
|
95
|
+
if (provider.proxy === true) {
|
|
96
|
+
violations.push({
|
|
97
|
+
ruleId: DECLARATION_RULE_IDS.proxyExplicitPolicy,
|
|
98
|
+
path: "proxy",
|
|
99
|
+
message: "proxy: true does not require resolvable proxy egress.",
|
|
100
|
+
fix: 'Replace proxy: true with an explicit policy such as proxy: { mode: "required", providers: ["smartproxy"] }.',
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!provider.proxy || typeof provider.proxy !== "object")
|
|
105
|
+
return;
|
|
106
|
+
const policy = provider.proxy;
|
|
107
|
+
const hasProvider = policy.provider !== undefined;
|
|
108
|
+
const hasProviders = policy.providers !== undefined;
|
|
109
|
+
if (hasProvider && hasProviders) {
|
|
110
|
+
violations.push({
|
|
111
|
+
ruleId: DECLARATION_RULE_IDS.proxyVendorExclusive,
|
|
112
|
+
path: "proxy",
|
|
113
|
+
message: "provider and providers are ambiguous when declared together.",
|
|
114
|
+
fix: "Keep either proxy.provider or proxy.providers, and remove the other field.",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const vendors = declaredProxyVendors(policy);
|
|
118
|
+
if (vendors.some((vendor) => MANAGED_PROXY_VENDORS.has(vendor)) &&
|
|
119
|
+
vendors.some((vendor) => STATIC_PROXY_VENDORS.has(vendor))) {
|
|
120
|
+
violations.push({
|
|
121
|
+
ruleId: DECLARATION_RULE_IDS.proxyNoMixedVendors,
|
|
122
|
+
path: hasProviders ? "proxy.providers" : "proxy.provider",
|
|
123
|
+
message: "managed and deprecated static proxy vendors cannot share a chain.",
|
|
124
|
+
fix: "Use only smartproxy/nodemaven vendors, or only deprecated static markers, in one policy.",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (vendors.includes("smartproxy")) {
|
|
128
|
+
for (const field of ["subdivision", "city"]) {
|
|
129
|
+
if (policy.geo?.[field] === undefined)
|
|
130
|
+
continue;
|
|
131
|
+
const path = `proxy.geo.${field}`;
|
|
132
|
+
violations.push({
|
|
133
|
+
ruleId: DECLARATION_RULE_IDS.proxySmartproxyGeo,
|
|
134
|
+
path,
|
|
135
|
+
message: `smartproxy cannot honor ${field}-level geo targeting.`,
|
|
136
|
+
fix: `Remove ${path} or use a vendor chain that can honor it.`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function declaredProxyVendors(policy) {
|
|
142
|
+
const vendors = [...(policy.providers ?? [])];
|
|
143
|
+
if (policy.provider !== undefined)
|
|
144
|
+
vendors.push(policy.provider);
|
|
145
|
+
return vendors;
|
|
146
|
+
}
|
|
147
|
+
function validateOperationDeclaration(provider, violations) {
|
|
148
|
+
for (const [operationId, operation] of Object.entries(provider.operations ?? {})) {
|
|
149
|
+
if (!operation.upstream?.proxy)
|
|
150
|
+
continue;
|
|
151
|
+
const path = `operations.${operationId}.upstream.proxy`;
|
|
152
|
+
violations.push({
|
|
153
|
+
ruleId: DECLARATION_RULE_IDS.operationUpstreamProxy,
|
|
154
|
+
path,
|
|
155
|
+
message: "operation-level proxy policy is not wired into operation execution.",
|
|
156
|
+
fix: `Remove ${path} and declare the effective policy at provider.proxy.`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
package/dist/define.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ interface ProviderImplementationProfile {
|
|
|
9
9
|
visibility: "internal" | "operator";
|
|
10
10
|
}
|
|
11
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"];
|
|
12
|
+
export declare const VALID_PROVIDER_CHALLENGE_KINDS: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf", "akamai_sec_cpt", "akamai_sensor"];
|
|
13
13
|
type ProviderOperation = OperationDefinition<SchemaLike, SchemaLike>;
|
|
14
14
|
type OperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<OperationDefinition<TInput, TOutput>, "handler"> & {
|
|
15
15
|
handler(ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0], input: InferSchemaOutput<TInput>): OperationHandlerResult<InferSchemaOutput<TOutput>> | Promise<OperationHandlerResult<InferSchemaOutput<TOutput>>>;
|
package/dist/define.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ms from "ms";
|
|
2
|
+
import { validateFailClosedDeclaration } from "./declaration-validation.js";
|
|
2
3
|
import { SDK_RUNTIME_OWNED_ERROR_CODES } from "./error-resolution.js";
|
|
3
4
|
import { ProviderError, ValidationError } from "./errors.js";
|
|
4
5
|
import { NativeEgressPolicyValidationError, validateNativeProviderConfig, } from "./native-egress-policy.js";
|
|
@@ -66,6 +67,8 @@ export const VALID_PROVIDER_CHALLENGE_KINDS = exhaustiveLiteralArray()([
|
|
|
66
67
|
"hcaptcha",
|
|
67
68
|
"cloudflare_interstitial",
|
|
68
69
|
"aws_waf",
|
|
70
|
+
"akamai_sec_cpt",
|
|
71
|
+
"akamai_sensor",
|
|
69
72
|
]);
|
|
70
73
|
const SMARTPROXY_APP_KEY_SECRET = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
|
|
71
74
|
const NODEMAVEN_USERNAME_SECRET = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
|
|
@@ -423,9 +426,15 @@ function validateProviderResolver(config) {
|
|
|
423
426
|
fix: `Set resolver for provider "${config.id}" to { vendors: ["2captcha"], kinds: ["turnstile"] }.`,
|
|
424
427
|
});
|
|
425
428
|
}
|
|
426
|
-
rejectUnknownFields(resolver, new Set(["vendors", "kinds"]), "resolver", config.id);
|
|
429
|
+
rejectUnknownFields(resolver, new Set(["vendors", "kinds", "clientProfile"]), "resolver", config.id);
|
|
427
430
|
validateResolverLiteralArray(resolver.vendors, "resolver.vendors", VALID_PROVIDER_RESOLVER_VENDORS, config.id);
|
|
428
431
|
validateResolverLiteralArray(resolver.kinds, "resolver.kinds", VALID_PROVIDER_CHALLENGE_KINDS, config.id);
|
|
432
|
+
if (resolver.clientProfile !== undefined &&
|
|
433
|
+
(typeof resolver.clientProfile !== "string" || !resolver.clientProfile.trim())) {
|
|
434
|
+
throw new ValidationError(`Provider "${config.id}" has invalid resolver.clientProfile: must be a non-empty string.`, {
|
|
435
|
+
fix: `Set resolver.clientProfile for provider "${config.id}" to a transport-owned profile name.`,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
429
438
|
}
|
|
430
439
|
function validateResolverLiteralArray(value, field, validValues, providerId) {
|
|
431
440
|
if (!Array.isArray(value)) {
|
|
@@ -1616,7 +1625,7 @@ export function defineProvider(config) {
|
|
|
1616
1625
|
});
|
|
1617
1626
|
if (config.browser && config.runtime !== "browser")
|
|
1618
1627
|
throw new ProviderError(`Provider "${config.id}" cannot define browser config unless runtime is "browser"`, { fix: 'Set runtime: "browser" or remove the browser config' });
|
|
1619
|
-
|
|
1628
|
+
const provider = {
|
|
1620
1629
|
id: config.id,
|
|
1621
1630
|
version: config.version,
|
|
1622
1631
|
runtime: config.runtime,
|
|
@@ -1645,4 +1654,6 @@ export function defineProvider(config) {
|
|
|
1645
1654
|
healthProbe: config.healthProbe ?? config.healthMonitor,
|
|
1646
1655
|
healthJourneys: config.healthJourneys,
|
|
1647
1656
|
};
|
|
1657
|
+
validateFailClosedDeclaration(provider);
|
|
1658
|
+
return provider;
|
|
1648
1659
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export * from "./ceremonies/index.js";
|
|
|
3
3
|
export * from "./choice-token.js";
|
|
4
4
|
export type { ApiFuseConfig, BrowserConfig, ProxyProtocol, ProxyResolutionOptions, ProxyResolutionSource, ProxyVendorName, ResolvedProxyConfig, SessionConfig, } from "./config/loader.js";
|
|
5
5
|
export { defineConfig, loadApiFuseConfig, resolveProxy } from "./config/loader.js";
|
|
6
|
-
export { canonicalJson, digestProviderContract, extractProviderContract, type JsonPrimitive, type JsonValue, PROVIDER_CONTRACT_SCHEMA_VERSION, type ProviderContractOperation, type ProviderContractSnapshot, } from "./contract.js";
|
|
6
|
+
export { canonicalJson, digestProviderContract, extractProviderContract, type JsonPrimitive, type JsonValue, OutputTextTrustProjectionError, PROVIDER_CONTRACT_SCHEMA_VERSION, type ProviderContractOperation, type ProviderContractSnapshot, } from "./contract.js";
|
|
7
7
|
export { centered, delayed, defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, defineStreamOperation, every, type ProviderConfig, } from "./define.js";
|
|
8
8
|
export type { DevServerOptions } from "./dev.js";
|
|
9
9
|
export { createDevServer, startDevServer } from "./dev.js";
|
|
@@ -30,13 +30,14 @@ export { type InstrumentationOptions, type InstrumentedProviderContext, wrapWith
|
|
|
30
30
|
export { type PrevalidateResult, prevalidate } from "./runtime/prevalidate.js";
|
|
31
31
|
export { getProviderBaseUrl } from "./runtime/provider.js";
|
|
32
32
|
export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, createResolverClientFromEnv, createUnsupportedResolverClient, DEFAULT_RESOLVER_TIMEOUT_MS, invalidateResolverSolution, type ResolverRuntimeOptions, } from "./runtime/resolver.js";
|
|
33
|
+
export type { ResolverVendorTransport } from "./runtime/resolver-vendors/types.js";
|
|
33
34
|
export { assertRequiredSecretsPresent, listMissingRequiredSecrets, MISSING_SECRET_CODE, } from "./runtime/secrets.js";
|
|
34
35
|
export { createUnsupportedProviderRuntimeState, UnsupportedProviderStateError, } from "./runtime/state.js";
|
|
35
36
|
export { createStealthClient } from "./runtime/stealth.js";
|
|
36
37
|
export { APIFUSE__OCR__API_KEY_ENV, APIFUSE__OCR__BACKEND_ENV, APIFUSE__OCR__BASE_URL_ENV, APIFUSE__OCR__CLOUDFLARE_API_TOKEN_ENV, APIFUSE__OCR__MODEL_ENV, CLOUDFLARE_ACCOUNT_ID_ENV, CLOUDFLARE_WORKERS_AI_OCR_BACKEND, createCloudflareWorkersAiOcrClient, createOcrClientFromEnv, createOpenAiCompatibleOcrClient, createUnsupportedOcrClient, DEFAULT_CLOUDFLARE_WORKERS_AI_OCR_MODEL, extractCaptchaCandidates, OPENAI_COMPATIBLE_OCR_BACKEND, } from "./runtime/ocr.js";
|
|
37
38
|
export { APIFUSE__STT__BACKEND_ENV, APIFUSE__STT__CLOUDFLARE_API_TOKEN_ENV, APIFUSE__STT__MODEL_ENV, createSttClientFromEnv, createUnsupportedSttClient, extractVerificationCode, resolveSttPrompt, } from "./runtime/stt.js";
|
|
38
39
|
export { type CreateTraceContextOptions, createTraceContext, type Span, type TraceContext, } from "./runtime/trace.js";
|
|
39
|
-
export { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SENSITIVE_KIND_META_KEY, APIFUSE_SENSITIVE_META_KEY, collectSensitivePaths, describeKey, field, fields, isSensitiveSchema, redactPayload, type SensitiveFieldKind, type SensitiveFieldOptions, type SensitivePath, sensitive, z, } from "./schema.js";
|
|
40
|
+
export { APIFUSE_TEXT_TRUST_META_KEY, APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SENSITIVE_KIND_META_KEY, APIFUSE_SENSITIVE_META_KEY, AUTO_TRUSTED_ZOD_STRING_FORMATS, collectOutputTextTrust, collectSensitivePaths, describeKey, field, fields, findUnclassifiedOutputTextPaths, isSensitiveSchema, OutputTextTrustCollectionError, OutputTextTrustSchemaError, redactPayload, type OutputTextTrustMap, type SensitiveFieldKind, type SensitiveFieldOptions, type SensitivePath, sensitive, textTrust, type TextTrust, type TextTrustMetadata, z, } from "./schema.js";
|
|
40
41
|
export { createServerApp, ERROR_OBSERVABILITY_HEADER, type ServeOptions, serve, } from "./server/index.js";
|
|
41
42
|
export { getStealthProfile, listStealthProfiles } from "./stealth/profiles.js";
|
|
42
43
|
export * from "./stream.js";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ export * from "./auth.js";
|
|
|
3
3
|
export * from "./ceremonies/index.js";
|
|
4
4
|
export * from "./choice-token.js";
|
|
5
5
|
export { defineConfig, loadApiFuseConfig, resolveProxy } from "./config/loader.js";
|
|
6
|
-
export { canonicalJson, digestProviderContract, extractProviderContract, PROVIDER_CONTRACT_SCHEMA_VERSION, } from "./contract.js";
|
|
6
|
+
export { canonicalJson, digestProviderContract, extractProviderContract, OutputTextTrustProjectionError, PROVIDER_CONTRACT_SCHEMA_VERSION, } from "./contract.js";
|
|
7
7
|
export { centered, delayed, defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, defineStreamOperation, every, } from "./define.js";
|
|
8
8
|
export { createDevServer, startDevServer } from "./dev.js";
|
|
9
9
|
export * from "./errors.js";
|
|
@@ -33,7 +33,7 @@ export { createStealthClient } from "./runtime/stealth.js";
|
|
|
33
33
|
export { APIFUSE__OCR__API_KEY_ENV, APIFUSE__OCR__BACKEND_ENV, APIFUSE__OCR__BASE_URL_ENV, APIFUSE__OCR__CLOUDFLARE_API_TOKEN_ENV, APIFUSE__OCR__MODEL_ENV, CLOUDFLARE_ACCOUNT_ID_ENV, CLOUDFLARE_WORKERS_AI_OCR_BACKEND, createCloudflareWorkersAiOcrClient, createOcrClientFromEnv, createOpenAiCompatibleOcrClient, createUnsupportedOcrClient, DEFAULT_CLOUDFLARE_WORKERS_AI_OCR_MODEL, extractCaptchaCandidates, OPENAI_COMPATIBLE_OCR_BACKEND, } from "./runtime/ocr.js";
|
|
34
34
|
export { APIFUSE__STT__BACKEND_ENV, APIFUSE__STT__CLOUDFLARE_API_TOKEN_ENV, APIFUSE__STT__MODEL_ENV, createSttClientFromEnv, createUnsupportedSttClient, extractVerificationCode, resolveSttPrompt, } from "./runtime/stt.js";
|
|
35
35
|
export { createTraceContext, } from "./runtime/trace.js";
|
|
36
|
-
export { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SENSITIVE_KIND_META_KEY, APIFUSE_SENSITIVE_META_KEY, collectSensitivePaths, describeKey, field, fields, isSensitiveSchema, redactPayload, sensitive, z, } from "./schema.js";
|
|
36
|
+
export { APIFUSE_TEXT_TRUST_META_KEY, APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SENSITIVE_KIND_META_KEY, APIFUSE_SENSITIVE_META_KEY, AUTO_TRUSTED_ZOD_STRING_FORMATS, collectOutputTextTrust, collectSensitivePaths, describeKey, field, fields, findUnclassifiedOutputTextPaths, isSensitiveSchema, OutputTextTrustCollectionError, OutputTextTrustSchemaError, redactPayload, sensitive, textTrust, z, } from "./schema.js";
|
|
37
37
|
export { createServerApp, ERROR_OBSERVABILITY_HEADER, serve, } from "./server/index.js";
|
|
38
38
|
export { getStealthProfile, listStealthProfiles } from "./stealth/profiles.js";
|
|
39
39
|
export * from "./stream.js";
|
package/dist/lint.js
CHANGED
|
@@ -1,7 +1,84 @@
|
|
|
1
1
|
import { SDK_RUNTIME_OWNED_ERROR_CODES, SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES, } from "./error-resolution.js";
|
|
2
2
|
import { lintPublicSchemaFieldNames } from "./public-schema-field-lint.js";
|
|
3
3
|
import { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_SENSITIVE_META_KEY } from "./schema.js";
|
|
4
|
+
// Operations that perform an auth-lifecycle action belong on the single
|
|
5
|
+
// `auth.flow` interface, never on a provider operation:
|
|
6
|
+
// - entry (login / signin / authenticate) => auth.flow.start/continue
|
|
7
|
+
// - exit (logout / signout / disconnect) => auth.flow.abort
|
|
8
|
+
//
|
|
9
|
+
// Matching works on `-`/`_` separated segments rather than a raw substring or a
|
|
10
|
+
// leading anchor, so `shop-logout`, `shop_logout` and `user-sign-out-everywhere`
|
|
11
|
+
// are all recognised: a domain prefix does not make the operation any less of an
|
|
12
|
+
// auth-lifecycle action, and operation ids may use either separator.
|
|
13
|
+
//
|
|
14
|
+
// Vocabulary is split into two tiers because auth words collide with ordinary
|
|
15
|
+
// domain verbs. Measured against the live fleet plus synthetic domain ids:
|
|
16
|
+
// - `authorize-payment`, `revoke-invitation`, `unlink-record`,
|
|
17
|
+
// `disconnect-device` are domain actions that never touch the connection
|
|
18
|
+
// credential, so these verbs are NOT matched as segments;
|
|
19
|
+
// - the same verbs as a complete operation id (`authorize`, `revoke`) do
|
|
20
|
+
// refer to the credential itself, so they are matched only in that form.
|
|
21
|
+
// `exchange`, `callback`, `connect`, `session`, `token`, `credential`,
|
|
22
|
+
// `password` and `otp` stay out entirely for the same reason.
|
|
23
|
+
const AUTH_LIFECYCLE_SEGMENT_WORDS = new Set([
|
|
24
|
+
"login",
|
|
25
|
+
"logout",
|
|
26
|
+
"signin",
|
|
27
|
+
"signout",
|
|
28
|
+
"signup",
|
|
29
|
+
"authenticate",
|
|
30
|
+
"reauth",
|
|
31
|
+
"auth",
|
|
32
|
+
]);
|
|
33
|
+
// Ambiguous as a prefix, unambiguous when they are the whole operation id.
|
|
34
|
+
const AUTH_LIFECYCLE_WHOLE_ID_WORDS = new Set([
|
|
35
|
+
"authorize",
|
|
36
|
+
"revoke",
|
|
37
|
+
"unlink",
|
|
38
|
+
"disconnect",
|
|
39
|
+
]);
|
|
40
|
+
// A verb stem followed by a direction word across two segments: `sign-out`,
|
|
41
|
+
// `user_sign_up_flow`, and the spelled-out `log-in` / `shop-log-out` forms
|
|
42
|
+
// (their fused equivalents `login`/`logout` live in the segment set above).
|
|
43
|
+
// `sign` pairs match anywhere; `log` pairs match only at the END of the id,
|
|
44
|
+
// because mid-id `log` is the noun in domain phrases measured against real
|
|
45
|
+
// fleets (`audit-log-in-range`, `change-log-out-of-band` are reads of a log,
|
|
46
|
+
// while `shop-log-out` is a logout).
|
|
47
|
+
const AUTH_DIRECTION_PAIRS = new Map([
|
|
48
|
+
["sign", { directions: new Set(["in", "out", "up"]), endOnly: false }],
|
|
49
|
+
["log", { directions: new Set(["in", "out"]), endOnly: true }],
|
|
50
|
+
]);
|
|
51
|
+
// Legacy anchored form kept for token-plumbing words whose bare use is only
|
|
52
|
+
// auth-related when it leads the operation id (`exchange-code`, `refresh`).
|
|
4
53
|
const AUTH_OPERATION_ID_PATTERN = /^(?:auth[-_])?(?:login|exchange|continue|refresh|callback)(?:[-_]|$)/i;
|
|
54
|
+
function isAuthLifecycleOperationId(operationId, authMode) {
|
|
55
|
+
const segments = operationId.toLowerCase().split(/[-_]+/).filter(Boolean);
|
|
56
|
+
if (segments.some((segment) => AUTH_LIFECYCLE_SEGMENT_WORDS.has(segment)))
|
|
57
|
+
return true;
|
|
58
|
+
// A verb stem + direction spread across two segments (`sign-out`,
|
|
59
|
+
// `sign_up`, `shop-log-out`); see AUTH_DIRECTION_PAIRS for positioning.
|
|
60
|
+
if (segments.some((segment, index) => {
|
|
61
|
+
const pair = AUTH_DIRECTION_PAIRS.get(segment);
|
|
62
|
+
if (pair === undefined || index + 1 >= segments.length)
|
|
63
|
+
return false;
|
|
64
|
+
if (!pair.directions.has(segments[index + 1]))
|
|
65
|
+
return false;
|
|
66
|
+
return pair.endOnly ? index + 2 === segments.length : true;
|
|
67
|
+
})) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
if (segments.length === 1 && AUTH_LIFECYCLE_WHOLE_ID_WORDS.has(segments[0])) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
// The legacy anchored pattern keeps its original scope. It matches ordinary
|
|
74
|
+
// domain ids such as `exchange-rates` and `refresh-catalog`, so extending it
|
|
75
|
+
// to `oauth2_proxied` would spread that behavior to providers it never
|
|
76
|
+
// applied to; proxied providers are covered by the segment tiers above.
|
|
77
|
+
if (authMode === "credentials" || authMode === "oauth2") {
|
|
78
|
+
return AUTH_OPERATION_ID_PATTERN.test(operationId);
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
5
82
|
function lintAllowedHosts(providerId, allowedHosts) {
|
|
6
83
|
const prefix = providerId ? `Provider "${providerId}"` : "Provider";
|
|
7
84
|
if (!allowedHosts) {
|
|
@@ -957,14 +1034,19 @@ export function lintProvider(provider, options = {}) {
|
|
|
957
1034
|
];
|
|
958
1035
|
if (provider.operations) {
|
|
959
1036
|
const authMode = provider.auth?.mode;
|
|
960
|
-
|
|
1037
|
+
// Every authenticated mode owns an auth.flow; `oauth2_proxied` was
|
|
1038
|
+
// previously exempt, which let auth-lifecycle operations ship on
|
|
1039
|
+
// proxied providers unchecked.
|
|
1040
|
+
if (authMode === "credentials" ||
|
|
1041
|
+
authMode === "oauth2" ||
|
|
1042
|
+
authMode === "oauth2_proxied") {
|
|
961
1043
|
for (const operationKey of Object.keys(provider.operations)) {
|
|
962
|
-
if (
|
|
1044
|
+
if (isAuthLifecycleOperationId(operationKey, authMode)) {
|
|
963
1045
|
diagnostics.push({
|
|
964
1046
|
rule: "auth-operation-unsupported",
|
|
965
1047
|
level: "error",
|
|
966
1048
|
field: `operations.${operationKey}`,
|
|
967
|
-
message: `Provider "${provider.id ?? "unknown"}" operation "${operationKey}"
|
|
1049
|
+
message: `Provider "${provider.id ?? "unknown"}" operation "${operationKey}" performs an auth-lifecycle action (login, logout, token exchange or similar). Authenticated providers must expose the whole credential lifecycle through the single auth.flow interface because Gateway persists only auth.flow complete turn data.credential as the connection credential, and an operation that mutates the session outside that interface leaves the stored connection stale. Move sign-in logic into auth.flow.start/continue and sign-out/disconnect logic into auth.flow.abort (served by POST /auth/disconnect) instead of a provider operation.`,
|
|
968
1050
|
});
|
|
969
1051
|
}
|
|
970
1052
|
}
|
package/dist/provider.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { centered, delayed, defineHealthJourney, defineOperation, defineProvider
|
|
|
6
6
|
export { AuthError, HttpRedirectError, isProviderError, isSessionExpiredError, isTransportError, ProviderError, SessionExpiredError, TransportError, ValidationError, } from "./errors.js";
|
|
7
7
|
export { getProviderLocalePath, providerLocaleKey, qualifyProviderLocaleKey, } from "./i18n/index.js";
|
|
8
8
|
export { type CreateProviderChoiceContextOptions, createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "./runtime/choice.js";
|
|
9
|
-
export { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SENSITIVE_KIND_META_KEY, APIFUSE_SENSITIVE_META_KEY, collectSensitivePaths, describeKey, field, fields, isSensitiveSchema, redactPayload, type SensitiveFieldKind, type SensitiveFieldOptions, type SensitivePath, sensitive, z, } from "./schema.js";
|
|
9
|
+
export { APIFUSE_TEXT_TRUST_META_KEY, APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SENSITIVE_KIND_META_KEY, APIFUSE_SENSITIVE_META_KEY, AUTO_TRUSTED_ZOD_STRING_FORMATS, collectOutputTextTrust, collectSensitivePaths, describeKey, field, fields, findUnclassifiedOutputTextPaths, isSensitiveSchema, OutputTextTrustCollectionError, OutputTextTrustSchemaError, redactPayload, type OutputTextTrustMap, type SensitiveFieldKind, type SensitiveFieldOptions, type SensitivePath, sensitive, textTrust, type TextTrust, type TextTrustMetadata, z, } from "./schema.js";
|
|
10
10
|
export type { AuthAbortData, AuthAbortRetry, AuthFlowTerminalContext, AuthMode, AuthSafeData, AuthSafeJson, FlowContext, HealthCheckAssertionContext, HealthCheckCase, HealthCheckSuite, HealthCheckUnsupported, HealthJourneyDefinition, HealthJourneyEventContext, HealthJourneyManualTriggerPolicy, HealthJourneyRunContext, HealthJourneyRunResult, HealthScheduleRandomization, HttpRedirectFailureReason, HttpRedirectPolicy, HttpRedirectPolicyMode, HttpRetryOptions, HttpRetrySummary, InferSchemaOutput, NativeContext, NativeNetworkClient, NativeNetworkCloseReason, NativeNetworkConnection, NativeNetworkConnectInput, NativeNetworkConnectOptions, NativeNetworkDynamicGrantOptions, NativeNetworkEgressGrant, NativeProviderConfig, NativeProviderContext, NativeProxyDrainHandler, NativeProxyEgressInfo, NativeProxyExpiringEvent, NativeProxyExpiringReason, NativeTcpDynamicEgressRule, NativeTcpEgressGrant, NativeTcpEgressRule, NativeTcpPortRange, NativeTcpTlsMode, NativeTlsConnectOptions, OperationApprovalPolicy, OperationContractMetadata, OperationDefinition, OperationDocMeta, OperationErrorCode, ProviderErrorStatus, OperationInputExample, OperationLifecycle, OperationObservabilityConfig, OperationObservabilitySensitiveConfig, OperationRelationships, OperationRiskClass, OperationSensitivePath, OperationToolRouterMetadata, OperationTransport, ProviderAccessVisibility, ProviderChoiceBindingOptions, ProviderChoiceContext, ProviderChoiceIssueOptions, ProviderChoiceParseOptions, ProviderContext, ProviderDefinition, ProviderDeploymentOverrides, ProviderFileRef, ProviderFilesContext, ProviderLocale, ProviderLocaleKey, ProviderLocaleKeyInput, ProviderLogoProfile, ProviderProxyPolicy, ProxiedOAuthConfig, ProviderPublicConnectionMode, ProviderPublicProfile, ProviderResolvedFile, ProviderRuntimeState, ProviderStateDurationString, ProviderStateNamespace, ProviderSupportLevel, RedirectRunReason, SchemaLike, SmsOtpMatcherDefinition, StandardSchemaV1, StateCasResult, StateNamespaceOptions, StateNamespaceScope, StateValue, StateWriteOptions, } from "./types.js";
|
|
11
11
|
export { createNativeNetworkClient, createEnvVendorCredentialResolver, deriveNativeCredentialAffinityKey, NativeEgressGrantExpiredError, NativeEgressNotDeclaredError, NativeIdleTimeoutError, NativeNetworkError, NativeProxyExpiredError, resolveNativeGatewayProxy, type NativeGatewayProxy, type NativeGatewayProxyResolutionInput, type NativeGatewayProxySkipReason, type NativeGatewayProxySynthesizer, type NativeGatewayProxySynthesisResult, type NativeGatewayProxySynthesisInput, type NativeNetworkClientOptions, type NativeNetworkErrorCode, type VendorCredentialLookup, type VendorCredentialResolver, } from "./runtime/native-network.js";
|
|
12
12
|
export { HttpRetryAfterPolicy, HttpRetryDelayStrategy, HttpRetryJitter, HttpRetryPreset, HttpRetryUnsafeMethodPolicy, } from "./types.js";
|
package/dist/provider.js
CHANGED
|
@@ -5,6 +5,6 @@ export { centered, delayed, defineHealthJourney, defineOperation, defineProvider
|
|
|
5
5
|
export { AuthError, HttpRedirectError, isProviderError, isSessionExpiredError, isTransportError, ProviderError, SessionExpiredError, TransportError, ValidationError, } from "./errors.js";
|
|
6
6
|
export { getProviderLocalePath, providerLocaleKey, qualifyProviderLocaleKey, } from "./i18n/index.js";
|
|
7
7
|
export { createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "./runtime/choice.js";
|
|
8
|
-
export { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SENSITIVE_KIND_META_KEY, APIFUSE_SENSITIVE_META_KEY, collectSensitivePaths, describeKey, field, fields, isSensitiveSchema, redactPayload, sensitive, z, } from "./schema.js";
|
|
8
|
+
export { APIFUSE_TEXT_TRUST_META_KEY, APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SENSITIVE_KIND_META_KEY, APIFUSE_SENSITIVE_META_KEY, AUTO_TRUSTED_ZOD_STRING_FORMATS, collectOutputTextTrust, collectSensitivePaths, describeKey, field, fields, findUnclassifiedOutputTextPaths, isSensitiveSchema, OutputTextTrustCollectionError, OutputTextTrustSchemaError, redactPayload, sensitive, textTrust, z, } from "./schema.js";
|
|
9
9
|
export { createNativeNetworkClient, createEnvVendorCredentialResolver, deriveNativeCredentialAffinityKey, NativeEgressGrantExpiredError, NativeEgressNotDeclaredError, NativeIdleTimeoutError, NativeNetworkError, NativeProxyExpiredError, resolveNativeGatewayProxy, } from "./runtime/native-network.js";
|
|
10
10
|
export { HttpRetryAfterPolicy, HttpRetryDelayStrategy, HttpRetryJitter, HttpRetryPreset, HttpRetryUnsafeMethodPolicy, } from "./types.js";
|
|
@@ -1,8 +1,48 @@
|
|
|
1
1
|
import type { ProviderChallenge } from "../../types.js";
|
|
2
2
|
import type { ResolverIssuingIdentity } from "./types.js";
|
|
3
3
|
export declare const RESOLVER_CHALLENGE_BINDINGS: {
|
|
4
|
-
readonly
|
|
5
|
-
|
|
4
|
+
readonly turnstile: {
|
|
5
|
+
readonly cacheable: false;
|
|
6
|
+
readonly identityBinding: "none";
|
|
7
|
+
readonly directCacheable: false;
|
|
8
|
+
};
|
|
9
|
+
readonly recaptcha_v2: {
|
|
10
|
+
readonly cacheable: false;
|
|
11
|
+
readonly identityBinding: "none";
|
|
12
|
+
readonly directCacheable: false;
|
|
13
|
+
};
|
|
14
|
+
readonly recaptcha_v3: {
|
|
15
|
+
readonly cacheable: false;
|
|
16
|
+
readonly identityBinding: "none";
|
|
17
|
+
readonly directCacheable: false;
|
|
18
|
+
};
|
|
19
|
+
readonly hcaptcha: {
|
|
20
|
+
readonly cacheable: false;
|
|
21
|
+
readonly identityBinding: "none";
|
|
22
|
+
readonly directCacheable: false;
|
|
23
|
+
};
|
|
24
|
+
readonly cloudflare_interstitial: {
|
|
25
|
+
readonly cacheable: true;
|
|
26
|
+
readonly identityBinding: "identity_scoped";
|
|
27
|
+
readonly directCacheable: true;
|
|
28
|
+
};
|
|
29
|
+
readonly aws_waf: {
|
|
30
|
+
readonly cacheable: true;
|
|
31
|
+
readonly identityBinding: "portable";
|
|
32
|
+
readonly directCacheable: true;
|
|
33
|
+
};
|
|
34
|
+
readonly akamai_sec_cpt: {
|
|
35
|
+
readonly cacheable: true;
|
|
36
|
+
readonly identityBinding: "identity_scoped";
|
|
37
|
+
readonly directCacheable: false;
|
|
38
|
+
};
|
|
39
|
+
readonly akamai_sensor: {
|
|
40
|
+
readonly cacheable: true;
|
|
41
|
+
readonly identityBinding: "identity_scoped";
|
|
42
|
+
readonly directCacheable: false;
|
|
43
|
+
};
|
|
6
44
|
};
|
|
45
|
+
export declare function resolverChallengeIsCacheable(challenge: ProviderChallenge): boolean;
|
|
46
|
+
export declare function resolverChallengeAllowsDirectCache(challenge: ProviderChallenge): boolean;
|
|
7
47
|
export declare function resolverChallengeIsIdentityScoped(challenge: ProviderChallenge): boolean;
|
|
8
48
|
export declare function resolverChallengeIssuingIdentity(challenge: ProviderChallenge, identity: ResolverIssuingIdentity): ResolverIssuingIdentity;
|
|
@@ -1,14 +1,39 @@
|
|
|
1
|
+
// An IP-bound artifact minted without any recorded egress identity is unsafe to
|
|
2
|
+
// share. The Akamai kinds therefore reject direct caching, while Cloudflare
|
|
3
|
+
// keeps its pre-existing direct-cache behavior pending measurement.
|
|
1
4
|
export const RESOLVER_CHALLENGE_BINDINGS = {
|
|
2
|
-
|
|
3
|
-
|
|
5
|
+
turnstile: { cacheable: false, identityBinding: "none", directCacheable: false },
|
|
6
|
+
recaptcha_v2: { cacheable: false, identityBinding: "none", directCacheable: false },
|
|
7
|
+
recaptcha_v3: { cacheable: false, identityBinding: "none", directCacheable: false },
|
|
8
|
+
hcaptcha: { cacheable: false, identityBinding: "none", directCacheable: false },
|
|
9
|
+
cloudflare_interstitial: {
|
|
10
|
+
cacheable: true,
|
|
11
|
+
identityBinding: "identity_scoped",
|
|
12
|
+
directCacheable: true,
|
|
13
|
+
},
|
|
14
|
+
aws_waf: { cacheable: true, identityBinding: "portable", directCacheable: true },
|
|
15
|
+
akamai_sec_cpt: {
|
|
16
|
+
cacheable: true,
|
|
17
|
+
identityBinding: "identity_scoped",
|
|
18
|
+
directCacheable: false,
|
|
19
|
+
},
|
|
20
|
+
akamai_sensor: {
|
|
21
|
+
cacheable: true,
|
|
22
|
+
identityBinding: "identity_scoped",
|
|
23
|
+
directCacheable: false,
|
|
24
|
+
},
|
|
4
25
|
};
|
|
26
|
+
export function resolverChallengeIsCacheable(challenge) {
|
|
27
|
+
return RESOLVER_CHALLENGE_BINDINGS[challenge.kind].cacheable;
|
|
28
|
+
}
|
|
29
|
+
export function resolverChallengeAllowsDirectCache(challenge) {
|
|
30
|
+
return RESOLVER_CHALLENGE_BINDINGS[challenge.kind].directCacheable;
|
|
31
|
+
}
|
|
5
32
|
export function resolverChallengeIsIdentityScoped(challenge) {
|
|
6
|
-
return
|
|
7
|
-
"identity_scoped");
|
|
33
|
+
return RESOLVER_CHALLENGE_BINDINGS[challenge.kind].identityBinding === "identity_scoped";
|
|
8
34
|
}
|
|
9
35
|
export function resolverChallengeIssuingIdentity(challenge, identity) {
|
|
10
|
-
|
|
11
|
-
if (binding === "portable") {
|
|
36
|
+
if (RESOLVER_CHALLENGE_BINDINGS[challenge.kind].identityBinding === "portable") {
|
|
12
37
|
return { userAgent: identity.userAgent };
|
|
13
38
|
}
|
|
14
39
|
return identity;
|
|
@@ -10,15 +10,11 @@ export interface BrowserResolverVendorOptions {
|
|
|
10
10
|
readonly allowedHosts: readonly string[];
|
|
11
11
|
readonly createClient?: BrowserClientFactory;
|
|
12
12
|
}
|
|
13
|
-
export type BrowserResolverSolution = Extract<ChallengeSolution, {
|
|
14
|
-
readonly form: "cookies";
|
|
15
|
-
}> & {
|
|
16
|
-
/** Unix seconds from the cookie that proved the challenge cleared. */
|
|
17
|
-
readonly expires?: number;
|
|
18
|
-
};
|
|
19
13
|
export interface BrowserResolverVendorAdapter extends ResolverVendorAdapter {
|
|
20
14
|
readonly id: "browser";
|
|
21
|
-
solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder): Promise<
|
|
15
|
+
solve(challenge: ProviderChallenge, identity: ResolverIdentity | undefined, signal: AbortSignal, traceRecorder?: TraceRecorder): Promise<Extract<ChallengeSolution, {
|
|
16
|
+
readonly form: "cookies";
|
|
17
|
+
}>>;
|
|
22
18
|
}
|
|
23
19
|
export declare function createBrowserResolverVendorAdapter(options: BrowserResolverVendorOptions): BrowserResolverVendorAdapter;
|
|
24
20
|
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { isProviderError
|
|
1
|
+
import { isProviderError } from "../../errors.js";
|
|
2
2
|
import { createBrowserClient } from "../browser.js";
|
|
3
3
|
import { resolverChallengeIssuingIdentity } from "./bindings.js";
|
|
4
|
+
import { assertResolverHostAllowed, normalizedResolverHostname } from "./hosts.js";
|
|
4
5
|
import { ResolverVendorUnavailableError, } from "./types.js";
|
|
5
6
|
const BROWSER_VENDOR_ID = "browser";
|
|
6
7
|
const DEFAULT_COOKIE_POLL_INTERVAL_MS = 100;
|
|
@@ -94,32 +95,16 @@ async function abortableDelay(ms, signal) {
|
|
|
94
95
|
function isSupportedKind(kind) {
|
|
95
96
|
return Object.hasOwn(SUCCESS_COOKIE_NAMES, kind);
|
|
96
97
|
}
|
|
97
|
-
function normalizedHostname(hostname) {
|
|
98
|
-
return hostname.trim().toLowerCase().replace(/\.$/, "");
|
|
99
|
-
}
|
|
100
|
-
function assertChallengeHostAllowed(pageUrl, allowedHosts) {
|
|
101
|
-
const challengeHost = normalizedHostname(new URL(pageUrl).hostname);
|
|
102
|
-
const isAllowed = allowedHosts.some((host) => {
|
|
103
|
-
const declaredHost = normalizedHostname(host);
|
|
104
|
-
return declaredHost.length > 0 && !declaredHost.includes("*") && declaredHost === challengeHost;
|
|
105
|
-
});
|
|
106
|
-
if (isAllowed)
|
|
107
|
-
return;
|
|
108
|
-
throw new ProviderError(`Resolver challenge host "${challengeHost}" is not declared`, {
|
|
109
|
-
code: "RESOLVER_HOST_NOT_ALLOWED",
|
|
110
|
-
fix: "Add the exact challenge hostname to the provider's allowedHosts declaration.",
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
98
|
function cookieDomainSpecificity(cookie) {
|
|
114
|
-
return
|
|
99
|
+
return normalizedResolverHostname(cookie.domain.replace(/^\./, "")).length;
|
|
115
100
|
}
|
|
116
101
|
function isHostOnlyCookieFor(cookie, hostname) {
|
|
117
102
|
return (!cookie.domain.startsWith(".") &&
|
|
118
|
-
|
|
103
|
+
normalizedResolverHostname(cookie.domain) === normalizedResolverHostname(hostname));
|
|
119
104
|
}
|
|
120
105
|
function cookieAppliesToUrl(cookie, url) {
|
|
121
|
-
const cookieDomain =
|
|
122
|
-
const requestHostname =
|
|
106
|
+
const cookieDomain = normalizedResolverHostname(cookie.domain.replace(/^\./, ""));
|
|
107
|
+
const requestHostname = normalizedResolverHostname(url.hostname);
|
|
123
108
|
const domainMatches = cookieDomain.length > 0 &&
|
|
124
109
|
(requestHostname === cookieDomain ||
|
|
125
110
|
(cookie.domain.startsWith(".") && requestHostname.endsWith(`.${cookieDomain}`)));
|
|
@@ -226,7 +211,7 @@ export function createBrowserResolverVendorAdapter(options) {
|
|
|
226
211
|
if (!isSupportedKind(challenge.kind)) {
|
|
227
212
|
throw new TypeError(`Browser resolver does not support ${challenge.kind}`);
|
|
228
213
|
}
|
|
229
|
-
|
|
214
|
+
assertResolverHostAllowed(challenge.pageUrl, options.allowedHosts);
|
|
230
215
|
const challengeKind = challenge.kind;
|
|
231
216
|
callerSignal.throwIfAborted();
|
|
232
217
|
const solveController = new AbortController();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ProviderError } from "../../errors.js";
|
|
2
|
+
export function normalizedResolverHostname(hostname) {
|
|
3
|
+
return hostname.trim().toLowerCase().replace(/\.$/, "");
|
|
4
|
+
}
|
|
5
|
+
export function assertResolverHostAllowed(targetUrl, allowedHosts) {
|
|
6
|
+
let targetUrlObject;
|
|
7
|
+
try {
|
|
8
|
+
targetUrlObject = new URL(targetUrl);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw new ProviderError("Resolver target URL is invalid", {
|
|
12
|
+
code: "RESOLVER_HOST_NOT_ALLOWED",
|
|
13
|
+
fix: "Use a valid URL whose exact hostname appears in the provider's allowedHosts declaration.",
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
if (targetUrlObject.protocol !== "http:" && targetUrlObject.protocol !== "https:") {
|
|
17
|
+
throw new ProviderError(`Resolver target URL scheme "${targetUrlObject.protocol}" is not allowed`, {
|
|
18
|
+
code: "RESOLVER_HOST_NOT_ALLOWED",
|
|
19
|
+
fix: "Use an http or https URL whose exact hostname appears in the provider's allowedHosts declaration.",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const targetHost = normalizedResolverHostname(targetUrlObject.hostname);
|
|
23
|
+
const isAllowed = allowedHosts.some((host) => {
|
|
24
|
+
const declaredHost = normalizedResolverHostname(host);
|
|
25
|
+
return declaredHost.length > 0 && !declaredHost.includes("*") && declaredHost === targetHost;
|
|
26
|
+
});
|
|
27
|
+
if (isAllowed)
|
|
28
|
+
return;
|
|
29
|
+
throw new ProviderError(`Resolver target host "${targetHost}" is not declared`, {
|
|
30
|
+
code: "RESOLVER_HOST_NOT_ALLOWED",
|
|
31
|
+
fix: "Add the exact target hostname to the provider's allowedHosts declaration.",
|
|
32
|
+
});
|
|
33
|
+
}
|