@apifuse/provider-sdk 2.2.0-beta.55 → 2.2.0-beta.56

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.2.0-beta.56
4
+
5
+ - Release candidate for main commit 390167b3f51488be0beba25a1ea5eb425d3bf839.
6
+
3
7
  ## 2.2.0-beta.55
4
8
 
5
9
  - Release candidate for main commit 1c495af089940a4c666e6baf58dc0dd577771a40.
@@ -292,6 +292,115 @@ const NEGATIVE_CONTROLS = [
292
292
  "",
293
293
  ].join("\n"),
294
294
  },
295
+ {
296
+ filename: "negative-control-telemetry-header-vendor.ts",
297
+ expectedCode: "TS2322",
298
+ description: "gateway telemetry headers reject an unbranded vendor string",
299
+ source: [
300
+ 'import type { TelemetryContributor } from "@apifuse/provider-sdk";',
301
+ "",
302
+ "const bad: TelemetryContributor<{}, { vendor: string }> = {",
303
+ '\tkey: "resolver",',
304
+ "\ttoLogPayload: () => ({}),",
305
+ '\ttoHeaderPayload: () => ({ vendor: "free-text" }),',
306
+ "};",
307
+ "",
308
+ ].join("\n"),
309
+ },
310
+ {
311
+ filename: "negative-control-telemetry-header-host.ts",
312
+ expectedCode: "TS2322",
313
+ description: "gateway telemetry headers reject an unbranded host string",
314
+ source: [
315
+ 'import type { TelemetryContributor } from "@apifuse/provider-sdk";',
316
+ "",
317
+ "const bad: TelemetryContributor<{}, { host: string }> = {",
318
+ '\tkey: "http",',
319
+ "\ttoLogPayload: () => ({}),",
320
+ '\ttoHeaderPayload: () => ({ host: "x.example" }),',
321
+ "};",
322
+ "",
323
+ ].join("\n"),
324
+ },
325
+ {
326
+ filename: "negative-control-telemetry-tenant-vendor.ts",
327
+ expectedCode: "TS2322",
328
+ description: "tenant-neutral projections reject vendor identities",
329
+ source: [
330
+ 'import type { TenantNeutral } from "@apifuse/provider-sdk";',
331
+ "",
332
+ 'export const bad: TenantNeutral<{ vendorUsed: "smartproxy" }> = { vendorUsed: "smartproxy" };',
333
+ "",
334
+ ].join("\n"),
335
+ },
336
+ {
337
+ filename: "negative-control-telemetry-header-any.ts",
338
+ expectedCode: "TS2322",
339
+ description: "gateway telemetry projections reject any-valued properties",
340
+ source: [
341
+ 'import type { GatewayIngestible } from "@apifuse/provider-sdk";',
342
+ "",
343
+ 'export const bad: GatewayIngestible<{ x: any }> = { x: "free-text" };',
344
+ "",
345
+ ].join("\n"),
346
+ },
347
+ {
348
+ filename: "negative-control-telemetry-header-union-array.ts",
349
+ expectedCode: "TS2322",
350
+ description: "gateway telemetry projections reject arrays mixing numbers and strings",
351
+ source: [
352
+ 'import type { TelemetryContributor } from "@apifuse/provider-sdk";',
353
+ "",
354
+ "const bad: TelemetryContributor<{}, { values: (number | string)[] }> = {",
355
+ '\tkey: "resolver",',
356
+ "\ttoLogPayload: () => ({}),",
357
+ '\ttoHeaderPayload: () => ({ values: [1, "free prose"] }),',
358
+ "};",
359
+ "",
360
+ ].join("\n"),
361
+ },
362
+ {
363
+ filename: "negative-control-telemetry-opaque-identity.ts",
364
+ expectedCode: "TS2322",
365
+ description: "tenant opaque array brands are not accepted outside the cache keys property",
366
+ source: [
367
+ 'import type { TenantNeutral } from "@apifuse/provider-sdk";',
368
+ "",
369
+ "type LocalOpaque = string[] & { readonly __tenantOpaqueCacheKeys: true };",
370
+ "declare const opaque: LocalOpaque;",
371
+ "export const bad: TenantNeutral<{ identity: LocalOpaque }> = { identity: opaque };",
372
+ "",
373
+ ].join("\n"),
374
+ },
375
+ {
376
+ filename: "negative-control-telemetry-meta-vendor.ts",
377
+ expectedCode: "TS2322",
378
+ description: "success metadata rejects tenant-visible vendor identity keys",
379
+ source: [
380
+ 'import { closedEnum, type ClosedEnum, type TenantNeutral } from "@apifuse/provider-sdk";',
381
+ "",
382
+ 'type Meta = { cached: boolean; detail: { vendorUsed: ClosedEnum<"smartproxy"> } };',
383
+ 'export const bad: TenantNeutral<Meta> = { cached: false, detail: { vendorUsed: closedEnum("smartproxy") } };',
384
+ "",
385
+ ].join("\n"),
386
+ },
387
+ {
388
+ filename: "positive-control-proxy-telemetry-contributor.ts",
389
+ expectedCode: "",
390
+ description: "proxy telemetry contributor satisfies the public contributor contract",
391
+ source: [
392
+ 'import { closedEnum, type ClosedEnum, type GatewayIngestible, type ProxyTelemetryHeaderPayload, type ProxyTelemetryLogPayload, type TelemetryContributor } from "@apifuse/provider-sdk";',
393
+ "",
394
+ "export const proxy: TelemetryContributor<ProxyTelemetryLogPayload, ProxyTelemetryHeaderPayload> = {",
395
+ '\tkey: "proxy",',
396
+ '\ttoLogPayload: () => ({ kind: "unresolved", vendors: [] }),',
397
+ '\ttoHeaderPayload: () => ({ kind: closedEnum("unresolved"), vendors: [] }),',
398
+ "};",
399
+ 'type Values = { values: ClosedEnum<"a" | "b">[] };',
400
+ 'export const values: GatewayIngestible<Values> = { values: [closedEnum("a"), closedEnum("b")] };',
401
+ "",
402
+ ].join("\n"),
403
+ },
295
404
  ] as const;
