@apifuse/provider-sdk 2.2.0-beta.13 → 2.2.0-beta.15

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