@push.rocks/smartproxy 27.18.0 → 27.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/changelog.md +28 -0
  2. package/dist_rust/rustproxy_linux_amd64 +0 -0
  3. package/dist_rust/rustproxy_linux_arm64 +0 -0
  4. package/dist_ts/00_commitinfo_data.js +1 -1
  5. package/dist_ts/core/models/route-context.d.ts +2 -0
  6. package/dist_ts/core/models/route-context.js +1 -1
  7. package/dist_ts/proxies/smart-proxy/datagram-handler-server.js +6 -4
  8. package/dist_ts/proxies/smart-proxy/models/metrics-types.d.ts +12 -0
  9. package/dist_ts/proxies/smart-proxy/models/route-types.d.ts +8 -0
  10. package/dist_ts/proxies/smart-proxy/models/route-types.js +1 -1
  11. package/dist_ts/proxies/smart-proxy/models/rust-types.d.ts +14 -0
  12. package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.d.ts +8 -1
  13. package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.js +73 -7
  14. package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.d.ts +2 -1
  15. package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.js +9 -1
  16. package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +5 -1
  17. package/dist_ts/proxies/smart-proxy/smart-proxy.js +7 -1
  18. package/dist_ts/proxies/smart-proxy/socket-handler-server.js +2 -1
  19. package/dist_ts/proxies/smart-proxy/utils/route-validator.js +28 -1
  20. package/package.json +1 -1
  21. package/ts/00_commitinfo_data.ts +1 -1
  22. package/ts/core/models/route-context.ts +2 -0
  23. package/ts/proxies/smart-proxy/datagram-handler-server.ts +14 -3
  24. package/ts/proxies/smart-proxy/models/metrics-types.ts +14 -0
  25. package/ts/proxies/smart-proxy/models/route-types.ts +10 -0
  26. package/ts/proxies/smart-proxy/models/rust-types.ts +16 -0
  27. package/ts/proxies/smart-proxy/rust-metrics-adapter.ts +110 -7
  28. package/ts/proxies/smart-proxy/rust-proxy-bridge.ts +14 -0
  29. package/ts/proxies/smart-proxy/smart-proxy.ts +8 -1
  30. package/ts/proxies/smart-proxy/socket-handler-server.ts +1 -0
  31. package/ts/proxies/smart-proxy/utils/route-validator.ts +28 -0
@@ -29,6 +29,18 @@ export interface IThroughputHistoryPoint {
29
29
  out: number;
30
30
  }
31
31
 
32
+ export interface IProtocolHistoryPoint {
33
+ timestamp: number;
34
+ total: number;
35
+ protocols: Record<string, number>;
36
+ }
37
+
38
+ export interface IMetricsHistory {
39
+ throughputHistory: IThroughputHistoryPoint[];
40
+ frontendConnectionHistory: IProtocolHistoryPoint[];
41
+ backendConnectionHistory: IProtocolHistoryPoint[];
42
+ }
43
+
32
44
  export interface IRequestRateMetrics {
33
45
  perSecond: number;
34
46
  lastMinute: number;
@@ -68,6 +80,8 @@ export interface IMetrics {
68
80
  topDomainRequests(limit?: number): Array<{ ip: string; domain: string; count: number }>;
69
81
  frontendProtocols(): IProtocolDistribution;
70
82
  backendProtocols(): IProtocolDistribution;
83
+ frontendHistory(seconds: number): Array<IProtocolHistoryPoint>;
84
+ backendHistory(seconds: number): Array<IProtocolHistoryPoint>;
71
85
  };
72
86
 
73
87
  // Throughput metrics (bytes per second)
@@ -24,6 +24,11 @@ export type TTlsMode = 'passthrough' | 'terminate' | 'terminate-and-reencrypt';
24
24
  */
25
25
  export type TTransportProtocol = 'tcp' | 'udp' | 'all';
26
26
 
27
+ /**
28
+ * Trusted transport path established by SmartProxy for an accepted connection.
29
+ */
30
+ export type TIngressOrigin = 'direct' | 'trustedProxy' | 'smartVpn';
31
+
27
32
  /**
28
33
  * Inbound PROXY protocol handling for a listener.
29
34
  */
@@ -197,6 +202,11 @@ export interface IRouteSecurity {
197
202
  allowedAssignedIps?: string[];
198
203
  };
199
204
 
205
+ ingress?: {
206
+ /** Omit ingress entirely for legacy all-origin behavior. */
207
+ allowedOrigins: TIngressOrigin[];
208
+ };
209
+
200
210
  /** Browser-facing challenge policy. SmartProxy enforces this in the HTTP path. */
201
211
  challenge?: plugins.smartchallenge.IRouteChallengeConfig;
202
212
  }