296
405
 
297
406
  const tempRoot = mkdtempSync(join(tmpdir(), "apifuse-provider-sdk-pack-types-"));
@@ -513,6 +622,15 @@ function assertNegativeControlFails(consumerDir: string): void {
513
622
  ],
514
623
  { cwd: consumerDir, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
515
624
  );
625
+ if (negativeControl.expectedCode === "") {
626
+ if (result.status !== 0) {
627
+ throw new Error(
628
+ `Positive control "${negativeControl.description}" failed to compile:\n${result.stdout}\n${result.stderr}`,
629
+ );
630
+ }
631
+ console.log(`Positive control accepted: ${negativeControl.description}`);
632
+ continue;
633
+ }
516
634
  if (result.status === 0) {
517
635
  throw new Error(
518
636
  'Negative control "' +
@@ -528,6 +646,9 @@ function assertNegativeControlFails(consumerDir: string): void {
528
646
  `Negative control "${negativeControl.description}" (${negativeControl.filename}) failed for an unexpected reason (wanted ${negativeControl.expectedCode}):\n${output}`,
529
647
  );
530
648
  }
649
+ console.log(
650
+ `Negative control rejected (${negativeControl.expectedCode}): ${negativeControl.description}`,
651
+ );
531
652
  }
532
653
  }
533
654
 
package/dist/index.d.ts CHANGED
@@ -32,7 +32,8 @@ export { generateInsights } from "./runtime/insights.js";
32
32
  export { type InstrumentationOptions, type InstrumentedProviderContext, wrapWithInstrumentation, } from "./runtime/instrumentation.js";
33
33
  export type { PrevalidateResult } from "./runtime/prevalidate.js";
34
34
  export { getProviderBaseUrl } from "./runtime/provider.js";
35
- export type { ProxyTelemetryLogPayload, ProxyTelemetryResolvedPayload, ProxyTelemetryUnresolvedPayload, } from "./runtime/proxy-telemetry.js";
35
+ export type { ProxyTelemetryHeaderPayload, ProxyHash, ProxyTelemetryLogPayload, ProxyTelemetryResolvedPayload, ProxyTelemetryUnresolvedPayload, } from "./runtime/proxy-telemetry.js";
36
+ export { RequestTelemetry, closedEnum, type ClosedEnum, type GatewayIngestible, type RequestTelemetryLogPayload, type SpanIndex, type TelemetryContributor, type TelemetryKey, type TenantNeutral, } from "./runtime/request-telemetry.js";
36
37
  export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, DEFAULT_RESOLVER_TIMEOUT_MS, } from "./runtime/resolver-config.js";
