@apifuse/provider-sdk 2.2.0-beta.14 → 2.2.0-beta.16
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 +52 -13
- package/CHANGELOG.md +11 -0
- package/dist/define.js +29 -1
- package/dist/error-resolution.d.ts +2 -0
- package/dist/error-resolution.js +90 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +19 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/native-egress-policy.d.ts +27 -0
- package/dist/native-egress-policy.js +225 -0
- package/dist/provider.d.ts +3 -3
- package/dist/provider.js +2 -2
- package/dist/runtime/http.js +189 -9
- package/dist/runtime/native-network.d.ts +39 -4
- package/dist/runtime/native-network.js +365 -20
- package/dist/runtime/redirects.d.ts +29 -0
- package/dist/runtime/redirects.js +36 -0
- package/dist/runtime/stealth.js +16 -44
- package/dist/server/serve.js +105 -94
- package/dist/testing/run.js +32 -13
- package/dist/types.d.ts +26 -3
- package/dist/types.js +1 -0
- package/package.json +1 -1
- package/src/define.ts +44 -0
- package/src/error-resolution.ts +91 -0
- package/src/errors.ts +28 -0
- package/src/index.ts +7 -0
- package/src/native-egress-policy.ts +285 -0
- package/src/provider.ts +8 -0
- package/src/runtime/http.ts +217 -9
- package/src/runtime/native-network.ts +474 -22
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/stealth.ts +20 -47
- package/src/server/serve.ts +141 -92
- package/src/testing/run.ts +39 -14
- package/src/types.ts +37 -3
package/dist/server/serve.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { Hono } from "hono";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { AuthAbortError, createAuthFlowHelpers } from "../auth.js";
|
|
6
|
+
import { SDK_OWNED_PROVIDER_ERROR_CODES, SDK_RUNTIME_OWNED_ERROR_CODES, } from "../error-resolution.js";
|
|
6
7
|
import { AuthError, isProviderError, isSessionExpiredError, isTransportError, isValidationError, ProviderError, } from "../errors.js";
|
|
7
8
|
import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog.js";
|
|
8
9
|
import { categoryForStatus, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability.js";
|
|
@@ -15,6 +16,7 @@ import { createEnvContext } from "../runtime/env.js";
|
|
|
15
16
|
import { executeOperation } from "../runtime/executor.js";
|
|
16
17
|
import { createHttpClient } from "../runtime/http.js";
|
|
17
18
|
import { wrapWithInstrumentation } from "../runtime/instrumentation.js";
|
|
19
|
+
import { createNativeNetworkClient } from "../runtime/native-network.js";
|
|
18
20
|
import { getProviderBaseUrl } from "../runtime/provider.js";
|
|
19
21
|
import { PROXY_AUTH_IP_DENIED_CODE, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_EXHAUSTED_CODE, } from "../runtime/proxy-errors.js";
|
|
20
22
|
import { PROVIDER_TELEMETRY_HEADER, ProxyTelemetryCollector } from "../runtime/proxy-telemetry.js";
|
|
@@ -28,6 +30,7 @@ import { STATEFUL_NONCE_HEADER as STATEFUL_FORWARDING_NONCE_HEADER, STATEFUL_SIG
|
|
|
28
30
|
import { StatefulRoutingDeadlineError } from "../stateful/stateful-provider-session-routing.js";
|
|
29
31
|
import { getStealthProfile } from "../stealth/profiles.js";
|
|
30
32
|
import { APIFUSE_STREAM_DONE_EVENT, APIFUSE_STREAM_ERROR_EVENT, encodeSseEvent, error as streamError, } from "../stream.js";
|
|
33
|
+
import { VALID_OPERATION_ERROR_STATUSES } from "../types.js";
|
|
31
34
|
import { createSelfTestApp, createSelfTestAuthFlowInvoke, createSelfTestInvoke, resolveSelfTestPort, } from "./self-test.js";
|
|
32
35
|
import { resolveSelfTestMasterSecrets } from "./self-test-token.js";
|
|
33
36
|
import { AuthFlowRequestSchema, OperationConnectionSchema, OperationRequestSchema, } from "./types.js";
|
|
@@ -148,6 +151,15 @@ export function resolveProviderProxyAffinityKey(provider, request, operationId)
|
|
|
148
151
|
function resolveOperationConnectionId(request) {
|
|
149
152
|
return request.connection?.id ?? request.connectionId;
|
|
150
153
|
}
|
|
154
|
+
function resolveNativeProxyPolicy(provider) {
|
|
155
|
+
if (typeof provider.proxy === "object")
|
|
156
|
+
return provider.proxy;
|
|
157
|
+
if (provider.proxy === true)
|
|
158
|
+
return { mode: "optional" };
|
|
159
|
+
if (provider.proxy === false)
|
|
160
|
+
return { mode: "disabled" };
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
151
163
|
function createProviderContext(provider, request, operationId, options = {}, state = createUnsupportedProviderRuntimeState(), proxyTelemetry) {
|
|
152
164
|
const baseUrl = getProviderBaseUrl(provider);
|
|
153
165
|
const stealthBaseUrl = getProviderStealthBaseUrl(provider);
|
|
@@ -206,6 +218,17 @@ function createProviderContext(provider, request, operationId, options = {}, sta
|
|
|
206
218
|
engine: provider.browser?.engine,
|
|
207
219
|
})
|
|
208
220
|
: createBrowserStub(),
|
|
221
|
+
...(provider.native
|
|
222
|
+
? {
|
|
223
|
+
native: {
|
|
224
|
+
network: createNativeNetworkClient({
|
|
225
|
+
egress: provider.native.network,
|
|
226
|
+
proxyPolicy: resolveNativeProxyPolicy(provider),
|
|
227
|
+
affinityKey: proxyClientOptions.affinityKey,
|
|
228
|
+
}),
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
: {}),
|
|
209
232
|
trace: createTraceContext(),
|
|
210
233
|
auth: createAuthStub(),
|
|
211
234
|
stt: options.stt ?? createSttClientFromEnv(provider.stt),
|
|
@@ -282,6 +305,17 @@ function createAuthFlowContext(provider, request, options = {}, signal) {
|
|
|
282
305
|
? createStealthClient(stealthBaseUrl, stealthProfile.name, stealthClientOptions)
|
|
283
306
|
: createStealthClient(stealthBaseUrl, stealthClientOptions)
|
|
284
307
|
: createStealthStub(),
|
|
308
|
+
...(provider.native
|
|
309
|
+
? {
|
|
310
|
+
native: {
|
|
311
|
+
network: createNativeNetworkClient({
|
|
312
|
+
egress: provider.native.network,
|
|
313
|
+
proxyPolicy: resolveNativeProxyPolicy(provider),
|
|
314
|
+
affinityKey: proxyClientOptions.affinityKey,
|
|
315
|
+
}),
|
|
316
|
+
},
|
|
317
|
+
}
|
|
318
|
+
: {}),
|
|
285
319
|
env: createEnvContext(provider.secrets?.map((secret) => secret.name)),
|
|
286
320
|
credential,
|
|
287
321
|
context: flowContextStore.context,
|
|
@@ -321,8 +355,8 @@ function zodDetails(error) {
|
|
|
321
355
|
message: issue.message,
|
|
322
356
|
}));
|
|
323
357
|
}
|
|
324
|
-
function toErrorResponse(error, requestId) {
|
|
325
|
-
const observability = errorObservabilityDetails(error);
|
|
358
|
+
function toErrorResponse(error, requestId, declaredErrorCode) {
|
|
359
|
+
const observability = errorObservabilityDetails(error, declaredErrorCode);
|
|
326
360
|
if (error instanceof StatefulRoutingDeadlineError) {
|
|
327
361
|
return {
|
|
328
362
|
error: {
|
|
@@ -382,7 +416,10 @@ function toErrorResponse(error, requestId) {
|
|
|
382
416
|
// narrowing from a ProviderError-typed value would collapse the negative branch
|
|
383
417
|
// to `never`. Narrowing from unknown avoids that while still recognizing errors
|
|
384
418
|
// from a duplicate SDK module instance.
|
|
385
|
-
function providerObservabilityDetails(error) {
|
|
419
|
+
function providerObservabilityDetails(error, declaredErrorCode) {
|
|
420
|
+
const declaredRetryable = sdkOwnsErrorResolution(error)
|
|
421
|
+
? undefined
|
|
422
|
+
: declaredErrorCode?.retryable;
|
|
386
423
|
// Session-expiry surfaces the credential_expired category + the opt-in
|
|
387
424
|
// retryable signal so Gateway/Credential Service can refresh and re-drive the
|
|
388
425
|
// operation (see design.md §4.3 D3). Without this branch the auth error would
|
|
@@ -392,7 +429,7 @@ function providerObservabilityDetails(error) {
|
|
|
392
429
|
return {
|
|
393
430
|
category: error.options?.category ?? "credential_expired",
|
|
394
431
|
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
395
|
-
retryable: error.options?.retryable ?? false,
|
|
432
|
+
retryable: error.options?.retryable ?? declaredRetryable ?? false,
|
|
396
433
|
};
|
|
397
434
|
}
|
|
398
435
|
// Missing-secret errors carry the canonical credential_unavailable category
|
|
@@ -404,7 +441,7 @@ function providerObservabilityDetails(error) {
|
|
|
404
441
|
return {
|
|
405
442
|
category: error.options?.category ?? "credential_unavailable",
|
|
406
443
|
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
407
|
-
retryable: error.options?.retryable ?? false,
|
|
444
|
+
retryable: error.options?.retryable ?? declaredRetryable ?? false,
|
|
408
445
|
};
|
|
409
446
|
}
|
|
410
447
|
if (!isTransportError(error)) {
|
|
@@ -435,17 +472,23 @@ function providerObservabilityDetails(error) {
|
|
|
435
472
|
...(error.upstreamStatus ? { upstreamStatus: error.upstreamStatus } : {}),
|
|
436
473
|
};
|
|
437
474
|
}
|
|
438
|
-
function errorObservabilityDetails(error) {
|
|
439
|
-
const
|
|
475
|
+
function errorObservabilityDetails(error, declaredErrorCode) {
|
|
476
|
+
const effectiveDeclaration = sdkOwnsErrorResolution(error) ? undefined : declaredErrorCode;
|
|
477
|
+
const providerDetails = providerObservabilityDetails(error, effectiveDeclaration);
|
|
440
478
|
if (providerDetails)
|
|
441
479
|
return providerDetails;
|
|
442
480
|
if (error instanceof z.ZodError || isValidationError(error)) {
|
|
481
|
+
const declaredStatus = effectiveDeclaration?.status;
|
|
443
482
|
return {
|
|
444
483
|
category: isProviderError(error) && error.options?.category
|
|
445
484
|
? error.options.category
|
|
446
|
-
:
|
|
485
|
+
: isEmittableErrorStatus(declaredStatus) && declaredStatus >= 500
|
|
486
|
+
? "provider_error"
|
|
487
|
+
: "input_validation",
|
|
447
488
|
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
448
|
-
retryable: isProviderError(error)
|
|
489
|
+
retryable: isProviderError(error)
|
|
490
|
+
? (error.options?.retryable ?? effectiveDeclaration?.retryable ?? false)
|
|
491
|
+
: false,
|
|
449
492
|
};
|
|
450
493
|
}
|
|
451
494
|
if (error instanceof StatefulRoutingDeadlineError) {
|
|
@@ -459,7 +502,7 @@ function errorObservabilityDetails(error) {
|
|
|
459
502
|
return {
|
|
460
503
|
category: error.options?.category ?? "provider_error",
|
|
461
504
|
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
462
|
-
retryable: error.options?.retryable ?? false,
|
|
505
|
+
retryable: error.options?.retryable ?? effectiveDeclaration?.retryable ?? false,
|
|
463
506
|
};
|
|
464
507
|
}
|
|
465
508
|
return {
|
|
@@ -468,9 +511,9 @@ function errorObservabilityDetails(error) {
|
|
|
468
511
|
retryable: false,
|
|
469
512
|
};
|
|
470
513
|
}
|
|
471
|
-
function responseWithErrorObservability(response, error) {
|
|
514
|
+
function responseWithErrorObservability(response, error, declaredErrorCode) {
|
|
472
515
|
const headers = new Headers(response.headers);
|
|
473
|
-
headers.set(ERROR_OBSERVABILITY_HEADER, JSON.stringify(errorObservabilityDetails(error)));
|
|
516
|
+
headers.set(ERROR_OBSERVABILITY_HEADER, JSON.stringify(errorObservabilityDetails(error, declaredErrorCode)));
|
|
474
517
|
return new Response(response.body, {
|
|
475
518
|
status: response.status,
|
|
476
519
|
statusText: response.statusText,
|
|
@@ -502,7 +545,11 @@ function publicProviderErrorMessage(error) {
|
|
|
502
545
|
}
|
|
503
546
|
return error.message;
|
|
504
547
|
}
|
|
505
|
-
function
|
|
548
|
+
function isEmittableErrorStatus(value) {
|
|
549
|
+
return (typeof value === "number" &&
|
|
550
|
+
VALID_OPERATION_ERROR_STATUSES.some((status) => status === value));
|
|
551
|
+
}
|
|
552
|
+
function toStatusCode(error, declaredErrorCode) {
|
|
506
553
|
if (error instanceof z.ZodError) {
|
|
507
554
|
return 400;
|
|
508
555
|
}
|
|
@@ -510,6 +557,10 @@ function toStatusCode(error) {
|
|
|
510
557
|
return 504;
|
|
511
558
|
}
|
|
512
559
|
if (isProviderError(error)) {
|
|
560
|
+
if (!sdkOwnsErrorResolution(error) &&
|
|
561
|
+
isEmittableErrorStatus(declaredErrorCode?.status)) {
|
|
562
|
+
return declaredErrorCode.status;
|
|
563
|
+
}
|
|
513
564
|
switch (error.code) {
|
|
514
565
|
case "AUTH_REQUIRED":
|
|
515
566
|
case "reauth_required":
|
|
@@ -544,77 +595,32 @@ function toStatusCode(error) {
|
|
|
544
595
|
}
|
|
545
596
|
return 500;
|
|
546
597
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
"
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
"turn_validation_error",
|
|
574
|
-
"context_access_error",
|
|
575
|
-
"UNSUPPORTED_STT_OPTION",
|
|
576
|
-
"INVALID_STT_AUDIO",
|
|
577
|
-
"STT_AUDIO_TOO_LARGE",
|
|
578
|
-
"STT_UPSTREAM_FAILED",
|
|
579
|
-
"INVALID_STT_VERIFICATION_CODE_OPTIONS",
|
|
580
|
-
"NO_CODE_FOUND",
|
|
581
|
-
"AMBIGUOUS_CODE",
|
|
582
|
-
"retry_invalid_policy",
|
|
583
|
-
"retry_unsafe_method",
|
|
584
|
-
"stealth_cookie_store_serialize_failed",
|
|
585
|
-
"response_too_large",
|
|
586
|
-
"transport_stream_unavailable",
|
|
587
|
-
"transport_invalid_method",
|
|
588
|
-
"http_transport_override_unsupported",
|
|
589
|
-
"transport_invalid_url",
|
|
590
|
-
"retry_exhausted",
|
|
591
|
-
"auth_abort_unsafe_data",
|
|
592
|
-
"credentials_auth_missing_credential_keys",
|
|
593
|
-
"credentials_auth_missing_credential",
|
|
594
|
-
"credentials_auth_invalid_login_result",
|
|
595
|
-
"credentials_auth_unknown_challenge",
|
|
596
|
-
"credentials_auth_unknown_pending_challenge",
|
|
597
|
-
"STATEFUL_FORWARDING_NOT_CONFIGURED",
|
|
598
|
-
"STATEFUL_FORWARDING_SIGNATURE_MISSING",
|
|
599
|
-
"STATEFUL_FORWARDING_NONCE_INVALID",
|
|
600
|
-
"STATEFUL_FORWARDING_TIMESTAMP_INVALID",
|
|
601
|
-
"STATEFUL_FORWARDING_SIGNATURE_INVALID",
|
|
602
|
-
"STATEFUL_FORWARDING_REPLAY_DETECTED",
|
|
603
|
-
"STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
|
|
604
|
-
"STATEFUL_FORWARDING_ENVELOPE_INVALID",
|
|
605
|
-
"STATEFUL_FORWARDING_PROVIDER_MISMATCH",
|
|
606
|
-
"STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
|
|
607
|
-
"STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
|
|
608
|
-
"STATEFUL_FORWARDING_REQUEST_FAILED",
|
|
609
|
-
"STATEFUL_FORWARDING_CONTEXT_MISSING",
|
|
610
|
-
"STATEFUL_FORWARDING_BAD_RESPONSE",
|
|
611
|
-
"STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
|
|
612
|
-
"STATEFUL_FILE_FORWARDING_UNSUPPORTED",
|
|
613
|
-
"STATEFUL_CONTROL_PLANE_OPERATION_AMBIGUOUS",
|
|
614
|
-
"STATEFUL_CONTROL_PLANE_REQUEST_FAILED",
|
|
615
|
-
"STATEFUL_CONTROL_PLANE_HTTP_ERROR",
|
|
616
|
-
"STATEFUL_CONTROL_PLANE_INVALID_RESPONSE",
|
|
617
|
-
]);
|
|
598
|
+
function sdkOwnsErrorResolution(error) {
|
|
599
|
+
if (isSessionExpiredError(error))
|
|
600
|
+
return true;
|
|
601
|
+
if (isTransportError(error))
|
|
602
|
+
return true;
|
|
603
|
+
if (error instanceof z.ZodError)
|
|
604
|
+
return true;
|
|
605
|
+
if (error instanceof StatefulRoutingDeadlineError)
|
|
606
|
+
return true;
|
|
607
|
+
return (isProviderError(error) &&
|
|
608
|
+
typeof error.code === "string" &&
|
|
609
|
+
SDK_RUNTIME_OWNED_ERROR_CODES.has(error.code));
|
|
610
|
+
}
|
|
611
|
+
function buildOperationErrorCodeLookup(provider) {
|
|
612
|
+
return new Map(Object.entries(provider.operations).flatMap(([operationId, operation]) => {
|
|
613
|
+
const errorCodes = operation.docs?.errorCodes;
|
|
614
|
+
return errorCodes?.length
|
|
615
|
+
? [[operationId, new Map(errorCodes.map((entry) => [entry.code, entry]))]]
|
|
616
|
+
: [];
|
|
617
|
+
}));
|
|
618
|
+
}
|
|
619
|
+
function declaredErrorCodeFor(error, operationId, lookup) {
|
|
620
|
+
if (!operationId || !isProviderError(error) || typeof error.code !== "string")
|
|
621
|
+
return undefined;
|
|
622
|
+
return lookup.get(operationId)?.get(error.code);
|
|
623
|
+
}
|
|
618
624
|
function extractRequestId(raw) {
|
|
619
625
|
if (!raw || typeof raw !== "object") {
|
|
620
626
|
return undefined;
|
|
@@ -622,7 +628,7 @@ function extractRequestId(raw) {
|
|
|
622
628
|
const value = Object.getOwnPropertyDescriptor(raw, "requestId")?.value;
|
|
623
629
|
return typeof value === "string" ? value : undefined;
|
|
624
630
|
}
|
|
625
|
-
function logProviderError(logger, provider, kind, route, requestId, error, status, cost) {
|
|
631
|
+
function logProviderError(logger, provider, kind, route, requestId, error, status, cost, declaredErrorCode) {
|
|
626
632
|
const code = isProviderError(error)
|
|
627
633
|
? (error.code ?? "provider_error")
|
|
628
634
|
: error instanceof z.ZodError
|
|
@@ -632,12 +638,13 @@ function logProviderError(logger, provider, kind, route, requestId, error, statu
|
|
|
632
638
|
: "internal_error";
|
|
633
639
|
const errorClass = error instanceof Error ? error.name : typeof error;
|
|
634
640
|
const message = error instanceof Error ? error.message : String(error);
|
|
635
|
-
const details = errorObservabilityDetails(error);
|
|
641
|
+
const details = errorObservabilityDetails(error, declaredErrorCode);
|
|
636
642
|
const isUnregisteredProviderErrorCode = status === 500 &&
|
|
637
643
|
isProviderError(error) &&
|
|
638
644
|
!isValidationError(error) &&
|
|
639
645
|
typeof error.code === "string" &&
|
|
640
|
-
!SDK_OWNED_PROVIDER_ERROR_CODES.has(error.code)
|
|
646
|
+
!SDK_OWNED_PROVIDER_ERROR_CODES.has(error.code) &&
|
|
647
|
+
declaredErrorCode === undefined;
|
|
641
648
|
const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
642
649
|
emit({
|
|
643
650
|
level: status >= 500 ? "error" : "warn",
|
|
@@ -1233,6 +1240,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1233
1240
|
validateStatefulServerConfig(options);
|
|
1234
1241
|
const app = new Hono();
|
|
1235
1242
|
const logger = options.logger ?? defaultProviderServerLogger;
|
|
1243
|
+
const operationErrorCodes = buildOperationErrorCodeLookup(provider);
|
|
1236
1244
|
const statefulForwardingReplayCache = new StatefulForwardingReplayCache(options.statefulForwarding?.replayCacheMaxEntries ??
|
|
1237
1245
|
DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES);
|
|
1238
1246
|
const state = options.state ??
|
|
@@ -1266,6 +1274,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1266
1274
|
app.post(STATEFUL_INTERNAL_OPERATIONS_ROUTE, async (c) => {
|
|
1267
1275
|
let rawBodyText = "";
|
|
1268
1276
|
let rawBody;
|
|
1277
|
+
let operationId;
|
|
1269
1278
|
const operation = "stateful-internal";
|
|
1270
1279
|
const requestCost = startRequestCost();
|
|
1271
1280
|
try {
|
|
@@ -1351,7 +1360,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1351
1360
|
});
|
|
1352
1361
|
}
|
|
1353
1362
|
const request = operationRequestFromForwardingEnvelope(envelope);
|
|
1354
|
-
|
|
1363
|
+
operationId = envelope.operationId;
|
|
1355
1364
|
const ctx = createProviderContext(provider, request, operationId, options, state);
|
|
1356
1365
|
if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
|
|
1357
1366
|
throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt);
|
|
@@ -1368,13 +1377,14 @@ export function createServerApp(provider, options = {}) {
|
|
|
1368
1377
|
return c.json({ data: output });
|
|
1369
1378
|
}
|
|
1370
1379
|
catch (error) {
|
|
1371
|
-
const
|
|
1380
|
+
const declaredErrorCode = declaredErrorCodeFor(error, operationId, operationErrorCodes);
|
|
1381
|
+
const status = toStatusCode(error, declaredErrorCode);
|
|
1372
1382
|
if (isProviderError(error) && error.code === "STATEFUL_FORWARDING_REPLAY_CACHE_FULL") {
|
|
1373
1383
|
c.header("Retry-After", String(STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS));
|
|
1374
1384
|
}
|
|
1375
1385
|
const requestId = extractRequestId(rawBody);
|
|
1376
|
-
logProviderError(logger, provider, "operation", operation, requestId, error, status, finishRequestCost(requestCost));
|
|
1377
|
-
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1386
|
+
logProviderError(logger, provider, "operation", operationId || operation, requestId, error, status, finishRequestCost(requestCost), declaredErrorCode);
|
|
1387
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, declaredErrorCode), status), error, declaredErrorCode);
|
|
1378
1388
|
}
|
|
1379
1389
|
});
|
|
1380
1390
|
app.post("/v1/:operation", async (c) => {
|
|
@@ -1402,13 +1412,14 @@ export function createServerApp(provider, options = {}) {
|
|
|
1402
1412
|
return c.json(response);
|
|
1403
1413
|
}
|
|
1404
1414
|
catch (error) {
|
|
1405
|
-
const
|
|
1415
|
+
const declaredErrorCode = declaredErrorCodeFor(error, operation, operationErrorCodes);
|
|
1416
|
+
const status = toStatusCode(error, declaredErrorCode);
|
|
1406
1417
|
const requestId = extractRequestId(rawBody);
|
|
1407
|
-
logProviderError(logger, provider, "operation", operation, requestId, error, status, finishRequestCost(requestCost));
|
|
1418
|
+
logProviderError(logger, provider, "operation", operation, requestId, error, status, finishRequestCost(requestCost), declaredErrorCode);
|
|
1408
1419
|
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
1409
1420
|
if (telemetryHeader)
|
|
1410
1421
|
c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
1411
|
-
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1422
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, declaredErrorCode), status), error, declaredErrorCode);
|
|
1412
1423
|
}
|
|
1413
1424
|
});
|
|
1414
1425
|
app.post("/auth/start", async (c) => {
|
package/dist/testing/run.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createProviderCache } from "../runtime/cache.js";
|
|
|
3
3
|
import { createTestProviderChoiceContext } from "../runtime/choice.js";
|
|
4
4
|
import { createMemoryProviderRuntimeState } from "../runtime/state.js";
|
|
5
5
|
import { createUnsupportedSttClient } from "../runtime/stt.js";
|
|
6
|
+
import { createNativeEgressAuthorization, NativeNetworkError, snapshotNativeConnectInput, snapshotNativeGrantInput, } from "../runtime/native-network.js";
|
|
6
7
|
import { safeParseSchemaSync } from "../schema.js";
|
|
7
8
|
import { requestPathForFixture } from "../fixture-sanitization.js";
|
|
8
9
|
import { findStreamCaptureGroup, replayStreamEvidence } from "../stream-evidence.js";
|
|
@@ -183,6 +184,14 @@ function createUpstreamContext(provider, operationName, upstreamStub) {
|
|
|
183
184
|
};
|
|
184
185
|
const request = { headers: {} };
|
|
185
186
|
const state = createMemoryProviderRuntimeState();
|
|
187
|
+
const nativeEgress = provider.native
|
|
188
|
+
? createNativeEgressAuthorization({ egress: provider.native.network })
|
|
189
|
+
: undefined;
|
|
190
|
+
const requireNativeEgress = () => {
|
|
191
|
+
if (!nativeEgress)
|
|
192
|
+
throw new NativeNetworkError("Native egress authorization is unavailable", "native_egress_policy_invalid");
|
|
193
|
+
return nativeEgress;
|
|
194
|
+
};
|
|
186
195
|
const dispatch = async (call) => {
|
|
187
196
|
const canned = await upstreamStub({ operationName, ...call });
|
|
188
197
|
if (canned === undefined) {
|
|
@@ -353,19 +362,29 @@ function createUpstreamContext(provider, operationName, upstreamStub) {
|
|
|
353
362
|
? {
|
|
354
363
|
native: {
|
|
355
364
|
network: {
|
|
356
|
-
connectTcp: async (options) =>
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
365
|
+
connectTcp: async (options) => {
|
|
366
|
+
const request = snapshotNativeConnectInput(options);
|
|
367
|
+
requireNativeEgress().assertConnect(request, "disabled");
|
|
368
|
+
return createNativeConnection(await dispatch({
|
|
369
|
+
transport: "native",
|
|
370
|
+
method: "connectTcp",
|
|
371
|
+
url: `tcp://${request.host}:${request.port}`,
|
|
372
|
+
options: request,
|
|
373
|
+
}), dispatch, `tcp://${request.host}:${request.port}`);
|
|
374
|
+
},
|
|
375
|
+
connectTls: async (options) => {
|
|
376
|
+
const request = snapshotNativeConnectInput(options);
|
|
377
|
+
requireNativeEgress().assertConnect(request, "required");
|
|
378
|
+
return createNativeConnection(await dispatch({
|
|
379
|
+
transport: "native",
|
|
380
|
+
method: "connectTls",
|
|
381
|
+
url: `tls://${request.host}:${request.port}`,
|
|
382
|
+
options: request,
|
|
383
|
+
}), dispatch, `tls://${request.host}:${request.port}`);
|
|
384
|
+
},
|
|
385
|
+
grantTcpEgress: (input) => {
|
|
386
|
+
return requireNativeEgress().grant(snapshotNativeGrantInput(input));
|
|
387
|
+
},
|
|
369
388
|
},
|
|
370
389
|
},
|
|
371
390
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -610,9 +610,11 @@ export interface HealthMonitorProbeOverride {
|
|
|
610
610
|
/** Optional degraded threshold override for generated registry probes. */
|
|
611
611
|
degradedThresholdMs?: number;
|
|
612
612
|
}
|
|
613
|
+
export declare const VALID_OPERATION_ERROR_STATUSES: readonly [400, 401, 404, 429, 500, 502, 503, 504];
|
|
614
|
+
export type ProviderErrorStatus = (typeof VALID_OPERATION_ERROR_STATUSES)[number];
|
|
613
615
|
export interface OperationErrorCode {
|
|
614
616
|
code: string;
|
|
615
|
-
status?:
|
|
617
|
+
status?: ProviderErrorStatus;
|
|
616
618
|
description: string;
|
|
617
619
|
retryable?: boolean;
|
|
618
620
|
}
|
|
@@ -859,9 +861,24 @@ export interface RequestOptions {
|
|
|
859
861
|
*/
|
|
860
862
|
throwOnHttpError?: boolean;
|
|
861
863
|
retry?: boolean | HttpRetryPreset | HttpRetryOptions;
|
|
864
|
+
/**
|
|
865
|
+
* Opt-in redirect-hop enforcement for ctx.http. When present, redirects are
|
|
866
|
+
* evaluated before the next request is issued. Existing callers that omit
|
|
867
|
+
* this policy retain the native fetch redirect behavior.
|
|
868
|
+
*/
|
|
869
|
+
redirectPolicy?: HttpRedirectPolicy;
|
|
862
870
|
}
|
|
871
|
+
export type RedirectRunReason = "completed" | "stopped" | "max_hops" | "missing_location" | "loop";
|
|
872
|
+
export type HttpRedirectPolicyMode = "same-origin";
|
|
873
|
+
export interface HttpRedirectPolicy {
|
|
874
|
+
/** Only follow redirects whose canonical scheme, host, and port match the initial URL. */
|
|
875
|
+
mode: HttpRedirectPolicyMode;
|
|
876
|
+
/** Maximum number of redirect hops that may be followed. Must be an integer from 0 to 20. */
|
|
877
|
+
maxHops: number;
|
|
878
|
+
}
|
|
879
|
+
export type HttpRedirectFailureReason = Exclude<RedirectRunReason, "completed">;
|
|
863
880
|
export type HttpMethod = "HEAD" | "head" | "GET" | "get" | "POST" | "post" | "PUT" | "put" | "DELETE" | "delete" | "OPTIONS" | "options" | "TRACE" | "trace" | "PATCH" | "patch";
|
|
864
|
-
export interface StealthFetchOptions extends RequestOptions {
|
|
881
|
+
export interface StealthFetchOptions extends Omit<RequestOptions, "redirectPolicy"> {
|
|
865
882
|
method?: HttpMethod;
|
|
866
883
|
body?: string | Buffer;
|
|
867
884
|
redirect?: "follow" | "manual" | "error";
|
|
@@ -967,7 +984,7 @@ export interface StealthRedirectRunOptions extends Omit<StealthFetchOptions, "re
|
|
|
967
984
|
export interface StealthRedirectRunResult {
|
|
968
985
|
final: StealthResponse;
|
|
969
986
|
hops: StealthRedirectHop[];
|
|
970
|
-
reason:
|
|
987
|
+
reason: RedirectRunReason;
|
|
971
988
|
/**
|
|
972
989
|
* Complete flat view across all redirect hosts. Attributes and duplicate names are lost.
|
|
973
990
|
* @deprecated Use cookieStore for lossless persistence.
|
|
@@ -1072,6 +1089,12 @@ export interface NativeTcpEgressRule {
|
|
|
1072
1089
|
/**
|
|
1073
1090
|
* Bounded native TCP egress discovered through a declared bootstrap endpoint.
|
|
1074
1091
|
* Host suffixes are exact DNS suffixes, not wildcard patterns.
|
|
1092
|
+
*
|
|
1093
|
+
* Dynamic rules are ordered. The first rule whose source, target, port, and TLS
|
|
1094
|
+
* selectors match exclusively owns the grant; its ttlMs and maxGrants bounds
|
|
1095
|
+
* apply, and an exhausted/shorter rule never falls through to a later overlap.
|
|
1096
|
+
* Every rule must declare a source host selector, source port list/range, and
|
|
1097
|
+
* target port list/range; omitted ttlMs/maxGrants remain unbounded.
|
|
1075
1098
|
*/
|
|
1076
1099
|
export interface NativeTcpDynamicEgressRule {
|
|
1077
1100
|
readonly sourceHost?: string;
|
package/dist/types.js
CHANGED
|
@@ -32,6 +32,7 @@ export const HEALTH_CHECK_TIMEOUT_MS_MIN = 1;
|
|
|
32
32
|
export const HEALTH_CHECK_TIMEOUT_MS_MAX = 60_000;
|
|
33
33
|
export const HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN = 1;
|
|
34
34
|
export const HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX = 60_000;
|
|
35
|
+
export const VALID_OPERATION_ERROR_STATUSES = [400, 401, 404, 429, 500, 502, 503, 504];
|
|
35
36
|
export const HttpRetryPreset = {
|
|
36
37
|
Off: "off",
|
|
37
38
|
TransportTransient: "transport_transient",
|
package/package.json
CHANGED
package/src/define.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import ms from "ms";
|
|
2
2
|
|
|
3
|
+
import { SDK_RUNTIME_OWNED_ERROR_CODES } from "./error-resolution.js";
|
|
3
4
|
import { ProviderError, ValidationError } from "./errors.js";
|
|
5
|
+
import {
|
|
6
|
+
NativeEgressPolicyValidationError,
|
|
7
|
+
validateNativeProviderConfig,
|
|
8
|
+
} from "./native-egress-policy.js";
|
|
4
9
|
import { safeParseSchemaSync } from "./schema.js";
|
|
5
10
|
import { resolveHealthCheckInputDateTokens } from "./server/self-test-input-tokens.js";
|
|
6
11
|
import type {
|
|
@@ -52,6 +57,7 @@ import {
|
|
|
52
57
|
STREAM_IDLE_TIMEOUT_MS_MIN,
|
|
53
58
|
STREAM_MAX_DURATION_MS_MAX,
|
|
54
59
|
STREAM_MAX_DURATION_MS_MIN,
|
|
60
|
+
VALID_OPERATION_ERROR_STATUSES,
|
|
55
61
|
} from "./types.js";
|
|
56
62
|
|
|
57
63
|
type ProviderImplementationSourceAccess =
|
|
@@ -796,6 +802,36 @@ function validateOperationObservability(
|
|
|
796
802
|
}
|
|
797
803
|
}
|
|
798
804
|
|
|
805
|
+
function validateOperationErrorCodes(
|
|
806
|
+
providerId: string,
|
|
807
|
+
operations: Record<string, ProviderOperation>,
|
|
808
|
+
): void {
|
|
809
|
+
for (const [operationName, operation] of Object.entries(operations)) {
|
|
810
|
+
for (const [index, errorCode] of (operation.docs?.errorCodes ?? []).entries()) {
|
|
811
|
+
if (
|
|
812
|
+
errorCode.status !== undefined &&
|
|
813
|
+
!VALID_OPERATION_ERROR_STATUSES.some((status) => status === errorCode.status)
|
|
814
|
+
) {
|
|
815
|
+
const field = `operations.${operationName}.docs.errorCodes[${index}].status`;
|
|
816
|
+
throw new ValidationError(
|
|
817
|
+
`Provider "${providerId}" has invalid ${field}: ${String(errorCode.status)} is not an emittable provider error status.`,
|
|
818
|
+
{
|
|
819
|
+
fix: `Set ${field} to one of ${VALID_OPERATION_ERROR_STATUSES.join(", ")}, or omit it.`,
|
|
820
|
+
},
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
if (
|
|
824
|
+
errorCode.status !== undefined &&
|
|
825
|
+
SDK_RUNTIME_OWNED_ERROR_CODES.has(errorCode.code)
|
|
826
|
+
) {
|
|
827
|
+
console.warn(
|
|
828
|
+
`[provider-sdk] Provider "${providerId}" operation "${operationName}" declares status ${errorCode.status} for SDK-owned error code "${errorCode.code}"; the declared status is documentation-only and will be ignored at runtime.`,
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
799
835
|
const JSON_TRANSPORT_FIELDS = new Set(["kind"]);
|
|
800
836
|
const SSE_TRANSPORT_FIELDS = new Set([
|
|
801
837
|
"kind",
|
|
@@ -2181,6 +2217,7 @@ export function defineProvider<
|
|
|
2181
2217
|
validateOperationIds(config.id, config.operations);
|
|
2182
2218
|
validateOperationAnnotations(config.id, config.operations);
|
|
2183
2219
|
validateOperationObservability(config.id, config.operations);
|
|
2220
|
+
validateOperationErrorCodes(config.id, config.operations);
|
|
2184
2221
|
validateOperationTransports(config.id, config.operations);
|
|
2185
2222
|
validateOperationContracts(config.id, config.operations);
|
|
2186
2223
|
validateToolRouterMetadata(config.id, config.operations);
|
|
@@ -2204,6 +2241,13 @@ export function defineProvider<
|
|
|
2204
2241
|
);
|
|
2205
2242
|
validateOperationFixtures(config.id, operations);
|
|
2206
2243
|
validateProviderDeployment(config.id, config.deployment);
|
|
2244
|
+
try {
|
|
2245
|
+
validateNativeProviderConfig(config.native);
|
|
2246
|
+
} catch (error) {
|
|
2247
|
+
if (error instanceof NativeEgressPolicyValidationError)
|
|
2248
|
+
throw new ValidationError(error.message);
|
|
2249
|
+
throw error;
|
|
2250
|
+
}
|
|
2207
2251
|
validateProviderProxy(config);
|
|
2208
2252
|
validateProviderStt(config);
|
|
2209
2253
|
if (config.runtime === "browser" && !config.browser)
|