@apifuse/provider-sdk 2.2.0-beta.15 → 2.2.0-beta.17
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 +92 -13
- package/CHANGELOG.md +10 -0
- package/dist/config/loader.d.ts +19 -0
- package/dist/config/loader.js +59 -28
- package/dist/define.js +20 -1
- package/dist/error-resolution.d.ts +2 -0
- package/dist/error-resolution.js +90 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/native-egress-policy.d.ts +1 -0
- package/dist/native-egress-policy.js +34 -21
- package/dist/native-ipv4.d.ts +22 -0
- package/dist/native-ipv4.js +98 -0
- package/dist/provider.d.ts +2 -2
- package/dist/provider.js +1 -1
- package/dist/runtime/native-network.d.ts +43 -7
- package/dist/runtime/native-network.js +567 -110
- package/dist/runtime/proxy-nodemaven.d.ts +7 -0
- package/dist/runtime/proxy-nodemaven.js +5 -5
- package/dist/server/serve.js +76 -100
- package/dist/types.d.ts +9 -2
- package/dist/types.js +1 -0
- package/package.json +1 -1
- package/src/config/loader.ts +110 -18
- package/src/define.ts +33 -0
- package/src/error-resolution.ts +91 -0
- package/src/index.ts +6 -0
- package/src/native-egress-policy.ts +39 -33
- package/src/native-ipv4.ts +118 -0
- package/src/provider.ts +6 -0
- package/src/runtime/native-network.ts +734 -131
- package/src/runtime/proxy-nodemaven.ts +13 -5
- package/src/server/serve.ts +118 -98
- package/src/types.ts +11 -2
package/src/config/loader.ts
CHANGED
|
@@ -7,7 +7,10 @@ import { Redis } from "ioredis";
|
|
|
7
7
|
import type { ProviderProxyPolicy, ProviderProxyProvider, TraceConfig } from "../types.js";
|
|
8
8
|
import {
|
|
9
9
|
NODEMAVEN_DEFAULT_PROTOCOL,
|
|
10
|
+
NODEMAVEN_FILTER_ENV,
|
|
10
11
|
NODEMAVEN_MAX_POOL_SIZE,
|
|
12
|
+
NODEMAVEN_PASSWORD_ENV,
|
|
13
|
+
NODEMAVEN_USERNAME_ENV,
|
|
11
14
|
type ProxyProtocol,
|
|
12
15
|
hasNodemavenCredentials,
|
|
13
16
|
nodemavenPoolSize,
|
|
@@ -683,7 +686,7 @@ export async function resolveProxy(
|
|
|
683
686
|
* for smartproxy, and is the only protocol ctx.http (Bun native fetch) supports.
|
|
684
687
|
* Override per call via ProxyResolutionOptions.protocol (harness/tests).
|
|
685
688
|
*/
|
|
686
|
-
const VENDOR_DEFAULT_PROTOCOL: Record<ProxyVendorName, ProxyProtocol
|
|
689
|
+
export const VENDOR_DEFAULT_PROTOCOL: Readonly<Record<ProxyVendorName, ProxyProtocol>> = {
|
|
687
690
|
smartproxy: "http",
|
|
688
691
|
nodemaven: NODEMAVEN_DEFAULT_PROTOCOL,
|
|
689
692
|
};
|
|
@@ -708,21 +711,59 @@ export function assertTunnelingScheme(url: string): void {
|
|
|
708
711
|
}
|
|
709
712
|
}
|
|
710
713
|
|
|
711
|
-
|
|
714
|
+
export type ProxyVendorResolutionContext = {
|
|
715
|
+
readonly protocol: ProxyProtocol;
|
|
716
|
+
readonly poolIndex: number;
|
|
717
|
+
readonly refreshEpoch: number;
|
|
718
|
+
/** Explicit vendor credentials. Omit only on the legacy ambient-env path. */
|
|
719
|
+
readonly credentials?: Readonly<Record<string, string>>;
|
|
720
|
+
/** Disable non-policy env defaults for deterministic injected adapters. */
|
|
721
|
+
readonly ambientDefaults?: boolean;
|
|
722
|
+
/** Disable env-discovered Redis sharing for deterministic injected adapters. */
|
|
723
|
+
readonly sharedCache?: boolean;
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
export async function resolveWithVendor(
|
|
712
727
|
vendor: ProxyVendorName,
|
|
713
728
|
policy: ProviderProxyPolicy,
|
|
714
729
|
options: ProxyResolutionOptions,
|
|
715
|
-
context:
|
|
730
|
+
context: ProxyVendorResolutionContext,
|
|
716
731
|
): Promise<ResolvedProxyConfig> {
|
|
717
732
|
if (vendor === "nodemaven") {
|
|
718
733
|
const startedAt = Date.now();
|
|
734
|
+
const username = (
|
|
735
|
+
context.credentials === undefined
|
|
736
|
+
? process.env[NODEMAVEN_USERNAME_ENV]
|
|
737
|
+
: context.credentials[NODEMAVEN_USERNAME_ENV]
|
|
738
|
+
)?.trim();
|
|
739
|
+
const password = (
|
|
740
|
+
context.credentials === undefined
|
|
741
|
+
? process.env[NODEMAVEN_PASSWORD_ENV]
|
|
742
|
+
: context.credentials[NODEMAVEN_PASSWORD_ENV]
|
|
743
|
+
)?.trim();
|
|
744
|
+
const filter =
|
|
745
|
+
context.credentials === undefined
|
|
746
|
+
? process.env[NODEMAVEN_FILTER_ENV]
|
|
747
|
+
: context.credentials[NODEMAVEN_FILTER_ENV];
|
|
748
|
+
if (!username || !password) {
|
|
749
|
+
throw new ProxyResolutionError(
|
|
750
|
+
"PROXY_ALLOCATION_FAILED",
|
|
751
|
+
`NodeMaven credentials missing: set ${NODEMAVEN_USERNAME_ENV} and ${NODEMAVEN_PASSWORD_ENV}.`,
|
|
752
|
+
{ vendor: "nodemaven" },
|
|
753
|
+
);
|
|
754
|
+
}
|
|
719
755
|
const synthesized = synthesizeNodemavenProxy({
|
|
720
756
|
policy,
|
|
757
|
+
credentials: {
|
|
758
|
+
username,
|
|
759
|
+
password,
|
|
760
|
+
...(filter ? { filter } : {}),
|
|
761
|
+
},
|
|
721
762
|
affinityKey: options.affinityKey,
|
|
722
763
|
protocol: context.protocol,
|
|
723
764
|
poolIndex: context.poolIndex,
|
|
724
765
|
refreshEpoch: context.refreshEpoch,
|
|
725
|
-
country: resolveSmartproxyCountry(policy),
|
|
766
|
+
country: resolveSmartproxyCountry(policy, context.ambientDefaults !== false),
|
|
726
767
|
});
|
|
727
768
|
options.telemetry?.recordProxyResolution({
|
|
728
769
|
provider: "nodemaven",
|
|
@@ -746,7 +787,11 @@ async function resolveWithVendor(
|
|
|
746
787
|
}
|
|
747
788
|
|
|
748
789
|
// smartproxy allocation-style vendor.
|
|
749
|
-
const appKey =
|
|
790
|
+
const appKey = (
|
|
791
|
+
context.credentials === undefined
|
|
792
|
+
? process.env[SMARTPROXY_APP_KEY_ENV]
|
|
793
|
+
: context.credentials[SMARTPROXY_APP_KEY_ENV]
|
|
794
|
+
)?.trim();
|
|
750
795
|
if (!appKey) {
|
|
751
796
|
// Guarded by vendorHasCredentials; treated as a vendor-internal failure.
|
|
752
797
|
throw new ProxyResolutionError(
|
|
@@ -755,13 +800,15 @@ async function resolveWithVendor(
|
|
|
755
800
|
{ vendor: "smartproxy" },
|
|
756
801
|
);
|
|
757
802
|
}
|
|
758
|
-
const lifetimeMinutes = resolveSmartproxyLifetime(policy);
|
|
803
|
+
const lifetimeMinutes = resolveSmartproxyLifetime(policy, context.ambientDefaults !== false);
|
|
759
804
|
const allocated = await allocateSmartproxy(
|
|
760
805
|
policy,
|
|
761
806
|
appKey,
|
|
762
807
|
lifetimeMinutes,
|
|
763
808
|
options.affinityKey,
|
|
764
809
|
context.protocol,
|
|
810
|
+
context.ambientDefaults !== false,
|
|
811
|
+
context.sharedCache !== false,
|
|
765
812
|
);
|
|
766
813
|
options.telemetry?.recordProxyResolution({ ...allocated.telemetry, protocol: context.protocol });
|
|
767
814
|
const poolIndex = selectProxyPoolIndex(allocated.pool.urls.length, context.poolIndex);
|
|
@@ -985,15 +1032,22 @@ export function mapFlatAttempt(
|
|
|
985
1032
|
return { vendorIndex: 0, poolIndex: 0 };
|
|
986
1033
|
}
|
|
987
1034
|
|
|
988
|
-
function resolveSmartproxyCountry(
|
|
1035
|
+
function resolveSmartproxyCountry(
|
|
1036
|
+
policy: ProviderProxyPolicy,
|
|
1037
|
+
ambientDefaults = true,
|
|
1038
|
+
): string | undefined {
|
|
989
1039
|
return (
|
|
990
|
-
policy.geo?.country ??
|
|
1040
|
+
policy.geo?.country ??
|
|
1041
|
+
(ambientDefaults
|
|
1042
|
+
? process.env[DEFAULT_PROXY_COUNTRY_ENV]?.trim().toUpperCase() || undefined
|
|
1043
|
+
: undefined)
|
|
991
1044
|
);
|
|
992
1045
|
}
|
|
993
1046
|
|
|
994
|
-
function resolveSmartproxyLifetime(policy: ProviderProxyPolicy): number {
|
|
1047
|
+
function resolveSmartproxyLifetime(policy: ProviderProxyPolicy, ambientDefaults = true): number {
|
|
995
1048
|
const configuredLifetime =
|
|
996
|
-
policy.session?.lifetimeMinutes ??
|
|
1049
|
+
policy.session?.lifetimeMinutes ??
|
|
1050
|
+
(ambientDefaults ? readPositiveNumberEnv(DEFAULT_PROXY_LIFETIME_ENV, 30) : 30);
|
|
997
1051
|
return Math.min(SMARTPROXY_MAX_LIFETIME_MINUTES, Math.max(1, Math.floor(configuredLifetime)));
|
|
998
1052
|
}
|
|
999
1053
|
|
|
@@ -1024,15 +1078,21 @@ function selectProxyPoolIndex(poolSize: number, attempt = 0): number {
|
|
|
1024
1078
|
|
|
1025
1079
|
function buildSmartproxyCacheKey(
|
|
1026
1080
|
policy: ProviderProxyPolicy,
|
|
1081
|
+
appKey: string,
|
|
1027
1082
|
affinityKey: string | undefined,
|
|
1028
1083
|
lifetimeMinutes: number,
|
|
1029
1084
|
protocol: ProxyProtocol,
|
|
1085
|
+
ambientDefaults = true,
|
|
1030
1086
|
): string {
|
|
1031
1087
|
const poolSize = resolveSmartproxyPoolSize(policy);
|
|
1032
1088
|
return JSON.stringify({
|
|
1033
1089
|
provider: "smartproxy",
|
|
1090
|
+
credentialHash: createHash("sha256")
|
|
1091
|
+
.update("apifuse-smartproxy-credential:v1\0")
|
|
1092
|
+
.update(appKey)
|
|
1093
|
+
.digest("hex"),
|
|
1034
1094
|
protocol,
|
|
1035
|
-
country: resolveSmartproxyCountry(policy),
|
|
1095
|
+
country: resolveSmartproxyCountry(policy, ambientDefaults),
|
|
1036
1096
|
affinity: policy.session?.affinity ?? "request",
|
|
1037
1097
|
affinityKey: (policy.session?.affinity ?? "request") === "request" ? undefined : affinityKey,
|
|
1038
1098
|
lifetimeMinutes,
|
|
@@ -1046,8 +1106,17 @@ async function allocateSmartproxy(
|
|
|
1046
1106
|
lifetimeMinutes: number,
|
|
1047
1107
|
affinityKey: string | undefined,
|
|
1048
1108
|
protocol: ProxyProtocol,
|
|
1109
|
+
ambientDefaults = true,
|
|
1110
|
+
sharedCache = true,
|
|
1049
1111
|
): Promise<SmartproxyAllocationResult> {
|
|
1050
|
-
const cacheKey = buildSmartproxyCacheKey(
|
|
1112
|
+
const cacheKey = buildSmartproxyCacheKey(
|
|
1113
|
+
policy,
|
|
1114
|
+
appKey,
|
|
1115
|
+
affinityKey,
|
|
1116
|
+
lifetimeMinutes,
|
|
1117
|
+
protocol,
|
|
1118
|
+
ambientDefaults,
|
|
1119
|
+
);
|
|
1051
1120
|
const startedAt = Date.now();
|
|
1052
1121
|
const now = startedAt;
|
|
1053
1122
|
const invalidatedUntil = invalidatedProxyKeys.get(cacheKey) ?? 0;
|
|
@@ -1055,7 +1124,15 @@ async function allocateSmartproxy(
|
|
|
1055
1124
|
const cached = proxyCache.get(cacheKey);
|
|
1056
1125
|
if (!skipCached && cached && isFresh(cached, now)) {
|
|
1057
1126
|
if (shouldSoftRefresh(cached, now)) {
|
|
1058
|
-
void refreshSmartproxyPool(
|
|
1127
|
+
void refreshSmartproxyPool(
|
|
1128
|
+
cacheKey,
|
|
1129
|
+
policy,
|
|
1130
|
+
appKey,
|
|
1131
|
+
lifetimeMinutes,
|
|
1132
|
+
protocol,
|
|
1133
|
+
ambientDefaults,
|
|
1134
|
+
sharedCache,
|
|
1135
|
+
);
|
|
1059
1136
|
return {
|
|
1060
1137
|
pool: cached,
|
|
1061
1138
|
telemetry: telemetryForPool(cached, "soft_stale_refresh", startedAt, {
|
|
@@ -1069,7 +1146,7 @@ async function allocateSmartproxy(
|
|
|
1069
1146
|
};
|
|
1070
1147
|
}
|
|
1071
1148
|
|
|
1072
|
-
if (!skipCached) {
|
|
1149
|
+
if (!skipCached && sharedCache) {
|
|
1073
1150
|
const redisResult = await readSmartproxyRedisPool(cacheKey, startedAt);
|
|
1074
1151
|
if (redisResult) return redisResult;
|
|
1075
1152
|
}
|
|
@@ -1092,6 +1169,8 @@ async function allocateSmartproxy(
|
|
|
1092
1169
|
lifetimeMinutes,
|
|
1093
1170
|
startedAt,
|
|
1094
1171
|
protocol,
|
|
1172
|
+
ambientDefaults,
|
|
1173
|
+
sharedCache,
|
|
1095
1174
|
).finally(() => {
|
|
1096
1175
|
proxyInflight.delete(cacheKey);
|
|
1097
1176
|
});
|
|
@@ -1131,6 +1210,8 @@ async function refreshSmartproxyPool(
|
|
|
1131
1210
|
appKey: string,
|
|
1132
1211
|
lifetimeMinutes: number,
|
|
1133
1212
|
protocol: ProxyProtocol,
|
|
1213
|
+
ambientDefaults: boolean,
|
|
1214
|
+
sharedCache: boolean,
|
|
1134
1215
|
): Promise<void> {
|
|
1135
1216
|
try {
|
|
1136
1217
|
await allocateSmartproxyShared(
|
|
@@ -1140,6 +1221,8 @@ async function refreshSmartproxyPool(
|
|
|
1140
1221
|
lifetimeMinutes,
|
|
1141
1222
|
Date.now(),
|
|
1142
1223
|
protocol,
|
|
1224
|
+
ambientDefaults,
|
|
1225
|
+
sharedCache,
|
|
1143
1226
|
{
|
|
1144
1227
|
background: true,
|
|
1145
1228
|
},
|
|
@@ -1156,9 +1239,11 @@ async function allocateSmartproxyShared(
|
|
|
1156
1239
|
lifetimeMinutes: number,
|
|
1157
1240
|
startedAt: number,
|
|
1158
1241
|
protocol: ProxyProtocol,
|
|
1242
|
+
ambientDefaults: boolean,
|
|
1243
|
+
sharedCache: boolean,
|
|
1159
1244
|
options: { background?: boolean } = {},
|
|
1160
1245
|
): Promise<SmartproxyAllocationResult> {
|
|
1161
|
-
const redis = getProxyRedis();
|
|
1246
|
+
const redis = sharedCache ? getProxyRedis() : undefined;
|
|
1162
1247
|
if (!redis || !(await ensureRedisReady(redis))) {
|
|
1163
1248
|
return await allocateAndStoreSmartproxyPool(
|
|
1164
1249
|
cacheKey,
|
|
@@ -1166,7 +1251,7 @@ async function allocateSmartproxyShared(
|
|
|
1166
1251
|
appKey,
|
|
1167
1252
|
lifetimeMinutes,
|
|
1168
1253
|
startedAt,
|
|
1169
|
-
{ cacheStatus: "allocator", protocol },
|
|
1254
|
+
{ cacheStatus: "allocator", protocol, ambientDefaults },
|
|
1170
1255
|
);
|
|
1171
1256
|
}
|
|
1172
1257
|
|
|
@@ -1192,6 +1277,7 @@ async function allocateSmartproxyShared(
|
|
|
1192
1277
|
redis,
|
|
1193
1278
|
poolKey,
|
|
1194
1279
|
protocol,
|
|
1280
|
+
ambientDefaults,
|
|
1195
1281
|
},
|
|
1196
1282
|
);
|
|
1197
1283
|
} finally {
|
|
@@ -1348,6 +1434,7 @@ async function allocateAndStoreSmartproxyPool(
|
|
|
1348
1434
|
redis?: ProxyRedisClient;
|
|
1349
1435
|
poolKey?: string;
|
|
1350
1436
|
protocol: ProxyProtocol;
|
|
1437
|
+
ambientDefaults: boolean;
|
|
1351
1438
|
},
|
|
1352
1439
|
): Promise<SmartproxyAllocationResult> {
|
|
1353
1440
|
const poolSize = resolveSmartproxyPoolSize(policy);
|
|
@@ -1357,6 +1444,7 @@ async function allocateAndStoreSmartproxyPool(
|
|
|
1357
1444
|
lifetimeMinutes,
|
|
1358
1445
|
poolSize,
|
|
1359
1446
|
options.protocol,
|
|
1447
|
+
options.ambientDefaults,
|
|
1360
1448
|
);
|
|
1361
1449
|
const allocatorStartedAt = Date.now();
|
|
1362
1450
|
const allocatorDeadlineAt = allocatorStartedAt + smartproxyAllocatorDeadlineMs();
|
|
@@ -1427,7 +1515,7 @@ async function allocateAndStoreSmartproxyPool(
|
|
|
1427
1515
|
expiresAt: allocatedAt + ttlMs,
|
|
1428
1516
|
diagnostics: {
|
|
1429
1517
|
provider: "smartproxy",
|
|
1430
|
-
country: resolveSmartproxyCountry(policy) ?? "default",
|
|
1518
|
+
country: resolveSmartproxyCountry(policy, options.ambientDefaults) ?? "default",
|
|
1431
1519
|
lifetimeMinutes,
|
|
1432
1520
|
affinity: policy.session?.affinity ?? "request",
|
|
1433
1521
|
rawConnect: true,
|
|
@@ -1590,6 +1678,7 @@ function buildSmartproxyAllocatorUrl(
|
|
|
1590
1678
|
lifetimeMinutes: number,
|
|
1591
1679
|
poolSize: number,
|
|
1592
1680
|
protocol: ProxyProtocol,
|
|
1681
|
+
ambientDefaults = true,
|
|
1593
1682
|
): string {
|
|
1594
1683
|
const params = new URLSearchParams({
|
|
1595
1684
|
app_key: appKey,
|
|
@@ -1600,7 +1689,7 @@ function buildSmartproxyAllocatorUrl(
|
|
|
1600
1689
|
format: "txt",
|
|
1601
1690
|
lb: "\\n",
|
|
1602
1691
|
});
|
|
1603
|
-
const country = resolveSmartproxyCountry(policy);
|
|
1692
|
+
const country = resolveSmartproxyCountry(policy, ambientDefaults);
|
|
1604
1693
|
if (country) {
|
|
1605
1694
|
params.set("cc", country);
|
|
1606
1695
|
}
|
|
@@ -1681,8 +1770,11 @@ function markSmartproxyCacheInvalidated(options: ProxyResolutionOptions = {}): s
|
|
|
1681
1770
|
}
|
|
1682
1771
|
|
|
1683
1772
|
const lifetimeMinutes = resolveSmartproxyLifetime(policy);
|
|
1773
|
+
const appKey = process.env[SMARTPROXY_APP_KEY_ENV]?.trim();
|
|
1774
|
+
if (!appKey) return undefined;
|
|
1684
1775
|
const cacheKey = buildSmartproxyCacheKey(
|
|
1685
1776
|
policy,
|
|
1777
|
+
appKey,
|
|
1686
1778
|
options.affinityKey,
|
|
1687
1779
|
lifetimeMinutes,
|
|
1688
1780
|
options.protocol ?? VENDOR_DEFAULT_PROTOCOL.smartproxy,
|
package/src/define.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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";
|
|
4
5
|
import {
|
|
5
6
|
NativeEgressPolicyValidationError,
|
|
@@ -56,6 +57,7 @@ import {
|
|
|
56
57
|
STREAM_IDLE_TIMEOUT_MS_MIN,
|
|
57
58
|
STREAM_MAX_DURATION_MS_MAX,
|
|
58
59
|
STREAM_MAX_DURATION_MS_MIN,
|
|
60
|
+
VALID_OPERATION_ERROR_STATUSES,
|
|
59
61
|
} from "./types.js";
|
|
60
62
|
|
|
61
63
|
type ProviderImplementationSourceAccess =
|
|
@@ -800,6 +802,36 @@ function validateOperationObservability(
|
|
|
800
802
|
}
|
|
801
803
|
}
|
|
802
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
|
+
|
|
803
835
|
const JSON_TRANSPORT_FIELDS = new Set(["kind"]);
|
|
804
836
|
const SSE_TRANSPORT_FIELDS = new Set([
|
|
805
837
|
"kind",
|
|
@@ -2185,6 +2217,7 @@ export function defineProvider<
|
|
|
2185
2217
|
validateOperationIds(config.id, config.operations);
|
|
2186
2218
|
validateOperationAnnotations(config.id, config.operations);
|
|
2187
2219
|
validateOperationObservability(config.id, config.operations);
|
|
2220
|
+
validateOperationErrorCodes(config.id, config.operations);
|
|
2188
2221
|
validateOperationTransports(config.id, config.operations);
|
|
2189
2222
|
validateOperationContracts(config.id, config.operations);
|
|
2190
2223
|
validateToolRouterMetadata(config.id, config.operations);
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// This set suppresses the unregistered-provider-error-code signal for codes
|
|
2
|
+
// intentionally emitted by SDK paths. It is not the complete authority for
|
|
3
|
+
// runtime error resolution: branded errors and additional canonical SDK codes
|
|
4
|
+
// must also remain immune to provider-declared status/retryability overrides.
|
|
5
|
+
export const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
|
|
6
|
+
"MISSING_SECRET",
|
|
7
|
+
"AUTH_PROMPT_UNAVAILABLE",
|
|
8
|
+
"BROWSER_CDP_POOL_REQUIRED",
|
|
9
|
+
"BROWSER_RUNTIME_UNSUPPORTED",
|
|
10
|
+
"STEALTH_RUNTIME_UNSUPPORTED",
|
|
11
|
+
"SSE_EVENT_UNDECLARED",
|
|
12
|
+
"STREAM_EVENT_TOO_LARGE",
|
|
13
|
+
"STREAM_CHUNK_TOO_LARGE",
|
|
14
|
+
"SSE_RESULT_UNSUPPORTED",
|
|
15
|
+
"STREAM_RESULT_UNSUPPORTED",
|
|
16
|
+
"AUTH_FLOW_NOT_CONFIGURED",
|
|
17
|
+
"refresh_not_supported",
|
|
18
|
+
"RUNTIME_UNSUPPORTED",
|
|
19
|
+
"PROVIDER_STATE_UNSUPPORTED",
|
|
20
|
+
"CHOICE_TOKEN_MASTER_SECRET_NOT_CONFIGURED",
|
|
21
|
+
"CHOICE_STATE_PAYLOAD_TOO_LARGE",
|
|
22
|
+
"CHOICE_STATE_UNAVAILABLE",
|
|
23
|
+
"CHOICE_CONTEXT_REQUIRED",
|
|
24
|
+
"unsupported_stealth_cookie_store_version",
|
|
25
|
+
"provider_secret_error",
|
|
26
|
+
"credential_key_error",
|
|
27
|
+
"credential_mode_error",
|
|
28
|
+
"flow_expired",
|
|
29
|
+
"turn_validation_error",
|
|
30
|
+
"context_access_error",
|
|
31
|
+
"UNSUPPORTED_STT_OPTION",
|
|
32
|
+
"INVALID_STT_AUDIO",
|
|
33
|
+
"STT_AUDIO_TOO_LARGE",
|
|
34
|
+
"STT_UPSTREAM_FAILED",
|
|
35
|
+
"INVALID_STT_VERIFICATION_CODE_OPTIONS",
|
|
36
|
+
"NO_CODE_FOUND",
|
|
37
|
+
"AMBIGUOUS_CODE",
|
|
38
|
+
"retry_invalid_policy",
|
|
39
|
+
"retry_unsafe_method",
|
|
40
|
+
"stealth_cookie_store_serialize_failed",
|
|
41
|
+
"response_too_large",
|
|
42
|
+
"transport_stream_unavailable",
|
|
43
|
+
"transport_invalid_method",
|
|
44
|
+
"http_transport_override_unsupported",
|
|
45
|
+
"http_redirect_policy_invalid",
|
|
46
|
+
"http_redirect_stopped",
|
|
47
|
+
"http_redirect_max_hops",
|
|
48
|
+
"http_redirect_missing_location",
|
|
49
|
+
"http_redirect_loop",
|
|
50
|
+
"transport_invalid_url",
|
|
51
|
+
"retry_exhausted",
|
|
52
|
+
"auth_abort_unsafe_data",
|
|
53
|
+
"credentials_auth_missing_credential_keys",
|
|
54
|
+
"credentials_auth_missing_credential",
|
|
55
|
+
"credentials_auth_invalid_login_result",
|
|
56
|
+
"credentials_auth_unknown_challenge",
|
|
57
|
+
"credentials_auth_unknown_pending_challenge",
|
|
58
|
+
"STATEFUL_FORWARDING_NOT_CONFIGURED",
|
|
59
|
+
"STATEFUL_FORWARDING_SIGNATURE_MISSING",
|
|
60
|
+
"STATEFUL_FORWARDING_NONCE_INVALID",
|
|
61
|
+
"STATEFUL_FORWARDING_TIMESTAMP_INVALID",
|
|
62
|
+
"STATEFUL_FORWARDING_SIGNATURE_INVALID",
|
|
63
|
+
"STATEFUL_FORWARDING_REPLAY_DETECTED",
|
|
64
|
+
"STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
|
|
65
|
+
"STATEFUL_FORWARDING_ENVELOPE_INVALID",
|
|
66
|
+
"STATEFUL_FORWARDING_PROVIDER_MISMATCH",
|
|
67
|
+
"STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
|
|
68
|
+
"STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
|
|
69
|
+
"STATEFUL_FORWARDING_REQUEST_FAILED",
|
|
70
|
+
"STATEFUL_FORWARDING_CONTEXT_MISSING",
|
|
71
|
+
"STATEFUL_FORWARDING_BAD_RESPONSE",
|
|
72
|
+
"STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
|
|
73
|
+
"STATEFUL_FILE_FORWARDING_UNSUPPORTED",
|
|
74
|
+
"STATEFUL_CONTROL_PLANE_OPERATION_AMBIGUOUS",
|
|
75
|
+
"STATEFUL_CONTROL_PLANE_REQUEST_FAILED",
|
|
76
|
+
"STATEFUL_CONTROL_PLANE_HTTP_ERROR",
|
|
77
|
+
"STATEFUL_CONTROL_PLANE_INVALID_RESPONSE",
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
// Complete code authority for provider-declared runtime resolution. Keep this
|
|
81
|
+
// separate from signal suppression: declarations may document these codes, but
|
|
82
|
+
// their status and retryability can never override the SDK's canonical result.
|
|
83
|
+
export const SDK_RUNTIME_OWNED_ERROR_CODES = new Set([
|
|
84
|
+
...SDK_OWNED_PROVIDER_ERROR_CODES,
|
|
85
|
+
"reauth_required",
|
|
86
|
+
"STT_UNAVAILABLE",
|
|
87
|
+
"UNSUPPORTED_STT_BACKEND",
|
|
88
|
+
"OUTPUT_VALIDATION_FAILED",
|
|
89
|
+
"NOT_FOUND",
|
|
90
|
+
"not_found",
|
|
91
|
+
]);
|
package/src/index.ts
CHANGED
|
@@ -72,6 +72,7 @@ export { executeOperation } from "./runtime/executor.js";
|
|
|
72
72
|
export { createHttpClient } from "./runtime/http.js";
|
|
73
73
|
export {
|
|
74
74
|
createNativeNetworkClient,
|
|
75
|
+
createEnvVendorCredentialResolver,
|
|
75
76
|
deriveNativeCredentialAffinityKey,
|
|
76
77
|
NativeEgressGrantExpiredError,
|
|
77
78
|
NativeEgressNotDeclaredError,
|
|
@@ -81,10 +82,14 @@ export {
|
|
|
81
82
|
resolveNativeGatewayProxy,
|
|
82
83
|
type NativeGatewayProxy,
|
|
83
84
|
type NativeGatewayProxyResolutionInput,
|
|
85
|
+
type NativeGatewayProxySkipReason,
|
|
84
86
|
type NativeGatewayProxySynthesizer,
|
|
87
|
+
type NativeGatewayProxySynthesisResult,
|
|
85
88
|
type NativeGatewayProxySynthesisInput,
|
|
86
89
|
type NativeNetworkClientOptions,
|
|
87
90
|
type NativeNetworkErrorCode,
|
|
91
|
+
type VendorCredentialLookup,
|
|
92
|
+
type VendorCredentialResolver,
|
|
88
93
|
} from "./runtime/native-network.js";
|
|
89
94
|
export type { Insight, InsightSeverity } from "./runtime/insights.js";
|
|
90
95
|
export { generateInsights } from "./runtime/insights.js";
|
|
@@ -230,6 +235,7 @@ export type {
|
|
|
230
235
|
OperationDeprecationMetadata,
|
|
231
236
|
OperationDocMeta,
|
|
232
237
|
OperationErrorCode,
|
|
238
|
+
ProviderErrorStatus,
|
|
233
239
|
OperationHandlerResult,
|
|
234
240
|
OperationInputExample,
|
|
235
241
|
OperationLifecycle,
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
NativeTcpPortRange,
|
|
6
6
|
NativeTcpTlsMode,
|
|
7
7
|
} from "./types.js";
|
|
8
|
+
import { canonicalizeEgressHost, parseIpv4Cidr } from "./native-ipv4.js";
|
|
8
9
|
|
|
9
10
|
type NativeNetworkDeclaration = NonNullable<NativeProviderConfig["network"]>;
|
|
10
11
|
|
|
@@ -26,6 +27,7 @@ const NATIVE_DYNAMIC_TCP_RULE_FIELD_RECORD = {
|
|
|
26
27
|
sourcePorts: true,
|
|
27
28
|
sourcePortRanges: true,
|
|
28
29
|
targetHostSuffixes: true,
|
|
30
|
+
targetIpv4Cidrs: true,
|
|
29
31
|
targetPorts: true,
|
|
30
32
|
targetPortRanges: true,
|
|
31
33
|
tls: true,
|
|
@@ -55,6 +57,7 @@ export type DynamicEgressRuleSnapshot = {
|
|
|
55
57
|
readonly sourcePorts: readonly number[];
|
|
56
58
|
readonly sourcePortRanges: readonly NativeTcpPortRange[];
|
|
57
59
|
readonly targetHostSuffixes: readonly string[];
|
|
60
|
+
readonly targetIpv4Cidrs: readonly string[];
|
|
58
61
|
readonly targetPorts: readonly number[];
|
|
59
62
|
readonly targetPortRanges: readonly NativeTcpPortRange[];
|
|
60
63
|
readonly tls: NativeTcpTlsMode;
|
|
@@ -117,28 +120,12 @@ function dataArray(value: unknown, fieldPath: string): readonly unknown[] {
|
|
|
117
120
|
return result;
|
|
118
121
|
}
|
|
119
122
|
|
|
120
|
-
function hasControlCharacter(value: string): boolean {
|
|
121
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
122
|
-
const code = value.charCodeAt(index);
|
|
123
|
-
if (code <= 31 || code === 127) return true;
|
|
124
|
-
}
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
123
|
function host(value: unknown, fieldPath: string, suffix = false): string {
|
|
129
|
-
if (
|
|
130
|
-
typeof value !== "string" ||
|
|
131
|
-
!value.trim() ||
|
|
132
|
-
hasControlCharacter(value) ||
|
|
133
|
-
/\s/.test(value) ||
|
|
134
|
-
value.includes("://")
|
|
135
|
-
)
|
|
136
|
-
fail(`${fieldPath} must be a non-empty hostname`);
|
|
137
|
-
if (value.includes("*"))
|
|
124
|
+
if (typeof value === "string" && value.includes("*"))
|
|
138
125
|
fail(`${fieldPath} must be an exact ${suffix ? "DNS suffix" : "hostname"}, not a wildcard`);
|
|
139
|
-
const
|
|
140
|
-
if (!
|
|
141
|
-
return
|
|
126
|
+
const canonical = canonicalizeEgressHost(value);
|
|
127
|
+
if (!canonical.ok) fail(`${fieldPath} must be a non-empty hostname`);
|
|
128
|
+
return canonical.host;
|
|
142
129
|
}
|
|
143
130
|
|
|
144
131
|
function port(value: unknown, fieldPath: string): number {
|
|
@@ -157,6 +144,24 @@ function hostSuffixes(value: unknown, fieldPath: string): readonly string[] {
|
|
|
157
144
|
);
|
|
158
145
|
}
|
|
159
146
|
|
|
147
|
+
function ipv4Cidrs(value: unknown, fieldPath: string): readonly string[] {
|
|
148
|
+
const seen = new Set<string>();
|
|
149
|
+
return dataArray(value, fieldPath).map((value, index) => {
|
|
150
|
+
const cidrPath = `${fieldPath}[${index}]`;
|
|
151
|
+
if (typeof value !== "string") fail(`${cidrPath} must be an IPv4 CIDR in a.b.c.d/nn form`);
|
|
152
|
+
const parsed = parseIpv4Cidr(value);
|
|
153
|
+
if (!parsed.ok) {
|
|
154
|
+
if (parsed.reason === "non-canonical-network")
|
|
155
|
+
fail(`${cidrPath} must use the canonical network address with no host bits set`);
|
|
156
|
+
fail(`${cidrPath} must be an IPv4 CIDR in a.b.c.d/nn form`);
|
|
157
|
+
}
|
|
158
|
+
const duplicateKey = `${parsed.network}/${parsed.prefix}`;
|
|
159
|
+
if (seen.has(duplicateKey)) fail(`${fieldPath} must not contain duplicate CIDRs`);
|
|
160
|
+
seen.add(duplicateKey);
|
|
161
|
+
return value;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
160
165
|
function ranges(value: unknown, fieldPath: string): readonly NativeTcpPortRange[] {
|
|
161
166
|
return dataArray(value, fieldPath).map((value, index) => {
|
|
162
167
|
const rangePath = `${fieldPath}[${index}]`;
|
|
@@ -212,9 +217,7 @@ export function parseNativeEgressPolicy(value: unknown): NativeEgressPolicySnaps
|
|
|
212
217
|
? []
|
|
213
218
|
: hostSuffixes(rule.sourceHostSuffixes, `${fieldPath}.sourceHostSuffixes`);
|
|
214
219
|
if (sourceHost === undefined && sourceHostSuffixes.length === 0)
|
|
215
|
-
fail(
|
|
216
|
-
`${fieldPath} must declare sourceHost or a non-empty sourceHostSuffixes list`,
|
|
217
|
-
);
|
|
220
|
+
fail(`${fieldPath} must declare sourceHost or a non-empty sourceHostSuffixes list`);
|
|
218
221
|
const sourcePorts =
|
|
219
222
|
rule.sourcePorts === undefined
|
|
220
223
|
? []
|
|
@@ -224,15 +227,19 @@ export function parseNativeEgressPolicy(value: unknown): NativeEgressPolicySnaps
|
|
|
224
227
|
? []
|
|
225
228
|
: ranges(rule.sourcePortRanges, `${fieldPath}.sourcePortRanges`);
|
|
226
229
|
if (sourcePorts.length === 0 && sourcePortRanges.length === 0)
|
|
230
|
+
fail(`${fieldPath} must declare a non-empty sourcePorts or sourcePortRanges list`);
|
|
231
|
+
const targetHostSuffixes =
|
|
232
|
+
rule.targetHostSuffixes === undefined
|
|
233
|
+
? []
|
|
234
|
+
: hostSuffixes(rule.targetHostSuffixes, `${fieldPath}.targetHostSuffixes`);
|
|
235
|
+
const targetIpv4Cidrs =
|
|
236
|
+
rule.targetIpv4Cidrs === undefined
|
|
237
|
+
? []
|
|
238
|
+
: ipv4Cidrs(rule.targetIpv4Cidrs, `${fieldPath}.targetIpv4Cidrs`);
|
|
239
|
+
if (targetHostSuffixes.length === 0 && targetIpv4Cidrs.length === 0)
|
|
227
240
|
fail(
|
|
228
|
-
`${fieldPath} must declare a non-empty
|
|
241
|
+
`${fieldPath} must declare a non-empty targetHostSuffixes or targetIpv4Cidrs list`,
|
|
229
242
|
);
|
|
230
|
-
const targetHostSuffixes = hostSuffixes(
|
|
231
|
-
rule.targetHostSuffixes,
|
|
232
|
-
`${fieldPath}.targetHostSuffixes`,
|
|
233
|
-
);
|
|
234
|
-
if (targetHostSuffixes.length === 0)
|
|
235
|
-
fail(`${fieldPath}.targetHostSuffixes must not be empty`);
|
|
236
243
|
const targetPorts =
|
|
237
244
|
rule.targetPorts === undefined
|
|
238
245
|
? []
|
|
@@ -242,15 +249,14 @@ export function parseNativeEgressPolicy(value: unknown): NativeEgressPolicySnaps
|
|
|
242
249
|
? []
|
|
243
250
|
: ranges(rule.targetPortRanges, `${fieldPath}.targetPortRanges`);
|
|
244
251
|
if (targetPorts.length === 0 && targetPortRanges.length === 0)
|
|
245
|
-
fail(
|
|
246
|
-
`${fieldPath} must declare a non-empty targetPorts or targetPortRanges list`,
|
|
247
|
-
);
|
|
252
|
+
fail(`${fieldPath} must declare a non-empty targetPorts or targetPortRanges list`);
|
|
248
253
|
return {
|
|
249
254
|
...(sourceHost === undefined ? {} : { sourceHost }),
|
|
250
255
|
sourceHostSuffixes,
|
|
251
256
|
sourcePorts,
|
|
252
257
|
sourcePortRanges,
|
|
253
258
|
targetHostSuffixes,
|
|
259
|
+
targetIpv4Cidrs,
|
|
254
260
|
targetPorts,
|
|
255
261
|
targetPortRanges,
|
|
256
262
|
tls: tls(rule.tls, `${fieldPath}.tls`),
|