@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/src/schema.ts
CHANGED
|
@@ -83,6 +83,8 @@ export function safeParseSchemaSync(
|
|
|
83
83
|
export const APIFUSE_SENSITIVE_META_KEY = "x-apifuse-sensitive";
|
|
84
84
|
export const APIFUSE_SENSITIVE_KIND_META_KEY = "x-apifuse-sensitive-kind";
|
|
85
85
|
export const APIFUSE_DESCRIPTION_KEY_META_KEY = "x-apifuse-description-key";
|
|
86
|
+
export const APIFUSE_CONTENT_TRUST_META_KEY = "x-apifuse-content-trust";
|
|
87
|
+
export const APIFUSE_CONTENT_PROVENANCE_META_KEY = "x-apifuse-content-provenance";
|
|
86
88
|
export const APIFUSE_REDACTION_MARKER = "<redacted>";
|
|
87
89
|
|
|
88
90
|
export type SensitivePathSegment = string | "*";
|
|
@@ -127,6 +129,21 @@ export function describeKey<TSchema extends ZodType>(
|
|
|
127
129
|
});
|
|
128
130
|
}
|
|
129
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Mark an output field as externally authored text (listing bodies, reviews,
|
|
134
|
+
* posts) that the provider relays verbatim. Sets both content-trust JSON Schema
|
|
135
|
+
* meta keys on the field node so the platform can enforce the boundary when it
|
|
136
|
+
* assembles tool output; validation and the parsed value are unchanged.
|
|
137
|
+
*/
|
|
138
|
+
export function untrustedContent<TSchema extends ZodType>(schema: TSchema): TSchema {
|
|
139
|
+
const metadata = schema.meta() ?? {};
|
|
140
|
+
return schema.meta({
|
|
141
|
+
...metadata,
|
|
142
|
+
[APIFUSE_CONTENT_TRUST_META_KEY]: "untrusted",
|
|
143
|
+
[APIFUSE_CONTENT_PROVENANCE_META_KEY]: "external",
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
130
147
|
declare module "zod" {
|
|
131
148
|
interface ZodType {
|
|
132
149
|
describeKey(key: ProviderLocaleKey | string): this;
|
package/src/server/index.ts
CHANGED
|
@@ -23,6 +23,27 @@ export {
|
|
|
23
23
|
type TelemetryKey,
|
|
24
24
|
type TenantNeutral,
|
|
25
25
|
} from "../runtime/request-telemetry.js";
|
|
26
|
+
export {
|
|
27
|
+
ResolverTelemetryCollector,
|
|
28
|
+
type ResolverAttemptSample,
|
|
29
|
+
type ResolverCacheReadTelemetryEvent,
|
|
30
|
+
type ResolverCacheWriteTelemetryEvent,
|
|
31
|
+
type ResolverFailoverTelemetryEvent,
|
|
32
|
+
type ResolverIdentityTelemetryEvent,
|
|
33
|
+
type ResolverOutcomeTelemetryEvent,
|
|
34
|
+
type ResolverTelemetryAttemptOutcome,
|
|
35
|
+
type ResolverTelemetryCacheStatus,
|
|
36
|
+
type ResolverTelemetryCacheWriteReason,
|
|
37
|
+
type ResolverTelemetryErrorClass,
|
|
38
|
+
type ResolverTelemetryHeaderPayload,
|
|
39
|
+
type ResolverTelemetryIdentitySource,
|
|
40
|
+
type ResolverTelemetryLogPayload,
|
|
41
|
+
type ResolverTelemetryOutcome,
|
|
42
|
+
type ResolverTelemetryPhase,
|
|
43
|
+
type ResolverTelemetrySink,
|
|
44
|
+
type ResolverVendorAttemptTelemetryEvent,
|
|
45
|
+
} from "../runtime/resolver-telemetry.js";
|
|
46
|
+
export type { ResolverVendorUnavailableReason } from "../runtime/resolver-vendors/types.js";
|
|
26
47
|
export type { Span, TraceContext } from "../runtime/trace.js";
|
|
27
48
|
export {
|
|
28
49
|
createServerApp,
|
package/src/server/self-test.ts
CHANGED
|
@@ -117,6 +117,8 @@ export type SelfTestAuthFlowInvoke = (args: {
|
|
|
117
117
|
externalRef?: string;
|
|
118
118
|
input?: Record<string, unknown>;
|
|
119
119
|
context?: Record<string, unknown>;
|
|
120
|
+
/** Opaque engine state from the previous turn (`engine` in the auth response), sent verbatim. */
|
|
121
|
+
engine?: { egressLease: string };
|
|
120
122
|
signal?: AbortSignal;
|
|
121
123
|
}) => Promise<{
|
|
122
124
|
status: number;
|
|
@@ -343,6 +345,7 @@ export function createSelfTestAuthFlowInvoke(app: {
|
|
|
343
345
|
externalRef,
|
|
344
346
|
input,
|
|
345
347
|
context,
|
|
348
|
+
engine,
|
|
346
349
|
signal,
|
|
347
350
|
}) => {
|
|
348
351
|
const response = await app.request(`/auth/${route}`, {
|
|
@@ -351,10 +354,14 @@ export function createSelfTestAuthFlowInvoke(app: {
|
|
|
351
354
|
body: JSON.stringify({
|
|
352
355
|
requestId,
|
|
353
356
|
flowId,
|
|
357
|
+
// Engine ceremony leases are scoped to a tenant; the self-test has no
|
|
358
|
+
// customer tenant, so it presents a fixed one that never collides.
|
|
359
|
+
tenantId: "self-test",
|
|
354
360
|
...(connectionId ? { connectionId } : {}),
|
|
355
361
|
...(externalRef ? { externalRef } : {}),
|
|
356
362
|
...(input ? { input } : {}),
|
|
357
363
|
...(context ? { context } : {}),
|
|
364
|
+
...(engine ? { engine } : {}),
|
|
358
365
|
}),
|
|
359
366
|
...(signal ? { signal } : {}),
|
|
360
367
|
});
|
|
@@ -550,6 +557,7 @@ type ParsedAuthFlowTurn =
|
|
|
550
557
|
ok: true;
|
|
551
558
|
turn: { kind: string; data?: unknown; expectedInput?: unknown };
|
|
552
559
|
contextPatch?: Record<string, unknown>;
|
|
560
|
+
engine?: { egressLease: string };
|
|
553
561
|
}
|
|
554
562
|
| { ok: false; code: string; message: string; httpStatus: number };
|
|
555
563
|
|
|
@@ -578,6 +586,7 @@ function parseAuthFlowResponse(result: { status: number; body: unknown }): Parse
|
|
|
578
586
|
};
|
|
579
587
|
}
|
|
580
588
|
const contextPatch = objectProperty(result.body, "contextPatch");
|
|
589
|
+
const egressLease = objectProperty(objectProperty(result.body, "engine"), "egressLease");
|
|
581
590
|
return {
|
|
582
591
|
ok: true,
|
|
583
592
|
turn: {
|
|
@@ -588,6 +597,7 @@ function parseAuthFlowResponse(result: { status: number; body: unknown }): Parse
|
|
|
588
597
|
...(contextPatch && typeof contextPatch === "object" && !Array.isArray(contextPatch)
|
|
589
598
|
? { contextPatch: contextPatch as Record<string, unknown> }
|
|
590
599
|
: {}),
|
|
600
|
+
...(typeof egressLease === "string" && egressLease ? { engine: { egressLease } } : {}),
|
|
591
601
|
};
|
|
592
602
|
}
|
|
593
603
|
|
|
@@ -777,6 +787,9 @@ async function materializeFlowCredential(
|
|
|
777
787
|
...(options.externalRef ? { externalRef: options.externalRef } : {}),
|
|
778
788
|
input: submitInputs,
|
|
779
789
|
...(Object.keys(flowContext).length > 0 ? { context: flowContext } : {}),
|
|
790
|
+
// The ceremony egress lease is engine state, not flow context: carry it the
|
|
791
|
+
// way the gateway does so the continue turn stays on the start turn's egress.
|
|
792
|
+
...(started.engine ? { engine: started.engine } : {}),
|
|
780
793
|
...(options.signal ? { signal: options.signal } : {}),
|
|
781
794
|
}),
|
|
782
795
|
);
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
isTransportError,
|
|
30
30
|
isValidationError,
|
|
31
31
|
ProviderError,
|
|
32
|
+
SDKError,
|
|
32
33
|
type ProviderErrorObservability,
|
|
33
34
|
type ProviderErrorOptions,
|
|
34
35
|
} from "../errors.js";
|
|
@@ -55,6 +56,10 @@ import {
|
|
|
55
56
|
PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
|
|
56
57
|
} from "../runtime/choice.js";
|
|
57
58
|
import { createCredentialContext } from "../runtime/credential.js";
|
|
59
|
+
import {
|
|
60
|
+
createCeremonyEgressLeaseRuntime,
|
|
61
|
+
ENGINE_CEREMONY_EGRESS_LEASE,
|
|
62
|
+
} from "../runtime/egress-lease.js";
|
|
58
63
|
import { createEnvContext } from "../runtime/env.js";
|
|
59
64
|
import { executeOperation } from "../runtime/executor.js";
|
|
60
65
|
import { createHttpClient } from "../runtime/http.js";
|
|
@@ -80,9 +85,15 @@ import {
|
|
|
80
85
|
type TenantNeutral,
|
|
81
86
|
type TenantOpaqueKeys,
|
|
82
87
|
} from "../runtime/request-telemetry.js";
|
|
88
|
+
import {
|
|
89
|
+
ResolverTelemetryCollector,
|
|
90
|
+
type ResolverTelemetryLogPayload,
|
|
91
|
+
} from "../runtime/resolver-telemetry.js";
|
|
83
92
|
import type * as ResolverRuntimeModule from "../runtime/resolver.js";
|
|
84
93
|
import {
|
|
85
94
|
createUnsupportedResolverClient,
|
|
95
|
+
bindResolverTelemetry,
|
|
96
|
+
inheritResolverTelemetryBinding,
|
|
86
97
|
type ResolverSolveWithRecorder,
|
|
87
98
|
} from "../runtime/resolver-shared.js";
|
|
88
99
|
import {
|
|
@@ -96,9 +107,11 @@ import {
|
|
|
96
107
|
} from "../runtime/state.js";
|
|
97
108
|
import { StealthCookieJar } from "../runtime/stealth-cookies.js";
|
|
98
109
|
import type * as StealthRuntimeModule from "../runtime/stealth.js";
|
|
110
|
+
import type { StealthChallengeRuntime } from "../runtime/stealth-akamai-sbsd.js";
|
|
99
111
|
import { createSttClientFromEnv } from "../runtime/stt.js";
|
|
100
112
|
import {
|
|
101
113
|
createTraceContext,
|
|
114
|
+
getTraceRecorder,
|
|
102
115
|
type TraceContext as RuntimeTraceContext,
|
|
103
116
|
type TraceRecorder,
|
|
104
117
|
updateTraceContextExportMetadata,
|
|
@@ -513,6 +526,9 @@ function createLazyStealthClient(
|
|
|
513
526
|
async fetch(...args) {
|
|
514
527
|
return (await getSession()).fetch(...args);
|
|
515
528
|
},
|
|
529
|
+
async replayChallenged(...args) {
|
|
530
|
+
return (await getSession()).replayChallenged(...args);
|
|
531
|
+
},
|
|
516
532
|
redirects: {
|
|
517
533
|
async run(...args) {
|
|
518
534
|
return (await getSession()).redirects.run(...args);
|
|
@@ -553,7 +569,7 @@ function bindResolverSignalWithoutRuntime(
|
|
|
553
569
|
defaultSignal: AbortSignal | undefined,
|
|
554
570
|
): ResolverContext {
|
|
555
571
|
if (!defaultSignal) return resolver;
|
|
556
|
-
|
|
572
|
+
const boundResolver: ResolverContext = {
|
|
557
573
|
solve(challenge, signal = defaultSignal, traceRecorder?: TraceRecorder) {
|
|
558
574
|
// Forward the instrumentation trace recorder so resolver.vendor.* spans
|
|
559
575
|
// survive this wrapper. See ResolverSolveWithRecorder in resolver-shared.ts.
|
|
@@ -564,6 +580,8 @@ function bindResolverSignalWithoutRuntime(
|
|
|
564
580
|
);
|
|
565
581
|
},
|
|
566
582
|
};
|
|
583
|
+
inheritResolverTelemetryBinding(resolver, boundResolver);
|
|
584
|
+
return boundResolver;
|
|
567
585
|
}
|
|
568
586
|
|
|
569
587
|
function getProviderStealthBaseUrl(provider: ProviderDefinition): string | undefined {
|
|
@@ -579,6 +597,99 @@ function getProviderStealthProfile(provider: ProviderDefinition) {
|
|
|
579
597
|
return provider.stealth ? getStealthProfile(provider.stealth) : undefined;
|
|
580
598
|
}
|
|
581
599
|
|
|
600
|
+
type ResolverRuntimeOptions = ResolverRuntimeModule.ResolverRuntimeOptions;
|
|
601
|
+
|
|
602
|
+
/** One option set for ctx.resolver and the automatic SBSD solve, so both share identity scope, cache, proxy intent, and telemetry. */
|
|
603
|
+
function createResolverRuntimeOptions(
|
|
604
|
+
provider: ProviderDefinition,
|
|
605
|
+
cache: ReturnType<typeof createProviderCache>,
|
|
606
|
+
identityScope: string,
|
|
607
|
+
proxyPolicy: ProviderProxyPolicy | undefined,
|
|
608
|
+
proxyClientOptions: Omit<
|
|
609
|
+
NonNullable<ResolverRuntimeOptions["proxyIntent"]>,
|
|
610
|
+
"mode" | "userAgent"
|
|
611
|
+
>,
|
|
612
|
+
stealthProfile: { readonly userAgent: string } | undefined,
|
|
613
|
+
telemetry: ResolverTelemetryCollector,
|
|
614
|
+
): ResolverRuntimeOptions {
|
|
615
|
+
return {
|
|
616
|
+
allowedHosts: provider.allowedHosts,
|
|
617
|
+
cache,
|
|
618
|
+
telemetry,
|
|
619
|
+
identityScope,
|
|
620
|
+
...(proxyPolicy
|
|
621
|
+
? {
|
|
622
|
+
proxyIntent: {
|
|
623
|
+
mode: proxyPolicy.mode,
|
|
624
|
+
...proxyClientOptions,
|
|
625
|
+
...(stealthProfile ? { userAgent: stealthProfile.userAgent } : {}),
|
|
626
|
+
},
|
|
627
|
+
}
|
|
628
|
+
: {}),
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** Shared predicate for the SBSD challenge runtime and the ceremony egress lease. */
|
|
633
|
+
function declaresAkamaiSbsdResolver(provider: ProviderDefinition): boolean {
|
|
634
|
+
return provider.resolver?.kinds.some((kind) => kind === "akamai_sbsd") === true;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function createStealthChallengeDetection(
|
|
638
|
+
provider: ProviderDefinition,
|
|
639
|
+
resolverRuntime: typeof ResolverRuntimeModule | undefined,
|
|
640
|
+
resolverOverride: ResolverContext | undefined,
|
|
641
|
+
resolverOptions: ResolverRuntimeOptions,
|
|
642
|
+
trace: RuntimeTraceContext,
|
|
643
|
+
signal: AbortSignal | undefined,
|
|
644
|
+
): StealthChallengeRuntime | undefined {
|
|
645
|
+
const resolverDeclared = declaresAkamaiSbsdResolver(provider);
|
|
646
|
+
const detectOnly = provider.stealth?.challengeDetection?.akamaiSbsd === true;
|
|
647
|
+
if (!resolverDeclared && !detectOnly) {
|
|
648
|
+
return undefined;
|
|
649
|
+
}
|
|
650
|
+
return {
|
|
651
|
+
akamaiSbsd: {
|
|
652
|
+
allowedHosts: provider.allowedHosts ?? [],
|
|
653
|
+
// The profile belongs to the SBSD resolver; a resolver declared for other kinds
|
|
654
|
+
// may name a different browser without affecting detect-only classification.
|
|
655
|
+
...(resolverDeclared && provider.resolver?.clientProfile
|
|
656
|
+
? { clientProfile: provider.resolver.clientProfile }
|
|
657
|
+
: {}),
|
|
658
|
+
...(resolverDeclared && resolverRuntime
|
|
659
|
+
? {
|
|
660
|
+
async solve(challenge, transport, solveSignal) {
|
|
661
|
+
const resolver = resolverRuntime.bindResolverSignal(
|
|
662
|
+
(resolverOverride
|
|
663
|
+
? bindResolverTelemetry(resolverOverride, resolverOptions.telemetry)
|
|
664
|
+
: undefined) ??
|
|
665
|
+
resolverRuntime.createResolverClientFromEnv(provider.resolver, undefined, {
|
|
666
|
+
allowedHosts: resolverOptions.allowedHosts,
|
|
667
|
+
cache: resolverOptions.cache,
|
|
668
|
+
telemetry: resolverOptions.telemetry,
|
|
669
|
+
identityScope: resolverOptions.identityScope,
|
|
670
|
+
// No proxyIntent: the transport is already bound to the initiating
|
|
671
|
+
// request's lease, and a second identity resolution would allocate
|
|
672
|
+
// again (and fail closed without allocator credentials) for nothing
|
|
673
|
+
// the SBSD adapters consume.
|
|
674
|
+
createTransport: () => transport,
|
|
675
|
+
}),
|
|
676
|
+
signal,
|
|
677
|
+
) as ResolverContext & Partial<ResolverSolveWithRecorder>;
|
|
678
|
+
// Same span and recorder threading as the instrumented ctx.resolver.solve.
|
|
679
|
+
const recorder = getTraceRecorder(trace);
|
|
680
|
+
const solve = () => resolver.solve(challenge, solveSignal, recorder ?? undefined);
|
|
681
|
+
return recorder
|
|
682
|
+
? recorder.runSpan("resolver.solve", solve, {
|
|
683
|
+
attributes: { challenge_kind: challenge.kind },
|
|
684
|
+
})
|
|
685
|
+
: solve();
|
|
686
|
+
},
|
|
687
|
+
}
|
|
688
|
+
: {}),
|
|
689
|
+
},
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
582
693
|
function isProductionProviderBrowserMode(provider: ProviderDefinition, env = process.env): boolean {
|
|
583
694
|
if (provider.runtime !== "browser") {
|
|
584
695
|
return false;
|
|
@@ -714,6 +825,7 @@ function providerSecretNames(provider: ProviderDefinition): string[] {
|
|
|
714
825
|
type RequestScopeContext = {
|
|
715
826
|
trace: RuntimeTraceContext;
|
|
716
827
|
telemetry: RequestTelemetry;
|
|
828
|
+
resolverTelemetry: ResolverTelemetryCollector;
|
|
717
829
|
};
|
|
718
830
|
|
|
719
831
|
function createProviderContext(
|
|
@@ -741,16 +853,43 @@ function createProviderContext(
|
|
|
741
853
|
proxyClientOptions.affinityKey,
|
|
742
854
|
request.requestId,
|
|
743
855
|
);
|
|
856
|
+
const cache = createProviderCache({ providerId: provider.id });
|
|
744
857
|
let wrappedContext: ProviderContext | undefined;
|
|
858
|
+
const { capabilityModules } = options;
|
|
859
|
+
const resolverOptions = createResolverRuntimeOptions(
|
|
860
|
+
provider,
|
|
861
|
+
cache,
|
|
862
|
+
resolverIdentityScope,
|
|
863
|
+
proxyPolicy,
|
|
864
|
+
proxyClientOptions,
|
|
865
|
+
stealthProfile,
|
|
866
|
+
scope.resolverTelemetry,
|
|
867
|
+
);
|
|
868
|
+
const challengeRuntime = createStealthChallengeDetection(
|
|
869
|
+
provider,
|
|
870
|
+
capabilityModules.resolver,
|
|
871
|
+
options.resolver,
|
|
872
|
+
resolverOptions,
|
|
873
|
+
scope.trace,
|
|
874
|
+
signal,
|
|
875
|
+
);
|
|
745
876
|
const stealthClientOptions = {
|
|
746
877
|
upstream: proxyClientOptions.upstream,
|
|
747
878
|
affinityKey: proxyClientOptions.affinityKey,
|
|
748
879
|
telemetry: scope.telemetry.proxy,
|
|
749
880
|
engineCredentials: engineProxyCredentials,
|
|
750
881
|
...(signal ? { signal } : {}),
|
|
751
|
-
|
|
882
|
+
// A declared resolver without a `stealth` block still gets the default Chrome client
|
|
883
|
+
// (auth FlowContext is not capability-gated), so the runtime must ride along.
|
|
884
|
+
...(provider.stealth || challengeRuntime
|
|
885
|
+
? {
|
|
886
|
+
stealth: {
|
|
887
|
+
...(provider.stealth ?? {}),
|
|
888
|
+
...(challengeRuntime ? { challengeRuntime } : {}),
|
|
889
|
+
},
|
|
890
|
+
}
|
|
891
|
+
: {}),
|
|
752
892
|
};
|
|
753
|
-
const { capabilityModules } = options;
|
|
754
893
|
const logStealthCleanupError = (error: unknown) =>
|
|
755
894
|
logProviderCleanupError(
|
|
756
895
|
options.logger,
|
|
@@ -777,7 +916,6 @@ function createProviderContext(
|
|
|
777
916
|
headers: request.headers ?? {},
|
|
778
917
|
};
|
|
779
918
|
const requestState = state.forConnection(requestContext.connectionId);
|
|
780
|
-
const cache = createProviderCache({ providerId: provider.id });
|
|
781
919
|
const bindings: ProviderEngineBindingCandidates = {
|
|
782
920
|
env,
|
|
783
921
|
credential,
|
|
@@ -829,25 +967,20 @@ function createProviderContext(
|
|
|
829
967
|
stt: options.stt ?? createSttClientFromEnv(provider.stt),
|
|
830
968
|
resolver: capabilityModules.resolver
|
|
831
969
|
? capabilityModules.resolver.bindResolverSignal(
|
|
832
|
-
options.resolver
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
mode: proxyPolicy.mode,
|
|
841
|
-
...proxyClientOptions,
|
|
842
|
-
...(stealthProfile ? { userAgent: stealthProfile.userAgent } : {}),
|
|
843
|
-
},
|
|
844
|
-
}
|
|
845
|
-
: {}),
|
|
846
|
-
}),
|
|
970
|
+
(options.resolver
|
|
971
|
+
? bindResolverTelemetry(options.resolver, resolverOptions.telemetry)
|
|
972
|
+
: undefined) ??
|
|
973
|
+
capabilityModules.resolver.createResolverClientFromEnv(
|
|
974
|
+
provider.resolver,
|
|
975
|
+
undefined,
|
|
976
|
+
resolverOptions,
|
|
977
|
+
),
|
|
847
978
|
signal,
|
|
848
979
|
)
|
|
849
980
|
: bindResolverSignalWithoutRuntime(
|
|
850
|
-
options.resolver
|
|
981
|
+
(options.resolver
|
|
982
|
+
? bindResolverTelemetry(options.resolver, resolverOptions.telemetry)
|
|
983
|
+
: undefined) ??
|
|
851
984
|
createUnsupportedResolverClient("Provider does not declare resolver capability"),
|
|
852
985
|
signal,
|
|
853
986
|
),
|
|
@@ -927,6 +1060,7 @@ export function resolveAuthFlowProxyAffinityKey(
|
|
|
927
1060
|
function createAuthFlowContext(
|
|
928
1061
|
provider: ProviderDefinition,
|
|
929
1062
|
request: AuthFlowRequest,
|
|
1063
|
+
route: AuthRoute,
|
|
930
1064
|
options: ProviderServerRuntimeOptions,
|
|
931
1065
|
state: ProviderRuntimeState,
|
|
932
1066
|
scope: RequestScopeContext,
|
|
@@ -934,6 +1068,7 @@ function createAuthFlowContext(
|
|
|
934
1068
|
): {
|
|
935
1069
|
context: FlowContext;
|
|
936
1070
|
getPatch: () => Record<string, unknown | null> | undefined;
|
|
1071
|
+
getEngineState: () => AuthFlowSuccessResponse["engine"];
|
|
937
1072
|
} {
|
|
938
1073
|
const baseUrl = getProviderBaseUrl(provider);
|
|
939
1074
|
const stealthBaseUrl = getProviderStealthBaseUrl(provider);
|
|
@@ -951,20 +1086,72 @@ function createAuthFlowContext(
|
|
|
951
1086
|
telemetry: scope.telemetry.proxy,
|
|
952
1087
|
engineCredentials: engineProxyCredentials,
|
|
953
1088
|
};
|
|
1089
|
+
// The lease pins the proxy egress a challenged ceremony solved on, so only providers
|
|
1090
|
+
// that declare the akamai_sbsd resolver kind carry one (same predicate as the challenge
|
|
1091
|
+
// runtime). `abort` never evaluates the key or tenant: a user must be able to cancel a
|
|
1092
|
+
// stranded flow even when engine provisioning is broken, and an abort is not a
|
|
1093
|
+
// continuation of the ceremony the lease pins.
|
|
1094
|
+
const ceremonyEgressLease =
|
|
1095
|
+
route !== "abort" &&
|
|
1096
|
+
declaresAkamaiSbsdResolver(provider) &&
|
|
1097
|
+
proxyPolicy &&
|
|
1098
|
+
proxyPolicy.mode !== "disabled"
|
|
1099
|
+
? createCeremonyEgressLeaseRuntime({
|
|
1100
|
+
tenantId: request.tenantId,
|
|
1101
|
+
providerId: provider.id,
|
|
1102
|
+
flowId: request.flowId,
|
|
1103
|
+
affinityKey: proxyClientOptions.affinityKey,
|
|
1104
|
+
...(request.engine?.egressLease ? { handle: request.engine.egressLease } : {}),
|
|
1105
|
+
})
|
|
1106
|
+
: undefined;
|
|
1107
|
+
if (route !== "abort" && request.engine?.egressLease && !ceremonyEgressLease) {
|
|
1108
|
+
throw new SDKError(
|
|
1109
|
+
"An egress lease was supplied for an auth flow that does not use a ceremony lease",
|
|
1110
|
+
{ code: "EGRESS_LEASE_INVALID" },
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
954
1113
|
const resolverIdentityScope = resolveProviderResolverIdentityScope(
|
|
955
1114
|
provider,
|
|
956
1115
|
proxyClientOptions.affinityKey,
|
|
957
1116
|
request.requestId,
|
|
958
1117
|
);
|
|
1118
|
+
const cache = createProviderCache({ providerId: provider.id });
|
|
1119
|
+
const { capabilityModules } = options;
|
|
1120
|
+
const resolverOptions = createResolverRuntimeOptions(
|
|
1121
|
+
provider,
|
|
1122
|
+
cache,
|
|
1123
|
+
resolverIdentityScope,
|
|
1124
|
+
proxyPolicy,
|
|
1125
|
+
proxyClientOptions,
|
|
1126
|
+
stealthProfile,
|
|
1127
|
+
scope.resolverTelemetry,
|
|
1128
|
+
);
|
|
1129
|
+
const challengeRuntime = createStealthChallengeDetection(
|
|
1130
|
+
provider,
|
|
1131
|
+
capabilityModules.resolver,
|
|
1132
|
+
options.resolver,
|
|
1133
|
+
resolverOptions,
|
|
1134
|
+
scope.trace,
|
|
1135
|
+
signal,
|
|
1136
|
+
);
|
|
959
1137
|
const stealthClientOptions = {
|
|
960
1138
|
upstream: proxyClientOptions.upstream,
|
|
961
1139
|
affinityKey: proxyClientOptions.affinityKey,
|
|
962
1140
|
telemetry: scope.telemetry.proxy,
|
|
963
1141
|
engineCredentials: engineProxyCredentials,
|
|
1142
|
+
...(ceremonyEgressLease ? { [ENGINE_CEREMONY_EGRESS_LEASE]: ceremonyEgressLease } : {}),
|
|
964
1143
|
...(signal ? { signal } : {}),
|
|
965
|
-
|
|
1144
|
+
// A declared resolver without a `stealth` block still gets the default Chrome client
|
|
1145
|
+
// (auth FlowContext is not capability-gated), so the runtime must ride along.
|
|
1146
|
+
...(provider.stealth || challengeRuntime
|
|
1147
|
+
? {
|
|
1148
|
+
stealth: {
|
|
1149
|
+
...(provider.stealth ?? {}),
|
|
1150
|
+
...(challengeRuntime ? { challengeRuntime } : {}),
|
|
1151
|
+
},
|
|
1152
|
+
}
|
|
1153
|
+
: {}),
|
|
966
1154
|
};
|
|
967
|
-
const { capabilityModules } = options;
|
|
968
1155
|
const logStealthCleanupError = (error: unknown) =>
|
|
969
1156
|
logProviderCleanupError(
|
|
970
1157
|
options.logger,
|
|
@@ -983,8 +1170,6 @@ function createAuthFlowContext(
|
|
|
983
1170
|
values: request.connection.secrets,
|
|
984
1171
|
})
|
|
985
1172
|
: undefined;
|
|
986
|
-
const cache = createProviderCache({ providerId: provider.id });
|
|
987
|
-
|
|
988
1173
|
const context: FlowContext = wrapWithInstrumentation({
|
|
989
1174
|
flowId: request.flowId,
|
|
990
1175
|
connectionId: resolveOperationConnectionId(request),
|
|
@@ -1026,25 +1211,20 @@ function createAuthFlowContext(
|
|
|
1026
1211
|
stt: options.stt ?? createSttClientFromEnv(provider.stt),
|
|
1027
1212
|
resolver: capabilityModules.resolver
|
|
1028
1213
|
? capabilityModules.resolver.bindResolverSignal(
|
|
1029
|
-
options.resolver
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
mode: proxyPolicy.mode,
|
|
1038
|
-
...proxyClientOptions,
|
|
1039
|
-
...(stealthProfile ? { userAgent: stealthProfile.userAgent } : {}),
|
|
1040
|
-
},
|
|
1041
|
-
}
|
|
1042
|
-
: {}),
|
|
1043
|
-
}),
|
|
1214
|
+
(options.resolver
|
|
1215
|
+
? bindResolverTelemetry(options.resolver, resolverOptions.telemetry)
|
|
1216
|
+
: undefined) ??
|
|
1217
|
+
capabilityModules.resolver.createResolverClientFromEnv(
|
|
1218
|
+
provider.resolver,
|
|
1219
|
+
undefined,
|
|
1220
|
+
resolverOptions,
|
|
1221
|
+
),
|
|
1044
1222
|
signal,
|
|
1045
1223
|
)
|
|
1046
1224
|
: bindResolverSignalWithoutRuntime(
|
|
1047
|
-
options.resolver
|
|
1225
|
+
(options.resolver
|
|
1226
|
+
? bindResolverTelemetry(options.resolver, resolverOptions.telemetry)
|
|
1227
|
+
: undefined) ??
|
|
1048
1228
|
createUnsupportedResolverClient("Provider does not declare resolver capability"),
|
|
1049
1229
|
signal,
|
|
1050
1230
|
),
|
|
@@ -1054,6 +1234,10 @@ function createAuthFlowContext(
|
|
|
1054
1234
|
return {
|
|
1055
1235
|
context,
|
|
1056
1236
|
getPatch: flowContextStore.getPatch,
|
|
1237
|
+
getEngineState() {
|
|
1238
|
+
const egressLease = ceremonyEgressLease?.handle();
|
|
1239
|
+
return egressLease ? { egressLease } : undefined;
|
|
1240
|
+
},
|
|
1057
1241
|
};
|
|
1058
1242
|
}
|
|
1059
1243
|
|
|
@@ -1082,6 +1266,7 @@ type ProviderServerLogEventBase = ProviderRequestCost & {
|
|
|
1082
1266
|
requestedProviderId?: string;
|
|
1083
1267
|
status: number;
|
|
1084
1268
|
proxy?: ProxyTelemetryLogPayload;
|
|
1269
|
+
resolver?: ResolverTelemetryLogPayload;
|
|
1085
1270
|
};
|
|
1086
1271
|
|
|
1087
1272
|
export type ProviderServerLogEvent =
|
|
@@ -1167,7 +1352,11 @@ export type ProviderServerOptions<TContext extends Partial<ProviderContext> = Pr
|
|
|
1167
1352
|
stt?: SttContext;
|
|
1168
1353
|
/** Optional OCR override for tests or custom hosts; local/prod normally resolves from env. */
|
|
1169
1354
|
ocr?: OcrContext;
|
|
1170
|
-
/**
|
|
1355
|
+
/**
|
|
1356
|
+
* Optional resolver override for tests or custom hosts; local/prod normally resolves from env.
|
|
1357
|
+
* SDK chains are reconstructed with each request's telemetry sink. An opaque host
|
|
1358
|
+
* resolver is reported as one custom invocation; its internal vendor work is not exposed.
|
|
1359
|
+
*/
|
|
1171
1360
|
resolver?: ResolverContext;
|
|
1172
1361
|
/** Optional runtime state override for tests or custom hosts. Production resolves Redis from env and fails closed when unavailable. */
|
|
1173
1362
|
state?: ProviderRuntimeState;
|
|
@@ -1888,8 +2077,10 @@ function createRequestScope(input: {
|
|
|
1888
2077
|
})
|
|
1889
2078
|
: createTraceContext();
|
|
1890
2079
|
const proxyCollector = new ProxyTelemetryCollector();
|
|
2080
|
+
const resolverCollector = new ResolverTelemetryCollector();
|
|
1891
2081
|
const telemetry = new RequestTelemetry(trace);
|
|
1892
2082
|
telemetry.register(proxyCollector);
|
|
2083
|
+
telemetry.register(resolverCollector);
|
|
1893
2084
|
let rootRunner: <T>(fn: () => Promise<T>) => Promise<T> = (fn) => fn();
|
|
1894
2085
|
let resolveRoot!: (outcome: RequestTerminalOutcome) => void;
|
|
1895
2086
|
const rootTerminal = new Promise<RequestTerminalOutcome>((resolve) => {
|
|
@@ -1967,6 +2158,7 @@ function createRequestScope(input: {
|
|
|
1967
2158
|
const scope: RequestScope = {
|
|
1968
2159
|
trace,
|
|
1969
2160
|
telemetry,
|
|
2161
|
+
resolverTelemetry: resolverCollector,
|
|
1970
2162
|
enrich(enrichment): void {
|
|
1971
2163
|
if (terminalOutcome) return;
|
|
1972
2164
|
if (enrichment.route !== undefined) details.route = enrichment.route;
|
|
@@ -2089,17 +2281,40 @@ function createRequestScope(input: {
|
|
|
2089
2281
|
return scope;
|
|
2090
2282
|
}
|
|
2091
2283
|
|
|
2284
|
+
// A raw `Response` result skips `toErrorResponse`, so its body stays
|
|
2285
|
+
// provider-owned, but the observability header is still SDK-owned: derive it
|
|
2286
|
+
// from the status. Only timeouts, rate limits, and 5xx are retryable;
|
|
2287
|
+
// `isRetryableCategory` would otherwise mark every other 4xx (`upstream_http`)
|
|
2288
|
+
// as retryable.
|
|
2289
|
+
function rawResponseErrorObservability(status: number): ErrorObservabilityDetails {
|
|
2290
|
+
return {
|
|
2291
|
+
category: categoryForStatus(status),
|
|
2292
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
2293
|
+
retryable: status === 408 || status === 429 || status >= 500,
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
// `rawStatusFallback` is decided by the caller from the outcome, not from a
|
|
2298
|
+
// missing snapshot: a failed outcome whose thrown value is `undefined` also has
|
|
2299
|
+
// no `errorObservability`, and its 500 carries the SDK envelope, not a raw body.
|
|
2092
2300
|
function responseWithRequestScopeHeaders(
|
|
2093
2301
|
response: Response,
|
|
2094
2302
|
finished: RequestScopeFinishResult,
|
|
2303
|
+
rawStatusFallback: boolean,
|
|
2095
2304
|
): Response {
|
|
2096
2305
|
const headers = new Headers(response.headers);
|
|
2097
2306
|
headers.delete(PROVIDER_TELEMETRY_HEADER);
|
|
2098
2307
|
if (finished.providerTelemetryHeader) {
|
|
2099
2308
|
headers.set(PROVIDER_TELEMETRY_HEADER, finished.providerTelemetryHeader);
|
|
2100
2309
|
}
|
|
2101
|
-
|
|
2102
|
-
|
|
2310
|
+
headers.delete(ERROR_OBSERVABILITY_HEADER);
|
|
2311
|
+
const errorObservability =
|
|
2312
|
+
finished.errorObservability ??
|
|
2313
|
+
(rawStatusFallback && response.status >= 400
|
|
2314
|
+
? rawResponseErrorObservability(response.status)
|
|
2315
|
+
: undefined);
|
|
2316
|
+
if (errorObservability) {
|
|
2317
|
+
headers.set(ERROR_OBSERVABILITY_HEADER, JSON.stringify(errorObservability));
|
|
2103
2318
|
}
|
|
2104
2319
|
return new Response(response.body, {
|
|
2105
2320
|
headers,
|
|
@@ -2118,6 +2333,7 @@ function finalizeRequestResponse(
|
|
|
2118
2333
|
const finalResponse = responseWithRequestScopeHeaders(
|
|
2119
2334
|
response,
|
|
2120
2335
|
scope.snapshotHeaders(error),
|
|
2336
|
+
outcome.kind === "completed",
|
|
2121
2337
|
);
|
|
2122
2338
|
scope.terminalize(outcome);
|
|
2123
2339
|
return finalResponse;
|
|
@@ -2484,6 +2700,7 @@ function getHttpStreamTransport(
|
|
|
2484
2700
|
function toAuthFlowResponse(
|
|
2485
2701
|
result: unknown,
|
|
2486
2702
|
contextPatch: Record<string, unknown | null> | undefined,
|
|
2703
|
+
engine: AuthFlowSuccessResponse["engine"],
|
|
2487
2704
|
): Response | AuthFlowSuccessResponse {
|
|
2488
2705
|
if (result instanceof Response) {
|
|
2489
2706
|
return result;
|
|
@@ -2496,6 +2713,7 @@ function toAuthFlowResponse(
|
|
|
2496
2713
|
return {
|
|
2497
2714
|
data: result,
|
|
2498
2715
|
...(contextPatch ? { contextPatch } : {}),
|
|
2716
|
+
...(engine ? { engine } : {}),
|
|
2499
2717
|
};
|
|
2500
2718
|
}
|
|
2501
2719
|
|
|
@@ -2672,10 +2890,12 @@ async function handleAuthFlow(
|
|
|
2672
2890
|
// depend on declared secrets (client ids/secrets), so fail structured before
|
|
2673
2891
|
// any flow code runs instead of at whatever point the ceremony first reads
|
|
2674
2892
|
// the env. `abort` stays exempt: a user must always be able to cancel a
|
|
2675
|
-
// stranded flow even when provisioning is broken
|
|
2676
|
-
|
|
2893
|
+
// stranded flow even when provisioning is broken (createAuthFlowContext
|
|
2894
|
+
// applies the same exemption to the engine ceremony lease).
|
|
2895
|
+
const { context, getPatch, getEngineState } = createAuthFlowContext(
|
|
2677
2896
|
provider,
|
|
2678
2897
|
request,
|
|
2898
|
+
route,
|
|
2679
2899
|
options,
|
|
2680
2900
|
state,
|
|
2681
2901
|
scope,
|
|
@@ -2715,10 +2935,10 @@ async function handleAuthFlow(
|
|
|
2715
2935
|
isAuthTurn(result)
|
|
2716
2936
|
? materializeAuthFlowTurn(provider, request, result)
|
|
2717
2937
|
: result;
|
|
2718
|
-
return toAuthFlowResponse(materializedResult, getPatch());
|
|
2938
|
+
return toAuthFlowResponse(materializedResult, getPatch(), getEngineState());
|
|
2719
2939
|
} catch (error) {
|
|
2720
2940
|
if (error instanceof AuthAbortError) {
|
|
2721
|
-
return toAuthFlowResponse(error.turn, getPatch());
|
|
2941
|
+
return toAuthFlowResponse(error.turn, getPatch(), getEngineState());
|
|
2722
2942
|
}
|
|
2723
2943
|
throw error;
|
|
2724
2944
|
} finally {
|
|
@@ -3185,7 +3405,7 @@ function createServerAppWithCapabilityModules(
|
|
|
3185
3405
|
);
|
|
3186
3406
|
const response = handled instanceof Response ? handled : c.json(handled);
|
|
3187
3407
|
return streaming
|
|
3188
|
-
? responseWithRequestScopeHeaders(response, requestScope.snapshotHeaders())
|
|
3408
|
+
? responseWithRequestScopeHeaders(response, requestScope.snapshotHeaders(), true)
|
|
3189
3409
|
: response;
|
|
3190
3410
|
};
|
|
3191
3411
|
const response = streaming
|