From bca3748757e472adec18f12c5736302b2238d699 Mon Sep 17 00:00:00 2001 From: Oskar Eichler Date: Thu, 27 Aug 2026 21:44:57 +0300 Subject: [PATCH 1/6] fix(server): preserve custom access-key IDs in metrics responses --- src/shadowbox/server/api.yml | 5 ++++- src/shadowbox/server/manager_metrics.ts | 9 +++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/shadowbox/server/api.yml b/src/shadowbox/server/api.yml index 2532d51c..47e6d5c1 100644 --- a/src/shadowbox/server/api.yml +++ b/src/shadowbox/server/api.yml @@ -208,7 +208,10 @@ paths: type: object properties: accessKeyId: - type: integer + description: Numeric for canonical safe integer IDs; otherwise the exact custom access key ID. + oneOf: + - type: integer + - type: string tunnelTime: type: object properties: diff --git a/src/shadowbox/server/manager_metrics.ts b/src/shadowbox/server/manager_metrics.ts index 422f1162..4f9dd174 100644 --- a/src/shadowbox/server/manager_metrics.ts +++ b/src/shadowbox/server/manager_metrics.ts @@ -61,7 +61,7 @@ interface ServerMetricsLocationEntry { } interface ServerMetricsAccessKeyEntry { - accessKeyId: number; + accessKeyId: number | string; tunnelTime: Duration; dataTransferred: Data; connection: ConnectionStats; @@ -297,7 +297,12 @@ function getServerMetricsAccessKeyEntry( let entry = map.get(accessKey); if (entry === undefined) { entry = { - accessKeyId: parseInt(accessKey), + // Preserve the existing numeric representation only when it is lossless. + // Custom IDs (including leading zeros and large integers) are valid keys. + accessKeyId: + Number.isSafeInteger(Number(accessKey)) && String(Number(accessKey)) === accessKey + ? Number(accessKey) + : accessKey, dataTransferred: {bytes: 0}, tunnelTime: {seconds: 0}, connection: { From 54cbe1f4c468e39f1fb26cc14290c8b2d55e4a8d Mon Sep 17 00:00:00 2001 From: Oskar Eichler Date: Thu, 27 Aug 2026 22:36:00 +0300 Subject: [PATCH 2/6] fix(server): preserve prototype-named usage keys --- src/shadowbox/server/manager_metrics.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/shadowbox/server/manager_metrics.ts b/src/shadowbox/server/manager_metrics.ts index 4f9dd174..f6a67b37 100644 --- a/src/shadowbox/server/manager_metrics.ts +++ b/src/shadowbox/server/manager_metrics.ts @@ -88,7 +88,8 @@ export class PrometheusManagerMetrics implements ManagerMetrics { const result = await this.prometheusClient.query( `sum(increase(shadowsocks_data_bytes{dir=~"ct"}[${timeframe.hours}h])) by (access_key)` ); - const usage = {} as {[userId: string]: number}; + // Custom IDs such as __proto__ must remain ordinary own properties. + const usage: {[userId: string]: number} = Object.create(null); for (const entry of result.result) { const bytes = Math.round(parseFloat(entry.value[1])); if (bytes === 0) { @@ -296,12 +297,13 @@ function getServerMetricsAccessKeyEntry( const accessKey = metric['access_key']; let entry = map.get(accessKey); if (entry === undefined) { + const numericAccessKey = Number(accessKey); entry = { // Preserve the existing numeric representation only when it is lossless. // Custom IDs (including leading zeros and large integers) are valid keys. accessKeyId: - Number.isSafeInteger(Number(accessKey)) && String(Number(accessKey)) === accessKey - ? Number(accessKey) + Number.isSafeInteger(numericAccessKey) && String(numericAccessKey) === accessKey + ? numericAccessKey : accessKey, dataTransferred: {bytes: 0}, tunnelTime: {seconds: 0}, From 8570517d375238d2dd3e76ed3f6e6c2f3d1a09b3 Mon Sep 17 00:00:00 2001 From: Oskar Eichler <62393985+OskarEichler@users.noreply.github.com> Date: Fri, 28 Aug 2026 13:08:50 +0300 Subject: [PATCH 3/6] Use string access key IDs in metrics responses --- src/shadowbox/server/api.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/shadowbox/server/api.yml b/src/shadowbox/server/api.yml index 47e6d5c1..4dc44ffb 100644 --- a/src/shadowbox/server/api.yml +++ b/src/shadowbox/server/api.yml @@ -208,10 +208,8 @@ paths: type: object properties: accessKeyId: - description: Numeric for canonical safe integer IDs; otherwise the exact custom access key ID. - oneOf: - - type: integer - - type: string + description: Exact access key ID. + type: string tunnelTime: type: object properties: From 0f77081123f9e1dcf4b8611f16e2894af4fc6d79 Mon Sep 17 00:00:00 2001 From: Oskar Eichler <62393985+OskarEichler@users.noreply.github.com> Date: Fri, 28 Aug 2026 13:08:53 +0300 Subject: [PATCH 4/6] Use string access key IDs in metrics responses --- src/shadowbox/server/manager_metrics.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/shadowbox/server/manager_metrics.ts b/src/shadowbox/server/manager_metrics.ts index f6a67b37..981573f9 100644 --- a/src/shadowbox/server/manager_metrics.ts +++ b/src/shadowbox/server/manager_metrics.ts @@ -61,7 +61,7 @@ interface ServerMetricsLocationEntry { } interface ServerMetricsAccessKeyEntry { - accessKeyId: number | string; + accessKeyId: string; tunnelTime: Duration; dataTransferred: Data; connection: ConnectionStats; @@ -297,14 +297,8 @@ function getServerMetricsAccessKeyEntry( const accessKey = metric['access_key']; let entry = map.get(accessKey); if (entry === undefined) { - const numericAccessKey = Number(accessKey); entry = { - // Preserve the existing numeric representation only when it is lossless. - // Custom IDs (including leading zeros and large integers) are valid keys. - accessKeyId: - Number.isSafeInteger(numericAccessKey) && String(numericAccessKey) === accessKey - ? numericAccessKey - : accessKey, + accessKeyId: accessKey, dataTransferred: {bytes: 0}, tunnelTime: {seconds: 0}, connection: { From b56c02101351b5c2116fbfea955c86a4f9455d05 Mon Sep 17 00:00:00 2001 From: Oskar Eichler <62393985+OskarEichler@users.noreply.github.com> Date: Fri, 28 Aug 2026 13:28:36 +0300 Subject: [PATCH 5/6] test: cover lossless custom metrics IDs --- src/shadowbox/server/manager_metrics.spec.ts | 30 ++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/shadowbox/server/manager_metrics.spec.ts b/src/shadowbox/server/manager_metrics.spec.ts index ed3107ff..2398c784 100644 --- a/src/shadowbox/server/manager_metrics.spec.ts +++ b/src/shadowbox/server/manager_metrics.spec.ts @@ -427,4 +427,34 @@ describe('PrometheusManagerMetrics', () => { expect(bytesTransferredByUserId['access-key-2']).toEqual(10000); done(); }); + + it('preserves custom access key IDs in server metrics', async () => { + const accessKeyIds = ['custom-id', '001', '9007199254740993']; + const prometheusClient = { + query: async (query: string): Promise => { + if (query.includes('shadowsocks_data_bytes{')) { + return { + resultType: 'vector', + result: accessKeyIds.map((accessKey, index) => ({ + metric: {access_key: accessKey}, + value: [1, String(index + 1)], + })), + }; + } + return {resultType: 'vector', result: []}; + }, + queryRange: async (query: string): Promise => { + if (query.includes('shadowsocks_data_bytes_per_location')) { + return {resultType: 'matrix', result: [{metric: {}, values: []}]}; + } + return {resultType: 'matrix', result: []}; + }, + } as PrometheusClient; + const managerMetrics = new PrometheusManagerMetrics(prometheusClient); + + const serverMetrics = await managerMetrics.getServerMetrics({seconds: 0}); + + expect(serverMetrics.accessKeys.map((entry) => entry.accessKeyId)).toEqual(accessKeyIds); + }); + }); From 6e6172d555bf4df5ff85aef91435baf07e373b58 Mon Sep 17 00:00:00 2001 From: Oskar Eichler Date: Sun, 30 Aug 2026 15:01:28 +0700 Subject: [PATCH 6/6] Update metrics ID expectations --- src/shadowbox/server/manager_metrics.spec.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/shadowbox/server/manager_metrics.spec.ts b/src/shadowbox/server/manager_metrics.spec.ts index 2398c784..78bce70d 100644 --- a/src/shadowbox/server/manager_metrics.spec.ts +++ b/src/shadowbox/server/manager_metrics.spec.ts @@ -194,7 +194,7 @@ describe('PrometheusManagerMetrics', () => { }, "accessKeys": [ { - "accessKeyId": 0, + "accessKeyId": "0", "dataTransferred": { "bytes": 1000 }, @@ -380,7 +380,7 @@ describe('PrometheusManagerMetrics', () => { }, "accessKeys": [ { - "accessKeyId": 1, + "accessKeyId": "1", "dataTransferred": { "bytes": 0 }, @@ -396,7 +396,7 @@ describe('PrometheusManagerMetrics', () => { } }, { - "accessKeyId": 0, + "accessKeyId": "0", "dataTransferred": { "bytes": 1000 }, @@ -456,5 +456,4 @@ describe('PrometheusManagerMetrics', () => { expect(serverMetrics.accessKeys.map((entry) => entry.accessKeyId)).toEqual(accessKeyIds); }); - });