@unleash/proxy
Advanced tools
Comparing version 0.4.0 to 0.5.0
# Changelog | ||
### 0.5.0 | ||
- feat: additional possibility to customize header for proxy secrets (#39) | ||
- fix: POST should handle empty toggle names | ||
- fix: pin dependencies | ||
- fix: Build multi-arch Docker container to support arm64 and amd64 (#30) | ||
- docs: fix typo in `docker pull` command (#26) | ||
- docs: Add proxyPort/PORT to the list of options/env vars (#28) | ||
### 0.4.0 | ||
@@ -4,0 +12,0 @@ - feat: add compression #23 |
@@ -49,2 +49,8 @@ "use strict"; | ||
} | ||
function loadClientKeys(option) { | ||
return (option.clientKeys || | ||
resolveStringToArray(process.env.UNLEASH_PROXY_CLIENT_KEYS) || | ||
option.proxySecrets || | ||
resolveStringToArray(process.env.UNLEASH_PROXY_SECRETS)); | ||
} | ||
function createProxyConfig(option) { | ||
@@ -61,6 +67,5 @@ const unleashUrl = option.unleashUrl || process.env.UNLEASH_URL; | ||
loadCustomStrategies(process.env.UNLEASH_CUSTOM_STRATEGIES_FILE); | ||
const proxySecrets = option.proxySecrets || | ||
resolveStringToArray(process.env.UNLEASH_PROXY_SECRETS); | ||
if (!proxySecrets) { | ||
throw new TypeError('You must specify the unleashProxySecrets option (UNLEASH_PROXY_SECRETS)'); | ||
const clientKeys = loadClientKeys(option); | ||
if (!clientKeys) { | ||
throw new TypeError('You must specify the clientKeys option (UNLEASH_PROXY_CLIENT_KEYS)'); | ||
} | ||
@@ -81,3 +86,3 @@ const logLevel = option.logLevel || process.env.LOG_LEVEL; | ||
customStrategies, | ||
proxySecrets, | ||
clientKeys, | ||
proxyBasePath: option.proxyBasePath || process.env.PROXY_BASE_PATH || '', | ||
@@ -95,2 +100,5 @@ refreshInterval: option.refreshInterval || | ||
tags, | ||
clientKeysHeaderName: option.clientKeysHeaderName || | ||
process.env.CLIENT_KEY_HEADER_NAME || | ||
'authorization', | ||
}; | ||
@@ -97,0 +105,0 @@ } |
@@ -92,3 +92,3 @@ "use strict"; | ||
process.env.UNLEASH_API_TOKEN = 'token'; | ||
process.env.UNLEASH_PROXY_SECRETS = 's1'; | ||
process.env.UNLEASH_PROXY_CLIENT_KEYS = 's1'; | ||
process.env.UNLEASH_INSTANCE_ID = 'i1'; | ||
@@ -98,4 +98,4 @@ const config = config_1.createProxyConfig({}); | ||
expect(config.unleashApiToken).toBe('token'); | ||
expect(config.proxySecrets.length).toBe(1); | ||
expect(config.proxySecrets[0]).toBe('s1'); | ||
expect(config.clientKeys.length).toBe(1); | ||
expect(config.clientKeys[0]).toBe('s1'); | ||
expect(config.unleashInstanceId).toBe('i1'); | ||
@@ -105,2 +105,18 @@ // cleanup | ||
delete process.env.UNLEASH_API_TOKEN; | ||
delete process.env.UNLEASH_PROXY_CLIENT_KEYS; | ||
delete process.env.UNLEASH_INSTANCE_ID; | ||
}); | ||
test('should allow old "UNLEASH_PROXY_SECRETS" option via env', () => { | ||
process.env.UNLEASH_URL = 'some'; | ||
process.env.UNLEASH_API_TOKEN = 'token'; | ||
process.env.UNLEASH_PROXY_SECRETS = 's1-token, s2-token'; | ||
const config = config_1.createProxyConfig({}); | ||
expect(config.unleashUrl).toBe('some'); | ||
expect(config.unleashApiToken).toBe('token'); | ||
expect(config.clientKeys.length).toBe(2); | ||
expect(config.clientKeys[0]).toBe('s1-token'); | ||
expect(config.clientKeys[1]).toBe('s2-token'); | ||
// cleanup | ||
delete process.env.UNLEASH_URL; | ||
delete process.env.UNLEASH_API_TOKEN; | ||
delete process.env.UNLEASH_PROXY_SECRETS; | ||
@@ -121,3 +137,3 @@ }); | ||
unleashApiToken: 'some', | ||
proxySecrets: ['s1'], | ||
clientKeys: ['s1'], | ||
customStrategies: [new TestStrat()], | ||
@@ -139,3 +155,3 @@ }); | ||
unleashApiToken: 'some', | ||
proxySecrets: ['s1'], | ||
clientKeys: ['s1'], | ||
}); | ||
@@ -156,3 +172,3 @@ expect(config.customStrategies?.length).toBe(1); | ||
namePrefix: 'somePrefix', | ||
proxySecrets: ['s1'], | ||
clientKeys: ['s1'], | ||
}); | ||
@@ -166,3 +182,3 @@ expect(config.namePrefix).toBe('somePrefix'); | ||
unleashApiToken: 'some', | ||
proxySecrets: ['s1'], | ||
clientKeys: ['s1'], | ||
}); | ||
@@ -194,3 +210,3 @@ expect(config.namePrefix).toBe('prefixViaEnv'); | ||
unleashApiToken: 'some', | ||
proxySecrets: ['s1'], | ||
clientKeys: ['s1'], | ||
}); | ||
@@ -205,3 +221,3 @@ expect(config.tags).toBeUndefined(); | ||
unleashApiToken: 'some', | ||
proxySecrets: ['s1'], | ||
clientKeys: ['s1'], | ||
}); | ||
@@ -208,0 +224,0 @@ expect(config.tags).toStrictEqual([ |
@@ -50,2 +50,84 @@ "use strict"; | ||
}); | ||
test('Should handle POST with empty body', () => { | ||
const toggles = [ | ||
{ | ||
name: 'test', | ||
enabled: true, | ||
}, | ||
{ | ||
name: 'test2', | ||
enabled: true, | ||
}, | ||
]; | ||
const client = new client_mock_1.default(toggles); | ||
const proxySecrets = ['sdf']; | ||
const app = app_1.createApp({ proxySecrets, unleashUrl, unleashApiToken }, client); | ||
client.emit('ready'); | ||
return supertest_1.default(app) | ||
.post('/proxy') | ||
.send({ blah: 'hello' }) | ||
.set('Accept', 'application/json') | ||
.set('Authorization', 'sdf') | ||
.expect(200) | ||
.expect('Content-Type', /json/) | ||
.then((response) => { | ||
expect(response.body.toggles).toHaveLength(0); | ||
}); | ||
}); | ||
test('Should handle POST with toggle names', () => { | ||
const toggles = [ | ||
{ | ||
name: 'test', | ||
enabled: true, | ||
}, | ||
{ | ||
name: 'test2', | ||
enabled: true, | ||
}, | ||
]; | ||
const client = new client_mock_1.default(toggles); | ||
const proxySecrets = ['sdf']; | ||
const app = app_1.createApp({ proxySecrets, unleashUrl, unleashApiToken }, client); | ||
client.emit('ready'); | ||
return supertest_1.default(app) | ||
.post('/proxy') | ||
.send({ toggles: ['test'] }) | ||
.set('Accept', 'application/json') | ||
.set('Authorization', 'sdf') | ||
.expect(200) | ||
.expect('Content-Type', /json/) | ||
.then((response) => { | ||
expect(response.body.toggles).toHaveLength(1); | ||
expect(response.body.toggles[0].name).toBe('test'); | ||
}); | ||
}); | ||
test('Should return list of toggles with custom proxy client key header', () => { | ||
const toggles = [ | ||
{ | ||
name: 'test', | ||
enabled: true, | ||
}, | ||
{ | ||
name: 'test2', | ||
enabled: true, | ||
}, | ||
]; | ||
const client = new client_mock_1.default(toggles); | ||
const proxySecrets = ['sdf']; | ||
const app = app_1.createApp({ | ||
proxySecrets, | ||
unleashUrl, | ||
unleashApiToken, | ||
clientKeysHeaderName: 'NotAuthorized', | ||
}, client); | ||
client.emit('ready'); | ||
return supertest_1.default(app) | ||
.get('/proxy') | ||
.set('NotAuthorized', 'sdf') | ||
.expect(200) | ||
.expect('Content-Type', /json/) | ||
.then((response) => { | ||
expect(response.body.toggles.length).toEqual(2); | ||
}); | ||
}); | ||
test('Should return list of toggles using env with multiple secrets', () => { | ||
@@ -52,0 +134,0 @@ process.env.UNLEASH_PROXY_SECRETS = 'secret1,secret2'; |
@@ -11,3 +11,4 @@ "use strict"; | ||
this.logger = config.logger; | ||
this.proxySecrets = config.proxySecrets; | ||
this.clientKeys = config.clientKeys; | ||
this.clientKeysHeaderName = config.clientKeysHeaderName; | ||
this.client = client; | ||
@@ -32,11 +33,15 @@ if (client.isReady()) { | ||
} | ||
setProxySecrets(proxySecrets) { | ||
this.proxySecrets = proxySecrets; | ||
// kept for backward compatibility | ||
setProxySecrets(clientKeys) { | ||
this.setClientKeys(clientKeys); | ||
} | ||
setClientKeys(clientKeys) { | ||
this.clientKeys = clientKeys; | ||
} | ||
getEnabledToggles(req, res) { | ||
const apiToken = req.header('authorization'); | ||
const apiToken = req.header(this.clientKeysHeaderName); | ||
if (!this.ready) { | ||
res.status(503).send(NOT_READY); | ||
} | ||
else if (!apiToken || !this.proxySecrets.includes(apiToken)) { | ||
else if (!apiToken || !this.clientKeys.includes(apiToken)) { | ||
res.sendStatus(401); | ||
@@ -54,13 +59,13 @@ } | ||
lookupToggles(req, res) { | ||
const apiToken = req.header('authorization'); | ||
const apiToken = req.header(this.clientKeysHeaderName); | ||
if (!this.ready) { | ||
res.status(503).send(NOT_READY); | ||
} | ||
else if (!apiToken || !this.proxySecrets.includes(apiToken)) { | ||
else if (!apiToken || !this.clientKeys.includes(apiToken)) { | ||
res.sendStatus(401); | ||
} | ||
else { | ||
const { context, toggles: toggleNames } = req.body; | ||
const { context, toggles: toggleNames = [] } = req.body; | ||
const toggles = this.client.getDefinedToggles(toggleNames, context); | ||
res.send(toggles); | ||
res.send({ toggles }); | ||
} | ||
@@ -67,0 +72,0 @@ } |
@@ -8,3 +8,3 @@ const port = process.env.PORT || 3000; | ||
unleashApiToken: '56907a2fa53c1d16101d509a10b78e36190b0f918d9f122d', | ||
proxySecrets: ['proxy-secret', 'another-proxy-secret', 's1'], | ||
clientKeys: ['proxy-secret', 'another-proxy-secret', 's1'], | ||
refreshInterval: 1000, | ||
@@ -18,3 +18,3 @@ // unleashInstanceId: '1337', | ||
app.listen(port, () => | ||
console.log(`Unleash-proxy is listening on port ${port}!`), | ||
console.log(`Unleash-proxy is listening on port ${port}!`), | ||
); |
{ | ||
"name": "@unleash/proxy", | ||
"version": "0.4.0", | ||
"version": "0.5.0", | ||
"description": "The Unleash Proxy (Open-Source)", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
@@ -33,3 +33,3 @@ ![Build & Tests](https://github.com/Unleash/unleash-proxy/workflows/Node.js%20CI/badge.svg?branch=main) | ||
```bash | ||
docker pull docker pull unleashorg/unleash-proxy | ||
docker pull unleashorg/unleash-proxy | ||
``` | ||
@@ -111,3 +111,5 @@ | ||
| unleashApiToken | `UNLEASH_API_TOKEN` | n/a | yes | API token (client) needed to connect to Unleash API. | | ||
| proxySecrets | `UNLEASH_PROXY_SECRETS` | n/a | yes | List of proxy secrets the proxy accept. Proxy SDKs needs to set the Proxy secret as the `Authorization` heder when querying the proxy | | ||
| clientKeys | `UNLEASH_CLIENT_KEYS` | n/a | yes | List of client keys that the proxy should accept. When querying the proxy, Proxy SDKs must set the request's _client keys header_ to one of these values. The default client keys header is `Authorization`. | | ||
| proxySecrets | `UNLEASH_PROXY_SECRETS` | n/a | no | Deprecated alias for `clientKeys`. Please use `clientKeys` instead. | | ||
| proxyPort | `PORT` | 3000 | no | The port where the proxy should listen. | | ||
| proxyBasePath | `PROXY_BASE_PATH` |"/proxy" | no | The base path to run the proxy from. Defaults to "/proxy" | | ||
@@ -126,2 +128,3 @@ | unleashAppName | `UNLEASH_APP_NAME` |"unleash-proxy"| no | App name to used when registering with Unleash | | ||
| tags | `UNLEASH_TAGS` | undefined | no | Used to filter features by using tags set for features. Format should be `tagName:tagValue,tagName2:tagValue2` | | ||
| clientKeysHeaderName | `CLIENT_KEYS_HEADER_NAME` | "authorization" | no | The name of the HTTP header to use for client keys. Incoming requests must set the value of this header to one of the Proxy's `clientKeys` to be authorized successfully. | | ||
@@ -148,3 +151,3 @@ ### Run with Node.js: | ||
unleashApiToken: '56907a2fa53c1d16101d509a10b78e36190b0f918d9f122d', | ||
proxySecrets: ['proxy-secret', 'another-proxy-secret', 's1'], | ||
clientKeys: ['proxy-secret', 'another-proxy-secret', 's1'], | ||
refreshInterval: 1000, | ||
@@ -151,0 +154,0 @@ // logLevel: 'info', |
@@ -12,2 +12,3 @@ import { Strategy, TagFilter } from 'unleash-client'; | ||
proxySecrets?: string[]; | ||
clientKeys?: string[]; | ||
proxyPort?: number; | ||
@@ -24,2 +25,3 @@ proxyBasePath?: string; | ||
tags?: Array<TagFilter>; | ||
clientKeysHeaderName?: string; | ||
} | ||
@@ -33,3 +35,3 @@ | ||
customStrategies?: Strategy[]; | ||
proxySecrets: string[]; | ||
clientKeys: string[]; | ||
proxyBasePath: string; | ||
@@ -45,2 +47,3 @@ refreshInterval: number; | ||
tags?: Array<TagFilter>; | ||
clientKeysHeaderName: string; | ||
} | ||
@@ -94,2 +97,11 @@ | ||
function loadClientKeys(option: IProxyOption): string[] | undefined { | ||
return ( | ||
option.clientKeys || | ||
resolveStringToArray(process.env.UNLEASH_PROXY_CLIENT_KEYS) || | ||
option.proxySecrets || | ||
resolveStringToArray(process.env.UNLEASH_PROXY_SECRETS) | ||
); | ||
} | ||
export function createProxyConfig(option: IProxyOption): IProxyConfig { | ||
@@ -115,8 +127,6 @@ const unleashUrl = option.unleashUrl || process.env.UNLEASH_URL; | ||
const proxySecrets = | ||
option.proxySecrets || | ||
resolveStringToArray(process.env.UNLEASH_PROXY_SECRETS); | ||
if (!proxySecrets) { | ||
const clientKeys = loadClientKeys(option); | ||
if (!clientKeys) { | ||
throw new TypeError( | ||
'You must specify the unleashProxySecrets option (UNLEASH_PROXY_SECRETS)', | ||
'You must specify the clientKeys option (UNLEASH_PROXY_CLIENT_KEYS)', | ||
); | ||
@@ -146,3 +156,3 @@ } | ||
customStrategies, | ||
proxySecrets, | ||
clientKeys, | ||
proxyBasePath: | ||
@@ -163,3 +173,7 @@ option.proxyBasePath || process.env.PROXY_BASE_PATH || '', | ||
tags, | ||
clientKeysHeaderName: | ||
option.clientKeysHeaderName || | ||
process.env.CLIENT_KEY_HEADER_NAME || | ||
'authorization', | ||
}; | ||
} |
@@ -88,3 +88,3 @@ import * as path from 'path'; | ||
process.env.UNLEASH_API_TOKEN = 'token'; | ||
process.env.UNLEASH_PROXY_SECRETS = 's1'; | ||
process.env.UNLEASH_PROXY_CLIENT_KEYS = 's1'; | ||
process.env.UNLEASH_INSTANCE_ID = 'i1'; | ||
@@ -95,4 +95,4 @@ const config = createProxyConfig({}); | ||
expect(config.unleashApiToken).toBe('token'); | ||
expect(config.proxySecrets.length).toBe(1); | ||
expect(config.proxySecrets[0]).toBe('s1'); | ||
expect(config.clientKeys.length).toBe(1); | ||
expect(config.clientKeys[0]).toBe('s1'); | ||
expect(config.unleashInstanceId).toBe('i1'); | ||
@@ -103,2 +103,21 @@ | ||
delete process.env.UNLEASH_API_TOKEN; | ||
delete process.env.UNLEASH_PROXY_CLIENT_KEYS; | ||
delete process.env.UNLEASH_INSTANCE_ID; | ||
}); | ||
test('should allow old "UNLEASH_PROXY_SECRETS" option via env', () => { | ||
process.env.UNLEASH_URL = 'some'; | ||
process.env.UNLEASH_API_TOKEN = 'token'; | ||
process.env.UNLEASH_PROXY_SECRETS = 's1-token, s2-token'; | ||
const config = createProxyConfig({}); | ||
expect(config.unleashUrl).toBe('some'); | ||
expect(config.unleashApiToken).toBe('token'); | ||
expect(config.clientKeys.length).toBe(2); | ||
expect(config.clientKeys[0]).toBe('s1-token'); | ||
expect(config.clientKeys[1]).toBe('s2-token'); | ||
// cleanup | ||
delete process.env.UNLEASH_URL; | ||
delete process.env.UNLEASH_API_TOKEN; | ||
delete process.env.UNLEASH_PROXY_SECRETS; | ||
@@ -122,3 +141,3 @@ }); | ||
unleashApiToken: 'some', | ||
proxySecrets: ['s1'], | ||
clientKeys: ['s1'], | ||
customStrategies: [new TestStrat()], | ||
@@ -142,3 +161,3 @@ }); | ||
unleashApiToken: 'some', | ||
proxySecrets: ['s1'], | ||
clientKeys: ['s1'], | ||
}); | ||
@@ -161,3 +180,3 @@ | ||
namePrefix: 'somePrefix', | ||
proxySecrets: ['s1'], | ||
clientKeys: ['s1'], | ||
}); | ||
@@ -173,3 +192,3 @@ | ||
unleashApiToken: 'some', | ||
proxySecrets: ['s1'], | ||
clientKeys: ['s1'], | ||
}); | ||
@@ -208,3 +227,3 @@ | ||
unleashApiToken: 'some', | ||
proxySecrets: ['s1'], | ||
clientKeys: ['s1'], | ||
}); | ||
@@ -221,3 +240,3 @@ | ||
unleashApiToken: 'some', | ||
proxySecrets: ['s1'], | ||
clientKeys: ['s1'], | ||
}); | ||
@@ -224,0 +243,0 @@ |
@@ -59,2 +59,102 @@ import request, { Response } from 'supertest'; | ||
test('Should handle POST with empty body', () => { | ||
const toggles = [ | ||
{ | ||
name: 'test', | ||
enabled: true, | ||
}, | ||
{ | ||
name: 'test2', | ||
enabled: true, | ||
}, | ||
]; | ||
const client = new MockClient(toggles); | ||
const proxySecrets = ['sdf']; | ||
const app = createApp( | ||
{ proxySecrets, unleashUrl, unleashApiToken }, | ||
client, | ||
); | ||
client.emit('ready'); | ||
return request(app) | ||
.post('/proxy') | ||
.send({ blah: 'hello' }) | ||
.set('Accept', 'application/json') | ||
.set('Authorization', 'sdf') | ||
.expect(200) | ||
.expect('Content-Type', /json/) | ||
.then((response) => { | ||
expect(response.body.toggles).toHaveLength(0); | ||
}); | ||
}); | ||
test('Should handle POST with toggle names', () => { | ||
const toggles = [ | ||
{ | ||
name: 'test', | ||
enabled: true, | ||
}, | ||
{ | ||
name: 'test2', | ||
enabled: true, | ||
}, | ||
]; | ||
const client = new MockClient(toggles); | ||
const proxySecrets = ['sdf']; | ||
const app = createApp( | ||
{ proxySecrets, unleashUrl, unleashApiToken }, | ||
client, | ||
); | ||
client.emit('ready'); | ||
return request(app) | ||
.post('/proxy') | ||
.send({ toggles: ['test'] }) | ||
.set('Accept', 'application/json') | ||
.set('Authorization', 'sdf') | ||
.expect(200) | ||
.expect('Content-Type', /json/) | ||
.then((response) => { | ||
expect(response.body.toggles).toHaveLength(1); | ||
expect(response.body.toggles[0].name).toBe('test'); | ||
}); | ||
}); | ||
test('Should return list of toggles with custom proxy client key header', () => { | ||
const toggles = [ | ||
{ | ||
name: 'test', | ||
enabled: true, | ||
}, | ||
{ | ||
name: 'test2', | ||
enabled: true, | ||
}, | ||
]; | ||
const client = new MockClient(toggles); | ||
const proxySecrets = ['sdf']; | ||
const app = createApp( | ||
{ | ||
proxySecrets, | ||
unleashUrl, | ||
unleashApiToken, | ||
clientKeysHeaderName: 'NotAuthorized', | ||
}, | ||
client, | ||
); | ||
client.emit('ready'); | ||
return request(app) | ||
.get('/proxy') | ||
.set('NotAuthorized', 'sdf') | ||
.expect(200) | ||
.expect('Content-Type', /json/) | ||
.then((response) => { | ||
expect(response.body.toggles.length).toEqual(2); | ||
}); | ||
}); | ||
test('Should return list of toggles using env with multiple secrets', () => { | ||
@@ -61,0 +161,0 @@ process.env.UNLEASH_PROXY_SECRETS = 'secret1,secret2'; |
@@ -14,4 +14,6 @@ import { Request, Response, Router } from 'express'; | ||
private proxySecrets: string[]; | ||
private clientKeys: string[]; | ||
private clientKeysHeaderName: string; | ||
private client: IClient; | ||
@@ -25,3 +27,4 @@ | ||
this.logger = config.logger; | ||
this.proxySecrets = config.proxySecrets; | ||
this.clientKeys = config.clientKeys; | ||
this.clientKeysHeaderName = config.clientKeysHeaderName; | ||
this.client = client; | ||
@@ -54,12 +57,17 @@ | ||
setProxySecrets(proxySecrets: string[]): void { | ||
this.proxySecrets = proxySecrets; | ||
// kept for backward compatibility | ||
setProxySecrets(clientKeys: string[]): void { | ||
this.setClientKeys(clientKeys); | ||
} | ||
setClientKeys(clientKeys: string[]): void { | ||
this.clientKeys = clientKeys; | ||
} | ||
getEnabledToggles(req: Request, res: Response): void { | ||
const apiToken = req.header('authorization'); | ||
const apiToken = req.header(this.clientKeysHeaderName); | ||
if (!this.ready) { | ||
res.status(503).send(NOT_READY); | ||
} else if (!apiToken || !this.proxySecrets.includes(apiToken)) { | ||
} else if (!apiToken || !this.clientKeys.includes(apiToken)) { | ||
res.sendStatus(401); | ||
@@ -77,13 +85,13 @@ } else { | ||
lookupToggles(req: Request, res: Response): void { | ||
const apiToken = req.header('authorization'); | ||
const apiToken = req.header(this.clientKeysHeaderName); | ||
if (!this.ready) { | ||
res.status(503).send(NOT_READY); | ||
} else if (!apiToken || !this.proxySecrets.includes(apiToken)) { | ||
} else if (!apiToken || !this.clientKeys.includes(apiToken)) { | ||
res.sendStatus(401); | ||
} else { | ||
const { context, toggles: toggleNames } = req.body; | ||
const { context, toggles: toggleNames = [] } = req.body; | ||
const toggles = this.client.getDefinedToggles(toggleNames, context); | ||
res.send(toggles); | ||
res.send({ toggles }); | ||
} | ||
@@ -90,0 +98,0 @@ } |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
157065
72
2848
170
47