37
38
  export { createUnsupportedResolverClient } from "./runtime/resolver-shared.js";
38
39
  export type { ResolverRuntimeOptions } from "./runtime/resolver.js";
package/dist/index.js CHANGED
@@ -26,6 +26,7 @@ export { NativeEgressGrantExpiredError, NativeEgressNotDeclaredError, NativeIdle
26
26
  export { generateInsights } from "./runtime/insights.js";
27
27
  export { wrapWithInstrumentation, } from "./runtime/instrumentation.js";
28
28
  export { getProviderBaseUrl } from "./runtime/provider.js";
29
+ export { RequestTelemetry, closedEnum, } from "./runtime/request-telemetry.js";
29
30
  export { APIFUSE__CDP_POOL__URL, APIFUSE__RESOLVER__2CAPTCHA__API_KEY, APIFUSE__RESOLVER__CAPMONSTER__API_KEY, APIFUSE__RESOLVER__CAPSOLVER__API_KEY, APIFUSE__RESOLVER__TIMEOUT_MS, DEFAULT_RESOLVER_TIMEOUT_MS, } from "./runtime/resolver-config.js";
30
31
  export { createUnsupportedResolverClient } from "./runtime/resolver-shared.js";
31
32
  export { assertRequiredSecretsPresent, listMissingRequiredSecrets, MISSING_SECRET_CODE, } from "./runtime/secrets.js";
@@ -1,5 +1,11 @@
1
1
  import type { ProxyAttemptTelemetryEvent, ProxyCacheStatus, ProxyProtocol, ProxyResolutionTelemetryEvent, ProxyTelemetrySink, ProxyUserAgentSource, ProxyVendorFailoverTelemetryEvent, ProxyVendorName, SmartproxyAllocatorBodyClass } from "../config/loader.js";
2
- export declare const PROVIDER_TELEMETRY_HEADER = "X-ApiFuse-Provider-Telemetry";
2
+ import { type ClosedEnum, type TelemetryContributor } from "./request-telemetry.js";
3
+ export { PROVIDER_TELEMETRY_HEADER } from "./request-telemetry.js";
4
+ declare const PROXY_HASH: unique symbol;
5
+ /** Bounded hexadecimal host hash used in proxy attempt samples. */
6
+ export type ProxyHash = ClosedEnum<string> & {
7
+ readonly [PROXY_HASH]: true;
8
+ };
3
9
  export type ProxyTelemetryResolvedPayload = {
4
10
  kind: "resolved";
5
11
  provider: ProxyVendorName;
@@ -77,11 +83,56 @@ export type ProxyTelemetryUnresolvedPayload = {
77
83
  }[];
78
84
  };
79
85
  export type ProxyTelemetryLogPayload = ProxyTelemetryResolvedPayload | ProxyTelemetryUnresolvedPayload;
80
- export declare class ProxyTelemetryCollector implements ProxyTelemetrySink {
86
+ /** Concrete gateway-safe projection of the unchanged proxy sibling. */
87
+ export type ProxyTelemetryHeaderPayload = {
88
+ kind: ClosedEnum<"resolved" | "unresolved">;
89
+ provider?: ClosedEnum<ProxyVendorName>;
90
+ userAgentSource?: ClosedEnum<ProxyUserAgentSource>;
91
+ protocol?: ClosedEnum<ProxyProtocol>;
92
+ cacheStatus?: ClosedEnum<ProxyCacheStatus>;
93
+ cacheHit?: boolean;
94
+ resolutionMs?: number;
95
+ allocatorMs?: number;
96
+ allocatorStatus?: number;
97
+ allocatorBodyClass?: ClosedEnum<SmartproxyAllocatorBodyClass>;
98
+ allocatorAttempts?: number;
99
+ lockWaitMs?: number;
100
+ redisReadMs?: number;
101
+ redisWriteMs?: number;
102
+ poolAgeMs?: number;
103
+ poolExpiresInMs?: number;
104
+ attempts?: number;
105
+ refreshes?: number;
106
+ attemptSamples?: {
107
+ n: number;
108
+ a: number;
109
+ i?: number;
110
+ h?: ProxyHash;
111
+ o: ClosedEnum<ProxyAttemptTelemetryEvent["outcome"]>;
112
+ c?: ClosedEnum<string>;
113
+ s?: number;
114
+ d?: number;
115
+ }[];
116
+ vendors?: ClosedEnum<ProxyVendorName>[];
117
+ failovers?: {
118
+ v: ClosedEnum<ProxyVendorName>;
119
+ nx?: ClosedEnum<ProxyVendorName>;
120
+ p: ClosedEnum<ProxyVendorFailoverTelemetryEvent["phase"]>;
121
+ r: ClosedEnum<ProxyVendorFailoverTelemetryEvent["reason"]>;
122
+ a?: number;
123
+ }[];
124
+ };
125
+ export declare class ProxyTelemetryCollector implements ProxyTelemetrySink, TelemetryContributor<ProxyTelemetryLogPayload, ProxyTelemetryHeaderPayload> {
81
126
  #private;
127
+ readonly key: "proxy";
82
128
  recordProxyResolution(event: ProxyResolutionTelemetryEvent): void;
129
+ /** Number of raw resolution events retained for bounded-history verification. */
130
+ get retainedResolutionEventCount(): number;
131
+ /** Total resolutions included in the incremental aggregate. */
132
+ get resolutionEventCount(): number;
83
133
  recordProxyVendorFailover(event: ProxyVendorFailoverTelemetryEvent): void;
84
134
  recordProxyAttempt(event: ProxyAttemptTelemetryEvent): void;
85
135
  toLogPayload(): ProxyTelemetryLogPayload | undefined;
136
+ toHeaderPayload(log: ProxyTelemetryLogPayload): ProxyTelemetryHeaderPayload;
86
137
  toHeaderValue(): string | undefined;
87
138
  }
@@ -1,7 +1,11 @@
1
- export const PROVIDER_TELEMETRY_HEADER = "X-ApiFuse-Provider-Telemetry";
2
- const MAX_HEADER_BYTES = 4_096;
1
+ import { closedEnum, RequestTelemetry, } from "./request-telemetry.js";
2
+ import { createTraceContext } from "./trace.js";
3
+ export { PROVIDER_TELEMETRY_HEADER } from "./request-telemetry.js";
3
4
  const MAX_PROXY_ATTEMPT_SAMPLES = 24;
4
5
  const MAX_PROXY_FAILOVER_SAMPLES = 12;
6
+ const MAX_RETAINED_PROXY_RESOLUTIONS = 64;
7
+ const PROXY_HEADER_TOKEN = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$/;
8
+ const PROXY_HEADER_HASH = /^[0-9a-f]{1,16}$/;
5
9
  const CACHE_STATUS_SEVERITY = {
6
10
  disabled: 0,
7
11
  memory_hit: 1,
@@ -23,17 +27,39 @@ function maxOptional(left, right) {
23
27
  function worseStatus(left, right) {
24
28
  return CACHE_STATUS_SEVERITY[right] > CACHE_STATUS_SEVERITY[left] ? right : left;
25
29
  }
26
- function encodeBase64Url(value) {
27
- return Buffer.from(value, "utf8").toString("base64url");
30
+ function aggregateResolution(acc, event) {
31
+ return {
32
+ provider: event.provider,
33
+ userAgentSource: event.userAgentSource ?? acc.userAgentSource,
34
+ cacheStatus: worseStatus(acc.cacheStatus, event.cacheStatus),
35
+ cacheHit: acc.cacheHit && event.cacheHit,
36
+ resolutionMs: acc.resolutionMs + event.resolutionMs,
37
+ allocatorMs: sumOptional(acc.allocatorMs, event.allocatorMs),
38
+ allocatorStatus: event.allocatorStatus ?? acc.allocatorStatus,
39
+ allocatorBodyClass: event.allocatorBodyClass ?? acc.allocatorBodyClass,
40
+ allocatorAttempts: sumOptional(acc.allocatorAttempts, event.allocatorAttempts),
41
+ lockWaitMs: sumOptional(acc.lockWaitMs, event.lockWaitMs),
42
+ redisReadMs: sumOptional(acc.redisReadMs, event.redisReadMs),
43
+ redisWriteMs: sumOptional(acc.redisWriteMs, event.redisWriteMs),
44
+ poolAgeMs: maxOptional(acc.poolAgeMs, event.poolAgeMs),
45
+ poolExpiresInMs: maxOptional(acc.poolExpiresInMs, event.poolExpiresInMs),
46
+ attempts: acc.attempts + event.attempts,
47
+ refreshes: sumOptional(acc.refreshes, event.refreshes),
48
+ };
28
49
  }
29
50
  export class ProxyTelemetryCollector {
51
+ key = "proxy";
30
52
  #events = [];
53
+ #resolutionCount = 0;
54
+ #aggregateResolution;
55
+ #lastSuccessfulResolution;
56
+ #vendors = [];
31
57
  #attempts = [];
32
58
  #failovers = [];
33
59
  #unresolvedVendors = [];
34
60
  recordProxyResolution(event) {
35
61
  const outcome = event.outcome === "error" ? "error" : "ok";
36
- this.#events.push({
62
+ const normalized = {
37
63
  provider: event.provider,
38
64
  outcome,
39
65
  ...(event.userAgentSource ? { userAgentSource: event.userAgentSource } : {}),
@@ -58,11 +84,29 @@ export class ProxyTelemetryCollector {
58
84
  : Math.max(0, Math.floor(event.poolExpiresInMs)),
59
85
  attempts: Math.max(1, Math.floor(event.attempts || 1)),
60
86
  refreshes: event.refreshes === undefined ? undefined : Math.max(0, Math.floor(event.refreshes)),
61
- });
87
+ };
88
+ this.#resolutionCount += 1;
89
+ if (this.#events.length < MAX_RETAINED_PROXY_RESOLUTIONS)
90
+ this.#events.push(normalized);
91
+ this.#aggregateResolution = this.#aggregateResolution
92
+ ? aggregateResolution(this.#aggregateResolution, normalized)
93
+ : normalized;
94
+ if (outcome === "ok")
95
+ this.#lastSuccessfulResolution = normalized;
96
+ if (!this.#vendors.includes(event.provider))
97
+ this.#vendors.push(event.provider);
62
98
  if (outcome === "error" && !this.#unresolvedVendors.includes(event.provider)) {
63
99
  this.#unresolvedVendors.push(event.provider);
64
100
  }
65
101
  }
102
+ /** Number of raw resolution events retained for bounded-history verification. */
103
+ get retainedResolutionEventCount() {
104
+ return this.#events.length;
105
+ }
106
+ /** Total resolutions included in the incremental aggregate. */
107
+ get resolutionEventCount() {
108
+ return this.#resolutionCount;
109
+ }
66
110
  recordProxyVendorFailover(event) {
67
111
  if (this.#failovers.length >= MAX_PROXY_FAILOVER_SAMPLES)
68
112
  return;
@@ -96,8 +140,7 @@ export class ProxyTelemetryCollector {
96
140
  });
97
141
  }
98
142
  toLogPayload() {
99
- const [first, ...rest] = this.#events;
100
- const okEvents = this.#events.filter((event) => event.outcome !== "error");
143
+ const aggregate = this.#aggregateResolution;
101
144
  const attemptSamples = this.#attempts.map((attempt, index) => ({
102
145
  n: index + 1,
103
146
  a: attempt.attempt,
@@ -115,31 +158,9 @@ export class ProxyTelemetryCollector {
115
158
  r: failover.reason,
116
159
  ...(failover.attempt === undefined ? {} : { a: failover.attempt }),
117
160
  }));
118
- if (okEvents.length === 0) {
119
- if (!first && failovers.length === 0)
161
+ if (!this.#lastSuccessfulResolution) {
162
+ if (!aggregate && failovers.length === 0)
120
163
  return undefined;
121
- const failureEvents = this.#events.filter((event) => event.outcome === "error");
122
- const [firstFailure, ...remainingFailures] = failureEvents;
123
- const aggregate = firstFailure
124
- ? remainingFailures.reduce((acc, event) => ({
125
- provider: event.provider,
126
- outcome: "error",
127
- cacheStatus: worseStatus(acc.cacheStatus, event.cacheStatus),
128
- cacheHit: acc.cacheHit && event.cacheHit,
129
- resolutionMs: acc.resolutionMs + event.resolutionMs,
130
- allocatorMs: sumOptional(acc.allocatorMs, event.allocatorMs),
131
- allocatorStatus: event.allocatorStatus ?? acc.allocatorStatus,
132
- allocatorBodyClass: event.allocatorBodyClass ?? acc.allocatorBodyClass,
133
- allocatorAttempts: sumOptional(acc.allocatorAttempts, event.allocatorAttempts),
134
- lockWaitMs: sumOptional(acc.lockWaitMs, event.lockWaitMs),
135
- redisReadMs: sumOptional(acc.redisReadMs, event.redisReadMs),
136
- redisWriteMs: sumOptional(acc.redisWriteMs, event.redisWriteMs),
137
- poolAgeMs: maxOptional(acc.poolAgeMs, event.poolAgeMs),
138
- poolExpiresInMs: maxOptional(acc.poolExpiresInMs, event.poolExpiresInMs),
139
- attempts: acc.attempts + event.attempts,
140
- refreshes: sumOptional(acc.refreshes, event.refreshes),
141
- }), firstFailure)
142
- : undefined;
143
164
  return {
144
165
  kind: "unresolved",
145
166
  vendors: [...this.#unresolvedVendors],
@@ -180,30 +201,9 @@ export class ProxyTelemetryCollector {
180
201
  };
181
202
  }
182
203
  // The serving vendor/protocol is the last successful resolution.
183
- const serving = okEvents[okEvents.length - 1] ?? first;
184
- const vendors = [];
185
- for (const event of this.#events) {
186
- if (!vendors.includes(event.provider))
187
- vendors.push(event.provider);
188
- }
189
- const aggregate = rest.reduce((acc, event) => ({
190
- provider: event.provider,
191
- userAgentSource: event.userAgentSource ?? acc.userAgentSource,
192
- cacheStatus: worseStatus(acc.cacheStatus, event.cacheStatus),
193
- cacheHit: acc.cacheHit && event.cacheHit,
194
- resolutionMs: acc.resolutionMs + event.resolutionMs,
195
- allocatorMs: sumOptional(acc.allocatorMs, event.allocatorMs),
196
- allocatorStatus: event.allocatorStatus ?? acc.allocatorStatus,
197
- allocatorBodyClass: event.allocatorBodyClass ?? acc.allocatorBodyClass,
198
- allocatorAttempts: sumOptional(acc.allocatorAttempts, event.allocatorAttempts),
199
- lockWaitMs: sumOptional(acc.lockWaitMs, event.lockWaitMs),
200
- redisReadMs: sumOptional(acc.redisReadMs, event.redisReadMs),
201
- redisWriteMs: sumOptional(acc.redisWriteMs, event.redisWriteMs),
202
- poolAgeMs: maxOptional(acc.poolAgeMs, event.poolAgeMs),
203
- poolExpiresInMs: maxOptional(acc.poolExpiresInMs, event.poolExpiresInMs),
204
- attempts: acc.attempts + event.attempts,
205
- refreshes: sumOptional(acc.refreshes, event.refreshes),
206
- }), first);
204
+ if (!aggregate)
205
+ return undefined;
206
+ const serving = this.#lastSuccessfulResolution;
207
207
  return {
208
208
  kind: "resolved",
209
209
  provider: serving.provider,
@@ -232,18 +232,59 @@ export class ProxyTelemetryCollector {
232
232
  attempts: aggregate.attempts,
233
233
  ...(aggregate.refreshes !== undefined ? { refreshes: aggregate.refreshes } : {}),
234
234
  ...(attemptSamples.length > 0 ? { attemptSamples } : {}),
235
- ...(vendors.length > 1 ? { vendors } : {}),
235
+ ...(this.#vendors.length > 1 ? { vendors: [...this.#vendors] } : {}),
236
236
  ...(failovers.length > 0 ? { failovers } : {}),
237
237
  };
238
238
  }
239
+ toHeaderPayload(log) {
240
+ const attemptSamples = log.attemptSamples?.map((sample) => ({
241
+ n: sample.n,
242
+ a: sample.a,
243
+ ...(sample.i === undefined ? {} : { i: sample.i }),
244
+ ...(sample.h && PROXY_HEADER_HASH.test(sample.h)
245
+ ? { h: closedEnum(sample.h) }
246
+ : {}),
247
+ o: closedEnum(sample.o),
248
+ ...(sample.c && PROXY_HEADER_TOKEN.test(sample.c) ? { c: closedEnum(sample.c) } : {}),
249
+ ...(sample.s === undefined ? {} : { s: sample.s }),
250
+ ...(sample.d === undefined ? {} : { d: sample.d }),
251
+ }));
252
+ const failovers = log.failovers?.map((failover) => ({
253
+ ...failover,
254
+ v: closedEnum(failover.v),
255
+ ...(failover.nx ? { nx: closedEnum(failover.nx) } : {}),
256
+ p: closedEnum(failover.p),
257
+ r: closedEnum(failover.r),
258
+ }));
259
+ if (log.kind === "resolved") {
260
+ return {
261
+ ...log,
262
+ kind: closedEnum(log.kind),
263
+ provider: closedEnum(log.provider),
264
+ ...(log.userAgentSource ? { userAgentSource: closedEnum(log.userAgentSource) } : {}),
265
+ ...(log.protocol ? { protocol: closedEnum(log.protocol) } : {}),
266
+ cacheStatus: closedEnum(log.cacheStatus),
267
+ ...(log.allocatorBodyClass
268
+ ? { allocatorBodyClass: closedEnum(log.allocatorBodyClass) }
269
+ : {}),
270
+ ...(attemptSamples ? { attemptSamples } : {}),
271
+ ...(log.vendors ? { vendors: log.vendors.map((vendor) => closedEnum(vendor)) } : {}),
272
+ ...(failovers ? { failovers } : {}),
273
+ };
274
+ }
275
+ return {
276
+ ...log,
277
+ kind: closedEnum(log.kind),
278
+ vendors: log.vendors.map((vendor) => closedEnum(vendor)),
279
+ ...(log.cacheStatus ? { cacheStatus: closedEnum(log.cacheStatus) } : {}),
280
+ ...(log.allocatorBodyClass ? { allocatorBodyClass: closedEnum(log.allocatorBodyClass) } : {}),
281
+ ...(failovers ? { failovers } : {}),
282
+ ...(attemptSamples ? { attemptSamples } : {}),
283
+ };
284
+ }
239
285
  toHeaderValue() {
240
- const proxy = this.toLogPayload();
241
- if (!proxy)
242
- return undefined;
243
- const payload = { v: 1, proxy };
244
- const encoded = encodeBase64Url(JSON.stringify(payload));
245
- if (encoded.length > MAX_HEADER_BYTES)
246
- return undefined;
247
- return encoded;
286
+ const telemetry = new RequestTelemetry(createTraceContext());
287
+ telemetry.register(this);
288
+ return telemetry.toHeaderValue();
248
289
  }
249
290
  }
@@ -0,0 +1,70 @@
1
+ import type { ProxyTelemetrySink } from "../config/loader.js";
2
+ import type { ProxyTelemetryLogPayload } from "./proxy-telemetry.js";
3
+ import type { Span, TraceContext } from "./trace.js";
4
+ export declare const PROVIDER_TELEMETRY_HEADER = "X-ApiFuse-Provider-Telemetry";
5
+ export type TelemetryKey = "proxy" | "resolver" | "http" | "stealth" | "native" | "browser" | "ocr" | "stt" | "cache" | "state" | "events";
6
+ declare const CLOSED_ENUM: unique symbol;
7
+ /** A string from an SDK-declared, closed observability taxonomy. */
8
+ export type ClosedEnum<T extends string> = T & {
9
+ readonly [CLOSED_ENUM]: true;
10
+ };
11
+ /** Brands a value from an SDK-declared closed string union without changing it. */
12
+ export declare function closedEnum<T extends string>(value: T): ClosedEnum<T>;
13
+ /** Explicit exemption for the existing implementation-shaped cache key list. */
14
+ export type TenantOpaqueKeys = string[] & {
15
+ readonly __tenantOpaqueCacheKeys: true;
16
+ };
17
+ /** Names the deliberate tenant cache-key exemption without changing its wire value. */
18
+ export declare function tenantOpaqueKeys(value: string[]): TenantOpaqueKeys;
19
+ /**
20
+ * Compile-time projection for the gateway ingestion contract. Plain strings,
21
+ * URLs, hostnames, free text, unknown values, and escape-hatch properties reduce to `never`.
22
+ */
23
+ export type GatewayIngestible<T> = 0 extends 1 & T ? never : T extends object ? keyof T extends never ? never : {
24
+ [K in keyof T]: 0 extends 1 & T[K] ? never : unknown extends T[K] ? never : [NonNullable<T[K]>] extends [number | boolean | ClosedEnum<string>] ? T[K] : [NonNullable<T[K]>] extends [readonly (infer U)[]] ? 0 extends 1 & U ? never : unknown extends U ? never : [U] extends [
25
+ number | boolean | ClosedEnum<string> | GatewayIngestible<U>
26
+ ] ? T[K] : never : [NonNullable<T[K]>] extends [object] ? [NonNullable<T[K]>] extends [GatewayIngestible<NonNullable<T[K]>>] ? T[K] : never : never;
27
+ } : never;
28
+ /** Compile-time projection for tenant-visible, identity-neutral metadata. */
29
+ export type TenantNeutral<T> = 0 extends 1 & T ? never : T extends object ? {
30
+ [K in keyof T]: K extends `vendor${string}` | "provider" | "engine" | "model" | `${string}Host` ? never : 0 extends 1 & T[K] ? never : unknown extends T[K] ? never : [NonNullable<T[K]>] extends [
31
+ string[] & {
32
+ readonly __tenantOpaqueCacheKeys: true;
33
+ }
34
+ ] ? K extends "keys" ? T[K] : never : [NonNullable<T[K]>] extends [number | boolean | ClosedEnum<string>] ? T[K] : [NonNullable<T[K]>] extends [object] ? [NonNullable<T[K]>] extends [TenantNeutral<NonNullable<T[K]>>] ? T[K] : never : never;
35
+ } : never;
36
+ export interface SpanIndex {
37
+ readonly spans: readonly Span[];
38
+ readonly byName: ReadonlyMap<string, readonly Span[]>;
39
+ count(name: string): number;
40
+ durationMs(name: string): number;
41
+ }
42
+ export interface TelemetryContributor<Log extends object, Header extends object> {
43
+ readonly key: TelemetryKey;
44
+ toLogPayload(spans: SpanIndex): Log | undefined;
45
+ toHeaderPayload(log: Log): 0 extends 1 & Header ? never : [Header] extends [GatewayIngestible<Header>] ? Header | undefined : never;
46
+ }
47
+ export type RequestTelemetryLogPayload = {
48
+ proxy?: ProxyTelemetryLogPayload;
49
+ } & Partial<Record<Exclude<TelemetryKey, "proxy">, object>>;
50
+ /**
51
+ * Bounded structural defence in depth against casts and accidental free text.
52
+ * Closed-enum brands are erased at runtime; the type-level guard is the contract,
53
+ * while this check only enforces safe token shapes and collection bounds.
54
+ */
55
+ export declare function isGatewayIngestible(value: unknown): boolean;
56
+ /** One finalisation owner for every request-scoped telemetry contributor. */
57
+ export declare class RequestTelemetry {
58
+ #private;
59
+ readonly trace: TraceContext;
60
+ readonly contributors: Readonly<Partial<Record<TelemetryKey, {
61
+ readonly key: TelemetryKey;
62
+ toLogPayload(spans: SpanIndex): object | undefined;
63
+ }>>>;
64
+ constructor(trace: TraceContext);
65
+ register<Log extends object, Header extends object>(contributor: TelemetryContributor<Log, Header> & (0 extends 1 & Log ? never : 0 extends 1 & Header ? never : [Header] extends [GatewayIngestible<Header>] ? unknown : never)): void;
66
+ get proxy(): ProxyTelemetrySink | undefined;
67
+ toLogPayload(): RequestTelemetryLogPayload | undefined;
68
+ toHeaderValue(): string | undefined;
69
+ }
70
+ export {};