diff --git a/example.js b/example.js index 718a955..1381327 100644 --- a/example.js +++ b/example.js @@ -8,6 +8,7 @@ const app = createApp({ clientKeys: ['proxy-secret', 'another-proxy-secret', 's1'], refreshInterval: 1000, logLevel: 'trace', + enableOAS: true, expServerSideSdkConfig: { tokens: ['server'], }, diff --git a/src/openapi/index.ts b/src/openapi/index.ts index 184e5bb..e61b20e 100644 --- a/src/openapi/index.ts +++ b/src/openapi/index.ts @@ -4,6 +4,7 @@ import { featureSchema } from './spec/feature-schema'; import { featuresSchema } from './spec/features-schema'; import { lookupTogglesSchema } from './spec/lookup-toggles-schema'; import { registerMetricsSchema } from './spec/register-metrics-schema'; +import { registerClientSchema } from './spec/register-client-schema'; import { unleashContextSchema } from './spec/unleash-context-schema'; import { variantSchema } from './spec/variant-schema'; @@ -53,6 +54,7 @@ export const createOpenApiSchema = ( featuresSchema, lookupTogglesSchema, registerMetricsSchema, + registerClientSchema, unleashContextSchema, variantSchema, }, diff --git a/src/openapi/spec/register-client-request.ts b/src/openapi/spec/register-client-request.ts new file mode 100644 index 0000000..7999ac6 --- /dev/null +++ b/src/openapi/spec/register-client-request.ts @@ -0,0 +1,11 @@ +import { OpenAPIV3 } from 'openapi-types'; + +export const registerClientRequest: OpenAPIV3.RequestBodyObject = { + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/registerClientSchema', + }, + }, + }, +}; diff --git a/src/openapi/spec/register-client-schema.ts b/src/openapi/spec/register-client-schema.ts new file mode 100644 index 0000000..a9cc749 --- /dev/null +++ b/src/openapi/spec/register-client-schema.ts @@ -0,0 +1,50 @@ +import { createSchemaObject, CreateSchemaType } from '../openapi-types'; + +export const schema = { + type: 'object', + required: ['appName', 'interval', 'started', 'strategies'], + properties: { + appName: { + type: 'string', + description: 'Name of the application using Unleash', + }, + instanceId: { + type: 'string', + description: + 'Instance id for this application (typically hostname, podId or similar)', + }, + sdkVersion: { + type: 'string', + description: + 'Optional field that describes the sdk version (name:version)', + }, + environment: { + type: 'string', + deprecated: true, + }, + interval: { + type: 'number', + description: + 'At which interval, in milliseconds, will this client be expected to send metrics', + }, + started: { + oneOf: [ + { type: 'string', format: 'date-time' }, + { type: 'number' }, + ], + description: + 'When this client started. Should be reported as ISO8601 time.', + }, + strategies: { + type: 'array', + items: { + type: 'string', + }, + description: 'List of strategies implemented by this application', + }, + }, +} as const; + +export type RegisterClientSchema = CreateSchemaType; + +export const registerClientSchema = createSchemaObject(schema); diff --git a/src/test/__snapshots__/openapi.test.ts.snap b/src/test/__snapshots__/openapi.test.ts.snap index ed48415..3ff86aa 100644 --- a/src/test/__snapshots__/openapi.test.ts.snap +++ b/src/test/__snapshots__/openapi.test.ts.snap @@ -375,6 +375,56 @@ Object { }, "type": "object", }, + "registerClientSchema": Object { + "properties": Object { + "appName": Object { + "description": "Name of the application using Unleash", + "type": "string", + }, + "environment": Object { + "deprecated": true, + "type": "string", + }, + "instanceId": Object { + "description": "Instance id for this application (typically hostname, podId or similar)", + "type": "string", + }, + "interval": Object { + "description": "At which interval, in milliseconds, will this client be expected to send metrics", + "type": "number", + }, + "sdkVersion": Object { + "description": "Optional field that describes the sdk version (name:version)", + "type": "string", + }, + "started": Object { + "description": "When this client started. Should be reported as ISO8601 time.", + "oneOf": Array [ + Object { + "format": "date-time", + "type": "string", + }, + Object { + "type": "number", + }, + ], + }, + "strategies": Object { + "description": "List of strategies implemented by this application", + "items": Object { + "type": "string", + }, + "type": "array", + }, + }, + "required": Array [ + "appName", + "interval", + "started", + "strategies", + ], + "type": "object", + }, "registerMetricsSchema": Object { "properties": Object { "appName": Object { @@ -852,6 +902,79 @@ Object { ], }, }, + "/proxy/client/register": Object { + "post": Object { + "description": "This endpoint lets you register application with Unleash. Accepts either one of the proxy's configured \`serverSideTokens\` or one of its \`clientKeys\` for authorization.", + "requestBody": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/registerClientSchema", + }, + }, + }, + }, + "responses": Object { + "200": Object { + "content": Object { + "text/plain": Object { + "schema": Object { + "example": "ok", + "type": "string", + }, + }, + }, + "description": "The request was successful.", + }, + "400": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "example": Object { + "error": "Request validation failed", + "validation": Array [ + Object { + "dataPath": ".body", + "keyword": "required", + "message": "should have required property 'appName'", + "params": Object { + "missingProperty": "appName", + }, + "schemaPath": "#/components/schemas/registerMetricsSchema/required", + }, + ], + }, + "properties": Object { + "error": Object { + "type": "string", + }, + "validation": Object { + "items": Object { + "type": "object", + }, + "type": "array", + }, + }, + "required": Array [ + "error", + ], + "type": "object", + }, + }, + }, + "description": "The provided request data is invalid.", + }, + "401": Object { + "description": "Authorization information is missing or invalid.", + }, + }, + "summary": "Register clients with Unleash.", + "tags": Array [ + "Operational", + "Server-side client", + ], + }, + }, "/proxy/health": Object { "get": Object { "description": "Returns a 200 OK if the proxy is ready to receive requests. Otherwise returns a 503 NOT READY.", diff --git a/src/test/unleash-proxy.test.ts b/src/test/unleash-proxy.test.ts index cb24694..40dda9f 100644 --- a/src/test/unleash-proxy.test.ts +++ b/src/test/unleash-proxy.test.ts @@ -523,3 +523,41 @@ test('Should return 400 bad request for malformed JSON', async () => { .expect(400) .expect('Content-Type', /json/); }); + +test('Should register server SDK', async () => { + const toggles = [ + { + name: 'test', + enabled: true, + impressionData: true, + }, + ]; + const client = new MockClient(toggles); + + const proxySecrets = ['sdf']; + const app = createApp( + { + unleashUrl, + unleashApiToken, + proxySecrets, + expServerSideSdkConfig: { tokens: ['s1'] }, + }, + client, + ); + client.emit('ready'); + + const res = await request(app) + .post('/proxy/client/register') + .send({ + appName: 'test', + instanceId: 'i1', + sdkVersion: 'custom1', + environment: 'prod', + interval: 10000, + started: new Date(), + strategies: ['default'], + }) + .set('Authorization', 'sdf'); + + expect(res.statusCode).toBe(200); +}); diff --git a/src/unleash-proxy.ts b/src/unleash-proxy.ts index 87e7873..78a233e 100644 --- a/src/unleash-proxy.ts +++ b/src/unleash-proxy.ts @@ -13,12 +13,14 @@ import { ApiRequestSchema } from './openapi/spec/api-request-schema'; import { FeaturesSchema } from './openapi/spec/features-schema'; import { lookupTogglesRequest } from './openapi/spec/lookup-toggles-request'; import { registerMetricsRequest } from './openapi/spec/register-metrics-request'; +import { registerClientRequest } from './openapi/spec/register-client-request'; import { createDeepObjectRequestParameters, createRequestParameters, } from './openapi/openapi-helpers'; import { RegisterMetricsSchema } from './openapi/spec/register-metrics-schema'; import { LookupTogglesSchema } from './openapi/spec/lookup-toggles-schema'; +import { RegisterClientSchema } from './openapi/spec/register-client-schema'; export default class UnleashProxy { private logger: Logger; @@ -139,6 +141,19 @@ export default class UnleashProxy { this.registerMetrics.bind(this), ); + router.post( + '/client/register', + openApiService.validPath({ + requestBody: registerClientRequest, + responses: standardResponses(200, 400, 401), + description: + "This endpoint lets you register application with Unleash. Accepts either one of the proxy's configured `serverSideTokens` or one of its `clientKeys` for authorization.", + summary: 'Register clients with Unleash.', + tags: ['Operational', 'Server-side client'], + }), + this.registerClient.bind(this), + ); + router.get( '/health', openApiService.validPath({ @@ -266,6 +281,21 @@ export default class UnleashProxy { } } + registerClient( + req: Request<{}, undefined, RegisterClientSchema>, + res: Response, + ): void { + const token = req.header(this.clientKeysHeaderName); + const validTokens = [...this.clientKeys, ...this.serverSideTokens]; + + if (token && validTokens.includes(token)) { + this.logger.debug('Client registration is not supported yet.'); + res.sendStatus(200); + } else { + res.sendStatus(401); + } + } + unleashApi(req: Request, res: Response): void { const apiToken = req.header(this.clientKeysHeaderName); if (!this.ready) {