@@ -105,6 +105,22 @@ export interface IRustThroughputSample {
105
105
  bytesOut: number;
106
106
  }
107
107
 
108
+ export interface IRustProtocolHistorySample {
109
+ timestampMs: number;
110
+ total: number;
111
+ h1Active: number;
112
+ h2Active: number;
113
+ h3Active: number;
114
+ wsActive: number;
115
+ otherActive: number;
116
+ }
117
+
118
+ export interface IRustMetricsHistory {
119
+ throughputHistory: IRustThroughputSample[];
120
+ frontendProtocolHistory: IRustProtocolHistorySample[];
121
+ backendProtocolHistory: IRustProtocolHistorySample[];
122
+ }
123
+
108
124
  export interface IRustRouteMetrics {
109
125
  activeConnections: number;
110
126
  totalConnections: number;
@@ -1,6 +1,76 @@
1
- import type { IMetrics, IBackendMetrics, IProtocolCacheEntry, IProtocolDistribution, IRequestRateMetrics, IThroughputData, IThroughputHistoryPoint } from './models/metrics-types.js';
1
+ import type {
2
+ IMetrics,
3
+ IMetricsHistory,
4
+ IBackendMetrics,
5
+ IProtocolCacheEntry,
6
+ IProtocolDistribution,
7
+ IProtocolHistoryPoint,
8
+ IRequestRateMetrics,
9
+ IThroughputData,
10
+ IThroughputHistoryPoint,
11
+ } from './models/metrics-types.js';
2
12
  import type { RustProxyBridge } from './rust-proxy-bridge.js';
3
- import type { IRustBackendMetrics, IRustHttpDomainRequestMetrics, IRustIpMetrics, IRustMetricsSnapshot, IRustRouteMetrics } from './models/rust-types.js';
13
+ import type {
14
+ IRustBackendMetrics,
15
+ IRustHttpDomainRequestMetrics,
16
+ IRustIpMetrics,
17
+ IRustMetricsHistory,
18
+ IRustMetricsSnapshot,
19
+ IRustProtocolHistorySample,
20
+ IRustRouteMetrics,
21
+ } from './models/rust-types.js';
22
+
23
+ function normalizeHistoryWindow(seconds: number, available: number): number {
24
+ if (!Number.isFinite(seconds) || seconds <= 0 || available <= 0) return 0;
25
+ return Math.min(Math.floor(seconds), available);
26
+ }
27
+
28
+ function sliceHistory<T>(points: T[], seconds: number): T[] {
29
+ const window = normalizeHistoryWindow(seconds, points.length);
30
+ return window === 0 ? [] : points.slice(-window);
31
+ }
32
+
33
+ function sliceWallClockHistory<T extends { timestampMs: number }>(
34
+ points: T[],
35
+ seconds: number,
36
+ ): T[] {
37
+ const requestedSeconds = Number.isFinite(seconds) ? Math.floor(seconds) : 0;
38
+ if (requestedSeconds <= 0 || points.length === 0) return [];
39
+
40
+ const newestTimestamp = points[points.length - 1].timestampMs;
41
+ const oldestTimestamp = newestTimestamp - (requestedSeconds - 1) * 1_000;
42
+ return points
43
+ .filter((point) => point.timestampMs >= oldestTimestamp)
44
+ .slice(-requestedSeconds);
45
+ }
46
+
47
+ function mergeThroughputHistory(
48
+ cachedHistory: IRustMetricsHistory['throughputHistory'],
49
+ snapshotHistory: IRustMetricsSnapshot['throughputHistory'],
50
+ ): IRustMetricsHistory['throughputHistory'] {
51
+ const mergedByTimestamp = new Map<number, IRustMetricsHistory['throughputHistory'][number]>();
52
+ for (const point of cachedHistory) {
53
+ mergedByTimestamp.set(point.timestampMs, point);
54
+ }
55
+ for (const point of snapshotHistory) {
56
+ mergedByTimestamp.set(point.timestampMs, point);
57
+ }
58
+ return [...mergedByTimestamp.values()].sort((a, b) => a.timestampMs - b.timestampMs);
59
+ }
60
+
61
+ function mapProtocolHistoryPoint(sample: IRustProtocolHistorySample): IProtocolHistoryPoint {
62
+ return {
63
+ timestamp: sample.timestampMs,
64
+ total: sample.total,
65
+ protocols: {
66
+ 'HTTP/1.1': sample.h1Active,
67
+ 'HTTP/2': sample.h2Active,
68
+ 'HTTP/3': sample.h3Active,
69
+ WebSocket: sample.wsActive,
70
+ Other: sample.otherActive,
71
+ },
72
+ };
73
+ }
4
74
 
5
75
  /**
6
76
  * Adapts Rust JSON metrics to the IMetrics interface.
@@ -16,6 +86,7 @@ import type { IRustBackendMetrics, IRustHttpDomainRequestMetrics, IRustIpMetrics
16
86
  export class RustMetricsAdapter implements IMetrics {
17
87
  private bridge: RustProxyBridge;
18
88
  private cache: IRustMetricsSnapshot | null = null;
89
+ private historyCache: IRustMetricsHistory | null = null;
19
90
  private pollTimer: ReturnType<typeof setInterval> | null = null;
20
91
  private pollIntervalMs: number;
21
92
 
@@ -35,6 +106,27 @@ export class RustMetricsAdapter implements IMetrics {
35
106
  }
36
107
  }
37
108
 
109
+ /**
110
+ * Fetch histories only when an observer needs them; regular polling stays lightweight.
111
+ */
112
+ public async refreshHistory(seconds: number): Promise<IMetricsHistory> {
113
+ const history = await this.bridge.getMetricsHistory(seconds);
114
+ this.historyCache = history;
115
+ return {
116
+ throughputHistory: history.throughputHistory.map((point) => ({
117
+ timestamp: point.timestampMs,
118
+ in: point.bytesIn,
119
+ out: point.bytesOut,
120
+ })),
121
+ frontendConnectionHistory: history.frontendProtocolHistory.map(
122
+ mapProtocolHistoryPoint,
123
+ ),
124
+ backendConnectionHistory: history.backendProtocolHistory.map(
125
+ mapProtocolHistoryPoint,
126
+ ),
127
+ };
128
+ }
129
+
38
130
  public startPolling(): void {
39
131
  if (this.pollTimer) return;
40
132
  // Immediate first poll so cache is populated ASAP
@@ -154,6 +246,14 @@ export class RustMetricsAdapter implements IMetrics {
154
246
  otherTotal: bp?.otherTotal ?? 0,
155
247
  };
156
248
  },
249
+ frontendHistory: (seconds: number): Array<IProtocolHistoryPoint> => {
250
+ const history = this.historyCache?.frontendProtocolHistory ?? [];
251
+ return sliceHistory(history, seconds).map(mapProtocolHistoryPoint);
252
+ },
253
+ backendHistory: (seconds: number): Array<IProtocolHistoryPoint> => {
254
+ const history = this.historyCache?.backendProtocolHistory ?? [];
255
+ return sliceHistory(history, seconds).map(mapProtocolHistoryPoint);
256
+ },
157
257
  };
