@apifuse/provider-sdk 2.2.0-beta.14 → 2.2.0-beta.16

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