@unleash/proxy
Advanced tools
Comparing version 0.14.0-beta.0 to 0.14.0-beta.1
@@ -14,2 +14,3 @@ "use strict"; | ||
const openapi_service_1 = require("./openapi/openapi-service"); | ||
const content_type_checker_1 = __importDefault(require("./content-type-checker")); | ||
function createApp(options, unleashClient, app = (0, express_1.default)()) { | ||
@@ -38,2 +39,3 @@ const config = (0, config_1.createProxyConfig)(options); | ||
app.use((0, compression_1.default)()); | ||
app.use((0, content_type_checker_1.default)()); | ||
app.use(`${config.proxyBasePath}/proxy`, (0, cors_1.default)(corsOptions), express_1.default.json(), proxy.middleware); | ||
@@ -40,0 +42,0 @@ openApiService.useErrorHandler(app); |
@@ -8,2 +8,3 @@ import { OpenAPIV3 } from 'openapi-types'; | ||
export declare const emptySuccessResponse: OpenAPIV3.ResponseObject; | ||
export declare const unsupportedMediaTypeResponse: OpenAPIV3.ResponseObject; | ||
export declare const internalServerErrorResponse: OpenAPIV3.ResponseObject; | ||
@@ -15,2 +16,3 @@ export declare const notImplementedError: OpenAPIV3.ResponseObject; | ||
readonly 401: OpenAPIV3.ResponseObject; | ||
readonly 415: OpenAPIV3.ResponseObject; | ||
readonly 500: OpenAPIV3.ResponseObject; | ||
@@ -17,0 +19,0 @@ readonly 501: OpenAPIV3.ResponseObject; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.standardResponses = exports.notImplementedError = exports.internalServerErrorResponse = exports.emptySuccessResponse = exports.badRequestResponse = exports.unauthorizedResponse = exports.notReadyResponse = exports.NOT_READY_MSG = exports.format500ErrorMessage = void 0; | ||
exports.standardResponses = exports.notImplementedError = exports.internalServerErrorResponse = exports.unsupportedMediaTypeResponse = exports.emptySuccessResponse = exports.badRequestResponse = exports.unauthorizedResponse = exports.notReadyResponse = exports.NOT_READY_MSG = exports.format500ErrorMessage = void 0; | ||
const format500ErrorMessage = (errorMessage) => `Whoops! We dropped the ball on this one (an unexpected error occurred): ${errorMessage}`; | ||
@@ -64,2 +64,13 @@ exports.format500ErrorMessage = format500ErrorMessage; | ||
}; | ||
exports.unsupportedMediaTypeResponse = { | ||
description: 'The operation does not support request payloads of the provided type.', | ||
content: { | ||
'text/plain': { | ||
schema: { | ||
type: 'string', | ||
example: 'Unsupported media type', | ||
}, | ||
}, | ||
}, | ||
}; | ||
exports.internalServerErrorResponse = { | ||
@@ -106,2 +117,3 @@ description: "Something went wrong on the server side and we were unable to recover. If you have custom strategies loaded, make sure they don't throw errors.", | ||
401: exports.unauthorizedResponse, | ||
415: exports.unsupportedMediaTypeResponse, | ||
500: exports.internalServerErrorResponse, | ||
@@ -108,0 +120,0 @@ 501: exports.notImplementedError, |
@@ -317,25 +317,54 @@ "use strict"; | ||
}); | ||
test('Should return not ready', () => { | ||
const client = new client_mock_1.default(); | ||
const proxySecrets = ['sdf']; | ||
const app = (0, app_1.createApp)({ unleashUrl, unleashApiToken, proxySecrets }, client); | ||
return (0, supertest_1.default)(app).get('/proxy/health').expect(503); | ||
describe.each([ | ||
'', | ||
'/all', | ||
'/health', | ||
'/client/features', | ||
'/internal-backstage/prometheus', | ||
])('GET should return not ready', (url) => { | ||
test(`for ${url}`, () => { | ||
const client = new client_mock_1.default(); | ||
const proxySecrets = ['sdf']; | ||
const app = (0, app_1.createApp)({ unleashUrl, unleashApiToken, proxySecrets }, client); | ||
return (0, supertest_1.default)(app).get(`/proxy${url}`).expect(503); | ||
}); | ||
}); | ||
test('Should return ready', () => { | ||
const client = new client_mock_1.default(); | ||
const proxySecrets = ['sdf']; | ||
const app = (0, app_1.createApp)({ unleashUrl, unleashApiToken, proxySecrets }, client); | ||
client.emit('ready'); | ||
return (0, supertest_1.default)(app) | ||
.get('/proxy/health') | ||
.expect(200) | ||
.then((response) => { | ||
expect(response.text).toEqual('ok'); | ||
describe.each(['', '/all', '/client/features'])('Requires valid token', (url) => { | ||
test(`Should return 401 if invalid api token for ${url}`, () => { | ||
const client = new client_mock_1.default(); | ||
const proxySecrets = ['secret']; | ||
const app = (0, app_1.createApp)({ proxySecrets, unleashUrl, unleashApiToken }, client); | ||
client.emit('ready'); | ||
return (0, supertest_1.default)(app) | ||
.get(`/proxy${url}`) | ||
.set('Authorization', 'I do not know your secret') | ||
.expect(401); | ||
}); | ||
test(`Should return 401 if no api token for ${url}`, () => { | ||
const client = new client_mock_1.default(); | ||
const proxySecrets = ['secret']; | ||
const app = (0, app_1.createApp)({ proxySecrets, unleashUrl, unleashApiToken }, client); | ||
client.emit('ready'); | ||
return (0, supertest_1.default)(app).get(`/proxy${url}`).expect(401); | ||
}); | ||
}); | ||
test('Should return 503 for /health', () => { | ||
const client = new client_mock_1.default(); | ||
const proxySecrets = ['sdf']; | ||
const app = (0, app_1.createApp)({ unleashUrl, unleashApiToken, proxySecrets }, client); | ||
return (0, supertest_1.default)(app).get('/proxy/health').expect(503); | ||
describe.each([ | ||
{ url: '/health', responseBody: 'ok' }, | ||
{ | ||
url: '/internal-backstage/prometheus', | ||
responseBody: 'unleash_proxy_up 1', | ||
}, | ||
])('Do not require valid token', ({ url, responseBody }) => { | ||
test(`Should not require a token for ${url}`, () => { | ||
const client = new client_mock_1.default(); | ||
const proxySecrets = ['sdf']; | ||
const app = (0, app_1.createApp)({ unleashUrl, unleashApiToken, proxySecrets }, client); | ||
client.emit('ready'); | ||
return (0, supertest_1.default)(app) | ||
.get(`/proxy${url}`) | ||
.expect(200) | ||
.then((response) => { | ||
expect(response.text).toMatch(responseBody); | ||
}); | ||
}); | ||
}); | ||
@@ -525,2 +554,44 @@ test('Should return 504 for proxy', () => { | ||
}); | ||
describe('Request content-types', () => { | ||
test.each(['/proxy', '/proxy/all'])('Should assign default content-type if the request has a body but no content-type (%s)', async (endpoint) => { | ||
const client = new client_mock_1.default(); | ||
const payload = { | ||
context: { appName: 'my-app' }, | ||
}; | ||
const proxySecrets = ['sdf']; | ||
const app = (0, app_1.createApp)({ | ||
unleashUrl, | ||
unleashApiToken, | ||
proxySecrets, | ||
enableAllEndpoint: true, | ||
}, client); | ||
client.emit('ready'); | ||
await (0, supertest_1.default)(app) | ||
.post(endpoint) | ||
.set('Authorization', 'sdf') | ||
.set('Content-Type', '') | ||
.send(payload) | ||
.expect(200) | ||
.then(() => { | ||
expect(client.queriedContexts[0].appName).toEqual(payload.context.appName); | ||
}); | ||
}); | ||
test.each(['/proxy', '/proxy/all'])('Should reject non-"content-type: application/json" for POST requests to %s', (endpoint) => { | ||
const client = new client_mock_1.default(); | ||
const proxySecrets = ['sdf']; | ||
const app = (0, app_1.createApp)({ | ||
unleashUrl, | ||
unleashApiToken, | ||
proxySecrets, | ||
enableAllEndpoint: true, | ||
}, client); | ||
client.emit('ready'); | ||
return (0, supertest_1.default)(app) | ||
.post(endpoint) | ||
.set('Authorization', 'sdf') | ||
.set('Content-Type', 'application/html') | ||
.send('<em>reject me!</em>') | ||
.expect(415); | ||
}); | ||
}); | ||
//# sourceMappingURL=unleash-proxy.test.js.map |
@@ -24,6 +24,8 @@ import { Request, Response, Router } from 'express'; | ||
setClientKeys(clientKeys: string[]): void; | ||
getAllToggles(req: Request, res: Response<FeaturesSchema | string>): void; | ||
getAllTogglesPOST(req: Request, res: Response<FeaturesSchema | string>): void; | ||
getEnabledToggles(req: Request, res: Response<FeaturesSchema | string>): void; | ||
lookupToggles(req: Request<any, any, LookupTogglesSchema>, res: Response<FeaturesSchema | string>): void; | ||
private readyMiddleware; | ||
private clientTokenMiddleware; | ||
getAllToggles(req: Request, res: Response<FeaturesSchema | string>): Promise<void>; | ||
getAllTogglesPOST(req: Request, res: Response<FeaturesSchema | string>): Promise<void>; | ||
getEnabledToggles(req: Request, res: Response<FeaturesSchema | string>): Promise<void>; | ||
lookupToggles(req: Request<any, any, LookupTogglesSchema>, res: Response<FeaturesSchema | string>): Promise<void>; | ||
health(_: Request, res: Response<string>): void; | ||
@@ -30,0 +32,0 @@ prometheus(_: Request, res: Response<string>): void; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const express_1 = require("express"); | ||
const create_context_1 = require("./create-context"); | ||
const enrich_context_1 = require("./enrich-context"); | ||
const features_response_1 = require("./openapi/spec/features-response"); | ||
@@ -14,2 +12,3 @@ const common_responses_1 = require("./openapi/common-responses"); | ||
const openapi_helpers_1 = require("./openapi/openapi-helpers"); | ||
const context_middleware_1 = require("./context-middleware"); | ||
class UnleashProxy { | ||
@@ -30,2 +29,3 @@ constructor(client, config, openApiService) { | ||
: []; | ||
const contextMiddleware = (0, context_middleware_1.createContexMiddleware)(this.contextEnrichers); | ||
if (client.isReady()) { | ||
@@ -65,3 +65,3 @@ this.setReady(); | ||
tags: ['Proxy client'], | ||
}), this.getEnabledToggles.bind(this)); | ||
}), this.readyMiddleware.bind(this), this.clientTokenMiddleware.bind(this), contextMiddleware, this.getEnabledToggles.bind(this)); | ||
router.get('/all', openApiService.validPath({ | ||
@@ -89,26 +89,38 @@ parameters: [ | ||
}, | ||
description: 'This endpoint returns the list of feature toggles that the proxy evaluates to enabled and disabled for the given context. As such, this endpoint always returns all feature toggles the proxy retrieves from unleash. Useful if you are migrating to unleash and need to know if the feature flag exists on the unleash server. However, using this endpoint will increase the payload size transmitted to your applications. Context values are provided as query parameters.', | ||
summary: 'Retrieve enabled feature toggles for the provided context.', | ||
description: `This endpoint returns all feature toggles known to the proxy, along with whether they are enabled or disabled for the provided context. This endpoint always returns **all** feature toggles the proxy retrieves from Unleash, in contrast to the \`/proxy\` endpoints that only return enabled toggles. | ||
Useful if you are migrating to unleash and need to know if the feature flag exists on the Unleash server. | ||
However, using this endpoint will increase the payload size transmitted to your applications. Context values are provided as query parameters.`, | ||
summary: 'Retrieve all feature toggles from the proxy.', | ||
tags: ['Proxy client'], | ||
}), this.getAllToggles.bind(this)); | ||
}), this.readyMiddleware.bind(this), this.clientTokenMiddleware.bind(this), contextMiddleware, this.getAllToggles.bind(this)); | ||
router.post('/all', openApiService.validPath({ | ||
requestBody: lookup_toggles_request_1.lookupTogglesRequest, | ||
responses: { | ||
...(0, common_responses_1.standardResponses)(401, 500, 501, 503), | ||
...(0, common_responses_1.standardResponses)(401, 415, 500, 501, 503), | ||
200: features_response_1.featuresResponse, | ||
}, | ||
description: 'This endpoint returns the list of feature toggles that the proxy evaluates to enabled and disabled for the given context. As such, this endpoint always returns all feature toggles the proxy retrieves from unleash. Useful if you are migrating to unleash and need to know if the feature flag exists on the unleash server. However, using this endpoint will increase the payload size transmitted to your applications. Context values are provided as query parameters.', | ||
summary: 'Retrieve enabled feature toggles for the provided context.', | ||
description: `This endpoint accepts a JSON object with a \`context\` property and an optional \`toggles\` property. | ||
If you provide the \`toggles\` property, the proxy will use the provided context value to evaluate each of the toggles you sent in. The proxy returns a list with all the toggles you provided in their fully evaluated states. | ||
If you don't provide the \`toggles\` property, then this operation functions exactly the same as the GET operation on this endpoint, except that it uses the \`context\` property instead of query parameters: The proxy will evaluate all its toggles against the context you provide and return a list of all known feature toggles and their evaluated state.`, | ||
summary: 'Evaluate some or all toggles against the provided context.', | ||
tags: ['Proxy client'], | ||
}), this.getAllTogglesPOST.bind(this)); | ||
}), this.readyMiddleware.bind(this), this.clientTokenMiddleware.bind(this), contextMiddleware, this.getAllTogglesPOST.bind(this)); | ||
router.post('', openApiService.validPath({ | ||
requestBody: lookup_toggles_request_1.lookupTogglesRequest, | ||
responses: { | ||
...(0, common_responses_1.standardResponses)(400, 401, 500, 503), | ||
...(0, common_responses_1.standardResponses)(400, 401, 415, 500, 503), | ||
200: features_response_1.featuresResponse, | ||
}, | ||
description: 'This endpoint accepts a JSON object with `context` and `toggles` properties. The Proxy will use the provided context values and evaluate the toggles provided in the `toggle` property. It returns the toggles that evaluate to false. As such, the list it returns is always a subset of the toggles you provide it.', | ||
summary: 'Which of the provided toggles are enabled given the provided context?', | ||
description: `This endpoint accepts a JSON object with a \`context\` property and an optional \`toggles\` property. | ||
If you provide the \`toggles\` property, the proxy will use the provided context value to evaluate each of the toggles you sent in. The proxy returns a list with all the toggles you provided in their fully evaluated states. | ||
If you don't provide the \`toggles\` property, then this operation functions exactly the same as the GET operation on this endpoint, except that it uses the \`context\` property instead of query parameters: The proxy will evaluate all its toggles against the context you provide and return a list of enabled toggles.`, | ||
summary: 'Evaluate specific toggles against the provided context.', | ||
tags: ['Proxy client'], | ||
}), this.lookupToggles.bind(this)); | ||
}), this.readyMiddleware.bind(this), this.clientTokenMiddleware.bind(this), contextMiddleware, this.lookupToggles.bind(this)); | ||
router.get('/client/features', openApiService.validPath({ | ||
@@ -122,3 +134,3 @@ responses: { | ||
tags: ['Server-side client'], | ||
}), this.unleashApi.bind(this)); | ||
}), this.readyMiddleware.bind(this), this.clientTokenMiddleware.bind(this), this.unleashApi.bind(this)); | ||
router.post('/client/metrics', openApiService.validPath({ | ||
@@ -146,3 +158,3 @@ requestBody: register_metrics_request_1.registerMetricsRequest, | ||
tags: ['Operational'], | ||
}), this.health.bind(this)); | ||
}), this.readyMiddleware.bind(this), this.health.bind(this)); | ||
router.get('/internal-backstage/prometheus', openApiService.validPath({ | ||
@@ -157,3 +169,3 @@ security: [], | ||
tags: ['Operational'], | ||
}), this.prometheus.bind(this)); | ||
}), this.readyMiddleware.bind(this), this.prometheus.bind(this)); | ||
} | ||
@@ -171,108 +183,74 @@ setReady() { | ||
} | ||
getAllToggles(req, res) { | ||
const apiToken = req.header(this.clientKeysHeaderName); | ||
if (!this.enableAllEndpoint) { | ||
res.status(501).send('The /proxy/all endpoint is disabled. Please check your server configuration. To enable it, set the `enableAllEndpoint` configuration option or `ENABLE_ALL_ENDPOINT` environment variable to `true`.'); | ||
} | ||
else if (!this.ready) { | ||
readyMiddleware(req, res, next) { | ||
if (!this.ready) { | ||
res.status(503).send(common_responses_1.NOT_READY_MSG); | ||
} | ||
else if (!apiToken || !this.clientKeys.includes(apiToken)) { | ||
res.sendStatus(401); | ||
} | ||
else { | ||
const { query } = req; | ||
query.remoteAddress = query.remoteAddress || req.ip; | ||
const context = (0, create_context_1.createContext)(query); | ||
const toggles = this.client.getAllToggles(context); | ||
res.set('Cache-control', 'public, max-age=2'); | ||
res.send({ toggles }); | ||
next(); | ||
} | ||
} | ||
getAllTogglesPOST(req, res) { | ||
clientTokenMiddleware(req, res, next) { | ||
const apiToken = req.header(this.clientKeysHeaderName); | ||
if (!this.enableAllEndpoint) { | ||
res.status(501).send('The /proxy/all endpoint is disabled. Please check your server configuration. To enable it, set the `enableAllEndpoint` configuration option or `ENABLE_ALL_ENDPOINT` environment variable to `true`.'); | ||
} | ||
else if (!this.ready) { | ||
res.status(503).send(common_responses_1.NOT_READY_MSG); | ||
} | ||
else if (!apiToken || !this.clientKeys.includes(apiToken)) { | ||
if (!apiToken || !this.clientKeys.includes(apiToken)) { | ||
res.sendStatus(401); | ||
} | ||
else { | ||
res.set('Cache-control', 'public, max-age=2'); | ||
const { context = {}, toggles: toggleNames = [] } = req.body; | ||
const actualContext = (0, create_context_1.createContext)(context); | ||
if (toggleNames.length > 0) { | ||
const toggles = this.client.getDefinedToggles(toggleNames, actualContext); | ||
res.send({ toggles }); | ||
} | ||
else { | ||
context.remoteAddress = context.remoteAddress || req.ip; | ||
const toggles = this.client.getAllToggles(actualContext); | ||
res.send({ toggles }); | ||
} | ||
next(); | ||
} | ||
} | ||
getEnabledToggles(req, res) { | ||
const apiToken = req.header(this.clientKeysHeaderName); | ||
if (!this.ready) { | ||
res.status(503).send(common_responses_1.NOT_READY_MSG); | ||
async getAllToggles(req, res) { | ||
if (!this.enableAllEndpoint) { | ||
res.status(501).send('The /proxy/all endpoint is disabled. Please check your server configuration. To enable it, set the `enableAllEndpoint` configuration option or `ENABLE_ALL_ENDPOINT` environment variable to `true`.'); | ||
return; | ||
} | ||
else if (!apiToken || !this.clientKeys.includes(apiToken)) { | ||
res.sendStatus(401); | ||
} | ||
else { | ||
const { query } = req; | ||
query.remoteAddress = query.remoteAddress || req.ip; | ||
(0, enrich_context_1.enrichContext)(this.contextEnrichers, (0, create_context_1.createContext)(query)).then((context) => { | ||
const toggles = this.client.getEnabledToggles(context); | ||
res.set('Cache-control', 'public, max-age=2'); | ||
res.send({ toggles }); | ||
}); | ||
} | ||
const { context } = res.locals; | ||
const toggles = this.client.getAllToggles(context); | ||
res.set('Cache-control', 'public, max-age=2'); | ||
res.send({ toggles }); | ||
} | ||
lookupToggles(req, res) { | ||
const clientToken = req.header(this.clientKeysHeaderName); | ||
if (!this.ready) { | ||
res.status(503).send(common_responses_1.NOT_READY_MSG); | ||
async getAllTogglesPOST(req, res) { | ||
if (!this.enableAllEndpoint) { | ||
res.status(501).send('The /proxy/all endpoint is disabled. Please check your server configuration. To enable it, set the `enableAllEndpoint` configuration option or `ENABLE_ALL_ENDPOINT` environment variable to `true`.'); | ||
return; | ||
} | ||
else if (!clientToken || !this.clientKeys.includes(clientToken)) { | ||
res.sendStatus(401); | ||
res.set('Cache-control', 'public, max-age=2'); | ||
const { toggles: toggleNames = [] } = req.body; | ||
const { context } = res.locals; | ||
if (toggleNames.length > 0) { | ||
const toggles = this.client.getDefinedToggles(toggleNames, context); | ||
res.send({ toggles }); | ||
} | ||
else { | ||
res.set('Cache-control', 'public, max-age=2'); | ||
const { context = {}, toggles: toggleNames = [] } = req.body; | ||
const actualContext = (0, create_context_1.createContext)(context); | ||
if (toggleNames.length > 0) { | ||
const toggles = this.client.getDefinedToggles(toggleNames, actualContext); | ||
res.send({ toggles }); | ||
} | ||
else { | ||
context.remoteAddress = context.remoteAddress || req.ip; | ||
const toggles = this.client.getEnabledToggles(actualContext); | ||
res.send({ toggles }); | ||
} | ||
const toggles = this.client.getAllToggles(context); | ||
res.send({ toggles }); | ||
} | ||
} | ||
health(_, res) { | ||
if (!this.ready) { | ||
res.status(503).send(common_responses_1.NOT_READY_MSG); | ||
async getEnabledToggles(req, res) { | ||
const { context } = res.locals; | ||
const toggles = this.client.getEnabledToggles(context); | ||
res.set('Cache-control', 'public, max-age=2'); | ||
res.send({ toggles }); | ||
} | ||
async lookupToggles(req, res) { | ||
res.set('Cache-control', 'public, max-age=2'); | ||
const { toggles: toggleNames = [] } = req.body; | ||
const { context } = res.locals; | ||
if (toggleNames.length > 0) { | ||
const toggles = this.client.getDefinedToggles(toggleNames, context); | ||
res.send({ toggles }); | ||
} | ||
else { | ||
res.send('ok'); | ||
const toggles = this.client.getEnabledToggles(context); | ||
res.send({ toggles }); | ||
} | ||
} | ||
health(_, res) { | ||
res.send('ok'); | ||
} | ||
prometheus(_, res) { | ||
if (!this.ready) { | ||
res.status(503).send(common_responses_1.NOT_READY_MSG); | ||
} | ||
else { | ||
const prometheusResponse = '# HELP unleash_proxy_up Indication that the service is up. \n' + | ||
'# TYPE unleash_proxy_up counter\n' + | ||
'unleash_proxy_up 1\n'; | ||
res.set('Content-type', 'text/plain'); | ||
res.send(prometheusResponse); | ||
} | ||
const prometheusResponse = '# HELP unleash_proxy_up Indication that the service is up. \n' + | ||
'# TYPE unleash_proxy_up counter\n' + | ||
'unleash_proxy_up 1\n'; | ||
res.set('Content-type', 'text/plain'); | ||
res.send(prometheusResponse); | ||
} | ||
@@ -302,14 +280,5 @@ registerMetrics(req, res) { | ||
unleashApi(req, res) { | ||
const apiToken = req.header(this.clientKeysHeaderName); | ||
if (!this.ready) { | ||
res.status(503).send(common_responses_1.NOT_READY_MSG); | ||
} | ||
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); | ||
} | ||
const features = this.client.getFeatureToggleDefinitions(); | ||
res.set('Cache-control', 'public, max-age=2'); | ||
res.send({ version: 2, features }); | ||
} | ||
@@ -316,0 +285,0 @@ } |
{ | ||
"name": "@unleash/proxy", | ||
"version": "0.14.0-beta.0", | ||
"version": "0.14.0-beta.1", | ||
"description": "The Unleash Proxy (Open-Source)", | ||
@@ -47,7 +47,8 @@ "main": "dist/index.js", | ||
"openapi-types": "^11.0.0", | ||
"type-is": "^1.6.18", | ||
"unleash-client": "^3.18.0" | ||
}, | ||
"devDependencies": { | ||
"@apidevtools/swagger-parser": "10.1.0", | ||
"@babel/core": "^7.17.10", | ||
"@types/node": "18.7.14", | ||
"@types/compression": "^1.7.2", | ||
@@ -57,6 +58,7 @@ "@types/cors": "^2.8.12", | ||
"@types/jest": "^27.5.0", | ||
"@types/node": "18.7.14", | ||
"@types/supertest": "^2.0.12", | ||
"@types/type-is": "^1.6.3", | ||
"@typescript-eslint/eslint-plugin": "^5.22.0", | ||
"@typescript-eslint/parser": "^5.22.0", | ||
"@apidevtools/swagger-parser": "10.1.0", | ||
"babel-jest": "^28.0.3", | ||
@@ -63,0 +65,0 @@ "eslint": "^8.14.0", |
594
README.md
@@ -1,221 +0,183 @@ | ||
![Build & Tests](https://github.com/Unleash/unleash-proxy/workflows/Node.js%20CI/badge.svg?branch=main) | ||
[![npm](https://img.shields.io/npm/v/@unleash/proxy)](https://www.npmjs.com/package/@unleash/proxy) | ||
[![Docker Pulls](https://img.shields.io/docker/pulls/unleashorg/unleash-proxy)](https://hub.docker.com/r/unleashorg/unleash-proxy) | ||
[![Build & Tests](https://github.com/Unleash/unleash-proxy/workflows/Node.js%20CI/badge.svg?branch=main)](https://github.com/Unleash/unleash-proxy/actions/workflows/node.js.yml) [![npm](https://img.shields.io/npm/v/@unleash/proxy)](https://www.npmjs.com/package/@unleash/proxy) [![Docker Pulls](https://img.shields.io/docker/pulls/unleashorg/unleash-proxy)](https://hub.docker.com/r/unleashorg/unleash-proxy) | ||
# The Unleash Proxy | ||
The Unleash Proxy simplifies integration with frontend & native applications running in the context of a specific user. The Unleash proxy sits between the proxy SDK and the | ||
Unleash API and ensures that your internal feature toggle configuration is not | ||
exposed to the world. | ||
The Unleash proxy offers a way to use Unleash in client-side applications, such as single-page and native apps. | ||
The proxy offers: | ||
The Unleash proxy sits between the Unleash API and your client-side SDK and does the evaluation of feature toggles for your client-side SDK. This way, you can keep your configuration private and secure, while still allowing your client-side apps to use Unleash's features. | ||
- **High performance** - a single proxy instance can handle thousands req/s, and can be horizontally scaled. | ||
- **Privacy for end-users** - Your end users are not exposed to the unleash API and can be hosted by you This ensures no user data (userId, IPs, etc) is shared. | ||
- **Secure** - It is controlled by you, and can hosted on your domain. In addition no feature toggle configuration is shared with the user, only evaluated toggles. | ||
The proxy offers three important features: | ||
- **Performance**: The caches all features in memory and can run close to your end-users. A single instance can able to handle thousands of requests per second, and you can easily scale it by adding additional instances. | ||
- **Security**: The proxy evaluates the features for the user on the server-side and by default only exposes results for features that are **enabled** for the specific user. No feature toggle configuration is ever shared with the user. | ||
- **Privacy**: If you run the proxy yourself, Unleash will never get any data on your end-users: no user ids, no IPs, no nothing. | ||
You can read more about [the proxy in our documentation](https://docs.getunleash.io/sdks/unleash-proxy) | ||
<figure> | ||
<img src="./.github/img/connection-overview.svg" /> | ||
<figcaption>Client-side apps connect to the Unleash proxy, which in turn connects to the Unleash API. The proxy itself uses the Unleash Node.js SDK to evaluate features. The SDK syncs with Unleash in the background. Local evaluation on the proxy provides privacy.</figcaption> | ||
</figure> | ||
## Run The Unleash Proxy | ||
## A note on privacy and the proxy | ||
The Unleash proxy is a small stateless HTTP application you run. The only requirement is that it needs to be able to talk with the Unleash API (either Unleash OSS or Unleash Hosted). | ||
Why would you not want to expose your Unleash configuration to your end-users? | ||
The way Unleash works, you can add all kinds of data to feature strategies and constraints. For instance, you might show a feature only to a specific subset of user IDs; or you might have a brand new and unannounced new feature with a revealing name. | ||
### Run with Docker | ||
If you just sent the regular Unleash client payload to your client-side apps, all of this — the user IDs and the new feature names — would be exposed to your users. | ||
The easies way to run Unleash is via Docker. We have published a [docker image on docker hub](https://hub.docker.com/r/unleashorg/unleash-proxy/). | ||
Single page apps work in the context of a specific user. The proxy allows you to only provide data that relates to that one user: **The proxy will default to only returning the evaluated toggles that should be enabled for that _specific_ user in that _specific_ context.** | ||
**Step 1: Pull** | ||
## API | ||
```bash | ||
docker pull unleashorg/unleash-proxy | ||
``` | ||
The Unleash proxy exposes a simple API for consumption by client-side SDKs. | ||
**Step 2: Start** | ||
### OpenAPI integration | ||
```bash | ||
docker run \ | ||
-e UNLEASH_PROXY_CLIENT_KEYS=some-secret \ | ||
-e UNLEASH_URL=https://app.unleash-hosted.com/demo/api/ \ | ||
-e UNLEASH_API_TOKEN=56907a2fa53c1d16101d509a10b78e36190b0f918d9f122d \ | ||
-p 3000:3000 \ | ||
unleashorg/unleash-proxy | ||
``` | ||
--- | ||
You should see the following output: | ||
ℹ️ **Availability** | ||
```bash | ||
Unleash-proxy is listening on port 3000! | ||
```` | ||
The OpenAPI integration is available in versions 0.9 and later of the Unleash proxy. | ||
--- | ||
**Step 3: verify** | ||
The proxy can expose a runtime-generated OpenAPI JSON spec and a corresponding OpenAPI UI for its API. The OpenAPI UI page is an interactive page where you can discover and test the API endpoints the proxy exposes. The JSON spec can be used to generate an OpenAPI client with OpenAPI tooling such as the [OpenAPI generator](https://openapi-generator.tech/). | ||
In order to verify the proxy you can use curl and see that you get a few evaluated feature toggles back: | ||
To enable the JSON spec and UI, set `ENABLE_OAS` (environment variable) or `enableOAS` (in-code configuration variable) to `true`. | ||
```bash | ||
curl http://localhost:3000/proxy -H "Authorization: some-secret" | ||
``` | ||
The spec and UI can then be found at `<base url>/docs/openapi.json` and `<base url>/docs/openapi` respectively. | ||
Expected output would be something like: | ||
You can refer to the [how to enable the OpenAPI spec](https://docs.getunleash.io/how-to/how-to-enable-openapi) guide for more detailed information on how to configure it. | ||
### `GET /proxy` | ||
The primary proxy API operation. This endpoint accepts an Unleash context encoded as query parameters, and will return all toggles that are evaluated as true for the provided context. | ||
<figure> | ||
<img src="./.github/img/get-request.png" /> | ||
<figcaption>When sending GET requests to the Unleash proxy's /proxy endpoint, the request should contain the current Unleash context as query parameters. The proxy will return all enabled toggles for the provided context.</figcaption> | ||
</figure> | ||
### Payload | ||
The `GET /proxy` operation returns information about toggles enabled for the current user. The payload is a JSON object with a `toggles` property, which contains a list of toggles. | ||
```json | ||
{ | ||
"toggles": [{ | ||
"name": "demo", | ||
"enabled": true, | ||
"variant": { | ||
"name": "disabled", | ||
"enabled": false | ||
} | ||
}, { | ||
"name": "demoApp.step1", | ||
"enabled": true, | ||
"variant": { | ||
"name": "disabled", | ||
"enabled": false | ||
} | ||
}] | ||
"toggles": [ | ||
{ | ||
"name": "demo", | ||
"enabled": true, | ||
"variant": { | ||
"name": "disabled", | ||
"enabled": false | ||
} | ||
}, | ||
{ | ||
"name": "demoApp.step1", | ||
"enabled": true, | ||
"variant": { | ||
"name": "disabled", | ||
"enabled": false | ||
} | ||
} | ||
] | ||
} | ||
``` | ||
**Health endpoint** | ||
#### Toggle data | ||
The proxy will try to synchronize with the Unleash API at startup, until it has successfully done that the proxy will return `HTTP 503 - Not Read?` for all request. You can use the health endpoint to validate that the proxy is ready to recieve requests: | ||
The data for a toggle without [variants](https://docs.getunleash.io/reference/feature-toggle-variants) looks like this: | ||
```bash | ||
curl http://localhost:3000/proxy/health -I | ||
``` | ||
```bash | ||
HTTP/1.1 200 OK | ||
Access-Control-Allow-Origin: * | ||
Access-Control-Expose-Headers: ETag | ||
Content-Type: text/html; charset=utf-8 | ||
Content-Length: 2 | ||
ETag: W/"2-eoX0dku9ba8cNUXvu/DyeabcC+s" | ||
Date: Fri, 04 Jun 2021 10:38:27 GMT | ||
Connection: keep-alive | ||
Keep-Alive: timeout=5 | ||
```json | ||
{ | ||
"name": "basic-toggle", | ||
"enabled": true, | ||
"variant": { | ||
"name": "disabled", | ||
"enabled": false | ||
} | ||
} | ||
``` | ||
### Available options | ||
- **`name`**: the name of the feature. | ||
- **`enabled`**: whether the toggle is enabled or not. Will always be `true`. | ||
- **`variant`**: describes whether the toggle has variants and, if it does, what variant is active for this user. If a toggle doesn't have any variants, it will always be `{"name": "disabled", "enabled": false}`. | ||
| Option | Environment Variable | Default value | Required | Description | | ||
|---------------------------|----------------------------------|-----------------|:--------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ||
| unleashUrl | `UNLEASH_URL` | n/a | yes | API Url to the Unleash instance to connect to | | ||
| unleashApiToken | `UNLEASH_API_TOKEN` | n/a | yes | API token (client) needed to connect to Unleash API. | | ||
| clientKeys | `UNLEASH_PROXY_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. | | ||
| n/a | `PORT` or `PROXY_PORT` | 3000 | no | The port where the proxy should listen. | | ||
| proxyBasePath | `PROXY_BASE_PATH` | "" | no | The base path to run the proxy from. "/proxy" will be added at the end. For instance, if `proxyBasePath` is `"base/path"`, the proxy will run at `/base/path/proxy`. | | ||
| unleashAppName | `UNLEASH_APP_NAME` | "unleash-proxy" | no | App name to used when registering with Unleash | | ||
| unleashInstanceId | `UNLEASH_INSTANCE_ID` | `generated` | no | Unleash instance id to used when registering with Unleash | | ||
| refreshInterval | `UNLEASH_FETCH_INTERVAL` | 5000 | no | How often the proxy should query Unleash for updates, defined in ms. | | ||
| metricsInterval | `UNLEASH_METRICS_INTERVAL` | 30000 | no | How often the proxy should send usage metrics back to Unleash, defined in ms. | | ||
| metricsJitter | `UNLEASH_METRICS_JITTER` | 0 | no | Adds jitter to the metrics interval to avoid multiple instances sending metrics at the same time, defined in ms. | | ||
| environment | `UNLEASH_ENVIRONMENT` | `undefined` | no | If set this will be the `environment` used by the proxy in the Unleash Context. It will not be possible for proxy SDKs to override the environment if set. | | ||
| projectName | `UNLEASH_PROJECT_NAME` | `undefined` | no | The projectName (id) to fetch feature toggles for. The proxy will only return know about feature toggles that belongs to the project, if specified. | | ||
| logger | n/a | SimpleLogger | no | Register a custom logger. | | ||
| useJsonLogger | `JSON_LOGGER` | false | no | Sets the default logger to log as one-line JSON. This has no effect if a custom logger is provided. | | ||
| logLevel | `LOG_LEVEL ` | "warn" | no | Used to set logLevel. Supported options: "debug", "info", "warn", "error" and "fatal" | | ||
| customStrategies | `UNLEASH_CUSTOM_STRATEGIES_FILE` | [] | no | Use this option to inject implementation of custom activation strategies. If you are using `UNLEASH_CUSTOM_STRATEGIES_FILE` you need to provide a valid path to a javascript file which exports an array of custom activation strategies and the SDK will automatically load these | | ||
| trustProxy | `TRUST_PROXY ` | `false` | no | By enabling the trustProxy option, Unleash Proxy will have knowledge that it's sitting behind a proxy and that the X-Forwarded-* header fields may be trusted, which otherwise may be easily spoofed. The proxy will automatically enrich the ip address in the Unleash Context. Can either be `true/false` (Trust all proxies), trust only given IP/CIDR (e.g. `'127.0.0.1'`) as a `string`. May be a list of comma separated values (e.g. `'127.0.0.1,192.168.1.1/24'` | | ||
| namePrefix | `UNLEASH_NAME_PREFIX` | undefined | no | Used to filter features by using prefix when requesting backend values. | | ||
| tags | `UNLEASH_TAGS` | undefined | no | Used to filter features by using tags set for features. Format should be `tagName:tagValue,tagName2:tagValue2` | | ||
| clientKeysHeaderName | `CLIENT_KEY_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. | | ||
| storageProvider | n/a | `undefined` | no | Register a custom storage provider. Refer to the [section on custom storage providers in the Node.js SDK's readme](https://github.com/Unleash/unleash-client-node/#custom-store-provider) for more information. | | ||
| enableOAS | `ENABLE_OAS` | `false` | no | Set to `true` to expose the proxy's OpenAPI spec at `/docs/openapi.json` and an interactive OpenAPI UI at `/docs/openapi`. Read more in the [OpenAPI section](#openapi). | | ||
| enableAllEndpoint | `ENABLE_ALL_ENDPOINT` | `false` | no | Set to `true` to expose the `/proxy/all` endpoint. Refer to the [section on returning all toggles](#return-enabled-and-disabled-toggles) for more info. | | ||
| cors | n/a | n/a | no | Pass custom options for [CORS module](https://www.npmjs.com/package/cors#configuration-options) | | ||
| cors.allowedHeaders | `CORS_ALLOWED_HEADERS` | n/a | no | Headers to allow for CORS | | ||
| cors.credentials | `CORS_CREDENTIALS` | `false` | no | Allow credentials in CORS requests | | ||
| cors.exposedHeaders | `CORS_EXPOSED_HEADERS` | "ETag" | no | Headers to expose for CORS | | ||
| cors.maxAge | `CORS_MAX_AGE` | 172800 | no | Maximum number of seconds to cache CORS results | | ||
| cors.methods | `CORS_METHODS` | "GET, POST" | no | Methods to allow for CORS | | ||
| cors.optionsSuccessStatus | `CORS_OPTIONS_SUCCESS_STATUS` | 204 | no | If the Options call passes CORS, which http status to return | | ||
| cors.origin | `CORS_ORIGIN` | * | no | Origin URL or list of comma separated list of URLs to whitelist for CORS | | ||
| cors.preflightContinue | `CORS_PREFLIGHT_CONTINUE` | `false` | no | | | ||
| httpOptions.rejectUnauthorized | `HTTP_OPTIONS_REJECT_UNAUTHORIZED` | `true` | no | If true, unleash-proxy will automatically reject connections to unleash server with invalid certificates | | ||
--- | ||
ℹ️ **The "disabled" variant** | ||
Unleash uses a fallback variant called "disabled" to indicate that a toggle has no variants. However, you are free to create a variant called "disabled" yourself. In that case you can tell them apart by checking the variant's `enabled` property: if the toggle has no variants, `enabled` will be `false`. If the toggle is the "disabled" variant that you created, it will have `enabled` set to `true`. | ||
### 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. | ||
If a toggle has variants, then the variant object can also contain an optional `payload` property. The `payload` will contain data about the variant's payload: what type it is, and what the content is. To learn more about variants and their payloads, check [the feature toggle variants documentation](https://docs.getunleash.io/reference/feature-toggle-variants). | ||
| 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. | | ||
| expCustomEnrichers | `EXP_CUSTOM_ENRICHERS_FILE` | [] | no | Use this option to inject implementation of custom context enrichers. If you are using `EXP_CUSTOM_ENRICHERS_FILE` you need to provide a valid path to a javascript file which exports an array of custom context enrichers and the SDK will automatically load these | | ||
Variant toggles without payloads look will have their name listed and the `enabled` property set to `true`: | ||
### Run with Node.js: | ||
```json | ||
{ | ||
"name": "toggle-with-variants", | ||
"enabled": true, | ||
"variant": { | ||
"name": "simple", | ||
"enabled": true | ||
} | ||
} | ||
``` | ||
**STEP 1: Install dependency** | ||
If the variant has a payload, the optional `payload` property will list the payload's type and it's content in a stringified form: | ||
```json | ||
{ | ||
"name": "toggle-with-variants", | ||
"enabled": true, | ||
"variant": { | ||
"name": "with-payload-string", | ||
"payload": { | ||
"type": "string", | ||
"value": "this string is the variant's payload" | ||
}, | ||
"enabled": true | ||
} | ||
} | ||
``` | ||
npm install @unleash/proxy | ||
``` | ||
For the `variant` property: | ||
**STEP 2: use in your code** | ||
- **`name`**: is the name of the variant, as shown in the Admin UI. | ||
- **`enabled`**: indicates whether the variant is enabled or not. If the toggle has variants, this is always `true`. | ||
- **`payload`** (optional): Only present if the variant has a payload. Describes the payload's type and content. | ||
```js | ||
const port = 3000; | ||
If the variant has a payload, the `payload` object contains: | ||
const { createApp } = require('@unleash/proxy'); | ||
- **`type`**: the type of the variant's payload | ||
- **`value`**: the value of the variant's payload | ||
The `value` will always be the payload's content as a string, escaped as necessary. For instance, a variant with a JSON payload would look like this: | ||
const app = createApp({ | ||
unleashUrl: 'https://app.unleash-hosted.com/demo/api/', | ||
unleashApiToken: '56907a2fa53c1d16101d509a10b78e36190b0f918d9f122d', | ||
clientKeys: ['proxy-secret', 'another-proxy-secret', 's1'], | ||
refreshInterval: 1000, | ||
// logLevel: 'info', | ||
// projectName: 'order-team', | ||
// environment: 'development', | ||
}); | ||
app.listen(port, () => | ||
// eslint-disable-next-line no-console | ||
console.log(`Unleash Proxy listening on http://localhost:${port}/proxy`), | ||
); | ||
```json | ||
{ | ||
"name": "toggle-with-variants", | ||
"enabled": true, | ||
"variant": { | ||
"name": "with-payload-json", | ||
"payload": { | ||
"type": "json", | ||
"value": "{\"description\": \"this is data delivered as a json string\"}" | ||
}, | ||
"enabled": true | ||
} | ||
} | ||
``` | ||
### `POST /proxy` | ||
## Proxy clients | ||
To make the integration simple we have developed proxy client SDKs. You can find them all in our [documentation](https://docs.getunleash.io/sdks/unleash-proxy#how-to-connect-to-the-proxy): | ||
The proxy also offers a POST API used to evaluate toggles This can be used to evaluate a list of know toggle names or to retrieve all _enabled_ toggles for a given context. | ||
#### Evaluate list of known toggles | ||
- [JavaScript Proxy SDK (browser)](https://github.com/unleash-hosted/unleash-proxy-client-js) | ||
- [Android Proxy SDK](https://github.com/Unleash/unleash-android-proxy-sdk) | ||
- [iOS Proxy SDK](https://github.com/Unleash/unleash-proxy-client-swift) | ||
This method will allow you to send a list of toggle names together with an Unleash Context and evaluate them accordingly. It will return enablement of all provided toggles. | ||
## OpenAPI | ||
The proxy can optionally expose a runtime-generated OpenAPI JSON spec and a corresponding OpenAPI UI for its API. The OpenAPI UI page is an interactive page where you can discover and test the API endpoints the proxy exposes. The JSON spec can be used to generate an OpenAPI client with OpenAPI tooling such as the [OpenAPI generator](https://openapi-generator.tech/). | ||
To enable the JSON spec and UI, set `ENABLE_OAS` (environment variable) or `enableOAS` (in-code configuration variable) to `true`. | ||
The spec and UI can then be found at `<base url>/docs/openapi.json` and `<base url>/docs/openapi` respectively. | ||
## Return enabled and disabled toggles | ||
By default, the proxy only returns enabled toggles. However, in certain use cases, you might want it to return **all** toggles, regardless of whether they're enabled or disabled. The `/proxy/all` endpoint does this. | ||
Because returning all toggles regardless of their state is a potential security vulnerability, the endpoint has to be explicitly enabled. To enable it, set the `enableAllEndpoint` configuration option or the `ENABLE_ALL_ENDPOINT` environment variable to `true`. | ||
## POST API | ||
The proxy also offers a POST API used to evaluate toggles This can be used to evaluate a list of know toggle names or to retrieve all _enabled_ toggles for a given context. | ||
### a) Evaluate list of known toggles | ||
This method will allow you to send a list of toggle names together with an Unleash Context and evaluate them accordingly. It will return enablement of all provided toggles. | ||
**URL**: `POST https://proxy-host.server/proxy` | ||
@@ -229,9 +191,10 @@ | ||
{ | ||
"toggles": ["demoApp.step1"], | ||
"context": { | ||
"appName": "someApp", | ||
"sessionId": "233312AFF22", | ||
} | ||
"toggles": ["demoApp.step1"], | ||
"context": { | ||
"appName": "someApp", | ||
"sessionId": "233312AFF22" | ||
} | ||
} | ||
``` | ||
Result: | ||
@@ -268,7 +231,6 @@ | ||
#### Evaluate all enabled toggles | ||
### a) Evaluate all enabled toggles | ||
This method will allow you to get all enabled toggles for a given context. | ||
This method will allow you to get all enabled toggles for a given context. | ||
**URL**: `POST https://proxy-host.server/proxy` | ||
@@ -282,8 +244,9 @@ | ||
{ | ||
"context": { | ||
"appName": "someApp", | ||
"sessionId": "233312AFF22", | ||
} | ||
"context": { | ||
"appName": "someApp", | ||
"sessionId": "233312AFF22" | ||
} | ||
} | ||
``` | ||
Result: | ||
@@ -349,2 +312,277 @@ | ||
``` | ||
``` | ||
### `GET /proxy/all` Return enabled **and** disabled toggles: | ||
By default, the proxy only returns enabled toggles. However, in certain use cases, you might want it to return **all** toggles, regardless of whether they're enabled or disabled. The `/proxy/all` endpoint does this. | ||
Because returning all toggles regardless of their state is a potential security vulnerability, the endpoint has to be explicitly enabled. To enable it, set the `enableAllEndpoint` configuration option or the `ENABLE_ALL_ENDPOINT` environment variable to `true`. | ||
The response payload follows the same format as the [`GET /proxy` response payload](#payload). | ||
### `GET /proxy/health`: Health endpoint | ||
The proxy will try to synchronize with the Unleash API at startup, until it has successfully done that the proxy will return `HTTP 503 - Not Ready` for all request. You can use the health endpoint to validate that the proxy is ready to receive requests: | ||
```bash | ||
curl http://localhost:3000/proxy/health -I | ||
``` | ||
If the proxy is ready, the response should look a little something like this: | ||
```bash | ||
HTTP/1.1 200 OK | ||
Access-Control-Allow-Origin: * | ||
Access-Control-Expose-Headers: ETag | ||
Content-Type: text/html; charset=utf-8 | ||
Content-Length: 2 | ||
ETag: W/"2-eoX0dku9ba8cNUXvu/DyeabcC+s" | ||
Date: Fri, 04 Jun 2021 10:38:27 GMT | ||
Connection: keep-alive | ||
Keep-Alive: timeout=5 | ||
``` | ||
## Configuration options | ||
The Proxy has a large number of configuration options that you can use to adjust it to your specific use case. The following table lists all the available options. | ||
--- | ||
ℹ️ **Required configuration** | ||
You **must configure** these three variables for the proxy to start successfully: | ||
- `unleashUrl` / `UNLEASH_URL` | ||
- `unleashApiToken` / `UNLEASH_API_TOKEN` | ||
- `clientKeys` / `UNLEASH_PROXY_CLIENT_KEYS` | ||
--- | ||
| Option | Environment Variable | Default value | Required | Description | | ||
| --- | --- | --- | :-: | --- | | ||
| unleashUrl | `UNLEASH_URL` | n/a | yes | API Url to the Unleash instance to connect to | | ||
| unleashApiToken | `UNLEASH_API_TOKEN` | n/a | yes | API token (client) needed to connect to Unleash API. | | ||
| clientKeys | `UNLEASH_PROXY_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. | | ||
| n/a | `PORT` or `PROXY_PORT` | 3000 | no | The port where the proxy should listen. | | ||
| proxyBasePath | `PROXY_BASE_PATH` | "" | no | The base path to run the proxy from. "/proxy" will be added at the end. For instance, if `proxyBasePath` is `"base/path"`, the proxy will run at `/base/path/proxy`. | | ||
| unleashAppName | `UNLEASH_APP_NAME` | "unleash-proxy" | no | App name to used when registering with Unleash | | ||
| unleashInstanceId | `UNLEASH_INSTANCE_ID` | `generated` | no | Unleash instance id to used when registering with Unleash | | ||
| refreshInterval | `UNLEASH_FETCH_INTERVAL` | 5000 | no | How often the proxy should query Unleash for updates, defined in ms. | | ||
| metricsInterval | `UNLEASH_METRICS_INTERVAL` | 30000 | no | How often the proxy should send usage metrics back to Unleash, defined in ms. | | ||
| metricsJitter | `UNLEASH_METRICS_JITTER` | 0 | no | Adds jitter to the metrics interval to avoid multiple instances sending metrics at the same time, defined in ms. | | ||
| environment | `UNLEASH_ENVIRONMENT` | `undefined` | no | If set this will be the `environment` used by the proxy in the Unleash Context. It will not be possible for proxy SDKs to override the environment if set. | | ||
| projectName | `UNLEASH_PROJECT_NAME` | `undefined` | no | The projectName (id) to fetch feature toggles for. The proxy will only return know about feature toggles that belongs to the project, if specified. | | ||
| logger | n/a | SimpleLogger | no | Register a custom logger. | | ||
| useJsonLogger | `JSON_LOGGER` | false | no | Sets the default logger to log as one-line JSON. This has no effect if a custom logger is provided. | | ||
| logLevel | `LOG_LEVEL ` | "warn" | no | Used to set logLevel. Supported options: "debug", "info", "warn", "error" and "fatal" | | ||
| customStrategies | `UNLEASH_CUSTOM_STRATEGIES_FILE` | [] | no | Use this option to inject implementation of custom activation strategies. If you are using `UNLEASH_CUSTOM_STRATEGIES_FILE` you need to provide a valid path to a javascript file which exports an array of custom activation strategies and the SDK will automatically load these | | ||
| trustProxy | `TRUST_PROXY ` | `false` | no | By enabling the trustProxy option, Unleash Proxy will have knowledge that it's sitting behind a proxy and that the X-Forwarded-\* header fields may be trusted, which otherwise may be easily spoofed. The proxy will automatically enrich the ip address in the Unleash Context. Can either be `true/false` (Trust all proxies), trust only given IP/CIDR (e.g. `'127.0.0.1'`) as a `string`. May be a list of comma separated values (e.g. `'127.0.0.1,192.168.1.1/24'` | | ||
| namePrefix | `UNLEASH_NAME_PREFIX` | undefined | no | Used to filter features by using prefix when requesting backend values. | | ||
| tags | `UNLEASH_TAGS` | undefined | no | Used to filter features by using tags set for features. Format should be `tagName:tagValue,tagName2:tagValue2` | | ||
| clientKeysHeaderName | `CLIENT_KEY_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. | | ||
| storageProvider | n/a | `undefined` | no | Register a custom storage provider. Refer to the [section on custom storage providers in the Node.js SDK's readme](https://github.com/Unleash/unleash-client-node/#custom-store-provider) for more information. | | ||
| enableOAS | `ENABLE_OAS` | `false` | no | Set to `true` to expose the proxy's OpenAPI spec at `/docs/openapi.json` and an interactive OpenAPI UI at `/docs/openapi`. Read more in the [OpenAPI section](#openapi). | | ||
| enableAllEndpoint | `ENABLE_ALL_ENDPOINT` | `false` | no | Set to `true` to expose the `/proxy/all` endpoint. Refer to the [section on returning all toggles](#return-enabled-and-disabled-toggles) for more info. | | ||
| cors | n/a | n/a | no | Pass custom options for [CORS module](https://www.npmjs.com/package/cors#configuration-options) | | ||
| cors.allowedHeaders | `CORS_ALLOWED_HEADERS` | n/a | no | Headers to allow for CORS | | ||
| cors.credentials | `CORS_CREDENTIALS` | `false` | no | Allow credentials in CORS requests | | ||
| cors.exposedHeaders | `CORS_EXPOSED_HEADERS` | "ETag" | no | Headers to expose for CORS | | ||
| cors.maxAge | `CORS_MAX_AGE` | 172800 | no | Maximum number of seconds to cache CORS results | | ||
| cors.methods | `CORS_METHODS` | "GET, POST" | no | Methods to allow for CORS | | ||
| cors.optionsSuccessStatus | `CORS_OPTIONS_SUCCESS_STATUS` | 204 | no | If the Options call passes CORS, which http status to return | | ||
| cors.origin | `CORS_ORIGIN` | \* | no | Origin URL or list of comma separated list of URLs to whitelist for CORS | | ||
| cors.preflightContinue | `CORS_PREFLIGHT_CONTINUE` | `false` | no | | | ||
| httpOptions.rejectUnauthorized | `HTTP_OPTIONS_REJECT_UNAUTHORIZED` | `true` | no | If true, unleash-proxy will automatically reject connections to unleash server with invalid certificates | | ||
### Experimental configuration options | ||
Some functionality is under validation and introduced as experimental. This allows us to test new functionality early and quickly. Experimental options can change or be removed at any point in the future and are explicitly **not** part of the proxy's semantic versioning. | ||
| 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. | | ||
| 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. | | ||
| expCustomEnrichers | `EXP_CUSTOM_ENRICHERS_FILE` | [] | no | Use this option to inject implementation of custom context enrichers. If you are using `EXP_CUSTOM_ENRICHERS_FILE` you need to provide a valid path to a javascript file which exports an array of custom context enrichers and the SDK will automatically load these | | ||
## Internal SDK capabilities | ||
The proxy uses the [Node.js SDK](https://github.com/Unleash/unleash-client-node) internally. When you start the proxy it initializes a client for you using any configuration option you provided on startup. You can also provide your own client. | ||
The client that the proxy uses supports all the SDK features that the Node.js SDK supports, as listed in the [Unleash server-side compatibility overview](https://docs.getunleash.io/reference/sdks#server-side-sdk-compatibility-table). The proxy has supported advanced [strategy constraints](https://docs.getunleash.io/reference/strategy-constraints) since version 0.8. Because the proxy uses the Node SDK internally, | ||
## Run The Unleash Proxy | ||
The Unleash proxy is a small stateless HTTP application you run. The only requirement is that it needs to be able to talk with the Unleash API (either Unleash OSS or Unleash Hosted). | ||
### Run with Docker | ||
The easies way to run Unleash is via Docker. We have published a [docker image on docker hub](https://hub.docker.com/r/unleashorg/unleash-proxy/). | ||
**Step 1: Pull** | ||
```bash | ||
docker pull unleashorg/unleash-proxy | ||
``` | ||
**Step 2: Start** | ||
```bash | ||
docker run \ | ||
-e UNLEASH_PROXY_CLIENT_KEYS=some-secret \ | ||
-e UNLEASH_URL=https://app.unleash-hosted.com/demo/api/ \ | ||
-e UNLEASH_API_TOKEN=56907a2fa53c1d16101d509a10b78e36190b0f918d9f122d \ | ||
-p 3000:3000 \ | ||
unleashorg/unleash-proxy | ||
``` | ||
You should see the following output: | ||
```bash | ||
Unleash-proxy is listening on port 3000! | ||
``` | ||
**Step 3: verify** | ||
In order to verify the proxy you can use curl and see that you get a few evaluated feature toggles back: | ||
```bash | ||
curl http://localhost:3000/proxy -H "Authorization: some-secret" | ||
``` | ||
Expected output would be something like: | ||
```json | ||
{ | ||
"toggles": [ | ||
{ | ||
"name": "demo", | ||
"enabled": true, | ||
"variant": { | ||
"name": "disabled", | ||
"enabled": false | ||
} | ||
}, | ||
{ | ||
"name": "demoApp.step1", | ||
"enabled": true, | ||
"variant": { | ||
"name": "disabled", | ||
"enabled": false | ||
} | ||
} | ||
] | ||
} | ||
``` | ||
**Health endpoint** | ||
The proxy will try to synchronize with the Unleash API at startup, until it has successfully done that the proxy will return `HTTP 503 - Not Read?` for all request. You can use the health endpoint to validate that the proxy is ready to recieve requests: | ||
```bash | ||
curl http://localhost:3000/proxy/health -I | ||
``` | ||
```bash | ||
HTTP/1.1 200 OK | ||
Access-Control-Allow-Origin: * | ||
Access-Control-Expose-Headers: ETag | ||
Content-Type: text/html; charset=utf-8 | ||
Content-Length: 2 | ||
ETag: W/"2-eoX0dku9ba8cNUXvu/DyeabcC+s" | ||
Date: Fri, 04 Jun 2021 10:38:27 GMT | ||
Connection: keep-alive | ||
Keep-Alive: timeout=5 | ||
``` | ||
### Run with Node.js: | ||
**STEP 1: Install dependency** | ||
``` | ||
npm install @unleash/proxy | ||
``` | ||
**STEP 2: use in your code** | ||
```js | ||
const port = 3000; | ||
const { createApp } = require('@unleash/proxy'); | ||
const app = createApp({ | ||
unleashUrl: 'https://app.unleash-hosted.com/demo/api/', | ||
unleashApiToken: '56907a2fa53c1d16101d509a10b78e36190b0f918d9f122d', | ||
clientKeys: ['proxy-secret', 'another-proxy-secret', 's1'], | ||
refreshInterval: 1000, | ||
// logLevel: 'info', | ||
// projectName: 'order-team', | ||
// environment: 'development', | ||
}); | ||
app.listen(port, () => | ||
// eslint-disable-next-line no-console | ||
console.log(`Unleash Proxy listening on http://localhost:${port}/proxy`), | ||
); | ||
``` | ||
## How to connect to the Proxy? | ||
Unleash offers several different [client-side SDKs](https://docs.getunleash.io/reference/sdks#client-side-sdks) for a number of use cases, and the community has created even more. These SDKs connect to the proxy and integrate with well with their designated language/framework. They also sync with the proxy at periodic intervals and post usage metrics back to Unleash via the proxy. | ||
For applications where there is no appropriate client-side SDK or where you simply want to avoid the dependency, you could also get away with using a simple HTTP-client if you just want to get the list of active features. | ||
The proxy is also ideal fit for serverless functions such as AWS Lambda. In that scenario the proxy can run on a small container near the serverless function, preferably in the same VPC, giving the lambda extremely fast access to feature flags, at a predictable cost. | ||
## Custom activation strategies | ||
--- | ||
ℹ️ **Limitations for hosted proxies** | ||
Custom activation strategies can **not** be used with the Unleash-hosted proxy available to Pro and Enterprise customers. | ||
--- | ||
The Unleash Proxy can load [custom activation strategies](https://docs.getunleash.io/reference/custom-activation-strategies) for [client-side SDKs](https://docs.getunleash.io/reference/sdks#client-side-sdks). For a step-by-step guide, refer to the [_how to use custom strategies_ guide](https://docs.getunleash.io/how-to/how-to-use-custom-strategies#step-3-b). | ||
To load custom strategies, use either of these two options: | ||
- the **`customStrategies`** option: use this if you're running the Unleash Proxy via Node directly. | ||
- the **`UNLEASH_CUSTOM_STRATEGIES_FILE`** environment variable: use this if you're running the proxy as a container. | ||
Both options take a list of file paths to JavaScript files that export custom strategy implementations. | ||
### Custom activation strategy files format | ||
Each strategy file must export a list of instantiated strategies. A file can export as many strategies as you'd like. | ||
Here's an example file that exports two custom strategies: | ||
```js | ||
const { Strategy } = require('unleash-client'); | ||
class MyCustomStrategy extends Strategy { | ||
// ... strategy implementation | ||
} | ||
class MyOtherCustomStrategy extends Strategy { | ||
// ... strategy implementation | ||
} | ||
// export strategies | ||
module.exports = [new MyCustomStrategy(), new MyOtherCustomStrategy()]; | ||
``` | ||
Refer the [custom activation strategy documentation](https://docs.getunleash.io/reference/custom-activation-strategies#implementation) for more details on how to implement a custom activation strategy. |
@@ -9,2 +9,3 @@ import compression from 'compression'; | ||
import { OpenApiService } from './openapi/openapi-service'; | ||
import requireContentType from './content-type-checker'; | ||
@@ -48,2 +49,4 @@ export function createApp( | ||
app.use(requireContentType()); | ||
app.use( | ||
@@ -50,0 +53,0 @@ `${config.proxyBasePath}/proxy`, |
@@ -71,2 +71,15 @@ import { OpenAPIV3 } from 'openapi-types'; | ||
export const unsupportedMediaTypeResponse: OpenAPIV3.ResponseObject = { | ||
description: | ||
'The operation does not support request payloads of the provided type.', | ||
content: { | ||
'text/plain': { | ||
schema: { | ||
type: 'string', | ||
example: 'Unsupported media type', | ||
}, | ||
}, | ||
}, | ||
}; | ||
export const internalServerErrorResponse: OpenAPIV3.ResponseObject = { | ||
@@ -120,2 +133,3 @@ description: | ||
401: unauthorizedResponse, | ||
415: unsupportedMediaTypeResponse, | ||
500: internalServerErrorResponse, | ||
@@ -122,0 +136,0 @@ 501: notImplementedError, |
@@ -400,42 +400,80 @@ import request, { Response } from 'supertest'; | ||
test('Should return not ready', () => { | ||
const client = new MockClient(); | ||
describe.each([ | ||
'', | ||
'/all', | ||
'/health', | ||
'/client/features', | ||
'/internal-backstage/prometheus', | ||
])('GET should return not ready', (url) => { | ||
test(`for ${url}`, () => { | ||
const client = new MockClient(); | ||
const proxySecrets = ['sdf']; | ||
const app = createApp( | ||
{ unleashUrl, unleashApiToken, proxySecrets }, | ||
client, | ||
); | ||
const proxySecrets = ['sdf']; | ||
const app = createApp( | ||
{ unleashUrl, unleashApiToken, proxySecrets }, | ||
client, | ||
); | ||
return request(app).get('/proxy/health').expect(503); | ||
return request(app).get(`/proxy${url}`).expect(503); | ||
}); | ||
}); | ||
test('Should return ready', () => { | ||
const client = new MockClient(); | ||
describe.each(['', '/all', '/client/features'])( | ||
'Requires valid token', | ||
(url) => { | ||
test(`Should return 401 if invalid api token for ${url}`, () => { | ||
const client = new MockClient(); | ||
const proxySecrets = ['sdf']; | ||
const app = createApp( | ||
{ unleashUrl, unleashApiToken, proxySecrets }, | ||
client, | ||
); | ||
client.emit('ready'); | ||
const proxySecrets = ['secret']; | ||
const app = createApp( | ||
{ proxySecrets, unleashUrl, unleashApiToken }, | ||
client, | ||
); | ||
client.emit('ready'); | ||
return request(app) | ||
.get('/proxy/health') | ||
.expect(200) | ||
.then((response) => { | ||
expect(response.text).toEqual('ok'); | ||
return request(app) | ||
.get(`/proxy${url}`) | ||
.set('Authorization', 'I do not know your secret') | ||
.expect(401); | ||
}); | ||
}); | ||
test('Should return 503 for /health', () => { | ||
const client = new MockClient(); | ||
test(`Should return 401 if no api token for ${url}`, () => { | ||
const client = new MockClient(); | ||
const proxySecrets = ['sdf']; | ||
const app = createApp( | ||
{ unleashUrl, unleashApiToken, proxySecrets }, | ||
client, | ||
); | ||
const proxySecrets = ['secret']; | ||
const app = createApp( | ||
{ proxySecrets, unleashUrl, unleashApiToken }, | ||
client, | ||
); | ||
client.emit('ready'); | ||
return request(app).get('/proxy/health').expect(503); | ||
return request(app).get(`/proxy${url}`).expect(401); | ||
}); | ||
}, | ||
); | ||
describe.each([ | ||
{ url: '/health', responseBody: 'ok' }, | ||
{ | ||
url: '/internal-backstage/prometheus', | ||
responseBody: 'unleash_proxy_up 1', | ||
}, | ||
])('Do not require valid token', ({ url, responseBody }) => { | ||
test(`Should not require a token for ${url}`, () => { | ||
const client = new MockClient(); | ||
const proxySecrets = ['sdf']; | ||
const app = createApp( | ||
{ unleashUrl, unleashApiToken, proxySecrets }, | ||
client, | ||
); | ||
client.emit('ready'); | ||
return request(app) | ||
.get(`/proxy${url}`) | ||
.expect(200) | ||
.then((response) => { | ||
expect(response.text).toMatch(responseBody); | ||
}); | ||
}); | ||
}); | ||
@@ -688,1 +726,63 @@ | ||
}); | ||
describe('Request content-types', () => { | ||
test.each(['/proxy', '/proxy/all'])( | ||
'Should assign default content-type if the request has a body but no content-type (%s)', | ||
async (endpoint) => { | ||
const client = new MockClient(); | ||
const payload = { | ||
context: { appName: 'my-app' }, | ||
}; | ||
const proxySecrets = ['sdf']; | ||
const app = createApp( | ||
{ | ||
unleashUrl, | ||
unleashApiToken, | ||
proxySecrets, | ||
enableAllEndpoint: true, | ||
}, | ||
client, | ||
); | ||
client.emit('ready'); | ||
await request(app) | ||
.post(endpoint) | ||
.set('Authorization', 'sdf') | ||
.set('Content-Type', '') | ||
.send(payload) | ||
.expect(200) | ||
.then(() => { | ||
expect(client.queriedContexts[0].appName).toEqual( | ||
payload.context.appName, | ||
); | ||
}); | ||
}, | ||
); | ||
test.each(['/proxy', '/proxy/all'])( | ||
'Should reject non-"content-type: application/json" for POST requests to %s', | ||
(endpoint) => { | ||
const client = new MockClient(); | ||
const proxySecrets = ['sdf']; | ||
const app = createApp( | ||
{ | ||
unleashUrl, | ||
unleashApiToken, | ||
proxySecrets, | ||
enableAllEndpoint: true, | ||
}, | ||
client, | ||
); | ||
client.emit('ready'); | ||
return request(app) | ||
.post(endpoint) | ||
.set('Authorization', 'sdf') | ||
.set('Content-Type', 'application/html') | ||
.send('<em>reject me!</em>') | ||
.expect(415); | ||
}, | ||
); | ||
}); |
@@ -1,6 +0,5 @@ | ||
import { Request, Response, Router } from 'express'; | ||
import { createContext } from './create-context'; | ||
import { NextFunction, Request, Response, Router } from 'express'; | ||
import { IProxyConfig } from './config'; | ||
import { IClient } from './client'; | ||
import { ContextEnricher, enrichContext } from './enrich-context'; | ||
import { ContextEnricher } from './enrich-context'; | ||
import { Logger } from './logger'; | ||
@@ -24,2 +23,3 @@ import { OpenApiService } from './openapi/openapi-service'; | ||
import { RegisterClientSchema } from './openapi/spec/register-client-schema'; | ||
import { createContexMiddleware } from './context-middleware'; | ||
@@ -62,2 +62,4 @@ export default class UnleashProxy { | ||
const contextMiddleware = createContexMiddleware(this.contextEnrichers); | ||
if (client.isReady()) { | ||
@@ -105,2 +107,5 @@ this.setReady(); | ||
}), | ||
this.readyMiddleware.bind(this), | ||
this.clientTokenMiddleware.bind(this), | ||
contextMiddleware, | ||
this.getEnabledToggles.bind(this), | ||
@@ -133,8 +138,13 @@ ); | ||
}, | ||
description: | ||
'This endpoint returns the list of feature toggles that the proxy evaluates to enabled and disabled for the given context. As such, this endpoint always returns all feature toggles the proxy retrieves from unleash. Useful if you are migrating to unleash and need to know if the feature flag exists on the unleash server. However, using this endpoint will increase the payload size transmitted to your applications. Context values are provided as query parameters.', | ||
summary: | ||
'Retrieve enabled feature toggles for the provided context.', | ||
description: `This endpoint returns all feature toggles known to the proxy, along with whether they are enabled or disabled for the provided context. This endpoint always returns **all** feature toggles the proxy retrieves from Unleash, in contrast to the \`/proxy\` endpoints that only return enabled toggles. | ||
Useful if you are migrating to unleash and need to know if the feature flag exists on the Unleash server. | ||
However, using this endpoint will increase the payload size transmitted to your applications. Context values are provided as query parameters.`, | ||
summary: 'Retrieve all feature toggles from the proxy.', | ||
tags: ['Proxy client'], | ||
}), | ||
this.readyMiddleware.bind(this), | ||
this.clientTokenMiddleware.bind(this), | ||
contextMiddleware, | ||
this.getAllToggles.bind(this), | ||
@@ -148,11 +158,17 @@ ); | ||
responses: { | ||
...standardResponses(401, 500, 501, 503), | ||
...standardResponses(401, 415, 500, 501, 503), | ||
200: featuresResponse, | ||
}, | ||
description: | ||
'This endpoint returns the list of feature toggles that the proxy evaluates to enabled and disabled for the given context. As such, this endpoint always returns all feature toggles the proxy retrieves from unleash. Useful if you are migrating to unleash and need to know if the feature flag exists on the unleash server. However, using this endpoint will increase the payload size transmitted to your applications. Context values are provided as query parameters.', | ||
description: `This endpoint accepts a JSON object with a \`context\` property and an optional \`toggles\` property. | ||
If you provide the \`toggles\` property, the proxy will use the provided context value to evaluate each of the toggles you sent in. The proxy returns a list with all the toggles you provided in their fully evaluated states. | ||
If you don't provide the \`toggles\` property, then this operation functions exactly the same as the GET operation on this endpoint, except that it uses the \`context\` property instead of query parameters: The proxy will evaluate all its toggles against the context you provide and return a list of all known feature toggles and their evaluated state.`, | ||
summary: | ||
'Retrieve enabled feature toggles for the provided context.', | ||
'Evaluate some or all toggles against the provided context.', | ||
tags: ['Proxy client'], | ||
}), | ||
this.readyMiddleware.bind(this), | ||
this.clientTokenMiddleware.bind(this), | ||
contextMiddleware, | ||
this.getAllTogglesPOST.bind(this), | ||
@@ -166,11 +182,17 @@ ); | ||
responses: { | ||
...standardResponses(400, 401, 500, 503), | ||
...standardResponses(400, 401, 415, 500, 503), | ||
200: featuresResponse, | ||
}, | ||
description: | ||
'This endpoint accepts a JSON object with `context` and `toggles` properties. The Proxy will use the provided context values and evaluate the toggles provided in the `toggle` property. It returns the toggles that evaluate to false. As such, the list it returns is always a subset of the toggles you provide it.', | ||
description: `This endpoint accepts a JSON object with a \`context\` property and an optional \`toggles\` property. | ||
If you provide the \`toggles\` property, the proxy will use the provided context value to evaluate each of the toggles you sent in. The proxy returns a list with all the toggles you provided in their fully evaluated states. | ||
If you don't provide the \`toggles\` property, then this operation functions exactly the same as the GET operation on this endpoint, except that it uses the \`context\` property instead of query parameters: The proxy will evaluate all its toggles against the context you provide and return a list of enabled toggles.`, | ||
summary: | ||
'Which of the provided toggles are enabled given the provided context?', | ||
'Evaluate specific toggles against the provided context.', | ||
tags: ['Proxy client'], | ||
}), | ||
this.readyMiddleware.bind(this), | ||
this.clientTokenMiddleware.bind(this), | ||
contextMiddleware, | ||
this.lookupToggles.bind(this), | ||
@@ -192,2 +214,4 @@ ); | ||
}), | ||
this.readyMiddleware.bind(this), | ||
this.clientTokenMiddleware.bind(this), | ||
this.unleashApi.bind(this), | ||
@@ -235,2 +259,3 @@ ); | ||
}), | ||
this.readyMiddleware.bind(this), | ||
this.health.bind(this), | ||
@@ -252,2 +277,3 @@ ); | ||
}), | ||
this.readyMiddleware.bind(this), | ||
this.prometheus.bind(this), | ||
@@ -273,5 +299,27 @@ ); | ||
getAllToggles(req: Request, res: Response<FeaturesSchema | string>): void { | ||
private readyMiddleware(req: Request, res: Response, next: NextFunction) { | ||
if (!this.ready) { | ||
res.status(503).send(NOT_READY_MSG); | ||
} else { | ||
next(); | ||
} | ||
} | ||
private clientTokenMiddleware( | ||
req: Request, | ||
res: Response, | ||
next: NextFunction, | ||
) { | ||
const apiToken = req.header(this.clientKeysHeaderName); | ||
if (!apiToken || !this.clientKeys.includes(apiToken)) { | ||
res.sendStatus(401); | ||
} else { | ||
next(); | ||
} | ||
} | ||
async getAllToggles( | ||
req: Request, | ||
res: Response<FeaturesSchema | string>, | ||
): Promise<void> { | ||
if (!this.enableAllEndpoint) { | ||
@@ -281,22 +329,15 @@ res.status(501).send( | ||
); | ||
} else if (!this.ready) { | ||
res.status(503).send(NOT_READY_MSG); | ||
} else if (!apiToken || !this.clientKeys.includes(apiToken)) { | ||
res.sendStatus(401); | ||
} else { | ||
const { query } = req; | ||
query.remoteAddress = query.remoteAddress || req.ip; | ||
const context = createContext(query); | ||
const toggles = this.client.getAllToggles(context); | ||
res.set('Cache-control', 'public, max-age=2'); | ||
res.send({ toggles }); | ||
return; | ||
} | ||
const { context } = res.locals; | ||
const toggles = this.client.getAllToggles(context); | ||
res.set('Cache-control', 'public, max-age=2'); | ||
res.send({ toggles }); | ||
} | ||
getAllTogglesPOST( | ||
async getAllTogglesPOST( | ||
req: Request, | ||
res: Response<FeaturesSchema | string>, | ||
): void { | ||
const apiToken = req.header(this.clientKeysHeaderName); | ||
): Promise<void> { | ||
if (!this.enableAllEndpoint) { | ||
@@ -306,74 +347,42 @@ res.status(501).send( | ||
); | ||
} else if (!this.ready) { | ||
res.status(503).send(NOT_READY_MSG); | ||
} else if (!apiToken || !this.clientKeys.includes(apiToken)) { | ||
res.sendStatus(401); | ||
return; | ||
} | ||
res.set('Cache-control', 'public, max-age=2'); | ||
const { toggles: toggleNames = [] } = req.body; | ||
const { context } = res.locals; | ||
if (toggleNames.length > 0) { | ||
const toggles = this.client.getDefinedToggles(toggleNames, context); | ||
res.send({ toggles }); | ||
} else { | ||
res.set('Cache-control', 'public, max-age=2'); | ||
const { context = {}, toggles: toggleNames = [] } = req.body; | ||
const actualContext = createContext(context); | ||
if (toggleNames.length > 0) { | ||
const toggles = this.client.getDefinedToggles( | ||
toggleNames, | ||
actualContext, | ||
); | ||
res.send({ toggles }); | ||
} else { | ||
context.remoteAddress = context.remoteAddress || req.ip; | ||
const toggles = this.client.getAllToggles(actualContext); | ||
res.send({ toggles }); | ||
} | ||
const toggles = this.client.getAllToggles(context); | ||
res.send({ toggles }); | ||
} | ||
} | ||
getEnabledToggles( | ||
async getEnabledToggles( | ||
req: Request, | ||
res: Response<FeaturesSchema | string>, | ||
): void { | ||
const apiToken = req.header(this.clientKeysHeaderName); | ||
if (!this.ready) { | ||
res.status(503).send(NOT_READY_MSG); | ||
} else if (!apiToken || !this.clientKeys.includes(apiToken)) { | ||
res.sendStatus(401); | ||
} else { | ||
const { query } = req; | ||
query.remoteAddress = query.remoteAddress || req.ip; | ||
enrichContext(this.contextEnrichers, createContext(query)).then( | ||
(context) => { | ||
const toggles = this.client.getEnabledToggles(context); | ||
res.set('Cache-control', 'public, max-age=2'); | ||
res.send({ toggles }); | ||
}, | ||
); | ||
} | ||
): Promise<void> { | ||
const { context } = res.locals; | ||
const toggles = this.client.getEnabledToggles(context); | ||
res.set('Cache-control', 'public, max-age=2'); | ||
res.send({ toggles }); | ||
} | ||
lookupToggles( | ||
async lookupToggles( | ||
req: Request<any, any, LookupTogglesSchema>, | ||
res: Response<FeaturesSchema | string>, | ||
): void { | ||
const clientToken = req.header(this.clientKeysHeaderName); | ||
): Promise<void> { | ||
res.set('Cache-control', 'public, max-age=2'); | ||
const { toggles: toggleNames = [] } = req.body; | ||
const { context } = res.locals; | ||
if (!this.ready) { | ||
res.status(503).send(NOT_READY_MSG); | ||
} else if (!clientToken || !this.clientKeys.includes(clientToken)) { | ||
res.sendStatus(401); | ||
if (toggleNames.length > 0) { | ||
const toggles = this.client.getDefinedToggles(toggleNames, context); | ||
res.send({ toggles }); | ||
} else { | ||
res.set('Cache-control', 'public, max-age=2'); | ||
const { context = {}, toggles: toggleNames = [] } = req.body; | ||
const actualContext = createContext(context); | ||
if (toggleNames.length > 0) { | ||
const toggles = this.client.getDefinedToggles( | ||
toggleNames, | ||
actualContext, | ||
); | ||
res.send({ toggles }); | ||
} else { | ||
context.remoteAddress = context.remoteAddress || req.ip; | ||
const toggles = this.client.getEnabledToggles(actualContext); | ||
res.send({ toggles }); | ||
} | ||
const toggles = this.client.getEnabledToggles(context); | ||
res.send({ toggles }); | ||
} | ||
@@ -383,20 +392,12 @@ } | ||
health(_: Request, res: Response<string>): void { | ||
if (!this.ready) { | ||
res.status(503).send(NOT_READY_MSG); | ||
} else { | ||
res.send('ok'); | ||
} | ||
res.send('ok'); | ||
} | ||
prometheus(_: Request, res: Response<string>): void { | ||
if (!this.ready) { | ||
res.status(503).send(NOT_READY_MSG); | ||
} else { | ||
const prometheusResponse = | ||
'# HELP unleash_proxy_up Indication that the service is up. \n' + | ||
'# TYPE unleash_proxy_up counter\n' + | ||
'unleash_proxy_up 1\n'; | ||
res.set('Content-type', 'text/plain'); | ||
res.send(prometheusResponse); | ||
} | ||
const prometheusResponse = | ||
'# HELP unleash_proxy_up Indication that the service is up. \n' + | ||
'# TYPE unleash_proxy_up counter\n' + | ||
'unleash_proxy_up 1\n'; | ||
res.set('Content-type', 'text/plain'); | ||
res.send(prometheusResponse); | ||
} | ||
@@ -435,13 +436,6 @@ | ||
unleashApi(req: Request, res: Response<string | ApiRequestSchema>): void { | ||
const apiToken = req.header(this.clientKeysHeaderName); | ||
if (!this.ready) { | ||
res.status(503).send(NOT_READY_MSG); | ||
} 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); | ||
} | ||
const features = this.client.getFeatureToggleDefinitions(); | ||
res.set('Cache-control', 'public, max-age=2'); | ||
res.send({ version: 2, features }); | ||
} | ||
} |
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
652503
189
7924
585
8
25
+ Addedtype-is@^1.6.18