@apifuse/provider-sdk 2.2.0-beta.13 → 2.2.0-beta.15
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 +70 -6
- package/CHANGELOG.md +12 -0
- package/dist/define.js +9 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +25 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- 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/executor.js +17 -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/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/serve.d.ts +9 -0
- package/dist/server/serve.js +190 -51
- package/dist/server/types.d.ts +3 -0
- package/dist/server/types.js +1 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +9 -1
- package/dist/testing/run.js +32 -13
- package/dist/types.d.ts +23 -2
- package/package.json +1 -1
- package/src/define.ts +11 -0
- package/src/errors.ts +37 -0
- package/src/index.ts +12 -1
- package/src/native-egress-policy.ts +285 -0
- package/src/provider.ts +7 -0
- package/src/runtime/executor.ts +22 -2
- 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/index.ts +2 -0
- package/src/server/serve.ts +226 -68
- package/src/server/types.ts +1 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +9 -1
- package/src/testing/run.ts +39 -14
- package/src/types.ts +32 -2
package/dist/server/serve.js
CHANGED
|
@@ -3,7 +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 { AuthError, isProviderError, isSessionExpiredError, isTransportError, ProviderError, } from "../errors.js";
|
|
6
|
+
import { AuthError, isProviderError, isSessionExpiredError, isTransportError, isValidationError, ProviderError, } from "../errors.js";
|
|
7
7
|
import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog.js";
|
|
8
8
|
import { categoryForStatus, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability.js";
|
|
9
9
|
import { createScratchpad } from "../runtime/auth-flow.js";
|
|
@@ -15,6 +15,7 @@ import { createEnvContext } from "../runtime/env.js";
|
|
|
15
15
|
import { executeOperation } from "../runtime/executor.js";
|
|
16
16
|
import { createHttpClient } from "../runtime/http.js";
|
|
17
17
|
import { wrapWithInstrumentation } from "../runtime/instrumentation.js";
|
|
18
|
+
import { createNativeNetworkClient } from "../runtime/native-network.js";
|
|
18
19
|
import { getProviderBaseUrl } from "../runtime/provider.js";
|
|
19
20
|
import { PROXY_AUTH_IP_DENIED_CODE, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_EXHAUSTED_CODE, } from "../runtime/proxy-errors.js";
|
|
20
21
|
import { PROVIDER_TELEMETRY_HEADER, ProxyTelemetryCollector } from "../runtime/proxy-telemetry.js";
|
|
@@ -33,6 +34,8 @@ import { resolveSelfTestMasterSecrets } from "./self-test-token.js";
|
|
|
33
34
|
import { AuthFlowRequestSchema, OperationConnectionSchema, OperationRequestSchema, } from "./types.js";
|
|
34
35
|
const DEFAULT_HOST = "0.0.0.0";
|
|
35
36
|
const DEFAULT_PORT = 3000;
|
|
37
|
+
/** Compact SDK-owned error classification emitted separately from the public response body. */
|
|
38
|
+
export const ERROR_OBSERVABILITY_HEADER = "X-ApiFuse-Error-Observability";
|
|
36
39
|
const AUTH_FLOW_LOCALES = ["en", "ko", "ja"];
|
|
37
40
|
const retryResponseMeta = new WeakMap();
|
|
38
41
|
const STATEFUL_INTERNAL_OPERATIONS_ROUTE = "/__apifuse/stateful/operations";
|
|
@@ -146,6 +149,15 @@ export function resolveProviderProxyAffinityKey(provider, request, operationId)
|
|
|
146
149
|
function resolveOperationConnectionId(request) {
|
|
147
150
|
return request.connection?.id ?? request.connectionId;
|
|
148
151
|
}
|
|
152
|
+
function resolveNativeProxyPolicy(provider) {
|
|
153
|
+
if (typeof provider.proxy === "object")
|
|
154
|
+
return provider.proxy;
|
|
155
|
+
if (provider.proxy === true)
|
|
156
|
+
return { mode: "optional" };
|
|
157
|
+
if (provider.proxy === false)
|
|
158
|
+
return { mode: "disabled" };
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
149
161
|
function createProviderContext(provider, request, operationId, options = {}, state = createUnsupportedProviderRuntimeState(), proxyTelemetry) {
|
|
150
162
|
const baseUrl = getProviderBaseUrl(provider);
|
|
151
163
|
const stealthBaseUrl = getProviderStealthBaseUrl(provider);
|
|
@@ -204,6 +216,17 @@ function createProviderContext(provider, request, operationId, options = {}, sta
|
|
|
204
216
|
engine: provider.browser?.engine,
|
|
205
217
|
})
|
|
206
218
|
: createBrowserStub(),
|
|
219
|
+
...(provider.native
|
|
220
|
+
? {
|
|
221
|
+
native: {
|
|
222
|
+
network: createNativeNetworkClient({
|
|
223
|
+
egress: provider.native.network,
|
|
224
|
+
proxyPolicy: resolveNativeProxyPolicy(provider),
|
|
225
|
+
affinityKey: proxyClientOptions.affinityKey,
|
|
226
|
+
}),
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
: {}),
|
|
207
230
|
trace: createTraceContext(),
|
|
208
231
|
auth: createAuthStub(),
|
|
209
232
|
stt: options.stt ?? createSttClientFromEnv(provider.stt),
|
|
@@ -280,6 +303,17 @@ function createAuthFlowContext(provider, request, options = {}, signal) {
|
|
|
280
303
|
? createStealthClient(stealthBaseUrl, stealthProfile.name, stealthClientOptions)
|
|
281
304
|
: createStealthClient(stealthBaseUrl, stealthClientOptions)
|
|
282
305
|
: createStealthStub(),
|
|
306
|
+
...(provider.native
|
|
307
|
+
? {
|
|
308
|
+
native: {
|
|
309
|
+
network: createNativeNetworkClient({
|
|
310
|
+
egress: provider.native.network,
|
|
311
|
+
proxyPolicy: resolveNativeProxyPolicy(provider),
|
|
312
|
+
affinityKey: proxyClientOptions.affinityKey,
|
|
313
|
+
}),
|
|
314
|
+
},
|
|
315
|
+
}
|
|
316
|
+
: {}),
|
|
283
317
|
env: createEnvContext(provider.secrets?.map((secret) => secret.name)),
|
|
284
318
|
credential,
|
|
285
319
|
context: flowContextStore.context,
|
|
@@ -320,25 +354,27 @@ function zodDetails(error) {
|
|
|
320
354
|
}));
|
|
321
355
|
}
|
|
322
356
|
function toErrorResponse(error, requestId) {
|
|
357
|
+
const observability = errorObservabilityDetails(error);
|
|
323
358
|
if (error instanceof StatefulRoutingDeadlineError) {
|
|
324
359
|
return {
|
|
325
360
|
error: {
|
|
326
361
|
code: "STATEFUL_FORWARDING_DEADLINE_EXPIRED",
|
|
327
362
|
message: "Stateful forwarding deadline expired.",
|
|
328
363
|
...(requestId ? { requestId } : {}),
|
|
329
|
-
|
|
364
|
+
retryable: observability.retryable,
|
|
330
365
|
},
|
|
331
366
|
};
|
|
332
367
|
}
|
|
333
368
|
if (isProviderError(error)) {
|
|
334
|
-
const details =
|
|
369
|
+
const details = error.details;
|
|
335
370
|
return {
|
|
336
371
|
error: {
|
|
337
372
|
code: error.code ?? "provider_error",
|
|
338
373
|
message: publicProviderErrorMessage(error),
|
|
339
374
|
...(requestId ? { requestId } : {}),
|
|
375
|
+
retryable: observability.retryable,
|
|
340
376
|
...(error.fix ? { fix: error.fix } : {}),
|
|
341
|
-
...(details ? { details } : {}),
|
|
377
|
+
...(details !== undefined ? { details } : {}),
|
|
342
378
|
},
|
|
343
379
|
};
|
|
344
380
|
}
|
|
@@ -348,6 +384,7 @@ function toErrorResponse(error, requestId) {
|
|
|
348
384
|
code: "invalid_request",
|
|
349
385
|
message: "Invalid request body",
|
|
350
386
|
...(requestId ? { requestId } : {}),
|
|
387
|
+
retryable: observability.retryable,
|
|
351
388
|
details: zodDetails(error),
|
|
352
389
|
},
|
|
353
390
|
};
|
|
@@ -363,6 +400,7 @@ function toErrorResponse(error, requestId) {
|
|
|
363
400
|
code: "internal_error",
|
|
364
401
|
message: "Internal error",
|
|
365
402
|
...(requestId ? { requestId } : {}),
|
|
403
|
+
retryable: observability.retryable,
|
|
366
404
|
details: {
|
|
367
405
|
retryable: false,
|
|
368
406
|
category: "internal_error",
|
|
@@ -371,26 +409,6 @@ function toErrorResponse(error, requestId) {
|
|
|
371
409
|
},
|
|
372
410
|
};
|
|
373
411
|
}
|
|
374
|
-
function publicProviderErrorDetails(error) {
|
|
375
|
-
const providerDetails = error.details;
|
|
376
|
-
const observabilityDetails = providerObservabilityDetails(error);
|
|
377
|
-
if (providerDetails === undefined) {
|
|
378
|
-
return observabilityDetails;
|
|
379
|
-
}
|
|
380
|
-
if (observabilityDetails === undefined) {
|
|
381
|
-
return providerDetails;
|
|
382
|
-
}
|
|
383
|
-
if (isPlainRecord(providerDetails) && isPlainRecord(observabilityDetails)) {
|
|
384
|
-
return { ...providerDetails, ...observabilityDetails };
|
|
385
|
-
}
|
|
386
|
-
return {
|
|
387
|
-
provider: providerDetails,
|
|
388
|
-
observability: observabilityDetails,
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
function isPlainRecord(value) {
|
|
392
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
393
|
-
}
|
|
394
412
|
// Accepts `unknown` so the branded guards narrow cleanly from the top: the
|
|
395
413
|
// subtype error classes are structurally compatible with ProviderError, so
|
|
396
414
|
// narrowing from a ProviderError-typed value would collapse the negative branch
|
|
@@ -449,6 +467,48 @@ function providerObservabilityDetails(error) {
|
|
|
449
467
|
...(error.upstreamStatus ? { upstreamStatus: error.upstreamStatus } : {}),
|
|
450
468
|
};
|
|
451
469
|
}
|
|
470
|
+
function errorObservabilityDetails(error) {
|
|
471
|
+
const providerDetails = providerObservabilityDetails(error);
|
|
472
|
+
if (providerDetails)
|
|
473
|
+
return providerDetails;
|
|
474
|
+
if (error instanceof z.ZodError || isValidationError(error)) {
|
|
475
|
+
return {
|
|
476
|
+
category: isProviderError(error) && error.options?.category
|
|
477
|
+
? error.options.category
|
|
478
|
+
: "input_validation",
|
|
479
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
480
|
+
retryable: isProviderError(error) ? (error.options?.retryable ?? false) : false,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
if (error instanceof StatefulRoutingDeadlineError) {
|
|
484
|
+
return {
|
|
485
|
+
category: "timeout",
|
|
486
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
487
|
+
retryable: false,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
if (isProviderError(error)) {
|
|
491
|
+
return {
|
|
492
|
+
category: error.options?.category ?? "provider_error",
|
|
493
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
494
|
+
retryable: error.options?.retryable ?? false,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
category: "internal_error",
|
|
499
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
500
|
+
retryable: false,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function responseWithErrorObservability(response, error) {
|
|
504
|
+
const headers = new Headers(response.headers);
|
|
505
|
+
headers.set(ERROR_OBSERVABILITY_HEADER, JSON.stringify(errorObservabilityDetails(error)));
|
|
506
|
+
return new Response(response.body, {
|
|
507
|
+
status: response.status,
|
|
508
|
+
statusText: response.statusText,
|
|
509
|
+
headers,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
452
512
|
function publicProviderErrorMessage(error) {
|
|
453
513
|
if (isTransportError(error)) {
|
|
454
514
|
if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
|
|
@@ -481,9 +541,6 @@ function toStatusCode(error) {
|
|
|
481
541
|
if (error instanceof StatefulRoutingDeadlineError) {
|
|
482
542
|
return 504;
|
|
483
543
|
}
|
|
484
|
-
if (isTransportError(error)) {
|
|
485
|
-
return error.code === "transport_timeout" ? 504 : 502;
|
|
486
|
-
}
|
|
487
544
|
if (isProviderError(error)) {
|
|
488
545
|
switch (error.code) {
|
|
489
546
|
case "AUTH_REQUIRED":
|
|
@@ -509,10 +566,92 @@ function toStatusCode(error) {
|
|
|
509
566
|
case "STATEFUL_FORWARDING_REPLAY_CACHE_FULL":
|
|
510
567
|
return 503;
|
|
511
568
|
}
|
|
512
|
-
|
|
569
|
+
if (isTransportError(error)) {
|
|
570
|
+
return error.code === "transport_timeout" ? 504 : 502;
|
|
571
|
+
}
|
|
572
|
+
if (isValidationError(error)) {
|
|
573
|
+
return error.options?.category === "output_validation" ? 500 : 400;
|
|
574
|
+
}
|
|
575
|
+
return 500;
|
|
513
576
|
}
|
|
514
577
|
return 500;
|
|
515
578
|
}
|
|
579
|
+
// Codes emitted by SDK-owned paths must never be attributed to provider
|
|
580
|
+
// authors by the unregistered-code signal, even when their intentional status
|
|
581
|
+
// is 500. Provider-authored codes not in this registry retain the signal.
|
|
582
|
+
const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
|
|
583
|
+
"AUTH_PROMPT_UNAVAILABLE",
|
|
584
|
+
"BROWSER_CDP_POOL_REQUIRED",
|
|
585
|
+
"BROWSER_RUNTIME_UNSUPPORTED",
|
|
586
|
+
"STEALTH_RUNTIME_UNSUPPORTED",
|
|
587
|
+
"SSE_EVENT_UNDECLARED",
|
|
588
|
+
"STREAM_EVENT_TOO_LARGE",
|
|
589
|
+
"STREAM_CHUNK_TOO_LARGE",
|
|
590
|
+
"SSE_RESULT_UNSUPPORTED",
|
|
591
|
+
"STREAM_RESULT_UNSUPPORTED",
|
|
592
|
+
"AUTH_FLOW_NOT_CONFIGURED",
|
|
593
|
+
"refresh_not_supported",
|
|
594
|
+
"RUNTIME_UNSUPPORTED",
|
|
595
|
+
"PROVIDER_STATE_UNSUPPORTED",
|
|
596
|
+
"CHOICE_TOKEN_MASTER_SECRET_NOT_CONFIGURED",
|
|
597
|
+
"CHOICE_STATE_PAYLOAD_TOO_LARGE",
|
|
598
|
+
"CHOICE_STATE_UNAVAILABLE",
|
|
599
|
+
"CHOICE_CONTEXT_REQUIRED",
|
|
600
|
+
"unsupported_stealth_cookie_store_version",
|
|
601
|
+
"provider_secret_error",
|
|
602
|
+
"credential_key_error",
|
|
603
|
+
"credential_mode_error",
|
|
604
|
+
"flow_expired",
|
|
605
|
+
"turn_validation_error",
|
|
606
|
+
"context_access_error",
|
|
607
|
+
"UNSUPPORTED_STT_OPTION",
|
|
608
|
+
"INVALID_STT_AUDIO",
|
|
609
|
+
"STT_AUDIO_TOO_LARGE",
|
|
610
|
+
"STT_UPSTREAM_FAILED",
|
|
611
|
+
"INVALID_STT_VERIFICATION_CODE_OPTIONS",
|
|
612
|
+
"NO_CODE_FOUND",
|
|
613
|
+
"AMBIGUOUS_CODE",
|
|
614
|
+
"retry_invalid_policy",
|
|
615
|
+
"retry_unsafe_method",
|
|
616
|
+
"stealth_cookie_store_serialize_failed",
|
|
617
|
+
"response_too_large",
|
|
618
|
+
"transport_stream_unavailable",
|
|
619
|
+
"transport_invalid_method",
|
|
620
|
+
"http_transport_override_unsupported",
|
|
621
|
+
"http_redirect_policy_invalid",
|
|
622
|
+
"http_redirect_stopped",
|
|
623
|
+
"http_redirect_max_hops",
|
|
624
|
+
"http_redirect_missing_location",
|
|
625
|
+
"http_redirect_loop",
|
|
626
|
+
"transport_invalid_url",
|
|
627
|
+
"retry_exhausted",
|
|
628
|
+
"auth_abort_unsafe_data",
|
|
629
|
+
"credentials_auth_missing_credential_keys",
|
|
630
|
+
"credentials_auth_missing_credential",
|
|
631
|
+
"credentials_auth_invalid_login_result",
|
|
632
|
+
"credentials_auth_unknown_challenge",
|
|
633
|
+
"credentials_auth_unknown_pending_challenge",
|
|
634
|
+
"STATEFUL_FORWARDING_NOT_CONFIGURED",
|
|
635
|
+
"STATEFUL_FORWARDING_SIGNATURE_MISSING",
|
|
636
|
+
"STATEFUL_FORWARDING_NONCE_INVALID",
|
|
637
|
+
"STATEFUL_FORWARDING_TIMESTAMP_INVALID",
|
|
638
|
+
"STATEFUL_FORWARDING_SIGNATURE_INVALID",
|
|
639
|
+
"STATEFUL_FORWARDING_REPLAY_DETECTED",
|
|
640
|
+
"STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
|
|
641
|
+
"STATEFUL_FORWARDING_ENVELOPE_INVALID",
|
|
642
|
+
"STATEFUL_FORWARDING_PROVIDER_MISMATCH",
|
|
643
|
+
"STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
|
|
644
|
+
"STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
|
|
645
|
+
"STATEFUL_FORWARDING_REQUEST_FAILED",
|
|
646
|
+
"STATEFUL_FORWARDING_CONTEXT_MISSING",
|
|
647
|
+
"STATEFUL_FORWARDING_BAD_RESPONSE",
|
|
648
|
+
"STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
|
|
649
|
+
"STATEFUL_FILE_FORWARDING_UNSUPPORTED",
|
|
650
|
+
"STATEFUL_CONTROL_PLANE_OPERATION_AMBIGUOUS",
|
|
651
|
+
"STATEFUL_CONTROL_PLANE_REQUEST_FAILED",
|
|
652
|
+
"STATEFUL_CONTROL_PLANE_HTTP_ERROR",
|
|
653
|
+
"STATEFUL_CONTROL_PLANE_INVALID_RESPONSE",
|
|
654
|
+
]);
|
|
516
655
|
function extractRequestId(raw) {
|
|
517
656
|
if (!raw || typeof raw !== "object") {
|
|
518
657
|
return undefined;
|
|
@@ -530,7 +669,12 @@ function logProviderError(logger, provider, kind, route, requestId, error, statu
|
|
|
530
669
|
: "internal_error";
|
|
531
670
|
const errorClass = error instanceof Error ? error.name : typeof error;
|
|
532
671
|
const message = error instanceof Error ? error.message : String(error);
|
|
533
|
-
const details =
|
|
672
|
+
const details = errorObservabilityDetails(error);
|
|
673
|
+
const isUnregisteredProviderErrorCode = status === 500 &&
|
|
674
|
+
isProviderError(error) &&
|
|
675
|
+
!isValidationError(error) &&
|
|
676
|
+
typeof error.code === "string" &&
|
|
677
|
+
!SDK_OWNED_PROVIDER_ERROR_CODES.has(error.code);
|
|
534
678
|
const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
535
679
|
emit({
|
|
536
680
|
level: status >= 500 ? "error" : "warn",
|
|
@@ -544,15 +688,12 @@ function logProviderError(logger, provider, kind, route, requestId, error, statu
|
|
|
544
688
|
code,
|
|
545
689
|
errorClass,
|
|
546
690
|
message,
|
|
547
|
-
...(
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
taxonomyVersion: details.taxonomyVersion,
|
|
554
|
-
retryable: details.retryable,
|
|
555
|
-
}
|
|
691
|
+
...(details.upstreamStatus ? { upstreamStatus: details.upstreamStatus } : {}),
|
|
692
|
+
errorCategory: details.category,
|
|
693
|
+
taxonomyVersion: details.taxonomyVersion,
|
|
694
|
+
retryable: details.retryable,
|
|
695
|
+
...(isUnregisteredProviderErrorCode
|
|
696
|
+
? { signal: "unregistered_provider_error_code" }
|
|
556
697
|
: {}),
|
|
557
698
|
...(error instanceof z.ZodError ? { issues: zodDetails(error) } : {}),
|
|
558
699
|
});
|
|
@@ -1150,12 +1291,10 @@ export function createServerApp(provider, options = {}) {
|
|
|
1150
1291
|
missingSecrets: missingSecretsAtBoot,
|
|
1151
1292
|
});
|
|
1152
1293
|
}
|
|
1153
|
-
app.notFound((c) =>
|
|
1154
|
-
error:
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
},
|
|
1158
|
-
}, 404));
|
|
1294
|
+
app.notFound((c) => {
|
|
1295
|
+
const error = new ProviderError("Not found", { code: "not_found", retryable: false });
|
|
1296
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error), 404), error);
|
|
1297
|
+
});
|
|
1159
1298
|
app.get("/health", (c) => c.json({
|
|
1160
1299
|
status: "ok",
|
|
1161
1300
|
provider: provider.id,
|
|
@@ -1272,7 +1411,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1272
1411
|
}
|
|
1273
1412
|
const requestId = extractRequestId(rawBody);
|
|
1274
1413
|
logProviderError(logger, provider, "operation", operation, requestId, error, status, finishRequestCost(requestCost));
|
|
1275
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1414
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1276
1415
|
}
|
|
1277
1416
|
});
|
|
1278
1417
|
app.post("/v1/:operation", async (c) => {
|
|
@@ -1306,7 +1445,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1306
1445
|
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
1307
1446
|
if (telemetryHeader)
|
|
1308
1447
|
c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
1309
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1448
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1310
1449
|
}
|
|
1311
1450
|
});
|
|
1312
1451
|
app.post("/auth/start", async (c) => {
|
|
@@ -1326,7 +1465,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1326
1465
|
const status = toStatusCode(error);
|
|
1327
1466
|
const requestId = extractRequestId(rawBody);
|
|
1328
1467
|
logProviderError(logger, provider, "auth", "start", requestId, error, status, finishRequestCost(requestCost));
|
|
1329
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1468
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1330
1469
|
}
|
|
1331
1470
|
});
|
|
1332
1471
|
app.post("/auth/continue", async (c) => {
|
|
@@ -1346,7 +1485,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1346
1485
|
const status = toStatusCode(error);
|
|
1347
1486
|
const requestId = extractRequestId(rawBody);
|
|
1348
1487
|
logProviderError(logger, provider, "auth", "continue", requestId, error, status, finishRequestCost(requestCost));
|
|
1349
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1488
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1350
1489
|
}
|
|
1351
1490
|
});
|
|
1352
1491
|
app.post("/auth/poll", async (c) => {
|
|
@@ -1366,7 +1505,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1366
1505
|
const status = toStatusCode(error);
|
|
1367
1506
|
const requestId = extractRequestId(rawBody);
|
|
1368
1507
|
logProviderError(logger, provider, "auth", "poll", requestId, error, status, finishRequestCost(requestCost));
|
|
1369
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1508
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1370
1509
|
}
|
|
1371
1510
|
});
|
|
1372
1511
|
app.post("/auth/refresh", async (c) => {
|
|
@@ -1386,7 +1525,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1386
1525
|
const status = toStatusCode(error);
|
|
1387
1526
|
const requestId = extractRequestId(rawBody);
|
|
1388
1527
|
logProviderError(logger, provider, "auth", "refresh", requestId, error, status, finishRequestCost(requestCost));
|
|
1389
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1528
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1390
1529
|
}
|
|
1391
1530
|
});
|
|
1392
1531
|
app.post("/auth/disconnect", async (c) => {
|
|
@@ -1406,7 +1545,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1406
1545
|
const status = toStatusCode(error);
|
|
1407
1546
|
const requestId = extractRequestId(rawBody);
|
|
1408
1547
|
logProviderError(logger, provider, "auth", "disconnect", requestId, error, status, finishRequestCost(requestCost));
|
|
1409
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1548
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1410
1549
|
}
|
|
1411
1550
|
});
|
|
1412
1551
|
return app;
|
package/dist/server/types.d.ts
CHANGED
|
@@ -42,6 +42,7 @@ export declare const ErrorEnvelopeSchema: z.ZodObject<{
|
|
|
42
42
|
code: z.ZodString;
|
|
43
43
|
message: z.ZodString;
|
|
44
44
|
requestId: z.ZodOptional<z.ZodString>;
|
|
45
|
+
retryable: z.ZodBoolean;
|
|
45
46
|
fix: z.ZodOptional<z.ZodString>;
|
|
46
47
|
details: z.ZodOptional<z.ZodUnknown>;
|
|
47
48
|
}, z.core.$strip>;
|
|
@@ -84,6 +85,7 @@ export declare const OperationErrorResponseSchema: z.ZodObject<{
|
|
|
84
85
|
code: z.ZodString;
|
|
85
86
|
message: z.ZodString;
|
|
86
87
|
requestId: z.ZodOptional<z.ZodString>;
|
|
88
|
+
retryable: z.ZodBoolean;
|
|
87
89
|
fix: z.ZodOptional<z.ZodString>;
|
|
88
90
|
details: z.ZodOptional<z.ZodUnknown>;
|
|
89
91
|
}, z.core.$strip>;
|
|
@@ -121,6 +123,7 @@ export declare const AuthFlowErrorResponseSchema: z.ZodObject<{
|
|
|
121
123
|
code: z.ZodString;
|
|
122
124
|
message: z.ZodString;
|
|
123
125
|
requestId: z.ZodOptional<z.ZodString>;
|
|
126
|
+
retryable: z.ZodBoolean;
|
|
124
127
|
fix: z.ZodOptional<z.ZodString>;
|
|
125
128
|
details: z.ZodOptional<z.ZodUnknown>;
|
|
126
129
|
}, z.core.$strip>;
|
package/dist/server/types.js
CHANGED
|
@@ -9,6 +9,14 @@ export const STATEFUL_FORWARDING_NONCE_HEADER = "x-apifuse-stateful-nonce";
|
|
|
9
9
|
export const STATEFUL_FORWARDING_SOURCE_POD_HEADER = "x-apifuse-stateful-source-pod";
|
|
10
10
|
const MAX_FORWARDED_HEADERS = 32;
|
|
11
11
|
const MAX_FORWARDED_HEADER_BYTES = 8 * 1024;
|
|
12
|
+
// Inbound compatibility boundary for rolling deploys: older owner pods omit
|
|
13
|
+
// top-level retryable. Emitted responses remain strict via
|
|
14
|
+
// OperationErrorResponseSchema.
|
|
15
|
+
const ForwardedOperationErrorResponseSchema = OperationErrorResponseSchema.extend({
|
|
16
|
+
error: OperationErrorResponseSchema.shape.error.extend({
|
|
17
|
+
retryable: z.boolean().optional().default(false),
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
12
20
|
const SENSITIVE_HEADER_NAMES = new Set([
|
|
13
21
|
"authorization",
|
|
14
22
|
"cookie",
|
|
@@ -183,7 +191,7 @@ async function parseForwardedResponse(response) {
|
|
|
183
191
|
if (response.ok && success.success) {
|
|
184
192
|
return { output: success.data.data };
|
|
185
193
|
}
|
|
186
|
-
const error =
|
|
194
|
+
const error = ForwardedOperationErrorResponseSchema.safeParse(body);
|
|
187
195
|
if (error.success) {
|
|
188
196
|
throw new StatefulOwnerForwardingError({
|
|
189
197
|
code: error.data.error.code,
|
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
|
@@ -859,9 +859,24 @@ export interface RequestOptions {
|
|
|
859
859
|
*/
|
|
860
860
|
throwOnHttpError?: boolean;
|
|
861
861
|
retry?: boolean | HttpRetryPreset | HttpRetryOptions;
|
|
862
|
+
/**
|
|
863
|
+
* Opt-in redirect-hop enforcement for ctx.http. When present, redirects are
|
|
864
|
+
* evaluated before the next request is issued. Existing callers that omit
|
|
865
|
+
* this policy retain the native fetch redirect behavior.
|
|
866
|
+
*/
|
|
867
|
+
redirectPolicy?: HttpRedirectPolicy;
|
|
862
868
|
}
|
|
869
|
+
export type RedirectRunReason = "completed" | "stopped" | "max_hops" | "missing_location" | "loop";
|
|
870
|
+
export type HttpRedirectPolicyMode = "same-origin";
|
|
871
|
+
export interface HttpRedirectPolicy {
|
|
872
|
+
/** Only follow redirects whose canonical scheme, host, and port match the initial URL. */
|
|
873
|
+
mode: HttpRedirectPolicyMode;
|
|
874
|
+
/** Maximum number of redirect hops that may be followed. Must be an integer from 0 to 20. */
|
|
875
|
+
maxHops: number;
|
|
876
|
+
}
|
|
877
|
+
export type HttpRedirectFailureReason = Exclude<RedirectRunReason, "completed">;
|
|
863
878
|
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 {
|
|
879
|
+
export interface StealthFetchOptions extends Omit<RequestOptions, "redirectPolicy"> {
|
|
865
880
|
method?: HttpMethod;
|
|
866
881
|
body?: string | Buffer;
|
|
867
882
|
redirect?: "follow" | "manual" | "error";
|
|
@@ -967,7 +982,7 @@ export interface StealthRedirectRunOptions extends Omit<StealthFetchOptions, "re
|
|
|
967
982
|
export interface StealthRedirectRunResult {
|
|
968
983
|
final: StealthResponse;
|
|
969
984
|
hops: StealthRedirectHop[];
|
|
970
|
-
reason:
|
|
985
|
+
reason: RedirectRunReason;
|
|
971
986
|
/**
|
|
972
987
|
* Complete flat view across all redirect hosts. Attributes and duplicate names are lost.
|
|
973
988
|
* @deprecated Use cookieStore for lossless persistence.
|
|
@@ -1072,6 +1087,12 @@ export interface NativeTcpEgressRule {
|
|
|
1072
1087
|
/**
|
|
1073
1088
|
* Bounded native TCP egress discovered through a declared bootstrap endpoint.
|
|
1074
1089
|
* Host suffixes are exact DNS suffixes, not wildcard patterns.
|
|
1090
|
+
*
|
|
1091
|
+
* Dynamic rules are ordered. The first rule whose source, target, port, and TLS
|
|
1092
|
+
* selectors match exclusively owns the grant; its ttlMs and maxGrants bounds
|
|
1093
|
+
* apply, and an exhausted/shorter rule never falls through to a later overlap.
|
|
1094
|
+
* Every rule must declare a source host selector, source port list/range, and
|
|
1095
|
+
* target port list/range; omitted ttlMs/maxGrants remain unbounded.
|
|
1075
1096
|
*/
|
|
1076
1097
|
export interface NativeTcpDynamicEgressRule {
|
|
1077
1098
|
readonly sourceHost?: string;
|
package/package.json
CHANGED
package/src/define.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import ms from "ms";
|
|
2
2
|
|
|
3
3
|
import { ProviderError, ValidationError } from "./errors.js";
|
|
4
|
+
import {
|
|
5
|
+
NativeEgressPolicyValidationError,
|
|
6
|
+
validateNativeProviderConfig,
|
|
7
|
+
} from "./native-egress-policy.js";
|
|
4
8
|
import { safeParseSchemaSync } from "./schema.js";
|
|
5
9
|
import { resolveHealthCheckInputDateTokens } from "./server/self-test-input-tokens.js";
|
|
6
10
|
import type {
|
|
@@ -2204,6 +2208,13 @@ export function defineProvider<
|
|
|
2204
2208
|
);
|
|
2205
2209
|
validateOperationFixtures(config.id, operations);
|
|
2206
2210
|
validateProviderDeployment(config.id, config.deployment);
|
|
2211
|
+
try {
|
|
2212
|
+
validateNativeProviderConfig(config.native);
|
|
2213
|
+
} catch (error) {
|
|
2214
|
+
if (error instanceof NativeEgressPolicyValidationError)
|
|
2215
|
+
throw new ValidationError(error.message);
|
|
2216
|
+
throw error;
|
|
2217
|
+
}
|
|
2207
2218
|
validateProviderProxy(config);
|
|
2208
2219
|
validateProviderStt(config);
|
|
2209
2220
|
if (config.runtime === "browser" && !config.browser)
|