@apifuse/provider-sdk 2.2.0-beta.5 → 2.2.0-beta.51
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 +537 -33
- package/CHANGELOG.md +205 -1
- package/README.md +83 -22
- package/SUBMISSION.md +2 -2
- package/bin/apifuse-check.ts +165 -11
- package/bin/apifuse-dev.ts +57 -13
- package/bin/apifuse-migrate-operation-declaration.ts +55 -0
- package/bin/apifuse-migrate-shape.ts +202 -0
- package/bin/apifuse-pack-check.ts +22 -2
- package/bin/apifuse-pack-smoke.ts +57 -2
- package/bin/apifuse-pack-types.ts +357 -38
- package/bin/apifuse-perf.ts +14 -13
- package/bin/apifuse-record.ts +709 -72
- package/bin/apifuse-submit-check.ts +2344 -324
- package/bin/apifuse-sync-assets.ts +117 -0
- package/bin/submit-check-delimited-text.ts +50 -0
- package/dist/auth-turn/index.d.ts +3 -3
- package/dist/auth-turn/index.js +1 -1
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +67 -0
- package/dist/ceremonies/index.d.ts +16 -0
- package/dist/ceremonies/index.js +141 -36
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +27 -0
- package/dist/cli/create.d.ts +4 -1
- package/dist/cli/create.js +42 -37
- package/dist/cli/migrate-operation-declaration.d.ts +59 -0
- package/dist/cli/migrate-operation-declaration.js +1249 -0
- package/dist/cli/migrate-operation-shape.d.ts +44 -0
- package/dist/cli/migrate-operation-shape.js +113 -0
- package/dist/cli/migrate-provider-shape.d.ts +52 -0
- package/dist/cli/migrate-provider-shape.js +578 -0
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
- package/dist/cli/templates/provider/README.md.tpl +5 -5
- package/dist/cli/templates/provider/index.ts.tpl +6 -3
- package/dist/cli/templates/provider/operations/ping.ts.tpl +4 -1
- package/dist/cli/templates/provider/provider.json.tpl +6 -0
- package/dist/config/loader.d.ts +179 -16
- package/dist/config/loader.js +435 -127
- package/dist/contract-serialization.js +4 -8
- package/dist/contract-types.d.ts +12 -5
- package/dist/contract.js +24 -10
- package/dist/declaration-validation.d.ts +33 -0
- package/dist/declaration-validation.js +266 -0
- package/dist/define.d.ts +66 -29
- package/dist/define.js +778 -69
- package/dist/dev.d.ts +3 -0
- package/dist/dev.js +1 -1
- package/dist/engine.d.ts +78 -0
- package/dist/engine.js +133 -0
- package/dist/error-observability.d.ts +7 -0
- package/dist/error-observability.js +61 -0
- package/dist/error-resolution.d.ts +4 -0
- package/dist/error-resolution.js +122 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.js +40 -0
- package/dist/fixture-sanitization.d.ts +28 -0
- package/dist/fixture-sanitization.js +227 -0
- package/dist/health-scenario.d.ts +1842 -0
- package/dist/health-scenario.js +624 -0
- package/dist/index.d.ts +21 -10
- package/dist/index.js +12 -7
- package/dist/lint.d.ts +23 -11
- package/dist/lint.js +572 -69
- package/dist/native-address.d.ts +43 -0
- package/dist/native-address.js +281 -0
- package/dist/native-egress-policy.d.ts +31 -0
- package/dist/native-egress-policy.js +288 -0
- package/dist/observability.d.ts +5 -2
- package/dist/observability.js +48 -1
- package/dist/provider.d.ts +10 -2
- package/dist/provider.js +4 -1
- package/dist/runtime/auth-flow.d.ts +5 -1
- package/dist/runtime/auth-flow.js +6 -0
- package/dist/runtime/browser.d.ts +1 -0
- package/dist/runtime/browser.js +492 -49
- package/dist/runtime/cache.d.ts +1 -0
- package/dist/runtime/cache.js +169 -15
- package/dist/runtime/choice-wordlist.d.ts +9 -0
- package/dist/runtime/choice-wordlist.js +138 -0
- package/dist/runtime/choice.d.ts +13 -1
- package/dist/runtime/choice.js +490 -102
- package/dist/runtime/chrome149-header-order.d.ts +58 -0
- package/dist/runtime/chrome149-header-order.js +289 -0
- package/dist/runtime/env.js +12 -0
- package/dist/runtime/executor.d.ts +3 -2
- package/dist/runtime/executor.js +26 -25
- package/dist/runtime/http.d.ts +1 -0
- package/dist/runtime/http.js +515 -53
- package/dist/runtime/insights.js +2 -2
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +366 -8
- package/dist/runtime/native-network-errors.d.ts +33 -0
- package/dist/runtime/native-network-errors.js +69 -0
- package/dist/runtime/native-network.d.ts +96 -0
- package/dist/runtime/native-network.js +1232 -0
- package/dist/runtime/ocr.d.ts +29 -0
- package/dist/runtime/ocr.js +440 -0
- package/dist/runtime/otlp.d.ts +71 -2
- package/dist/runtime/otlp.js +397 -16
- package/dist/runtime/proxy-errors.js +6 -2
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +146 -0
- package/dist/runtime/proxy-telemetry.d.ts +80 -1
- package/dist/runtime/proxy-telemetry.js +154 -47
- package/dist/runtime/redirects.d.ts +29 -0
- package/dist/runtime/redirects.js +36 -0
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +4 -2
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/resolver-config.d.ts +6 -0
- package/dist/runtime/resolver-config.js +6 -0
- package/dist/runtime/resolver-public.d.ts +1 -0
- package/dist/runtime/resolver-public.js +1 -0
- package/dist/runtime/resolver-shared.d.ts +14 -0
- package/dist/runtime/resolver-shared.js +12 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
- package/dist/runtime/resolver-vendors/bindings.js +40 -0
- package/dist/runtime/resolver-vendors/browser.d.ts +22 -0
- package/dist/runtime/resolver-vendors/browser.js +377 -0
- package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
- package/dist/runtime/resolver-vendors/capsolver.js +531 -0
- package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
- package/dist/runtime/resolver-vendors/hosts.js +33 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
- package/dist/runtime/resolver-vendors/twocaptcha.js +408 -0
- package/dist/runtime/resolver-vendors/types.d.ts +94 -0
- package/dist/runtime/resolver-vendors/types.js +96 -0
- package/dist/runtime/resolver.d.ts +72 -0
- package/dist/runtime/resolver.js +771 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +277 -71
- package/dist/runtime/stealth-cookies.d.ts +20 -0
- package/dist/runtime/stealth-cookies.js +111 -0
- package/dist/runtime/stealth.d.ts +41 -5
- package/dist/runtime/stealth.js +997 -291
- package/dist/runtime/stt.js +1 -12
- package/dist/runtime/timeout.d.ts +5 -0
- package/dist/runtime/timeout.js +12 -0
- package/dist/runtime/trace-config.d.ts +12 -0
- package/dist/runtime/trace-config.js +62 -0
- package/dist/runtime/trace.d.ts +5 -0
- package/dist/runtime/trace.js +43 -10
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/error-observability.d.ts +1 -0
- package/dist/server/error-observability.js +1 -0
- package/dist/server/index.d.ts +5 -2
- package/dist/server/index.js +2 -2
- package/dist/server/self-test-input-tokens.d.ts +2 -1
- package/dist/server/self-test-input-tokens.js +18 -14
- package/dist/server/self-test.d.ts +14 -3
- package/dist/server/self-test.js +129 -58
- package/dist/server/serve-implementation.d.ts +230 -0
- package/dist/server/serve-implementation.js +2265 -0
- package/dist/server/serve.d.ts +1 -70
- package/dist/server/serve.js +1 -1131
- package/dist/server/trace-output.d.ts +6 -0
- package/dist/server/trace-output.js +79 -0
- package/dist/server/types.d.ts +30 -5
- package/dist/server/types.js +13 -1
- package/dist/stateful/errors.d.ts +19 -0
- package/dist/stateful/errors.js +24 -0
- package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
- package/dist/stateful/http-provider-event-emitter.js +237 -0
- package/dist/stateful/http-session-owner-registry.d.ts +44 -0
- package/dist/stateful/http-session-owner-registry.js +210 -0
- package/dist/stateful/index.d.ts +18 -0
- package/dist/stateful/index.js +18 -0
- package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
- package/dist/stateful/provider-event-delivery-failures.js +43 -0
- package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
- package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
- package/dist/stateful/provider-event-pipeline.d.ts +50 -0
- package/dist/stateful/provider-event-pipeline.js +1 -0
- package/dist/stateful/provider-events.d.ts +101 -0
- package/dist/stateful/provider-events.js +289 -0
- package/dist/stateful/session-key.d.ts +15 -0
- package/dist/stateful/session-key.js +86 -0
- package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
- package/dist/stateful/stateful-provider-adapter-context.js +42 -0
- package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
- package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
- package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
- package/dist/stateful/stateful-provider-adapter.js +287 -0
- package/dist/stateful/stateful-provider-observability.d.ts +62 -0
- package/dist/stateful/stateful-provider-observability.js +161 -0
- package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
- package/dist/stateful/stateful-provider-runtime-context.js +60 -0
- package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
- package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
- package/dist/stateful/stateful-provider-session-routing.js +345 -0
- package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
- package/dist/stateful/stateful-provider-session-runtime.js +245 -0
- package/dist/stateful-signing.d.ts +18 -0
- package/dist/stateful-signing.js +27 -0
- package/dist/stealth/profiles.d.ts +9 -4
- package/dist/stealth/profiles.js +160 -211
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.js +7 -1
- package/dist/testing/index.d.ts +2 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +489 -21
- package/dist/trace-sanitization.d.ts +5 -0
- package/dist/trace-sanitization.js +45 -0
- package/dist/types.d.ts +673 -131
- package/dist/types.js +1 -0
- package/package.json +45 -5
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +118 -0
- package/src/ceremonies/index.ts +189 -46
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +10 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +8 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-hoisted-array-spread.ts.txt +15 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-imported-array-spread.ts.txt +9 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +5 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-mixed-array-spread.ts.txt +24 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +16 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +31 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +21 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +14 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +15 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +4 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +12 -0
- package/src/cli/commands.ts +33 -0
- package/src/cli/create.ts +50 -37
- package/src/cli/migrate-operation-declaration.ts +1751 -0
- package/src/cli/migrate-operation-shape.ts +184 -0
- package/src/cli/migrate-provider-shape.ts +772 -0
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/cli/templates/provider/Dockerfile.tpl +1 -1
- package/src/cli/templates/provider/README.md.tpl +5 -5
- package/src/cli/templates/provider/index.ts.tpl +6 -3
- package/src/cli/templates/provider/operations/ping.ts.tpl +4 -1
- package/src/cli/templates/provider/provider.json.tpl +6 -0
- package/src/config/loader.ts +690 -163
- package/src/contract-serialization.ts +5 -7
- package/src/contract-types.ts +12 -5
- package/src/contract.ts +24 -10
- package/src/declaration-validation.ts +330 -0
- package/src/define.ts +1048 -131
- package/src/dev.ts +4 -1
- package/src/engine.ts +279 -0
- package/src/error-observability.ts +64 -0
- package/src/error-resolution.ts +127 -0
- package/src/errors.ts +68 -0
- package/src/fixture-sanitization.ts +264 -0
- package/src/health-scenario.ts +875 -0
- package/src/index.ts +220 -13
- package/src/lint.ts +698 -90
- package/src/native-address.ts +340 -0
- package/src/native-egress-policy.ts +358 -0
- package/src/observability.ts +51 -1
- package/src/provider.ts +161 -3
- package/src/runtime/auth-flow.ts +12 -0
- package/src/runtime/browser.ts +661 -63
- package/src/runtime/cache.ts +189 -14
- package/src/runtime/choice-wordlist.ts +145 -0
- package/src/runtime/choice.ts +631 -120
- package/src/runtime/chrome149-header-order.ts +330 -0
- package/src/runtime/env.ts +13 -0
- package/src/runtime/executor.ts +43 -31
- package/src/runtime/http.ts +641 -61
- package/src/runtime/insights.ts +2 -2
- package/src/runtime/instrumentation.ts +520 -15
- package/src/runtime/native-network-errors.ts +99 -0
- package/src/runtime/native-network.ts +1605 -0
- package/src/runtime/ocr.ts +523 -0
- package/src/runtime/otlp.ts +467 -21
- package/src/runtime/proxy-errors.ts +12 -4
- package/src/runtime/proxy-nodemaven.ts +221 -0
- package/src/runtime/proxy-telemetry.ts +244 -75
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +7 -2
- package/src/runtime/request-options.ts +680 -1
- package/src/runtime/resolver-config.ts +6 -0
- package/src/runtime/resolver-public.ts +23 -0
- package/src/runtime/resolver-shared.ts +33 -0
- package/src/runtime/resolver-vendors/bindings.ts +56 -0
- package/src/runtime/resolver-vendors/browser.ts +533 -0
- package/src/runtime/resolver-vendors/capsolver.ts +707 -0
- package/src/runtime/resolver-vendors/hosts.ts +38 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +540 -0
- package/src/runtime/resolver-vendors/types.ts +212 -0
- package/src/runtime/resolver.ts +1157 -0
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +394 -77
- package/src/runtime/stealth-cookies.ts +132 -0
- package/src/runtime/stealth.ts +1280 -336
- package/src/runtime/stt.ts +1 -19
- package/src/runtime/timeout.ts +18 -0
- package/src/runtime/trace-config.ts +78 -0
- package/src/runtime/trace.ts +57 -17
- package/src/serve.ts +6 -1
- package/src/server/error-observability.ts +1 -0
- package/src/server/index.ts +34 -2
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test.ts +191 -68
- package/src/server/serve-implementation.ts +3378 -0
- package/src/server/serve.ts +1 -1632
- package/src/server/trace-output.ts +129 -0
- package/src/server/types.ts +13 -1
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +35 -0
- package/src/stateful/http-provider-event-emitter.ts +314 -0
- package/src/stateful/http-session-owner-registry.ts +306 -0
- package/src/stateful/index.ts +18 -0
- package/src/stateful/provider-event-delivery-failures.ts +80 -0
- package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
- package/src/stateful/provider-event-pipeline.ts +61 -0
- package/src/stateful/provider-events.ts +462 -0
- package/src/stateful/session-key.ts +111 -0
- package/src/stateful/stateful-provider-adapter-context.ts +59 -0
- package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
- package/src/stateful/stateful-provider-adapter.ts +562 -0
- package/src/stateful/stateful-provider-observability.ts +261 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
- package/src/stateful/stateful-provider-runtime-context.ts +92 -0
- package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
- package/src/stateful/stateful-provider-session-routing.ts +546 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +202 -231
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +8 -1
- package/src/testing/index.ts +10 -1
- package/src/testing/run.ts +658 -15
- package/src/trace-sanitization.ts +63 -0
- package/src/types.ts +788 -174
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/dist/runtime/otlp.js
CHANGED
|
@@ -1,3 +1,183 @@
|
|
|
1
|
+
import { AsyncResource } from "node:async_hooks";
|
|
2
|
+
export const OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT";
|
|
3
|
+
export const OTEL_EXPORTER_OTLP_ENDPOINT = "OTEL_EXPORTER_OTLP_ENDPOINT";
|
|
4
|
+
export const OTEL_EXPORTER_OTLP_TRACES_HEADERS = "OTEL_EXPORTER_OTLP_TRACES_HEADERS";
|
|
5
|
+
export const OTEL_EXPORTER_OTLP_HEADERS = "OTEL_EXPORTER_OTLP_HEADERS";
|
|
6
|
+
export const OTEL_SERVICE_NAME = "OTEL_SERVICE_NAME";
|
|
7
|
+
export const OTEL_RESOURCE_ATTRIBUTES = "OTEL_RESOURCE_ATTRIBUTES";
|
|
8
|
+
const OTLP_HTTP_PROTOCOLS = new Set(["http:", "https:"]);
|
|
9
|
+
const INVALID_ENDPOINT_URL = "is not an absolute http(s) URL";
|
|
10
|
+
const INVALID_ENDPOINT_CREDENTIALS = `embeds credentials in the URL; send them through ${OTEL_EXPORTER_OTLP_HEADERS} instead`;
|
|
11
|
+
const INVALID_HEADERS = "contains an HTTP header name or value that cannot be sent";
|
|
12
|
+
const EXPLICIT_ENDPOINT_SOURCE = "the configured OTLP endpoint";
|
|
13
|
+
const EXPLICIT_HEADERS_SOURCE = "the configured OTLP headers";
|
|
14
|
+
/** Only an unset or empty variable is absent (OTel env rules); whitespace is a value and is validated as one. */
|
|
15
|
+
function presentValue(value) {
|
|
16
|
+
return value === undefined || value === "" ? undefined : value;
|
|
17
|
+
}
|
|
18
|
+
function parseHttpUrl(value) {
|
|
19
|
+
let url;
|
|
20
|
+
try {
|
|
21
|
+
url = new URL(value);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return { reason: INVALID_ENDPOINT_URL };
|
|
25
|
+
}
|
|
26
|
+
if (!OTLP_HTTP_PROTOCOLS.has(url.protocol))
|
|
27
|
+
return { reason: INVALID_ENDPOINT_URL };
|
|
28
|
+
if (url.username || url.password)
|
|
29
|
+
return { reason: INVALID_ENDPOINT_CREDENTIALS };
|
|
30
|
+
return { url };
|
|
31
|
+
}
|
|
32
|
+
/** fetch() rejects malformed header names/values per request; catch that once at resolve time instead. */
|
|
33
|
+
function headersAreSendable(headers) {
|
|
34
|
+
try {
|
|
35
|
+
new Headers(headers);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Appends the traces signal path to a base URL, keeping every configured path byte and adding only the separator. */
|
|
43
|
+
function appendTracesPath(base) {
|
|
44
|
+
const endpoint = new URL(base.toString());
|
|
45
|
+
const separator = endpoint.pathname.endsWith("/") ? "" : "/";
|
|
46
|
+
endpoint.pathname = `${endpoint.pathname}${separator}v1/traces`;
|
|
47
|
+
return endpoint.toString();
|
|
48
|
+
}
|
|
49
|
+
/** HTTP header names are case-insensitive: a later source replaces an earlier one whatever its casing. */
|
|
50
|
+
function mergeHeaders(...sources) {
|
|
51
|
+
const merged = new Map();
|
|
52
|
+
for (const source of sources) {
|
|
53
|
+
for (const [key, value] of Object.entries(source ?? {})) {
|
|
54
|
+
merged.set(key.toLowerCase(), [key, value]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return Object.fromEntries(merged.values());
|
|
58
|
+
}
|
|
59
|
+
function decodeHeaderMember(value) {
|
|
60
|
+
try {
|
|
61
|
+
return decodeURIComponent(value);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// A literal "%" that is not a valid escape is passed through rather than dropping the header.
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Baggage-style header list: malformed members are skipped and an invalid percent-escape keeps the raw text. */
|
|
69
|
+
function parseHeaderList(value) {
|
|
70
|
+
const entries = [];
|
|
71
|
+
for (const member of value?.split(",") ?? []) {
|
|
72
|
+
const separator = member.indexOf("=");
|
|
73
|
+
if (separator <= 0)
|
|
74
|
+
continue;
|
|
75
|
+
const key = decodeHeaderMember(member.slice(0, separator).trim());
|
|
76
|
+
if (!key)
|
|
77
|
+
continue;
|
|
78
|
+
entries.push([key, decodeHeaderMember(member.slice(separator + 1).trim())]);
|
|
79
|
+
}
|
|
80
|
+
return mergeHeaders(Object.fromEntries(entries));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* OTel resource list: a member without `key=value` or with an invalid
|
|
84
|
+
* percent-escape discards the whole value, as the Resource SDK spec requires,
|
|
85
|
+
* so a partially malformed variable can never export a wrong identity.
|
|
86
|
+
*/
|
|
87
|
+
function parseResourceAttributeList(value) {
|
|
88
|
+
const entries = [];
|
|
89
|
+
for (const member of value.split(",")) {
|
|
90
|
+
// Every member must be `key=value`; an empty member (`a=b,,c=d`, a trailing comma, a
|
|
91
|
+
// whitespace-only value) is a parse error and discards the whole variable.
|
|
92
|
+
const separator = member.indexOf("=");
|
|
93
|
+
if (separator <= 0)
|
|
94
|
+
return undefined;
|
|
95
|
+
try {
|
|
96
|
+
const key = decodeURIComponent(member.slice(0, separator).trim());
|
|
97
|
+
if (!key)
|
|
98
|
+
return undefined;
|
|
99
|
+
entries.push([key, decodeURIComponent(member.slice(separator + 1).trim())]);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return Object.fromEntries(entries);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Resolves the OTLP/HTTP export target from explicit configuration and the
|
|
109
|
+
* standard OpenTelemetry environment contract. Endpoint precedence is explicit
|
|
110
|
+
* config, then OTEL_EXPORTER_OTLP_TRACES_ENDPOINT verbatim, then
|
|
111
|
+
* OTEL_EXPORTER_OTLP_ENDPOINT with `/v1/traces` appended. The winning candidate
|
|
112
|
+
* must be an absolute http(s) URL; an invalid one fails closed instead of
|
|
113
|
+
* falling through to a lower-precedence destination. Header values are never
|
|
114
|
+
* surfaced in the result beyond the request options themselves.
|
|
115
|
+
*/
|
|
116
|
+
export function resolveOTLPExportOptions(explicit, env = process.env) {
|
|
117
|
+
const candidates = [
|
|
118
|
+
{ source: EXPLICIT_ENDPOINT_SOURCE, value: explicit.endpoint, appendPath: false },
|
|
119
|
+
{
|
|
120
|
+
source: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
|
|
121
|
+
value: env[OTEL_EXPORTER_OTLP_TRACES_ENDPOINT],
|
|
122
|
+
appendPath: false,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
source: OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
126
|
+
value: env[OTEL_EXPORTER_OTLP_ENDPOINT],
|
|
127
|
+
appendPath: true,
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
const candidate = candidates
|
|
131
|
+
.map((entry) => ({ ...entry, value: presentValue(entry.value) }))
|
|
132
|
+
.find((entry) => entry.value !== undefined);
|
|
133
|
+
if (!candidate)
|
|
134
|
+
return { status: "unconfigured" };
|
|
135
|
+
const parsed = parseHttpUrl(candidate.value);
|
|
136
|
+
if ("reason" in parsed) {
|
|
137
|
+
return { status: "invalid", source: candidate.source, reason: parsed.reason };
|
|
138
|
+
}
|
|
139
|
+
const endpoint = candidate.appendPath ? appendTracesPath(parsed.url) : candidate.value;
|
|
140
|
+
const tracesHeaders = presentValue(env[OTEL_EXPORTER_OTLP_TRACES_HEADERS]);
|
|
141
|
+
const headersSource = tracesHeaders !== undefined ? OTEL_EXPORTER_OTLP_TRACES_HEADERS : OTEL_EXPORTER_OTLP_HEADERS;
|
|
142
|
+
const envHeaders = parseHeaderList(tracesHeaders ?? presentValue(env[OTEL_EXPORTER_OTLP_HEADERS]));
|
|
143
|
+
if (!headersAreSendable(envHeaders)) {
|
|
144
|
+
return { status: "invalid", source: headersSource, reason: INVALID_HEADERS };
|
|
145
|
+
}
|
|
146
|
+
const headers = mergeHeaders(envHeaders, explicit.headers);
|
|
147
|
+
if (!headersAreSendable(headers)) {
|
|
148
|
+
return { status: "invalid", source: EXPLICIT_HEADERS_SOURCE, reason: INVALID_HEADERS };
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
status: "resolved",
|
|
152
|
+
options: {
|
|
153
|
+
endpoint,
|
|
154
|
+
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
|
155
|
+
...(explicit.timeout !== undefined ? { timeout: explicit.timeout } : {}),
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Merges OTEL_RESOURCE_ATTRIBUTES and OTEL_SERVICE_NAME under the caller's
|
|
161
|
+
* explicit resource attributes: explicit wins per key, OTEL_SERVICE_NAME wins
|
|
162
|
+
* over a `service.name` inside OTEL_RESOURCE_ATTRIBUTES. An unparseable
|
|
163
|
+
* OTEL_RESOURCE_ATTRIBUTES value is discarded as a unit and reported in `discarded`.
|
|
164
|
+
*/
|
|
165
|
+
export function resolveOTLPResourceAttributes(explicit, env = process.env) {
|
|
166
|
+
const discarded = [];
|
|
167
|
+
const resourceList = presentValue(env[OTEL_RESOURCE_ATTRIBUTES]);
|
|
168
|
+
const resourceAttributes = resourceList === undefined ? {} : parseResourceAttributeList(resourceList);
|
|
169
|
+
if (resourceAttributes === undefined)
|
|
170
|
+
discarded.push(OTEL_RESOURCE_ATTRIBUTES);
|
|
171
|
+
const serviceName = presentValue(env[OTEL_SERVICE_NAME]);
|
|
172
|
+
return {
|
|
173
|
+
attributes: {
|
|
174
|
+
...resourceAttributes,
|
|
175
|
+
...(serviceName ? { "service.name": serviceName } : {}),
|
|
176
|
+
...explicit,
|
|
177
|
+
},
|
|
178
|
+
discarded,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
1
181
|
let nextTraceId = 1n;
|
|
2
182
|
let replayableTraceId = null;
|
|
3
183
|
function createBatchSignature(spans, resourceAttributes) {
|
|
@@ -36,8 +216,8 @@ function toAttributeValue(value) {
|
|
|
36
216
|
}
|
|
37
217
|
return { stringValue: String(value) };
|
|
38
218
|
}
|
|
39
|
-
export function spansToOTLP(spans, resourceAttributes) {
|
|
40
|
-
const
|
|
219
|
+
export function spansToOTLP(spans, resourceAttributes, traceId) {
|
|
220
|
+
const batchTraceId = traceId ?? createTraceId(createBatchSignature(spans, resourceAttributes));
|
|
41
221
|
return {
|
|
42
222
|
resourceSpans: [
|
|
43
223
|
{
|
|
@@ -54,7 +234,7 @@ export function spansToOTLP(spans, resourceAttributes) {
|
|
|
54
234
|
version: "0.1.0",
|
|
55
235
|
},
|
|
56
236
|
spans: spans.map((span) => ({
|
|
57
|
-
traceId,
|
|
237
|
+
traceId: batchTraceId,
|
|
58
238
|
spanId: normalizeHexId(span.id, 16) ?? "0000000000000001",
|
|
59
239
|
parentSpanId: normalizeHexId(span.parentId, 16),
|
|
60
240
|
name: span.name,
|
|
@@ -73,31 +253,232 @@ export function spansToOTLP(spans, resourceAttributes) {
|
|
|
73
253
|
],
|
|
74
254
|
};
|
|
75
255
|
}
|
|
76
|
-
|
|
77
|
-
|
|
256
|
+
/**
|
|
257
|
+
* Binds the transport that carries collector credentials.
|
|
258
|
+
*
|
|
259
|
+
* Trust model: in production the provider's own entry file is the process entry, so
|
|
260
|
+
* provider-controlled code (its earlier imports, a `bun --preload`, or a bunfig preload) can run
|
|
261
|
+
* before any SDK module evaluates; capture timing alone therefore cannot establish trust. On Bun
|
|
262
|
+
* the native fetch is also exposed as `Bun.fetch`, and both the `Bun` global and its `fetch`
|
|
263
|
+
* property are read-only and non-configurable, so no JavaScript in the process can replace that
|
|
264
|
+
* reference at any point. The engine binds it here and never consults `globalThis.fetch`.
|
|
265
|
+
*
|
|
266
|
+
* Residual assumptions: on a runtime without `Bun.fetch` the fallback is `globalThis.fetch` as
|
|
267
|
+
* seen when this module first evaluates, which code that runs earlier in the same process can
|
|
268
|
+
* have replaced (or can have forged a `Bun` global). Code with process authority can also patch
|
|
269
|
+
* other builtins on this path (Map, Object.fromEntries, Headers, AbortController, setTimeout),
|
|
270
|
+
* install module loader plugins, or reach the internal test seam below by importing this module
|
|
271
|
+
* by path; an in-process boundary cannot defend against any of that. The CLI flows load provider
|
|
272
|
+
* modules only after the engine has loaded.
|
|
273
|
+
*/
|
|
274
|
+
function bindEngineTransport() {
|
|
275
|
+
const bunFetch = typeof Bun !== "undefined" ? Bun.fetch : undefined;
|
|
276
|
+
return typeof bunFetch === "function" ? bunFetch : globalThis.fetch;
|
|
277
|
+
}
|
|
278
|
+
const engineTransport = bindEngineTransport();
|
|
279
|
+
let transport = engineTransport;
|
|
280
|
+
// Deliveries run inside this engine-owned async scope, created while the module evaluates, so a
|
|
281
|
+
// batch admitted from another batch's completion never inherits that request's async context.
|
|
282
|
+
// (The scope snapshots the async context active at module load: empty under the static imports
|
|
283
|
+
// the engine uses.)
|
|
284
|
+
const exportScope = new AsyncResource("apifuse.otlp.export");
|
|
285
|
+
/** Process-wide bounds so a collector outage can never turn into unbounded sockets, memory, or log volume. */
|
|
286
|
+
export const OTLP_EXPORT_LIMITS = {
|
|
287
|
+
maxInFlight: 4,
|
|
288
|
+
maxQueued: 64,
|
|
289
|
+
maxAttempts: 3,
|
|
290
|
+
retryBaseDelayMs: 200,
|
|
291
|
+
retryMaxDelayMs: 2_000,
|
|
292
|
+
warningCooldownMs: 10_000,
|
|
293
|
+
};
|
|
294
|
+
const RETRYABLE_STATUSES = new Set([408, 429, 502, 503, 504]);
|
|
295
|
+
const TIMEOUT_ERROR_NAMES = new Set(["AbortError", "TimeoutError"]);
|
|
296
|
+
/** Certificate failures do not clear up on retry; they are reported once and the batch dropped. */
|
|
297
|
+
const CERTIFICATE_ERROR_CODES = new Set([
|
|
298
|
+
"CERT_HAS_EXPIRED",
|
|
299
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
300
|
+
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
301
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
302
|
+
"ERR_TLS_CERT_ALTNAME_INVALID",
|
|
303
|
+
]);
|
|
304
|
+
/** Only these system codes are ever echoed; anything else is reported as a plain network error. */
|
|
305
|
+
const NETWORK_ERROR_CODES = new Set([
|
|
306
|
+
"ECONNREFUSED",
|
|
307
|
+
"ECONNRESET",
|
|
308
|
+
"ECONNABORTED",
|
|
309
|
+
"ENOTFOUND",
|
|
310
|
+
"EAI_AGAIN",
|
|
311
|
+
"ETIMEDOUT",
|
|
312
|
+
"EHOSTUNREACH",
|
|
313
|
+
"ENETUNREACH",
|
|
314
|
+
"EPIPE",
|
|
315
|
+
"CERT_HAS_EXPIRED",
|
|
316
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
317
|
+
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
318
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
319
|
+
"ERR_TLS_CERT_ALTNAME_INVALID",
|
|
320
|
+
"ConnectionRefused",
|
|
321
|
+
"ConnectionClosed",
|
|
322
|
+
"FailedToOpenSocket",
|
|
323
|
+
]);
|
|
324
|
+
const queue = [];
|
|
325
|
+
let inFlight = 0;
|
|
326
|
+
let lastWarningAt = Number.NEGATIVE_INFINITY;
|
|
327
|
+
let suppressedDrops = 0;
|
|
328
|
+
let suppressedFlush;
|
|
329
|
+
function errorCode(error) {
|
|
330
|
+
const own = error?.code;
|
|
331
|
+
if (typeof own === "string")
|
|
332
|
+
return own;
|
|
333
|
+
const cause = error?.cause?.code;
|
|
334
|
+
return typeof cause === "string" ? cause : undefined;
|
|
335
|
+
}
|
|
336
|
+
/** Maps a transport failure onto a fixed vocabulary; nothing from the error object is interpolated. */
|
|
337
|
+
function describeExportFailure(error) {
|
|
338
|
+
if (error instanceof Error && TIMEOUT_ERROR_NAMES.has(error.name)) {
|
|
339
|
+
return { reason: "timeout", retryable: true };
|
|
340
|
+
}
|
|
341
|
+
const code = errorCode(error);
|
|
342
|
+
if (code !== undefined && CERTIFICATE_ERROR_CODES.has(code)) {
|
|
343
|
+
return { reason: `certificate error: ${code}`, retryable: false };
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
reason: code !== undefined && NETWORK_ERROR_CODES.has(code)
|
|
347
|
+
? `network error: ${code}`
|
|
348
|
+
: "network error",
|
|
349
|
+
retryable: true,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function batchesLabel(count) {
|
|
353
|
+
return `${count} ${count === 1 ? "batch" : "batches"}`;
|
|
354
|
+
}
|
|
355
|
+
/** Emits the count of drops suppressed during a cooldown once it ends, so a burst is never under-reported. */
|
|
356
|
+
function flushSuppressedDrops() {
|
|
357
|
+
suppressedFlush = undefined;
|
|
358
|
+
if (suppressedDrops === 0)
|
|
359
|
+
return;
|
|
360
|
+
const dropped = suppressedDrops;
|
|
361
|
+
suppressedDrops = 0;
|
|
362
|
+
lastWarningAt = Date.now();
|
|
363
|
+
console.warn(`[apifuse] OTLP export: ${batchesLabel(dropped)} more dropped since the last warning.`);
|
|
364
|
+
}
|
|
365
|
+
function noteDroppedBatch(reason) {
|
|
366
|
+
const now = Date.now();
|
|
367
|
+
const sinceLastWarning = now - lastWarningAt;
|
|
368
|
+
if (sinceLastWarning < OTLP_EXPORT_LIMITS.warningCooldownMs) {
|
|
369
|
+
suppressedDrops += 1;
|
|
370
|
+
if (suppressedFlush === undefined) {
|
|
371
|
+
suppressedFlush = setTimeout(flushSuppressedDrops, OTLP_EXPORT_LIMITS.warningCooldownMs - sinceLastWarning);
|
|
372
|
+
suppressedFlush.unref?.();
|
|
373
|
+
}
|
|
78
374
|
return;
|
|
79
375
|
}
|
|
376
|
+
lastWarningAt = now;
|
|
377
|
+
console.warn(`[apifuse] OTLP export failed (${reason}); ${batchesLabel(1)} dropped.`);
|
|
378
|
+
}
|
|
379
|
+
async function sendBatch(batch) {
|
|
80
380
|
const controller = new AbortController();
|
|
81
|
-
const timer = setTimeout(() => controller.abort(), options.timeout ?? 5_000);
|
|
381
|
+
const timer = setTimeout(() => controller.abort(), batch.options.timeout ?? 5_000);
|
|
82
382
|
try {
|
|
83
|
-
const response = await
|
|
383
|
+
const response = await transport(batch.options.endpoint, {
|
|
84
384
|
method: "POST",
|
|
85
|
-
headers: {
|
|
86
|
-
|
|
87
|
-
...options.headers,
|
|
88
|
-
},
|
|
89
|
-
body: JSON.stringify(spansToOTLP(spans, resourceAttributes)),
|
|
385
|
+
headers: mergeHeaders(batch.options.headers, { "Content-Type": "application/json" }),
|
|
386
|
+
body: batch.body,
|
|
90
387
|
signal: controller.signal,
|
|
91
388
|
});
|
|
92
|
-
|
|
93
|
-
|
|
389
|
+
// The reply body is not needed; cancel it while the abort timer still bounds the socket.
|
|
390
|
+
try {
|
|
391
|
+
await response.body?.cancel();
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
// A body that cannot be cancelled does not change the outcome of the export.
|
|
94
395
|
}
|
|
396
|
+
if (response.ok)
|
|
397
|
+
return { ok: true };
|
|
398
|
+
return {
|
|
399
|
+
ok: false,
|
|
400
|
+
reason: `HTTP ${response.status}`,
|
|
401
|
+
retryable: RETRYABLE_STATUSES.has(response.status),
|
|
402
|
+
};
|
|
95
403
|
}
|
|
96
404
|
catch (error) {
|
|
97
|
-
|
|
98
|
-
console.warn("[apifuse] OTLP export failed:", message);
|
|
405
|
+
return { ok: false, ...describeExportFailure(error) };
|
|
99
406
|
}
|
|
100
407
|
finally {
|
|
101
408
|
clearTimeout(timer);
|
|
102
409
|
}
|
|
103
410
|
}
|
|
411
|
+
/** Exponential backoff between attempts: base * 2^(attempt-1), capped at the maximum delay. */
|
|
412
|
+
export function retryDelayMs(attempt) {
|
|
413
|
+
return Math.min(OTLP_EXPORT_LIMITS.retryBaseDelayMs * 2 ** (attempt - 1), OTLP_EXPORT_LIMITS.retryMaxDelayMs);
|
|
414
|
+
}
|
|
415
|
+
async function deliverBatch(batch) {
|
|
416
|
+
for (let attempt = 1;; attempt += 1) {
|
|
417
|
+
const outcome = await sendBatch(batch);
|
|
418
|
+
if (outcome.ok)
|
|
419
|
+
return;
|
|
420
|
+
if (!outcome.retryable || attempt >= OTLP_EXPORT_LIMITS.maxAttempts) {
|
|
421
|
+
noteDroppedBatch(attempt > 1 ? `${outcome.reason} after ${attempt} attempts` : outcome.reason);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
await new Promise((resolve) => {
|
|
425
|
+
setTimeout(resolve, retryDelayMs(attempt)).unref?.();
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function pumpQueue() {
|
|
430
|
+
while (inFlight < OTLP_EXPORT_LIMITS.maxInFlight && queue.length > 0) {
|
|
431
|
+
const batch = queue.shift();
|
|
432
|
+
if (!batch)
|
|
433
|
+
return;
|
|
434
|
+
inFlight += 1;
|
|
435
|
+
void exportScope
|
|
436
|
+
.runInAsyncScope(() => deliverBatch(batch))
|
|
437
|
+
.finally(() => {
|
|
438
|
+
inFlight -= 1;
|
|
439
|
+
batch.settle();
|
|
440
|
+
pumpQueue();
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Queues one export batch behind the process-wide concurrency and queue bounds.
|
|
446
|
+
* Resolves once the batch has been delivered or dropped; it never rejects, so
|
|
447
|
+
* callers can fire and forget.
|
|
448
|
+
*/
|
|
449
|
+
export function exportSpansOTLP(spans, options, resourceAttributes, traceId) {
|
|
450
|
+
if (spans.length === 0) {
|
|
451
|
+
return Promise.resolve();
|
|
452
|
+
}
|
|
453
|
+
if (queue.length >= OTLP_EXPORT_LIMITS.maxQueued) {
|
|
454
|
+
noteDroppedBatch("export queue is full");
|
|
455
|
+
return Promise.resolve();
|
|
456
|
+
}
|
|
457
|
+
let body;
|
|
458
|
+
try {
|
|
459
|
+
body = JSON.stringify(spansToOTLP(spans, resourceAttributes, traceId));
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
noteDroppedBatch("span serialization failed");
|
|
463
|
+
return Promise.resolve();
|
|
464
|
+
}
|
|
465
|
+
return new Promise((settle) => {
|
|
466
|
+
queue.push({ body, options, settle });
|
|
467
|
+
pumpQueue();
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
/** Test seam (not re-exported from any package entry point): substitute the engine transport. */
|
|
471
|
+
export function swapOTLPTransportForTests(next) {
|
|
472
|
+
transport = next ?? engineTransport;
|
|
473
|
+
}
|
|
474
|
+
/** Test seam: drop queued batches, clear the warning throttle, and restore the engine transport. */
|
|
475
|
+
export function resetOTLPExportForTests() {
|
|
476
|
+
for (const batch of queue.splice(0))
|
|
477
|
+
batch.settle();
|
|
478
|
+
if (suppressedFlush !== undefined)
|
|
479
|
+
clearTimeout(suppressedFlush);
|
|
480
|
+
suppressedFlush = undefined;
|
|
481
|
+
lastWarningAt = Number.NEGATIVE_INFINITY;
|
|
482
|
+
suppressedDrops = 0;
|
|
483
|
+
transport = engineTransport;
|
|
484
|
+
}
|
|
@@ -11,8 +11,12 @@ const PROXY_POOL_STALE_STATUS_CODES = new Set([509, 512]);
|
|
|
11
11
|
const PROXY_EDGE_TLS_REJECTED_STATUS_CODES = new Set([495]);
|
|
12
12
|
const PROXY_AUTH_IP_DENIED_PATTERN = /\b(?:source|egress|client)\s+ip\b.{0,120}\b(?:deny|denied|unauthori[sz]ed|not\s+authori[sz]ed|white\s*list|allow\s*list)\b|\b(?:white\s*list|allow\s*list)\b.{0,120}\b(?:source|egress|client)\s+ip\b/i;
|
|
13
13
|
const PROXY_EDGE_AUTH_REJECTED_PATTERN = /\bauth\s+ip\s+err\b|\bproxy\b.{0,120}\bauth(?:entication)?\b.{0,120}\b(?:reject(?:ed)?|fail(?:ed)?|invalid|den(?:y|ied)|unauthori[sz]ed)\b|\bauth(?:entication)?\b.{0,120}\b(?:reject(?:ed)?|fail(?:ed)?|invalid|den(?:y|ied)|unauthori[sz]ed)\b.{0,120}\bproxy\b/i;
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// Vendor host tokens that can appear in upstream error strings. Adding a proxy
|
|
15
|
+
// vendor updates every classifier below in one place. `proxy` is the generic
|
|
16
|
+
// fallback so vendor-agnostic messages still classify.
|
|
17
|
+
const PROXY_VENDOR_ALTERNATION = "smartproxy|nodemaven|proxy";
|
|
18
|
+
const PROXY_POOL_STALE_MESSAGE_PATTERN = new RegExp(`\\bproxy\\b.{0,120}\\b(?:pool|lease|expired|unavailable|exhausted|non[\\s-]?200\\s+code:\\s*(?:509|512))\\b|\\bnon[\\s-]?200\\s+code:\\s*(?:509|512)\\b.{0,120}\\bproxy\\b|\\b(?:${PROXY_VENDOR_ALTERNATION})\\b.{0,120}\\b(?:509|512)\\b`, "i");
|
|
19
|
+
const PROXY_EDGE_TLS_REJECTED_MESSAGE_PATTERN = new RegExp(`\\b(?:${PROXY_VENDOR_ALTERNATION})\\b.{0,160}\\b(?:495|ssl|tls|cert(?:ificate)?|handshake|edge|connect|non[\\s-]?200)\\b|\\b(?:495|ssl|tls|cert(?:ificate)?|handshake|edge|connect|non[\\s-]?200)\\b.{0,160}\\b(?:${PROXY_VENDOR_ALTERNATION})\\b`, "i");
|
|
16
20
|
export function isProxyAuthIpDeniedMessage(message) {
|
|
17
21
|
return PROXY_AUTH_IP_DENIED_PATTERN.test(message);
|
|
18
22
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ProviderProxyPolicy } from "../types.js";
|
|
2
|
+
export declare const NODEMAVEN_USERNAME_ENV = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
|
|
3
|
+
export declare const NODEMAVEN_PASSWORD_ENV = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
|
|
4
|
+
export declare const NODEMAVEN_FILTER_ENV = "APIFUSE__PROXY__NODEMAVEN_FILTER";
|
|
5
|
+
export declare const NODEMAVEN_GATEWAY_HOST = "gate.nodemaven.com";
|
|
6
|
+
export type NodemavenCredentials = {
|
|
7
|
+
readonly username: string;
|
|
8
|
+
readonly password: string;
|
|
9
|
+
readonly filter?: string;
|
|
10
|
+
};
|
|
11
|
+
/** Both schemes tunnel bytes end-to-end, preserving the client TLS handshake. */
|
|
12
|
+
export type ProxyProtocol = "http" | "socks5";
|
|
13
|
+
/**
|
|
14
|
+
* NodeMaven's fastest protocol: HTTP CONNECT. Benchmarks (KR, cold + warm)
|
|
15
|
+
* showed socks5 through the gateway adds ~500ms per request over http, so
|
|
16
|
+
* NodeMaven never defaults to socks5.
|
|
17
|
+
*/
|
|
18
|
+
export declare const NODEMAVEN_DEFAULT_PROTOCOL: ProxyProtocol;
|
|
19
|
+
export declare const NODEMAVEN_MAX_POOL_SIZE = 50;
|
|
20
|
+
export declare function hasNodemavenCredentials(): boolean;
|
|
21
|
+
export declare function nodemavenPoolSize(policy: ProviderProxyPolicy): number;
|
|
22
|
+
export declare function nodemavenLifetimeMinutes(policy: ProviderProxyPolicy): number;
|
|
23
|
+
export type NodemavenSessionWindow = {
|
|
24
|
+
refreshEpoch: number;
|
|
25
|
+
expiresAt: string;
|
|
26
|
+
};
|
|
27
|
+
/** Stable wall-clock window shared by every process deriving the same sticky sid. */
|
|
28
|
+
export declare function nodemavenSessionWindow(policy: ProviderProxyPolicy, now?: number): NodemavenSessionWindow;
|
|
29
|
+
export type NodemavenSynthesisInput = {
|
|
30
|
+
policy: ProviderProxyPolicy;
|
|
31
|
+
/** Explicit credentials; synthesis never reads process-global state. */
|
|
32
|
+
credentials: NodemavenCredentials;
|
|
33
|
+
affinityKey: string | undefined;
|
|
34
|
+
protocol: ProxyProtocol;
|
|
35
|
+
poolIndex: number;
|
|
36
|
+
refreshEpoch: number;
|
|
37
|
+
/** ISO 3166-1 alpha-2, already resolved by the caller (falls back to env). */
|
|
38
|
+
country?: string;
|
|
39
|
+
/** Clock injection used by native sticky-expiry lifecycle tests. */
|
|
40
|
+
now?: number;
|
|
41
|
+
/** Stable caller-owned hard expiry when the refresh epoch is wall-clock-derived. */
|
|
42
|
+
expiresAt?: string;
|
|
43
|
+
};
|
|
44
|
+
export type NodemavenSynthesis = {
|
|
45
|
+
url: string;
|
|
46
|
+
protocol: ProxyProtocol;
|
|
47
|
+
sticky: boolean;
|
|
48
|
+
sessionId: string;
|
|
49
|
+
expiresAt?: string;
|
|
50
|
+
diagnostics: Record<string, string | number | boolean>;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Synthesize a NodeMaven gateway proxy URL locally from static credentials.
|
|
54
|
+
* There is no allocation API — geo/session are encoded in the username.
|
|
55
|
+
*/
|
|
56
|
+
export declare function synthesizeNodemavenProxy(input: NodemavenSynthesisInput): NodemavenSynthesis;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
export const NODEMAVEN_USERNAME_ENV = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
|
|
3
|
+
export const NODEMAVEN_PASSWORD_ENV = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
|
|
4
|
+
export const NODEMAVEN_FILTER_ENV = "APIFUSE__PROXY__NODEMAVEN_FILTER";
|
|
5
|
+
export const NODEMAVEN_GATEWAY_HOST = "gate.nodemaven.com";
|
|
6
|
+
/**
|
|
7
|
+
* NodeMaven's fastest protocol: HTTP CONNECT. Benchmarks (KR, cold + warm)
|
|
8
|
+
* showed socks5 through the gateway adds ~500ms per request over http, so
|
|
9
|
+
* NodeMaven never defaults to socks5.
|
|
10
|
+
*/
|
|
11
|
+
export const NODEMAVEN_DEFAULT_PROTOCOL = "http";
|
|
12
|
+
/** NodeMaven gateway port ranges per protocol (docs: HTTP 8080-9080, SOCKS5 1080-2080). */
|
|
13
|
+
const NODEMAVEN_PORTS = {
|
|
14
|
+
http: { min: 8080, max: 9080 },
|
|
15
|
+
socks5: { min: 1080, max: 2080 },
|
|
16
|
+
};
|
|
17
|
+
const NODEMAVEN_FILTERS = new Set(["medium", "high"]);
|
|
18
|
+
const DEFAULT_NODEMAVEN_FILTER = "medium";
|
|
19
|
+
const DEFAULT_NODEMAVEN_POOL_SIZE = 20;
|
|
20
|
+
export const NODEMAVEN_MAX_POOL_SIZE = 50;
|
|
21
|
+
/** NodeMaven sticky sessions persist up to 24h server-side, keyed by the sid. */
|
|
22
|
+
const NODEMAVEN_MAX_LIFETIME_MINUTES = 1440;
|
|
23
|
+
const SID_LENGTH = 10;
|
|
24
|
+
export function hasNodemavenCredentials() {
|
|
25
|
+
return Boolean(readNodemavenUsername() && readNodemavenPassword());
|
|
26
|
+
}
|
|
27
|
+
function readNodemavenUsername() {
|
|
28
|
+
return process.env[NODEMAVEN_USERNAME_ENV]?.trim() || undefined;
|
|
29
|
+
}
|
|
30
|
+
function readNodemavenPassword() {
|
|
31
|
+
return process.env[NODEMAVEN_PASSWORD_ENV]?.trim() || undefined;
|
|
32
|
+
}
|
|
33
|
+
function resolveNodemavenFilter(value) {
|
|
34
|
+
const raw = value?.trim().toLowerCase();
|
|
35
|
+
if (!raw)
|
|
36
|
+
return DEFAULT_NODEMAVEN_FILTER;
|
|
37
|
+
if (!NODEMAVEN_FILTERS.has(raw)) {
|
|
38
|
+
throw new Error(`${NODEMAVEN_FILTER_ENV} must be "medium" or "high"`);
|
|
39
|
+
}
|
|
40
|
+
return raw;
|
|
41
|
+
}
|
|
42
|
+
export function nodemavenPoolSize(policy) {
|
|
43
|
+
return Math.min(NODEMAVEN_MAX_POOL_SIZE, Math.max(1, Math.floor(policy.session?.poolSize ?? DEFAULT_NODEMAVEN_POOL_SIZE)));
|
|
44
|
+
}
|
|
45
|
+
export function nodemavenLifetimeMinutes(policy) {
|
|
46
|
+
const configured = policy.session?.lifetimeMinutes;
|
|
47
|
+
if (typeof configured !== "number" || !Number.isFinite(configured) || configured <= 0) {
|
|
48
|
+
return NODEMAVEN_MAX_LIFETIME_MINUTES;
|
|
49
|
+
}
|
|
50
|
+
return Math.min(NODEMAVEN_MAX_LIFETIME_MINUTES, Math.max(1, Math.floor(configured)));
|
|
51
|
+
}
|
|
52
|
+
/** Stable wall-clock window shared by every process deriving the same sticky sid. */
|
|
53
|
+
export function nodemavenSessionWindow(policy, now = Date.now()) {
|
|
54
|
+
const lifetimeMs = nodemavenLifetimeMinutes(policy) * 60_000;
|
|
55
|
+
const refreshEpoch = Math.floor(now / lifetimeMs);
|
|
56
|
+
return {
|
|
57
|
+
refreshEpoch,
|
|
58
|
+
expiresAt: new Date((refreshEpoch + 1) * lifetimeMs).toISOString(),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** NodeMaven username tokens accept `[a-z0-9]`; slugify geo values to that set. */
|
|
62
|
+
function slugifyGeo(value) {
|
|
63
|
+
if (!value)
|
|
64
|
+
return undefined;
|
|
65
|
+
const slug = value
|
|
66
|
+
.trim()
|
|
67
|
+
.toLowerCase()
|
|
68
|
+
.replace(/[^a-z0-9]+/g, "");
|
|
69
|
+
return slug || undefined;
|
|
70
|
+
}
|
|
71
|
+
function isStickyAffinity(policy) {
|
|
72
|
+
return (policy.session?.affinity ?? "request") !== "request";
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A sticky sid is deterministic from the affinity key so every process serving
|
|
76
|
+
* the same connection derives the same egress IP without shared storage. A
|
|
77
|
+
* rotating sid is random per call (a fresh egress IP per request).
|
|
78
|
+
*/
|
|
79
|
+
function deriveSid(policy, affinityKey, poolIndex, refreshEpoch) {
|
|
80
|
+
if (!isStickyAffinity(policy) || !affinityKey) {
|
|
81
|
+
return randomBytes(SID_LENGTH).toString("hex").slice(0, SID_LENGTH);
|
|
82
|
+
}
|
|
83
|
+
const digest = createHash("sha256")
|
|
84
|
+
.update(`${affinityKey}:${refreshEpoch}:${poolIndex}`)
|
|
85
|
+
.digest("hex");
|
|
86
|
+
// hex digits are a subset of the allowed [a-z0-9] sid charset.
|
|
87
|
+
return digest.slice(0, SID_LENGTH);
|
|
88
|
+
}
|
|
89
|
+
function selectPort(protocol, sid, poolIndex) {
|
|
90
|
+
const { min, max } = NODEMAVEN_PORTS[protocol];
|
|
91
|
+
const span = max - min + 1;
|
|
92
|
+
const hashInt = Number.parseInt(createHash("sha256").update(`${sid}:${poolIndex}`).digest("hex").slice(0, 8), 16);
|
|
93
|
+
return min + (hashInt % span);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Synthesize a NodeMaven gateway proxy URL locally from static credentials.
|
|
97
|
+
* There is no allocation API — geo/session are encoded in the username.
|
|
98
|
+
*/
|
|
99
|
+
export function synthesizeNodemavenProxy(input) {
|
|
100
|
+
const username = input.credentials.username.trim();
|
|
101
|
+
const password = input.credentials.password.trim();
|
|
102
|
+
if (!username || !password) {
|
|
103
|
+
throw new Error(`NodeMaven credentials missing: set ${NODEMAVEN_USERNAME_ENV} and ${NODEMAVEN_PASSWORD_ENV}.`);
|
|
104
|
+
}
|
|
105
|
+
const filter = resolveNodemavenFilter(input.credentials.filter);
|
|
106
|
+
const sid = deriveSid(input.policy, input.affinityKey, input.poolIndex, input.refreshEpoch);
|
|
107
|
+
const port = selectPort(input.protocol, sid, input.poolIndex);
|
|
108
|
+
const lifetimeMinutes = nodemavenLifetimeMinutes(input.policy);
|
|
109
|
+
const sticky = isStickyAffinity(input.policy);
|
|
110
|
+
const country = slugifyGeo(input.country ?? input.policy.geo?.country);
|
|
111
|
+
const region = slugifyGeo(input.policy.geo?.subdivision);
|
|
112
|
+
const city = slugifyGeo(input.policy.geo?.city);
|
|
113
|
+
const tokens = [username];
|
|
114
|
+
if (country)
|
|
115
|
+
tokens.push("country", country);
|
|
116
|
+
if (region)
|
|
117
|
+
tokens.push("region", region);
|
|
118
|
+
if (city)
|
|
119
|
+
tokens.push("city", city);
|
|
120
|
+
tokens.push("sid", sid);
|
|
121
|
+
tokens.push("filter", filter);
|
|
122
|
+
tokens.push("ipv4", "true");
|
|
123
|
+
const proxyUsername = tokens.join("-");
|
|
124
|
+
// Username tokens are [a-z0-9-] only, which survive URL encoding unchanged.
|
|
125
|
+
const url = `${input.protocol}://${proxyUsername}:${encodeURIComponent(password)}@${NODEMAVEN_GATEWAY_HOST}:${port}`;
|
|
126
|
+
return {
|
|
127
|
+
url,
|
|
128
|
+
protocol: input.protocol,
|
|
129
|
+
sticky,
|
|
130
|
+
sessionId: sid,
|
|
131
|
+
...(sticky
|
|
132
|
+
? {
|
|
133
|
+
expiresAt: input.expiresAt ??
|
|
134
|
+
new Date((input.now ?? Date.now()) + lifetimeMinutes * 60_000).toISOString(),
|
|
135
|
+
}
|
|
136
|
+
: {}),
|
|
137
|
+
diagnostics: {
|
|
138
|
+
vendor: "nodemaven",
|
|
139
|
+
protocol: input.protocol,
|
|
140
|
+
sticky,
|
|
141
|
+
filter,
|
|
142
|
+
lifetimeMinutes,
|
|
143
|
+
...(country ? { country } : {}),
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|