@apifuse/provider-sdk 2.2.0-beta.13 → 2.2.0-beta.14
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 +6 -0
- package/dist/errors.d.ts +1 -0
- package/dist/errors.js +6 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime/executor.js +17 -2
- 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 +153 -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/package.json +1 -1
- package/src/errors.ts +9 -0
- package/src/index.ts +6 -1
- package/src/runtime/executor.ts +22 -2
- package/src/server/index.ts +2 -0
- package/src/server/serve.ts +190 -68
- package/src/server/types.ts +1 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +9 -1
package/AUTHORING.md
CHANGED
|
@@ -393,6 +393,70 @@ External contributors are expected to submit standalone Provider source plus:
|
|
|
393
393
|
Maintainers own monorepo import under `providers/<id>/`, registry generation,
|
|
394
394
|
deployment projection checks, and release workflows.
|
|
395
395
|
|
|
396
|
+
### Error responses
|
|
397
|
+
|
|
398
|
+
Provider-server failures use a stable public envelope:
|
|
399
|
+
|
|
400
|
+
```json
|
|
401
|
+
{
|
|
402
|
+
"error": {
|
|
403
|
+
"code": "UPSTREAM_ERROR",
|
|
404
|
+
"message": "The upstream service failed",
|
|
405
|
+
"requestId": "req_123",
|
|
406
|
+
"retryable": true,
|
|
407
|
+
"details": { "providerReason": "temporarily_unavailable" }
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
`retryable` is always present on responses emitted by the current SDK. Set
|
|
413
|
+
`retryable` in the `ProviderError` options when the provider knows the answer;
|
|
414
|
+
an explicit `true` or `false` wins over SDK derivation. When it is omitted, the
|
|
415
|
+
SDK derives the value for its known error classes and otherwise defaults to
|
|
416
|
+
`false`. During stateful rolling upgrades, the forwarding client also accepts
|
|
417
|
+
an older owner response that omits `retryable` and treats it as `false` without
|
|
418
|
+
loosening the emitted response contract. Existing optional `fix` guidance is
|
|
419
|
+
also preserved when a `ProviderError` supplies it.
|
|
420
|
+
|
|
421
|
+
`details` belongs exclusively to the provider. The server passes
|
|
422
|
+
`ProviderError.options.details` through verbatim, including strings and arrays,
|
|
423
|
+
and never merges, overwrites, or wraps it. Do not put SDK taxonomy fields there.
|
|
424
|
+
SDK-owned validation and masked-internal-error paths retain their own diagnostic
|
|
425
|
+
details.
|
|
426
|
+
|
|
427
|
+
SDK observability is emitted separately in the
|
|
428
|
+
`X-ApiFuse-Error-Observability` response header as compact, single-line JSON:
|
|
429
|
+
|
|
430
|
+
```json
|
|
431
|
+
{"category":"upstream_http","taxonomyVersion":"2026-05-26","retryable":true,"upstreamStatus":502}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Treat this header as telemetry, not as provider-controlled public error detail.
|
|
435
|
+
Its category, taxonomy version, retryability, and optional upstream status match
|
|
436
|
+
the structured `provider_request_failed` log event.
|
|
437
|
+
|
|
438
|
+
Registered error-code mappings take precedence for every `ProviderError`,
|
|
439
|
+
including `ValidationError`:
|
|
440
|
+
|
|
441
|
+
| Error code or fallback | HTTP status |
|
|
442
|
+
| --- | ---: |
|
|
443
|
+
| `AUTH_REQUIRED`, `reauth_required` | 401 |
|
|
444
|
+
| `MISSING_SECRET` | 400 |
|
|
445
|
+
| `NOT_FOUND`, `not_found`, `NO_DATA` | 404 |
|
|
446
|
+
| `RATE_LIMITED`, `UPSTREAM_RATE_LIMIT`, `LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR` | 429 |
|
|
447
|
+
| `UPSTREAM_ERROR`, `BLOCKED` | 502 |
|
|
448
|
+
| `STT_UNAVAILABLE`, `UNSUPPORTED_STT_BACKEND`, `STATEFUL_FORWARDING_REPLAY_CACHE_FULL` | 503 |
|
|
449
|
+
| Unregistered input `ValidationError` code | 400 |
|
|
450
|
+
| Other unregistered `ProviderError` code | 500 |
|
|
451
|
+
|
|
452
|
+
An unregistered non-validation `ProviderError` code returns HTTP 500 and emits
|
|
453
|
+
the greppable `unregistered_provider_error_code` signal with the code in the
|
|
454
|
+
structured failure log. Before publishing a new code, register its status in
|
|
455
|
+
the SDK mapping and add it to the mapping tests; declaring it only in operation
|
|
456
|
+
documentation does not change runtime status selection. The HTTP 400
|
|
457
|
+
`ValidationError` behavior is only the fallback for unregistered input
|
|
458
|
+
validation codes; a registered code such as `NOT_FOUND` retains its mapped 404.
|
|
459
|
+
|
|
396
460
|
### Declared secrets are SDK-enforced
|
|
397
461
|
|
|
398
462
|
Environment/secret presence validation is single-sourced in the SDK. Declare
|
|
@@ -411,12 +475,12 @@ secrets: [
|
|
|
411
475
|
The runtime validates every `required: true` declaration before any operation
|
|
412
476
|
handler or auth-flow handler (except `abort`) runs. When a required secret is
|
|
413
477
|
unset or whitespace-only, the invocation fails with the canonical structured
|
|
414
|
-
error — code `MISSING_SECRET`, HTTP 400, `
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
`
|
|
418
|
-
at boot so unprovisioned deployments are
|
|
419
|
-
the pod.
|
|
478
|
+
error — code `MISSING_SECRET`, HTTP 400, top-level `retryable: false`, and a
|
|
479
|
+
`fix` naming every missing secret — across `/v1/{operation}`, self-test probes,
|
|
480
|
+
`apifuse perf`, and `apifuse record`. Its error-observability header carries the
|
|
481
|
+
`credential_unavailable` category. The server also emits a
|
|
482
|
+
`provider_secrets_missing` warn log at boot so unprovisioned deployments are
|
|
483
|
+
visible immediately without crashing the pod.
|
|
420
484
|
|
|
421
485
|
Provider-local presence re-validation is **deprecated**: do not write
|
|
422
486
|
`requireServiceKey`/`requireApiKey`-style guards that re-check `ctx.env.get()`
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# @apifuse/provider-sdk Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.0-beta.14
|
|
4
|
+
|
|
5
|
+
- Release candidate for main commit 3491acd253ca17b517985e8a618f1c2904a664a9.
|
|
6
|
+
|
|
3
7
|
## 2.2.0-beta.13
|
|
4
8
|
|
|
5
9
|
- Release candidate for main commit 75e840d0614aea3b99a1e5cef4f93f8cdccf0507.
|
|
@@ -82,6 +86,8 @@
|
|
|
82
86
|
|
|
83
87
|
## Unreleased
|
|
84
88
|
|
|
89
|
+
- **Breaking:** Provider error `details` is now passed through verbatim; SDK observability fields (`category`, `taxonomyVersion`, `upstreamStatus`, and derived `retryable`) are no longer merged into the public body. Emitted error envelopes now require top-level `retryable`, while inbound stateful forwarding tolerates an older owner response that omits it and defaults it to `false`. The removed observability metadata is available in the new `X-ApiFuse-Error-Observability` response header.
|
|
90
|
+
- Unregistered `ProviderError` codes now default to HTTP 500 instead of 400 and emit an `unregistered_provider_error_code` structured-log signal; registered mappings remain unchanged and take precedence over the HTTP 400 fallback for unregistered input `ValidationError` codes.
|
|
85
91
|
- Add an opt-in native connection idle read timeout with a typed error, independently from TCP/SOCKS/TLS establishment deadlines.
|
|
86
92
|
- Add opt-in `maxBodyBytes` enforcement to stealth fetches and redirect hops, aborting oversized decoded response streams with `response_too_large`.
|
|
87
93
|
- Resolve relative date tokens in fixture requests before input-schema validation, add KST capture-date `fixtures.recordedAt` metadata, and support explicit KST/UTC calendars in the shared health-input resolver.
|
package/dist/errors.d.ts
CHANGED
|
@@ -47,6 +47,7 @@ export declare class TransportError extends ProviderError {
|
|
|
47
47
|
export declare function isProviderError(value: unknown): value is ProviderError;
|
|
48
48
|
export declare function isSessionExpiredError(value: unknown): value is SessionExpiredError;
|
|
49
49
|
export declare function isTransportError(value: unknown): value is TransportError;
|
|
50
|
+
export declare function isValidationError(value: unknown): value is ValidationError;
|
|
50
51
|
export declare class ProviderSecretError extends ProviderError {
|
|
51
52
|
constructor(message: string, options?: ProviderErrorOptions);
|
|
52
53
|
}
|
package/dist/errors.js
CHANGED
|
@@ -8,6 +8,7 @@ const PROVIDER_ERROR_BRAND = Symbol.for("@apifuse/provider-sdk/error-brand@1");
|
|
|
8
8
|
const PROVIDER_ERROR_BRAND_VALUE = 1;
|
|
9
9
|
const SESSION_EXPIRED_BRAND = Symbol.for("@apifuse/provider-sdk/error-kind/session-expired@1");
|
|
10
10
|
const TRANSPORT_BRAND = Symbol.for("@apifuse/provider-sdk/error-kind/transport@1");
|
|
11
|
+
const VALIDATION_BRAND = Symbol.for("@apifuse/provider-sdk/error-kind/validation@1");
|
|
11
12
|
// Defines a non-enumerable, non-writable, non-configurable own data property.
|
|
12
13
|
// Immutable + own means a guard can trust it via a single descriptor read
|
|
13
14
|
// without invoking attacker-controlled getters or accepting inherited brands.
|
|
@@ -96,6 +97,7 @@ export class ValidationError extends ProviderError {
|
|
|
96
97
|
super(message, options);
|
|
97
98
|
this.name = "ValidationError";
|
|
98
99
|
this.zodError = options?.zodError;
|
|
100
|
+
defineErrorBrand(this, VALIDATION_BRAND, true);
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
103
|
export class TransportError extends ProviderError {
|
|
@@ -122,6 +124,10 @@ export function isSessionExpiredError(value) {
|
|
|
122
124
|
export function isTransportError(value) {
|
|
123
125
|
return isProviderError(value) && hasOwnBrand(value, TRANSPORT_BRAND, true);
|
|
124
126
|
}
|
|
127
|
+
export function isValidationError(value) {
|
|
128
|
+
return (isProviderError(value) &&
|
|
129
|
+
(hasOwnBrand(value, VALIDATION_BRAND, true) || value.name === "ValidationError"));
|
|
130
|
+
}
|
|
125
131
|
export class ProviderSecretError extends ProviderError {
|
|
126
132
|
constructor(message, options) {
|
|
127
133
|
super(message, { code: "provider_secret_error", ...options });
|
package/dist/index.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export { createStealthClient } from "./runtime/stealth.js";
|
|
|
34
34
|
export { APIFUSE__STT__BACKEND_ENV, APIFUSE__STT__CLOUDFLARE_API_TOKEN_ENV, APIFUSE__STT__MODEL_ENV, createSttClientFromEnv, createUnsupportedSttClient, extractVerificationCode, resolveSttPrompt, } from "./runtime/stt.js";
|
|
35
35
|
export { type CreateTraceContextOptions, createTraceContext, type Span, type TraceContext, } from "./runtime/trace.js";
|
|
36
36
|
export { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SENSITIVE_KIND_META_KEY, APIFUSE_SENSITIVE_META_KEY, collectSensitivePaths, describeKey, field, fields, isSensitiveSchema, redactPayload, type SensitiveFieldKind, type SensitiveFieldOptions, type SensitivePath, sensitive, z, } from "./schema.js";
|
|
37
|
-
export { createServerApp, type ServeOptions, serve } from "./server/index.js";
|
|
37
|
+
export { createServerApp, ERROR_OBSERVABILITY_HEADER, type ServeOptions, serve, } from "./server/index.js";
|
|
38
38
|
export { getStealthProfile, listStealthProfiles } from "./stealth/profiles.js";
|
|
39
39
|
export * from "./stream.js";
|
|
40
40
|
export type { ApiFuseResponse, AuthConfig, AuthContext, AuthFlowDefinition, AuthFlowInputHandler, AuthFlowStartHandler, AuthMode, AuthTurn, Bcp47Locale, BrowserEngine, BrowserOptions, BrowserResourceBody, BrowserResourceDecision, BrowserResourceMethod, BrowserResourcePolicy, BrowserResourceRequest, BrowserResourceRoute, ConnectionMode, ContextDeclaration, CookieJar, ContextScratchpad, CredentialContext, CredentialDeclaration, E164PhoneNumber, EnvContext, FlowContext, FlowContextStore, HealthCheckAssertionContext, HealthCheckCase, HealthCheckCaseResult, HealthCheckSuite, HealthCheckUnsupported, HealthJourneyDefinition, HealthJourneyEventContext, HealthJourneyGatewayContext, HealthJourneyJournalContext, HealthJourneyManualTriggerPolicy, HealthJourneyRunContext, HealthJourneyRunResult, HealthJourneySchedule, HealthScheduleRandomization, HealthJourneySmsContext, HealthJourneyStep, HttpClient, HttpMethod, HttpResponse, HttpRetryOptions, HttpRetrySummary, HttpStreamResponse, IanaTimeZone, InferSchemaOutput, Iso3166Alpha2CountryCode, Iso4217CurrencyCode, Iso8601Duration, NativeContext, NativeNetworkClient, NativeNetworkCloseReason, NativeNetworkConnection, NativeNetworkConnectInput, NativeNetworkConnectOptions, NativeNetworkDynamicGrantOptions, NativeNetworkEgressGrant, NativeProviderConfig, NativeProviderContext, NativeProxyDrainHandler, NativeProxyEgressInfo, NativeProxyExpiringEvent, NativeProxyExpiringReason, NativeTcpDynamicEgressRule, NativeTcpEgressGrant, NativeTcpEgressRule, NativeTcpPortRange, NativeTcpTlsMode, NativeTlsConnectOptions, OperationAnnotations, OperationApprovalPolicy, OperationContractMetadata, OperationDefinition, OperationDeprecationMetadata, OperationDocMeta, OperationErrorCode, OperationHandlerResult, OperationInputExample, OperationLifecycle, OperationObservabilityConfig, OperationObservabilitySensitiveConfig, OperationRelationships, OperationRiskClass, OperationSensitivePath, OperationToolRouterMetadata, OperationTransport, OperationTransportKind, ProbeInterval, ProviderAccessConfig, ProviderAccessVisibility, ProviderCache, ProviderCacheGetOrSetOptions, ProviderCacheKeyOptions, ProviderCacheLookupMeta, ProviderCacheResponseMeta, ProviderCacheResult, ProviderChoiceBindingOptions, ProviderChoiceContext, ProviderChoiceIssueOptions, ProviderChoiceParseOptions, ProviderContext, ProviderDefinition, ProviderDeploymentOverrides, ProviderFileRef, ProviderFilesContext, ProviderHealthMonitorConfig, ProviderHealthProbeConfig, ProviderLocale, ProviderLocaleKey, ProviderLocaleKeyInput, ProviderLogoProfile, ProviderLogoSource, ProviderMeta, ProviderProxyConfig, ProviderProxyMode, ProviderProxyPolicy, ProviderProxyProvider, ProviderProxySessionAffinity, ProviderPublicConnectionMode, ProviderPublicProfile, ProviderReviewed, ProviderResolvedFile, ProviderRuntimeState, ProviderSecretDeclaration, ProviderStateDurationString, ProviderStateNamespace, ProviderStreamEvent, ProviderSttConfig, ProviderSttMode, ProviderSupportLevel, RequestOptions, Rfc3339Instant, SchemaLike, SmsOrigin, SmsOtpExtractionPattern, SmsOtpMatcherDefinition, SseMessage, StandardSchemaV1, StateCasResult, StateNamespaceOptions, StateValue, StateWriteOptions, StealthClient, StealthCookieStore, StealthCookieStoreV1, StealthFetchOptions, StealthPlatform, StealthProfile, StealthRedirectHop, StealthRedirectRunOptions, StealthRedirectRunResult, StealthResponse, StealthSession, StealthSessionCookies, SttAudioInput, SttContext, SttPromptPolicy, SttSegment, SttTranscribeMode, SttTranscribeRequest, SttTranscript, SttUnsupportedOptionPolicy, SttUsage, SttVerificationCodeOptions, SttWarning, TraceConfig, TraceSpan, VerificationCodeCandidate, VerificationCodeCandidateSource, VerificationCodeExtractionResult, } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ export { createStealthClient } from "./runtime/stealth.js";
|
|
|
31
31
|
export { APIFUSE__STT__BACKEND_ENV, APIFUSE__STT__CLOUDFLARE_API_TOKEN_ENV, APIFUSE__STT__MODEL_ENV, createSttClientFromEnv, createUnsupportedSttClient, extractVerificationCode, resolveSttPrompt, } from "./runtime/stt.js";
|
|
32
32
|
export { createTraceContext, } from "./runtime/trace.js";
|
|
33
33
|
export { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SENSITIVE_KIND_META_KEY, APIFUSE_SENSITIVE_META_KEY, collectSensitivePaths, describeKey, field, fields, isSensitiveSchema, redactPayload, sensitive, z, } from "./schema.js";
|
|
34
|
-
export { createServerApp, serve } from "./server/index.js";
|
|
34
|
+
export { createServerApp, ERROR_OBSERVABILITY_HEADER, serve, } from "./server/index.js";
|
|
35
35
|
export { getStealthProfile, listStealthProfiles } from "./stealth/profiles.js";
|
|
36
36
|
export * from "./stream.js";
|
|
37
37
|
export { DEFAULT_OPERATION_TRANSPORT, HttpRetryAfterPolicy, HttpRetryDelayStrategy, HttpRetryJitter, HttpRetryPreset, HttpRetryUnsafeMethodPolicy, PROBE_INTERVALS, STREAM_CHUNK_BYTES_MAX, STREAM_CHUNK_BYTES_MIN, STREAM_HEARTBEAT_MS_MAX, STREAM_HEARTBEAT_MS_MIN, STREAM_IDLE_TIMEOUT_MS_MAX, STREAM_IDLE_TIMEOUT_MS_MIN, STREAM_MAX_DURATION_MS_MAX, STREAM_MAX_DURATION_MS_MIN, } from "./types.js";
|
package/dist/runtime/executor.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { isSessionExpiredError, ProviderError, SessionExpiredError } from "../errors.js";
|
|
1
|
+
import { isSessionExpiredError, isValidationError, ProviderError, SessionExpiredError, ValidationError, } from "../errors.js";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
import { parseSchema } from "../schema.js";
|
|
3
4
|
import { assertRequiredSecretsPresent } from "./secrets.js";
|
|
4
5
|
export function isStreamingOperation(provider, operationId) {
|
|
@@ -59,5 +60,19 @@ export async function executeOperation(provider, operationId, ctx, input, _optio
|
|
|
59
60
|
if (isStreamingOperation(provider, operationId)) {
|
|
60
61
|
return result;
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
+
try {
|
|
64
|
+
return await parseSchema(operation.output, result, `operations.${operationId}.output`);
|
|
65
|
+
}
|
|
66
|
+
catch (cause) {
|
|
67
|
+
if (!(cause instanceof z.ZodError) && !isValidationError(cause)) {
|
|
68
|
+
throw cause;
|
|
69
|
+
}
|
|
70
|
+
throw new ValidationError(`Operation handler output failed schema validation.`, {
|
|
71
|
+
code: "OUTPUT_VALIDATION_FAILED",
|
|
72
|
+
category: "output_validation",
|
|
73
|
+
retryable: false,
|
|
74
|
+
zodError: isValidationError(cause) ? cause.zodError : cause,
|
|
75
|
+
...(cause instanceof Error ? { cause } : {}),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
63
78
|
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createServerApp, type ProviderServerCloseOptions, type ProviderServerHandle, type ProviderServerLogEvent, type ProviderServerLogger, type ProviderServerOperationExecutor, type ProviderServerOperationExecutorInput, type ProviderServerOptions, type ProviderServerStatefulForwardEnvelope, type ServeOptions, serve, } from "./serve.js";
|
|
1
|
+
export { createServerApp, ERROR_OBSERVABILITY_HEADER, type ErrorObservabilityDetails, type ProviderServerCloseOptions, type ProviderServerHandle, type ProviderServerLogEvent, type ProviderServerLogger, type ProviderServerOperationExecutor, type ProviderServerOperationExecutorInput, type ProviderServerOptions, type ProviderServerStatefulForwardEnvelope, type ServeOptions, serve, } from "./serve.js";
|
|
2
2
|
export { computeSelfTestPlanDigest, createSelfTestApp, createSelfTestAuthFlowInvoke, createSelfTestInvoke, DEFAULT_SELF_TEST_REQUEST_BUDGET_MS, isSelfTestReadOnlyOperation, PROVIDER_RUNTIME_SELF_TEST_REQUEST_BUDGET_MS_ENV, resolveSelfTestPort, SELF_TEST_AUTH_FLOW_MULTI_TURN_SKIP_REASON, SELF_TEST_AUTH_FLOW_REJECTED_SKIP_REASON, SELF_TEST_HEALTHZ_PATH, SELF_TEST_PATH, SELF_TEST_SCHEMA_VERSION, type SelfTestAppOptions, type SelfTestAuthFlowInvoke, type SelfTestAuthFlowRoute, type SelfTestCaseResult, type SelfTestCaseStatus, type SelfTestOperationInvoke, type SelfTestRequest, SelfTestRequestSchema, type SelfTestResponse, } from "./self-test.js";
|
|
3
3
|
export { type InputDateTokenCalendar, resolveHealthCheckInputDateTokens, } from "./self-test-input-tokens.js";
|
|
4
4
|
export { collectSelfTestSensitiveValues, redactSelfTestText, SELF_TEST_MAX_TEXT_LENGTH, SELF_TEST_REDACTED_PLACEHOLDER, } from "./self-test-redaction.js";
|
package/dist/server/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createServerApp, serve, } from "./serve.js";
|
|
1
|
+
export { createServerApp, ERROR_OBSERVABILITY_HEADER, serve, } from "./serve.js";
|
|
2
2
|
export { computeSelfTestPlanDigest, createSelfTestApp, createSelfTestAuthFlowInvoke, createSelfTestInvoke, DEFAULT_SELF_TEST_REQUEST_BUDGET_MS, isSelfTestReadOnlyOperation, PROVIDER_RUNTIME_SELF_TEST_REQUEST_BUDGET_MS_ENV, resolveSelfTestPort, SELF_TEST_AUTH_FLOW_MULTI_TURN_SKIP_REASON, SELF_TEST_AUTH_FLOW_REJECTED_SKIP_REASON, SELF_TEST_HEALTHZ_PATH, SELF_TEST_PATH, SELF_TEST_SCHEMA_VERSION, SelfTestRequestSchema, } from "./self-test.js";
|
|
3
3
|
export { resolveHealthCheckInputDateTokens, } from "./self-test-input-tokens.js";
|
|
4
4
|
export { collectSelfTestSensitiveValues, redactSelfTestText, SELF_TEST_MAX_TEXT_LENGTH, SELF_TEST_REDACTED_PLACEHOLDER, } from "./self-test-redaction.js";
|
package/dist/server/serve.d.ts
CHANGED
|
@@ -3,6 +3,14 @@ import { z } from "zod";
|
|
|
3
3
|
import { type ProviderErrorCategory } from "../observability.js";
|
|
4
4
|
import type { ProviderContext, ProviderDefinition, ProviderRuntimeState, SttContext } from "../types.js";
|
|
5
5
|
import { type OperationRequest } from "./types.js";
|
|
6
|
+
/** Compact SDK-owned error classification emitted separately from the public response body. */
|
|
7
|
+
export declare const ERROR_OBSERVABILITY_HEADER = "X-ApiFuse-Error-Observability";
|
|
8
|
+
export type ErrorObservabilityDetails = {
|
|
9
|
+
category: ProviderErrorCategory;
|
|
10
|
+
taxonomyVersion: string;
|
|
11
|
+
retryable: boolean;
|
|
12
|
+
upstreamStatus?: number;
|
|
13
|
+
};
|
|
6
14
|
export declare const ProviderServerStatefulForwardEnvelopeSchema: z.ZodObject<{
|
|
7
15
|
requestId: z.ZodString;
|
|
8
16
|
providerId: z.ZodString;
|
|
@@ -78,6 +86,7 @@ export type ProviderServerLogEvent = (ProviderServerLogEventBase & {
|
|
|
78
86
|
errorCategory?: ProviderErrorCategory;
|
|
79
87
|
taxonomyVersion?: string;
|
|
80
88
|
retryable?: boolean;
|
|
89
|
+
signal?: "unregistered_provider_error_code";
|
|
81
90
|
issues?: Array<{
|
|
82
91
|
path: string;
|
|
83
92
|
code: string;
|
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";
|
|
@@ -33,6 +33,8 @@ import { resolveSelfTestMasterSecrets } from "./self-test-token.js";
|
|
|
33
33
|
import { AuthFlowRequestSchema, OperationConnectionSchema, OperationRequestSchema, } from "./types.js";
|
|
34
34
|
const DEFAULT_HOST = "0.0.0.0";
|
|
35
35
|
const DEFAULT_PORT = 3000;
|
|
36
|
+
/** Compact SDK-owned error classification emitted separately from the public response body. */
|
|
37
|
+
export const ERROR_OBSERVABILITY_HEADER = "X-ApiFuse-Error-Observability";
|
|
36
38
|
const AUTH_FLOW_LOCALES = ["en", "ko", "ja"];
|
|
37
39
|
const retryResponseMeta = new WeakMap();
|
|
38
40
|
const STATEFUL_INTERNAL_OPERATIONS_ROUTE = "/__apifuse/stateful/operations";
|
|
@@ -320,25 +322,27 @@ function zodDetails(error) {
|
|
|
320
322
|
}));
|
|
321
323
|
}
|
|
322
324
|
function toErrorResponse(error, requestId) {
|
|
325
|
+
const observability = errorObservabilityDetails(error);
|
|
323
326
|
if (error instanceof StatefulRoutingDeadlineError) {
|
|
324
327
|
return {
|
|
325
328
|
error: {
|
|
326
329
|
code: "STATEFUL_FORWARDING_DEADLINE_EXPIRED",
|
|
327
330
|
message: "Stateful forwarding deadline expired.",
|
|
328
331
|
...(requestId ? { requestId } : {}),
|
|
329
|
-
|
|
332
|
+
retryable: observability.retryable,
|
|
330
333
|
},
|
|
331
334
|
};
|
|
332
335
|
}
|
|
333
336
|
if (isProviderError(error)) {
|
|
334
|
-
const details =
|
|
337
|
+
const details = error.details;
|
|
335
338
|
return {
|
|
336
339
|
error: {
|
|
337
340
|
code: error.code ?? "provider_error",
|
|
338
341
|
message: publicProviderErrorMessage(error),
|
|
339
342
|
...(requestId ? { requestId } : {}),
|
|
343
|
+
retryable: observability.retryable,
|
|
340
344
|
...(error.fix ? { fix: error.fix } : {}),
|
|
341
|
-
...(details ? { details } : {}),
|
|
345
|
+
...(details !== undefined ? { details } : {}),
|
|
342
346
|
},
|
|
343
347
|
};
|
|
344
348
|
}
|
|
@@ -348,6 +352,7 @@ function toErrorResponse(error, requestId) {
|
|
|
348
352
|
code: "invalid_request",
|
|
349
353
|
message: "Invalid request body",
|
|
350
354
|
...(requestId ? { requestId } : {}),
|
|
355
|
+
retryable: observability.retryable,
|
|
351
356
|
details: zodDetails(error),
|
|
352
357
|
},
|
|
353
358
|
};
|
|
@@ -363,6 +368,7 @@ function toErrorResponse(error, requestId) {
|
|
|
363
368
|
code: "internal_error",
|
|
364
369
|
message: "Internal error",
|
|
365
370
|
...(requestId ? { requestId } : {}),
|
|
371
|
+
retryable: observability.retryable,
|
|
366
372
|
details: {
|
|
367
373
|
retryable: false,
|
|
368
374
|
category: "internal_error",
|
|
@@ -371,26 +377,6 @@ function toErrorResponse(error, requestId) {
|
|
|
371
377
|
},
|
|
372
378
|
};
|
|
373
379
|
}
|
|
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
380
|
// Accepts `unknown` so the branded guards narrow cleanly from the top: the
|
|
395
381
|
// subtype error classes are structurally compatible with ProviderError, so
|
|
396
382
|
// narrowing from a ProviderError-typed value would collapse the negative branch
|
|
@@ -449,6 +435,48 @@ function providerObservabilityDetails(error) {
|
|
|
449
435
|
...(error.upstreamStatus ? { upstreamStatus: error.upstreamStatus } : {}),
|
|
450
436
|
};
|
|
451
437
|
}
|
|
438
|
+
function errorObservabilityDetails(error) {
|
|
439
|
+
const providerDetails = providerObservabilityDetails(error);
|
|
440
|
+
if (providerDetails)
|
|
441
|
+
return providerDetails;
|
|
442
|
+
if (error instanceof z.ZodError || isValidationError(error)) {
|
|
443
|
+
return {
|
|
444
|
+
category: isProviderError(error) && error.options?.category
|
|
445
|
+
? error.options.category
|
|
446
|
+
: "input_validation",
|
|
447
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
448
|
+
retryable: isProviderError(error) ? (error.options?.retryable ?? false) : false,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
if (error instanceof StatefulRoutingDeadlineError) {
|
|
452
|
+
return {
|
|
453
|
+
category: "timeout",
|
|
454
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
455
|
+
retryable: false,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
if (isProviderError(error)) {
|
|
459
|
+
return {
|
|
460
|
+
category: error.options?.category ?? "provider_error",
|
|
461
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
462
|
+
retryable: error.options?.retryable ?? false,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
category: "internal_error",
|
|
467
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
468
|
+
retryable: false,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function responseWithErrorObservability(response, error) {
|
|
472
|
+
const headers = new Headers(response.headers);
|
|
473
|
+
headers.set(ERROR_OBSERVABILITY_HEADER, JSON.stringify(errorObservabilityDetails(error)));
|
|
474
|
+
return new Response(response.body, {
|
|
475
|
+
status: response.status,
|
|
476
|
+
statusText: response.statusText,
|
|
477
|
+
headers,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
452
480
|
function publicProviderErrorMessage(error) {
|
|
453
481
|
if (isTransportError(error)) {
|
|
454
482
|
if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
|
|
@@ -481,9 +509,6 @@ function toStatusCode(error) {
|
|
|
481
509
|
if (error instanceof StatefulRoutingDeadlineError) {
|
|
482
510
|
return 504;
|
|
483
511
|
}
|
|
484
|
-
if (isTransportError(error)) {
|
|
485
|
-
return error.code === "transport_timeout" ? 504 : 502;
|
|
486
|
-
}
|
|
487
512
|
if (isProviderError(error)) {
|
|
488
513
|
switch (error.code) {
|
|
489
514
|
case "AUTH_REQUIRED":
|
|
@@ -509,10 +534,87 @@ function toStatusCode(error) {
|
|
|
509
534
|
case "STATEFUL_FORWARDING_REPLAY_CACHE_FULL":
|
|
510
535
|
return 503;
|
|
511
536
|
}
|
|
512
|
-
|
|
537
|
+
if (isTransportError(error)) {
|
|
538
|
+
return error.code === "transport_timeout" ? 504 : 502;
|
|
539
|
+
}
|
|
540
|
+
if (isValidationError(error)) {
|
|
541
|
+
return error.options?.category === "output_validation" ? 500 : 400;
|
|
542
|
+
}
|
|
543
|
+
return 500;
|
|
513
544
|
}
|
|
514
545
|
return 500;
|
|
515
546
|
}
|
|
547
|
+
// Codes emitted by SDK-owned paths must never be attributed to provider
|
|
548
|
+
// authors by the unregistered-code signal, even when their intentional status
|
|
549
|
+
// is 500. Provider-authored codes not in this registry retain the signal.
|
|
550
|
+
const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
|
|
551
|
+
"AUTH_PROMPT_UNAVAILABLE",
|
|
552
|
+
"BROWSER_CDP_POOL_REQUIRED",
|
|
553
|
+
"BROWSER_RUNTIME_UNSUPPORTED",
|
|
554
|
+
"STEALTH_RUNTIME_UNSUPPORTED",
|
|
555
|
+
"SSE_EVENT_UNDECLARED",
|
|
556
|
+
"STREAM_EVENT_TOO_LARGE",
|
|
557
|
+
"STREAM_CHUNK_TOO_LARGE",
|
|
558
|
+
"SSE_RESULT_UNSUPPORTED",
|
|
559
|
+
"STREAM_RESULT_UNSUPPORTED",
|
|
560
|
+
"AUTH_FLOW_NOT_CONFIGURED",
|
|
561
|
+
"refresh_not_supported",
|
|
562
|
+
"RUNTIME_UNSUPPORTED",
|
|
563
|
+
"PROVIDER_STATE_UNSUPPORTED",
|
|
564
|
+
"CHOICE_TOKEN_MASTER_SECRET_NOT_CONFIGURED",
|
|
565
|
+
"CHOICE_STATE_PAYLOAD_TOO_LARGE",
|
|
566
|
+
"CHOICE_STATE_UNAVAILABLE",
|
|
567
|
+
"CHOICE_CONTEXT_REQUIRED",
|
|
568
|
+
"unsupported_stealth_cookie_store_version",
|
|
569
|
+
"provider_secret_error",
|
|
570
|
+
"credential_key_error",
|
|
571
|
+
"credential_mode_error",
|
|
572
|
+
"flow_expired",
|
|
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
|
+
]);
|
|
516
618
|
function extractRequestId(raw) {
|
|
517
619
|
if (!raw || typeof raw !== "object") {
|
|
518
620
|
return undefined;
|
|
@@ -530,7 +632,12 @@ function logProviderError(logger, provider, kind, route, requestId, error, statu
|
|
|
530
632
|
: "internal_error";
|
|
531
633
|
const errorClass = error instanceof Error ? error.name : typeof error;
|
|
532
634
|
const message = error instanceof Error ? error.message : String(error);
|
|
533
|
-
const details =
|
|
635
|
+
const details = errorObservabilityDetails(error);
|
|
636
|
+
const isUnregisteredProviderErrorCode = status === 500 &&
|
|
637
|
+
isProviderError(error) &&
|
|
638
|
+
!isValidationError(error) &&
|
|
639
|
+
typeof error.code === "string" &&
|
|
640
|
+
!SDK_OWNED_PROVIDER_ERROR_CODES.has(error.code);
|
|
534
641
|
const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
535
642
|
emit({
|
|
536
643
|
level: status >= 500 ? "error" : "warn",
|
|
@@ -544,15 +651,12 @@ function logProviderError(logger, provider, kind, route, requestId, error, statu
|
|
|
544
651
|
code,
|
|
545
652
|
errorClass,
|
|
546
653
|
message,
|
|
547
|
-
...(
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
taxonomyVersion: details.taxonomyVersion,
|
|
554
|
-
retryable: details.retryable,
|
|
555
|
-
}
|
|
654
|
+
...(details.upstreamStatus ? { upstreamStatus: details.upstreamStatus } : {}),
|
|
655
|
+
errorCategory: details.category,
|
|
656
|
+
taxonomyVersion: details.taxonomyVersion,
|
|
657
|
+
retryable: details.retryable,
|
|
658
|
+
...(isUnregisteredProviderErrorCode
|
|
659
|
+
? { signal: "unregistered_provider_error_code" }
|
|
556
660
|
: {}),
|
|
557
661
|
...(error instanceof z.ZodError ? { issues: zodDetails(error) } : {}),
|
|
558
662
|
});
|
|
@@ -1150,12 +1254,10 @@ export function createServerApp(provider, options = {}) {
|
|
|
1150
1254
|
missingSecrets: missingSecretsAtBoot,
|
|
1151
1255
|
});
|
|
1152
1256
|
}
|
|
1153
|
-
app.notFound((c) =>
|
|
1154
|
-
error:
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
},
|
|
1158
|
-
}, 404));
|
|
1257
|
+
app.notFound((c) => {
|
|
1258
|
+
const error = new ProviderError("Not found", { code: "not_found", retryable: false });
|
|
1259
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error), 404), error);
|
|
1260
|
+
});
|
|
1159
1261
|
app.get("/health", (c) => c.json({
|
|
1160
1262
|
status: "ok",
|
|
1161
1263
|
provider: provider.id,
|
|
@@ -1272,7 +1374,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1272
1374
|
}
|
|
1273
1375
|
const requestId = extractRequestId(rawBody);
|
|
1274
1376
|
logProviderError(logger, provider, "operation", operation, requestId, error, status, finishRequestCost(requestCost));
|
|
1275
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1377
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1276
1378
|
}
|
|
1277
1379
|
});
|
|
1278
1380
|
app.post("/v1/:operation", async (c) => {
|
|
@@ -1306,7 +1408,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1306
1408
|
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
1307
1409
|
if (telemetryHeader)
|
|
1308
1410
|
c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
1309
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1411
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1310
1412
|
}
|
|
1311
1413
|
});
|
|
1312
1414
|
app.post("/auth/start", async (c) => {
|
|
@@ -1326,7 +1428,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1326
1428
|
const status = toStatusCode(error);
|
|
1327
1429
|
const requestId = extractRequestId(rawBody);
|
|
1328
1430
|
logProviderError(logger, provider, "auth", "start", requestId, error, status, finishRequestCost(requestCost));
|
|
1329
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1431
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1330
1432
|
}
|
|
1331
1433
|
});
|
|
1332
1434
|
app.post("/auth/continue", async (c) => {
|
|
@@ -1346,7 +1448,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1346
1448
|
const status = toStatusCode(error);
|
|
1347
1449
|
const requestId = extractRequestId(rawBody);
|
|
1348
1450
|
logProviderError(logger, provider, "auth", "continue", requestId, error, status, finishRequestCost(requestCost));
|
|
1349
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1451
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1350
1452
|
}
|
|
1351
1453
|
});
|
|
1352
1454
|
app.post("/auth/poll", async (c) => {
|
|
@@ -1366,7 +1468,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1366
1468
|
const status = toStatusCode(error);
|
|
1367
1469
|
const requestId = extractRequestId(rawBody);
|
|
1368
1470
|
logProviderError(logger, provider, "auth", "poll", requestId, error, status, finishRequestCost(requestCost));
|
|
1369
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1471
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1370
1472
|
}
|
|
1371
1473
|
});
|
|
1372
1474
|
app.post("/auth/refresh", async (c) => {
|
|
@@ -1386,7 +1488,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1386
1488
|
const status = toStatusCode(error);
|
|
1387
1489
|
const requestId = extractRequestId(rawBody);
|
|
1388
1490
|
logProviderError(logger, provider, "auth", "refresh", requestId, error, status, finishRequestCost(requestCost));
|
|
1389
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1491
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1390
1492
|
}
|
|
1391
1493
|
});
|
|
1392
1494
|
app.post("/auth/disconnect", async (c) => {
|
|
@@ -1406,7 +1508,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1406
1508
|
const status = toStatusCode(error);
|
|
1407
1509
|
const requestId = extractRequestId(rawBody);
|
|
1408
1510
|
logProviderError(logger, provider, "auth", "disconnect", requestId, error, status, finishRequestCost(requestCost));
|
|
1409
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1511
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1410
1512
|
}
|
|
1411
1513
|
});
|
|
1412
1514
|
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/package.json
CHANGED
package/src/errors.ts
CHANGED
|
@@ -10,6 +10,7 @@ const PROVIDER_ERROR_BRAND = Symbol.for("@apifuse/provider-sdk/error-brand@1");
|
|
|
10
10
|
const PROVIDER_ERROR_BRAND_VALUE = 1;
|
|
11
11
|
const SESSION_EXPIRED_BRAND = Symbol.for("@apifuse/provider-sdk/error-kind/session-expired@1");
|
|
12
12
|
const TRANSPORT_BRAND = Symbol.for("@apifuse/provider-sdk/error-kind/transport@1");
|
|
13
|
+
const VALIDATION_BRAND = Symbol.for("@apifuse/provider-sdk/error-kind/validation@1");
|
|
13
14
|
|
|
14
15
|
// Defines a non-enumerable, non-writable, non-configurable own data property.
|
|
15
16
|
// Immutable + own means a guard can trust it via a single descriptor read
|
|
@@ -125,6 +126,7 @@ export class ValidationError extends ProviderError {
|
|
|
125
126
|
super(message, options);
|
|
126
127
|
this.name = "ValidationError";
|
|
127
128
|
this.zodError = options?.zodError;
|
|
129
|
+
defineErrorBrand(this, VALIDATION_BRAND, true);
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
132
|
|
|
@@ -162,6 +164,13 @@ export function isTransportError(value: unknown): value is TransportError {
|
|
|
162
164
|
return isProviderError(value) && hasOwnBrand(value, TRANSPORT_BRAND, true);
|
|
163
165
|
}
|
|
164
166
|
|
|
167
|
+
export function isValidationError(value: unknown): value is ValidationError {
|
|
168
|
+
return (
|
|
169
|
+
isProviderError(value) &&
|
|
170
|
+
(hasOwnBrand(value, VALIDATION_BRAND, true) || value.name === "ValidationError")
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
165
174
|
export class ProviderSecretError extends ProviderError {
|
|
166
175
|
constructor(message: string, options?: ProviderErrorOptions) {
|
|
167
176
|
super(message, { code: "provider_secret_error", ...options });
|
package/src/index.ts
CHANGED
|
@@ -135,7 +135,12 @@ export {
|
|
|
135
135
|
sensitive,
|
|
136
136
|
z,
|
|
137
137
|
} from "./schema.js";
|
|
138
|
-
export {
|
|
138
|
+
export {
|
|
139
|
+
createServerApp,
|
|
140
|
+
ERROR_OBSERVABILITY_HEADER,
|
|
141
|
+
type ServeOptions,
|
|
142
|
+
serve,
|
|
143
|
+
} from "./server/index.js";
|
|
139
144
|
export { getStealthProfile, listStealthProfiles } from "./stealth/profiles.js";
|
|
140
145
|
export * from "./stream.js";
|
|
141
146
|
export type {
|
package/src/runtime/executor.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isSessionExpiredError,
|
|
3
|
+
isValidationError,
|
|
4
|
+
ProviderError,
|
|
5
|
+
SessionExpiredError,
|
|
6
|
+
ValidationError,
|
|
7
|
+
} from "../errors.js";
|
|
8
|
+
import { z } from "zod";
|
|
2
9
|
import { parseSchema } from "../schema.js";
|
|
3
10
|
import type { ProviderContext, ProviderDefinition } from "../types.js";
|
|
4
11
|
import { assertRequiredSecretsPresent } from "./secrets.js";
|
|
@@ -81,5 +88,18 @@ export async function executeOperation(
|
|
|
81
88
|
return result;
|
|
82
89
|
}
|
|
83
90
|
|
|
84
|
-
|
|
91
|
+
try {
|
|
92
|
+
return await parseSchema(operation.output, result, `operations.${operationId}.output`);
|
|
93
|
+
} catch (cause) {
|
|
94
|
+
if (!(cause instanceof z.ZodError) && !isValidationError(cause)) {
|
|
95
|
+
throw cause;
|
|
96
|
+
}
|
|
97
|
+
throw new ValidationError(`Operation handler output failed schema validation.`, {
|
|
98
|
+
code: "OUTPUT_VALIDATION_FAILED",
|
|
99
|
+
category: "output_validation",
|
|
100
|
+
retryable: false,
|
|
101
|
+
zodError: isValidationError(cause) ? cause.zodError : cause,
|
|
102
|
+
...(cause instanceof Error ? { cause } : {}),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
85
105
|
}
|
package/src/server/index.ts
CHANGED
package/src/server/serve.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
isProviderError,
|
|
10
10
|
isSessionExpiredError,
|
|
11
11
|
isTransportError,
|
|
12
|
+
isValidationError,
|
|
12
13
|
ProviderError,
|
|
13
14
|
} from "../errors.js";
|
|
14
15
|
import {
|
|
@@ -108,6 +109,14 @@ import {
|
|
|
108
109
|
|
|
109
110
|
const DEFAULT_HOST = "0.0.0.0";
|
|
110
111
|
const DEFAULT_PORT = 3000;
|
|
112
|
+
/** Compact SDK-owned error classification emitted separately from the public response body. */
|
|
113
|
+
export const ERROR_OBSERVABILITY_HEADER = "X-ApiFuse-Error-Observability";
|
|
114
|
+
export type ErrorObservabilityDetails = {
|
|
115
|
+
category: ProviderErrorCategory;
|
|
116
|
+
taxonomyVersion: string;
|
|
117
|
+
retryable: boolean;
|
|
118
|
+
upstreamStatus?: number;
|
|
119
|
+
};
|
|
111
120
|
const AUTH_FLOW_LOCALES = ["en", "ko", "ja"] as const;
|
|
112
121
|
const retryResponseMeta = new WeakMap<ProviderContext, HttpRetrySummary>();
|
|
113
122
|
const STATEFUL_INTERNAL_OPERATIONS_ROUTE = "/__apifuse/stateful/operations";
|
|
@@ -486,6 +495,7 @@ export type ProviderServerLogEvent =
|
|
|
486
495
|
errorCategory?: ProviderErrorCategory;
|
|
487
496
|
taxonomyVersion?: string;
|
|
488
497
|
retryable?: boolean;
|
|
498
|
+
signal?: "unregistered_provider_error_code";
|
|
489
499
|
issues?: Array<{ path: string; code: string; message: string }>;
|
|
490
500
|
})
|
|
491
501
|
| {
|
|
@@ -604,26 +614,28 @@ function zodDetails(error: z.ZodError): Array<{
|
|
|
604
614
|
}
|
|
605
615
|
|
|
606
616
|
function toErrorResponse(error: unknown, requestId?: string): OperationErrorResponse {
|
|
617
|
+
const observability = errorObservabilityDetails(error);
|
|
607
618
|
if (error instanceof StatefulRoutingDeadlineError) {
|
|
608
619
|
return {
|
|
609
620
|
error: {
|
|
610
621
|
code: "STATEFUL_FORWARDING_DEADLINE_EXPIRED",
|
|
611
622
|
message: "Stateful forwarding deadline expired.",
|
|
612
623
|
...(requestId ? { requestId } : {}),
|
|
613
|
-
|
|
624
|
+
retryable: observability.retryable,
|
|
614
625
|
},
|
|
615
626
|
};
|
|
616
627
|
}
|
|
617
628
|
|
|
618
629
|
if (isProviderError(error)) {
|
|
619
|
-
const details =
|
|
630
|
+
const details = error.details;
|
|
620
631
|
return {
|
|
621
632
|
error: {
|
|
622
633
|
code: error.code ?? "provider_error",
|
|
623
634
|
message: publicProviderErrorMessage(error),
|
|
624
635
|
...(requestId ? { requestId } : {}),
|
|
636
|
+
retryable: observability.retryable,
|
|
625
637
|
...(error.fix ? { fix: error.fix } : {}),
|
|
626
|
-
...(details ? { details } : {}),
|
|
638
|
+
...(details !== undefined ? { details } : {}),
|
|
627
639
|
},
|
|
628
640
|
};
|
|
629
641
|
}
|
|
@@ -634,6 +646,7 @@ function toErrorResponse(error: unknown, requestId?: string): OperationErrorResp
|
|
|
634
646
|
code: "invalid_request",
|
|
635
647
|
message: "Invalid request body",
|
|
636
648
|
...(requestId ? { requestId } : {}),
|
|
649
|
+
retryable: observability.retryable,
|
|
637
650
|
details: zodDetails(error),
|
|
638
651
|
},
|
|
639
652
|
};
|
|
@@ -650,6 +663,7 @@ function toErrorResponse(error: unknown, requestId?: string): OperationErrorResp
|
|
|
650
663
|
code: "internal_error",
|
|
651
664
|
message: "Internal error",
|
|
652
665
|
...(requestId ? { requestId } : {}),
|
|
666
|
+
retryable: observability.retryable,
|
|
653
667
|
details: {
|
|
654
668
|
retryable: false,
|
|
655
669
|
category: "internal_error",
|
|
@@ -659,42 +673,12 @@ function toErrorResponse(error: unknown, requestId?: string): OperationErrorResp
|
|
|
659
673
|
};
|
|
660
674
|
}
|
|
661
675
|
|
|
662
|
-
function publicProviderErrorDetails(error: ProviderError): unknown {
|
|
663
|
-
const providerDetails = error.details;
|
|
664
|
-
const observabilityDetails = providerObservabilityDetails(error);
|
|
665
|
-
|
|
666
|
-
if (providerDetails === undefined) {
|
|
667
|
-
return observabilityDetails;
|
|
668
|
-
}
|
|
669
|
-
if (observabilityDetails === undefined) {
|
|
670
|
-
return providerDetails;
|
|
671
|
-
}
|
|
672
|
-
if (isPlainRecord(providerDetails) && isPlainRecord(observabilityDetails)) {
|
|
673
|
-
return { ...providerDetails, ...observabilityDetails };
|
|
674
|
-
}
|
|
675
|
-
return {
|
|
676
|
-
provider: providerDetails,
|
|
677
|
-
observability: observabilityDetails,
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
682
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
676
|
// Accepts `unknown` so the branded guards narrow cleanly from the top: the
|
|
686
677
|
// subtype error classes are structurally compatible with ProviderError, so
|
|
687
678
|
// narrowing from a ProviderError-typed value would collapse the negative branch
|
|
688
679
|
// to `never`. Narrowing from unknown avoids that while still recognizing errors
|
|
689
680
|
// from a duplicate SDK module instance.
|
|
690
|
-
function providerObservabilityDetails(error: unknown):
|
|
691
|
-
| {
|
|
692
|
-
category: ProviderErrorCategory;
|
|
693
|
-
taxonomyVersion: string;
|
|
694
|
-
retryable: boolean;
|
|
695
|
-
upstreamStatus?: number;
|
|
696
|
-
}
|
|
697
|
-
| undefined {
|
|
681
|
+
function providerObservabilityDetails(error: unknown): ErrorObservabilityDetails | undefined {
|
|
698
682
|
// Session-expiry surfaces the credential_expired category + the opt-in
|
|
699
683
|
// retryable signal so Gateway/Credential Service can refresh and re-drive the
|
|
700
684
|
// operation (see design.md §4.3 D3). Without this branch the auth error would
|
|
@@ -751,6 +735,54 @@ function providerObservabilityDetails(error: unknown):
|
|
|
751
735
|
};
|
|
752
736
|
}
|
|
753
737
|
|
|
738
|
+
function errorObservabilityDetails(error: unknown): ErrorObservabilityDetails {
|
|
739
|
+
const providerDetails = providerObservabilityDetails(error);
|
|
740
|
+
if (providerDetails) return providerDetails;
|
|
741
|
+
|
|
742
|
+
if (error instanceof z.ZodError || isValidationError(error)) {
|
|
743
|
+
return {
|
|
744
|
+
category:
|
|
745
|
+
isProviderError(error) && error.options?.category
|
|
746
|
+
? error.options.category
|
|
747
|
+
: "input_validation",
|
|
748
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
749
|
+
retryable: isProviderError(error) ? (error.options?.retryable ?? false) : false,
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (error instanceof StatefulRoutingDeadlineError) {
|
|
754
|
+
return {
|
|
755
|
+
category: "timeout",
|
|
756
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
757
|
+
retryable: false,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (isProviderError(error)) {
|
|
762
|
+
return {
|
|
763
|
+
category: error.options?.category ?? "provider_error",
|
|
764
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
765
|
+
retryable: error.options?.retryable ?? false,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
return {
|
|
770
|
+
category: "internal_error",
|
|
771
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
772
|
+
retryable: false,
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function responseWithErrorObservability(response: Response, error: unknown): Response {
|
|
777
|
+
const headers = new Headers(response.headers);
|
|
778
|
+
headers.set(ERROR_OBSERVABILITY_HEADER, JSON.stringify(errorObservabilityDetails(error)));
|
|
779
|
+
return new Response(response.body, {
|
|
780
|
+
status: response.status,
|
|
781
|
+
statusText: response.statusText,
|
|
782
|
+
headers,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
754
786
|
function publicProviderErrorMessage(error: ProviderError): string {
|
|
755
787
|
if (isTransportError(error)) {
|
|
756
788
|
if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
|
|
@@ -782,11 +814,6 @@ function toStatusCode(error: unknown): 400 | 401 | 404 | 429 | 500 | 502 | 503 |
|
|
|
782
814
|
if (error instanceof StatefulRoutingDeadlineError) {
|
|
783
815
|
return 504;
|
|
784
816
|
}
|
|
785
|
-
|
|
786
|
-
if (isTransportError(error)) {
|
|
787
|
-
return error.code === "transport_timeout" ? 504 : 502;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
817
|
if (isProviderError(error)) {
|
|
791
818
|
switch (error.code) {
|
|
792
819
|
case "AUTH_REQUIRED":
|
|
@@ -812,13 +839,91 @@ function toStatusCode(error: unknown): 400 | 401 | 404 | 429 | 500 | 502 | 503 |
|
|
|
812
839
|
case "STATEFUL_FORWARDING_REPLAY_CACHE_FULL":
|
|
813
840
|
return 503;
|
|
814
841
|
}
|
|
842
|
+
if (isTransportError(error)) {
|
|
843
|
+
return error.code === "transport_timeout" ? 504 : 502;
|
|
844
|
+
}
|
|
845
|
+
if (isValidationError(error)) {
|
|
846
|
+
return error.options?.category === "output_validation" ? 500 : 400;
|
|
847
|
+
}
|
|
815
848
|
|
|
816
|
-
return
|
|
849
|
+
return 500;
|
|
817
850
|
}
|
|
818
851
|
|
|
819
852
|
return 500;
|
|
820
853
|
}
|
|
821
854
|
|
|
855
|
+
// Codes emitted by SDK-owned paths must never be attributed to provider
|
|
856
|
+
// authors by the unregistered-code signal, even when their intentional status
|
|
857
|
+
// is 500. Provider-authored codes not in this registry retain the signal.
|
|
858
|
+
const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
|
|
859
|
+
"AUTH_PROMPT_UNAVAILABLE",
|
|
860
|
+
"BROWSER_CDP_POOL_REQUIRED",
|
|
861
|
+
"BROWSER_RUNTIME_UNSUPPORTED",
|
|
862
|
+
"STEALTH_RUNTIME_UNSUPPORTED",
|
|
863
|
+
"SSE_EVENT_UNDECLARED",
|
|
864
|
+
"STREAM_EVENT_TOO_LARGE",
|
|
865
|
+
"STREAM_CHUNK_TOO_LARGE",
|
|
866
|
+
"SSE_RESULT_UNSUPPORTED",
|
|
867
|
+
"STREAM_RESULT_UNSUPPORTED",
|
|
868
|
+
"AUTH_FLOW_NOT_CONFIGURED",
|
|
869
|
+
"refresh_not_supported",
|
|
870
|
+
"RUNTIME_UNSUPPORTED",
|
|
871
|
+
"PROVIDER_STATE_UNSUPPORTED",
|
|
872
|
+
"CHOICE_TOKEN_MASTER_SECRET_NOT_CONFIGURED",
|
|
873
|
+
"CHOICE_STATE_PAYLOAD_TOO_LARGE",
|
|
874
|
+
"CHOICE_STATE_UNAVAILABLE",
|
|
875
|
+
"CHOICE_CONTEXT_REQUIRED",
|
|
876
|
+
"unsupported_stealth_cookie_store_version",
|
|
877
|
+
"provider_secret_error",
|
|
878
|
+
"credential_key_error",
|
|
879
|
+
"credential_mode_error",
|
|
880
|
+
"flow_expired",
|
|
881
|
+
"turn_validation_error",
|
|
882
|
+
"context_access_error",
|
|
883
|
+
"UNSUPPORTED_STT_OPTION",
|
|
884
|
+
"INVALID_STT_AUDIO",
|
|
885
|
+
"STT_AUDIO_TOO_LARGE",
|
|
886
|
+
"STT_UPSTREAM_FAILED",
|
|
887
|
+
"INVALID_STT_VERIFICATION_CODE_OPTIONS",
|
|
888
|
+
"NO_CODE_FOUND",
|
|
889
|
+
"AMBIGUOUS_CODE",
|
|
890
|
+
"retry_invalid_policy",
|
|
891
|
+
"retry_unsafe_method",
|
|
892
|
+
"stealth_cookie_store_serialize_failed",
|
|
893
|
+
"response_too_large",
|
|
894
|
+
"transport_stream_unavailable",
|
|
895
|
+
"transport_invalid_method",
|
|
896
|
+
"http_transport_override_unsupported",
|
|
897
|
+
"transport_invalid_url",
|
|
898
|
+
"retry_exhausted",
|
|
899
|
+
"auth_abort_unsafe_data",
|
|
900
|
+
"credentials_auth_missing_credential_keys",
|
|
901
|
+
"credentials_auth_missing_credential",
|
|
902
|
+
"credentials_auth_invalid_login_result",
|
|
903
|
+
"credentials_auth_unknown_challenge",
|
|
904
|
+
"credentials_auth_unknown_pending_challenge",
|
|
905
|
+
"STATEFUL_FORWARDING_NOT_CONFIGURED",
|
|
906
|
+
"STATEFUL_FORWARDING_SIGNATURE_MISSING",
|
|
907
|
+
"STATEFUL_FORWARDING_NONCE_INVALID",
|
|
908
|
+
"STATEFUL_FORWARDING_TIMESTAMP_INVALID",
|
|
909
|
+
"STATEFUL_FORWARDING_SIGNATURE_INVALID",
|
|
910
|
+
"STATEFUL_FORWARDING_REPLAY_DETECTED",
|
|
911
|
+
"STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
|
|
912
|
+
"STATEFUL_FORWARDING_ENVELOPE_INVALID",
|
|
913
|
+
"STATEFUL_FORWARDING_PROVIDER_MISMATCH",
|
|
914
|
+
"STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
|
|
915
|
+
"STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
|
|
916
|
+
"STATEFUL_FORWARDING_REQUEST_FAILED",
|
|
917
|
+
"STATEFUL_FORWARDING_CONTEXT_MISSING",
|
|
918
|
+
"STATEFUL_FORWARDING_BAD_RESPONSE",
|
|
919
|
+
"STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
|
|
920
|
+
"STATEFUL_FILE_FORWARDING_UNSUPPORTED",
|
|
921
|
+
"STATEFUL_CONTROL_PLANE_OPERATION_AMBIGUOUS",
|
|
922
|
+
"STATEFUL_CONTROL_PLANE_REQUEST_FAILED",
|
|
923
|
+
"STATEFUL_CONTROL_PLANE_HTTP_ERROR",
|
|
924
|
+
"STATEFUL_CONTROL_PLANE_INVALID_RESPONSE",
|
|
925
|
+
]);
|
|
926
|
+
|
|
822
927
|
function extractRequestId(raw: unknown): string | undefined {
|
|
823
928
|
if (!raw || typeof raw !== "object") {
|
|
824
929
|
return undefined;
|
|
@@ -847,7 +952,13 @@ function logProviderError(
|
|
|
847
952
|
: "internal_error";
|
|
848
953
|
const errorClass = error instanceof Error ? error.name : typeof error;
|
|
849
954
|
const message = error instanceof Error ? error.message : String(error);
|
|
850
|
-
const details =
|
|
955
|
+
const details = errorObservabilityDetails(error);
|
|
956
|
+
const isUnregisteredProviderErrorCode =
|
|
957
|
+
status === 500 &&
|
|
958
|
+
isProviderError(error) &&
|
|
959
|
+
!isValidationError(error) &&
|
|
960
|
+
typeof error.code === "string" &&
|
|
961
|
+
!SDK_OWNED_PROVIDER_ERROR_CODES.has(error.code);
|
|
851
962
|
const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
852
963
|
emit({
|
|
853
964
|
level: status >= 500 ? "error" : "warn",
|
|
@@ -861,15 +972,12 @@ function logProviderError(
|
|
|
861
972
|
code,
|
|
862
973
|
errorClass,
|
|
863
974
|
message,
|
|
864
|
-
...(
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
taxonomyVersion: details.taxonomyVersion,
|
|
871
|
-
retryable: details.retryable,
|
|
872
|
-
}
|
|
975
|
+
...(details.upstreamStatus ? { upstreamStatus: details.upstreamStatus } : {}),
|
|
976
|
+
errorCategory: details.category,
|
|
977
|
+
taxonomyVersion: details.taxonomyVersion,
|
|
978
|
+
retryable: details.retryable,
|
|
979
|
+
...(isUnregisteredProviderErrorCode
|
|
980
|
+
? { signal: "unregistered_provider_error_code" as const }
|
|
873
981
|
: {}),
|
|
874
982
|
...(error instanceof z.ZodError ? { issues: zodDetails(error) } : {}),
|
|
875
983
|
});
|
|
@@ -1614,17 +1722,10 @@ export function createServerApp(
|
|
|
1614
1722
|
});
|
|
1615
1723
|
}
|
|
1616
1724
|
|
|
1617
|
-
app.notFound((c) =>
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
code: "not_found",
|
|
1622
|
-
message: "Not found",
|
|
1623
|
-
},
|
|
1624
|
-
},
|
|
1625
|
-
404,
|
|
1626
|
-
),
|
|
1627
|
-
);
|
|
1725
|
+
app.notFound((c) => {
|
|
1726
|
+
const error = new ProviderError("Not found", { code: "not_found", retryable: false });
|
|
1727
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error), 404), error);
|
|
1728
|
+
});
|
|
1628
1729
|
|
|
1629
1730
|
app.get("/health", (c) =>
|
|
1630
1731
|
c.json({
|
|
@@ -1786,7 +1887,10 @@ export function createServerApp(
|
|
|
1786
1887
|
status,
|
|
1787
1888
|
finishRequestCost(requestCost),
|
|
1788
1889
|
);
|
|
1789
|
-
return
|
|
1890
|
+
return responseWithErrorObservability(
|
|
1891
|
+
c.json(toErrorResponse(error, requestId), status),
|
|
1892
|
+
error,
|
|
1893
|
+
);
|
|
1790
1894
|
}
|
|
1791
1895
|
});
|
|
1792
1896
|
|
|
@@ -1850,7 +1954,10 @@ export function createServerApp(
|
|
|
1850
1954
|
);
|
|
1851
1955
|
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
1852
1956
|
if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
1853
|
-
return
|
|
1957
|
+
return responseWithErrorObservability(
|
|
1958
|
+
c.json(toErrorResponse(error, requestId), status),
|
|
1959
|
+
error,
|
|
1960
|
+
);
|
|
1854
1961
|
}
|
|
1855
1962
|
});
|
|
1856
1963
|
|
|
@@ -1887,7 +1994,10 @@ export function createServerApp(
|
|
|
1887
1994
|
status,
|
|
1888
1995
|
finishRequestCost(requestCost),
|
|
1889
1996
|
);
|
|
1890
|
-
return
|
|
1997
|
+
return responseWithErrorObservability(
|
|
1998
|
+
c.json(toErrorResponse(error, requestId), status),
|
|
1999
|
+
error,
|
|
2000
|
+
);
|
|
1891
2001
|
}
|
|
1892
2002
|
});
|
|
1893
2003
|
|
|
@@ -1924,7 +2034,10 @@ export function createServerApp(
|
|
|
1924
2034
|
status,
|
|
1925
2035
|
finishRequestCost(requestCost),
|
|
1926
2036
|
);
|
|
1927
|
-
return
|
|
2037
|
+
return responseWithErrorObservability(
|
|
2038
|
+
c.json(toErrorResponse(error, requestId), status),
|
|
2039
|
+
error,
|
|
2040
|
+
);
|
|
1928
2041
|
}
|
|
1929
2042
|
});
|
|
1930
2043
|
|
|
@@ -1961,7 +2074,10 @@ export function createServerApp(
|
|
|
1961
2074
|
status,
|
|
1962
2075
|
finishRequestCost(requestCost),
|
|
1963
2076
|
);
|
|
1964
|
-
return
|
|
2077
|
+
return responseWithErrorObservability(
|
|
2078
|
+
c.json(toErrorResponse(error, requestId), status),
|
|
2079
|
+
error,
|
|
2080
|
+
);
|
|
1965
2081
|
}
|
|
1966
2082
|
});
|
|
1967
2083
|
|
|
@@ -1998,7 +2114,10 @@ export function createServerApp(
|
|
|
1998
2114
|
status,
|
|
1999
2115
|
finishRequestCost(requestCost),
|
|
2000
2116
|
);
|
|
2001
|
-
return
|
|
2117
|
+
return responseWithErrorObservability(
|
|
2118
|
+
c.json(toErrorResponse(error, requestId), status),
|
|
2119
|
+
error,
|
|
2120
|
+
);
|
|
2002
2121
|
}
|
|
2003
2122
|
});
|
|
2004
2123
|
|
|
@@ -2035,7 +2154,10 @@ export function createServerApp(
|
|
|
2035
2154
|
status,
|
|
2036
2155
|
finishRequestCost(requestCost),
|
|
2037
2156
|
);
|
|
2038
|
-
return
|
|
2157
|
+
return responseWithErrorObservability(
|
|
2158
|
+
c.json(toErrorResponse(error, requestId), status),
|
|
2159
|
+
error,
|
|
2160
|
+
);
|
|
2039
2161
|
}
|
|
2040
2162
|
});
|
|
2041
2163
|
|
package/src/server/types.ts
CHANGED
|
@@ -35,6 +35,14 @@ type FetchTransport = (url: string | URL | Request, init?: RequestInit) => Promi
|
|
|
35
35
|
|
|
36
36
|
const MAX_FORWARDED_HEADERS = 32;
|
|
37
37
|
const MAX_FORWARDED_HEADER_BYTES = 8 * 1024;
|
|
38
|
+
// Inbound compatibility boundary for rolling deploys: older owner pods omit
|
|
39
|
+
// top-level retryable. Emitted responses remain strict via
|
|
40
|
+
// OperationErrorResponseSchema.
|
|
41
|
+
const ForwardedOperationErrorResponseSchema = OperationErrorResponseSchema.extend({
|
|
42
|
+
error: OperationErrorResponseSchema.shape.error.extend({
|
|
43
|
+
retryable: z.boolean().optional().default(false),
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
38
46
|
const SENSITIVE_HEADER_NAMES = new Set([
|
|
39
47
|
"authorization",
|
|
40
48
|
"cookie",
|
|
@@ -253,7 +261,7 @@ async function parseForwardedResponse(response: Response): Promise<StatefulOpera
|
|
|
253
261
|
return { output: success.data.data };
|
|
254
262
|
}
|
|
255
263
|
|
|
256
|
-
const error =
|
|
264
|
+
const error = ForwardedOperationErrorResponseSchema.safeParse(body);
|
|
257
265
|
if (error.success) {
|
|
258
266
|
throw new StatefulOwnerForwardingError({
|
|
259
267
|
code: error.data.error.code,
|