@unleash/proxy
Advanced tools
Comparing version 0.6.1 to 0.7.0
# Changelog | ||
### 0.7.0 | ||
- feat: generate type output to publish them to npm (#54) | ||
- feat: experimental support for bootstrap (#44) | ||
- fix: upgrade unleash-client to v3.11.3 | ||
- fix: require authorization header for /client/metrics (#55) | ||
- fix: do not crash for invalid trustProxy option (#50) | ||
### 0.6.1 | ||
@@ -4,0 +12,0 @@ - fix: upgrade unleash-client to v3.11.2 |
@@ -22,3 +22,8 @@ "use strict"; | ||
app.disable('x-powered-by'); | ||
app.set('trust proxy', config.trustProxy); | ||
try { | ||
app.set('trust proxy', config.trustProxy); | ||
} | ||
catch (err) { | ||
config.logger.error(`The provided "trustProxy" option was not valid ("${config.trustProxy}")`, err); | ||
} | ||
app.use(cors_1.default(corsOptions)); | ||
@@ -25,0 +30,0 @@ app.use(compression_1.default()); |
@@ -33,2 +33,3 @@ "use strict"; | ||
customHeadersFunction, | ||
bootstrap: config.bootstrap, | ||
}); | ||
@@ -89,2 +90,5 @@ // Custom metrics Instance | ||
} | ||
getFeatureToggleDefinitions() { | ||
return this.unleash.getFeatureToggleDefinitions(); | ||
} | ||
/* | ||
@@ -91,0 +95,0 @@ * A very simplistic implementation which support counts. |
@@ -55,2 +55,26 @@ "use strict"; | ||
} | ||
function loadServerSideSdkConfig(option) { | ||
if (option.expServerSideSdkConfig) { | ||
return option.expServerSideSdkConfig; | ||
} | ||
const tokens = resolveStringToArray(process.env.EXP_SERVER_SIDE_SDK_CONFIG_TOKENS); | ||
return tokens ? { tokens } : undefined; | ||
} | ||
function loadBootstrapOptions(option) { | ||
if (option.expBootstrap) { | ||
return option.expBootstrap; | ||
} | ||
const bootstrapUrl = process.env.EXP_BOOTSTRAP_URL; | ||
const expBootstrapAuthorization = process.env.EXP_BOOTSTRAP_AUTHORIZATION; | ||
const headers = expBootstrapAuthorization | ||
? { Authorization: expBootstrapAuthorization } | ||
: undefined; | ||
if (bootstrapUrl) { | ||
return { | ||
url: bootstrapUrl, | ||
urlHeaders: headers, | ||
}; | ||
} | ||
return undefined; | ||
} | ||
function createProxyConfig(option) { | ||
@@ -101,2 +125,4 @@ const unleashUrl = option.unleashUrl || process.env.UNLEASH_URL; | ||
'authorization', | ||
serverSideSdkConfig: loadServerSideSdkConfig(option), | ||
bootstrap: loadBootstrapOptions(option), | ||
}; | ||
@@ -103,0 +129,0 @@ } |
@@ -15,2 +15,5 @@ "use strict"; | ||
} | ||
getFeatureToggleDefinitions() { | ||
throw new Error('Method not implemented.'); | ||
} | ||
isReady() { | ||
@@ -17,0 +20,0 @@ return false; |
@@ -221,2 +221,30 @@ "use strict"; | ||
}); | ||
test('should read serverSideSdkConfig from env vars', () => { | ||
process.env.EXP_SERVER_SIDE_SDK_CONFIG_TOKENS = 'super1, super2'; | ||
const config = config_1.createProxyConfig({ | ||
unleashUrl: 'some', | ||
unleashApiToken: 'some', | ||
clientKeys: ['s1'], | ||
}); | ||
expect(config.serverSideSdkConfig?.tokens).toStrictEqual([ | ||
'super1', | ||
'super2', | ||
]); | ||
delete process.env.EXP_SERVER_SIDE_SDK_CONFIG_TOKENS; | ||
}); | ||
test('should read bootstrap from env vars', () => { | ||
process.env.EXP_BOOTSTRAP_URL = 'https://boostrap.unleash.run'; | ||
process.env.EXP_BOOTSTRAP_AUTHORIZATION = 'AUTH-BOOTSTRAP'; | ||
const config = config_1.createProxyConfig({ | ||
unleashUrl: 'some', | ||
unleashApiToken: 'some', | ||
clientKeys: ['s1'], | ||
}); | ||
expect(config.bootstrap).toStrictEqual({ | ||
url: 'https://boostrap.unleash.run', | ||
urlHeaders: { Authorization: 'AUTH-BOOTSTRAP' }, | ||
}); | ||
delete process.env.EXP_BOOTSTRAP_URL; | ||
delete process.env.EXP_BOOTSTRAP_AUTHORIZATION; | ||
}); | ||
//# sourceMappingURL=config.test.js.map |
@@ -12,2 +12,5 @@ "use strict"; | ||
this.clientKeys = config.clientKeys; | ||
this.serverSideTokens = config.serverSideSdkConfig | ||
? config.serverSideSdkConfig.tokens | ||
: []; | ||
this.clientKeysHeaderName = config.clientKeysHeaderName; | ||
@@ -28,2 +31,3 @@ this.client = client; | ||
router.post('/client/metrics', this.registerMetrics.bind(this)); | ||
router.get('/client/features', this.unleashApi.bind(this)); | ||
} | ||
@@ -59,7 +63,7 @@ setReady() { | ||
lookupToggles(req, res) { | ||
const apiToken = req.header(this.clientKeysHeaderName); | ||
const clientToken = req.header(this.clientKeysHeaderName); | ||
if (!this.ready) { | ||
res.status(503).send(NOT_READY); | ||
} | ||
else if (!apiToken || !this.clientKeys.includes(apiToken)) { | ||
else if (!clientToken || !this.clientKeys.includes(clientToken)) { | ||
res.sendStatus(401); | ||
@@ -82,14 +86,35 @@ } | ||
registerMetrics(req, res) { | ||
const data = req.body; | ||
const { error, value } = metrics_schema_1.clientMetricsSchema.validate(data); | ||
if (error) { | ||
this.logger.warn('Invalid metrics posted', error); | ||
res.status(400).json(error); | ||
return; | ||
const token = req.header(this.clientKeysHeaderName); | ||
const validTokens = [...this.clientKeys, ...this.serverSideTokens]; | ||
if (token && validTokens.includes(token)) { | ||
const data = req.body; | ||
const { error, value } = metrics_schema_1.clientMetricsSchema.validate(data); | ||
if (error) { | ||
this.logger.warn('Invalid metrics posted', error); | ||
res.status(400).json(error); | ||
return; | ||
} | ||
this.client.registerMetrics(value); | ||
res.sendStatus(200); | ||
} | ||
this.client.registerMetrics(value); | ||
res.sendStatus(200); | ||
else { | ||
res.sendStatus(401); | ||
} | ||
} | ||
unleashApi(req, res) { | ||
const apiToken = req.header(this.clientKeysHeaderName); | ||
if (!this.ready) { | ||
res.status(503).send(NOT_READY); | ||
} | ||
else if (apiToken && this.serverSideTokens.includes(apiToken)) { | ||
const features = this.client.getFeatureToggleDefinitions(); | ||
res.set('Cache-control', 'public, max-age=2'); | ||
res.send({ version: 2, features }); | ||
} | ||
else { | ||
res.sendStatus(401); | ||
} | ||
} | ||
} | ||
exports.default = UnleashProxy; | ||
//# sourceMappingURL=unleash-proxy.js.map |
@@ -10,2 +10,12 @@ const port = process.env.PORT || 3000; | ||
refreshInterval: 1000, | ||
logLevel: 'trace', | ||
expServerSideSdkConfig: { | ||
tokens: ['server'], | ||
}, | ||
expBootstrap: { | ||
url: 'https://localhost:4000', | ||
urlHeaders: { | ||
Authorization: 'bootstrap-token', | ||
}, | ||
}, | ||
// unleashInstanceId: '1337', | ||
@@ -12,0 +22,0 @@ // logLevel: 'info', |
{ | ||
"name": "@unleash/proxy", | ||
"version": "0.6.1", | ||
"version": "0.7.0", | ||
"description": "The Unleash Proxy (Open-Source)", | ||
"main": "dist/index.js", | ||
"types": "dist/index.d.ts", | ||
"scripts": { | ||
@@ -37,3 +38,3 @@ "build": "tsc --pretty", | ||
"joi": "^17.4.0", | ||
"unleash-client": "^3.11.2" | ||
"unleash-client": "^3.11.3" | ||
}, | ||
@@ -40,0 +41,0 @@ "devDependencies": { |
@@ -128,2 +128,13 @@ ![Build & Tests](https://github.com/Unleash/unleash-proxy/workflows/Node.js%20CI/badge.svg?branch=main) | ||
### Experimental options | ||
Some functionality is under validation and introduced as experimental, to allow us to test new functionality early. You should expect these to change in any future feature release. | ||
| Option | Environment Variable | Default value | Required | Description | | ||
| ------------- |---------------------- |---------- |:--------:|---------------| | ||
| expBootstrap | n/a | n/a | no | Where the Proxy can bootstrap configuration from. See [Node.js SDK](https://github.com/Unleash/unleash-client-node#bootstrap) for details. | | ||
| expBootstrap.url | `EXP_BOOTSTRAP_URL` | n/a | no | Url where the Proxy can bootstrap configuration from. See [Node.js SDK](https://github.com/Unleash/unleash-client-node#bootstrap) for details. | | ||
| expBootstrap.urlHeaders.Authorization | `EXP_BOOTSTRAP_AUTHORIZATION` | n/a | no | Authorization header value to be used when bootstrapping | | ||
| expServerSideSdkConfig.tokens | `EXP_SERVER_SIDE_SDK_CONFIG_TOKENS` | n/a | no | API tokens that can be used by Server SDKs (and proxies) to read feature toggle configuration from this Proxy instance. | | ||
### Run with Node.js: | ||
@@ -130,0 +141,0 @@ |
@@ -25,3 +25,11 @@ import compression from 'compression'; | ||
app.disable('x-powered-by'); | ||
app.set('trust proxy', config.trustProxy); | ||
try { | ||
app.set('trust proxy', config.trustProxy); | ||
} catch (err) { | ||
config.logger.error( | ||
`The provided "trustProxy" option was not valid ("${config.trustProxy}")`, | ||
err, | ||
); | ||
} | ||
app.use(cors(corsOptions)); | ||
@@ -28,0 +36,0 @@ |
import EventEmitter from 'events'; | ||
import { Context, initialize, Unleash, Variant } from 'unleash-client'; | ||
import { FeatureInterface } from 'unleash-client/lib/feature'; | ||
import Metrics from 'unleash-client/lib/metrics'; | ||
@@ -38,2 +39,3 @@ import { defaultStrategies } from 'unleash-client/lib/strategy'; | ||
) => FeatureToggleStatus[]; | ||
getFeatureToggleDefinitions(): FeatureInterface[]; | ||
registerMetrics(metrics: any): void; | ||
@@ -79,2 +81,3 @@ isReady(): boolean; | ||
customHeadersFunction, | ||
bootstrap: config.bootstrap, | ||
}); | ||
@@ -149,2 +152,6 @@ | ||
getFeatureToggleDefinitions(): FeatureInterface[] { | ||
return this.unleash.getFeatureToggleDefinitions(); | ||
} | ||
/* | ||
@@ -151,0 +158,0 @@ * A very simplistic implementation which support counts. |
import { Strategy, TagFilter } from 'unleash-client'; | ||
import { BootstrapOptions } from 'unleash-client/lib/repository/bootstrap-provider'; | ||
import { Logger, LogLevel, SimpleLogger } from './logger'; | ||
import { generateInstanceId } from './util'; | ||
export interface ServerSideSdkConfig { | ||
tokens: string[]; | ||
} | ||
export interface IProxyOption { | ||
@@ -25,2 +29,5 @@ unleashUrl?: string; | ||
clientKeysHeaderName?: string; | ||
// experimental options | ||
expBootstrap?: BootstrapOptions; | ||
expServerSideSdkConfig?: ServerSideSdkConfig; | ||
} | ||
@@ -46,2 +53,4 @@ | ||
clientKeysHeaderName: string; | ||
serverSideSdkConfig?: ServerSideSdkConfig; | ||
bootstrap?: BootstrapOptions; | ||
} | ||
@@ -104,2 +113,36 @@ | ||
function loadServerSideSdkConfig( | ||
option: IProxyOption, | ||
): ServerSideSdkConfig | undefined { | ||
if (option.expServerSideSdkConfig) { | ||
return option.expServerSideSdkConfig; | ||
} | ||
const tokens = resolveStringToArray( | ||
process.env.EXP_SERVER_SIDE_SDK_CONFIG_TOKENS, | ||
); | ||
return tokens ? { tokens } : undefined; | ||
} | ||
function loadBootstrapOptions( | ||
option: IProxyOption, | ||
): BootstrapOptions | undefined { | ||
if (option.expBootstrap) { | ||
return option.expBootstrap; | ||
} | ||
const bootstrapUrl = process.env.EXP_BOOTSTRAP_URL; | ||
const expBootstrapAuthorization = process.env.EXP_BOOTSTRAP_AUTHORIZATION; | ||
const headers = expBootstrapAuthorization | ||
? { Authorization: expBootstrapAuthorization } | ||
: undefined; | ||
if (bootstrapUrl) { | ||
return { | ||
url: bootstrapUrl, | ||
urlHeaders: headers, | ||
}; | ||
} | ||
return undefined; | ||
} | ||
export function createProxyConfig(option: IProxyOption): IProxyConfig { | ||
@@ -173,3 +216,5 @@ const unleashUrl = option.unleashUrl || process.env.UNLEASH_URL; | ||
'authorization', | ||
serverSideSdkConfig: loadServerSideSdkConfig(option), | ||
bootstrap: loadBootstrapOptions(option), | ||
}; | ||
} |
import EventEmitter from 'events'; | ||
import { Context } from 'unleash-client'; | ||
import { FeatureInterface } from 'unleash-client/lib/feature'; | ||
import { FeatureToggleStatus, IClient } from '../client'; | ||
@@ -20,2 +21,6 @@ | ||
getFeatureToggleDefinitions(): FeatureInterface[] { | ||
throw new Error('Method not implemented.'); | ||
} | ||
isReady(): boolean { | ||
@@ -22,0 +27,0 @@ return false; |
@@ -241,1 +241,33 @@ import * as path from 'path'; | ||
}); | ||
test('should read serverSideSdkConfig from env vars', () => { | ||
process.env.EXP_SERVER_SIDE_SDK_CONFIG_TOKENS = 'super1, super2'; | ||
const config = createProxyConfig({ | ||
unleashUrl: 'some', | ||
unleashApiToken: 'some', | ||
clientKeys: ['s1'], | ||
}); | ||
expect(config.serverSideSdkConfig?.tokens).toStrictEqual([ | ||
'super1', | ||
'super2', | ||
]); | ||
delete process.env.EXP_SERVER_SIDE_SDK_CONFIG_TOKENS; | ||
}); | ||
test('should read bootstrap from env vars', () => { | ||
process.env.EXP_BOOTSTRAP_URL = 'https://boostrap.unleash.run'; | ||
process.env.EXP_BOOTSTRAP_AUTHORIZATION = 'AUTH-BOOTSTRAP'; | ||
const config = createProxyConfig({ | ||
unleashUrl: 'some', | ||
unleashApiToken: 'some', | ||
clientKeys: ['s1'], | ||
}); | ||
expect(config.bootstrap).toStrictEqual({ | ||
url: 'https://boostrap.unleash.run', | ||
urlHeaders: { Authorization: 'AUTH-BOOTSTRAP' }, | ||
}); | ||
delete process.env.EXP_BOOTSTRAP_URL; | ||
delete process.env.EXP_BOOTSTRAP_AUTHORIZATION; | ||
}); |
@@ -16,2 +16,4 @@ import { Request, Response, Router } from 'express'; | ||
private serverSideTokens: string[]; | ||
private clientKeysHeaderName: string; | ||
@@ -28,2 +30,5 @@ | ||
this.clientKeys = config.clientKeys; | ||
this.serverSideTokens = config.serverSideSdkConfig | ||
? config.serverSideSdkConfig.tokens | ||
: []; | ||
this.clientKeysHeaderName = config.clientKeysHeaderName; | ||
@@ -48,2 +53,3 @@ this.client = client; | ||
router.post('/client/metrics', this.registerMetrics.bind(this)); | ||
router.get('/client/features', this.unleashApi.bind(this)); | ||
} | ||
@@ -85,7 +91,7 @@ | ||
lookupToggles(req: Request, res: Response): void { | ||
const apiToken = req.header(this.clientKeysHeaderName); | ||
const clientToken = req.header(this.clientKeysHeaderName); | ||
if (!this.ready) { | ||
res.status(503).send(NOT_READY); | ||
} else if (!apiToken || !this.clientKeys.includes(apiToken)) { | ||
} else if (!clientToken || !this.clientKeys.includes(clientToken)) { | ||
res.sendStatus(401); | ||
@@ -109,15 +115,32 @@ } else { | ||
registerMetrics(req: Request, res: Response): void { | ||
const data = req.body; | ||
const token = req.header(this.clientKeysHeaderName); | ||
const validTokens = [...this.clientKeys, ...this.serverSideTokens]; | ||
const { error, value } = clientMetricsSchema.validate(data); | ||
if (token && validTokens.includes(token)) { | ||
const data = req.body; | ||
const { error, value } = clientMetricsSchema.validate(data); | ||
if (error) { | ||
this.logger.warn('Invalid metrics posted', error); | ||
res.status(400).json(error); | ||
return; | ||
} | ||
this.client.registerMetrics(value); | ||
res.sendStatus(200); | ||
} else { | ||
res.sendStatus(401); | ||
} | ||
} | ||
if (error) { | ||
this.logger.warn('Invalid metrics posted', error); | ||
res.status(400).json(error); | ||
return; | ||
unleashApi(req: Request, res: Response): void { | ||
const apiToken = req.header(this.clientKeysHeaderName); | ||
if (!this.ready) { | ||
res.status(503).send(NOT_READY); | ||
} else if (apiToken && this.serverSideTokens.includes(apiToken)) { | ||
const features = this.client.getFeatureToggleDefinitions(); | ||
res.set('Cache-control', 'public, max-age=2'); | ||
res.send({ version: 2, features }); | ||
} else { | ||
res.sendStatus(401); | ||
} | ||
this.client.registerMetrics(value); | ||
res.sendStatus(200); | ||
} | ||
} |
@@ -13,3 +13,3 @@ { | ||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ | ||
// "declaration": true, /* Generates corresponding '.d.ts' file. */ | ||
"declaration": true, /* Generates corresponding '.d.ts' file. */ | ||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ | ||
@@ -16,0 +16,0 @@ "sourceMap": true, /* Generates corresponding '.map' file. */ |
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
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 3 instances in 1 package
182276
90
3319
181
56
Updatedunleash-client@^3.11.3