From 86715faa34d116826abb5ed40211722641a2993d Mon Sep 17 00:00:00 2001 From: Maksim Likharev Date: Mon, 15 Apr 2019 13:17:08 -0700 Subject: [PATCH] sync 04/15/2019 (#7) * WIP - wicked 1.0 SDK - this is still subject to heavy change * Some minor beautifications and fixes * Set installing wicked-sdk to other repos straight(er) * Adaptions for handling the scopes of the portal-api * Updated dependencies * Some additional exports * Add --copy option for just copying node-sdk * Refactoring: Moved to Typescript for node-sdk - wip * Cleanup - SDK now fully typescript typed; full coverage of all API calls * New API property: passthroughUsers and scopeUrl * Move OidcProfile to wicked SDK * Added PassthroughScope request and response types * Disallow -> disable * Refactoring into three parts: Interfaces, implementation and exports (index). * Reworked documentation entirely; uses typedoc. * Small script to update the docs * Take out the docs from the master branch again * Push docs to gh-pages * Minor changes, added two convenience functions * Added and extended some Kong types. * Missing uris property of a Kong API * Added some Kong types and utility functions * Added some specific Kong types; refactored a little * Add KongApiConfig to kong-interfaces. * Forgot to export... * Some additional types and fixes * Don't pack the docs into the * Take out KongCollection.total, it's not always present * hide credential flag propagation * Revert * Hide credentials flag * The "trusted" property can be passed in at subscription create time * Missing function "deleteUser". * Raise timeout for wicked API calls (needed for slow Postgres ops) * Add proxy function for the internal portal URL * Add more info on importing users, with pre-hashed passwords * Surfe isApiReachable() function; minor change in kong Type * New function getInternalKongProxyUrl() * Don't do global replace * Add forgotten patchUser call * Use sub= notation also for SDK direct calls * Ahem. * Add an undocumented feature "bundle" for bundling APIs (beta/experimental) * Added propertx "methods" to CORS plugin definition * Add retry logic on 5xx errors when calling the wicked API * New global passwordStrategy; added to SDK * Add api settings for user groups for portal-api and echo api * Introduce optional kongProxyUrl for special types of setups * Minor fixes; refresh_token_ttl in interface * Regression bug - _getInternalUrl does unexpected things * Support for async/await/Promises; types for... ... auth method type external; see also Haufe-Lexware/wicked.haufe.io#128 * Added missing types; restructured exports and imports * Changes for Haufe-Lexware/wicked.haufe.io#138, bugfix in patchSubscription * Strong typing, better support for TypeScript (function overlads) * Missed an export... * Repo renaming * Add property for environment of the API * TypeScript type for Haufe-Lexware/wicked.haufe.io#159 - Added support for more granular client types * ... and export it as well * Updated some comments and documentation. No functional changes. * Add optional kongProxyUrl to network config type * Ignore git_* (may be present from box build) * Ignore build_date file * Bump to version 1.0.0-rc.1 * Minor audit fix * Allow any semver as user agent version * Oops * Release script for wicked-sdk * Add "protected" property for auth methods * Updated version * Enable multiple init calls to SDK * New property redirectUris (for multiple redirectUris) * Added forgotten endpoint deleteSubscription(As) * Add a Jenkinsfile for publishing from Jenkins * Part of fix of Haufe-Lexware/wicked.haufe.io#191 * Update request * Explicitly add typescript as dev dependency * Make sure jq is available --- .dockerignore | 1 + .gitignore | 6 + .npmignore | 10 + Dockerfile-publish | 7 +- Jenkinsfile | 21 + README.md | 489 +++--------- index.js | 960 ----------------------- install-local-sdk.sh | 68 +- make-docs.sh | 30 + package-lock.json | 400 ++++++++++ package.json | 21 +- publish.sh | 23 + src/implementation.ts | 1056 +++++++++++++++++++++++++ src/index.ts | 1701 ++++++++++++++++++++++++++++++++++++++++ src/interfaces.ts | 837 ++++++++++++++++++++ src/kong-interfaces.ts | 379 +++++++++ src/kong.ts | 113 +++ src/wicked-error.ts | 18 + tsconfig.json | 17 + 19 files changed, 4781 insertions(+), 1376 deletions(-) create mode 100644 Jenkinsfile delete mode 100644 index.js create mode 100755 make-docs.sh create mode 100644 package-lock.json create mode 100755 publish.sh create mode 100644 src/implementation.ts create mode 100644 src/index.ts create mode 100644 src/interfaces.ts create mode 100644 src/kong-interfaces.ts create mode 100644 src/kong.ts create mode 100644 src/wicked-error.ts create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore index 20cc946..101d95b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,3 +2,4 @@ node_modules .DS_Store .gitignore .git +*.log diff --git a/.gitignore b/.gitignore index 1faf560..e2a9cf0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ node_modules .DS_Store *.tgz +*.log +dist +docs +docs-git +git_* +build_date diff --git a/.npmignore b/.npmignore index b645dbf..2bcf937 100644 --- a/.npmignore +++ b/.npmignore @@ -3,3 +3,13 @@ Dockerfile-publish *.sh .DS_Store +*.log +*.tgz +src +tsconfig.json +docs +docs-git +Dockerfile* +publish.sh +Jenkinsfile +.eslint* diff --git a/Dockerfile-publish b/Dockerfile-publish index cd23caf..848e36a 100644 --- a/Dockerfile-publish +++ b/Dockerfile-publish @@ -1,9 +1,10 @@ -FROM node:6-alpine +FROM node:10-alpine +RUN apk update && apk add jq RUN mkdir -p /usr/src/app WORKDIR /usr/src/app -RUN npm install -g npm-cli-login +RUN npm install -g npm-cli-login typescript@$(jq .devDependencies.typescript | tr -d '"') COPY package.json /usr/src/app COPY . /usr/src/app @@ -12,4 +13,4 @@ ARG NPM_PASS ARG NPM_EMAIL RUN echo User: $NPM_USER, Email: $NPM_EMAIL -RUN npm-cli-login -u $NPM_USER -p $NPM_PASS -e $NPM_EMAIL && npm publish && npm logout +RUN npm-cli-login -u $NPM_USER -p $NPM_PASS -e $NPM_EMAIL && npm install && tsc && npm publish && npm logout diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..c815315 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,21 @@ +properties([ + pipelineTriggers([ + // [$class: "SCMTrigger", scmpoll_spec: "H/10 * * * *"] + ]) +]) + +node('docker') { + + stage('Checkout') { + checkout scm + } + + stage('Build and Publish') { + withCredentials([ + usernamePassword(credentialsId: 'npmjs_wicked', usernameVariable: 'NPM_USER', passwordVariable: 'NPM_PASS'), + string(credentialsId: 'npmjs_wicked_email', variable: 'NPM_EMAIL') + ]) { + sh './publish.sh' + } + } +} diff --git a/README.md b/README.md index ed10c92..5c9da1f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,14 @@ # wicked.haufe.io SDK -This node.js module is the SDK for building plugins and additions to the wicked.haufe.io API Management system. +## DISCLAIMER + +**IMPORTANT**: This is the documentation of the wicked.haufe.io node SDK as of version 1.0.0! It will **NOT** work with previous versions of wicked.haufe.io. + +Full documentation at [apim-haufe-io.github.com/wicked.node-sdk](https://apim-haufe-io.github.io/wicked.node-sdk/). + +## Introduction + +This node.js module is an SDK for building plugins and additions to the wicked.haufe.io API Management system. You can find more information on wicked.haufe.io here: @@ -12,8 +20,11 @@ This package is the base for the following wicked modules: * Kong Adapter * Mailer * Chatbot +* Portal -It may be used for e.g. implementing Authorization Servers in node.js (upcoming: Stub implementation of a Google/Github Authorization Server). +It may be used for simpler interaction with the wicked User Management API, e.g. for creating users and registrations +from the outside, and not using the wicked Portal UI. This makes using wicked as an Identity Provider for your own +applications a lot easier. # Usage @@ -23,18 +34,40 @@ To install the SDK into your node.js application, run $ npm install wicked-sdk --save --save-exact ``` -The SDK will be kept downwards-compatible for as long as possible; it will be tried hard to make earlier versions of the SDK compatible with a later release of wicked.haufe.io, so using the `--save-exact` is a safe bet if you don't need newer features. **Note**: If you are using wicked >= 0.12.0, you will need to install this SDK in a version >= 0.12.0 as well. Everything else should behave as before though. +The common initialization of the wicked SDK is like this, creating a machine user for the component which uses the wicked SDK to enable admin +access to the wicked API directly from your node.js, including type declarations: ```javascript -var wicked = require('wicked-sdk'); - -wicked.initialize(function (err) { - if (err) +const async = require('async'); +const wicked = require('wicked-sdk'); + +const wickedOptions = { + userAgentName: 'your-component', + userAgentVersion: '1.0.0', + apiMaxTries: 10, // optional, defaults to 10 + apiRetryDelay: 500, // optional, defaults to 500 +}; + +// Init wicked SDK and register a machine user. +async.series([ + callback => wicked.initialize(wickedOptions, callback), + callback => wicked.initMachineUser('yourcomponent', callback), +], function (err) { + if (err) { + debug('Failed waiting for API.'); throw err; - // We're done, the wicked API is reachable. + } - const apiUrl = wicked.getApiUrl(); - // ... + // Remember some URLs + app.set('api_url', wicked.getInternalApiUrl()); + ... + + // Now you can use wicked as you wish. + wicked.getUserRegistrations('regpool', someUserId, (err, registrationCollection) => { + if (err) // ... + + // + }); }); ``` @@ -42,421 +75,91 @@ wicked.initialize(function (err) { ### Initialization Functionality -#### `wicked.initialize([options,] callback)` - -Waits for the wicked API to be available and returns a `null` error if successful, otherwise an error message. If you want to change the way the SDK waits for the Portal API, you may supply `options` (see definition of `awaitUrl` below for a description of the await options). - -**New as of version 0.11.0**: - -The following options are supported in addition to the `awaitOptions` below: - -* `userAgentName`: The name of your component, e.g. `company.auth-server`` -* `userAgentVersion`: The version of your component, e.g. `1.2.3` -* `doNotPollConfigHash`: Boolean value, set to `true` in order not to automatically shut down the component after a configuration hash has changed. This means you will have to reconfigure your component yourself; otherwise a restart would automatically do this (this is the default behavior which all wicked base components use). - -In case you use a `userAgentName` which starts with `wicked`, the version of your component **must** be the exact same version as the Portal API's version, otherwise any call to the wicked API will be rejected with a `428` status code (precondition failed/version mismatch). +The `initialize()` function waits for the wicked API to be available and returns a `null` error if successful, otherwise an error message. If you want to change the way the SDK waits for the Portal API, you may supply `options` as [WickedInitOptions](interfaces/_interfaces_.wickedinitoptions.html). The `initialize()` call will look for the Portal API URL in the following way: * If the environment variable `PORTAL_API_URL` is set, this will be used -* Otherwise, if the environment is Linux (where it assumes it runs in `docker`), it will default to `http://portal-api:3001` -* Otherwise, if the environment is Windows or Mac, it will assume a local development environment, and use `http://localhost:3001` - -**In case you need to work with a wicked installation prior to 0.11.0, please use the node SDK 0.10.13; a version 0.11.0 node SDK will not be compatible with a 0.10.x installation of wicked.!** - -After initialization, you have the following functions you may call: +* Otherwise, if the environment is a containerized Linux (where it assumes it runs in `docker`), it will default to `http://portal-api:3001` +* Otherwise, if the environment is Windows or Mac, it will assume a local development environment, and use `http://localhost:3001` -#### `wicked.isDevelopmentMode()` +Please consider this behaviour when deploying your own applications alongside of wicked, and pass in the correct `PORTAL_API_URL` so that the SDK can retrieve the correct settings automatically from the wicked API. -Returns `true` if the wicked instance is running in development mode. This is assumed to be the case if and only if the specified `network.schema` in `globals.json` is **not** set to `https`. +### Machine Users -You may use this to enable certain behaviour in case your module is running on your local machine. +It is convenient to use the [`initMachineUser`](modules/_index_.html#initmachineuser) function to create a machine user for your component to use to talk with the wicked API. **IMPORTANT**: Automatically, this machine user will have complete and full admin rights to the wicked API, so that you must handle this user with care, and e.g. not tunnel calls directly from an end user to the wicked API. This end user would then automatically have admin rights in wicked, which probably is not what you want. -#### `wicked.awaitUrl(url, [options,] callback)` - -Tries to reach the URL in `url` for a certain amount of times. If successful, it will return without an error, otherwise the callback will contain an error. - -Options (optional): - -```javascript -const awaitOptions = { - statusCode: 200, - maxTries: 100, - retryDelay: 1000 -} -``` +### SDK functionality -* `statusCode`: The status code to wait for, defaults to `200` (OK) -* `maxTries`: The maximum number of tries to call the URL, defaults to `100` -* `retryDelay`: The delay between unsuccesful retries, in milliseconds. Defaults to `1000` (1 second) - -**Example**: - -```javascript -wicked.awaitUrl(wicked.getInternalKongAdapterUrl() + 'ping', null, function (err) { - if (err) { - // Do some error handling - } - // Now we can speak to the Kong Adapter -}); -``` - -#### `wicked.awaitKongAdapter([awaitOptions,] callback)` - -Makes sure the Kong Adapter is up and running. In case you are implementing an Authorization Server, you will need the Kong Adapter to authorize users with Kong, so at startup you should make sure it is up and running. - -**Example**: - -```javascript -var async = require('async'); -var wicked = required('wicked-sdk'); - -async.series({ - initWicked: callback => wicked.initialize(callback), - waitForAdapter: callback => wicked.awaitKongAdapter(callback) -}, function (err, results) { - if (err) - throw err; // Something went wrong - - // We're all set, the portal API and the Kong Adapter both are alive. -}); -``` - -#### `wicked.initMachineUser(serviceId, callback)` - -Creates a new machine user for a service, or retrieves its ID if already present in wicked's user database. Use this functionality to create a machine user for your service implementation; this user will belong to the `admin` group and has full rights in the Portal API. - -The `serviceId` must only contain lowercase and uppercase charactera (case-sensitive), numbers, dashes and underscores (`^[a-zA-Z\-_0-9]+$`). - -Will return an `application/json` response containing (at least) the following information: - -```json -{ - "id": "ac283232789cdbcd2390f029", - "customId": "internal:(your service id)", - "firstName": "Machine-User", - "lastName": "(your service id)", - "validated": true -} -``` +The functions this SDK offers can best be viewed on the [wicked SDK index page](modules/_index_.html). -The `id` which is returned here has to be used in subsequent calls to the portal API to authorize the service to the portal API (in the `X-UserId` header for wicked <= 0.11.x, in 'X-Authenticated-UserId` for wicked >= 0.12.x). +Most functions have two different versions: One simple version which automatically uses the machine user identity, and one function suffixed `As`, which takes an additional user ID so that this function can also be called in the context of a different user. In most cases, these functions aren't needed, but it may be necessary to use them. Either to show on behalf of whom an action on the wicked API was done, or to reduce risk by not using the machine user. ### API Interaction +As mentioned above, the entire wicked API is encapsulated in this SDK, so that it's usually not necessary to call the API directly, using any of the generic purpose `apiGet`, `apiPost`, `apiDelete`, `apiPatch` and `apiPut` functions. It is recommended to rather use the dedicated functions, as they also contain type information of the return values. This is especially useful if you are already developing using TypeScript, but also from JavaScript certain editors can take advantage of the type information (e.g. Visual Studio Code). + **IMPORTANT**: As of version 0.11.0 of the wicked SDK, the SDK will continuously poll the `/confighash` end point of the portal API to detect configuration changes. Changes are detected by comparing the `confighash` retrieved at initialization (the SDK does this as a default) with the current value returned by `/confighash`. In case the values do not match, the SDK will **forcefully exit the entire node process in order to make the component restart and retrieve a new configuration**. In case you do not want this behavior, but rather would want to control yourself when to restart or reconfigure your component, specify `doNotPollConfigHash: true` in the initialization options (see above). -#### `wicked.apiGet(urlPath, [userId,] callback)` - -Issues a `GET` to the Portal API end point specified in `urlPath`, using the user ID passed in as `userId`. If `userId` is left out, the call is done without authentication. Check the API Swagger definition for details. The result of the API call is returned as the second parameter of the `callback` (node standard). - -The function will callback with an error if a hard error occurred, or the status code is not OK (larger than `299`). Details on the failure, in case it was an unexpected status code, can be found in `err.res` and `err.statusCode`, so that you may react e.g. to a `404`, which may be expected in certain cases. - -Use `initMachineUser` to initialize a suitable user ID to use with your service. If you have called `initMachineUser`, `apiGet` will automatically pick up the machine user ID. If you specify a different `userId`, that will be used. - -**Example**: - -```javascript -// It is assumed wicked.initialize() was successfully called -wicked.apiGet('plans', function (err, results) { - if (err) - return next(err); // or whatever suits your error handling - console.log(results); // writes the plans.json -}); -``` - -#### `wicked.apiPost(urlPath, postBody, [userId,] callback)` - -Issues a `POST` to the Portal API end point specified in `urlPath`, using the user ID passed in as `userId`. If `userId` is left out, the call is done without authentication. Check the API Swagger definition for details. The result of the API call is returned as the second parameter of the `callback` (node standard). - -The function will callback with an error if a hard error occurred, or if the status code is larger than `299`. If the function fails due to an unexpected status code, details can be found in `err.statusCode` and `err.res`. - -Use `initMachineUser` to initialize a suitable user ID to use with your service. If you have called `initMachineUser`, `apiPost` will automatically pick up the machine user ID. If you specify a different `userId`, that will be used. - -#### `wicked.apiPut(urlPath, putBody, [userId,] callback)` - -Issues a `PUT` to the Portal API end point specified in `urlPath`, using the user ID passed in as `userId`. If `userId` is left out, the call is done without authentication. Check the API Swagger definition for details. The result of the API call is returned as the second parameter of the `callback` (node standard). - -The function will callback with an error if a hard error occurred, or if the status code is larger than `299`. If the function fails due to an unexpected status code, details can be found in `err.statusCode` and `err.res`. - -Use `initMachineUser` to initialize a suitable user ID to use with your service. If you have called `initMachineUser`, `apiPost` will automatically pick up the machine user ID. If you specify a different `userId`, that will be used. - -#### `wicked.apiPatch(urlPath, patchBody, [userId,] callback)` - -Issues a `PATCH` to the Portal API end point specified in `urlPath`, using the user ID passed in as `userId`. If `userId` is left out, the call is done without authentication. Check the API Swagger definition for details. The result of the API call is returned as the second parameter of the `callback` (node standard). - -The function will callback with an error if a hard error occurred, or if the status code is larger than `299`. If the function fails due to an unexpected status code, details can be found in `err.statusCode` and `err.res`. - -Use `initMachineUser` to initialize a suitable user ID to use with your service. If you have called `initMachineUser`, `apiPost` will automatically pick up the machine user ID. If you specify a different `userId`, that will be used. -#### `wicked.apiDelete(urlPath, [userId,] callback)` +## Convenience Functionality -Issues a `DELETE` to the Portal API end point specified in `urlPath`, using the user ID passed in as `userId`. If `userId` is left out, the call is done without authentication. Check the API Swagger definition for details. The result of the API call is returned as the second parameter of the `callback` (node standard). +### `wicked.correlationIdHandler()` -The function will callback with an error if a hard error occurred, or if the status code is larger than `299`. If the function fails due to an unexpected status code, details can be found in `err.statusCode` and `err.res`. +The wicked SDK comes with a [correlation ID handler](modules/_index_.html#correlationidhandler) you can use as an express middleware. It will do the following thing: -Use `initMachineUser` to initialize a suitable user ID to use with your service. If you have called `initMachineUser`, `apiPost` will automatically pick up the machine user ID. If you specify a different `userId`, that will be used. - -### OAuth2 Functionality - -#### `wicked.oauth2AuthorizeImplicit(userInfo, callback)` - -Alias: `getRedirectUriWithAccessToken`. - -This is a convenience function which calls the `/oauth2/register` end point of the Kong Adapter, which registers an end user for use with an API which is configured to be accessed using the OAuth2 Implicit Grant Flow (authorization type `oauth2` with "Implicit Grant" ticked). This is implemented in the Kong Adapter, and does the following checks before issuing an access token: - -* Does the given client (with the given `client_id`) have an active subscription to the given `api_id`? -* Are the API and Application configured correctly (for use with the implicit grant)? -* Make Kong create an access token/redirect URI - -If it succeeds, it will return an `application/json` type response containing a redirect URI for the registered application, giving the access token in the fragment of the redirect URI. +* For incoming requests, check whether there is a header `correlation-id`, and if so, store that internally in the SDK, and in the `req.correlationId` property +* If there is no such header, create a new GUID and store it as `req.correlationId` and internally in the SDK +* For outgoing API calls (using any of the `api*()` functions), the correlation ID will be passed on as a `Correlation-Id` header -Main use case for this function is implementing Authorization Servers for use with wicked. The information you need to insert here must be retrieved from your own identity provider. The decision on whether you will allow user access to the API is also up to you (you are implementing the Authorization Server). After a positive decision, you use the function to register the user with the API Gateway. +Upstream wicked functionality will pick up this header and display it in logs. -**Example** (using `express`): +**Usage**: ```javascript -app.get('/authorize', function (req, res, next) { - // You would get this information from your own IdP - const userInfo = { - authenticated_userid: 'end-user-id', - api_id: 'some-api', - client_id: 'client-id-for-app-from-portal', - auth_server: 'your-auth-server-id', - scope: ['scope1', 'scope2'] - }; - wicked.oauth2AuthorizeImplicit(userInfo, function (err, result) { - if (err) { - // Handle the error in a suitable way, at least - return next(err); - } - - // result looks like this: - // { redirect_uri: 'https://your.app.com#access_token=87289df7987890129080&expires_in=1800&token_type=bearer' } - - res.redirect(result.redirect_uri); - }); -}); -``` - -The content of the `userInfo` structure: - -* `authenticated_userid`: This is one part of the payload of the token; in this property, pass in the user identity of the end user in **your** system (or the authenticating system). The backend API will receive this string (can contain mostly anything) as an `X-Authenticated-Userid` header when using the token to go through the API Gateway -* `api_id`: The API for which an access token shall be created; this is used to verify that there actually is a subscription for the `client_id` to the `api_id`, and to retrieve some settings on the API (such as the request path). The `api_id` can either be hard coded or also passed in to your implementation, depending on your use case -* `client_id`: This is the client ID of the calling application; this has to be passed in to your authorization server implementation from the outside -* `auth_server`: Semi-optional -- use this to verify that the API for which the token is to be created actually is configured for use with an authorization server (the Kong Adapter will do this for you). **IMPORTANT**: If you do not specify this, any authorization server may be used with any API, as long as it's configured for the implicit grant. -* `scope`: For which scope shall the access token be created; this is the main task of an Authorization Server: Which scopes (e.g. rights) does the authenticated end user have in your API; if you use scopes, these also have to be configured in your `apis.json` configuration. - -#### `wicked.oauth2GetAuthorizationCode(userInfo, callback)` - -**Version:** Works as of wicked 0.10.1. - -Works exactly like `wicked.oauth2AuthorizeImplicit()`, but you will not get back an access token directly, but rather an Authorization Code, which the client needs to use together with its `client_id` and `client_secret` with the API's `/oauth2/token` endpoint to actually retrieve an access token and refresh token. - -This is the implementation which is suitable for use with the OAuth2 Authoriazation Code Grant. - -#### `wicked.oauth2GetAccessTokenPasswordGrant(userInfo, callback)` - -For use in Authorization Server applications which want to create access tokens for use with the OAuth2 Resource Owner Password Grant. The actual implementation of the Username/Password check has to be done within your implementation of an Authorization Server. After you have done that, you can use this convenience end point to create an access token and a refresh token for use with your API. +const wicked = require('wicked-sdk'); +const app = require('express')(); -It takes a payload in `userInfo` like this (exactly as for the implicit grant above): - -```json -{ - "client_id": "client-id-for-app-from-portal", - "api_id": "some-api", - "authenticated_userid": "end-user-id", - "auth_server": "auth-server-id", - "scope": ["scope1", "scope2"] -} -``` - -This call goes to the Kong Adapter (which has to run in the same network as your implementation) and does the following things: - -* Does the given client (with the given `client_id`) have an active subscription to the given `api_id`? -* Are the API and Application configured correctly (for use with the password grant)? -* Make Kong create an access/refresh token - -The same as for the implicit grant applies: If you do not specify an `auth_server`, the Kong Adapter will **not** check whether the API is actually configured to use a specific authorization server, and will allow token creation using **any authorization server**. - -#### `wicked.oauth2RefreshAccessToken(tokenInfo, callback)` - -For access/refresh tokens created e.g. with the above `oauth2GetAccessTokenPasswordGrant` function, you may use this convenience function to refresh an access token using the `refresh_token` grant. This will only work for APIs configured to be secured with OAuth 2.0, using either the Authorization Code Grant, or the Resource Owner Password Grant. - -Currently, the SDK only explicitly supports the Resource Owner Password Grant (may change in the future). - -Payload for `tokenInfo`: - -```json -{ - "client_id": "client-id-for-app-from-portal", - "refresh_token": "the-refresh-token-you-received", - "auth_server": "auth-server-id", -} +app.use(wicked.correlationIdHandler()); +// ... ``` -The Kong Adapter will perform the following actions: - -* Does the `client_id` still have a valid subscription to the API? -* Is the API configured to grant refresh token requests? -* Ask Kong to refresh the access token and issue a new access/refresh token pair. - -The previous refresh token will then be invalid, and the new refresh token needs to be used. - -The same as for the implicit grant applies: If you do not specify an `auth_server`, the Kong Adapter will **not** check whether the API is actually configured to use a specific authorization server, and will allow token creation using **any authorization server**. - -If you have a trusted application, use the APIs `/oauth2/token` end point directly, and additionally pass in the `client_secret` into the request body. - -#### `wicked.getAccessTokenInfo(accessToken, callback)` +## Promise support -Retrieve information on an access token. Use this to get the information on the authenticated user back based on an access token. A typical use case for this is to find out whether a user is still entitled to use a specific API (authorization). +All functions have an overload which also supports returning a promise instead of using the callback. If you do not supply the `callback` parameter, all functions will return a `Promise` instead. -Returns (a superset of): +**Example**: -```json -{ - "authenticated_userid": "237982738273", - "authenticated_scope": ["scope1", "scope2"], - "access_token": "euro4598475983475984798584", - "refresh_token": "3048983094830958098r090tre098t0947touoi5454" -} ``` +const wicked = require('wicked-sdk'); + +const wickedOptions = { + userAgentName: 'your-component', + userAgentVersion: '1.0.0', + apiMaxTries: 10, // optional, defaults to 10 + apiRetryDelay: 500, // optional, defaults to 500 +}; + +// You wouldn't do it like this, this is just an example. +(async () => { + // Init wicked SDK and register a machine user. + try { + const wickedGlobals = await wicked.initialize(wickedOptions); + await wicked.initMachineUser('yourcomponent'); + } catch (err) { + console.error('Failed to initialize wicked:'); + console.error(err); + throw err; + } -#### `wicked.getRefreshTokenInfo(refreshToken, callback)` - -Retrieve information on an access token. Use this to get the information on the authenticated user back based on an access token. A typical use case for this is to find out whether a user is still entitled to use a specific API (authorization), e.g. before you issue a new pair of access/refresh tokens. - -The decision whether an end user (as opposed to the client) is allowed to continue using an API is entirely up to you, as the implementor of an Authorization Server; this has nothing to do with OAuth2 in general, but needs to be implemented depending on your use case. - -Returns (a superset of): + // Remember some URLs + app.set('api_url', wicked.getInternalApiUrl()); + ... -```json -{ - "authenticated_userid": "237982738273", - "authenticated_scope": ["scope1", "scope2"], - "access_token": "euro4598475983475984798584", - "refresh_token": "3048983094830958098r090tre098t0947touoi5454" -} + // Now you can use wicked as you wish. + const registrationCollection = await wicked.getUserRegistrations('regpool', someUserId); + // ... +})(); ``` - -#### `wicked.revokeAccessToken(accessToken, callback)` - -**Version:** Works as of wicked 0.11.6. - -Revokes an access token by access token string. - -After calling this (allow up to a second for the action to take effect), the access token will no longer be valid for calling an API via the API Gateway. - -This is useful for implementing a logout functionality. - -#### `wicked.revokeAccessTokenByUserId(authenticatedUserID, callback)` - -**Version:** Works as of wicked 0.11.6. - -Revokes all access tokens which were issued to `authenticatedUserId`. - -After calling this (allow up to a second for the action to take effect), the access tokens which were issued to the user with the `authenticatedUserId` will no longer be valid for calling an API via the API Gateway. By first retrieving all access tokens for the given user, all access tokens are sequentially deleted from the access token store. This can take a little while (allow 100ms per token). - -This is useful for implementing a logout functionality. - -#### `wicked.getSubscriptionByClientId(clientId, apiId, callback)` - -Convenience method which does the following: - -* Lookup subscription information based on the `clientId` (which is attached to a subscription in the Portal) -* If found, check that the subscription is matching the API given in the `apiId` - -If successful, it will return the following information in the second parameter of the `callback` function (`function (err, results)`): - -* In `results.application`, you will find the information on the registered application -* In `results.subscription`, the data on the subscription (including API and Plan) will be returned - -See the Swagger definition of the Portal API for more information. This method maps directly to `/subscriptions/:clientId`. - -### Settings Retrieval - -#### `wicked.getGlobals()` - -Returns the content of the `globals.json` you deployed with your Portal API instance (in your configuration). - -**Note**: Will throw an exception if `initialize()` has not yet successfully finished. - -#### `wicked.getConfigHash()` - -**Version requirement**: wicked SDK 0.11.1, wicked installation 0.11.0 - -Returns the config hash which was retrieved at initialization. You may use this functionality to retrieve the config hash manually, in case you do not wish to use the SDK API functions but still want to make sure you are working against the correct configuration version (by passing a `X-Config-Hash` header manually). - -**Note**: Will throw an exception if `initialize()` has not yet successfully finished. - -#### `wicked.getExternalPortalUrl()` - -Returns the URL of the portal, the way it is reachable from the outside, e.g. usually from the public internet. This is a convenience method which uses information from the `globals.json` configuration to assemble the URL. - -**Note**: Will throw an exception if `initialize()` has not yet successfully finished. - -#### `wicked.getExternalApiUrl()` - -Returns the URL of the API Gateway, the way it is reachable from the outside, e.g. usually from the public internet. This is a convenience method which uses information from the `globals.json` configuration to assemble the URL. - -**Note**: Will throw an exception if `initialize()` has not yet successfully finished. - -#### `wicked.getInternalApiUrl()` - -Returns a fully qualified URL to the portal API, as seen from inside the docker environment, usually this will be `http://portal-api:3001`. This is **not** an URL you can use from the outside, it's only intended for use within the same docker network as the Portal API. This is the same URL which was used to successfully connect to the API in the `initialization()` call. - -**Note**: Will throw an exception if `initialize()` has not yet successfully finished. - -#### `wicked.getInternalKongAdapterUrl()` - -Returns a fully qualified URL to the Kong Adapter API; same restrictions as above apply. Usually this is `http://portal-kong-adapter:3002`, but if you have overridden the setting in your environment, the content of the `globals.network.kongAdapterUrl` setting is returned (possibly depending on your running environment). - -**Note**: Will throw an exception if `initialize()` has not yet successfully finished. - -#### `wicked.getInternalKongAdminUrl()` - -Returns a fully qualified URL to the Kong Admin API, reachable **only** from within the docker network. Usually this is `http://kong:8001`, but if you have overridden the setting in your environment, the content of the `globals.network.kongAdminUrl` setting is returned (possibly depending on your running environment). - -**Note**: Will throw an exception if `initialize()` has not yet successfully finished. - -#### `wicked.getInternalChatbotUrl()` - -Returns a fully qualified URL to the Portal Chatbot, reachable **only** from within the docker network. Usually this is `http://portal-chatbot:3004`, but if you have overridden the setting in your environment, the content of the `globals.network.chatbotUrl` setting is returned (possibly depending on your running environment). - -**Note**: Will throw an exception if `initialize()` has not yet successfully finished. - -#### `wicked.getInternalMailerUrl()` - -Returns a fully qualified URL to the Portal Chatbot, reachable **only** from within the docker network. Usually this is `http://portal-mailer:3003`, but if you have overridden the setting in your environment, the content of the `globals.network.mailerUrl` setting is returned (possibly depending on your running environment). - -**Note**: Will throw an exception if `initialize()` has not yet successfully finished. - -#### `wicked.getInternalUrl(globalSettingsProperty)` - -Returns an arbitrary URL defined in the `globals.network` JSON configuration. You may use this to add other network settings to the `globals.json` and retrieve it using this mechanism. Please note that this function will throw an `Error` in case the property `globalSettingsProperty` cannot be found in the `networks` section of `globals.json`, whereas the other ones default to the docker container URLs as in the default configuration (e.g. `http://portal-chatbot:3004` etc. pp.). - -**Note**: Will throw an exception if `initialize()` has not yet successfully finished. - -### Convenience Functionality - -#### `wicked.correlationIdHandler()` - -The wicked SDK comes with a correlation ID handler you can use as an express middleware. It will do the following thing: - -* For incoming requests, check whether there is a header `correlation-id`, and if so, store that internally in the SDK, and in the `req.correlationId` property -* If there is no such header, create a new GUID and store it as `req.correlationId` and internally in the SDK -* For outgoing API calls (using any of the `api*()` functions), the correlation ID will be passed on as a `Correlation-Id` header - -Upstream wicked functionality will pick up this header and display it in logs. - -**Usage**: - -```javascript -var wicked = require('wicked-sdk'); -var app = require('express')(); - -app.use(wicked.correlationIdHandler()); -// ... -``` \ No newline at end of file diff --git a/index.js b/index.js deleted file mode 100644 index 1ad656d..0000000 --- a/index.js +++ /dev/null @@ -1,960 +0,0 @@ -'use strict'; - -const os = require('os'); -const debug = require('debug')('wicked-sdk'); -const request = require('request'); -const qs = require('querystring'); -const uuid = require('node-uuid'); - -const WICKED_TIMEOUT = 2000; // request timeout for wicked API operations -const KONG_TIMEOUT = 5000; // request timeout for kong admin API operations -const TRYGET_TIMEOUT = 2000; // request timeout for single calls in awaitUrl - -// ====== VARIABLES ====== - -// Use this for local caching of things. Usually just the globals. -// The apiUrl will - after initialization - contain the URL which -// was used to access the portal API with. -const wickedStorage = { - initialized: false, - kongAdapterInitialized: false, - machineUserId: null, - apiUrl: null, - globals: null, - correlationId: null, - configHash: null, - userAgent: null, - pendingExit: false, - apiReachable: false, - // This field will not necessarily be filled. - apiVersion: null, - isV012OrHigher: false -}; - -// ======= SDK INTERFACE ======= - -// ======= INITIALIZATION ======= - -exports.initialize = function (options, callback) { - initialize(options, callback); -}; - -exports.isDevelopmentMode = function () { - return isDevelopmentMode(); -}; - -exports.initMachineUser = function (serviceId, callback) { - initMachineUser(serviceId, callback); -}; - -exports.awaitUrl = function (url, options, callback) { - awaitUrl(url, options, callback); -}; - -exports.awaitKongAdapter = function (awaitOptions, callback) { - awaitKongAdapter(awaitOptions, callback); -}; - -// ======= INFORMATION RETRIEVAL ======= - -exports.getGlobals = function () { - return getGlobals(); -}; - -exports.getConfigHash = function () { - return getConfigHash(); -}; - -exports.getExternalPortalUrl = function () { - return getExternalPortalUrl(); -}; - -exports.getExternalApiUrl = function () { - return getExternalGatewayUrl(); -}; - -exports.getInternalApiUrl = function () { - return getInternalApiUrl(); -}; - -exports.getInternalKongAdminUrl = function () { - return getInternalKongAdminUrl(); -}; - -exports.getInternalKongAdapterUrl = function () { - return getInternalKongAdapterUrl(); -}; - -exports.getInternalChatbotUrl = function () { - return getInternalChatbotUrl(); -}; - -exports.getInternalMailerUrl = function () { - return getInternalMailerUrl(); -}; - -exports.getInternalUrl = function (globalSettingsProperty) { - return getInternalUrl(globalSettingsProperty, null); -}; - -// ======= API FUNCTIONALITY ======= - -exports.apiGet = function (urlPath, userId, callback) { - apiGet(urlPath, userId, callback); -}; - -exports.apiPost = function (urlPath, postBody, userId, callback) { - apiPost(urlPath, postBody, userId, callback); -}; - -exports.apiPut = function (urlPath, putBody, userId, callback) { - apiPut(urlPath, putBody, userId, callback); -}; - -exports.apiPatch = function (urlPath, patchBody, userId, callback) { - apiPatch(urlPath, patchBody, userId, callback); -}; - -exports.apiDelete = function (urlPath, userId, callback) { - apiDelete(urlPath, userId, callback); -}; - -// ======= OAUTH2 CONVENIENCE FUNCTIONS ======= - -exports.getRedirectUriWithAccessToken = function (userInfo, callback) { - getRedirectUriWithAccessToken(userInfo, callback); -}; - -exports.oauth2AuthorizeImplicit = function (userInfo, callback) { - oauth2AuthorizeImplicit(userInfo, callback); -}; - -exports.oauth2GetAuthorizationCode = function (userInfo, callback) { - oauth2GetAuthorizationCode(userInfo, callback); -}; - -exports.oauth2GetAccessTokenPasswordGrant = function (userInfo, callback) { - oauth2GetAccessTokenPasswordGrant(userInfo, callback); -}; - -exports.oauth2RefreshAccessToken = function (tokenInfo, callback) { - oauth2RefreshAccessToken(tokenInfo, callback); -}; - -exports.oauth2GetAccessTokenInfo = function (accessToken, callback) { - oauth2GetAccessTokenInfo(accessToken, callback); -}; - -exports.oauth2GetRefreshTokenInfo = function (refreshToken, callback) { - oauth2GetRefreshTokenInfo(refreshToken, callback); -}; - -exports.getSubscriptionByClientId = function (clientId, apiId, callback) { - getSubscriptionByClientId(clientId, apiId, callback); -}; - -exports.revokeAccessToken = function (accessToken, callback) { - revokeAccessToken(accessToken, callback); -}; - -exports.revokeAccessTokensByUserId = function (authenticatedUserId, callback) { - revokeAccessTokensByUserId(authenticatedUserId, callback); -}; - -// ======= CORRELATION ID HANDLER ======= - -exports.correlationIdHandler = function () { - return function (req, res, next) { - const correlationId = req.get('correlation-id'); - if (correlationId) { - debug('Picking up correlation id: ' + correlationId); - req.correlationId = correlationId; - } else { - req.correlationId = uuid.v4(); - debug('Creating a new correlation id: ' + req.correlationId); - } - wickedStorage.correlationId = correlationId; - return next(); - }; -}; - -// ======= IMPLEMENTATION ====== - -function initialize(options, callback) { - debug('initialize()'); - if (!callback && (typeof (options) === 'function')) { - callback = options; - options = null; - } - if (options) { - debug('options:'); - debug(options); - } - - const validationError = validateOptions(options); - if (validationError) { - return callback(validationError); - } - - // I know, this would look a lot nicer with async or Promises, - // but I did not want to pull in additional dependencies. - const apiUrl = resolveApiUrl(); - debug('Awaiting portal API at ' + apiUrl); - awaitUrl(apiUrl + 'ping', options, function (err, pingResult) { - if (err) { - debug('awaitUrl returned an error:'); - debug(err); - return callback(err); - } - - debug('Ping result:'); - debug(pingResult); - const pingJson = getJson(pingResult); - if (pingJson.version) { - // The version field is not filled until wicked 0.12.0 - wickedStorage.apiVersion = pingJson.version; - wickedStorage.isV012OrHigher = true; - } - - wickedStorage.apiUrl = apiUrl; - if (options.userAgentName && options.userAgentVersion) - wickedStorage.userAgent = options.userAgentName + '/' + options.userAgentVersion; - request.get({ - url: apiUrl + 'confighash', - timeout: WICKED_TIMEOUT - }, function (err, res, body) { - if (err) { - debug('GET /confighash failed'); - debug(err); - return callback(err); - } - - if (200 != res.statusCode) { - debug('GET /confighash returned status code: ' + res.statusCode); - debug('Body: ' + body); - return callback(new Error('GET /confighash returned unexpected status code: ' + res.statusCode + ' (Body: ' + body + ')')); - } - - wickedStorage.configHash = '' + body; - - request.get({ - url: apiUrl + 'globals', - headers: { - 'User-Agent': wickedStorage.userAgent, - 'X-Config-Hash': wickedStorage.configHash - }, - timeout: WICKED_TIMEOUT - }, function (err, res, body) { - if (err) { - debug('GET /globals failed'); - debug(err); - return callback(err); - } - if (res.statusCode !== 200) { - debug('GET /globals returned status code ' + res.statusCode); - return callback(new Error('GET /globals return unexpected error code: ' + res.statusCode)); - } - - let globals = null; - try { - globals = getJson(body); - wickedStorage.globals = globals; - wickedStorage.initialized = true; - wickedStorage.apiReachable = true; - } catch (ex) { - return callback(new Error('Parsing globals failed: ' + ex.message)); - } - - // Success, set up config hash checker loop (if not switched off) - if (!options.doNotPollConfigHash) { - setInterval(checkConfigHash, 10000); - } - - return callback(null, globals); - }); - }); - }); -} - -function validateOptions(options) { - if ((options.userAgentName && !options.userAgentVersion) || - (!options.userAgentName && options.userAgentVersion)) - return new Error('You need to specify both userAgentName and userAgentVersion'); - if (options.userAgentName && - !/^[a-zA-Z\ \-\_\.0-9]+$/.test(options.userAgentName)) - return new Error('The userAgentName must only contain characters a-z, A-Z, 0-9, -, _ and space.'); - if (options.userAgentVersion && - !/^[0-9\.]+$/.test(options.userAgentVersion)) - return new Error('The userAgentVersion must only contain characters 0-9 and .'); - return null; -} - -function checkConfigHash() { - debug('checkConfigHash()'); - - request.get({ - url: wickedStorage.apiUrl + 'confighash', - timeout: WICKED_TIMEOUT - }, function (err, res, body) { - wickedStorage.apiReachable = false; - if (err) { - console.error('checkConfigHash(): An error occurred.'); - console.error(err); - console.error(err.stack); - return; - } - if (200 !== res.statusCode) { - console.error('checkConfigHash(): Returned unexpected status code: ' + res.statusCode); - return; - } - wickedStorage.apiReachable = true; - const configHash = '' + body; - - if (configHash !== wickedStorage.configHash) { - console.log('checkConfigHash() - Detected new configuration version, scheduling shutdown in 2 seconds.'); - wickedStorage.pendingExit = true; - setTimeout(forceExit, 2000); - } - }); -} - -function forceExit() { - console.log('Exiting component due to outdated configuration (confighash mismatch).'); - process.exit(0); -} - -function isDevelopmentMode() { - checkInitialized('isDevelopmentMode'); - - if (wickedStorage.globals && - wickedStorage.globals.network && - wickedStorage.globals.network.schema && - wickedStorage.globals.network.schema === 'https') - return false; - return true; -} - -const DEFAULT_AWAIT_OPTIONS = { - statusCode: 200, - maxTries: 100, - retryDelay: 1000 -}; - -function awaitUrl(url, options, callback) { - debug('awaitUrl(): ' + url); - if (!callback && (typeof (options) === 'function')) { - callback = options; - options = null; - } - // Copy the settings from the defaults; otherwise we'd change them haphazardly - const awaitOptions = { - statusCode: DEFAULT_AWAIT_OPTIONS.statusCode, - maxTries: DEFAULT_AWAIT_OPTIONS.maxTries, - retryDelay: DEFAULT_AWAIT_OPTIONS.retryDelay - }; - if (options) { - if (options.statusCode) - awaitOptions.statusCode = options.statusCode; - if (options.maxTries) - awaitOptions.maxTries = options.maxTries; - if (options.retryDelay) - awaitOptions.retryDelay = options.retryDelay; - } - - debug('Invoking tryGet()'); - tryGet(url, awaitOptions.statusCode, awaitOptions.maxTries, 0, awaitOptions.retryDelay, function (err, body) { - debug('tryGet() returned.'); - if (err) { - debug('but tryGet() errored.'); - debug(err); - return callback(err); - } - callback(null, body); - }); -} - -function awaitKongAdapter(awaitOptions, callback) { - debug('awaitKongAdapter()'); - checkInitialized('awaitKongAdapter'); - if (!callback && (typeof (awaitOptions) === 'function')) { - callback = awaitOptions; - awaitOptions = null; - } - if (awaitOptions) { - debug('awaitOptions:'); - debug(awaitOptions); - } - - const adapterPingUrl = getInternalKongAdapterUrl() + 'ping'; - awaitUrl(adapterPingUrl, awaitOptions, function (err, body) { - if (err) - return callback(err); - wickedStorage.kongAdapterInitialized = true; - return callback(null, body); - }); -} - -function initMachineUser(serviceId, callback) { - debug('initMachineUser()'); - checkInitialized('initMachineUser'); - - if (!/^[a-zA-Z\-_0-9]+$/.test(serviceId)) - return callback(new Error('Invalid Service ID, must only contain a-z, A-Z, 0-9, - and _.')); - - const customId = makeMachineUserCustomId(serviceId); - apiGet('users?customId=' + qs.escape(customId), function (err, userInfo) { - if (err && err.statusCode == 404) { - // Not found - return createMachineUser(serviceId, callback); - } else if (err) { - return callback(err); - } - if (!Array.isArray(userInfo)) - return callback(new Error('GET of user with customId ' + customId + ' did not return expected array.')); - if (userInfo.length !== 1) - return callback(new Error('GET of user with customId ' + customId + ' did not return array of length 1 (length == ' + userInfo.length + ').')); - userInfo = userInfo[0]; // Pick the user from the list. - debug('Machine user info:'); - debug(userInfo); - debug('Setting machine user id: ' + userInfo.id); - wickedStorage.machineUserId = userInfo.id; - return callback(null, userInfo); - }); -} - -function makeMachineUserCustomId(serviceId) { - const customId = 'internal:' + serviceId; - return customId; -} - -function createMachineUser(serviceId, callback) { - const customId = makeMachineUserCustomId(serviceId); - const userInfo = { - customId: customId, - firstName: 'Machine-User', - lastName: serviceId, - email: serviceId + '@wicked.haufe.io', - validated: true, - groups: ['admin'] - }; - apiPost('users', userInfo, function (err, userInfo) { - if (err) - return callback(err); - debug('Machine user info:'); - debug(userInfo); - debug('Setting machine user id: ' + userInfo.id); - wickedStorage.machineUserId = userInfo.id; - return callback(null, userInfo); - }); -} - -function getGlobals() { - debug('getGlobals()'); - checkInitialized('getGlobals'); - - return wickedStorage.globals; -} - -function getConfigHash() { - debug('getConfigHash()'); - checkInitialized('getConfigHash'); - - return wickedStorage.configHash; -} - -function getExternalPortalUrl() { - debug('getExternalPortalUrl()'); - checkInitialized('getExternalPortalUrl'); - - return checkSlash(getSchema() + '://' + getPortalHost()); -} - -function getExternalGatewayUrl() { - debug('getExternalGatewayUrl()'); - checkInitialized('getExternalGatewayUrl'); - - return checkSlash(getSchema() + '://' + getApiHost()); -} - -function getInternalApiUrl() { - debug('getInternalApiUrl()'); - checkInitialized('getInternalApiUrl'); - - return checkSlash(wickedStorage.apiUrl); -} - -function getInternalKongAdminUrl() { - debug('getInternalKongAdminUrl()'); - checkInitialized('getInternalKongAdminUrl'); - - return getInternalUrl('kongAdminUrl', 'kong', 8001); -} - -function getInternalMailerUrl() { - debug('getInternalMailerUrl'); - checkInitialized('getInternalMailerUrl'); - - return getInternalUrl('mailerUrl', 'portal-mailer', 3003); -} - -function getInternalChatbotUrl() { - debug('getInternalChatbotUrl()'); - checkInitialized('getInternalChatbotUrl'); - - return getInternalUrl('chatbotUrl', 'portal-chatbot', 3004); -} - -function getInternalKongAdapterUrl() { - debug('getInternalKongAdapterUrl()'); - checkInitialized('getInternalKongAdapterUrl'); - - return getInternalUrl('kongAdapterUrl', 'portal-kong-adapter', 3002); -} - -function getInternalUrl(globalSettingsProperty, defaultHost, defaultPort) { - debug('getInternalUrl("' + globalSettingsProperty + '")'); - checkInitialized('getInternalUrl'); - - if (wickedStorage.globals.network && - wickedStorage.globals.network.hasOwnProperty(globalSettingsProperty)) { - return checkSlash(wickedStorage.globals.network[globalSettingsProperty]); - } - if (defaultHost && defaultPort) - return checkSlash(guessServiceUrl(defaultHost, defaultPort)); - throw new Error('Configuration property "' + globalSettingsProperty + '" not defined in globals.json: network.'); -} - -// ======= UTILITY FUNCTIONS ====== - -function checkSlash(someUrl) { - if (someUrl.endsWith('/')) - return someUrl; - return someUrl + '/'; -} - -function getSchema() { - if (wickedStorage.globals.network && - wickedStorage.globals.network.schema) - return wickedStorage.globals.network.schema; - console.error('In globals.json, network.schema is not defined. Defaulting to https.'); - return 'https'; -} - -function getPortalHost() { - if (wickedStorage.globals.network && - wickedStorage.globals.network.portalHost) - return wickedStorage.globals.network.portalHost; - throw new Error('In globals.json, portalHost is not defined. Cannot return any default.'); -} - -function getApiHost() { - if (wickedStorage.globals.network && - wickedStorage.globals.network.apiHost) - return wickedStorage.globals.network.apiHost; - throw new Error('In globals.json, apiHost is not defined. Cannot return any default.'); -} - -function checkInitialized(callingFunction) { - if (!wickedStorage.initialized) - throw new Error('Before calling ' + callingFunction + '(), initialize() must have been called and has to have returned successfully.'); -} - -function checkKongAdapterInitialized(callingFunction) { - if (!wickedStorage.kongAdapterInitialized) - throw new Error('Before calling ' + callingFunction + '(), awaitKongAdapter() must have been called and has to have returned successfully.'); -} - -function guessServiceUrl(defaultHost, defaultPort) { - debug('guessServiceUrl() - defaultHost: ' + defaultHost + ', defaultPort: ' + defaultPort); - let url = 'http://' + defaultHost + ':' + defaultPort + '/'; - // Are we not running on Linux? Then guess we're in local development mode. - if (os.type() != 'Linux') { - const defaultLocalIP = getDefaultLocalIP(); - url = 'http://' + defaultLocalIP + ':' + defaultPort + '/'; - } - debug(url); - return url; -} - -function resolveApiUrl() { - let apiUrl = process.env.PORTAL_API_URL; - if (!apiUrl) { - apiUrl = guessServiceUrl('portal-api', '3001'); - console.error('Environment variable PORTAL_API_URL is not set, defaulting to ' + apiUrl + '. If this is not correct, please set before starting this process.'); - } - if (!apiUrl.endsWith('/')) // Add trailing slash - apiUrl += '/'; - return apiUrl; -} - -function getDefaultLocalIP() { - const localIPs = getLocalIPs(); - if (localIPs.length > 0) - return localIPs[0]; - return "localhost"; -} - -function getLocalIPs() { - debug('getLocalIPs()'); - const interfaces = os.networkInterfaces(); - const addresses = []; - for (let k in interfaces) { - for (let k2 in interfaces[k]) { - const address = interfaces[k][k2]; - if (address.family === 'IPv4' && !address.internal) { - addresses.push(address.address); - } - } - } - debug(addresses); - return addresses; -} - -function tryGet(url, statusCode, maxTries, tryCounter, timeout, callback) { - debug('Try #' + tryCounter + ' to GET ' + url); - request.get({ url: url, timeout: TRYGET_TIMEOUT }, function (err, res, body) { - if (err || res.statusCode !== statusCode) { - if (tryCounter < maxTries || maxTries < 0) - return setTimeout(tryGet, timeout, url, statusCode, maxTries, tryCounter + 1, timeout, callback); - debug('Giving up.'); - if (!err) - err = new Error('Too many unsuccessful retries to GET ' + url + '. Gave up after ' + maxTries + ' tries.'); - return callback(err); - } - callback(null, body); - }); -} - -function getJson(ob) { - if (ob instanceof String || typeof ob === "string") - return JSON.parse(ob); - return ob; -} - -function getText(ob) { - if (ob instanceof String || typeof ob === "string") - return ob; - return JSON.stringify(ob, null, 2); -} - -function apiGet(urlPath, userId, callback) { - debug('apiGet(): ' + urlPath); - checkInitialized('apiGet'); - - apiAction('GET', urlPath, null, userId, callback); -} - -function apiPost(urlPath, postBody, userId, callback) { - debug('apiGet(): ' + urlPath); - checkInitialized('apiPost'); - - apiAction('POST', urlPath, postBody, userId, callback); -} - -function apiPut(urlPath, putBody, userId, callback) { - debug('apiPut(): ' + urlPath); - checkInitialized('apiPut'); - - apiAction('PUT', urlPath, putBody, userId, callback); -} - -function apiPatch(urlPath, patchBody, userId, callback) { - debug('apiPatch(): ' + urlPath); - checkInitialized('apiPatch'); - - apiAction('PATCH', urlPath, patchBody, userId, callback); -} - -function apiDelete(urlPath, userId, callback) { - debug('apiDelete(): ' + urlPath); - checkInitialized('apiDelete'); - - apiAction('DELETE', urlPath, null, userId, callback); -} - -function apiAction(method, urlPath, actionBody, userId, callback) { - debug('apiAction(' + method + '): ' + urlPath); - - if (!wickedStorage.apiReachable) - return callback(new Error('The wicked API is currently not reachable. Try again later.')); - if (wickedStorage.pendingExit) - return callback(new Error('A shutdown due to changed configuration is pending.')); - - if (actionBody) - debug(actionBody); - - if (!callback && (typeof (userId) === 'function')) { - callback = userId; - userId = null; - } - if (!userId && wickedStorage.machineUserId) { - debug('Picking up machine user id: ' + wickedStorage.machineUserId); - userId = wickedStorage.machineUserId; - } - - if (urlPath.startsWith('/')) - urlPath = urlPath.substring(1); // strip slash in beginning; it's in the API url - - const url = getInternalApiUrl() + urlPath; - debug(method + ' ' + url); - const reqInfo = { - method: method, - url: url, - timeout: WICKED_TIMEOUT - }; - if (method != 'DELETE' && - method != 'GET') { - // DELETE and GET ain't got no body. - reqInfo.body = actionBody; - reqInfo.json = true; - } - // This is the config hash we saw at init; send it to make sure we don't - // run on an outdated configuration. - reqInfo.headers = { 'X-Config-Hash': wickedStorage.configHash }; - if (userId) { - if (wickedStorage.isV012OrHigher) { - reqInfo.headers['X-Authenticated-UserId'] = userId; - reqInfo.headers['X-Authenticated-Scope'] = 'admin'; - } else { - reqInfo.headers['X-UserId'] = userId; - } - } - if (wickedStorage.correlationId) { - debug('Using correlation id: ' + wickedStorage.correlationId); - reqInfo.headers['Correlation-Id'] = wickedStorage.correlationId; - } - if (wickedStorage.userAgent) { - debug('Using User-Agent: ' + wickedStorage.userAgent); - reqInfo.headers['User-Agent'] = wickedStorage.userAgent; - } - - request(reqInfo, function (err, res, body) { - if (err) - return callback(err); - if (res.statusCode > 299) { - // Looks bad - const err = new Error('api' + nice(method) + '() ' + urlPath + ' returned non-OK status code: ' + res.statusCode + ', check err.statusCode and err.body for details'); - err.statusCode = res.statusCode; - err.body = body; - return callback(err); - } - if (res.statusCode !== 204) { - const contentType = res.headers['content-type']; - let returnValue = null; - try { - if (contentType.startsWith('text')) - returnValue = getText(body); - else - returnValue = getJson(body); - } catch (ex) { - return callback(new Error('api' + nice(method) + '() ' + urlPath + ' returned non-parseable JSON: ' + ex.message)); - } - return callback(null, returnValue); - } else { - // Empty response - return callback(null); - } - }); -} - -function nice(methodName) { - return methodName.substring(0, 1) + methodName.substring(1).toLowerCase(); -} - -// ====== OAUTH2 ====== - -function kongAdapterAction(method, url, body, callback) { - const actionUrl = getInternalKongAdapterUrl() + url; - const reqBody = { - method: method, - url: actionUrl, - timeout: KONG_TIMEOUT - }; - if (method !== 'GET') { - reqBody.json = true; - reqBody.body = body; - } - request(reqBody, function (err, res, body) { - if (err) { - debug(method + ' to ' + actionUrl + ' failed.'); - debug(err); - return callback(err); - } - if (res.statusCode > 299) { - const err = new Error(method + ' to ' + actionUrl + ' returned unexpected status code: ' + res.statusCode + '. Details in err.body and err.statusCode.'); - debug('Unexpected status code.'); - debug('Status Code: ' + res.statusCode); - debug('Body: ' + body); - err.statusCode = res.statusCode; - err.body = body; - return callback(err); - } - let jsonBody = null; - try { - jsonBody = getJson(body); - debug(jsonBody); - } catch (ex) { - const err = new Error(method + ' to ' + actionUrl + ' returned non-parseable JSON: ' + ex.message + '. Possible details in err.body.'); - err.body = body; - return callback(err); - } - return callback(null, jsonBody); - }); -} - -function getRedirectUriWithAccessToken(userInfo, callback) { - debug('getRedirectUriWithAccessToken()'); - oauth2AuthorizeImplicit(userInfo, callback); -} - -function oauth2AuthorizeImplicit(userInfo, callback) { - debug('oauth2AuthorizeImplicit()'); - checkInitialized('oauth2AuthorizeImplicit'); - checkKongAdapterInitialized('oauth2AuthorizeImplicit'); - - if (!userInfo.client_id) - return callback(new Error('client_id is mandatory')); - if (!userInfo.api_id) - return callback(new Error('api_id is mandatory')); - if (!userInfo.authenticated_userid) - return callback(new Error('authenticated_userid is mandatory')); - if (!userInfo.auth_server) - console.error('WARNING: wicked-sdk: oauth2AuthorizeImplicit() - auth_server is not passed in to call; this means it is not checked whether the API has the correct auth server configured.'); - - kongAdapterAction('POST', 'oauth2/token/implicit', userInfo, function (err, redirectUri) { - if (err) - return callback(err); - callback(null, redirectUri); - }); -} - -function oauth2GetAuthorizationCode(userInfo, callback) { - debug('oauth2GetAuthorizationCode()'); - checkInitialized('oauth2GetAuthorizationCode'); - checkKongAdapterInitialized('oauth2GetAuthorizationCode'); - - if (!userInfo.client_id) - return callback(new Error('client_id is mandatory')); - if (!userInfo.api_id) - return callback(new Error('api_id is mandatory')); - if (!userInfo.authenticated_userid) - return callback(new Error('authenticated_userid is mandatory')); - if (!userInfo.auth_server) - console.error('WARNING: wicked-sdk: oauth2GetAuthorizationCode() - auth_server is not passed in to call; this means it is not checked whether the API has the correct auth server configured.'); - - kongAdapterAction('POST', 'oauth2/token/code', userInfo, function (err, redirectUri) { - if (err) - return callback(err); - callback(null, redirectUri); - }); -} - -function oauth2GetAccessTokenPasswordGrant(userInfo, callback) { - debug('oauth2GetAccessTokenPasswordGrant()'); - checkInitialized('oauth2GetAccessTokenPasswordGrant'); - checkKongAdapterInitialized('oauth2GetAccessTokenPasswordGrant'); - - if (!userInfo.client_id) - return callback(new Error('client_id is mandatory')); - if (!userInfo.api_id) - return callback(new Error('api_id is mandatory')); - if (!userInfo.authenticated_userid) - return callback(new Error('authenticated_userid is mandatory')); - if (!userInfo.auth_server) - console.error('WARNING: wicked-sdk: oauth2GetAccessTokenPasswordGrant() - auth_server is not passed in to call; this means it is not checked whether the API has the correct auth server configured.'); - - kongAdapterAction('POST', 'oauth2/token/password', userInfo, function (err, accessToken) { - if (err) - return callback(err); - callback(null, accessToken); - }); -} - -function oauth2RefreshAccessToken(tokenInfo, callback) { - debug('oauth2RefreshAccessToken'); - checkInitialized('oauth2RefreshAccessToken'); - checkKongAdapterInitialized('oauth2RefreshAccessToken'); - - if (!tokenInfo.refresh_token) - return callback(new Error('refresh_token is mandatory')); - if (!tokenInfo.client_id) - return callback(new Error('client_id is mandatory')); - if (!tokenInfo.auth_server) - console.error('WARNING: wicked-sdk: oauth2RefreshAccessToken() - auth_server is not passed in to call; this means it is not checked whether the API has the correct auth server configured.'); - - kongAdapterAction('POST', 'oauth2/token/refresh', tokenInfo, function (err, accessToken) { - if (err) - return callback(err); - callback(null, accessToken); - }); -} - -function oauth2GetAccessTokenInfo(accessToken, callback) { - debug('oauth2GetAccessTokenInfo()'); - checkInitialized('oauth2GetAccessTokenInfo'); - checkKongAdapterInitialized('oauth2GetAccessTokenInfo'); - - kongAdapterAction('GET', 'oauth2/token?access_token=' + qs.escape(accessToken), null, function (err, tokenInfo) { - if (err) - return callback(err); - callback(null, tokenInfo); - }); -} - -function oauth2GetRefreshTokenInfo(refreshToken, callback) { - debug('oauth2GetRefreshTokenInfo()'); - checkInitialized('oauth2GetRefreshTokenInfo'); - checkKongAdapterInitialized('oauth2GetRefreshTokenInfo'); - - kongAdapterAction('GET', 'oauth2/token?refresh_token=' + qs.escape(refreshToken), null, function (err, tokenInfo) { - if (err) - return callback(err); - callback(null, tokenInfo); - }); -} - -function revokeAccessToken(accessToken, callback) { - debug(`revokeAccessToken(${accessToken})`); - checkInitialized('revokeAccessToken()'); - checkKongAdapterInitialized('revokeAccessToken()'); - - kongAdapterAction('DELETE', 'oauth2/token?access_token=' + qs.escape(accessToken), null, callback); -} - -function revokeAccessTokensByUserId(authenticatedUserId, callback) { - debug(`revokeAccessTokenByUserId(${authenticatedUserId})`); - checkInitialized('revokeAccessTokenByUserId()'); - checkKongAdapterInitialized('revokeAccessTokenByUserId()'); - - kongAdapterAction('DELETE', 'oauth2/token?authenticated_userid=' + qs.escape(authenticatedUserId), null, callback); -} - -function getSubscriptionByClientId(clientId, apiId, callback) { - debug('getSubscriptionByClientId()'); - checkInitialized('getSubscriptionByClientId'); - - // Validate format of clientId - if (!/^[a-zA-Z0-9\-]+$/.test(clientId)) { - return callback(new Error('Invalid client_id format.')); - } - - // Check whether we know this client ID, otherwise we won't bother. - apiGet('subscriptions/' + qs.escape(clientId), function (err, subsInfo) { - if (err) { - debug('GET of susbcription for client_id ' + clientId + ' failed.'); - debug(err); - return callback(new Error('Could not identify application with given client_id.')); - } - debug('subscription info:'); - debug(subsInfo); - if (!subsInfo.subscription) - return callback(new Error('Could not successfully retrieve subscription information.')); - if (subsInfo.subscription.api != apiId) { - debug('subsInfo.api != apiId: ' + subsInfo.subscription.api + ' != ' + apiId); - return callback(new Error('Bad request. The client_id does not match the API.')); - } - debug('Successfully identified application: ' + subsInfo.subscription.application); - - return callback(null, subsInfo); - }); -} diff --git a/install-local-sdk.sh b/install-local-sdk.sh index fed1046..046739e 100755 --- a/install-local-sdk.sh +++ b/install-local-sdk.sh @@ -1,27 +1,69 @@ #!/bin/bash +echo "==== STARTING ==== $0" + +trap failure ERR + +function failure { + echo "==================" + echo "==== ERROR ==== $0" + echo "==================" +} + set -e -if [ -z "$1" ]; then - echo "Usage: $0 " +# Check whether jq is installed (should be) +if ! which jq > /dev/null; then + echo "ERROR: This script requires 'jq' to be installed." + exit 1 +fi + +currentDir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +pushd ${currentDir} > /dev/null + +sdkVersion=$(cat package.json | jq '.version' | tr -d '"') +if [[ -z "${sdkVersion}" ]]; then + echo "ERROR: Could not retrieve wicked SDK version from package.json" exit 1 fi +echo "INFO: wicked-sdk v${sdkVersion}" + +rm -f install-local-sdk.log -npm install -npm pack +npm install > /dev/null +./node_modules/typescript/bin/tsc +packageFile=$(npm pack) +echo "INFO: Package file: ${packageFile}" -pushd .. +pushd .. > /dev/null -for dir in wicked.portal \ - wicked.portal-mailer \ - wicked.portal-chatbot \ - wicked.portal-kong-adapter; do +baseDir=$(pwd) + +for dir in wicked.ui \ + wicked.mailer \ + wicked.chatbot \ + wicked.auth \ + wicked.kong-adapter \ + wicked.test/portal-auth \ + wicked.k8s-init; do echo "INFO: Installing node-sdk into $dir" - pushd $dir - npm install ../wicked.node-sdk/wicked-sdk-$1.tgz - popd + pushd $dir > /dev/null + cp -f ${baseDir}/wicked.node-sdk/${packageFile} ./wicked-sdk.tgz + if [ "$1" = "--copy" ]; then + echo "INFO: Just copying node-sdk, npm install has to be run later." + else + npm install wicked-sdk.tgz >> ${baseDir}/wicked.node-sdk/install-local-sdk.log 2>&1 + fi + popd > /dev/null done +# Make sure the package is in the env directory as well, as it's +# needed when building the docker image. +echo "INFO: Copying ${packageFile} to wicked.env" +cp -f ./wicked.node-sdk/${packageFile} ./wicked.env/wicked-sdk.tgz + +popd > /dev/null # .. +popd > /dev/null # currentDir -popd +echo "==== SUCCESS ==== $0" diff --git a/make-docs.sh b/make-docs.sh new file mode 100755 index 0000000..4d001f7 --- /dev/null +++ b/make-docs.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +set -e + +if ! npm list --depth 1 --global typedoc > /dev/null 2>&1; then + echo "INFO: typedoc is not installed, installing globally." + npm install -g typedoc +else + echo "INFO: typedoc is already installed. Excellent." +fi + +rm -rf docs/* +typedoc --mode modules --out ./docs ./src + +if [ ! -d ./docs-git ]; then + mkdir -p docs-git +fi +if [ ! -d ./docs-git/wicked.node-sdk ]; then + pushd docs-git + git clone https://github.com/apim-haufe-io/wicked.node-sdk + popd +fi + +pushd docs-git/wicked.node-sdk +git checkout gh-pages +cp -R ../../docs/* . +git add . +git commit -m "Updated documentation." +git push +popd diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..bca2f69 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,400 @@ +{ + "name": "wicked-sdk", + "version": "1.0.0-rc.4", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/node": { + "version": "10.12.26", + "resolved": "https://pollo-de-goma.myonboarding.de:8081/@types%2fnode/-/node-10.12.26.tgz", + "integrity": "sha512-nMRqS+mL1TOnIJrL6LKJcNZPB8V3eTfRo9FQA2b5gDvrHurC8XbSA86KNe0dShlEL7ReWJv/OU9NL7Z0dnqWTg==" + }, + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "requires": { + "lodash": "^4.17.10" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "containerized": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/containerized/-/containerized-1.0.2.tgz", + "integrity": "sha1-3+0xSeq1BC7wsG6FHH2PTcZ1DYg=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" + }, + "mime-db": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", + "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" + }, + "mime-types": { + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", + "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", + "requires": { + "mime-db": "~1.38.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node-uuid": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz", + "integrity": "sha1-baWhdmjEs91ZYjvaEc9/pMH2Cm8=" + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "psl": { + "version": "1.1.31", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", + "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver": { + "version": "5.6.0", + "resolved": "https://pollo-de-goma.myonboarding.de:8081/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "typescript": { + "version": "3.4.1", + "resolved": "https://pollo-de-goma.myonboarding.de:8081/typescript/-/typescript-3.4.1.tgz", + "integrity": "sha512-3NSMb2VzDQm8oBTLH6Nj55VVtUEpe/rgkIzMir0qVoLyjDZlnMBva0U6vDiV3IH+sl/Yu6oP5QwsAQtHPmDd2Q==", + "dev": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + } + } +} diff --git a/package.json b/package.json index cf8adcb..339114e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wicked-sdk", - "version": "0.12.0", + "version": "1.0.0-rc.4", "description": "node.js SDK for wicked.haufe.io", "keywords": [ "api", @@ -8,7 +8,7 @@ "sdk", "management" ], - "main": "index.js", + "main": "dist/index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, @@ -23,12 +23,19 @@ }, "homepage": "https://github.com/Haufe-Lexware/wicked.node-sdk#readme", "dependencies": { - "debug": "2.2.0", - "request": "2.74.0", - "node-uuid": "1.4.7" + "@types/node": "^10.12.26", + "async": "2.6.1", + "containerized": "1.0.2", + "debug": "3.1.0", + "node-uuid": "1.4.7", + "request": "2.88.0", + "semver": "5.6.0" }, "jshintConfig": { - "node": true, + "node": true, "esversion": 6 - } + }, + "devDependencies": { + "typescript": "3.4.1" + } } diff --git a/publish.sh b/publish.sh new file mode 100755 index 0000000..7a7fed0 --- /dev/null +++ b/publish.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -e + +if [[ -z "${NPM_USER}" ]]; then + echo "*** Env var NPM_USER is not set." + exit 1 +fi +if [[ -z "${NPM_PASS}" ]]; then + echo "*** Env var NPM_PASS is not set." + exit 1 +fi +if [[ -z "${NPM_EMAIL}" ]]; then + echo "*** Env var NPM_EMAIL is not set." + exit 1 +fi + +docker build -f Dockerfile-publish -t wicked-sdk-tmp \ + --build-arg NPM_USER=${NPM_USER} \ + --build-arg NPM_PASS=${NPM_PASS} \ + --build-arg NPM_EMAIL=${NPM_EMAIL} \ + . +docker rmi wicked-sdk-tmp diff --git a/src/implementation.ts b/src/implementation.ts new file mode 100644 index 0000000..1d94a19 --- /dev/null +++ b/src/implementation.ts @@ -0,0 +1,1056 @@ +'use strict'; + +import { WickedInitOptions, Callback, WickedGlobals, WickedGetOptions, WickedGetCollectionOptions, WickedAwaitOptions, WickedUserInfo, ErrorCallback, WickedSubscriptionInfo } from "./interfaces"; +import { WickedError } from "./wicked-error"; + +import * as async from 'async'; +import * as semver from 'semver'; + +/** @hidden */ +const os = require('os'); +/** @hidden */ +const request = require('request'); +/** @hidden */ +const containerized = require('containerized'); +/** @hidden */ +const debug = require('debug')('wicked-sdk'); +/** @hidden */ +const qs = require('querystring'); + +/** @hidden */ +const isLinux = (os.platform() === 'linux'); +/** @hidden */ +const isContainerized = isLinux && containerized(); + +const WICKED_TIMEOUT = 10000; // request timeout for wicked API operations +// const KONG_TIMEOUT = 5000; // request timeout for kong admin API operations +const TRYGET_TIMEOUT = 5000; // request timeout for single calls in awaitUrl + +// ====== VARIABLES ====== + +/** @hidden */ +const EMPTY_STORAGE = { + initialized: false, + kongAdapterInitialized: false, + machineUserId: null, + apiUrl: null, + globals: null, + configHash: null, + userAgent: null, + pendingExit: false, + apiReachable: false, + isPollingApi: false, + // This field will not necessarily be filled. + apiVersion: null, + isV012OrHigher: false, + isV100OrHigher: false, + portalApiScope: null, + apiMaxTries: 10, + apiRetryDelay: 500 +}; + +/** + * Use this for local caching of things. Usually just the globals. + * The apiUrl will - after initialization - contain the URL which + * was used to access the portal API with. + * @hidden + */ +let wickedStorage = _clone(EMPTY_STORAGE); + +/** + * This is used for passing the correlation ID from the correlation handler. + * @hidden + */ +export const requestRuntime = { + correlationId: null +}; + +// ==================== +// INTERNAL TYPES +// ==================== + +/** @hidden */ +interface RequestBody { + method: string, + url: string, + headers?: { + [headerName: string]: string + } + timeout?: number, + json?: boolean, + body?: any +} + +// ======= IMPLEMENTATION ====== + +/** @hidden */ +export function _initialize(options: WickedInitOptions, callback?: Callback) { + debug('initialize()'); + if (!callback && (typeof (options) === 'function')) { + callback = options; + options = null; + } + + const func = _initialize; + if (!callback) { + return new Promise(function (resolve, reject) { + func(options, function (err, g) { + err ? reject(err) : resolve(g); + }); + }); + } + + // Reset the storage when (re-)initializing + wickedStorage = _clone(EMPTY_STORAGE); + + if (options) { + debug('options:'); + debug(options); + + if (options.apiMaxTries) { + wickedStorage.apiMaxTries = options.apiMaxTries; + wickedStorage.apiRetryDelay = options.apiRetryDelay; + } + } + + const validationError = validateOptions(options); + if (validationError) { + return callback(validationError); + } + + // I know, this would look a lot nicer with async or Promises, + // but I did not want to pull in additional dependencies. + const apiUrl = resolveApiUrl(); + debug('Awaiting portal API at ' + apiUrl); + _awaitUrl(apiUrl + 'ping', options, function (err, pingResult) { + if (err) { + debug('awaitUrl returned an error:'); + debug(err); + return callback(err); + } + + debug('Ping result:'); + debug(pingResult); + const pingJson = getJson(pingResult); + if (pingJson.version) { + // The version field is not filled until wicked 0.12.0 + wickedStorage.apiVersion = pingJson.version; + wickedStorage.isV012OrHigher = true; + if (pingJson.version >= '1.0.0') { + wickedStorage.isV100OrHigher = true; + } + } + + wickedStorage.apiUrl = apiUrl; + if (options.userAgentName && options.userAgentVersion) + wickedStorage.userAgent = options.userAgentName + '/' + options.userAgentVersion; + request.get({ + url: apiUrl + 'confighash', + timeout: WICKED_TIMEOUT + }, function (err, res, body) { + if (err) { + debug('GET /confighash failed'); + debug(err); + return callback(err); + } + + if (200 != res.statusCode) { + debug('GET /confighash returned status code: ' + res.statusCode); + debug('Body: ' + body); + return callback(new Error('GET /confighash returned unexpected status code: ' + res.statusCode + ' (Body: ' + body + ')')); + } + + wickedStorage.configHash = '' + body; + + request.get({ + url: apiUrl + 'globals', + headers: { + 'User-Agent': wickedStorage.userAgent, + 'X-Config-Hash': wickedStorage.configHash + }, + timeout: WICKED_TIMEOUT + }, function (err, res, body) { + if (err) { + debug('GET /globals failed'); + debug(err); + return callback(err); + } + if (res.statusCode !== 200) { + debug('GET /globals returned status code ' + res.statusCode); + return callback(new Error('GET /globals return unexpected error code: ' + res.statusCode)); + } + + let globals = null; + try { + globals = getJson(body); + wickedStorage.globals = globals; + wickedStorage.initialized = true; + wickedStorage.apiReachable = true; + } catch (ex) { + return callback(new Error('Parsing globals failed: ' + ex.message)); + } + + // Success, set up config hash checker loop (if not switched off) + if (!options.doNotPollConfigHash) { + wickedStorage.isPollingApi = true; + setInterval(checkConfigHash, 10000); + } + + return callback(null, globals); + }); + }); + }); +} + +/** @hidden */ +function _clone(o) { + return JSON.parse(JSON.stringify(o)); +} + +/** @hidden */ +export function validateOptions(options) { + if ((options.userAgentName && !options.userAgentVersion) || + (!options.userAgentName && options.userAgentVersion)) + return new Error('You need to specify both userAgentName and userAgentVersion'); + if (options.userAgentName && + !/^[a-zA-Z\ \-\_\.0-9]+$/.test(options.userAgentName)) + return new Error('The userAgentName must only contain characters a-z, A-Z, 0-9, -, _ and space.'); + if (options.userAgentVersion && + semver.valid(options.userAgentVersion) == null) + return new Error(`The userAgentVersion ${options.userAgentVersion} is not a valid semver (see http://semver.org)`); + return null; +} + +/** @hidden */ +export function validateGetOptions(options: WickedGetOptions): WickedGetOptions { + const o = {} as WickedGetOptions; + if (options) { + if (options.offset) + o.offset = options.offset; + if (options.limit) + o.limit = options.limit; + } + return o; +} + +/** @hidden */ +export function validateGetCollectionOptions(options: WickedGetCollectionOptions): WickedGetCollectionOptions { + const o = {} as WickedGetCollectionOptions; + if (options) { + if (options.filter) + o.filter = options.filter; + if (options.offset) + o.offset = options.offset; + if (options.limit) + o.limit = options.limit; + if (options.order_by) + o.order_by = options.order_by; + if (options.no_cache) + o.no_cache = options.no_cache; + } + return o; +} + +/** @hidden */ +export function checkConfigHash() { + debug('checkConfigHash()'); + + request.get({ + url: wickedStorage.apiUrl + 'confighash', + timeout: WICKED_TIMEOUT + }, function (err, res, body) { + wickedStorage.apiReachable = false; + if (err) { + console.error('checkConfigHash(): An error occurred.'); + console.error(err); + console.error(err.stack); + return; + } + if (200 !== res.statusCode) { + console.error('checkConfigHash(): Returned unexpected status code: ' + res.statusCode); + return; + } + wickedStorage.apiReachable = true; + const configHash = '' + body; + + if (configHash !== wickedStorage.configHash) { + console.log('checkConfigHash() - Detected new configuration version, scheduling shutdown in 2 seconds.'); + wickedStorage.pendingExit = true; + setTimeout(forceExit, 2000); + } + }); +} + +/** @hidden */ +export function forceExit() { + console.log('Exiting component due to outdated configuration (confighash mismatch).'); + process.exit(0); +} + +/** @hidden */ +export function _isApiReachable() { + checkInitialized('isApiReachable'); + if (!wickedStorage.isPollingApi) + throw new Error('isApiReachable() can only be used if wicked-sdk is polling the API continuously (option "doNotPollConfigHash").') + return wickedStorage.apiReachable; +} + +/** @hidden */ +export function _isDevelopmentMode() { + checkInitialized('isDevelopmentMode'); + + if (wickedStorage.globals && + wickedStorage.globals.network && + wickedStorage.globals.network.schema && + wickedStorage.globals.network.schema === 'https') + return false; + return true; +} + +const DEFAULT_AWAIT_OPTIONS = { + statusCode: 200, + maxTries: 100, + retryDelay: 1000 +}; + +/** @hidden */ +export function _awaitUrl(url: string, options: WickedAwaitOptions, callback?: Callback): void | Promise { + debug('awaitUrl(): ' + url); + if (!callback && (typeof (options) === 'function')) { + callback = options; + options = null; + } + + const func = _awaitUrl; + if (!callback) { + return new Promise(function (resolve, reject) { + func(url, options, function (err, result) { + err ? reject(err) : resolve(result); + }); + }); + } + + // Copy the settings from the defaults; otherwise we'd change them haphazardly + const awaitOptions: WickedAwaitOptions = { + statusCode: DEFAULT_AWAIT_OPTIONS.statusCode, + maxTries: DEFAULT_AWAIT_OPTIONS.maxTries, + retryDelay: DEFAULT_AWAIT_OPTIONS.retryDelay + }; + if (options) { + if (options.statusCode) + awaitOptions.statusCode = options.statusCode; + if (options.maxTries) + awaitOptions.maxTries = options.maxTries; + if (options.retryDelay) + awaitOptions.retryDelay = options.retryDelay; + } + + debug('Invoking tryGet()'); + tryGet(url, awaitOptions.statusCode, awaitOptions.maxTries, 0, awaitOptions.retryDelay, function (err, body) { + debug('tryGet() returned.'); + if (err) { + debug('but tryGet() errored.'); + debug(err); + return callback(err); + } + callback(null, body); + }); +} + +/** @hidden */ +export function _awaitKongAdapter(awaitOptions, callback?): void | Promise { + debug('awaitKongAdapter()'); + checkInitialized('awaitKongAdapter'); + if (!callback && (typeof (awaitOptions) === 'function')) { + callback = awaitOptions; + awaitOptions = null; + } + + const func = _awaitKongAdapter; + if (!callback) { + return new Promise(function (resolve, reject) { + func(awaitOptions, function (err, result) { + err ? reject(err) : resolve(result); + }); + }); + } + + if (awaitOptions) { + debug('awaitOptions:'); + debug(awaitOptions); + } + + const adapterPingUrl = _getInternalKongAdapterUrl() + 'ping'; + _awaitUrl(adapterPingUrl, awaitOptions, function (err, body) { + if (err) + return callback(err); + wickedStorage.kongAdapterInitialized = true; + return callback(null, body); + }); +} + +/** @hidden */ +export function _initMachineUser(serviceId: string, callback?: ErrorCallback): void | Promise { + debug('initMachineUser()'); + checkInitialized('initMachineUser'); + + const func = _initMachineUser; + if (!callback) { + return new Promise(function (resolve, reject) { + func(serviceId, function (err) { + err ? reject(err) : resolve(); + }); + }); + } + + retrieveOrCreateMachineUser(serviceId, (err, _) => { + if (err) + return callback(err); + // wickedStorage.machineUserId has been filled now; + // now we want to retrieve the API scopes of portal-api. + return initPortalApiScopes(callback); + }); +} + +/** @hidden */ +export function retrieveOrCreateMachineUser(serviceId: string, callback: Callback) { + debug('retrieveOrCreateMachineUser()'); + if (!/^[a-zA-Z\-_0-9]+$/.test(serviceId)) + return callback(new Error('Invalid Service ID, must only contain a-z, A-Z, 0-9, - and _.')); + + const customId = makeMachineUserCustomId(serviceId); + _apiGet('users?customId=' + qs.escape(customId), null, 'read_users', function (err, userInfo) { + if (err && err.statusCode == 404) { + // Not found + return createMachineUser(serviceId, callback); + } else if (err) { + return callback(err); + } + if (!Array.isArray(userInfo)) + return callback(new Error('GET of user with customId ' + customId + ' did not return expected array.')); + if (userInfo.length !== 1) + return callback(new Error('GET of user with customId ' + customId + ' did not return array of length 1 (length == ' + userInfo.length + ').')); + userInfo = userInfo[0]; // Pick the user from the list. + storeMachineUser(userInfo); + return callback(null, userInfo); + }); +} + +/** @hidden */ +export function storeMachineUser(userInfo) { + debug('Machine user info:'); + debug(userInfo); + debug('Setting machine user id: ' + userInfo.id); + wickedStorage.machineUserId = userInfo.id; +} + +/** @hidden */ +function makeMachineUserCustomId(serviceId) { + const customId = 'internal:' + serviceId; + return customId; +} + +/** @hidden */ +export function createMachineUser(serviceId, callback) { + const customId = makeMachineUserCustomId(serviceId); + const userInfo = { + customId: customId, + firstName: 'Machine-User', + lastName: serviceId, + email: serviceId + '@wicked.haufe.io', + validated: true, + groups: ['admin'] + }; + _apiPost('users/machine', userInfo, null, function (err, userInfo) { + if (err) + return callback(err); + storeMachineUser(userInfo); + return callback(null, userInfo); + }); +} + +/** @hidden */ +function initPortalApiScopes(callback) { + debug('initPortalApiScopes()'); + if (!wickedStorage.machineUserId) + return callback(new Error('initPortalApiScopes: Machine user id not initialized.')); + _apiGet('apis/portal-api', null, 'read_apis', (err, apiInfo) => { + if (err) + return callback(err); + debug(apiInfo); + if (!apiInfo.settings) + return callback(new Error('initPortalApiScope: Property settings not found.')); + if (!apiInfo.settings.scopes) + return callback(new Error('initPortalApiScope: Property settings.scopes not found.')); + const scopeList = []; + for (let scope in apiInfo.settings.scopes) { + scopeList.push(scope); + } + wickedStorage.portalApiScope = scopeList.join(' '); + debug(`initPortalApiScopes: Full API Scope: "${wickedStorage.portalApiScope}"`); + return callback(null); + }); +} + +/** @hidden */ +export function _getGlobals() { + debug('getGlobals()'); + checkInitialized('getGlobals'); + + return wickedStorage.globals; +} + +/** @hidden */ +export function _getConfigHash() { + debug('getConfigHash()'); + checkInitialized('getConfigHash'); + + return wickedStorage.configHash; +} + +/** @hidden */ +export function _getExternalPortalHost() { + debug('getExternalPortalHost()'); + checkInitialized('getExternalPortalHost'); + + return checkNoSlash(getPortalHost()); +} + +/** @hidden */ +export function _getExternalPortalUrl() { + debug('getExternalPortalUrl()'); + checkInitialized('getExternalPortalUrl'); + + return checkSlash(_getSchema() + '://' + getPortalHost()); +} + +/** @hidden */ +export function _getExternalGatewayHost() { + debug('getExternalGatewayHost()'); + checkInitialized('getExternalGatewayHost()'); + + return checkNoSlash(getApiHost()); +} + +/** @hidden */ +export function _getExternalGatewayUrl() { + debug('getExternalGatewayUrl()'); + checkInitialized('getExternalGatewayUrl'); + + return checkSlash(_getSchema() + '://' + getApiHost()); +} + +/** @hidden */ +export function _getInternalApiUrl() { + debug('getInternalApiUrl()'); + checkInitialized('getInternalApiUrl'); + + return checkSlash(wickedStorage.apiUrl); +} + +/** @hidden */ +export function _getPortalApiScope() { + debug('getPortalApiScope()'); + checkInitialized('getPortalApiScope'); + + if (wickedStorage.isV100OrHigher && wickedStorage.portalApiScope) + return wickedStorage.portalApiScope; + debug('WARNING: portalApiScope is not defined, or wicked API is <1.0.0'); + return ''; +} + +/** @hidden */ +export function _getInternalPortalUrl() { + debug('getInternalPortalUrl()'); + checkInitialized('getInternalPortalUrl'); + + return _getInternalUrl('portalUrl', 'portal', 3000); +} + +/** @hidden */ +export function _getInternalKongAdminUrl() { + debug('getInternalKongAdminUrl()'); + checkInitialized('getInternalKongAdminUrl'); + + return _getInternalUrl('kongAdminUrl', 'kong', 8001); +} + +/** @hidden */ +export function _getInternalKongProxyUrl() { + debug('getInternalKongProxyUrl()'); + checkInitialized('getInternalKongProxyUrl'); + + // Check if it's there, but only if the property is present + if (wickedStorage.globals.network && + wickedStorage.globals.network.kongProxyUrl) { + try { + const proxyUrl = _getInternalUrl('kongProxyUrl', 'kong', 8000); + if (proxyUrl && proxyUrl !== '' && proxyUrl !== '/') + return proxyUrl; + } catch (ex) { + debug(ex); + } + } + debug(`globals.json: network.kongProxyUrl is not defined, falling back to admin URL.`) + // Fallback: Deduce from Kong Admin URL + const adminUrl = _getInternalKongAdminUrl(); + return adminUrl.replace(/8001/, '8000'); +} + +/** @hidden */ +export function _getInternalMailerUrl() { + debug('getInternalMailerUrl'); + checkInitialized('getInternalMailerUrl'); + + return _getInternalUrl('mailerUrl', 'portal-mailer', 3003); +} + +/** @hidden */ +export function _getInternalChatbotUrl() { + debug('getInternalChatbotUrl()'); + checkInitialized('getInternalChatbotUrl'); + + return _getInternalUrl('chatbotUrl', 'portal-chatbot', 3004); +} + +/** @hidden */ +export function _getInternalKongAdapterUrl() { + debug('getInternalKongAdapterUrl()'); + checkInitialized('getInternalKongAdapterUrl'); + + return _getInternalUrl('kongAdapterUrl', 'portal-kong-adapter', 3002); +} + +/** @hidden */ +export function _getInternalKongOAuth2Url() { + debug('getInternalKongOAuth2Url()'); + checkInitialized('getInternalKongOAuth2Url'); + + return _getInternalUrl('kongOAuth2Url', 'portal-kong-oauth2', 3006); +} + +/** @hidden */ +export function _getInternalUrl(globalSettingsProperty: string, defaultHost: string, defaultPort: number) { + debug('getInternalUrl("' + globalSettingsProperty + '")'); + checkInitialized('getInternalUrl'); + + if (wickedStorage.globals.network && + wickedStorage.globals.network.hasOwnProperty(globalSettingsProperty)) { + return checkSlash(wickedStorage.globals.network[globalSettingsProperty]); + } + if (defaultHost && defaultPort) + return checkSlash(guessServiceUrl(defaultHost, defaultPort)); + throw new Error('Configuration property "' + globalSettingsProperty + '" not defined in globals.json: network.'); +} + +/** @hidden */ +export function _getKongAdapterIgnoreList(): string[] { + debug('getKongAdapterIgnoreList()'); + checkInitialized('getKongAdapterIgnoreList'); + + const glob = wickedStorage.globals as WickedGlobals; + if (glob.kongAdapter && glob.kongAdapter.useKongAdapter && glob.kongAdapter.ignoreList) { + return glob.kongAdapter.ignoreList; + } + return []; +} + +/** @hidden */ +export function _getApiKeyHeader(): string { + debug('getApiKeyHeader()'); + checkInitialized('getApiKeyHeader'); + + const glob = wickedStorage.globals as WickedGlobals; + if (glob.api && glob.api.headerName) + return glob.api.headerName; + return 'X-ApiKey'; +} + +/** @hidden */ +export function _getPasswordStrategy(): string { + debug('getPasswordStrategy()'); + checkInitialized('getPasswordStrategy()'); + const glob = wickedStorage.globals as WickedGlobals; + if (glob.passwordStrategy) + return glob.passwordStrategy; + return 'PW_6_24'; +} + +// ======= UTILITY FUNCTIONS ====== + +/** @hidden */ +function checkSlash(someUrl) { + if (someUrl.endsWith('/')) + return someUrl; + return someUrl + '/'; +} + +/** @hidden */ +function checkNoSlash(someUrl) { + if (someUrl.endsWith('/')) + return someUrl.substring(0, someUrl.length - 1); + return someUrl; +} + +/** @hidden */ +export function _getSchema() { + checkInitialized('getSchema'); + if (wickedStorage.globals.network && + wickedStorage.globals.network.schema) + return wickedStorage.globals.network.schema; + console.error('In globals.json, network.schema is not defined. Defaulting to https.'); + return 'https'; +} + +/** @hidden */ +export function getPortalHost() { + if (wickedStorage.globals.network && + wickedStorage.globals.network.portalHost) + return wickedStorage.globals.network.portalHost; + throw new Error('In globals.json, portalHost is not defined. Cannot return any default.'); +} + +/** @hidden */ +export function getApiHost() { + if (wickedStorage.globals.network && + wickedStorage.globals.network.apiHost) + return wickedStorage.globals.network.apiHost; + throw new Error('In globals.json, apiHost is not defined. Cannot return any default.'); +} + +/** @hidden */ +export function checkInitialized(callingFunction) { + if (!wickedStorage.initialized) + throw new Error('Before calling ' + callingFunction + '(), initialize() must have been called and has to have returned successfully.'); +} + +/** @hidden */ +export function checkKongAdapterInitialized(callingFunction) { + if (!wickedStorage.kongAdapterInitialized) + throw new Error('Before calling ' + callingFunction + '(), awaitKongAdapter() must have been called and has to have returned successfully.'); +} + +/** @hidden */ +function guessServiceUrl(defaultHost, defaultPort) { + debug('guessServiceUrl() - defaultHost: ' + defaultHost + ', defaultPort: ' + defaultPort); + let url = 'http://' + defaultHost + ':' + defaultPort + '/'; + // Are we not running containerized? Then guess we're in local development mode. + if (!isContainerized) { + const defaultLocalIP = getDefaultLocalIP(); + url = 'http://' + defaultLocalIP + ':' + defaultPort + '/'; + } + debug(url); + return url; +} + +/** @hidden */ +function resolveApiUrl() { + let apiUrl = process.env.PORTAL_API_URL; + if (!apiUrl) { + apiUrl = guessServiceUrl('portal-api', '3001'); + console.error('Environment variable PORTAL_API_URL is not set, defaulting to ' + apiUrl + '. If this is not correct, please set before starting this process.'); + } + if (!apiUrl.endsWith('/')) // Add trailing slash + apiUrl += '/'; + return apiUrl; +} + +/** @hidden */ +function getDefaultLocalIP() { + const localIPs = getLocalIPs(); + if (localIPs.length > 0) + return localIPs[0]; + return "localhost"; +} + +/** @hidden */ +function getLocalIPs() { + debug('getLocalIPs()'); + const interfaces = os.networkInterfaces(); + const addresses = []; + for (let k in interfaces) { + for (let k2 in interfaces[k]) { + const address = interfaces[k][k2]; + if (address.family === 'IPv4' && !address.internal) { + addresses.push(address.address); + } + } + } + debug(addresses); + return addresses; +} + +/** @hidden */ +function tryGet(url, statusCode, maxTries, tryCounter, timeout, callback) { + debug('Try #' + tryCounter + ' to GET ' + url); + request.get({ url: url, timeout: TRYGET_TIMEOUT }, function (err, res, body) { + if (err || res.statusCode !== statusCode) { + if (tryCounter < maxTries || maxTries < 0) + return setTimeout(tryGet, timeout, url, statusCode, maxTries, tryCounter + 1, timeout, callback); + debug('Giving up.'); + if (!err) + err = new Error('Too many unsuccessful retries to GET ' + url + '. Gave up after ' + maxTries + ' tries.'); + return callback(err); + } + callback(null, body); + }); +} + +/** @hidden */ +function getJson(ob) { + if (typeof ob === "string") + return JSON.parse(ob); + return ob; +} + +/** @hidden */ +function getText(ob) { + if (ob instanceof String || typeof ob === "string") + return ob; + return JSON.stringify(ob, null, 2); +} + +/** @hidden */ +export function _apiGet(urlPath, userId, scope, callback) { + debug('apiGet(): ' + urlPath); + checkInitialized('apiGet'); + if (arguments.length !== 4 && arguments.length !== 3) + throw new Error('apiGet was called with wrong number of arguments'); + + return apiAction('GET', urlPath, null, userId, scope, callback); +} + +/** @hidden */ +export function _apiPost(urlPath, postBody, userId, callback) { + debug('apiPost(): ' + urlPath); + checkInitialized('apiPost'); + if (arguments.length !== 4 && arguments.length !== 3) + throw new Error('apiPost was called with wrong number of arguments'); + + return apiAction('POST', urlPath, postBody, userId, null, callback); +} + +/** @hidden */ +export function _apiPut(urlPath, putBody, userId, callback) { + debug('apiPut(): ' + urlPath); + checkInitialized('apiPut'); + if (arguments.length !== 4 && arguments.length !== 3) + throw new Error('apiPut was called with wrong number of arguments'); + + return apiAction('PUT', urlPath, putBody, userId, null, callback); +} + +/** @hidden */ +export function _apiPatch(urlPath, patchBody, userId, callback) { + debug('apiPatch(): ' + urlPath); + checkInitialized('apiPatch'); + if (arguments.length !== 4 && arguments.length !== 3) + throw new Error('apiPatch was called with wrong number of arguments'); + + return apiAction('PATCH', urlPath, patchBody, userId, null, callback); +} + +/** @hidden */ +export function _apiDelete(urlPath, userId, callback) { + debug('apiDelete(): ' + urlPath); + checkInitialized('apiDelete'); + if (arguments.length !== 3 && arguments.length !== 2) + throw new Error('apiDelete was called with wrong number of arguments'); + + return apiAction('DELETE', urlPath, null, userId, null, callback); +} + +/** @hidden */ +function apiAction(method, urlPath, actionBody, userId, scope, callback) { + const func = apiAction; + + if (!callback) { + debug(`apiAction(): Promisifying ${method} ${urlPath}`); + return new Promise(function (resolve, reject) { + func(method, urlPath, actionBody, userId, scope, function (err, result) { + err ? reject(err) : resolve(result) + }); + }); + } + + debug('apiAction(' + method + '): ' + urlPath); + if (arguments.length !== 6) + throw new Error('apiAction called with wrong number of arguments'); + if (typeof (callback) !== 'function') + throw new Error('apiAction: callback is not a function'); + + if (!wickedStorage.apiReachable) + return callback(new Error('The wicked API is currently not reachable. Try again later.')); + // This is not needed anymore: The API accepts the current and the previous config hash now. + // if (wickedStorage.pendingExit) + // return callback(new Error('A shutdown due to changed configuration is pending.')); + + if (!scope) { + if (wickedStorage.portalApiScope) + scope = wickedStorage.portalApiScope; + else + scope = ''; + } + debug(`apiAction: Using scope ${scope}`); + + if (actionBody) + debug(actionBody); + + if (!userId && wickedStorage.machineUserId) { + debug('Picking up machine user id: ' + wickedStorage.machineUserId); + userId = wickedStorage.machineUserId; + } + + if (urlPath.startsWith('/')) + urlPath = urlPath.substring(1); // strip slash in beginning; it's in the API url + + const url = _getInternalApiUrl() + urlPath; + debug(method + ' ' + url); + const reqInfo: RequestBody = { + method: method, + url: url, + timeout: WICKED_TIMEOUT + }; + if (method != 'DELETE' && + method != 'GET') { + // DELETE and GET ain't got no body. + reqInfo.body = actionBody; + reqInfo.json = true; + } + // This is the config hash we saw at init; send it to make sure we don't + // run on an outdated configuration. + reqInfo.headers = { 'X-Config-Hash': wickedStorage.configHash }; + if (userId) { + if (wickedStorage.isV100OrHigher) { + reqInfo.headers['X-Authenticated-UserId'] = `sub=${userId}`; + } else if (wickedStorage.isV012OrHigher) { + reqInfo.headers['X-Authenticated-UserId'] = userId; + } else { + reqInfo.headers['X-UserId'] = userId; + } + } + if (wickedStorage.isV100OrHigher) { + reqInfo.headers['X-Authenticated-Scope'] = scope; + } + if (requestRuntime.correlationId) { + debug('Using correlation id: ' + requestRuntime.correlationId); + reqInfo.headers['Correlation-Id'] = requestRuntime.correlationId; + } + if (wickedStorage.userAgent) { + debug('Using User-Agent: ' + wickedStorage.userAgent); + reqInfo.headers['User-Agent'] = wickedStorage.userAgent; + } + + async.retry({ + tries: wickedStorage.apiMaxTries, + interval: wickedStorage.apiRetryDelay, + errorFilter: function (err) { + // Errors which are not "hard" but have an error code are permanent if they have a non-5xx error code. + if (err.statusCode) { + // In that case, abort the retry flow + if (err.statusCode < 500) + return false; + // Retry on 5xx + return true; + } + // In case of hard errors (such es E_CONN_xxx), continue retrying + return true; + } + }, function (callback) { + debug(`Attempting to ${reqInfo.method} ${reqInfo.url}`); + request(reqInfo, function (err, res, body) { + if (err) + return callback(err); + if (res.statusCode > 299) { + // Looks bad + const err = new WickedError(`api${nice(method)}() ${urlPath} returned non-OK status code: ${res.statusCode}, check err.statusCode and err.body for details`, res.statusCode, body); + return callback(err); + } + if (res.statusCode !== 204) { + const contentType = res.headers['content-type']; + let returnValue = null; + try { + if (contentType.startsWith('text')) + returnValue = getText(body); + else + returnValue = getJson(body); + } catch (ex) { + return callback(new WickedError(`api${nice(method)}() ${urlPath} returned non-parseable JSON: ${ex.message}`, 500, body)); + } + return callback(null, returnValue); + } else { + // Empty response + return callback(null); + } + }); + }, callback); +} + +/** @hidden */ +function nice(methodName) { + return methodName.substring(0, 1) + methodName.substring(1).toLowerCase(); +} + +/** @hidden */ +export function buildUrl(base, queryParams) { + let url = base; + let first = true; + for (let p in queryParams) { + if (first) { + url += '?'; + first = false; + } else { + url += '&'; + } + const v = queryParams[p]; + if (typeof v === 'number') + url += v; + else if (typeof v === 'string') + url += qs.escape(v); + else if (typeof v === 'boolean') + url += v ? 'true' : 'false'; + else // Object or array or whatever + url += qs.escape(JSON.stringify(v)); + } + return url; +} + +/** @hidden */ +export function _getSubscriptionByClientId(clientId: string, apiId: string, asUserId: string, callback?: Callback): void | Promise { + debug('getSubscriptionByClientId()'); + checkInitialized('getSubscriptionByClientId'); + + const func = _getSubscriptionByClientId; + if (!callback) { + return new Promise(function (resolve, reject) { + func(clientId, apiId, asUserId, function (err, result) { + err ? reject(err) : resolve(result) + }); + }); + } + + // Validate format of clientId + if (!/^[a-zA-Z0-9\-]+$/.test(clientId)) { + return callback(new Error('Invalid client_id format.')); + } + + // Check whether we know this client ID, otherwise we won't bother. + _apiGet('subscriptions/' + qs.escape(clientId), asUserId, 'read_subscriptions', function (err, subsInfo) { + if (err) { + debug('GET of susbcription for client_id ' + clientId + ' failed.'); + debug(err); + return callback(new Error('Could not identify application with given client_id.')); + } + debug('subscription info:'); + debug(subsInfo); + if (!subsInfo.subscription) + return callback(new Error('Could not successfully retrieve subscription information.')); + if (subsInfo.subscription.api != apiId) { + debug('subsInfo.api != apiId: ' + subsInfo.subscription.api + ' != ' + apiId); + return callback(new Error('Bad request. The client_id does not match the API.')); + } + debug('Successfully identified application: ' + subsInfo.subscription.application); + + return callback(null, subsInfo); + }); +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..2fcd52c --- /dev/null +++ b/src/index.ts @@ -0,0 +1,1701 @@ +'use strict'; + +/** @hidden */ +const debug = require('debug')('wicked-sdk'); +/** @hidden */ +const qs = require('querystring'); +/** @hidden */ +const uuid = require('node-uuid'); + +import { + WickedInitOptions, + Callback, + WickedGlobals, + ErrorCallback, + WickedAwaitOptions, + WickedApiCollection, + WickedApi, + WickedCollection, + WickedSubscription, + WickedApiPlan, + WickedApiPlanCollection, + WickedGroupCollection, + WickedUserShortInfo, + WickedGetOptions, + WickedUserCreateInfo, + WickedUserInfo, + WickedGetCollectionOptions, + WickedApplicationCreateInfo, + WickedApplication, + WickedApplicationRole, + WickedApplicationRoleType, + WickedSubscriptionPatchInfo, + WickedApproval, + WickedVerification, + WickedComponentHealth, + WickedChatbotTemplates, + WickedEmailTemplateType, + WickedAuthServer, + WickedWebhookListener, + WickedEvent, + WickedPoolMap, + WickedPool, + WickedNamespace, + WickedGetRegistrationOptions, + WickedRegistration, + WickedRegistrationMap, + WickedGrant, + ExpressHandler, + WickedSubscriptionInfo, + WickedSubscriptionCreateInfo, + OidcProfile, + PassthroughScopeRequest, + PassthroughScopeResponse, + WickedSessionStoreType, + WickedApiScopes, + WickedApiSettings, + WickedScopeGrant, + WickedPasswordStrategy, + ExternalRefreshRequest, + ExternalRefreshResponse, + ExternalUserPassRequest, + ExternalUserPassResponse, + ScopeLookupResponse, + WickedSubscriptionScopeModeType, + WickedClientType, +} from "./interfaces"; +export { + WickedInitOptions, + Callback, + WickedGlobals, + ErrorCallback, + WickedAwaitOptions, + WickedApiCollection, + WickedApi, + WickedCollection, + WickedSubscription, + WickedApiPlan, + WickedApiPlanCollection, + WickedGroupCollection, + WickedUserShortInfo, + WickedGetOptions, + WickedUserCreateInfo, + WickedUserInfo, + WickedGetCollectionOptions, + WickedApplicationCreateInfo, + WickedApplication, + WickedApplicationRole, + WickedApplicationRoleType, + WickedSubscriptionPatchInfo, + WickedApproval, + WickedVerification, + WickedComponentHealth, + WickedChatbotTemplates, + WickedEmailTemplateType, + WickedAuthServer, + WickedWebhookListener, + WickedEvent, + WickedPoolMap, + WickedPool, + WickedNamespace, + WickedGetRegistrationOptions, + WickedRegistration, + WickedRegistrationMap, + WickedGrant, + ExpressHandler, + WickedSubscriptionInfo, + WickedSubscriptionCreateInfo, + OidcProfile, + PassthroughScopeRequest, + PassthroughScopeResponse, + WickedSessionStoreType, + WickedApiScopes, + WickedApiSettings, + WickedScopeGrant, + WickedPasswordStrategy, + ExternalRefreshRequest, + ExternalRefreshResponse, + ExternalUserPassRequest, + ExternalUserPassResponse, + ScopeLookupResponse, + WickedSubscriptionScopeModeType, + WickedClientType, +} from "./interfaces"; +export { WickedError } from './wicked-error'; +export { + KongApi, + KongPlugin, + KongService, + KongRoute, + ProtocolType, + KongCollection, + KongConsumer, + KongGlobals, + KongProxyListener, + KongHttpDirective, + KongStatus, + KongPluginCorrelationId, + KongPluginCors, + KongPluginBotDetection, + KongPluginCorrelationIdGeneratorType, + KongPluginOAuth2, + KongPluginRateLimiting, + KongPluginRequestTransformer, + KongPluginResponseTransformer, + KongPluginWhiteBlackList, + KongApiConfig, + KongPluginHmacAuth +} from './kong-interfaces'; +export { + kongServiceRouteToApi, + kongApiToServiceRoute +} from './kong'; + + +/** @hidden */ +import * as implementation from './implementation'; + +// ======= INITIALIZATION ======= + +/** + * Initialize the wicked node SDK. + * + * @param options SDK global options + * @param callback Returns an error or the content of the `globals.json` file (as second argument) + */ +export function initialize(options: WickedInitOptions): Promise; +export function initialize(options: WickedInitOptions, callback: Callback); +export function initialize(options: WickedInitOptions, callback?: Callback) { + return implementation._initialize(options, callback); +} + +/** + * Returns true if the wicked SDK is currently able to reach the wicked API. + */ +export function isApiReachable(): boolean { + return implementation._isApiReachable(); +} + +/** + * Returns true if the system is in "development mode". This usually means that transport is not secure + * (not via https). + */ +export function isDevelopmentMode(): boolean { + return implementation._isDevelopmentMode(); +}; + +/** + * Create a machine administrator user for a given service. This method can be used to get "backdoor" + * access to the wicked API on behalf of a machine user. If you call this method, the machine user ID + * will be stored internally in the SDK and will be used for any API calls using the SDK. + * + * @param serviceId A unique service ID for the service to create a machine user for + * @param callback Returns `null` if succeeded, or an error. + */ +export function initMachineUser(serviceId: string): Promise; +export function initMachineUser(serviceId: string, callback: ErrorCallback); +export function initMachineUser(serviceId: string, callback?: ErrorCallback) { + return implementation._initMachineUser(serviceId, callback); +}; + +/** + * Awaits return code `200` for the specified URL. + * + * @param url The URL to wait for to return `200` + * @param options Await options (see interface) + * @param callback Returns `null` plus the returned content of the URL, or an error + */ +export function awaitUrl(url: string, options: WickedAwaitOptions): Promise; +export function awaitUrl(url: string, options: WickedAwaitOptions, callback: Callback); +export function awaitUrl(url: string, options: WickedAwaitOptions, callback?: Callback) { + return implementation._awaitUrl(url, options, callback); +}; + +/** + * Convenience function to make sure the Kong Adapter is up and running. + * + * @param awaitOptions Await options + * @param callback Returns `null` plus the returned content of the URL, or an error + */ +export function awaitKongAdapter(awaitOptions: WickedAwaitOptions): Promise; +export function awaitKongAdapter(awaitOptions: WickedAwaitOptions, callback: Callback); +export function awaitKongAdapter(awaitOptions: WickedAwaitOptions, callback?: Callback) { + return implementation._awaitKongAdapter(awaitOptions, callback); +}; + +// ======= INFORMATION RETRIEVAL ======= + +/** + * Returns the content of the `globals.json` file, with resolved environment variables, if applicable. + */ +export function getGlobals(): WickedGlobals { + return implementation._getGlobals(); +}; + +/** + * Returns the current hash of the static configuration. This is used to check whether the static + * configuration has changed, and if so, decide to restart/stop components like the mailer or the kong + * adapter. + */ +export function getConfigHash(): string { + return implementation._getConfigHash(); +}; + +/** + * Return the currently configured user facing schema (`http` or `https`). This information is contained + * in the `globals.json`. + */ +export function getSchema(): string { + return implementation._getSchema(); +}; + +/** + * Returns the external portal host for the currently configured environment, e.g. `developer.mycompany.com` + */ +export function getExternalPortalHost(): string { + return implementation._getExternalPortalHost(); +}; + +/** + * Returns the complete URL to the wicked portal UI, as seen from outside the deployment, e.g. `https://developer.mycompany.com` + */ +export function getExternalPortalUrl(): string { + return implementation._getExternalPortalUrl(); +}; + +/** + * Returns the host name for the API Gateway for the currently configured environment, e.g. `api.mycompany.com` + */ +export function getExternalApiHost(): string { + return implementation._getExternalGatewayHost(); +}; + +/** + * Returns the complete base URL to the API Gateway, as seen from outside the deployment. E.g., `https://api.mycompany.com` + */ +export function getExternalApiUrl(): string { + return implementation._getExternalGatewayUrl(); +}; + +/** + * Returns the URL to the wicked API, as seen from *inside* the deployment + */ +export function getInternalApiUrl(): string { + return implementation._getInternalApiUrl(); +}; + +/** + * Returns the full scope to the wicked API (all scope strings, space separated). + */ +export function getPortalApiScope(): string { + return implementation._getPortalApiScope(); +}; + +/** + * Returns the full URL to the portal UI instance, e.g. `http://portal:3000`, as seen from inside the deployment. + */ +export function getInternalPortalUrl(): string { + return implementation._getInternalPortalUrl(); +} + +/** + * Returns the full URL to the admin port of the Kong instance(s), as seen from inside the deployment. E.g., `http://kong:8001`. + */ +export function getInternalKongAdminUrl(): string { + return implementation._getInternalKongAdminUrl(); +}; + +/** + * Returns the full URL to the proxy port of the Kong instance(s), as seen from inside the deployment. E.g., `http://kong:8000`. + */ +export function getInternalKongProxyUrl(): string { + return implementation._getInternalKongProxyUrl(); +}; + +/** + * Returns the full URL to the Kong Adapter, as seen from inside the deployment, e.g. `http://portal-kong-adapter:3002` + */ +export function getInternalKongAdapterUrl(): string { + return implementation._getInternalKongAdapterUrl(); +}; + +/** + * Returns the full URL to the chatbot, as seen from inside the deployment, e.g. `http://portal-chatbot:3004` + */ +export function getInternalChatbotUrl(): string { + return implementation._getInternalChatbotUrl(); +}; + +/** + * Returns the full URL to the mailer, as seen from inside the deployment, e.g. `http://portal-mailer:3003` + */ +export function getInternalMailerUrl(): string { + return implementation._getInternalMailerUrl(); +}; + +export function getInternalUrl(globalSettingsProperty: string): string { + return implementation._getInternalUrl(globalSettingsProperty, null, 0); +}; + +/** + * Returns the list of Kong plugins which the Kong Adapter will not touch. + */ +export function getKongAdapterIgnoreList(): string[] { + return implementation._getKongAdapterIgnoreList(); +} + +/** + * Returns the header name to use for key auth purposes. Defaults to `X-ApiKey`. + */ +export function getApiKeyHeader(): string { + return implementation._getApiKeyHeader(); +} + +/** + * Returns the selected password validation strategy identifier. + */ +export function getPasswordStrategy(): string { + return implementation._getPasswordStrategy(); +} + +// ======= API FUNCTIONALITY ======= + +/** + * General purpose `GET` operation on the wicked API; you do not use this directly usually, but use one of + * the dedicated SDK functions. + * + * @param urlPath relative URL path + * @param userIdOrCallback user ID to perform the `GET` operation as, or `callback` + * @param callback Callback containing an `err` (or `null` if success) and the `GET` returned content. + */ +export function apiGet(urlPath: string, userIdOrCallback, callback) { + let userId = userIdOrCallback; + if (!callback && typeof (userIdOrCallback) === 'function') { + callback = userIdOrCallback; + userId = null; + } + return implementation._apiGet(urlPath, userId, null, callback); +}; + +/** + * General purpose `POST` operation on the wicked API; you do not use this directly usually, but use one of + * the dedicated SDK functions. + * + * @param urlPath relative URL path + * @param postBody Body to post + * @param userIdOrCallback user ID to perform the `GET` operation as, or `callback` + * @param callback Callback containing an `err` (or `null` if success) and the `GET` returned content. + */ +export function apiPost(urlPath: string, postBody: object, userIdOrCallback, callback) { + let userId = userIdOrCallback; + if (!callback && typeof (userIdOrCallback) === 'function') { + callback = userIdOrCallback; + userId = null; + } + return implementation._apiPost(urlPath, postBody, userId, callback); +}; + +/** + * General purpose `PUT` operation on the wicked API; you do not use this directly usually, but use one of + * the dedicated SDK functions. + * + * @param urlPath relative URL path + * @param putBody Body to post + * @param userIdOrCallback user ID to perform the `GET` operation as, or `callback` + * @param callback Callback containing an `err` (or `null` if success) and the `GET` returned content. + */ +export function apiPut(urlPath: string, putBody: object, userIdOrCallback, callback) { + let userId = userIdOrCallback; + if (!callback && typeof (userIdOrCallback) === 'function') { + callback = userIdOrCallback; + userId = null; + } + return implementation._apiPut(urlPath, putBody, userId, callback); +}; + +/** + * General purpose `PATCH` operation on the wicked API; you do not use this directly usually, but use one of + * the dedicated SDK functions. + * + * @param urlPath relative URL path + * @param putBody Body to patch + * @param userIdOrCallback user ID to perform the `GET` operation as, or `callback` + * @param callback Callback containing an `err` (or `null` if success) and the `GET` returned content. + */ +export function apiPatch(urlPath: string, patchBody: object, userIdOrCallback, callback) { + let userId = userIdOrCallback; + if (!callback && typeof (userIdOrCallback) === 'function') { + callback = userIdOrCallback; + userId = null; + } + return implementation._apiPatch(urlPath, patchBody, userId, callback); +}; + +/** + * General purpose `DELETE` operation on the wicked API; you do not use this directly usually, but use one of + * the dedicated SDK functions. + * + * @param urlPath relative URL path + * @param userIdOrCallback user ID to perform the `GET` operation as, or `callback` + * @param callback Callback containing an `err` (or `null` if success) and the `GET` returned content. + */ +export function apiDelete(urlPath: string, userIdOrCallback, callback) { + let userId = userIdOrCallback; + if (!callback && typeof (userIdOrCallback) === 'function') { + callback = userIdOrCallback; + userId = null; + } + return implementation._apiDelete(urlPath, userId, callback); +}; + +// ======= API CONVENIENCE FUNCTIONS ======= + +// APIS + +/** + * Returns a collection of API definitions (corresponds to the `apis.json`). + * @param callback + * @category APIs + */ +export function getApis(): Promise; +export function getApis(callback: Callback); +export function getApis(callback?: Callback) { + return getApisAs(null, callback); +} + +export function getApisAs(asUserId: string): Promise; +export function getApisAs(asUserId: string, callback: Callback); +export function getApisAs(asUserId: string, callback?: Callback) { + return apiGet('apis', asUserId, callback); +} + +/** + * Return the generic APIs description (for all APIs). Returns markdown code. + * @param callback + */ +export function getApisDescription(): Promise; +export function getApisDescription(callback: Callback); +export function getApisDescription(callback?: Callback) { + return getApisDescriptionAs(null, callback); +} + +export function getApisDescriptionAs(asUserId: string): Promise; +export function getApisDescriptionAs(asUserId: string, callback: Callback); +export function getApisDescriptionAs(asUserId: string, callback?: Callback) { + return apiGet(`apis/desc`, asUserId, callback); +} + +/** + * Returns the API definition for a specific API. + * + * @param apiId The id of the API to retrieve + * @param callback + */ +export function getApi(apiId: string): Promise; +export function getApi(apiId: string, callback: Callback); +export function getApi(apiId: string, callback?: Callback) { + return getApiAs(apiId, null, callback); +} + +export function getApiAs(apiId: string, asUserId: string): Promise; +export function getApiAs(apiId: string, asUserId: string, callback: Callback); +export function getApiAs(apiId: string, asUserId: string, callback?: Callback) { + return apiGet(`apis/${apiId}`, asUserId, callback); +} + +/** + * Retrieve the (markdown) API description of a specific API. + * + * @param apiId The id of the API to retrieve the description for + * @param callback + */ +export function getApiDescription(apiId: string): Promise; +export function getApiDescription(apiId: string, callback: Callback); +export function getApiDescription(apiId: string, callback?: Callback) { + return getApiDescriptionAs(apiId, null, callback); +} + +export function getApiDescriptionAs(apiId: string, asUserId: string): Promise; +export function getApiDescriptionAs(apiId: string, asUserId: string, callback: Callback); +export function getApiDescriptionAs(apiId: string, asUserId: string, callback?: Callback) { + return apiGet(`apis/${apiId}/desc`, asUserId, callback); +} + +/** + * Retrieve the API specific Kong configuration for a specific API. + * + * @param apiId The id of the API to retrieve the Kong config for + * @param callback + */ +export function getApiConfig(apiId: string): Promise; +export function getApiConfig(apiId: string, callback: Callback); +export function getApiConfig(apiId: string, callback?: Callback) { + return getApiConfigAs(apiId, null, callback); +} + +export function getApiConfigAs(apiId: string, asUserId: string): Promise; +export function getApiConfigAs(apiId: string, asUserId: string, callback: Callback); +export function getApiConfigAs(apiId: string, asUserId: string, callback?: Callback) { + return apiGet(`apis/${apiId}/config`, asUserId, callback); +} + +/** + * Retrieve a JSON representation of the Swagger information for a specific API; contains authorization information (injected). + * + * @param apiId The id of the API to retrieve the Swagger JSON for + * @param callback + */ +export function getApiSwagger(apiId: string): Promise; +export function getApiSwagger(apiId: string, callback: Callback); +export function getApiSwagger(apiId: string, callback?: Callback) { + return getApiSwaggerAs(apiId, null, callback); +} + +export function getApiSwaggerAs(apiId: string, asUserId: string): Promise; +export function getApiSwaggerAs(apiId: string, asUserId: string, callback: Callback); +export function getApiSwaggerAs(apiId: string, asUserId: string, callback?: Callback) { + return apiGet(`apis/${apiId}/swagger`, asUserId, callback); +} + +/** + * Retrieve a list of subscriptions to a specific API. + * + * @param apiId The id of the API to retrieve subscriptions for. + * @param callback + */ +export function getApiSubscriptions(apiId: string): Promise>; +export function getApiSubscriptions(apiId: string, callback: Callback>); +export function getApiSubscriptions(apiId: string, callback?: Callback>) { + return getApiSubscriptionsAs(apiId, null, callback); +} + +export function getApiSubscriptionsAs(apiId: string, asUserId: string): Promise>; +export function getApiSubscriptionsAs(apiId: string, asUserId: string, callback: Callback>); +export function getApiSubscriptionsAs(apiId: string, asUserId: string, callback?: Callback>) { + return apiGet(`apis/${apiId}/subscriptions`, asUserId, callback); +} + +// PLANS + +/** + * Retrieve a list of API Plans for a specific API. + * + * @param apiId The id of the API to retrieve the associated plans for + * @param callback + */ +export function getApiPlans(apiId: string): Promise; +export function getApiPlans(apiId: string, callback: Callback); +export function getApiPlans(apiId: string, callback?: Callback) { + return getApiPlansAs(apiId, null, callback); +} + +export function getApiPlansAs(apiId: string, asUserId: string): Promise; +export function getApiPlansAs(apiId: string, asUserId: string, callback: Callback); +export function getApiPlansAs(apiId: string, asUserId: string, callback?: Callback) { + return apiGet(`apis/${apiId}/plans`, asUserId, callback); +} + +/** + * Return a collection of all API plans, disregarding their association with APIs or not. This is an open + * endpoint, so there is no `As` alternative. + * @param callback + */ +export function getPlans(): Promise; +export function getPlans(callback: Callback); +export function getPlans(callback?: Callback) { + return apiGet('plans', null, callback); +} + +// GROUPS + +/** + * Retrieve a collection of all wicked user groups. This is an open + * endpoint, so there is no `As` alternative. + * @param callback + */ +export function getGroups(): Promise; +export function getGroups(callback: Callback); +export function getGroups(callback?: Callback) { + return apiGet('groups', null, callback); +} + +// USERS + +/** + * Retrieves user short info by custom id. + * + * @param customId The custom id of the user to retrieve + * @param callback + */ +export function getUserByCustomId(customId: string): Promise; +export function getUserByCustomId(customId: string, callback: Callback); +export function getUserByCustomId(customId: string, callback?: Callback) { + return apiGet(`users?customId=${qs.escape(customId)}`, null, callback); +} + +/** + * Retrieves user short info by email address. + * + * @param email The email address of the user to retrieve + * @param callback + */ +export function getUserByEmail(email: string): Promise; +export function getUserByEmail(email: string, callback: Callback); +export function getUserByEmail(email: string, callback?: Callback) { + return apiGet(`users?email=${qs.escape(email)}`, null, callback); +} + +/** + * Retrieve list of users matching the given options. Chances are good you will rather want to use + * getRegistrations(). + * + * @param options Collection get options + * @param callback + */ +export function getUsers(options: WickedGetOptions): Promise; +export function getUsers(options: WickedGetOptions, callback: Callback); +export function getUsers(options: WickedGetOptions, callback?: Callback) { + return getUsersAs(options, null, callback); +} + +export function getUsersAs(options: WickedGetOptions, asUserId: string): Promise; +export function getUsersAs(options: WickedGetOptions, asUserId: string, callback: Callback); +export function getUsersAs(options: WickedGetOptions, asUserId: string, callback?: Callback) { + let o = implementation.validateGetOptions(options); + let url = implementation.buildUrl('users', o); + return apiGet(url, asUserId, callback); +} + +/** + * Creates a new user from the given information. Returns a user information object also containing + * the new internal ID of the user. + * + * @param userCreateInfo The basic user info needed to create a user + * @param callback + */ +export function createUser(userCreateInfo: WickedUserCreateInfo): Promise; +export function createUser(userCreateInfo: WickedUserCreateInfo, callback: Callback); +export function createUser(userCreateInfo: WickedUserCreateInfo, callback?: Callback) { + return createUserAs(userCreateInfo, null, callback); +} + +export function createUserAs(userCreateInfo: WickedUserCreateInfo, asUserId: string): Promise; +export function createUserAs(userCreateInfo: WickedUserCreateInfo, asUserId: string, callback: Callback); +export function createUserAs(userCreateInfo: WickedUserCreateInfo, asUserId: string, callback?: Callback) { + return apiPost('users', userCreateInfo, asUserId, callback); +} + +/** + * Patches a user. Returns the updated user information. + * + * @param userPatchInfo The information of the user to update (password, groups...) + * @param callback + */ +export function patchUser(userId: string, userPatchInfo: WickedUserCreateInfo): Promise; +export function patchUser(userId: string, userPatchInfo: WickedUserCreateInfo, callback: Callback); +export function patchUser(userId: string, userPatchInfo: WickedUserCreateInfo, callback?: Callback) { + return patchUserAs(userId, userPatchInfo, null, callback); +} + +export function patchUserAs(userId: string, userPatchInfo: WickedUserCreateInfo, asUserId: string): Promise; +export function patchUserAs(userId: string, userPatchInfo: WickedUserCreateInfo, asUserId: string, callback: Callback); +export function patchUserAs(userId: string, userPatchInfo: WickedUserCreateInfo, asUserId: string, callback?: Callback) { + return apiPatch(`users/${userId}`, userPatchInfo, asUserId, callback); +} + +/** + * Deletes a user. This function will only succeed if the user does not have any associated applications. + * If the user has applications, these have to be deleted or re-owned first. + * + * @param userId ID of user to delete + * @param callback + */ +export function deleteUser(userId: string): Promise; +export function deleteUser(userId: string, callback: ErrorCallback); +export function deleteUser(userId: string, callback?: ErrorCallback) { + return deleteUserAs(userId, null, callback); +} + +export function deleteUserAs(userId: string, asUserId: string): Promise; +export function deleteUserAs(userId: string, asUserId: string, callback: ErrorCallback); +export function deleteUserAs(userId: string, asUserId: string, callback?: ErrorCallback) { + return apiDelete(`users/${userId}`, asUserId, callback); +} + +/** + * Retrieves user information for a specific user. + * + * @param userId ID of user to retrieve + * @param callback + */ +export function getUser(userId: string): Promise; +export function getUser(userId: string, callback: Callback): void; +export function getUser(userId: string, callback?: Callback) { + return getUserAs(userId, null, callback); +} + +export function getUserAs(userId: string, asUserId: string): Promise; +export function getUserAs(userId: string, asUserId: string, callback: Callback); +export function getUserAs(userId: string, asUserId: string, callback?: Callback) { + return apiGet(`users/${userId}`, asUserId, callback); +} + +/** + * Special function which deletes the password for a specific user; this user will no longer be able to + * log in using username and password anymore. + * + * @param userId ID of user to delete password for. + * @param callback + */ +export function deleteUserPassword(userId: string): Promise; +export function deleteUserPassword(userId: string, callback: ErrorCallback); +export function deleteUserPassword(userId: string, callback?: ErrorCallback) { + return deleteUserPasswordAs(userId, null, callback); +} + +export function deleteUserPasswordAs(userId: string, asUserId: string): Promise; +export function deleteUserPasswordAs(userId: string, asUserId: string, callback: ErrorCallback); +export function deleteUserPasswordAs(userId: string, asUserId: string, callback?: ErrorCallback) { + return apiDelete(`users/${userId}/password`, asUserId, callback); +} + +// APPLICATIONS + +/** + * Retrieves all registered wicked applications. + * + * @param options Get options (filtering, paging) + * @param callback + */ +export function getApplications(options: WickedGetCollectionOptions): Promise>; +export function getApplications(options: WickedGetCollectionOptions, callback: Callback>); +export function getApplications(options: WickedGetCollectionOptions, callback?: Callback>) { + return getApplicationsAs(options, null, callback); +} + +export function getApplicationsAs(options: WickedGetCollectionOptions, asUserId: string): Promise>; +export function getApplicationsAs(options: WickedGetCollectionOptions, asUserId: string, callback: Callback>); +export function getApplicationsAs(options: WickedGetCollectionOptions, asUserId: string, callback?: Callback>) { + const o = implementation.validateGetCollectionOptions(options); + const url = implementation.buildUrl('applications', o); + return apiGet(url, asUserId, callback); +} + +/** + * Creates a new wicked application based on the given information. Please note that the `clientType` takes precedence over + * the `confidential` property. Using only `clientType` is recommended. If none is passed in, `clientType` defaults to `public_spa`, + * which is the least secure option. + * + * @param appCreateInfo Application information for new application + * @param callback + */ +export function createApplication(appCreateInfo: WickedApplicationCreateInfo): Promise; +export function createApplication(appCreateInfo: WickedApplicationCreateInfo, callback: Callback); +export function createApplication(appCreateInfo: WickedApplicationCreateInfo, callback?: Callback) { + return createApplicationAs(appCreateInfo, null, callback); +} + +export function createApplicationAs(appCreateInfo: WickedApplicationCreateInfo, asUserId: string): Promise; +export function createApplicationAs(appCreateInfo: WickedApplicationCreateInfo, asUserId: string, callback: Callback); +export function createApplicationAs(appCreateInfo: WickedApplicationCreateInfo, asUserId: string, callback?: Callback) { + return apiPost('applications', appCreateInfo, asUserId, callback); +} + +/** + * Retrieves the list of (predefined) application roles. + * + * @param callback + */ +export function getApplicationRoles(): Promise; +export function getApplicationRoles(callback: Callback); +export function getApplicationRoles(callback?: Callback) { + return apiGet('applications/roles', null, callback); +} + +/** + * Retrieve information on the given application. + * + * @param appId ID of application to retrieve + * @param callback + */ +export function getApplication(appId: string): Promise; +export function getApplication(appId: string, callback: Callback); +export function getApplication(appId: string, callback?: Callback) { + return getApplicationAs(appId, null, callback); +} + +export function getApplicationAs(appId: string, asUserId: string): Promise; +export function getApplicationAs(appId: string, asUserId: string, callback: Callback); +export function getApplicationAs(appId: string, asUserId: string, callback?: Callback) { + return apiGet(`applications/${appId}`, asUserId, callback); +} + +/** + * Patch an application, e.g. change it's name, redirect URL or `clientType`. + * + * @param appId ID of application to patch + * @param appPatchInfo Patch body + * @param callback + */ +export function patchApplication(appId: string, appPatchInfo: WickedApplicationCreateInfo): Promise; +export function patchApplication(appId: string, appPatchInfo: WickedApplicationCreateInfo, callback: Callback); +export function patchApplication(appId: string, appPatchInfo: WickedApplicationCreateInfo, callback?: Callback) { + return patchApplicationAs(appId, appPatchInfo, null, callback); +} + +export function patchApplicationAs(appId: string, appPatchInfo: WickedApplicationCreateInfo, asUserId: string): Promise; +export function patchApplicationAs(appId: string, appPatchInfo: WickedApplicationCreateInfo, asUserId: string, callback: Callback); +export function patchApplicationAs(appId: string, appPatchInfo: WickedApplicationCreateInfo, asUserId: string, callback?: Callback) { + return apiPatch(`applications/${appId}`, appPatchInfo, asUserId, callback); +} + +/** + * Delete an application entirely. + * + * @param appId ID of application to delete + * @param callback + */ +export function deleteApplication(appId: string): Promise; +export function deleteApplication(appId: string, callback: ErrorCallback); +export function deleteApplication(appId: string, callback?: ErrorCallback) { + return deleteApplicationAs(appId, null, callback); +} + +export function deleteApplicationAs(appId: string, asUserId): Promise; +export function deleteApplicationAs(appId: string, asUserId: string, callback: ErrorCallback); +export function deleteApplicationAs(appId: string, asUserId: string, callback?: ErrorCallback) { + return apiDelete(`applications/${appId}`, asUserId, callback); +} + +/** + * Add an owner to a specific application. + * + * @param appId ID of application to add an owner for + * @param email Email address of additional owner + * @param role The role of the additional owner + * @param callback + */ +export function addApplicationOwner(appId: string, email: string, role: WickedApplicationRoleType): Promise; +export function addApplicationOwner(appId: string, email: string, role: WickedApplicationRoleType, callback: Callback); +export function addApplicationOwner(appId: string, email: string, role: WickedApplicationRoleType, callback?: Callback) { + return addApplicationOwnerAs(appId, email, role, null, callback); +} + +export function addApplicationOwnerAs(appId: string, email: string, role: WickedApplicationRoleType, asUserId: string): Promise; +export function addApplicationOwnerAs(appId: string, email: string, role: WickedApplicationRoleType, asUserId: string, callback: Callback); +export function addApplicationOwnerAs(appId: string, email: string, role: WickedApplicationRoleType, asUserId: string, callback?: Callback) { + const body = { + email: email, + role: role + }; + return apiPost(`applications/${appId}/owners`, body, asUserId, callback); +} + +/** + * Delete an owner from an application. + * + * @param appId ID of application to delete the owner from + * @param email Email address of owner to delete from application + * @param callback + */ +export function deleteApplicationOwner(appId: string, email: string): Promise; +export function deleteApplicationOwner(appId: string, email: string, callback: Callback); +export function deleteApplicationOwner(appId: string, email: string, callback?: Callback) { + return deleteApplicationOwnerAs(appId, email, null, callback); +} + +export function deleteApplicationOwnerAs(appId: string, email: string, asUserId: string): Promise; +export function deleteApplicationOwnerAs(appId: string, email: string, asUserId: string, callback: Callback); +export function deleteApplicationOwnerAs(appId: string, email: string, asUserId: string, callback?: Callback) { + return apiDelete(`applications/${appId}/owners?email=${qs.escape(email)}`, asUserId, callback); +} + +// SUBSCRIPTIONS + +/** + * Retrieve all API subscriptions for a specific application. + * + * @param appId ID of application to retrieve subscriptions for + * @param callback + */ +export function getSubscriptions(appId: string): Promise; +export function getSubscriptions(appId: string, callback: Callback); +export function getSubscriptions(appId: string, callback?: Callback) { + return getSubscriptionsAs(appId, null, callback); +} + +export function getSubscriptionsAs(appId: string, asUserId: string): Promise; +export function getSubscriptionsAs(appId: string, asUserId: string, callback: Callback); +export function getSubscriptionsAs(appId: string, asUserId: string, callback?: Callback) { + return apiGet(`applications/${appId}/subscriptions`, asUserId, callback); +} + +/** + * Retrieve subscription information for an application based on an OAuth2 client ID and a given API. + * + * @param clientId OAuth2 client ID of application + * @param apiId ID of API + * @param callback + */ +export function getSubscriptionByClientId(clientId: string, apiId: string): Promise; +export function getSubscriptionByClientId(clientId: string, apiId: string, callback: Callback); +export function getSubscriptionByClientId(clientId: string, apiId: string, callback?: Callback) { + return getSubscriptionByClientIdAs(clientId, apiId, null, callback); +} + +export function getSubscriptionByClientIdAs(clientId: string, apiId: string, asUserId: string): Promise; +export function getSubscriptionByClientIdAs(clientId: string, apiId: string, asUserId: string, callback: Callback); +export function getSubscriptionByClientIdAs(clientId: string, apiId: string, asUserId: string, callback?: Callback) { + return implementation._getSubscriptionByClientId(clientId, apiId, asUserId, callback); +} + +/** + * Create a new API subscription for an application. + * + * @param appId ID of application to create a subscription for + * @param subsCreateInfo Subscription create info (see type) + * @param callback + */ +export function createSubscription(appId: string, subsCreateInfo: WickedSubscriptionCreateInfo): Promise; +export function createSubscription(appId: string, subsCreateInfo: WickedSubscriptionCreateInfo, callback: Callback); +export function createSubscription(appId: string, subsCreateInfo: WickedSubscriptionCreateInfo, callback?: Callback) { + return createSubscriptionAs(appId, subsCreateInfo, null, callback); +} + +export function createSubscriptionAs(appId: string, subsCreateInfo: WickedSubscriptionCreateInfo, asUserId: string): Promise; +export function createSubscriptionAs(appId: string, subsCreateInfo: WickedSubscriptionCreateInfo, asUserId: string, callback: Callback); +export function createSubscriptionAs(appId: string, subsCreateInfo: WickedSubscriptionCreateInfo, asUserId: string, callback?: Callback) { + return apiPost(`applications/${appId}/subscriptions`, subsCreateInfo, asUserId, callback); +} + +/** + * Retrieve a specific application API subscription. + * + * @param appId ID of application to retrieve subscription for + * @param apiId ID of API to which the subscription applies + * @param callback + */ +export function getSubscription(appId: string, apiId: string): Promise; +export function getSubscription(appId: string, apiId: string, callback: Callback); +export function getSubscription(appId: string, apiId: string, callback?: Callback) { + return getSubscriptionAs(appId, apiId, null, callback); +} + +export function getSubscriptionAs(appId: string, apiId: string, asUserId: string): Promise; +export function getSubscriptionAs(appId: string, apiId: string, asUserId: string, callback: Callback); +export function getSubscriptionAs(appId: string, apiId: string, asUserId: string, callback?: Callback) { + return apiGet(`applications/${appId}/subscriptions/${apiId}`, asUserId, callback); +} + +/** + * Patch a subscription. This function is only used for approval workflows: Use this + * to patch the subscription to be approved. + * + * @param appId ID of application of which to patch the subscription + * @param apiId ID of API + * @param patchInfo Patch information (see type) + * @param callback + */ +export function patchSubscription(appId: string, apiId: string, patchInfo: WickedSubscriptionPatchInfo): Promise; +export function patchSubscription(appId: string, apiId: string, patchInfo: WickedSubscriptionPatchInfo, callback: Callback); +export function patchSubscription(appId: string, apiId: string, patchInfo: WickedSubscriptionPatchInfo, callback?: Callback) { + return patchSubscriptionAs(appId, apiId, patchInfo, null, callback); +} + +export function patchSubscriptionAs(appId: string, apiId: string, patchInfo: WickedSubscriptionPatchInfo, asUserId: string): Promise; +export function patchSubscriptionAs(appId: string, apiId: string, patchInfo: WickedSubscriptionPatchInfo, asUserId: string, callback: Callback); +export function patchSubscriptionAs(appId: string, apiId: string, patchInfo: WickedSubscriptionPatchInfo, asUserId: string, callback?: Callback) { + return apiPatch(`applications/${appId}/subscriptions/${apiId}`, patchInfo, asUserId, callback); +} + +/** + * Deletes a subscription to an API for an application. + * + * @param appId ID of application to delete the subscription for + * @param apiId ID of API to delete subscription for + */ +export function deleteSubscription(appId: string, apiId: string): Promise; +export function deleteSubscription(appId: string, apiId: string, callback: ErrorCallback); +export function deleteSubscription(appId: string, apiId: string, callback?: ErrorCallback) { + return deleteSubscriptionAs(appId, apiId, null, callback); +} + +export function deleteSubscriptionAs(appId: string, apiId: string, asUserId: string): Promise; +export function deleteSubscriptionAs(appId: string, apiId: string, asUserId: string, callback: ErrorCallback); +export function deleteSubscriptionAs(appId: string, apiId: string, asUserId: string, callback?: ErrorCallback) { + return apiDelete(`applications/${appId}/subscriptions/${apiId}`, asUserId, callback); +} + +// APPROVALS + +/** + * Retrieve a list of all pending subscription approvals. + * + * @param callback + */ +export function getApprovals(): Promise; +export function getApprovals(callback: Callback); +export function getApprovals(callback?: Callback) { + return getApprovalsAs(null, callback); +} + +export function getApprovalsAs(asUserId: string): Promise; +export function getApprovalsAs(asUserId: string, callback: Callback); +export function getApprovalsAs(asUserId: string, callback?: Callback) { + return apiGet('approvals', asUserId, callback); +} + +/** + * Retrieve a specific approval request by ID. + * + * @param approvalId ID of approval to retrieve + * @param callback + */ +export function getApproval(approvalId: string): Promise; +export function getApproval(approvalId: string, callback: Callback); +export function getApproval(approvalId: string, callback?: Callback) { + return getApprovalAs(approvalId, null, callback); +} + +export function getApprovalAs(approvalId: string, asUserId: string): Promise; +export function getApprovalAs(approvalId: string, asUserId: string, callback: Callback); +export function getApprovalAs(approvalId: string, asUserId: string, callback?: Callback) { + return apiGet(`approvals/${approvalId}`, asUserId, callback); +} + +// VERIFICATIONS + +/** + * Creates a verification record; depending on the type of the verification record, this may trigger + * certain workflows, such as the "lost password" or "verify email address" workflow, given that the + * wicked mailer is correctly configured and deployed. + * + * @param verification Verification information to create a verification record for + * @param callback + */ +export function createVerification(verification: WickedVerification): Promise; +export function createVerification(verification: WickedVerification, callback: ErrorCallback); +export function createVerification(verification: WickedVerification, callback?: ErrorCallback) { + return createVerificationAs(verification, null, callback); +} + +export function createVerificationAs(verification: WickedVerification, asUserId: string): Promise; +export function createVerificationAs(verification: WickedVerification, asUserId: string, callback: ErrorCallback); +export function createVerificationAs(verification: WickedVerification, asUserId: string, callback?: ErrorCallback) { + return apiPost('verifications', verification, asUserId, callback); +} + +/** + * Retrieve all pending verifications. + * + * @param callback + */ +export function getVerifications(): Promise; +export function getVerifications(callback: Callback); +export function getVerifications(callback?: Callback) { + return getVerificationsAs(null, callback); +} + +export function getVerificationsAs(asUserId: string): Promise; +export function getVerificationsAs(asUserId: string, callback: Callback); +export function getVerificationsAs(asUserId: string, callback?: Callback) { + return apiGet('verificaations', asUserId, callback); +} + +/** + * Retrieve a specific verification by its ID. + * + * @param verificationId ID of verification to retrieve. + * @param callback + */ +export function getVerification(verificationId: string): Promise; +export function getVerification(verificationId: string, callback: Callback); +export function getVerification(verificationId: string, callback?: Callback) { + return getVerificationAs(verificationId, null, callback); +} + +export function getVerificationAs(verificationId: string, asUserId: string): Promise; +export function getVerificationAs(verificationId: string, asUserId: string, callback: Callback); +export function getVerificationAs(verificationId: string, asUserId: string, callback?: Callback) { + return apiGet(`verifications/${verificationId}`, asUserId, callback); +} + +/** + * Delete a verification by ID. + * + * @param verificationId ID of verification to delete. + * @param callback + */ +export function deleteVerification(verificationId: string): Promise; +export function deleteVerification(verificationId: string, callback: ErrorCallback); +export function deleteVerification(verificationId: string, callback?: ErrorCallback) { + return deleteVerificationAs(verificationId, null, callback); +} + +export function deleteVerificationAs(verificationId: string, asUserId: string): Promise; +export function deleteVerificationAs(verificationId: string, asUserId: string, callback: ErrorCallback); +export function deleteVerificationAs(verificationId: string, asUserId: string, callback?: ErrorCallback) { + return apiDelete(`verifications/${verificationId}`, asUserId, callback); +} + +// SYSTEM HEALTH + +export function getSystemHealth(): Promise; +export function getSystemHealth(callback: Callback); +export function getSystemHealth(callback?: Callback) { + return getSystemHealthAs(null, callback); +} + +export function getSystemHealthAs(asUserId: string): Promise; +export function getSystemHealthAs(asUserId: string, callback: Callback); +export function getSystemHealthAs(asUserId: string, callback?: Callback) { + return apiGet('systemhealth', asUserId, callback); +} + +// TEMPLATES + +export function getChatbotTemplates(): Promise; +export function getChatbotTemplates(callback: Callback); +export function getChatbotTemplates(callback?: Callback) { + return getChatbotTemplatesAs(null, callback); +} + +export function getChatbotTemplatesAs(asUserId: string): Promise; +export function getChatbotTemplatesAs(asUserId: string, callback: Callback); +export function getChatbotTemplatesAs(asUserId: string, callback?: Callback) { + return apiGet('templates/chatbot', asUserId, callback); +} + +export function getEmailTemplate(templateId: WickedEmailTemplateType): Promise; +export function getEmailTemplate(templateId: WickedEmailTemplateType, callback: Callback); +export function getEmailTemplate(templateId: WickedEmailTemplateType, callback?: Callback) { + return getEmailTemplateAs(templateId, null, callback); +} + +export function getEmailTemplateAs(templateId: WickedEmailTemplateType, asUserId: string): Promise; +export function getEmailTemplateAs(templateId: WickedEmailTemplateType, asUserId: string, callback: Callback); +export function getEmailTemplateAs(templateId: WickedEmailTemplateType, asUserId: string, callback?: Callback) { + return apiGet(`templates/email/${templateId}`, asUserId, callback); +} + +// AUTH-SERVERS + +/** + * Retrieve a string list of registered authorization servers. This just returns a list of names, to + * get further information, use getAuthServer(). + * @param callback + */ +export function getAuthServerNames(): Promise; +export function getAuthServerNames(callback: Callback); +export function getAuthServerNames(callback?: Callback) { + return getAuthServerNamesAs(null, callback); +} + +export function getAuthServerNamesAs(asUserId: string): Promise; +export function getAuthServerNamesAs(asUserId: string, callback: Callback); +export function getAuthServerNamesAs(asUserId: string, callback?: Callback) { + return apiGet('auth-servers', asUserId, callback); +} + +/** + * Retrieve information on a specific authorization server. + * + * @param serverId ID of authorization server to retrieve information on. + * @param callback + */ +export function getAuthServer(serverId: string): Promise; +export function getAuthServer(serverId: string, callback: Callback); +export function getAuthServer(serverId: string, callback?: Callback) { + return getAuthServerAs(serverId, null, callback); +} + +export function getAuthServerAs(serverId: string, asUserId: string): Promise; +export function getAuthServerAs(serverId: string, asUserId: string, callback: Callback); +export function getAuthServerAs(serverId: string, asUserId: string, callback?: Callback) { + return apiGet(`auth-servers/${serverId}`, asUserId, callback); +} + +// WEBHOOKS + +/** + * Retrieve a list of all currently registered webhook listeners. + * + * @param callback + */ +export function getWebhookListeners(): Promise; +export function getWebhookListeners(callback: Callback); +export function getWebhookListeners(callback?: Callback) { + return getWebhookListenersAs(null, callback); +} + +export function getWebhookListenersAs(asUserId: string): Promise; +export function getWebhookListenersAs(asUserId: string, callback: Callback); +export function getWebhookListenersAs(asUserId: string, callback?: Callback) { + return apiGet('webhooks/listeners', asUserId, callback); +} + +/** + * Insert or update data of a specific webhook listener. After upserting the information of + * a new webhook listener, the wicked API will start to accumulate events for this webhook + * listener. These events can be retrieved using `getWebhookEvents` and deleted via + * `deleteWebhookEvents`. + * + * @param listenerId ID of listener to insert or update + * @param listener Data of listener to insert or update + * @param callback + */ +export function upsertWebhookListener(listenerId: string, listener: WickedWebhookListener): Promise; +export function upsertWebhookListener(listenerId: string, listener: WickedWebhookListener, callback: ErrorCallback); +export function upsertWebhookListener(listenerId: string, listener: WickedWebhookListener, callback?: ErrorCallback) { + return upsertWebhookListenerAs(listenerId, listener, null, callback); +} + +export function upsertWebhookListenerAs(listenerId: string, listener: WickedWebhookListener, asUserId: string): Promise; +export function upsertWebhookListenerAs(listenerId: string, listener: WickedWebhookListener, asUserId: string, callback: ErrorCallback); +export function upsertWebhookListenerAs(listenerId: string, listener: WickedWebhookListener, asUserId: string, callback?: ErrorCallback) { + return apiPut(`webhooks/listeners/${listenerId}`, listener, asUserId, callback); +} + +/** + * Delete a specific webhook listener. + * + * @param listenerId ID of webhook listener to delete + * @param callback + */ +export function deleteWebhookListener(listenerId: string): Promise; +export function deleteWebhookListener(listenerId: string, callback: ErrorCallback); +export function deleteWebhookListener(listenerId: string, callback?: ErrorCallback) { + return deleteWebhookListenerAs(listenerId, null, callback); +} + +export function deleteWebhookListenerAs(listenerId: string, asUserId: string): Promise; +export function deleteWebhookListenerAs(listenerId: string, asUserId: string, callback: ErrorCallback); +export function deleteWebhookListenerAs(listenerId: string, asUserId: string, callback?: ErrorCallback) { + return apiDelete(`webhooks/listeners/${listenerId}`, asUserId, callback); +} + +/** + * Retrieve all pending webhook events for a specific webhook listener. This operation is idempotent. + * To delete the webhook events, subsequently call deleteWebhookEvent. + * + * @param listenerId ID of webhook listener to retrieve pending events for + * @param callback + */ +export function getWebhookEvents(listenerId: string): Promise; +export function getWebhookEvents(listenerId: string, callback: Callback); +export function getWebhookEvents(listenerId: string, callback?: Callback) { + return getWebhookEventsAs(listenerId, null, callback); +} + +export function getWebhookEventsAs(listenerId: string, asUserId: string): Promise; +export function getWebhookEventsAs(listenerId: string, asUserId: string, callback: Callback); +export function getWebhookEventsAs(listenerId: string, asUserId: string, callback?: Callback) { + return apiGet(`webhooks/events/${listenerId}`, asUserId, callback); +} + +/** + * Flush/delete all pending webhook events for a specific webhook listener. + * + * @param listenerId ID of webhook listener to flush all events for. + * @param callback + */ +export function flushWebhookEvents(listenerId: string): Promise; +export function flushWebhookEvents(listenerId: string, callback: ErrorCallback); +export function flushWebhookEvents(listenerId: string, callback?: ErrorCallback) { + return flushWebhookEventsAs(listenerId, null, callback); +} + +export function flushWebhookEventsAs(listenerId: string, asUserId: string): Promise; +export function flushWebhookEventsAs(listenerId: string, asUserId: string, callback: ErrorCallback); +export function flushWebhookEventsAs(listenerId: string, asUserId: string, callback?: ErrorCallback) { + return apiDelete(`webhooks/events/${listenerId}`, asUserId, callback); +} + +/** + * Delete a specific webhook event for a specific webhook listener from the event queue. + * + * @param listenerId ID of webhook listener to delete an event for + * @param eventId ID of event to delete + * @param callback + */ +export function deleteWebhookEvent(listenerId: string, eventId: string): Promise; +export function deleteWebhookEvent(listenerId: string, eventId: string, callback: ErrorCallback); +export function deleteWebhookEvent(listenerId: string, eventId: string, callback?: ErrorCallback) { + return deleteWebhookEventAs(listenerId, eventId, null, callback); +} + +export function deleteWebhookEventAs(listenerId: string, eventId: string, asUserId: string): Promise; +export function deleteWebhookEventAs(listenerId: string, eventId: string, asUserId: string, callback: ErrorCallback); +export function deleteWebhookEventAs(listenerId: string, eventId: string, asUserId: string, callback?: ErrorCallback) { + return apiDelete(`webhooks/events/${listenerId}/${eventId}`, asUserId, callback); +} + +// REGISTRATION POOLS + +/** + * Retrieve a map of registration pools and registration pool information. + * + * @param callback + */ +export function getRegistrationPools(): Promise; +export function getRegistrationPools(callback: Callback); +export function getRegistrationPools(callback?: Callback) { + return getRegistrationPoolsAs(null, callback); +} + +export function getRegistrationPoolsAs(asUserId: string): Promise; +export function getRegistrationPoolsAs(asUserId: string, callback: Callback); +export function getRegistrationPoolsAs(asUserId: string, callback?: Callback) { + return apiGet('pools', asUserId, callback); +} + +/** + * Retrieve information on a specific registration pool. + * + * @param poolId ID of pool to retrieve information on + * @param callback + */ +export function getRegistrationPool(poolId: string): Promise; +export function getRegistrationPool(poolId: string, callback: Callback); +export function getRegistrationPool(poolId: string, callback?: Callback) { + return getRegistrationPoolAs(poolId, null, callback); +} + +export function getRegistrationPoolAs(poolId: string, asUserId: string): Promise; +export function getRegistrationPoolAs(poolId: string, asUserId: string, callback: Callback); +export function getRegistrationPoolAs(poolId: string, asUserId: string, callback?: Callback) { + return apiGet(`pools/${poolId}`, asUserId, callback); +} + +// NAMESPACES + +/** + * Retrieve a collection of namespaces for a given registration pool (`poolId`). **Note**: The registration pool + * must have the `requireNamespace` option set for the namespace functions to be valid to call. + * + * @param poolId ID of pool to retrieve namespaces for + * @param options Get retrieval options (paging, filtering) + * @param callback + */ +export function getPoolNamespaces(poolId: string, options: WickedGetCollectionOptions): Promise>; +export function getPoolNamespaces(poolId: string, options: WickedGetCollectionOptions, callback: Callback>); +export function getPoolNamespaces(poolId: string, options: WickedGetCollectionOptions, callback?: Callback>) { + return getPoolNamespacesAs(poolId, options, null, callback); +} + +export function getPoolNamespacesAs(poolId: string, options: WickedGetCollectionOptions, asUserId: string): Promise>; +export function getPoolNamespacesAs(poolId: string, options: WickedGetCollectionOptions, asUserId: string, callback: Callback>); +export function getPoolNamespacesAs(poolId: string, options: WickedGetCollectionOptions, asUserId: string, callback?: Callback>) { + const o = implementation.validateGetCollectionOptions(options); + const url = implementation.buildUrl(`pools/${poolId}/namespaces`, options); + return apiGet(url, asUserId, callback); +} + +/** + * Retrieve information on a specific namespace of a specific registration pool. Namespaces are usually + * mapped to things like "tenants", so the description of a namespace can be a tenant name or similar. + * + * @param poolId ID of pool to retrieve a namespace for + * @param namespaceId ID of namespace to retrieve + * @param callback + */ +export function getPoolNamespace(poolId: string, namespaceId: string): Promise; +export function getPoolNamespace(poolId: string, namespaceId: string, callback: Callback); +export function getPoolNamespace(poolId: string, namespaceId: string, callback?: Callback) { + return getPoolNamespaceAs(poolId, namespaceId, null, callback); +} + +export function getPoolNamespaceAs(poolId: string, namespaceId: string, asUserId: string): Promise; +export function getPoolNamespaceAs(poolId: string, namespaceId: string, asUserId: string, callback: Callback); +export function getPoolNamespaceAs(poolId: string, namespaceId: string, asUserId: string, callback?: Callback) { + return apiGet(`pools/${poolId}/namespaces/${namespaceId}`, asUserId, callback); +} + +/** + * Upsert a namespace in a specific registration pool. In order to create registrations for a specific + * namespace, this function has to have been called for the namespace which is to be used. + * + * @param poolId ID of pool to which the namespace to upsert belongs + * @param namespaceId Id of namespace to upsert + * @param namespaceInfo New namespace data to store for this namespace + * @param callback + */ +export function upsertPoolNamespace(poolId: string, namespaceId: string, namespaceInfo: WickedNamespace): Promise; +export function upsertPoolNamespace(poolId: string, namespaceId: string, namespaceInfo: WickedNamespace, callback: ErrorCallback); +export function upsertPoolNamespace(poolId: string, namespaceId: string, namespaceInfo: WickedNamespace, callback?: ErrorCallback) { + return upsertPoolNamespaceAs(poolId, namespaceId, namespaceInfo, null, callback); +} + +export function upsertPoolNamespaceAs(poolId: string, namespaceId: string, namespaceInfo: WickedNamespace, asUserId: string): Promise; +export function upsertPoolNamespaceAs(poolId: string, namespaceId: string, namespaceInfo: WickedNamespace, asUserId: string, callback: ErrorCallback); +export function upsertPoolNamespaceAs(poolId: string, namespaceId: string, namespaceInfo: WickedNamespace, asUserId: string, callback?: ErrorCallback) { + return apiPut(`pools/${poolId}/namespaces/${namespaceId}`, namespaceInfo, asUserId, callback); +} + +/** + * Delete a registration pool namespace. Subsequently, it cannot be used to create or enumerate + * registrations. + * + * @param poolId ID of pool to which the namespace to delete belongs + * @param namespaceId ID of namespace to delete + * @param callback + */ +export function deletePoolNamespace(poolId: string, namespaceId: string): Promise; +export function deletePoolNamespace(poolId: string, namespaceId: string, callback: ErrorCallback); +export function deletePoolNamespace(poolId: string, namespaceId: string, callback?: ErrorCallback) { + return deletePoolNamespaceAs(poolId, namespaceId, null, callback); +} + +export function deletePoolNamespaceAs(poolId: string, namespaceId: string, asUserId: string): Promise; +export function deletePoolNamespaceAs(poolId: string, namespaceId: string, asUserId: string, callback: ErrorCallback); +export function deletePoolNamespaceAs(poolId: string, namespaceId: string, asUserId: string, callback?: ErrorCallback) { + return apiDelete(`pools/${poolId}/namespaces/${namespaceId}`, asUserId, callback); +} + +// REGISTRATIONS + +/** + * Retrieve all registrations for a specific registration pool; use the `namespace` filtering inside the `options` + * parameter to retrieve registrations for specific namespaces. Please note that the `namespace` option is required + * for registration pools which requires namespaces, and is forbidden for registration pools which do not require + * namespaces. + * + * @param poolId ID of registration pool to retrieve registrations for + * @param options Get options, e.g. namespace filtering, generic filtering and paging + * @param callback + */ +export function getPoolRegistrations(poolId: string, options: WickedGetRegistrationOptions): Promise>; +export function getPoolRegistrations(poolId: string, options: WickedGetRegistrationOptions, callback: Callback>); +export function getPoolRegistrations(poolId: string, options: WickedGetRegistrationOptions, callback?: Callback>) { + return getPoolRegistrationsAs(poolId, options, null, callback); +} + +export function getPoolRegistrationsAs(poolId: string, options: WickedGetRegistrationOptions, asUserId: string): Promise>; +export function getPoolRegistrationsAs(poolId: string, options: WickedGetRegistrationOptions, asUserId: string, callback: Callback>); +export function getPoolRegistrationsAs(poolId: string, options: WickedGetRegistrationOptions, asUserId: string, callback?: Callback>) { + const o = implementation.validateGetCollectionOptions(options) as WickedGetRegistrationOptions; + if (options.namespace) + o.namespace = options.namespace; + const url = implementation.buildUrl(`registrations/pools/${poolId}`, o); + return apiGet(url, asUserId, callback); +} + +/** + * Retrieve a collection of user registrations for a specific registration pool id. This can + * be a collection of 0 or more registration objects; it's valid for a user to have multiple + * registrations for a single registration pool in case the registration pool requires + * namespaces (but only one registration per namespace). In case the registration pool does not + * require/support namespaces, the result will be an array of eiher 0 or 1 elements. + * + * @param poolId ID of pool for which to retrieve a user's registrations + * @param userId ID of user to retrieve registrations for + * @param callback + */ +export function getUserRegistrations(poolId: string, userId: string): Promise>; +export function getUserRegistrations(poolId: string, userId: string, callback: Callback>); +export function getUserRegistrations(poolId: string, userId: string, callback?: Callback>) { + return getUserRegistrationsAs(poolId, userId, null, callback); +} + +export function getUserRegistrationsAs(poolId: string, userId: string, asUserId: string): Promise>; +export function getUserRegistrationsAs(poolId: string, userId: string, asUserId: string, callback: Callback>); +export function getUserRegistrationsAs(poolId: string, userId: string, asUserId: string, callback?: Callback>) { + return apiGet(`registrations/pools/${poolId}/users/${userId}`, asUserId, callback); +} + +/** + * Upsert a user registration. Note that if the registration pool requires the use of namespaces + * the `userRegistration` object **must** contain a `namespace` property. Vice versa, if the registration + * pool does not require/support namespaces, the `userRegistration` object must **not** contain + * a `namespace` property. + * + * @param poolId ID of pool to upsert a user registration for + * @param userId ID of user to upsert a registration for + * @param userRegistration User registration data. + * @param callback + */ +export function upsertUserRegistration(poolId: string, userId: string, userRegistration: WickedRegistration): Promise; +export function upsertUserRegistration(poolId: string, userId: string, userRegistration: WickedRegistration, callback: ErrorCallback); +export function upsertUserRegistration(poolId: string, userId: string, userRegistration: WickedRegistration, callback?: ErrorCallback) { + return upsertUserRegistrationAs(poolId, userId, userRegistration, null, callback); +} + +export function upsertUserRegistrationAs(poolId: string, userId: string, userRegistration: WickedRegistration, asUserId: string): Promise; +export function upsertUserRegistrationAs(poolId: string, userId: string, userRegistration: WickedRegistration, asUserId: string, callback: ErrorCallback); +export function upsertUserRegistrationAs(poolId: string, userId: string, userRegistration: WickedRegistration, asUserId: string, callback?: ErrorCallback) { + return apiPut(`registrations/pools/${poolId}/users/${userId}`, userRegistration, asUserId, callback); +} + +/** + * Delete a specific user registration for a given registration pool (and optionally namespace). + * + * @param poolId ID of registration pool to delete a user registration from + * @param userId ID of user to delete a registration for + * @param namespaceId Namespace to delete registration for; for registration pools not requiring a namespace, this must be `null`, otherwise it must be specified + * @param callback + */ +export function deleteUserRegistration(poolId: string, userId: string, namespaceId: string): Promise; +export function deleteUserRegistration(poolId: string, userId: string, namespaceId: string, callback: ErrorCallback); +export function deleteUserRegistration(poolId: string, userId: string, namespaceId: string, callback?: ErrorCallback) { + return deleteUserRegistrationAs(poolId, userId, namespaceId, null, callback); +} + +export function deleteUserRegistrationAs(poolId: string, userId: string, namespaceId: string, asUserId: string): Promise; +export function deleteUserRegistrationAs(poolId: string, userId: string, namespaceId: string, asUserId: string, callback: ErrorCallback); +export function deleteUserRegistrationAs(poolId: string, userId: string, namespaceId: string, asUserId: string, callback?: ErrorCallback) { + const o = {} as any; + if (namespaceId) + o.namespace = namespaceId; + const url = implementation.buildUrl(`registrations/pools/${poolId}/users/${userId}`, o); + return apiDelete(url, asUserId, callback); +} + +/** + * Retrieve a map of all registrations, across all registration pools, a user has. + * + * @param userId ID of user to retrieve all registrations for. + * @param callback + */ +export function getAllUserRegistrations(userId: string): Promise; +export function getAllUserRegistrations(userId: string, callback: Callback); +export function getAllUserRegistrations(userId: string, callback?: Callback) { + return getAllUserRegistrationsAs(userId, null, callback); +} + +export function getAllUserRegistrationsAs(userId: string, asUserId: string): Promise; +export function getAllUserRegistrationsAs(userId: string, asUserId: string, callback: Callback); +export function getAllUserRegistrationsAs(userId: string, asUserId: string, callback?: Callback) { + return apiGet(`registrations/users/${userId}`, asUserId, callback); +} + +// GRANTS + +/** + * Retrieve all grants a user has allowed to any application for accessing any API. + * + * @param userId ID of user to retrieve grants for + * @param options Get options (filtering, paging,...) + * @param callback + */ +export function getUserGrants(userId: string, options: WickedGetOptions): Promise>; +export function getUserGrants(userId: string, options: WickedGetOptions, callback: Callback>); +export function getUserGrants(userId: string, options: WickedGetOptions, callback?: Callback>) { + return getUserGrantsAs(userId, options, null, callback); +} + +export function getUserGrantsAs(userId: string, options: WickedGetOptions, asUserId: string): Promise>; +export function getUserGrantsAs(userId: string, options: WickedGetOptions, asUserId: string, callback: Callback>); +export function getUserGrantsAs(userId: string, options: WickedGetOptions, asUserId: string, callback?: Callback>) { + const o = implementation.validateGetOptions(options); + const url = implementation.buildUrl(`grants/${userId}`, o); + return apiGet(url, asUserId, callback); +} + +/** + * Delete all grants a user has made to any application to access APIs on behalf of himself. After calling this + * method, any non-trusted application will need to ask permission to the user again to access the user's data on + * behalf of the user. + * + * @param userId ID of user to delete all grants for. + * @param callback + */ +export function deleteAllUserGrants(userId: string): Promise; +export function deleteAllUserGrants(userId: string, callback: ErrorCallback); +export function deleteAllUserGrants(userId: string, callback?: ErrorCallback) { + return deleteAllUserGrantsAs(userId, null, callback); +} + +export function deleteAllUserGrantsAs(userId: string, asUserId: string): Promise; +export function deleteAllUserGrantsAs(userId: string, asUserId: string, callback: ErrorCallback); +export function deleteAllUserGrantsAs(userId: string, asUserId: string, callback?: ErrorCallback) { + return apiDelete(`grants/${userId}`, asUserId, callback); +} + +/** + * Retrieve a specific access grant for a specific, user, application and API. + * + * @param userId ID of user to retrieve a grant for + * @param applicationId ID of application to retrieve a grant for + * @param apiId ID of API for which to retrieve the grant + * @param callback + */ +export function getUserGrant(userId: string, applicationId: string, apiId: string): Promise; +export function getUserGrant(userId: string, applicationId: string, apiId: string, callback: Callback); +export function getUserGrant(userId: string, applicationId: string, apiId: string, callback?: Callback) { + return getUserGrantAs(userId, applicationId, apiId, null, callback); +} + +export function getUserGrantAs(userId: string, applicationId: string, apiId: string, asUserId: string): Promise; +export function getUserGrantAs(userId: string, applicationId: string, apiId: string, asUserId: string, callback: Callback); +export function getUserGrantAs(userId: string, applicationId: string, apiId: string, asUserId: string, callback?: Callback) { + return apiGet(`grants/${userId}/applications/${applicationId}/apis/${apiId}`, asUserId, callback); +} + +/** + * Upsert a grant information for a user, so that a given application can access the given API + * with a specific set of scopes on the user's behalf. This method is foremost used automatically + * by the Authorization Server after it has asked the user whether a certain application is allowed + * to access the user's data on the user's behalf. + * + * @param userId ID of user to upsert a grant for + * @param applicationId ID of application to upsert a grant for + * @param apiId ID of API to upsert a grant for + * @param grantInfo Grant information to store + * @param callback + */ +export function upsertUserGrant(userId: string, applicationId: string, apiId: string, grantInfo: WickedGrant): Promise; +export function upsertUserGrant(userId: string, applicationId: string, apiId: string, grantInfo: WickedGrant, callback: ErrorCallback); +export function upsertUserGrant(userId: string, applicationId: string, apiId: string, grantInfo: WickedGrant, callback?: ErrorCallback) { + return upsertUserGrantAs(userId, applicationId, apiId, grantInfo, null, callback); +} + +export function upsertUserGrantAs(userId: string, applicationId: string, apiId: string, grantInfo: WickedGrant, asUserId: string): Promise; +export function upsertUserGrantAs(userId: string, applicationId: string, apiId: string, grantInfo: WickedGrant, asUserId: string, callback: ErrorCallback); +export function upsertUserGrantAs(userId: string, applicationId: string, apiId: string, grantInfo: WickedGrant, asUserId: string, callback?: ErrorCallback) { + return apiPut(`grants/${userId}/applications/${applicationId}/apis/${apiId}`, grantInfo, asUserId, callback); +} + +/** + * Delete a user's grant of access to a specific application and API. + * + * @param userId ID of user of which to delete a grant + * @param applicationId ID of application of which to delete a grant + * @param apiId ID of API to delete a grant for + * @param callback + */ +export function deleteUserGrant(userId: string, applicationId: string, apiId: string): Promise; +export function deleteUserGrant(userId: string, applicationId: string, apiId: string, callback: ErrorCallback); +export function deleteUserGrant(userId: string, applicationId: string, apiId: string, callback?: ErrorCallback) { + return deleteUserGrantAs(userId, applicationId, apiId, null, callback); +} + +export function deleteUserGrantAs(userId: string, applicationId: string, apiId: string, asUserId: string): Promise; +export function deleteUserGrantAs(userId: string, applicationId: string, apiId: string, asUserId: string, callback: ErrorCallback); +export function deleteUserGrantAs(userId: string, applicationId: string, apiId: string, asUserId: string, callback?: ErrorCallback) { + return apiDelete(`grants/${userId}/applications/${applicationId}/apis/${apiId}`, asUserId, callback); +} + +// ======= CORRELATION ID HANDLER ======= + +/** + * Express middleware implementation of a correlation ID handler; it inserts + * a header `Correlation-Id` if it's not already present and passes it on to the + * wicked API. In case a header is already present, it re-uses the content. The + * usual format of the correlation ID is a UUID. + * + * Usage: `app.use(wicked.correlationIdHandler());` + */ +export function correlationIdHandler(): ExpressHandler { + return function (req, res, next) { + const correlationId = req.get('correlation-id'); + if (correlationId) { + debug('Picking up correlation id: ' + correlationId); + req.correlationId = correlationId; + } else { + req.correlationId = uuid.v4(); + debug('Creating a new correlation id: ' + req.correlationId); + } + implementation.requestRuntime.correlationId = correlationId; + return next(); + }; +} diff --git a/src/interfaces.ts b/src/interfaces.ts new file mode 100644 index 0000000..a7396fb --- /dev/null +++ b/src/interfaces.ts @@ -0,0 +1,837 @@ +'use strict'; + +import { KongPlugin, KongApi, KongApiConfig } from "./kong-interfaces"; + + +// ==================== +// WICKED TYPES +// ==================== + +/** + * Options defining how wicked awaits an URL. + */ +export interface WickedAwaitOptions { + /** The expected status code */ + statusCode?: number, + /** Maximum number of retries until wicked gives up */ + maxTries?: number, + /** Delay between retries (ms) */ + retryDelay?: number +} + +/** + * Initialization options for the wicked SDK. + */ +export interface WickedInitOptions extends WickedAwaitOptions { + /** A user agent name; do not use the `wicked` prefix, otherwise a strict version check is enforced. */ + userAgentName: string, + /** The version of your user agent, must be a valid SemVer (see http://semver.org). */ + userAgentVersion: string, + /** Defaults to `false`; if `false`, the wicked SDK will poll the `/confighash` end point of the wicked API to check for updated static configuration; if such is detected, the SDK will force quit the component to make the assumed orchestrator restart it. */ + doNotPollConfigHash?: boolean, + /** Retries before failing: The number of retries the SDK does before failing when accessing the wicked API. Defaults to 10. */ + apiMaxTries?: number, + /** Retry interval in ms; if an API call is failing, the SDK will retry after a certain amount of time again. Defaults to 500ms. */ + apiRetryDelay?: number +} + +export interface WickedGlobals { + /** The wicked static config version (written by the Kickstarter, don't change this) */ + version: number, + title: string, + footer: string, + /** This contains the NODE_ENV of the API container */ + environment: string, + /** Selected password strategy identifier; use wicked SDK to get a list of supported strategies */ + passwordStrategy: string, + company: string, + /** Group validated users are automatically assigned to */ + validatedUsergGroup?: string, + /** Used to validate that the secret config key is correct */ + configKeyCheck: string, + api?: WickedGlobalsApi + network: WickedGlobalsNetwork, + db: WickedGlobalsDb, + + sessionStore: WickedSessionStoreConfig, + kongAdapter?: WickedKongAdapterConfig, + portal: WickedPortalConfig, + storage: WickedStorageConfig, + + initialUsers: WickedGlobalsInitialUser[], + recaptcha: WickedRecaptchaConfig + mailer: WickedMailerConfig + chatbot: WickedChatbotConfig, + layouts?: WickedLayoutConfig + views?: WickedViewsConfig, +} + +export interface WickedPasswordStrategy { + /** Identifier of the strategy */ + strategy: string, + /** Description of the strategy */ + description: string, + /** Regex string of the password strategy, for use with `new RegExp()` */ + regex: string +} + +export interface WickedStorageConfig { + type: WickedStorageType + pgHost?: string + pgPort?: number, + pgUser?: string, + pgPassword?: string +} + +export enum WickedStorageType { + JSON = 'json', + Postgres = 'postgres' +} + +export interface WickedPortalConfig { + /** + * Array of allowed auth methods for the portal login; in the form + * `:`. + * + * Example: `["default:local", "default:google"]` + */ + authMethods: string[] +} + +export interface WickedKongAdapterConfig { + useKongAdapter: boolean, + /** List of Kong plugins which the Kong Adapter doesn't touch when configuring Kong */ + ignoreList: string[] +} + +export interface WickedSessionStoreConfig { + type: WickedSessionStoreType + host?: string, + port?: number, + password?: string +} + +export enum WickedSessionStoreType { + Redis = 'redis', + File = 'file' +} + +export interface WickedViewsConfig { + apis: { + showApiIcon: boolean, + titleTagline: string + }, + applications: { + titleTagline: string + }, + application: { + titleTagline: string + } +} + +export interface WickedLayoutConfig { + defautRootUrl: string, + defautRootUrlTarget: string, + defautRootUrlText: null, + menu: { + homeLinkText: string, + apisLinkVisibleToGuest: boolean, + applicationsLinkVisibleToGuest: boolean, + contactLinkVisibleToGuest: boolean, + contentLinkVisibleToGuest: boolean, + classForLoginSignupPosition: string, + showSignupLink: boolean, + loginLinkText: string + }, + footer: { + showBuiltBy: boolean, + showBuilds: boolean + }, + swaggerUi: { + menu: { + homeLinkText: string, + showContactLink: boolean, + showContentLink: boolean + } + } +} + +export interface WickedChatbotConfig { + username: string, + icon_url: string, + hookUrls: string[], + events: WickedChatbotEventsConfig +} + +export interface WickedChatbotEventsConfig { + userSignedUp: boolean, + userValidatedEmail: boolean, + applicationAdded: boolean, + applicationDeleted: boolean, + subscriptionAdded: boolean, + subscriptionDeleted: boolean, + approvalRequired: boolean, + lostPasswordRequest: boolean, + verifyEmailRequest: boolean +} + +export interface WickedMailerConfig { + senderEmail: string, + senderName: string, + smtpHost: string, + smtpPort?: number, + username?: string, + password?: string, + adminEmail: string, + adminName: string +} + +export interface WickedRecaptchaConfig { + useRecaptcha: boolean, + websiteKey: string, + secretKey: string +} + +export interface WickedGlobalsApi { + headerName: string, + /** Required user group for subscribing to the portal-api internal API */ + apiUserGroup?: string, + /** Required user group for subscribing to the echo internal API */ + echoUserGroup?: string +} + +export interface WickedGlobalsNetwork { + schema: string, + portalHost: string, + apiHost: string, + apiUrl: string, + portalUrl: string, + kongAdapterUrl: string, + kongAdminUrl: string, + kongProxyUrl?: string, + mailerUrl: string, + chatbotUrl: string +} + +export interface WickedGlobalsDb { + staticConfig: string, + dynamicConfig?: string +} + +export interface WickedGlobalsInitialUser { + id: string, + customId?: string, + name: string + email: string, + password?: string, + validated?: boolean, + groups: string[] +} + +export interface WickedUserShortInfo { + id: string, + customId?: string, + email: string, +} + +export interface WickedUserCreateInfo { + /** Specify the id of the user in case you are importing from a different system and + * want to re-use the user IDs (for whatever reason). + */ + id?: string, + customId?: string, + email: string, + password?: string, + /** Pass "true" if you are creating the user with a pre-hashed password; supported hashing mechanisms are: + * - `bcrypt(password)` + * - `bcrypt(SHA256(password))` + */ + passwordIsHashed?: boolean, + /** Pass "true" if the user must change the password when logging in the first time. */ + mustChangePassword?: boolean, + validated?: boolean, + groups: string[] +} + +export interface WickedUserInfo extends WickedUserCreateInfo { + id: string, + applications?: WickedApplication[] +} + +export interface OidcProfile { + sub: string, + email?: string, + email_verified?: boolean, + preferred_username?: string, + username?: string, + name?: string, + given_name?: string, + family_name?: string, + phone?: string, + [key: string]: any +}; + +export interface WickedApi { + id: string, + name: string, + desc: string, + auth: string, + bundle?: string, + tags?: string[], + authMethods?: string[], + registrationPool?: string, + requiredGroup?: string, + hide_credentials?: boolean, + passthroughUsers?: boolean, + passthroughScopeUrl?: string, + settings: WickedApiSettings, + _links?: any +} + +export interface WickedApiCollection { + apis: WickedApi[], + _links?: any +} + +export interface WickedApiSettings { + enable_client_credentials?: boolean, + enable_implicit_grant?: boolean, + enable_authorization_code?: boolean, + enable_password_grant?: boolean, + mandatory_scope?: boolean, + accept_http_if_terminated?: boolean, + token_expiration?: string, + refresh_token_ttl?: string, + scopes: WickedApiScopes, + tags: string[], + plans: string[], + internal?: boolean +} + +export interface WickedApiScopes { + [scope: string]: { + description: string + } +} + +export interface WickedApiPlan { + id: string, + name: string, + desc: string, + needsApproval?: boolean, + requiredGroup?: string, + config: { + plugins: KongPlugin[] + } +} + +export interface WickedApiPlanCollection { + plans: WickedApiPlan[] +} + +export interface WickedScopeGrant { + scope: string, + /** This is a date time object */ + grantedDate?: string +} + +export interface WickedGrant { + userId?: string, + apiId?: string, + applicationId?: string, + grants: WickedScopeGrant[] +} + +export interface WickedAuthMethod { + /** Protected Auth Methods aren't displayed in the UI (only for admins) */ + protected?: boolean, + enabled: string, + name: string, + type: string, + friendlyShort: string, + friendlyLong: string, + config: any +} + +export interface WickedAuthServer { + id: string, + name: string, + authMethods: WickedAuthMethod[], + config: KongApiConfig +} + +export enum WickedOwnerRole { + Owner = "owner", + Collaborator = "collaborator", + Reader = "reader" +} + +export interface WickedOwner { + userId: string, + email: string, + role: WickedOwnerRole +} + +export enum WickedClientType { + /** Confidential client, i.e. a client which can keep the client secret securely stored in the backend. Typical session-based applications. */ + Confidential = "confidential", + /** Public, browser based client. Cannot store a secret confidentially due to lack of backend server component. Typically a single page application. + * When using the OAuth2 Authorization Code Grant, PKCE is required, and wicked will **not** return a refresh token. */ + Public_SPA = "public_spa", + /** Public, e.g. native or mobile application. Cannot store a secret confidentially, but can keep runtime data reasonably secure. + * When using the OAuth2 Authorization Code Grant, PKCE is required, but in distinction to the `public_spa` client type, a refresh token + * **is issued**. */ + Public_Native = "public_native" +} + +export interface WickedApplicationCreateInfo { + id: string, + name: string, + /** Pass in either `redirectUri` or `redirectUris`; the latter is recommended for support of multiple redirect URIs. */ + redirectUri?: string, + /** Pass in either `redirectUri` or `redirectUris`; the latter is recommended for support of multiple redirect URIs. */ + redirectUris?: string[], + /** Deprecated; use `clientType` instead. */ + confidential?: boolean, + clientType?: WickedClientType +} + +export interface WickedApplication extends WickedApplicationCreateInfo { + ownerList: WickedOwner[] +} + +export enum WickedAuthType { + KeyAuth = "key-auth", + OAuth2 = "oauth2" +} + +export enum WickedApplicationRoleType { + Admin = "admin", + Collaborator = "collaborator", + Reader = "reader" +} + +export interface WickedApplicationRole { + role: WickedApplicationRoleType, + desc: string +} + +export interface WickedSubscriptionCreateInfo { + application: string, + api: string, + plan: string, + auth: WickedAuthType, + apikey?: string, + trusted?: boolean, +} + +export interface WickedSubscription extends WickedSubscriptionCreateInfo { + clientId?: string, + clientSecret?: string, + approved: boolean, + allowedScopesMode?: WickedSubscriptionScopeModeType, + allowedScopes?: string[], + changedBy?: string, + changedDate?: string +} + +export enum WickedSubscriptionScopeModeType { + All = "all", + None = "none", + Select = "select" +} + +export interface WickedSubscriptionPatchInfo { + approved?: boolean, + trusted?: boolean +} + +export interface WickedSubscriptionInfo { + application: WickedApplication, + subscription: WickedSubscription +} + +export enum WickedPoolPropertyType { + String = "string" +} + +export interface WickedPoolProperty { + id: string, + description: string, + type: string, + maxLength: number, + minLength: number, + required: boolean, + oidcClaim: string +} + +export interface WickedPool { + id: string, + name: string, + requiresNamespace?: boolean, + /** Disable interactive registration */ + disableRegister?: boolean, + properties: WickedPoolProperty[] +} + +export interface WickedPoolMap { + [poolId: string]: WickedPool +} + +export interface WickedRegistration { + userId: string, + poolId: string, + namespace?: string, + [key: string]: any +} + +export interface WickedRegistrationMap { + pools: { + [poolId: string]: WickedRegistration[] + } +} + +export interface WickedNamespace { + namespace: string, + poolId: string, + description: string +} + +export interface WickedGroup { + id: string, + name: string, + alt_ids?: string[], + adminGroup?: boolean, + approverGroup?: boolean +} + +export interface WickedGroupCollection { + groups: WickedGroup[] +} + +export interface WickedApproval { + id: string, + user: { + id: string, + name: string, + email: string + }, + api: { + id: string, + name: string + }, + application: { + id: string, + name: string + }, + plan: { + id: string, + name: string + } +} + +export interface WickedVerification { + id: string, + type: WickedVerificationType, + email: string, + /** Not needed when creating, is returned on retrieval */ + userId?: string, + /** The fully qualified link to the verification page, with a placeholder for the ID (mustache {{id}}) */ + link?: string +} + +export enum WickedVerificationType { + Email = 'email', + LostPassword = 'lostpassword' +} + +export interface WickedComponentHealth { + name: string, + message?: string, + uptime: number, + healthy: WickedComponentHealthType, + pingUrl: string, + pendingEvents: number +} + +export enum WickedComponentHealthType { + NotHealthy = 0, + Healthy = 1, + Initializing = 2 +} + +export interface WickedChatbotTemplates { + userLoggedIn: string, + userSignedUp: string, + userValidatedEmail: string, + applicationAdded: string, + applicationDeleted: string, + subscriptionAdded: string, + subscriptionDeleted: string, + approvalRequired: string, + lostPasswordRequest: string, + verifyEmailRequest: string +} + +export enum WickedEmailTemplateType { + LostPassword = 'lost_password', + PendingApproval = 'pending_approval', + VerifyEmail = 'verify_email' +} + +export interface WickedWebhookListener { + id: string, + url: string +} + +export interface WickedEvent { + id: string, + action: WickedEventActionType, + entity: WickedEventEntityType, + href?: string, + data?: object +} + +export enum WickedEventActionType { + Add = 'add', + Update = 'update', + Delete = 'delete', + Password = 'password', + Validated = 'validated', + Login = 'login', + /** Deprecated */ + ImportFailed = 'failed', + /** Deprecated */ + ImportDone = 'done' +} + +export enum WickedEventEntityType { + Application = 'application', + User = 'user', + Subscription = 'subscription', + Approval = 'approval', + Owner = 'owner', + Verification = 'verification', + VerificationLostPassword = 'verification_lost_password', + VerificationEmail = 'verification_email', + /** Deprecated */ + Export = 'export', + /** Deprecated */ + Import = 'import' +} + +// OPTION TYPES + +/** + * Generic parameters for paging collection output. + */ +export interface WickedGetOptions { + /** The offset of the collection items to retrieve. Defaults to `0`. */ + offset?: number, + /** The maximum number of items a get operation retrieves. Specify `0` to retrieve **all** elements. **Note**: This can be a dangerous operation, depending on the amount of data in your data store. */ + limit?: number +} + +/** + * Extended options for getting collections. + */ +export interface WickedGetCollectionOptions extends WickedGetOptions { + /** + * Specify keys and values to filter for when retrieving information. The filtering is case insensitive, and + * searches specifically only for substrings. This does (currently) **not** support wild card searches. + * + * Example: `{ filter: { name: "herbert" }, order_by: "name ASC" }` + */ + filter?: { + [field: string]: string + }, + /** Order by clause; syntax: ` `, e.g. `name DESC`. */ + order_by?: string, + /** + * Specify `false` to make sure the paging return values (item count and such) are re-calculated. Otherwise + * the wicked API makes use of a cached value for the item count for short amount of time. This option is + * usually only used for integration testing, and does not play a role in application development. + */ + no_cache?: boolean +} + +/** + * Extended get options for wicked registrations. + */ +export interface WickedGetRegistrationOptions extends WickedGetCollectionOptions { + /** + * The namespace for which to retrieve registrations. In case the registration pool requires + * namespaces, this is a required option, otherwise it's a forbidden option. */ + namespace?: string +} + +// ==================== +// GENERICS +// ==================== + +/** + * A wrapper interface for returning typed collections. Contains additional information + * which is useful for paging UI scenarios. + */ +export interface WickedCollection { + items: T[], + /** The total count of items, disregarding paging (limit and options). */ + count: number, + /** Contains `true` if the total count (property `count`) was retrieved from the cache. */ + count_cached: boolean, + /** The offset of the items which were retrieved, if specified, otherwise `0`. */ + offset: number, + /** The maximum number of items in `items`, if specified, otherwise `0`. */ + limit: number +} + + +// ==================== +// CALLBACK TYPES +// ==================== + +export interface ErrorCallback { + (err): void +} + +export interface Callback { + (err, t?: T): void +} + + +// ==================== +// FUNCTION TYPES +// ==================== + +export interface ExpressHandler { + (req, res, next?): void +} + + +// ==================== +// PASSTHROUGH HANDLING TYPES +// ==================== + +/** + * For APIs which use the `passthroughScopeUrl` property, this is the type of the + * payload which is sent by `POST` to the instance to which the scope decision is + * delegated. + */ +export interface PassthroughScopeRequest { + /** If `scope` was passed to the authorize request, the scope is passed on upstream by the Authorization Server. Otherwise `null` or not present. */ + scope?: string[], + /** + * The OpenID Connect compatible profile of the authenticated user. You will find a unique ID in the `sub` property, + * plus other properties, depending on the type of identity provider which was used. + */ + profile: OidcProfile +} + +/** + * This is the expected response type of a service which is used for APIs which have specified + * the `passthroughScopeUrl` parameter. Mandatory properties are `allow` and `authenticated_userid`, + * which must contain information on whether the operation is allowed and if so, which user id is + * to be associated with the access token which is about to be created. + */ +export interface PassthroughScopeResponse { + /** Return `true` to allow the operation to continue, otherwise `false`. */ + allow: boolean, + /** In case `allow` contains `false`, it is recommended to return an error message stating "why" here. */ + error_message?: string, + /** Specify which user id is passed as the `X-Authenticated-UserId` to the API backends when presenting the created access token. */ + authenticated_userid: string, + /** An array of valid scopes for the API; please note that the list of scopes must be configured on the API, otherwise a subsequent error will be generated (by the API Gateway). */ + authenticated_scope?: string[] +} + +/** + * Interface describing the expected response of a scope lookup request from the portal API, if this + * property is set on a specific API. When doing a GET on the specified endpoint, this is the format + * of the data which has to be returned. + * + * Example: + * + * ``` + * { + * "scope1": { + * "description": "This is scope 1" + * }, + * "scope2": { + * "description": "This is another scope" + * } + * } + * ``` + */ +export interface ScopeLookupResponse { + [scope: string]: { + description: string + } +} + +/** This is the request which is sent out as a POST to a 3rd party username/password validator + * if an auth method of the type "external" is configured. + */ +export interface ExternalUserPassRequest { + /** The username in clear text; this does not necessarily have to be an email address, but may be */ + username: string, + /** The password in clear text */ + password: string +} + +/** + * The expected response type, as a JSON object, of a request to the username/password validation end + * point, for auth methods using the "external" auth method type. + */ +export interface ExternalUserPassResponse { + /** + * Mandatory properties: + * - sub + * - email + * + * Can be left empty if request is not successful. + */ + profile?: OidcProfile, + /** Short error message if request was not successful */ + error?: string, + /** Optional longer error message if request was not successful */ + error_description?: string +} + +/** + * For auth methods using the ”external" type, this is the payload of requests which are sent to + * the backend prior to allowing access tokens to be refreshed. + */ +export interface ExternalRefreshRequest { + /** + * The authenticated user ID of the user requesting a refreshed token; depending on whether + * the API uses passthrough users or wicked backed users, this is either the value + * from the the @ExternalUserPassResponse as `sub= (when using + * passthrough users), or a field containing `sub=` (when using wicked backed + * users). May also contain a `;namespaces=` property. + */ + authenticated_userid: string, + /** + * This is just for information the scope for which the initial access token was created. + * Refreshed tokens are created with the same scope, and cannot be changed here. + */ + authenticated_scope?: string +} + +/** + * Expected response (as JSON) of an ExternalRefreshRequest. Contains information on whether + * the refresh request shall be allowed or not. + */ +export interface ExternalRefreshResponse { + /** Return true to allow refresh, otherwise false. */ + allow_refresh: boolean, + /** + * Optional error message if allow_refresh is returned as false. This string may be used + * in end-user facing communication. + */ + error?: string, + /** + * Optional longer error description in case allow_refresh is returned as false. + * This string may be used in end-user facing communication. + */ + error_description?: string, +} diff --git a/src/kong-interfaces.ts b/src/kong-interfaces.ts new file mode 100644 index 0000000..3fb890a --- /dev/null +++ b/src/kong-interfaces.ts @@ -0,0 +1,379 @@ +'use strict'; + +// ==================== +// KONG TYPES +// ==================== + +export interface KongApi { + created_at?: number, + hosts?: string[], + uris?: string[], + http_if_terminated?: boolean, + https_only?: boolean, + id?: string, + name: string, + preserve_host?: boolean, + retries?: number, + strip_uri?: boolean, + hide_credentials?: boolean, + upstream_connect_timeout?: number, + upstream_read_timeout?: number, + upstream_send_timeout?: number, + upstream_url: string +} + +export enum ProtocolType { + http = 'http', + https = 'https' +} + +export interface KongService { + id?: string, + name: string, + created_at?: number, + updated_at?: number, + protocol: ProtocolType, + host: string, + port: number, + path: string, + retries?: number, + connect_timeout?: number, + read_timeout?: number, + write_timeout?: number, +} + +export interface KongRoute { + id?: string, + created_at?: number, + updated_at?: number, + protocols: ProtocolType[], + methods?: string[], + hosts?: string[], + paths?: string[], + regex_priority?: number, + strip_path: boolean, + preserve_host: boolean, + service: { + id: string + } +} + +export interface KongPlugin { + id?: string, + name: string, + enabled: boolean, + api_id?: string, + service_id?: string, + route_id?: string, + consumer_id?: string, + config?: any +} + +export interface KongPluginCors extends KongPlugin { + config: { + credentials?: boolean, + origins: string[], + preflight_continue?: boolean, + // Can be comma-separated string or string[] + methods?: any + } +} + +export interface KongPluginRequestTransformer extends KongPlugin { + config: { + http_method?: string, + remove?: { + headers?: string[], + querystring?: string[], + body?: string[], + }, + replace?: { + headers?: string[], + querystring?: string[], + body?: string[] + }, + rename?: { + headers?: string[], + querystring?: string[], + body?: string[], + }, + append?: { + headers?: string[] + querystring?: string[], + body?: string[], + }, + add?: { + headers?: string[] + querystring?: string[], + body?: string[], + } + } +} + +export interface KongPluginResponseTransformer extends KongPlugin { + config: { + remove?: { + headers?: string[], + json?: string[] + }, + replace?: { + headers?: string[], + json?: string[] + }, + add?: { + headers?: string[], + json?: string[] + }, + append?: { + headers?: string[], + json?: string[] + } + } +} + +export interface KongPluginOAuth2 extends KongPlugin { + config: { + refresh_token_ttl?: number, + provision_key?: string, + accept_http_if_already_terminated?: boolean, + hide_credentials?: boolean, + global_credentials?: boolean, + enable_client_credentials?: boolean, + enable_authorization_code?: boolean, + enable_implicit_grant?: boolean, + enable_password_grant?: boolean, + token_expiration: number, + anonymous?: string, + scopes?: string[], + mandatory_scope?: boolean, + auth_header_name?: string + } +} + +export interface KongPluginRateLimiting extends KongPlugin { + config: { + fault_tolerant?: boolean, + hide_client_headers?: boolean, + limit_by?: string, + second?: number, + minute?: number, + hour?: number, + day?: number, + month?: number, + year?: number, + policy?: string, + redis_host?: string, + redis_database?: number, + redis_port?: number, + redis_password?: string, + redis_timeout?: number + } +} + +export interface KongPluginWhiteBlackList extends KongPlugin { + config: { + /** Comma-separated list of IP addresses */ + whitelist?: string, + /** Comma-separated list of IP addresses */ + blacklist?: string + } +} + +export interface KongPluginBotDetection extends KongPlugin { + config: { + /** A comma separated array of regular expressions that should be whitelisted. The regular expressions will be checked against the User-Agent header. */ + whitelist?: string, + /** A comma separated array of regular expressions that should be blacklisted. The regular expressions will be checked against the User-Agent header. */ + blacklist?: string + } +} + +export enum KongPluginCorrelationIdGeneratorType { + UUID = 'uuid', + UUIDCounter = 'uuid#counter', + Tracker = 'tracker' +} + +export interface KongPluginCorrelationId extends KongPlugin { + config: { + header_name?: string, + generator?: KongPluginCorrelationIdGeneratorType, + echo_downstream?: boolean + } +} + +export interface KongPluginHmacAuth extends KongPlugin { + hide_credentials?: boolean, + clock_skew?: number, + anonymous?: string, + validate_request_body?: boolean, + enforce_headers?: string[], + algorithms: string[] +} + + +export interface KongCollection { + // This property is not always present, take it out, use data.length + // total: number, + data: T[], + next?: string +} + +export interface KongConsumer { + id?: string, + created_at?: number, + username?: string, + custom_id: string +} + +export interface KongProxyListener { + ssl: boolean, + ip: string, + proxy_protocol: boolean, + port: number, + http2: boolean + listener: string +} + +export interface KongHttpDirective { + value: string, + name: string +} + +/** + * This is a possibly incomplete set of properties which a get to :8001/ returns. + */ +export interface KongGlobals { + version: string, + host: string, + tagline: string, + node_id: string, + lua_version: string, + plugins: { + enabled_in_cluster: string[], + available_on_server: { + [plugin_name: string]: boolean + } + }, + configuration: { + plugins: string[], + admin_listen: string[], + lua_ssl_verify_depth: number, + trusted_ips: string[], + prefix: string, + loaded_plugins: { + [plugin_name: string]: boolean + }, + cassandra_username: string, + admin_ssl_cert_csr_default: string, + ssl_cert_key: string, + dns_resolver: object, + pg_user: string, + mem_cache_size: string, + cassandra_data_centers: string[], + nginx_admin_directives: any, + custom_plugins: any, + pg_host: string, + nginx_acc_logs: string, + proxy_listen: string[], + client_ssl_cert_default: string, + ssl_cert_key_default: string, + dns_no_sync: boolean, + db_update_propagation: number, + nginx_err_logs: string, + cassandra_port: number, + dns_order: string[], + dns_error_ttl: number, + headers: string[], + dns_stale_ttl: number, + nginx_optimizations: boolean, + database: string, + pg_database: string, + nginx_worker_processes: string, + lua_package_cpath: string, + admin_acc_logs: string, + lua_package_path: string, + nginx_pid: string, + upstream_keepalive: number, + cassandra_contact_points: string[], + client_ssl_cert_csr_default: string, + proxy_listeners: KongProxyListener[], + proxy_ssl_enabled: boolean, + admin_access_log: string, + pg_password: string, + enabled_headers: { + latency_tokens: boolean, + "X-Kong-Proxy-Latency": boolean, + Via: boolean, + server_tokens: boolean, + Server: boolean, + "X-Kong-Upstream-Latency": boolean, + "X-Kong-Upstream-Status": boolean + }, + cassandra_ssl: boolean, + ssl_cert_csr_default: string, + db_resurrect_ttl: number, + client_max_body_size: string, + cassandra_consistency: string, + db_cache_ttl: number, + admin_error_log: string, + pg_ssl_verify: boolean, + dns_not_found_ttl: boolean, + pg_ssl: boolean, + client_ssl: boolean, + db_update_frequency: number, + cassandra_repl_strategy: string, + nginx_kong_conf: string, + cassandra_repl_factor: number, + nginx_http_directives: KongHttpDirective[], + error_default_type: string, + kong_env: string, + cassandra_schema_consensus_timeout: number, + dns_hostsfile: string, + admin_listeners: KongProxyListener[], + real_ip_header: string, + ssl_cert: string, + proxy_access_log: string, + admin_ssl_cert_key_default: string, + cassandra_ssl_verify: boolean, + cassandra_lb_policy: string, + ssl_cipher_suite: string, + real_ip_recursive: string, + proxy_error_log: string, + client_ssl_cert_key_default: string, + nginx_daemon: string, + anonymous_reports: boolean, + cassandra_timeout: number, + nginx_proxy_directives: any, + pg_port: number, + log_level: string, + client_body_buffer_size: string, + ssl_ciphers: string, + lua_socket_pool_size: number, + admin_ssl_cert_default: string, + cassandra_keyspace: string, + ssl_cert_default: string, + nginx_conf: string, + admin_ssl_enabled: boolean + } +} + +export interface KongStatus { + database: { + reachable: boolean + }, + server: { + connections_writing: number, + total_requests: number, + connections_handled: number, + connections_accepted: number, + connections_reading: number, + connections_active: number, + connections_waiting: number + } +} + +export interface KongApiConfig { + api: KongApi, + plugins?: KongPlugin[] +} diff --git a/src/kong.ts b/src/kong.ts new file mode 100644 index 0000000..47311d4 --- /dev/null +++ b/src/kong.ts @@ -0,0 +1,113 @@ +'use strict'; + +import { KongService, KongRoute, KongApi, ProtocolType } from "./kong-interfaces"; + +/** @hidden */ +function deducePort(url: URL): number { + if (url.port) { + if (typeof url.port === 'number') { + return url.port; + } + try { + const port = parseInt(url.port); + return port; + } catch (err) { + console.warn(`deducePort(): Could not parse port from upstream url (port: ${url.port}), guessing by protocol.`); + console.warn(err); + } + } + + if (url.protocol) { + switch (url.protocol) { + case 'http:': return 80; + case 'https:': return 443; + default: + console.warn(`deducePort(): Unknown protocol for upstream url: ${url.protocol}, default port to 443`); + return 443; + } + } + + console.warn(`deducePort(): Upstream URL has neither port nor protocol, defaulting to port 80`); + return 80; +} + +/** @hidden */ +function deducePath(url: URL): string { + if (url.pathname) + return url.pathname; + return '/'; +} + +/** @hidden */ +function deduceProtocol(url: URL): ProtocolType { + switch (url.protocol) { + case 'http:': return ProtocolType.http; + case 'https:': return ProtocolType.https; + } + console.warn(`deducePort(): Unknown protocol for upstream url: ${url.protocol}, defaulting to https`); + return ProtocolType.https; +} + +// Service+Route <-> API +export function kongApiToServiceRoute(api: KongApi): { service: KongService, route: KongRoute } { + let upstreamUrl; + try { + upstreamUrl = new URL(api.upstream_url); + } catch (err) { + console.error(`kongApiToServiceRoute: The upstream URL "${api.upstream_url}" is not a valid URL. Setting to http://dummy.org/foo`); + console.error(err); + upstreamUrl = new URL('http://dummy.org/foo'); + } + const service: KongService = { + id: api.id, + protocol: deduceProtocol(upstreamUrl), + host: upstreamUrl.hostname, + port: deducePort(upstreamUrl), + path: deducePath(upstreamUrl), + name: api.name, + retries: api.retries, + connect_timeout: api.upstream_connect_timeout, + read_timeout: api.upstream_read_timeout, + write_timeout: api.upstream_send_timeout, + } + const route: KongRoute = { + hosts: null, + protocols: [ProtocolType.http, ProtocolType.https], + paths: api.uris, + methods: null, + regex_priority: 0, + strip_path: api.strip_uri, + preserve_host: api.preserve_host, + service: { + id: api.id + } + }; + return { + service: service, + route: route + }; +} + +export function kongServiceRouteToApi(service: KongService, route: KongRoute): KongApi { + let upstreamUrl; + try { + upstreamUrl = new URL(`${service.protocol}://${service.host}:${service.port}${service.path}`); + } catch (err) { + console.error(`kongServiceRouteToApi: Could not assemble valid URL from service definition (see next line), setting to http://dummy.org/foo`); + console.error(service); + upstreamUrl = new URL('http://dummy.org/foo'); + } + return { + id: service.id, + name: service.name, + upstream_url: upstreamUrl.toString(), + hosts: route.hosts, + uris: route.paths, + strip_uri: route.strip_path, + preserve_host: route.preserve_host, + retries: service.retries, + upstream_connect_timeout: service.connect_timeout, + upstream_read_timeout: service.read_timeout, + upstream_send_timeout: service.write_timeout + }; +} diff --git a/src/wicked-error.ts b/src/wicked-error.ts new file mode 100644 index 0000000..eddb591 --- /dev/null +++ b/src/wicked-error.ts @@ -0,0 +1,18 @@ +'use strict'; + +export class WickedError extends Error { + public status?: number; + public statusCode?: number; + public body?: any; + + constructor(message: string, statusCode?: number, body?: any) { + super(message); + // See https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html + Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain + // Pass in for both statusCode and status; some libraries do one or the other, + // we do both. + this.statusCode = statusCode; + this.status = statusCode; + this.body = body; + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d885731 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "declaration": true, + "types": [ + "node" + ], + "outDir": "./dist", + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noImplicitThis": true + }, + "include": [ + "src/**/*" + ] +} \ No newline at end of file