158
258
 
159
259
  public throughput = {
@@ -176,11 +276,14 @@ export class RustMetricsAdapter implements IMetrics {
176
276
  return this.throughput.instant();
177
277
  },
178
278
  history: (seconds: number): Array<IThroughputHistoryPoint> => {
179
- if (!this.cache?.throughputHistory) return [];
180
- return this.cache.throughputHistory.slice(-seconds).map((p) => ({
181
- timestamp: p.timestampMs,
182
- in: p.bytesIn,
183
- out: p.bytesOut,
279
+ const history = mergeThroughputHistory(
280
+ this.historyCache?.throughputHistory ?? [],
281
+ this.cache?.throughputHistory ?? [],
282
+ );
283
+ return sliceWallClockHistory(history, seconds).map((point) => ({
284
+ timestamp: point.timestampMs,
285
+ in: point.bytesIn,
286
+ out: point.bytesOut,
184
287
  }));
185
288
  },
186
289
  byRoute: (_windowSeconds?: number): Map<string, IThroughputData> => {
@@ -5,12 +5,14 @@ import type {
5
5
  IRustChallengeOptions,
6
6
  IRustMetricsSnapshot,
7
7
  IRustProxyOptions,
8
+ IRustMetricsHistory,
8
9
  IRustRouteConfig,
9
10
  IRustStatistics,
10
11
  } from './models/rust-types.js';
11
12
  import type { IActiveConnectionSnapshot, IActiveConnectionSnapshotOptions, IActiveConnectionSnapshotPage, ISmartProxySecurityPolicy } from './models/interfaces.js';
12
13
 
13
14
  const MAXIMUM_RUST_CONNECTION_ID = 18_446_744_073_709_551_615n;
15
+ const MAXIMUM_METRICS_HISTORY_SECONDS = 600;
14
16
 
15
17
  function validateActiveConnectionSnapshotOptions(options: IActiveConnectionSnapshotOptions): void {
16
18
  if (options.afterId === undefined) {
@@ -33,6 +35,7 @@ type TSmartProxyCommands = {
33
35
  updateRoutes: { params: { routes: IRustRouteConfig[] }; result: void };
34
36
  setSecurityPolicy: { params: { policy: ISmartProxySecurityPolicy }; result: void };
35
37
  getMetrics: { params: Record<string, never>; result: IRustMetricsSnapshot };
38
+ getMetricsHistory: { params: { seconds: number }; result: IRustMetricsHistory };
36
39
  getStatistics: { params: Record<string, never>; result: IRustStatistics };
37
40
  getActiveConnectionSnapshots: { params: IActiveConnectionSnapshotOptions; result: IActiveConnectionSnapshot[] };
38
41
  getActiveConnectionSnapshotPage: { params: IActiveConnectionSnapshotOptions; result: IActiveConnectionSnapshotPage };
@@ -167,6 +170,17 @@ export class RustProxyBridge extends plugins.EventEmitter {
167
170
  return this.bridge.sendCommand('getMetrics', {} as Record<string, never>);
168
171
  }
169
172
 
173
+ public async getMetricsHistory(seconds: number): Promise<IRustMetricsHistory> {
174
+ if (!Number.isFinite(seconds) || seconds < 0) {
175
+ throw new Error('Metrics history seconds must be a finite non-negative number');
176
+ }
177
+ const boundedSeconds = Math.min(
178
+ MAXIMUM_METRICS_HISTORY_SECONDS,
179
+ Math.floor(seconds),
180
+ );
181
+ return this.bridge.sendCommand('getMetricsHistory', { seconds: boundedSeconds });
182
+ }
183
+
170
184
  public async getStatistics(): Promise<IRustStatistics> {
171
185
  return this.bridge.sendCommand('getStatistics', {} as Record<string, never>);
172
186
  }
@@ -20,7 +20,7 @@ import { ConcurrencySemaphore } from './utils/concurrency-semaphore.js';
20
20
  // Types
21
21
  import type { ISmartProxyOptions, ISmartProxySecurityPolicy, TSmartProxyCertProvisionObject, IAcmeOptions, ICertProvisionEventComms, ICertificateIssuedEvent, ICertificateFailedEvent, ISmartProxyCertProvisionCertificate, IActiveConnectionSnapshot, IActiveConnectionSnapshotOptions, IActiveConnectionSnapshotPage } from './models/interfaces.js';
22
22
  import type { IRouteConfig } from './models/route-types.js';
23
- import type { IMetrics } from './models/metrics-types.js';
23
+ import type { IMetrics, IMetricsHistory } from './models/metrics-types.js';
24
24
  import type { IRustCertificateStatus, IRustChallengeOptions, IRustProxyOptions, IRustStatistics } from './models/rust-types.js';
25
25
 
26
26
  type TChallengeProvider = plugins.smartchallenge.IChallengeProvider;
@@ -496,6 +496,13 @@ export class SmartProxy extends plugins.EventEmitter {
496
496
  return this.metricsAdapter;
497
497
  }
498
498
 
499
+ /**
500
+ * Refresh bounded histories on demand without expanding the periodic metrics poll.
501
+ */
502
+ public async refreshMetricsHistory(seconds: number): Promise<IMetricsHistory> {
503
+ return this.metricsAdapter.refreshHistory(seconds);
504
+ }
505
+
499
506
  /**
500
507
  * Get sanitized active connection snapshots from the Rust engine.
501
508
  */
@@ -169,6 +169,7 @@ export class SocketHandlerServer {
169
169
  domain: metadata.domain,
170
170
  clientIp: metadata.remoteIP || 'unknown',
171
171
  serverIp: '0.0.0.0',
172
+ ingressOrigin: metadata.ingressOrigin,
172
173
  vpn: metadata.vpn,
173
174
  path: metadata.path,
174
175
  isTls: metadata.isTLS || false,
@@ -269,6 +269,34 @@ export class RouteValidator {
269
269
  }
270
270
  }
271
271
 
272
+ if (route.security.ingress) {
273
+ const origins = route.security.ingress.allowedOrigins;
274
+ if (!Array.isArray(origins) || origins.length === 0) {
275
+ errors.push('security.ingress.allowedOrigins must be a non-empty array');
276
+ } else {
277
+ const supportedOrigins = new Set(['direct', 'trustedProxy', 'smartVpn']);
278
+ const uniqueOrigins = new Set(origins);
279
+ if (uniqueOrigins.size !== origins.length) {
280
+ errors.push('security.ingress.allowedOrigins must not contain duplicates');
281
+ }
282
+ for (const origin of origins) {
283
+ if (!supportedOrigins.has(origin)) {
284
+ errors.push(`Invalid security ingress origin: ${origin}`);
285
+ }
286
+ }
287
+
288
+ const inboundMode = route.match.inboundProxyProtocol?.mode;
289
+ const allowsDirect = origins.includes('direct');
290
+ const allowsTrustedProxy = origins.includes('trustedProxy') || origins.includes('smartVpn');
291
+ if (allowsDirect && inboundMode === 'required') {
292
+ errors.push('inboundProxyProtocol.required conflicts with the allowed direct ingress origin');
293
+ }
294
+ if (allowsTrustedProxy && inboundMode === 'reject') {
295
+ errors.push('inboundProxyProtocol.reject conflicts with trustedProxy or smartVpn ingress origins');
296
+ }
297
+ }
298
+ }
299
+
272
300
  // Validate connection limits
273
301
  if (route.security.maxConnections && route.security.maxConnections < 0) {
274
302
  errors.push('Max connections must be positive');