From 854bad0f01cd89d9a82971769ad58cd76998b77c Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Mon, 24 May 2021 12:20:45 -0400 Subject: [PATCH 01/19] First commit --- .gitignore | 2 + .vscode/launch.json | 48 ++ README.md | 156 ++++++ data/member-a/cert.pem | 17 + data/member-a/config.json | 12 + data/member-a/key.pem | 28 + data/member-a/peer-certs/org-b.pem | 17 + data/member-b/cert.pem | 17 + data/member-b/config.json | 12 + data/member-b/key.pem | 28 + data/member-b/peer-certs/org-a.pem | 17 + nodemon.json | 6 + package-lock.json | 862 +++++++++++++++++++++++++++++ package.json | 39 ++ src/app.ts | 96 ++++ src/custom.d.ts | 21 + src/handlers/blobs.ts | 94 ++++ src/handlers/events.ts | 37 ++ src/handlers/messages.ts | 55 ++ src/index.ts | 9 + src/lib/cert.ts | 21 + src/lib/config.ts | 36 ++ src/lib/interfaces.ts | 97 ++++ src/lib/request-error.ts | 22 + src/lib/utils.ts | 100 ++++ src/routers/api.ts | 102 ++++ src/routers/p2p.ts | 48 ++ src/schemas/config.json | 36 ++ src/swagger.yaml | 238 ++++++++ tsconfig.json | 19 + 30 files changed, 2292 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 README.md create mode 100644 data/member-a/cert.pem create mode 100644 data/member-a/config.json create mode 100644 data/member-a/key.pem create mode 100644 data/member-a/peer-certs/org-b.pem create mode 100644 data/member-b/cert.pem create mode 100644 data/member-b/config.json create mode 100644 data/member-b/key.pem create mode 100644 data/member-b/peer-certs/org-a.pem create mode 100644 nodemon.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/app.ts create mode 100644 src/custom.d.ts create mode 100644 src/handlers/blobs.ts create mode 100644 src/handlers/events.ts create mode 100644 src/handlers/messages.ts create mode 100644 src/index.ts create mode 100644 src/lib/cert.ts create mode 100644 src/lib/config.ts create mode 100644 src/lib/interfaces.ts create mode 100644 src/lib/request-error.ts create mode 100644 src/lib/utils.ts create mode 100644 src/routers/api.ts create mode 100644 src/routers/p2p.ts create mode 100644 src/schemas/config.json create mode 100644 src/swagger.yaml create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b7dab5e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +build \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..073e087 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,48 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Member A", + "runtimeExecutable": "nodemon", + "resolveSourceMapLocations": [ + "!**/node_modules/**" + ], + "env": { + "DATA_DIRECTORY": "${workspaceFolder}/data/member-a", + "LOG_LEVEL": "info" + }, + "args": [ + "|", + "bunyan", + "-o", + "short" + ], + "outputCapture": "std", + }, + { + "type": "node", + "request": "launch", + "name": "Member B", + "runtimeExecutable": "nodemon", + "resolveSourceMapLocations": [ + "!**/node_modules/**" + ], + "env": { + "DATA_DIRECTORY": "${workspaceFolder}/data/member-b", + "LOG_LEVEL": "info", + }, + "args": [ + "|", + "bunyan", + "-o", + "short" + ], + "outputCapture": "std", + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..e51136c --- /dev/null +++ b/README.md @@ -0,0 +1,156 @@ +# Firefly MTLS Data Exchange + +The following steps show how to setup Firefly MTLS Data Exchange for two organizations named `org-a` and `org-b` running on `localhost`. + +## Setup org-a + +#### Environment variables + +Open a command line window and set the following environment variables, assigning an appropriate location to `DATA_DIRECTORY`. This is where configuration and certificate files will reside: +``` +export DATA_DIRECTORY=/data-a +export LOG_LEVEL=info +``` + +#### Configuration file + +Create `config.json` in the data directory and set its content to: +``` +{ + "$schema": "../../src/schemas/config.json", + "apiPort": 3000, + "p2pPort": 3001, + "apiKey": "xxxxx", + "peers": [ + { + "name": "org-b", + "endpoint": "https://localhost:4001" + } + ] +} +``` + +Based on this configuration: +- Port 3000 will be used to access the API +- Port 3001 will be used for P2P communications +- The API key will be set to `xxxxx` (bearer token) +- There is one peer named `org-b` whose P2P endpoint is `https://localhost:4001` + +#### Generate certificate + +In the data directory, run the following command: +``` +openssl req -new -x509 -nodes -days 365 -subj '/CN=localhost/O=org-a' -keyout key.pem -out cert.pem +``` +This will generate files `key.pem` and `cert.pem`. Notice that the common name is `localhost` while the organization name is `org-a`. + +## Setup org-b + +#### Environment variables + +Open a second command line window and set the following environment variables, assigning an appropriate location to `DATA_DIRECTORY`. This is where configuration and certificate files will reside: +``` +export DATA_DIRECTORY=/data-b +export LOG_LEVEL=info +``` + +#### Configuration file + +``` +{ + "$schema": "../../src/schemas/config.json", + "apiPort": 4000, + "p2pPort": 4001, + "apiKey": "yyyyy", + "peers": [ + { + "name": "org-b", + "endpoint": "https://localhost:4001" + } + ] +} +``` + +Based on this configuration: +- Port 4000 will be used to access the API +- Port 4001 will be used for P2P communications +- The API key will be set to `yyyyy` (bearer token) +- There is one peer named `org-a` whose P2P endpoint is `https://localhost:3001` + + +#### Generate certificate + +``` +openssl req -new -x509 -nodes -days 365 -subj '/CN=localhost/O=org-b' -keyout key.pem -out cert.pem +``` + +This will generate files `key.pem` and `cert.pem`. Notice that the common name is `localhost` while the organization name is `org-b`. + +## Copy certificates + +- Copy `/org-a/cert.pem` to `/org-b/peer-certs/org-a.pem`. +- Copy `/org-b/cert.pem` to `/org-a/peer-certs/org-b.pem`. + +This will make it possible for the organizations to establish MTLS communications with each other. + +## Build and run the processes + +- Run `npm run build`. +- In the command line window for `org-a` run `npm start` +- In the command line window for `org-b` run `npm start` + +## Access the API Swagger + +- Open a new web browser window and navigate to `http://localhost:3000` +- Open another web browser window and navigate to `http://localhost:4000` + +## WebSocket Events + + +| Type | Description | Additional properties +|-----------------|------------------------------------------------------------|----------------------- +|blob-received | Emitted to the recipient when a blob has been transferred | sender, path, hash +|blob-delivered | Emitted to the sender when a blob has been delivered | recipient, path +|blob-failed | Emitted to the sender when a blob could not be delivered | recipient, path +|message-received | Emitted to the recipient when a message has been sent | sender, message +|message-delivered| Emitted to the sender when a message has been delivered | recipient, message +|message-failed | Emitted to the sender when a message could not be delivered| recipient, message + +- After receiving a websocket message, a commit must be sent in order to receive the next one: + ``` + { "action": "commit" } + ``` +- Messages arrive in the same order they were sent +- Up to 1,000 messages will be queued + +## Alternative setup using CA + +Generate CA key and cert: +``` +openssl req -new -x509 -nodes -days 365 -subj '/CN=blob-exchange-ca' -keyout ca-key.pem -out ca.crt +``` + +Generate `org-a` key: +``` +openssl genrsa -out org-a.key 2048 +``` +Generate `org-a` CSR: +``` +openssl req -new -key org-a.key -subj '/CN=localhost,O=org-a' -out org-a.csr +``` +Create signed certificate using CSR, CA +``` +openssl x509 -req -in org-a.csr -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -days 365 -out org-a.crt +``` +Generate `org-b` key: +``` +openssl genrsa -out org-b.key 2048 +``` +Generate `org-b` CSR: +``` +openssl req -new -key org-b.key -subj '/CN=localhost,O=org-b' -out org-b.csr +``` +Create signed certificate using CSR, CA +``` +openssl x509 -req -in org-b.csr -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -days 365 -out org-b.crt +``` diff --git a/data/member-a/cert.pem b/data/member-a/cert.pem new file mode 100644 index 0000000..f984b1b --- /dev/null +++ b/data/member-a/cert.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICxDCCAawCCQCwQ58VBeUg+TANBgkqhkiG9w0BAQsFADAkMRIwEAYDVQQDDAls +b2NhbGhvc3QxDjAMBgNVBAoMBW9yZy1hMB4XDTIxMDUyMDAyMjUzN1oXDTIyMDUy +MDAyMjUzN1owJDESMBAGA1UEAwwJbG9jYWxob3N0MQ4wDAYDVQQKDAVvcmctYTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK/bZPBvaA+V6HoXNP+oOQaK +DPuLgFO3R7TTIBTCJttELv1lbfuw95Up+/5b4dPnnIF3wwE2GZsiMWJV7RgfDAUi +x7saJJIIa2E8gLrmoaG99w+PYQW41OBz0p54tw4abN5RNsgineu1N5pIJOMJ6cMs +D8IYfWSHDjZZvu3F2YCItSVYZlfmGBCcFCf4HASF6m4lUTZCDRBurjOQrwW9mXJv +hkIa1HEa4l7Nq09d8Bokvieq1vHJUB78kYTR027z+sm4H2o4pXjzBOaV/z2yB7+t +yqWbMjm8aq2m/gmpZTjDBPgX9XLMYHWBZWCpk1iHo/eqw+UeZ6kRZ0RR78Co+IsC +AwEAATANBgkqhkiG9w0BAQsFAAOCAQEAH6rQx8khtfzH93YCstHduvEzUCHSyifF +sDyRLvSmS0qHz1liLJyvWbT9xDQCpDZrxhQKJLN2eZxxlNaH6XBEfHQgi2I43hxu +sd2KZ4wOWOk/HyM9BcibKNMtfHJdgQ5EYRc6OWDY1c8bQQfRJUBzrSJKldqfQjqC +mPEeHXDH++yTg2Vfm7GZiogxqSWn/+ILzHNeWrvr0HJ86Guyg/NPKBxs0uasvgI7 +KqW+fcZ/9Vg6a4e+zRTL8EwYBX6dTSwgt4X9wuwMvt/K0qodgW6I4paqEpVJOS+d +Z6WevHgHwZTr+mS3mJ6Y6u/DBD8/06uPAf3T5d/dC6Xtl4sEFkpNAA== +-----END CERTIFICATE----- diff --git a/data/member-a/config.json b/data/member-a/config.json new file mode 100644 index 0000000..0b083f5 --- /dev/null +++ b/data/member-a/config.json @@ -0,0 +1,12 @@ +{ + "$schema": "../../src/schemas/config.json", + "apiPort": 3000, + "p2pPort": 3001, + "apiKey": "xxxxx", + "peers": [ + { + "name": "org-b", + "endpoint": "https://localhost:4001" + } + ] +} diff --git a/data/member-a/key.pem b/data/member-a/key.pem new file mode 100644 index 0000000..edb1501 --- /dev/null +++ b/data/member-a/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCv22Twb2gPleh6 +FzT/qDkGigz7i4BTt0e00yAUwibbRC79ZW37sPeVKfv+W+HT55yBd8MBNhmbIjFi +Ve0YHwwFIse7GiSSCGthPIC65qGhvfcPj2EFuNTgc9KeeLcOGmzeUTbIIp3rtTea +SCTjCenDLA/CGH1khw42Wb7txdmAiLUlWGZX5hgQnBQn+BwEhepuJVE2Qg0Qbq4z +kK8FvZlyb4ZCGtRxGuJezatPXfAaJL4nqtbxyVAe/JGE0dNu8/rJuB9qOKV48wTm +lf89sge/rcqlmzI5vGqtpv4JqWU4wwT4F/VyzGB1gWVgqZNYh6P3qsPlHmepEWdE +Ue/AqPiLAgMBAAECggEAfeVtQAwhzXaetjlDAfwmxx2x6auXNVezCCtVfkb4lmUb +1uD0egnZVXp3I0QYSiI4Ex/wBT/72OoDvMiTMn+XlV2u663tnHZWmYg9CJDwCeD7 +rLIS3YvTKtUAZd85/ejBI++0blcKA6L6qYMYlUmVhtpWdbgenQdrD1H0tDi+W5tS +ZQBMi0ZuhcyicYs4tovFznyteuEJVZF0Nn3kJSgP0CIS9+pw10Xjf5f6kwroy5Lm +mNerHczL0sI0CsyNJfedJn5BMtIl7/mWvM48/UK0MEOhLhpCgEfO4uHo0gaRMLuE +SqDuOI30an6QvhGtEjCSEp1YSwvt2jGC1f1FxlkMMQKBgQDmvrRV2lSDxvkuwKb1 +n/rLQiHo2i6mHsRp9r2OH9QqkEJELV2eEyCa3ynOw/xvo+iyVn5WBGU9w5dH/Bv7 +PRK8s/kBv4B6WjM0jOvjgl8nhGNoBJzNLvJC9oq5H9YZyIY/wlpfWTlNE2CDhiVY +pH+7pPjIhWZoRP9zacJukSY9zQKBgQDDGsW16vOpzl8dxDff9rdAKNL9BnPIEwKH +gOW1k8sA3G0q5ESwWa9cFSsL6/BZb4DLgpy3/AwNYh4h4gQqOPJ040HGWU27sLg0 +MCAm0lytsJd5rYkRxo/i13vjM6nrDP5rtuVm580J19wX0C4RUrxQaZJDycRHxhzn +OwRWOXr3twKBgQCnNiNw45rDM/l3S9yxupD/opj4KMQNVdZ4A3ox+BbEEW40AbwJ +xUqncHjXgWb5cAo80jkTFHRZYdfLLoMIeaWOYc1c2u27vInG1yhJ4jwaYvG2e0E1 +34Nny0mUBeIdJRdENQ9QcVP46sXSCfAeYHbAADjY9vLTNMz5Ufa4MS9JMQKBgDjx +v3LxOFz/VtOhSY2cvK+FOs+O6owiwEI8ZM8ya7W8oEA8j6/I/V2q7/1yx5vS54x+ +eq9YaUwerxzEkuKf7GQhUDlMJ1v4oErbIQczrskjGZLyC2ecxLI4ongVxCpOiJN3 +tkzqqvWMgziQEmIL+9qcdYxDf35TXGxA1Ws1K6bRAoGBAOF2KcsJZzHtgduL87R1 +KOdl24CqBjfVnz4vs+TPdU250Yayvu3nGxeq75TgLPTe81INnfViPiNwc6qMuC8K +s03CbFQE3g1tze27KsCQvvk58bOL1brlwYY7b9hkRGT5kYz97p2Mc6lGJMkSM/uX +CIbhMm+oiN8OjQehE20W+A25 +-----END PRIVATE KEY----- diff --git a/data/member-a/peer-certs/org-b.pem b/data/member-a/peer-certs/org-b.pem new file mode 100644 index 0000000..37988eb --- /dev/null +++ b/data/member-a/peer-certs/org-b.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICxDCCAawCCQCoKGH3VybuuDANBgkqhkiG9w0BAQsFADAkMRIwEAYDVQQDDAls +b2NhbGhvc3QxDjAMBgNVBAoMBW9yZy1iMB4XDTIxMDUyMDAyMjYxNFoXDTIyMDUy +MDAyMjYxNFowJDESMBAGA1UEAwwJbG9jYWxob3N0MQ4wDAYDVQQKDAVvcmctYjCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN21/OyZAHIM+6B/HdR7pQLX +cEh60nUYHEJbCa5dlV7WyqpbKF6jSnbxz3/ZC59qHQKSgQ6Clm+AK7GaMpuN2mmN +ZL0ycR5+Ulax+rDJ827z2j+yCUz31gL8p3/kGTe66KeYVvuG3AntCDC+rCrShkE3 +s8EzR26k1vLMavZovZVT6xfONFPPbFEGS/x8LTkC0n0gQgDAAjwVKqjXl6OGVqs4 +VsByzeHGHmmJa5hTP0ACu0y5m0R5ifd49GiOxB8zNJNOYf2vyYiinCbprjy3DZgI +UMKyJeqFkJ+mD+Hh50tuhAZu4ffBZSDmb4AKF1z2t+2Bc1TZeDuCRBJbVZnMTB8C +AwEAATANBgkqhkiG9w0BAQsFAAOCAQEAa/cv4g1DJI8KYFcmOdhawwAAIZxg3/G/ +rk3Y2wShftZpTFk2mEnX0KUbXnt1JKDENcwDSldDLQF5bJHPJMyjiwPNSCxwmWSv +PzE7GwUZzUoh2Xp4sg12zaJQaHjqq6SBbv4hrpuLvuK+aSOLuN7GDuwatfouaium +5kCOgiSZJNYw0jJy723XuZoQAmXhqY7LuxYYVipW/nRQZSdUWW53gWqjutRbvBPy +pc+enThDeZQE8PGQw9OfhK+UavVOWfsBJMpTO416pMD1+JSfm7bJmaz43b6Pwwok +6lPdB9Pk1fPDS60p6SGP6Ovnr66mXKKqToECIL1m3GUx8iKH7ymeHQ== +-----END CERTIFICATE----- diff --git a/data/member-b/cert.pem b/data/member-b/cert.pem new file mode 100644 index 0000000..37988eb --- /dev/null +++ b/data/member-b/cert.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICxDCCAawCCQCoKGH3VybuuDANBgkqhkiG9w0BAQsFADAkMRIwEAYDVQQDDAls +b2NhbGhvc3QxDjAMBgNVBAoMBW9yZy1iMB4XDTIxMDUyMDAyMjYxNFoXDTIyMDUy +MDAyMjYxNFowJDESMBAGA1UEAwwJbG9jYWxob3N0MQ4wDAYDVQQKDAVvcmctYjCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN21/OyZAHIM+6B/HdR7pQLX +cEh60nUYHEJbCa5dlV7WyqpbKF6jSnbxz3/ZC59qHQKSgQ6Clm+AK7GaMpuN2mmN +ZL0ycR5+Ulax+rDJ827z2j+yCUz31gL8p3/kGTe66KeYVvuG3AntCDC+rCrShkE3 +s8EzR26k1vLMavZovZVT6xfONFPPbFEGS/x8LTkC0n0gQgDAAjwVKqjXl6OGVqs4 +VsByzeHGHmmJa5hTP0ACu0y5m0R5ifd49GiOxB8zNJNOYf2vyYiinCbprjy3DZgI +UMKyJeqFkJ+mD+Hh50tuhAZu4ffBZSDmb4AKF1z2t+2Bc1TZeDuCRBJbVZnMTB8C +AwEAATANBgkqhkiG9w0BAQsFAAOCAQEAa/cv4g1DJI8KYFcmOdhawwAAIZxg3/G/ +rk3Y2wShftZpTFk2mEnX0KUbXnt1JKDENcwDSldDLQF5bJHPJMyjiwPNSCxwmWSv +PzE7GwUZzUoh2Xp4sg12zaJQaHjqq6SBbv4hrpuLvuK+aSOLuN7GDuwatfouaium +5kCOgiSZJNYw0jJy723XuZoQAmXhqY7LuxYYVipW/nRQZSdUWW53gWqjutRbvBPy +pc+enThDeZQE8PGQw9OfhK+UavVOWfsBJMpTO416pMD1+JSfm7bJmaz43b6Pwwok +6lPdB9Pk1fPDS60p6SGP6Ovnr66mXKKqToECIL1m3GUx8iKH7ymeHQ== +-----END CERTIFICATE----- diff --git a/data/member-b/config.json b/data/member-b/config.json new file mode 100644 index 0000000..f7f9970 --- /dev/null +++ b/data/member-b/config.json @@ -0,0 +1,12 @@ +{ + "$schema": "../../src/schemas/config.json", + "apiPort": 4000, + "p2pPort": 4001, + "apiKey": "yyyyy", + "peers": [ + { + "name": "org-a", + "endpoint": "https://localhost:3001" + } + ] +} \ No newline at end of file diff --git a/data/member-b/key.pem b/data/member-b/key.pem new file mode 100644 index 0000000..36435ef --- /dev/null +++ b/data/member-b/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDdtfzsmQByDPug +fx3Ue6UC13BIetJ1GBxCWwmuXZVe1sqqWyheo0p28c9/2Qufah0CkoEOgpZvgCux +mjKbjdppjWS9MnEeflJWsfqwyfNu89o/sglM99YC/Kd/5Bk3uuinmFb7htwJ7Qgw +vqwq0oZBN7PBM0dupNbyzGr2aL2VU+sXzjRTz2xRBkv8fC05AtJ9IEIAwAI8FSqo +15ejhlarOFbAcs3hxh5piWuYUz9AArtMuZtEeYn3ePRojsQfMzSTTmH9r8mIopwm +6a48tw2YCFDCsiXqhZCfpg/h4edLboQGbuH3wWUg5m+AChdc9rftgXNU2Xg7gkQS +W1WZzEwfAgMBAAECggEBAKiHwueychU/6zIYDnvLNSaQz0g5HKtnlWuXOiex+W4r +BckzG9+8bkk2DI3ZVa1rAvxGkxWNjH7bRdtuJBP8Y08i7LWolzYfvcUq6y3hhUsM +0BTk5477QVHp7gUYRVcIm/txoIT4AWEGLdinx8WRW/5NMjWlHiJr6PyNCK2f9qR/ +yG1Sjs2rBaAv0pPE12PNjhQ/0LvpnLXszROihe9WmXTmWJk3LuZ/BG/9i+qwir8M +TjHwvv9cKpxOXtkADtuAVfipYEBKNPtq8hFAaILIUwit9A9OsSAbhQz9KclF0YVz +2L2YjorwbUzO7tRi0Qvvj3KHfl4oxtOR1OG7Efd/rNkCgYEA9hhke2b3QiNbAZ1b +vE8acOAUhnGoOwyXBA6gp67KkjZjSGgfbHzyJK61B7gVKMTYBacQ6kUNb4Hzs951 +8v9DiZIOTlkMqiohAEQ+FCNFqpqi4NdpdX7eHy0+R/CVMcMPIqqKTpQo++96dEG2 +V8YR/zTAAonvLtiRkjthtLCnjp0CgYEA5qJauLacPVcGIBN/KfV4aEOa/L8Cr2kf +M3Zsw28fKHxghU44ArPjlml6Lcicp7IXE/vspwclIvYb599e7SdxaQHW6AWAWho/ +dF3yqE/2HN2aPWH4m98YFcNHYxtNGHgCO7hVV021lnw8jAR4m1oRQKSI43eIOl59 +5DJ6Gv7fSusCgYBmcRb664zH4lHS5T83rzIRhKXmpU9jbUU78h7cTn2SycMgc/+I +uKZPsM447V8Zfn9yMu5uptoF7fGVkWhGBA6IKN19rcIA39Km+sFgvqIUd5SPxfvn +Zi1uivXfGn3wngMh6h3yweghn7m4xVXzScdaFgpLxEFlnc9TMRbmEZdeXQKBgAMJ +ekZaaT5JihQcDZ2g1OASm9TeMwvaR4Xm9lGwgemHkcHPoN8wPTv60ZgOvzlaGAG0 +XI5qgquuL/nisB5RWaX3Vzwg7mrBU7qVjh93RhdlN6W9R4fN7UREGQmOD3rWAbmF +mOIYbN65bhat7GSnT/jY8dCE/289VU0O+Rqn5orXAoGBAKQq+E9s+avF1OZ8QMVk +PxmoXY4DC6pttbFKTQrlVoBAaRnCUpf4KS1MHzrl8APk5wEP6Czk2k+IVqA2eqGg +cb9HNA2jzX7JkJC3MXS4bXRbZugq/H6imFd+EmRR9KmMD6QF7NHXzKqxO4O7Ve7N +rex3JnPj7odUG+Mh4Z3johRk +-----END PRIVATE KEY----- diff --git a/data/member-b/peer-certs/org-a.pem b/data/member-b/peer-certs/org-a.pem new file mode 100644 index 0000000..f984b1b --- /dev/null +++ b/data/member-b/peer-certs/org-a.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICxDCCAawCCQCwQ58VBeUg+TANBgkqhkiG9w0BAQsFADAkMRIwEAYDVQQDDAls +b2NhbGhvc3QxDjAMBgNVBAoMBW9yZy1hMB4XDTIxMDUyMDAyMjUzN1oXDTIyMDUy +MDAyMjUzN1owJDESMBAGA1UEAwwJbG9jYWxob3N0MQ4wDAYDVQQKDAVvcmctYTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK/bZPBvaA+V6HoXNP+oOQaK +DPuLgFO3R7TTIBTCJttELv1lbfuw95Up+/5b4dPnnIF3wwE2GZsiMWJV7RgfDAUi +x7saJJIIa2E8gLrmoaG99w+PYQW41OBz0p54tw4abN5RNsgineu1N5pIJOMJ6cMs +D8IYfWSHDjZZvu3F2YCItSVYZlfmGBCcFCf4HASF6m4lUTZCDRBurjOQrwW9mXJv +hkIa1HEa4l7Nq09d8Bokvieq1vHJUB78kYTR027z+sm4H2o4pXjzBOaV/z2yB7+t +yqWbMjm8aq2m/gmpZTjDBPgX9XLMYHWBZWCpk1iHo/eqw+UeZ6kRZ0RR78Co+IsC +AwEAATANBgkqhkiG9w0BAQsFAAOCAQEAH6rQx8khtfzH93YCstHduvEzUCHSyifF +sDyRLvSmS0qHz1liLJyvWbT9xDQCpDZrxhQKJLN2eZxxlNaH6XBEfHQgi2I43hxu +sd2KZ4wOWOk/HyM9BcibKNMtfHJdgQ5EYRc6OWDY1c8bQQfRJUBzrSJKldqfQjqC +mPEeHXDH++yTg2Vfm7GZiogxqSWn/+ILzHNeWrvr0HJ86Guyg/NPKBxs0uasvgI7 +KqW+fcZ/9Vg6a4e+zRTL8EwYBX6dTSwgt4X9wuwMvt/K0qodgW6I4paqEpVJOS+d +Z6WevHgHwZTr+mS3mJ6Y6u/DBD8/06uPAf3T5d/dC6Xtl4sEFkpNAA== +-----END CERTIFICATE----- diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..e964528 --- /dev/null +++ b/nodemon.json @@ -0,0 +1,6 @@ +{ + "watch": ["src"], + "ext": ".ts,.js", + "ignore": [], + "exec": "ts-node --files ./src/index.ts" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c5983ba --- /dev/null +++ b/package-lock.json @@ -0,0 +1,862 @@ +{ + "name": "blob-exchange", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/bunyan": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.6.tgz", + "integrity": "sha512-YiozPOOsS6bIuz31ilYqR5SlLif4TBWsousN2aCWLi5233nZSX19tFbcQUPdR7xJ8ypPyxkCGNxg0CIV5n9qxQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/busboy": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-0.2.3.tgz", + "integrity": "sha1-ZpetKYcyRsUw8Jo/9aQIYYJCMNU=", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", + "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/express": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.11.tgz", + "integrity": "sha512-no+R6rW60JEc59977wIxreQVsIEOAYwgCqldrA/vkpCnbD7MqTefO97lmoBe4WE0F156bC4uLSP1XHDOySnChg==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.19.tgz", + "integrity": "sha512-DJOSHzX7pCiSElWaGR8kCprwibCB/3yW6vcT8VG3P0SJjnv19gnWG/AZMfM60Xj/YJIp/YCaDHyvzsFVeniARA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "dev": true + }, + "@types/node": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.3.tgz", + "integrity": "sha512-/WbxFeBU+0F79z9RdEOXH4CsDga+ibi5M8uEYr91u3CkT/pdWcV8MCook+4wDPnZBexRdwWS+PiVZ2xJviAzcQ==", + "dev": true + }, + "@types/qs": { + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz", + "integrity": "sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", + "dev": true + }, + "@types/serve-static": { + "version": "1.13.9", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz", + "integrity": "sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA==", + "dev": true, + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "@types/swagger-ui-express": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.2.tgz", + "integrity": "sha512-t9teFTU8dKe69rX9EwL6OM2hbVquYdFM+sQ0REny4RalPlxAm+zyP04B12j4c7qEuDS6CnlwICywqWStPA3v4g==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "@types/ws": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.4.tgz", + "integrity": "sha512-d/7W23JAXPodQNbOZNXvl2K+bqAQrCMwlh/nuQsPSQk6Fq0opHoPrUw43aHsvSbIiQPr8Of2hkFbnz1XBFVyZQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/yamljs": { + "version": "0.2.31", + "resolved": "https://registry.npmjs.org/@types/yamljs/-/yamljs-0.2.31.tgz", + "integrity": "sha512-QcJ5ZczaXAqbVD3o8mw/mEBhRvO5UAdTtbvgwL/OgoWubvNBh6/MxLBAigtcgIFaq3shon9m3POIxQaLQt4fxQ==", + "dev": true + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "ajv": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.4.0.tgz", + "integrity": "sha512-7QD2l6+KBSLwf+7MuYocbWvRPdOu63/trReTLu2KFwkgctnub1auoF+Y1WYcm09CTM7quuscrzqmASaLHC/K4Q==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "bunyan": { + "version": "1.8.15", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz", + "integrity": "sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==", + "requires": { + "dtrace-provider": "~0.8", + "moment": "^2.19.3", + "mv": "~2", + "safe-json-stringify": "~1" + } + }, + "busboy": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.3.1.tgz", + "integrity": "sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw==", + "requires": { + "dicer": "0.3.0" + } + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "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=" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "dicer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", + "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==", + "requires": { + "streamsearch": "0.1.2" + } + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" + }, + "dtrace-provider": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", + "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", + "optional": true, + "requires": { + "nan": "^2.14.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "follow-redirects": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", + "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "optional": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz", + "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==" + }, + "mime-types": { + "version": "2.1.30", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz", + "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==", + "requires": { + "mime-db": "1.47.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "optional": true + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "optional": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "moment": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", + "optional": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "optional": true, + "requires": { + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" + } + }, + "nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "optional": true + }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "optional": true + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, + "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.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, + "rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "optional": true, + "requires": { + "glob": "^6.0.1" + } + }, + "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==" + }, + "safe-json-stringify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, + "swagger-ui-dist": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.49.0.tgz", + "integrity": "sha512-R1+eT16XNP1bBLfacISifZAkFJlpwvWsS2vVurF5pbIFZnmCasD/hj+9r/q7urYdQyb0B6v11mDnuYU7rUpfQg==" + }, + "swagger-ui-express": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz", + "integrity": "sha512-Xs2BGGudvDBtL7RXcYtNvHsFtP1DBFPMJFRxHe5ez/VG/rzVOEjazJOOSc/kSCyxreCTKfJrII6MJlL9a6t8vw==", + "requires": { + "swagger-ui-dist": "^3.18.1" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "ts-node": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", + "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "requires": { + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + } + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typescript": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "requires": { + "punycode": "^2.1.0" + } + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", + "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==" + }, + "yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "requires": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + }, + "dependencies": { + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6119b47 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "blob-exchange", + "version": "1.0.0", + "description": "Blob Exchange", + "main": "index.js", + "scripts": { + "clean": "rimraf ./build", + "copy-swagger": "cp ./src/swagger.yaml ./build", + "build": "npm run clean && tsc && npm run copy-swagger", + "start:dev": "nodemon", + "start": "node build/index.js", + "dev": "ts-node src/index.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "ajv": "^8.4.0", + "axios": "^0.21.1", + "bunyan": "^1.8.15", + "busboy": "^0.3.1", + "express": "^4.17.1", + "form-data": "^4.0.0", + "swagger-ui-express": "^4.1.6", + "ts-node": "^9.1.1", + "typescript": "^4.2.4", + "ws": "^7.4.5", + "yamljs": "^0.3.0" + }, + "devDependencies": { + "@types/bunyan": "^1.8.6", + "@types/busboy": "^0.2.3", + "@types/express": "^4.17.11", + "@types/node": "^15.0.3", + "@types/swagger-ui-express": "^4.1.2", + "@types/ws": "^7.4.4", + "@types/yamljs": "^0.2.31" + } +} diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..274cd72 --- /dev/null +++ b/src/app.ts @@ -0,0 +1,96 @@ +import express from 'express'; +import https from 'https'; +import http from 'http'; +import WebSocket from 'ws'; +import { init as initConfig, config } from './lib/config'; +import { init as initCert, key, cert, ca } from './lib/cert'; +import { createLogger, LogLevelString } from 'bunyan'; +import * as utils from './lib/utils'; +import { router as apiRouter } from './routers/api'; +import { router as p2pRouter, eventEmitter as p2pEventEmitter } from './routers/p2p'; +import RequestError, { errorHandler } from './lib/request-error'; +import * as eventsHandler from './handlers/events' +import { eventEmitter as blobsEventEmitter } from './handlers/blobs'; +import { eventEmitter as messagesEventEmitter } from './handlers/messages'; +import swaggerUi from 'swagger-ui-express'; +import YAML from 'yamljs'; +import path from 'path'; + +const log = createLogger({ name: 'app.ts', level: utils.constants.LOG_LEVEL as LogLevelString }); + +const swaggerDocument = YAML.load(path.join(__dirname, './swagger.yaml')); + +export const start = async () => { + await initConfig(); + await initCert(); + + const apiApp = express(); + const apiServer = http.createServer(apiApp); + + const p2pApp = express(); + const p2pServer = https.createServer({ + key, + cert, + ca, + rejectUnauthorized: true, + requestCert: true, + }, p2pApp); + + const wss = new WebSocket.Server({ server: apiServer }); + + p2pEventEmitter.addListener('event', event => eventsHandler.queueEvent(event)); + blobsEventEmitter.addListener('event', event => eventsHandler.queueEvent(event)); + messagesEventEmitter.addListener('event', event => eventsHandler.queueEvent(event)); + eventsHandler.eventEmitter.addListener('event', event => wss.clients.forEach(client => client.send(JSON.stringify(event)))); + + wss.on('connection', (webSocket: WebSocket) => { + const event = eventsHandler.getCurrentEvent(); + if(event !== undefined) { + webSocket.send(JSON.stringify(event)); + } + + webSocket.on('message', async message => { + try { + const messageContent = JSON.parse(message.toLocaleString()); + if(messageContent.action === 'commit') { + eventsHandler.handleCommit(); + } + } catch (err) { + log.error(`Failed to process websocket message ${err}`); + } + }); + + }); + + apiApp.use('/swagger', swaggerUi.serve, swaggerUi.setup(swaggerDocument, { + swaggerOptions: { + authAction :{ JWT: {name: "JWT", schema: {type: "apiKey", in: "header", name: "Authorization", description: ""}, value: "Bearer "} } + } + })); + + apiApp.use((req, res, next) => { + if(req.path === '/') { + res.redirect('/swagger'); + } else { + if (req.headers['authorization'] !== `Bearer ${config.apiKey}`) { + next(new RequestError('Unauthorized', 401)); + } else { + next(); + } + } + }); + + apiApp.use(express.urlencoded({ extended: true })); + apiApp.use(express.json()); + apiApp.use('/api/v1', apiRouter); + apiApp.use(errorHandler); + + p2pApp.use('/api/v1', p2pRouter); + p2pApp.use(errorHandler); + + const apiServerPromise = new Promise(resolve => apiServer.listen(config.apiPort, () => resolve())); + const p2pServerPromise = new Promise(resolve => p2pServer.listen(config.p2pPort, () => resolve())); + await Promise.all([apiServerPromise, p2pServerPromise]); + log.info(`Blob exchange listening on ports ${config.apiPort} (API) and ${config.p2pPort} (P2P) - log level "${utils.constants.LOG_LEVEL}"`); + +}; diff --git a/src/custom.d.ts b/src/custom.d.ts new file mode 100644 index 0000000..3eaac9a --- /dev/null +++ b/src/custom.d.ts @@ -0,0 +1,21 @@ +export {} + +declare global{ + namespace Express { + export interface Request { + client: { + authorized: boolean + getCertificate: () => { + issuer: { + O: string + } + }, + getPeerCertificate: () => { + issuer: { + O: string + } + } + } + } + } +} diff --git a/src/handlers/blobs.ts b/src/handlers/blobs.ts new file mode 100644 index 0000000..765927b --- /dev/null +++ b/src/handlers/blobs.ts @@ -0,0 +1,94 @@ +import { promises as fs, createReadStream, createWriteStream } from 'fs'; +import path from 'path'; +import * as utils from '../lib/utils'; +import { BlobTask, IBlobDeliveredEvent, IBlobFailedEvent, IFile } from "../lib/interfaces"; +import stream from 'stream'; +import RequestError from '../lib/request-error'; +import crypto from 'crypto'; +import FormData from 'form-data'; +import https from 'https'; +import { key, cert, ca } from '../lib/cert'; +import { createLogger, LogLevelString } from 'bunyan'; +import EventEmitter from 'events'; + +const log = createLogger({ name: 'handlers/blobs.ts', level: utils.constants.LOG_LEVEL as LogLevelString }); + +let blobQueue: BlobTask[] = []; +let sending = false; +export const eventEmitter = new EventEmitter(); + +export const retreiveBlob = async (filePath: string) => { + const resolvedFilePath = path.join(utils.constants.DATA_DIRECTORY, utils.constants.BLOBS_SUBDIRECTORY, filePath); + if (!(await utils.fileExists(resolvedFilePath))) { + throw new RequestError(`Blob not found`, 404); + } + return createReadStream(resolvedFilePath); +}; + +export const storeBlob = async (file: IFile, filePath: string) => { + const resolvedFilePath = path.join(utils.constants.DATA_DIRECTORY, utils.constants.BLOBS_SUBDIRECTORY, filePath); + await fs.mkdir(path.parse(resolvedFilePath).dir, { recursive: true }); + let hash = crypto.createHash(utils.constants.TRANSFER_HASH_ALGORITHM); + let hashCalculator = new stream.Transform({ + async transform(chunk, _enc, cb) { + hash.update(chunk); + cb(undefined, chunk); + } + }); + const writeStream = createWriteStream(resolvedFilePath); + return new Promise((resolve, reject) => { + file.readableStream.on('end', () => { + resolve(hash.digest('hex')); + }).on('error', err => { + reject(err); + }); + file.readableStream.pipe(hashCalculator).pipe(writeStream); + }); +}; + +export const sendBlob = async (blobPath: string, recipient: string, recipientURL: string) => { + if (sending) { + blobQueue.push({ blobPath, recipient, recipientURL }); + } else { + sending = true; + blobQueue.push({ blobPath, recipient, recipientURL }); + while (blobQueue.length > 0) { + await deliverBlob(blobQueue.shift()!); + } + sending = false; + } +}; + +export const deliverBlob = async ({ blobPath, recipient, recipientURL }: BlobTask) => { + const resolvedFilePath = path.join(utils.constants.DATA_DIRECTORY, utils.constants.BLOBS_SUBDIRECTORY, blobPath); + if (!(await utils.fileExists(resolvedFilePath))) { + throw new RequestError('Blob not found', 404); + } + const stream = createReadStream(resolvedFilePath); + const formData = new FormData(); + formData.append('blob', stream); + const httpsAgent = new https.Agent({ cert, key, ca }); + log.trace(`Delivering blob ${blobPath} to ${recipient} at ${recipientURL}`); + try { + await utils.axiosWithRetry({ + method: 'put', + url: `${recipientURL}/api/v1/blobs${blobPath}`, + data: formData, + headers: formData.getHeaders(), + httpsAgent + }); + eventEmitter.emit('event', { + type: 'blob-delivered', + path: blobPath, + recipient + } as IBlobDeliveredEvent); + log.trace(`Blob delivered`); + } catch (err) { + eventEmitter.emit('event', { + type: 'blob-failed', + path: blobPath, + recipient + } as IBlobFailedEvent); + log.error(`Failed to deliver blob ${err}`); + } +}; diff --git a/src/handlers/events.ts b/src/handlers/events.ts new file mode 100644 index 0000000..63a0b4c --- /dev/null +++ b/src/handlers/events.ts @@ -0,0 +1,37 @@ +import { createLogger, LogLevelString } from "bunyan"; +import EventEmitter from "events"; +import { OutboundEvent } from "../lib/interfaces"; +import * as utils from '../lib/utils'; + +const log = createLogger({ name: 'handlers/events.ts', level: utils.constants.LOG_LEVEL as LogLevelString }); + +let eventQueue: OutboundEvent[] = []; +export const eventEmitter = new EventEmitter(); + +export const queueEvent = (socketEvent: OutboundEvent) => { + if(eventQueue.length < utils.constants.MAX_EVENT_QUEUE_SIZE) { + eventQueue.push(socketEvent); + if(eventQueue.length === 1) { + eventEmitter.emit('event', eventQueue[0]); + } + } else { + log.warn('Max queue size reached'); + } +}; + +export const handleCommit = () => { + eventQueue.shift(); + if(eventQueue.length > 0) { + eventEmitter.emit('event', eventQueue[0]); + } +} + +export const getCurrentEvent = () => { + if(eventQueue.length > 0) { + return eventQueue[0]; + } +}; + +export const getQueueSize = () => { + return eventQueue.length; +}; diff --git a/src/handlers/messages.ts b/src/handlers/messages.ts new file mode 100644 index 0000000..d4b5fce --- /dev/null +++ b/src/handlers/messages.ts @@ -0,0 +1,55 @@ +import https from 'https'; +import * as utils from '../lib/utils'; +import { key, cert, ca } from '../lib/cert'; +import { IMessageDeliveredEvent, IMessageFailedEvent, MessageTask } from '../lib/interfaces'; +import FormData from 'form-data'; +import EventEmitter from 'events'; +import { createLogger, LogLevelString } from 'bunyan'; + +const log = createLogger({ name: 'handlers/messages.ts', level: utils.constants.LOG_LEVEL as LogLevelString }); + +let messageQueue: MessageTask[] = []; +let sending = false; +export const eventEmitter = new EventEmitter(); + +export const sendMessage = async (message: string, recipient: string, recipientURL: string) => { + if (sending) { + messageQueue.push({ message, recipient, recipientURL }); + } else { + sending = true; + messageQueue.push({ message, recipient, recipientURL }); + while (messageQueue.length > 0) { + await deliverMessage(messageQueue.shift()!); + } + sending = false; + } +}; + +export const deliverMessage = async ({ message, recipient, recipientURL }: MessageTask) => { + const httpsAgent = new https.Agent({ cert, key, ca }); + const formData = new FormData(); + formData.append('message', message); + log.trace(`Delivering message to ${recipient} at ${recipientURL}`); + try { + await utils.axiosWithRetry({ + method: 'post', + url: `${recipientURL}/api/v1/messages`, + data: formData, + headers: formData.getHeaders(), + httpsAgent + }); + eventEmitter.emit('event', { + type: 'message-delivered', + message, + recipient + } as IMessageDeliveredEvent); + log.trace(`Message delivered`); + } catch(err) { + eventEmitter.emit('event', { + type: 'message-failed', + message, + recipient + } as IMessageFailedEvent); + log.error(`Failed to deliver message ${err}`); + } +}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..d6e8286 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,9 @@ +import { createLogger, LogLevelString } from 'bunyan'; +import * as utils from './lib/utils'; +import { start } from './app'; + +const log = createLogger({ name: 'index.ts', level: utils.constants.LOG_LEVEL as LogLevelString }); + +start().catch(err => { + log.error(`Failed to start blob exchange ${err}`); +}); diff --git a/src/lib/cert.ts b/src/lib/cert.ts new file mode 100644 index 0000000..f604c81 --- /dev/null +++ b/src/lib/cert.ts @@ -0,0 +1,21 @@ +import * as utils from '../lib/utils'; +import { promises as fs } from 'fs'; +import path from 'path'; +import { createLogger, LogLevelString } from 'bunyan'; + +const log = createLogger({ name: 'lib/certs.ts', level: utils.constants.LOG_LEVEL as LogLevelString }); + +export let key: string; +export let cert: string; +export let ca: string[] = []; + +export const init = async () => { + key = (await fs.readFile(path.join(utils.constants.DATA_DIRECTORY, utils.constants.KEY_FILE))).toString(); + cert = (await fs.readFile(path.join(utils.constants.DATA_DIRECTORY, utils.constants.CERT_FILE))).toString(); + const peerCertsPath = path.join(utils.constants.DATA_DIRECTORY, utils.constants.PEER_CERTS_SUBDIRECTORY); + const peerCerts = await fs.readdir(peerCertsPath); + for(const peerCert of peerCerts) { + ca.push((await fs.readFile(path.join(peerCertsPath, peerCert))).toString()); + } + log.debug(`Loaded ${ca.length} peer certificate(s)`); +}; diff --git a/src/lib/config.ts b/src/lib/config.ts new file mode 100644 index 0000000..0dfa265 --- /dev/null +++ b/src/lib/config.ts @@ -0,0 +1,36 @@ +import { promisify } from 'util'; +import { readFile } from 'fs'; +import Ajv from 'ajv'; +import configSchema from '../schemas/config.json'; +import * as utils from './utils'; +import { IConfig } from './interfaces'; +import path from 'path'; + +const asyncReadFile = promisify(readFile); +const ajv = new Ajv(); +const validateConfig = ajv.compile(configSchema); +const configFilePath = path.join(utils.constants.DATA_DIRECTORY, utils.constants.CONFIG_FILE_NAME); + +export let config: IConfig; + +export const init = async () => { + await loadConfigFile(); +}; + +const loadConfigFile = async () => { + try { + const data = JSON.parse(await asyncReadFile(configFilePath, 'utf8')); + if(validateConfig(data)) { + config = data as IConfig; + for(const peer of config.peers) { + if(peer.endpoint.endsWith('/')) { + peer.endpoint = peer.endpoint.slice(-0, -1); + } + } + } else { + throw new Error('Invalid configuration file'); + } + } catch(err) { + throw new Error(`Failed to read configuration file. ${err}`); + } +}; diff --git a/src/lib/interfaces.ts b/src/lib/interfaces.ts new file mode 100644 index 0000000..7ba5f04 --- /dev/null +++ b/src/lib/interfaces.ts @@ -0,0 +1,97 @@ +import { AxiosRequestConfig } from "axios" +import FormData from "form-data" + +export interface IConfig { + apiPort: number + p2pPort: number + apiKey: string + peers: { + name: string + endpoint: string + }[] +} + +export interface IFile { + key: string + name: string + readableStream: NodeJS.ReadableStream +} + +export type OutboundEvent = + IMessageReceivedEvent | + IMessageDeliveredEvent | + IMessageFailedEvent | + IBlobReceivedEvent | + IBlobDeliveredEvent | + IBlobFailedEvent + +export interface IMessageReceivedEvent { + type: 'message-received' + sender: string + message: string +} + +export interface IMessageDeliveredEvent { + type: 'message-delivered' + recipient: string + message: string +} + +export interface IMessageFailedEvent { + type: 'message-failed' + recipient: string + message: string +} + +export interface IBlobReceivedEvent { + type: 'blob-received' + sender: string + path: string + hash: string +} + +export interface IBlobDeliveredEvent { + type: 'blob-delivered' + recipient: string + path: string +} + +export interface IBlobFailedEvent { + type: 'blob-failed' + recipient: string + path: string +} + +export type InboundEvent = + IMessageEvent | + ICommitEvent + +export interface IMessageEvent { + type: 'message' + recipient: string + message: string +} + +export interface ICommitEvent { + type: 'commit' +} + +export type MessageTask = { + message: string + recipient: string + recipientURL: string +} + +export type BlobTask = { + blobPath: string + recipient: string + recipientURL: string +} + +export interface IStatus { + messageQueueSize: number + peers: { + name: string + available: boolean + }[] +} diff --git a/src/lib/request-error.ts b/src/lib/request-error.ts new file mode 100644 index 0000000..a677f56 --- /dev/null +++ b/src/lib/request-error.ts @@ -0,0 +1,22 @@ +import { NextFunction, Request, Response } from 'express'; + +export default class RequestError extends Error { + + details?: object; + responseCode: number; + + constructor(message: string, responseCode = 500, details?: any) { + super(message); + this.details = details; + this.responseCode = responseCode; + } + +} + +export const errorHandler = (err: Error, _req: Request, res: Response, _next: NextFunction) => { + if(err instanceof RequestError) { + res.status(err.responseCode).send({error: err.message, details: err.details}); + } else { + res.status(500).send({ error: err.message }); + } +}; \ No newline at end of file diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..4c1e089 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,100 @@ +import { Request } from 'express'; +import { promises as fs } from 'fs'; +import { IFile } from './interfaces'; +import RequestError from './request-error'; +import Busboy from 'busboy'; +import axios, { AxiosRequestConfig } from 'axios'; +import { createLogger, LogLevelString } from 'bunyan'; + +export const constants = { + LOG_LEVEL: process.env.LOG_LEVEL || 'info', + DATA_DIRECTORY: process.env.DATA_DIRECTORY || '/data', + PEER_CERTS_SUBDIRECTORY: 'peer-certs', + BLOBS_SUBDIRECTORY: 'blobs', + RECEIVED_BLOBS_SUBDIRECTORY: 'received', + CONFIG_FILE_NAME: 'config.json', + CERT_FILE: 'cert.pem', + KEY_FILE: 'key.pem', + CA_FILE: 'ca.pem', + TRANSFER_HASH_ALGORITHM: 'sha256', + REST_API_CALL_MAX_ATTEMPTS: 5, + REST_API_CALL_RETRY_DELAY_MS: 500, + MAX_EVENT_QUEUE_SIZE: 1000 +}; +const log = createLogger({ name: 'utils.ts', level: constants.LOG_LEVEL as LogLevelString }); + +export const regexp = { + FILE_KEY: /^(\/[a-z0-9+-_.]+)+$/ +}; + +export const fileExists = async (filePath: string): Promise => { + try { + const stats = await fs.stat(filePath); + return !stats.isDirectory(); + } catch (err) { + if (err.errno === -2) { + return false; + } else { + throw err; + } + } + return true; +} + +export const extractFileFromMultipartForm = (req: Request): Promise => { + return new Promise(async (resolve, reject) => { + let fileFound = false; + req.pipe(new Busboy({ headers: req.headers }) + .on('file', (fieldname, readableStream, fileName) => { + fileFound = true; + resolve({ + key: fieldname, + name: fileName, + readableStream + }); + })).on('finish', () => { + if (!fileFound) { + reject(new RequestError('Missing blob', 400)); + } + }); + }); +}; + +export const extractMessageFromMultipartForm = (req: Request): Promise => { + return new Promise(async (resolve, reject) => { + let fieldFound = false; + req.pipe(new Busboy({ headers: req.headers }) + .on('field', (fieldname, value) => { + if(fieldname === 'message') { + fieldFound = true; + resolve(value); + } + })).on('finish', () => { + if (!fieldFound) { + reject(new RequestError('Missing message', 400)); + } + }); + }); +}; + +export const axiosWithRetry = async (config: AxiosRequestConfig) => { + let attempts = 0; + let currentError; + while (attempts < constants.REST_API_CALL_MAX_ATTEMPTS) { + try { + log.debug(`${config.method} ${config.url}`); + return await axios(config); + } catch (err) { + const data = err.response?.data; + log.error(`${config.method} ${config.url} attempt ${attempts} [${err.response?.status}]`, (data && !data.on) ? data : err.stack); + if (err.response?.status === 404) { + throw err; + } else { + currentError = err; + attempts++; + await new Promise(resolve => setTimeout(resolve, constants.REST_API_CALL_RETRY_DELAY_MS)); + } + } + } + throw currentError; +}; diff --git a/src/routers/api.ts b/src/routers/api.ts new file mode 100644 index 0000000..fe45ff1 --- /dev/null +++ b/src/routers/api.ts @@ -0,0 +1,102 @@ +import { Router } from 'express'; +import * as blobsHandler from '../handlers/blobs'; +import * as messagesHandler from '../handlers/messages'; +import * as utils from '../lib/utils'; +import RequestError from '../lib/request-error'; +import { config } from '../lib/config'; +import { IStatus } from '../lib/interfaces'; +import https from 'https'; +import { key, cert, ca } from '../lib/cert'; +import * as eventsHandler from '../handlers/events'; + +export const router = Router(); + +router.get('/status', async (_req, res, next) => { + try { + let status: IStatus = { + messageQueueSize: eventsHandler.getQueueSize(), + peers: [] + }; + let promises = []; + const httpsAgent = new https.Agent({ cert, key, ca }); + for (const peer of config.peers) { + promises.push(utils.axiosWithRetry({ + method: 'head', + url: `${peer.endpoint}/api/v1/ping`, + httpsAgent + })); + } + const responses = await (Promise as any).allSettled(promises); + let i = 0; + for (const peer of config.peers) { + status.peers.push({ + name: peer.name, + available: responses[i++].status === 'fulfilled' + }) + } + res.send(status); + } catch (err) { + next(err); + } +}); + +router.post('/messages', async (req, res, next) => { + try { + if (req.body.message === undefined) { + throw new RequestError('Missing message', 400); + } + if (req.body.recipient === undefined) { + throw new RequestError('Missing recipient', 400); + } + let recipientURL = config.peers.find(peer => peer.name === req.body.recipient)?.endpoint; + if (recipientURL === undefined) { + throw new RequestError(`Unknown recipient`, 400); + } + messagesHandler.sendMessage(req.body.message, req.body.recipient, recipientURL); + res.send({ status: 'submitted' }); + } catch (err) { + next(err); + } +}); + +router.get('/blobs/*', async (req, res, next) => { + try { + let blobStream = await blobsHandler.retreiveBlob(req.params[0]); + blobStream.on('end', () => res.end()); + blobStream.pipe(res); + } catch (err) { + next(err); + } +}); + +router.put('/blobs/*', async (req, res, next) => { + try { + const file = await utils.extractFileFromMultipartForm(req); + const hash = await blobsHandler.storeBlob(file, req.params[0]); + res.send({ hash }); + } catch (err) { + next(err); + } +}); + +router.post('/transfers', async (req, res, next) => { + try { + if (req.body.path === undefined) { + throw new RequestError('Missing path', 400); + } + if (!utils.regexp.FILE_KEY.test(req.body.path)) { + throw new RequestError('Invalid path', 400); + } + if (req.body.recipient === undefined) { + throw new RequestError('Missing recipient', 400); + } + let recipientURL = config.peers.find(peer => peer.name === req.body.recipient)?.endpoint; + if (recipientURL === undefined) { + throw new RequestError(`Unknown recipient`, 400); + } + blobsHandler.sendBlob(req.body.path, req.body.recipient, recipientURL); + res.send({ status: 'submitted' }); + } catch (err) { + next(err); + } +}); diff --git a/src/routers/p2p.ts b/src/routers/p2p.ts new file mode 100644 index 0000000..20b88c4 --- /dev/null +++ b/src/routers/p2p.ts @@ -0,0 +1,48 @@ +import { Router } from 'express'; +import * as utils from '../lib/utils'; +import * as blobsHandler from '../handlers/blobs'; +import path from 'path'; +import { EventEmitter } from 'events'; +import { IBlobReceivedEvent, IMessageReceivedEvent } from '../lib/interfaces'; + +export const router = Router(); +export const eventEmitter = new EventEmitter(); + +router.head('/ping', (_req, res) => { + res.sendStatus(204); +}); + +router.post('/messages', async (req, res, next) => { + try { + const cert = req.client.getPeerCertificate(); + const sender = cert.issuer.O; + const message = await utils.extractMessageFromMultipartForm(req); + eventEmitter.emit('event', { + type: 'message-received', + sender, + message + } as IMessageReceivedEvent); + res.sendStatus(204); + } catch (err) { + next(err); + } +}); + +router.put('/blobs/*', async (req, res, next) => { + try { + const cert = req.client.getPeerCertificate(); + const sender = cert.issuer.O; + const file = await utils.extractFileFromMultipartForm(req); + const blobPath = path.join(utils.constants.RECEIVED_BLOBS_SUBDIRECTORY, sender, req.params[0]); + const hash = await blobsHandler.storeBlob(file, blobPath); + res.sendStatus(204); + eventEmitter.emit('event', { + type: 'blob-received', + sender, + path: blobPath, + hash + } as IBlobReceivedEvent); + } catch (err) { + next(err); + } +}); diff --git a/src/schemas/config.json b/src/schemas/config.json new file mode 100644 index 0000000..85e74db --- /dev/null +++ b/src/schemas/config.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "apiPort", + "p2pPort", + "apiKey", + "peers" + ], + "properties": { + "port": { + "type": "integer" + }, + "apiKey": { + "type": "string" + }, + "peers": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "endpoint" + ], + "properties": { + "name": { + "type": "string" + }, + "endpoint": { + "type": "string" + } + } + } + } + } +} diff --git a/src/swagger.yaml b/src/swagger.yaml new file mode 100644 index 0000000..c34ed4d --- /dev/null +++ b/src/swagger.yaml @@ -0,0 +1,238 @@ +--- + openapi: 3.0.0 + info: + version: '1.0' + title: Data Exchange API + description: To invoke the API programmatically, include the API key as Bearer token in the authorization header. + servers: + - url: /api/v1 + paths: + /status: + get: + tags: + - Status + description: Status + responses: + '200': + description: Status + content: + application/json: + schema: + $ref: '#/components/schemas/Status' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /messages: + post: + tags: + - Messages + description: Send message + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + responses: + '200': + description: Message submitted + content: + application/json: + schema: + $ref: '#/components/schemas/Submitted' + '400': + description: Invalid message or recipient + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /blobs/{blobPath}: + get: + tags: + - Blobs + description: Retreive blob + parameters: + - in: path + name: blobPath + required: true + schema: + type: string + description: Blob path + responses: + '200': + description: Blob content + content: + application/json: + schema: + type: string + format: binary + '404': + description: Blob not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + put: + tags: + - Blobs + description: Store blob + parameters: + - in: path + name: blobPath + required: true + schema: + type: string + description: Blob path + requestBody: + description: Blob + required: true + content: + multipart/form-data: + schema: + type: object + properties: + fileName: + type: string + format: binary + responses: + '200': + description: Blob hash + content: + application/json: + schema: + $ref: '#/components/schemas/BlobHash' + '400': + description: Missing blob + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /transfers: + post: + tags: + - Transfers + description: Transfer blob + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Transfer' + responses: + '200': + description: Transfer submitted + content: + application/json: + schema: + $ref: '#/components/schemas/Submitted' + '400': + description: Invalid path or recipient + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Blob not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +# schemas + components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + schemas: + Status: + type: object + required: + - messageQueueSize + - peers + properties: + messageQueueSize: + type: integer + peers: + type: array + items: + type: object + required: + - name + - available + properties: + name: + type: string + available: + type: boolean + Message: + type: object + required: + - message + - recipient + properties: + message: + type: string + recipient: + type: string + BlobHash: + type: object + required: + - hash + properties: + hash: + type: string + Transfer: + type: object + required: + - path + - recipient + properties: + path: + type: string + recipient: + type: string + Submitted: + type: object + required: + - status + properties: + status: + type: string + enum: ['submitted'] + Error: + type: object + required: + - error + properties: + error: + type: string + security: + - bearerAuth: [] \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0c16fd4 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "lib": ["es6"], + "allowJs": true, + "outDir": "build", + "rootDir": "src", + "strict": true, + "sourceMap": true, + "strictNullChecks": true, + "noImplicitAny": true, + "esModuleInterop": true, + "resolveJsonModule": true, + // "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true + } +} \ No newline at end of file From 38d17d8d8c391c8446f4f4be487efe5acfc75947 Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Mon, 24 May 2021 22:26:05 -0400 Subject: [PATCH 02/19] Add webSocket authorization --- src/app.ts | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/app.ts b/src/app.ts index 274cd72..e50e8ec 100644 --- a/src/app.ts +++ b/src/app.ts @@ -36,23 +36,32 @@ export const start = async () => { requestCert: true, }, p2pApp); - const wss = new WebSocket.Server({ server: apiServer }); + const wss = new WebSocket.Server({ + server: apiServer, verifyClient: (info, cb) => { + if (info.req.headers.authorization === `Bearer ${config.apiKey}`) { + cb(true); + } else { + cb(false, 401, 'Unauthorized'); + } + } + }); - p2pEventEmitter.addListener('event', event => eventsHandler.queueEvent(event)); - blobsEventEmitter.addListener('event', event => eventsHandler.queueEvent(event)); - messagesEventEmitter.addListener('event', event => eventsHandler.queueEvent(event)); + p2pEventEmitter.addListener('event', event => eventsHandler.queueEvent(event)); + blobsEventEmitter.addListener('event', event => eventsHandler.queueEvent(event)); + messagesEventEmitter.addListener('event', event => eventsHandler.queueEvent(event)); eventsHandler.eventEmitter.addListener('event', event => wss.clients.forEach(client => client.send(JSON.stringify(event)))); wss.on('connection', (webSocket: WebSocket) => { + const event = eventsHandler.getCurrentEvent(); - if(event !== undefined) { + if (event !== undefined) { webSocket.send(JSON.stringify(event)); } webSocket.on('message', async message => { try { const messageContent = JSON.parse(message.toLocaleString()); - if(messageContent.action === 'commit') { + if (messageContent.action === 'commit') { eventsHandler.handleCommit(); } } catch (err) { @@ -62,14 +71,10 @@ export const start = async () => { }); - apiApp.use('/swagger', swaggerUi.serve, swaggerUi.setup(swaggerDocument, { - swaggerOptions: { - authAction :{ JWT: {name: "JWT", schema: {type: "apiKey", in: "header", name: "Authorization", description: ""}, value: "Bearer "} } - } - })); + apiApp.use('/swagger', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); apiApp.use((req, res, next) => { - if(req.path === '/') { + if (req.path === '/') { res.redirect('/swagger'); } else { if (req.headers['authorization'] !== `Bearer ${config.apiKey}`) { From c1c2ce3d45ce18a44f574d4e5777ae8f6d43cf84 Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Tue, 25 May 2021 14:21:51 -0400 Subject: [PATCH 03/19] Use X-API-KEY header for api key --- README.md | 4 ++-- src/app.ts | 4 ++-- src/swagger.yaml | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e51136c..c28bda9 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Create `config.json` in the data directory and set its content to: Based on this configuration: - Port 3000 will be used to access the API - Port 3001 will be used for P2P communications -- The API key will be set to `xxxxx` (bearer token) +- The API key will be set to `xxxxx` - There is one peer named `org-b` whose P2P endpoint is `https://localhost:4001` #### Generate certificate @@ -74,7 +74,7 @@ export LOG_LEVEL=info Based on this configuration: - Port 4000 will be used to access the API - Port 4001 will be used for P2P communications -- The API key will be set to `yyyyy` (bearer token) +- The API key will be set to `yyyyy` - There is one peer named `org-a` whose P2P endpoint is `https://localhost:3001` diff --git a/src/app.ts b/src/app.ts index e50e8ec..419fc2a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -38,7 +38,7 @@ export const start = async () => { const wss = new WebSocket.Server({ server: apiServer, verifyClient: (info, cb) => { - if (info.req.headers.authorization === `Bearer ${config.apiKey}`) { + if (info.req.headers['x-api-key'] === config.apiKey) { cb(true); } else { cb(false, 401, 'Unauthorized'); @@ -77,7 +77,7 @@ export const start = async () => { if (req.path === '/') { res.redirect('/swagger'); } else { - if (req.headers['authorization'] !== `Bearer ${config.apiKey}`) { + if (req.headers['x-api-key'] !== config.apiKey) { next(new RequestError('Unauthorized', 401)); } else { next(); diff --git a/src/swagger.yaml b/src/swagger.yaml index c34ed4d..dced283 100644 --- a/src/swagger.yaml +++ b/src/swagger.yaml @@ -167,10 +167,10 @@ # schemas components: securitySchemes: - bearerAuth: - type: http - scheme: bearer - bearerFormat: JWT + ApiKeyAuth: + type: apiKey + in: header + name: X-API-KEY schemas: Status: type: object @@ -235,4 +235,4 @@ error: type: string security: - - bearerAuth: [] \ No newline at end of file + - ApiKeyAuth: [] \ No newline at end of file From 43c77e094ede86df6efef709ef9f04da191a31cd Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Tue, 25 May 2021 17:10:56 -0400 Subject: [PATCH 04/19] Add peers endpoint, check for consecutive dots in file paths --- src/lib/utils.ts | 3 ++- src/routers/api.ts | 12 +++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 4c1e089..2e6e651 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -24,7 +24,8 @@ export const constants = { const log = createLogger({ name: 'utils.ts', level: constants.LOG_LEVEL as LogLevelString }); export const regexp = { - FILE_KEY: /^(\/[a-z0-9+-_.]+)+$/ + FILE_KEY: /^(\/[a-z0-9\+\-\_\.]+)+$/, + CONSECUTIVE_DOTS: /\.\./ }; export const fileExists = async (filePath: string): Promise => { diff --git a/src/routers/api.ts b/src/routers/api.ts index fe45ff1..d255db1 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -40,6 +40,10 @@ router.get('/status', async (_req, res, next) => { } }); +router.get('/peers', async (_req, res) => { + res.send(config.peers); +}); + router.post('/messages', async (req, res, next) => { try { if (req.body.message === undefined) { @@ -61,6 +65,9 @@ router.post('/messages', async (req, res, next) => { router.get('/blobs/*', async (req, res, next) => { try { + if (!utils.regexp.FILE_KEY.test(req.body.path) || utils.regexp.CONSECUTIVE_DOTS.test(req.body.path)) { + throw new RequestError('Invalid path', 400); + } let blobStream = await blobsHandler.retreiveBlob(req.params[0]); blobStream.on('end', () => res.end()); blobStream.pipe(res); @@ -71,6 +78,9 @@ router.get('/blobs/*', async (req, res, next) => { router.put('/blobs/*', async (req, res, next) => { try { + if (!utils.regexp.FILE_KEY.test(req.body.path) || utils.regexp.CONSECUTIVE_DOTS.test(req.body.path)) { + throw new RequestError('Invalid path', 400); + } const file = await utils.extractFileFromMultipartForm(req); const hash = await blobsHandler.storeBlob(file, req.params[0]); res.send({ hash }); @@ -84,7 +94,7 @@ router.post('/transfers', async (req, res, next) => { if (req.body.path === undefined) { throw new RequestError('Missing path', 400); } - if (!utils.regexp.FILE_KEY.test(req.body.path)) { + if (!utils.regexp.FILE_KEY.test(req.body.path) || utils.regexp.CONSECUTIVE_DOTS.test(req.body.path)) { throw new RequestError('Invalid path', 400); } if (req.body.recipient === undefined) { From f0462a89251d56f6f1f81dc7d856290fadc1c7bc Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Tue, 25 May 2021 17:44:12 -0400 Subject: [PATCH 05/19] Add peer registration API --- src/lib/config.ts | 10 ++++++---- src/routers/api.ts | 25 ++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/lib/config.ts b/src/lib/config.ts index 0dfa265..0d90f8c 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -1,12 +1,10 @@ -import { promisify } from 'util'; -import { readFile } from 'fs'; +import { promises as fs } from 'fs'; import Ajv from 'ajv'; import configSchema from '../schemas/config.json'; import * as utils from './utils'; import { IConfig } from './interfaces'; import path from 'path'; -const asyncReadFile = promisify(readFile); const ajv = new Ajv(); const validateConfig = ajv.compile(configSchema); const configFilePath = path.join(utils.constants.DATA_DIRECTORY, utils.constants.CONFIG_FILE_NAME); @@ -19,7 +17,7 @@ export const init = async () => { const loadConfigFile = async () => { try { - const data = JSON.parse(await asyncReadFile(configFilePath, 'utf8')); + const data = JSON.parse(await fs.readFile(configFilePath, 'utf8')); if(validateConfig(data)) { config = data as IConfig; for(const peer of config.peers) { @@ -34,3 +32,7 @@ const loadConfigFile = async () => { throw new Error(`Failed to read configuration file. ${err}`); } }; + +export const persistConfig = async () => { + await fs.writeFile(configFilePath, JSON.stringify(config)); +}; \ No newline at end of file diff --git a/src/routers/api.ts b/src/routers/api.ts index d255db1..1493037 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -3,11 +3,13 @@ import * as blobsHandler from '../handlers/blobs'; import * as messagesHandler from '../handlers/messages'; import * as utils from '../lib/utils'; import RequestError from '../lib/request-error'; -import { config } from '../lib/config'; +import { config, persistConfig } from '../lib/config'; import { IStatus } from '../lib/interfaces'; import https from 'https'; import { key, cert, ca } from '../lib/cert'; import * as eventsHandler from '../handlers/events'; +import { promises as fs } from 'fs'; +import path from 'path'; export const router = Router(); @@ -44,6 +46,27 @@ router.get('/peers', async (_req, res) => { res.send(config.peers); }); +router.put('/peers/:name', async (req, res, next) => { + try { + if(req.body.endpoint === undefined) { + throw new RequestError('Missing endpoint', 400); + } + await fs.writeFile(path.join(utils.constants.DATA_DIRECTORY, utils.constants.PEER_CERTS_SUBDIRECTORY, `${req.params.name}.pem`), req.body.certificate); + let peer = config.peers.find(peer => peer.name === req.params.name); + if(peer === undefined) { + peer = { + name: req.params.name, + endpoint: req.body.endpoint + }; + config.peers.push(peer); + } + await persistConfig(); + res.send({ status: 'stored' }); + } catch (err) { + next(err); + } +}); + router.post('/messages', async (req, res, next) => { try { if (req.body.message === undefined) { From 1550c2f5edabfed1dd282c6e6f7aa931295e0102 Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Tue, 25 May 2021 22:13:37 -0400 Subject: [PATCH 06/19] Reload CAs when registering a member, removing the need to restart --- src/lib/cert.ts | 4 ++++ src/lib/config.ts | 2 +- src/routers/api.ts | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/lib/cert.ts b/src/lib/cert.ts index f604c81..8a7682e 100644 --- a/src/lib/cert.ts +++ b/src/lib/cert.ts @@ -12,6 +12,10 @@ export let ca: string[] = []; export const init = async () => { key = (await fs.readFile(path.join(utils.constants.DATA_DIRECTORY, utils.constants.KEY_FILE))).toString(); cert = (await fs.readFile(path.join(utils.constants.DATA_DIRECTORY, utils.constants.CERT_FILE))).toString(); + loadCAs(); +}; + +export const loadCAs = async () => { const peerCertsPath = path.join(utils.constants.DATA_DIRECTORY, utils.constants.PEER_CERTS_SUBDIRECTORY); const peerCerts = await fs.readdir(peerCertsPath); for(const peerCert of peerCerts) { diff --git a/src/lib/config.ts b/src/lib/config.ts index 0d90f8c..30204cc 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -34,5 +34,5 @@ const loadConfigFile = async () => { }; export const persistConfig = async () => { - await fs.writeFile(configFilePath, JSON.stringify(config)); + await fs.writeFile(configFilePath, JSON.stringify(config, null, 2)); }; \ No newline at end of file diff --git a/src/routers/api.ts b/src/routers/api.ts index 1493037..2f52169 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -6,7 +6,7 @@ import RequestError from '../lib/request-error'; import { config, persistConfig } from '../lib/config'; import { IStatus } from '../lib/interfaces'; import https from 'https'; -import { key, cert, ca } from '../lib/cert'; +import { key, cert, ca, loadCAs } from '../lib/cert'; import * as eventsHandler from '../handlers/events'; import { promises as fs } from 'fs'; import path from 'path'; @@ -61,6 +61,7 @@ router.put('/peers/:name', async (req, res, next) => { config.peers.push(peer); } await persistConfig(); + await loadCAs(); res.send({ status: 'stored' }); } catch (err) { next(err); From f0d2eef04a3239fd90a932c371b753124165ca71 Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Tue, 25 May 2021 22:22:57 -0400 Subject: [PATCH 07/19] Add peer removal endpoint --- data/member-a/config.json | 2 +- src/routers/api.ts | 25 +++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/data/member-a/config.json b/data/member-a/config.json index 0b083f5..55ae27e 100644 --- a/data/member-a/config.json +++ b/data/member-a/config.json @@ -9,4 +9,4 @@ "endpoint": "https://localhost:4001" } ] -} +} \ No newline at end of file diff --git a/src/routers/api.ts b/src/routers/api.ts index 2f52169..be3aad3 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -48,12 +48,12 @@ router.get('/peers', async (_req, res) => { router.put('/peers/:name', async (req, res, next) => { try { - if(req.body.endpoint === undefined) { + if (req.body.endpoint === undefined) { throw new RequestError('Missing endpoint', 400); } await fs.writeFile(path.join(utils.constants.DATA_DIRECTORY, utils.constants.PEER_CERTS_SUBDIRECTORY, `${req.params.name}.pem`), req.body.certificate); let peer = config.peers.find(peer => peer.name === req.params.name); - if(peer === undefined) { + if (peer === undefined) { peer = { name: req.params.name, endpoint: req.body.endpoint @@ -68,6 +68,27 @@ router.put('/peers/:name', async (req, res, next) => { } }); +router.delete('/peers/:name', async (req, res, next) => { + try { + if (!config.peers.some(peer => peer.name === req.params.name)) { + throw new RequestError('Peer not found', 404); + } + try { + await fs.rm(path.join(utils.constants.DATA_DIRECTORY, utils.constants.PEER_CERTS_SUBDIRECTORY, `${req.params.name}.pem`)); + } catch (err) { + if (err.errno !== -2) { + throw new RequestError(`Failed to remove peer certificate`); + } + } + config.peers = config.peers.filter(peer => peer.name !== req.params.name); + await persistConfig(); + await loadCAs(); + res.send({ status: 'deleted' }); + } catch (err) { + next(err); + } +}); + router.post('/messages', async (req, res, next) => { try { if (req.body.message === undefined) { From 8e616d8b910a195d54dc0508edf0f5618be7ed58 Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Tue, 25 May 2021 22:24:48 -0400 Subject: [PATCH 08/19] Make peer cert optional on registration since it may not always be used --- src/routers/api.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/routers/api.ts b/src/routers/api.ts index be3aad3..2ab359d 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -51,7 +51,9 @@ router.put('/peers/:name', async (req, res, next) => { if (req.body.endpoint === undefined) { throw new RequestError('Missing endpoint', 400); } - await fs.writeFile(path.join(utils.constants.DATA_DIRECTORY, utils.constants.PEER_CERTS_SUBDIRECTORY, `${req.params.name}.pem`), req.body.certificate); + if(req.body.certificate !== undefined) { + await fs.writeFile(path.join(utils.constants.DATA_DIRECTORY, utils.constants.PEER_CERTS_SUBDIRECTORY, `${req.params.name}.pem`), req.body.certificate); + } let peer = config.peers.find(peer => peer.name === req.params.name); if (peer === undefined) { peer = { From a9a0b4b21cfddc547fda2c17a1e2e6f19e89cd50 Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Wed, 26 May 2021 14:25:26 -0400 Subject: [PATCH 09/19] Update swagger --- src/routers/api.ts | 8 ++-- src/swagger.yaml | 112 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 114 insertions(+), 6 deletions(-) diff --git a/src/routers/api.ts b/src/routers/api.ts index 2ab359d..db56374 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -42,7 +42,7 @@ router.get('/status', async (_req, res, next) => { } }); -router.get('/peers', async (_req, res) => { +router.get('/peers', (_req, res) => { res.send(config.peers); }); @@ -51,7 +51,7 @@ router.put('/peers/:name', async (req, res, next) => { if (req.body.endpoint === undefined) { throw new RequestError('Missing endpoint', 400); } - if(req.body.certificate !== undefined) { + if (req.body.certificate !== undefined) { await fs.writeFile(path.join(utils.constants.DATA_DIRECTORY, utils.constants.PEER_CERTS_SUBDIRECTORY, `${req.params.name}.pem`), req.body.certificate); } let peer = config.peers.find(peer => peer.name === req.params.name); @@ -64,7 +64,7 @@ router.put('/peers/:name', async (req, res, next) => { } await persistConfig(); await loadCAs(); - res.send({ status: 'stored' }); + res.send({ status: 'added' }); } catch (err) { next(err); } @@ -85,7 +85,7 @@ router.delete('/peers/:name', async (req, res, next) => { config.peers = config.peers.filter(peer => peer.name !== req.params.name); await persistConfig(); await loadCAs(); - res.send({ status: 'deleted' }); + res.send({ status: 'removed' }); } catch (err) { next(err); } diff --git a/src/swagger.yaml b/src/swagger.yaml index dced283..fb5a8d6 100644 --- a/src/swagger.yaml +++ b/src/swagger.yaml @@ -25,6 +25,85 @@ application/json: schema: $ref: '#/components/schemas/Error' + /peers: + get: + tags: + - Peers + description: List peers + responses: + '200': + description: Peers + content: + application/json: + schema: + $ref: '#/components/schemas/Peers' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /peers/{name}: + put: + tags: + - Peers + description: Add peer + parameters: + - in: path + name: name + required: true + schema: + type: string + description: Peer name + responses: + '200': + description: Peer added + content: + application/json: + schema: + $ref: '#/components/schemas/Added' + '400': + description: Missing peer endpoint + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + delete: + tags: + - Peers + description: Remove peer + parameters: + - in: path + name: name + required: true + schema: + type: string + description: Peer name + responses: + '200': + description: Peer removed + content: + application/json: + schema: + $ref: '#/components/schemas/Removed' + '404': + description: Peer not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /messages: post: tags: @@ -163,8 +242,7 @@ content: application/json: schema: - $ref: '#/components/schemas/Error' -# schemas + $ref: '#/components/schemas/Error' components: securitySchemes: ApiKeyAuth: @@ -192,6 +270,36 @@ type: string available: type: boolean + Peer: + type: object + required: + - name + - endpoint + properties: + name: + type: string + endpoint: + type: string + Peers: + type: array + items: + $ref: '#/components/schemas/Peer' + Added: + type: object + required: + - status + properties: + status: + type: string + enum: ['added'] + Removed: + type: object + required: + - status + properties: + status: + type: string + enum: ['removed'] Message: type: object required: From 92429aec14aa8d6f5bb1af6785a0e8949e8f9123 Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Thu, 27 May 2021 16:03:48 -0400 Subject: [PATCH 10/19] Use delegate WebSocket client --- src/app.ts | 32 ++++++++++++++++++++++++-------- src/lib/cert.ts | 2 +- src/routers/api.ts | 10 ++++++---- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/app.ts b/src/app.ts index 419fc2a..7d01b54 100644 --- a/src/app.ts +++ b/src/app.ts @@ -20,6 +20,8 @@ const log = createLogger({ name: 'app.ts', level: utils.constants.LOG_LEVEL as L const swaggerDocument = YAML.load(path.join(__dirname, './swagger.yaml')); +let delegatedWebSocket: WebSocket | undefined = undefined; + export const start = async () => { await initConfig(); await initCert(); @@ -49,15 +51,18 @@ export const start = async () => { p2pEventEmitter.addListener('event', event => eventsHandler.queueEvent(event)); blobsEventEmitter.addListener('event', event => eventsHandler.queueEvent(event)); messagesEventEmitter.addListener('event', event => eventsHandler.queueEvent(event)); - eventsHandler.eventEmitter.addListener('event', event => wss.clients.forEach(client => client.send(JSON.stringify(event)))); - - wss.on('connection', (webSocket: WebSocket) => { - - const event = eventsHandler.getCurrentEvent(); - if (event !== undefined) { - webSocket.send(JSON.stringify(event)); + + eventsHandler.eventEmitter.addListener('event', event => { + if(delegatedWebSocket !== undefined) { + delegatedWebSocket.send(JSON.stringify(event)); } + }); + + // eventsHandler.eventEmitter.addListener('event', event => wss.clients.forEach(client => client.send(JSON.stringify(event)))); + const assignWebSocketDelegate = (webSocket: WebSocket) => { + delegatedWebSocket = webSocket; + const event = eventsHandler.getCurrentEvent(); webSocket.on('message', async message => { try { const messageContent = JSON.parse(message.toLocaleString()); @@ -68,7 +73,18 @@ export const start = async () => { log.error(`Failed to process websocket message ${err}`); } }); + if (event !== undefined) { + webSocket.send(JSON.stringify(event)); + } + webSocket.on('close', () => { + assignWebSocketDelegate(wss.clients.values().next().value); + }); + }; + wss.on('connection', (webSocket: WebSocket) => { + if(delegatedWebSocket === undefined) { + assignWebSocketDelegate(webSocket); + } }); apiApp.use('/swagger', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); @@ -96,6 +112,6 @@ export const start = async () => { const apiServerPromise = new Promise(resolve => apiServer.listen(config.apiPort, () => resolve())); const p2pServerPromise = new Promise(resolve => p2pServer.listen(config.p2pPort, () => resolve())); await Promise.all([apiServerPromise, p2pServerPromise]); - log.info(`Blob exchange listening on ports ${config.apiPort} (API) and ${config.p2pPort} (P2P) - log level "${utils.constants.LOG_LEVEL}"`); + log.info(`Data exchange listening on ports ${config.apiPort} (API) and ${config.p2pPort} (P2P) - log level "${utils.constants.LOG_LEVEL}"`); }; diff --git a/src/lib/cert.ts b/src/lib/cert.ts index 8a7682e..8f08c6b 100644 --- a/src/lib/cert.ts +++ b/src/lib/cert.ts @@ -12,7 +12,7 @@ export let ca: string[] = []; export const init = async () => { key = (await fs.readFile(path.join(utils.constants.DATA_DIRECTORY, utils.constants.KEY_FILE))).toString(); cert = (await fs.readFile(path.join(utils.constants.DATA_DIRECTORY, utils.constants.CERT_FILE))).toString(); - loadCAs(); + await loadCAs(); }; export const loadCAs = async () => { diff --git a/src/routers/api.ts b/src/routers/api.ts index db56374..82fd389 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -112,10 +112,11 @@ router.post('/messages', async (req, res, next) => { router.get('/blobs/*', async (req, res, next) => { try { - if (!utils.regexp.FILE_KEY.test(req.body.path) || utils.regexp.CONSECUTIVE_DOTS.test(req.body.path)) { + const blobPath = `/${req.params[0]}`; + if (!utils.regexp.FILE_KEY.test(blobPath) || utils.regexp.CONSECUTIVE_DOTS.test(blobPath)) { throw new RequestError('Invalid path', 400); } - let blobStream = await blobsHandler.retreiveBlob(req.params[0]); + let blobStream = await blobsHandler.retreiveBlob(blobPath); blobStream.on('end', () => res.end()); blobStream.pipe(res); } catch (err) { @@ -125,11 +126,12 @@ router.get('/blobs/*', async (req, res, next) => { router.put('/blobs/*', async (req, res, next) => { try { - if (!utils.regexp.FILE_KEY.test(req.body.path) || utils.regexp.CONSECUTIVE_DOTS.test(req.body.path)) { + const blobPath = `/${req.params[0]}`; + if (!utils.regexp.FILE_KEY.test(blobPath) || utils.regexp.CONSECUTIVE_DOTS.test(blobPath)) { throw new RequestError('Invalid path', 400); } const file = await utils.extractFileFromMultipartForm(req); - const hash = await blobsHandler.storeBlob(file, req.params[0]); + const hash = await blobsHandler.storeBlob(file, blobPath); res.send({ hash }); } catch (err) { next(err); From 7733f45f89fe16a61e9a7692f6f3f7800c31fc23 Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Thu, 27 May 2021 16:27:46 -0400 Subject: [PATCH 11/19] Update readme --- README.md | 4 ++++ diagram.png | Bin 0 -> 203955 bytes 2 files changed, 4 insertions(+) create mode 100644 diagram.png diff --git a/README.md b/README.md index c28bda9..671f857 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ The following steps show how to setup Firefly MTLS Data Exchange for two organizations named `org-a` and `org-b` running on `localhost`. +![Data exchange diagram](./diagram.png) + +`org-a` will use port `3000` for API and port `3001` for P2P. `org-b` will use port `4000` for API and port `4001` for P2P. Each organization will have its own private key and self-signed certificate. + ## Setup org-a #### Environment variables diff --git a/diagram.png b/diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..88c919846b8d2074450e8730b24b99ba0ce78cb9 GIT binary patch literal 203955 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%0Dk(>2?GwEsi$t;4+qWWF-{xD_7vkwcRA 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 sXT142_x9D*; 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 BCZPZT literal 0 HcmV?d00001 From fcf3054b42ab69d12c8ad041744cdc8d20ea834e Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Fri, 28 May 2021 10:43:16 -0400 Subject: [PATCH 12/19] Add copyright headers --- src/app.ts | 18 ++++++++++++++++-- src/custom.d.ts | 16 ++++++++++++++++ src/handlers/blobs.ts | 16 ++++++++++++++++ src/handlers/events.ts | 16 ++++++++++++++++ src/handlers/messages.ts | 16 ++++++++++++++++ src/index.ts | 16 ++++++++++++++++ src/lib/cert.ts | 16 ++++++++++++++++ src/lib/config.ts | 16 ++++++++++++++++ src/lib/interfaces.ts | 17 +++++++++++++++-- src/lib/request-error.ts | 16 ++++++++++++++++ src/lib/utils.ts | 16 ++++++++++++++++ src/routers/api.ts | 16 ++++++++++++++++ src/routers/p2p.ts | 16 ++++++++++++++++ tsconfig.json | 2 +- 14 files changed, 208 insertions(+), 5 deletions(-) diff --git a/src/app.ts b/src/app.ts index 7d01b54..37b8061 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,3 +1,19 @@ +// Copyright © 2021 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + import express from 'express'; import https from 'https'; import http from 'http'; @@ -58,8 +74,6 @@ export const start = async () => { } }); - // eventsHandler.eventEmitter.addListener('event', event => wss.clients.forEach(client => client.send(JSON.stringify(event)))); - const assignWebSocketDelegate = (webSocket: WebSocket) => { delegatedWebSocket = webSocket; const event = eventsHandler.getCurrentEvent(); diff --git a/src/custom.d.ts b/src/custom.d.ts index 3eaac9a..c391773 100644 --- a/src/custom.d.ts +++ b/src/custom.d.ts @@ -1,3 +1,19 @@ +// Copyright © 2021 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + export {} declare global{ diff --git a/src/handlers/blobs.ts b/src/handlers/blobs.ts index 765927b..11f8304 100644 --- a/src/handlers/blobs.ts +++ b/src/handlers/blobs.ts @@ -1,3 +1,19 @@ +// Copyright © 2021 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + import { promises as fs, createReadStream, createWriteStream } from 'fs'; import path from 'path'; import * as utils from '../lib/utils'; diff --git a/src/handlers/events.ts b/src/handlers/events.ts index 63a0b4c..deedf57 100644 --- a/src/handlers/events.ts +++ b/src/handlers/events.ts @@ -1,3 +1,19 @@ +// Copyright © 2021 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + import { createLogger, LogLevelString } from "bunyan"; import EventEmitter from "events"; import { OutboundEvent } from "../lib/interfaces"; diff --git a/src/handlers/messages.ts b/src/handlers/messages.ts index d4b5fce..0fe0e91 100644 --- a/src/handlers/messages.ts +++ b/src/handlers/messages.ts @@ -1,3 +1,19 @@ +// Copyright © 2021 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + import https from 'https'; import * as utils from '../lib/utils'; import { key, cert, ca } from '../lib/cert'; diff --git a/src/index.ts b/src/index.ts index d6e8286..08340eb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,19 @@ +// Copyright © 2021 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + import { createLogger, LogLevelString } from 'bunyan'; import * as utils from './lib/utils'; import { start } from './app'; diff --git a/src/lib/cert.ts b/src/lib/cert.ts index 8f08c6b..53bf6b1 100644 --- a/src/lib/cert.ts +++ b/src/lib/cert.ts @@ -1,3 +1,19 @@ +// Copyright © 2021 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + import * as utils from '../lib/utils'; import { promises as fs } from 'fs'; import path from 'path'; diff --git a/src/lib/config.ts b/src/lib/config.ts index 30204cc..856719c 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -1,3 +1,19 @@ +// Copyright © 2021 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + import { promises as fs } from 'fs'; import Ajv from 'ajv'; import configSchema from '../schemas/config.json'; diff --git a/src/lib/interfaces.ts b/src/lib/interfaces.ts index 7ba5f04..ce94d3c 100644 --- a/src/lib/interfaces.ts +++ b/src/lib/interfaces.ts @@ -1,5 +1,18 @@ -import { AxiosRequestConfig } from "axios" -import FormData from "form-data" +// Copyright © 2021 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. export interface IConfig { apiPort: number diff --git a/src/lib/request-error.ts b/src/lib/request-error.ts index a677f56..996a06f 100644 --- a/src/lib/request-error.ts +++ b/src/lib/request-error.ts @@ -1,3 +1,19 @@ +// Copyright © 2021 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + import { NextFunction, Request, Response } from 'express'; export default class RequestError extends Error { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 2e6e651..9f3c56a 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,3 +1,19 @@ +// Copyright © 2021 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + import { Request } from 'express'; import { promises as fs } from 'fs'; import { IFile } from './interfaces'; diff --git a/src/routers/api.ts b/src/routers/api.ts index 82fd389..8d36108 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -1,3 +1,19 @@ +// Copyright © 2021 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + import { Router } from 'express'; import * as blobsHandler from '../handlers/blobs'; import * as messagesHandler from '../handlers/messages'; diff --git a/src/routers/p2p.ts b/src/routers/p2p.ts index 20b88c4..be68c4d 100644 --- a/src/routers/p2p.ts +++ b/src/routers/p2p.ts @@ -1,3 +1,19 @@ +// Copyright © 2021 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + import { Router } from 'express'; import * as utils from '../lib/utils'; import * as blobsHandler from '../handlers/blobs'; diff --git a/tsconfig.json b/tsconfig.json index 0c16fd4..d7a195e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "noImplicitAny": true, "esModuleInterop": true, "resolveJsonModule": true, - // "noUnusedLocals": true, + "noUnusedLocals": true, "noUnusedParameters": true, "skipLibCheck": true } From 2a1b51451177c97e72d89bce96c0187948dba1d8 Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Fri, 28 May 2021 10:45:46 -0400 Subject: [PATCH 13/19] Add Apache license --- LICENSE | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file From d7ac584b941a13b6fbb099c23962e8f6a073b4c7 Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Wed, 2 Jun 2021 14:45:29 -0400 Subject: [PATCH 14/19] Add optional request ID To allow clients to correlate messages and blob transfer confirmations --- package-lock.json | 6 +++--- package.json | 2 +- src/handlers/blobs.ts | 14 ++++++++------ src/handlers/messages.ts | 14 ++++++++------ src/lib/interfaces.ts | 3 +++ src/routers/api.ts | 12 ++++++++++-- 6 files changed, 33 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index c5983ba..010ad7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -825,9 +825,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", - "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==" + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" }, "yamljs": { "version": "0.3.0", diff --git a/package.json b/package.json index 6119b47..badfa5a 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "swagger-ui-express": "^4.1.6", "ts-node": "^9.1.1", "typescript": "^4.2.4", - "ws": "^7.4.5", + "ws": "^7.4.6", "yamljs": "^0.3.0" }, "devDependencies": { diff --git a/src/handlers/blobs.ts b/src/handlers/blobs.ts index 11f8304..493444b 100644 --- a/src/handlers/blobs.ts +++ b/src/handlers/blobs.ts @@ -62,12 +62,12 @@ export const storeBlob = async (file: IFile, filePath: string) => { }); }; -export const sendBlob = async (blobPath: string, recipient: string, recipientURL: string) => { +export const sendBlob = async (blobPath: string, recipient: string, recipientURL: string, requestID: string | undefined) => { if (sending) { - blobQueue.push({ blobPath, recipient, recipientURL }); + blobQueue.push({ blobPath, recipient, recipientURL, requestID }); } else { sending = true; - blobQueue.push({ blobPath, recipient, recipientURL }); + blobQueue.push({ blobPath, recipient, recipientURL, requestID }); while (blobQueue.length > 0) { await deliverBlob(blobQueue.shift()!); } @@ -75,7 +75,7 @@ export const sendBlob = async (blobPath: string, recipient: string, recipientURL } }; -export const deliverBlob = async ({ blobPath, recipient, recipientURL }: BlobTask) => { +export const deliverBlob = async ({ blobPath, recipient, recipientURL, requestID }: BlobTask) => { const resolvedFilePath = path.join(utils.constants.DATA_DIRECTORY, utils.constants.BLOBS_SUBDIRECTORY, blobPath); if (!(await utils.fileExists(resolvedFilePath))) { throw new RequestError('Blob not found', 404); @@ -96,14 +96,16 @@ export const deliverBlob = async ({ blobPath, recipient, recipientURL }: BlobTas eventEmitter.emit('event', { type: 'blob-delivered', path: blobPath, - recipient + recipient, + requestID } as IBlobDeliveredEvent); log.trace(`Blob delivered`); } catch (err) { eventEmitter.emit('event', { type: 'blob-failed', path: blobPath, - recipient + recipient, + requestID } as IBlobFailedEvent); log.error(`Failed to deliver blob ${err}`); } diff --git a/src/handlers/messages.ts b/src/handlers/messages.ts index 0fe0e91..79ae6fb 100644 --- a/src/handlers/messages.ts +++ b/src/handlers/messages.ts @@ -28,12 +28,12 @@ let messageQueue: MessageTask[] = []; let sending = false; export const eventEmitter = new EventEmitter(); -export const sendMessage = async (message: string, recipient: string, recipientURL: string) => { +export const sendMessage = async (message: string, recipient: string, recipientURL: string, requestID: string | undefined) => { if (sending) { - messageQueue.push({ message, recipient, recipientURL }); + messageQueue.push({ message, recipient, recipientURL, requestID }); } else { sending = true; - messageQueue.push({ message, recipient, recipientURL }); + messageQueue.push({ message, recipient, recipientURL, requestID }); while (messageQueue.length > 0) { await deliverMessage(messageQueue.shift()!); } @@ -41,7 +41,7 @@ export const sendMessage = async (message: string, recipient: string, recipientU } }; -export const deliverMessage = async ({ message, recipient, recipientURL }: MessageTask) => { +export const deliverMessage = async ({ message, recipient, recipientURL, requestID }: MessageTask) => { const httpsAgent = new https.Agent({ cert, key, ca }); const formData = new FormData(); formData.append('message', message); @@ -57,14 +57,16 @@ export const deliverMessage = async ({ message, recipient, recipientURL }: Messa eventEmitter.emit('event', { type: 'message-delivered', message, - recipient + recipient, + requestID } as IMessageDeliveredEvent); log.trace(`Message delivered`); } catch(err) { eventEmitter.emit('event', { type: 'message-failed', message, - recipient + recipient, + requestID } as IMessageFailedEvent); log.error(`Failed to deliver message ${err}`); } diff --git a/src/lib/interfaces.ts b/src/lib/interfaces.ts index ce94d3c..f879b59 100644 --- a/src/lib/interfaces.ts +++ b/src/lib/interfaces.ts @@ -54,6 +54,7 @@ export interface IMessageFailedEvent { type: 'message-failed' recipient: string message: string + requestID?: string } export interface IBlobReceivedEvent { @@ -90,12 +91,14 @@ export interface ICommitEvent { } export type MessageTask = { + requestID?: string message: string recipient: string recipientURL: string } export type BlobTask = { + requestID?: string blobPath: string recipient: string recipientURL: string diff --git a/src/routers/api.ts b/src/routers/api.ts index 8d36108..d279afa 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -119,7 +119,11 @@ router.post('/messages', async (req, res, next) => { if (recipientURL === undefined) { throw new RequestError(`Unknown recipient`, 400); } - messagesHandler.sendMessage(req.body.message, req.body.recipient, recipientURL); + let requestID: string | undefined = undefined; + if(typeof req.body.requestID === 'string') { + requestID = req.body.requestID; + } + messagesHandler.sendMessage(req.body.message, req.body.recipient, recipientURL, requestID); res.send({ status: 'submitted' }); } catch (err) { next(err); @@ -169,7 +173,11 @@ router.post('/transfers', async (req, res, next) => { if (recipientURL === undefined) { throw new RequestError(`Unknown recipient`, 400); } - blobsHandler.sendBlob(req.body.path, req.body.recipient, recipientURL); + let requestID: string | undefined = undefined; + if(typeof req.body.requestID === 'string') { + requestID = req.body.requestID; + } + blobsHandler.sendBlob(req.body.path, req.body.recipient, recipientURL, requestID); res.send({ status: 'submitted' }); } catch (err) { next(err); From 38a40605a496c571f7be93a4e1d90c651d9c40ee Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Wed, 2 Jun 2021 14:49:12 -0400 Subject: [PATCH 15/19] Update swagger and readme --- README.md | 8 ++++---- src/swagger.yaml | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 671f857..8a9b117 100644 --- a/README.md +++ b/README.md @@ -114,11 +114,11 @@ This will make it possible for the organizations to establish MTLS communication | Type | Description | Additional properties |-----------------|------------------------------------------------------------|----------------------- |blob-received | Emitted to the recipient when a blob has been transferred | sender, path, hash -|blob-delivered | Emitted to the sender when a blob has been delivered | recipient, path -|blob-failed | Emitted to the sender when a blob could not be delivered | recipient, path +|blob-delivered | Emitted to the sender when a blob has been delivered | recipient, path, requestID (optional) +|blob-failed | Emitted to the sender when a blob could not be delivered | recipient, path, requestID (optional) |message-received | Emitted to the recipient when a message has been sent | sender, message -|message-delivered| Emitted to the sender when a message has been delivered | recipient, message -|message-failed | Emitted to the sender when a message could not be delivered| recipient, message +|message-delivered| Emitted to the sender when a message has been delivered | recipient, message, requestID (optional) +|message-failed | Emitted to the sender when a message could not be delivered| recipient, message, requestID (optional) - After receiving a websocket message, a commit must be sent in order to receive the next one: ``` diff --git a/src/swagger.yaml b/src/swagger.yaml index fb5a8d6..c21984b 100644 --- a/src/swagger.yaml +++ b/src/swagger.yaml @@ -310,6 +310,8 @@ type: string recipient: type: string + requestID: + type: string BlobHash: type: object required: @@ -327,6 +329,8 @@ type: string recipient: type: string + requestID: + type: string Submitted: type: object required: From 178a0eab2cf567ff67e2f6f26ab4056f3264c023 Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Fri, 4 Jun 2021 20:13:22 -0400 Subject: [PATCH 16/19] Introduce new features to enhance portability --- README.md | 40 ++++++++++++++++--------- data/member-a/config.json | 12 ++++++-- data/member-b/config.json | 14 ++++++--- package-lock.json | 17 +++++++++-- package.json | 4 ++- src/app.ts | 17 ++++++----- src/custom.d.ts | 1 + src/lib/cert.ts | 3 ++ src/lib/interfaces.ts | 21 +++++++++---- src/lib/utils.ts | 33 ++++++++++++++++++++- src/routers/api.ts | 32 +++++++++++++------- src/routers/p2p.ts | 4 +-- src/schemas/config.json | 38 ++++++++++++++++++++---- src/swagger.yaml | 62 +++++++++++++++++++++++++++------------ 14 files changed, 222 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 8a9b117..f47f335 100644 --- a/README.md +++ b/README.md @@ -22,12 +22,18 @@ Create `config.json` in the data directory and set its content to: ``` { "$schema": "../../src/schemas/config.json", - "apiPort": 3000, - "p2pPort": 3001, + "api": { + "hostname": "localhost", + "port": 3000 + }, + "p2p": { + "hostname": "localhost", + "port": 3001 + }, "apiKey": "xxxxx", "peers": [ { - "name": "org-b", + "id": "org-b", "endpoint": "https://localhost:4001" } ] @@ -35,9 +41,9 @@ Create `config.json` in the data directory and set its content to: ``` Based on this configuration: -- Port 3000 will be used to access the API -- Port 3001 will be used for P2P communications -- The API key will be set to `xxxxx` +- API will be accessed via localhost:3000 +- P2P communications will use localhost:3001 +- The API key will be set to `xxxxx` (this is optional) - There is one peer named `org-b` whose P2P endpoint is `https://localhost:4001` #### Generate certificate @@ -63,22 +69,28 @@ export LOG_LEVEL=info ``` { "$schema": "../../src/schemas/config.json", - "apiPort": 4000, - "p2pPort": 4001, - "apiKey": "yyyyy", + "api": { + "hostname": "localhost", + "port": 4000 + }, + "p2p": { + "hostname": "localhost", + "port": 4001 + }, + "apiKey": "xxxxx", "peers": [ { - "name": "org-b", - "endpoint": "https://localhost:4001" + "id": "org-b", + "endpoint": "https://localhost:3001" } ] } ``` Based on this configuration: -- Port 4000 will be used to access the API -- Port 4001 will be used for P2P communications -- The API key will be set to `yyyyy` +- API will be accessed via localhost:4000 +- P2P communications will use localhost:4001 +- The API key will be set to `xxxxx` (this is optional) - There is one peer named `org-a` whose P2P endpoint is `https://localhost:3001` diff --git a/data/member-a/config.json b/data/member-a/config.json index 55ae27e..8fdfbf1 100644 --- a/data/member-a/config.json +++ b/data/member-a/config.json @@ -1,11 +1,17 @@ { "$schema": "../../src/schemas/config.json", - "apiPort": 3000, - "p2pPort": 3001, + "api": { + "hostname": "localhost", + "port": 3000 + }, + "p2p": { + "hostname": "localhost", + "port": 3001 + }, "apiKey": "xxxxx", "peers": [ { - "name": "org-b", + "id": "org-b", "endpoint": "https://localhost:4001" } ] diff --git a/data/member-b/config.json b/data/member-b/config.json index f7f9970..4b4701c 100644 --- a/data/member-b/config.json +++ b/data/member-b/config.json @@ -1,11 +1,17 @@ { "$schema": "../../src/schemas/config.json", - "apiPort": 4000, - "p2pPort": 4001, - "apiKey": "yyyyy", + "api": { + "hostname": "localhost", + "port": 4000 + }, + "p2p": { + "hostname": "localhost", + "port": 4001 + }, + "apiKey": "xxxxx", "peers": [ { - "name": "org-a", + "id": "org-b", "endpoint": "https://localhost:3001" } ] diff --git a/package-lock.json b/package-lock.json index 010ad7e..545f039 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,6 +64,12 @@ "@types/range-parser": "*" } }, + "@types/jsrsasign": { + "version": "8.0.12", + "resolved": "https://registry.npmjs.org/@types/jsrsasign/-/jsrsasign-8.0.12.tgz", + "integrity": "sha512-FLXKbwbB+4fsJECYOpIiYX2GSqSHYnkO/UnrFqlZn6crpyyOtk4LRab+G1HC7dTbT1NB7spkHecZRQGXoCWiJQ==", + "dev": true + }, "@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -133,9 +139,9 @@ } }, "ajv": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.4.0.tgz", - "integrity": "sha512-7QD2l6+KBSLwf+7MuYocbWvRPdOu63/trReTLu2KFwkgctnub1auoF+Y1WYcm09CTM7quuscrzqmASaLHC/K4Q==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.5.0.tgz", + "integrity": "sha512-Y2l399Tt1AguU3BPRP9Fn4eN+Or+StUGWCUpbnFyXSo8NZ9S4uj+AG2pjs5apK+ZMOwYOz1+a+VKvKH7CudXgQ==", "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -483,6 +489,11 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, + "jsrsasign": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-10.3.0.tgz", + "integrity": "sha512-irDIKKFW++EAELgP3fjFi5/Fn0XEyfuQTTgpbeFwCGkV6tRIYZl3uraRea2HTXWCstcSZuDaCbdAhU1n+075Bg==" + }, "make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", diff --git a/package.json b/package.json index badfa5a..22ebc40 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,13 @@ "author": "", "license": "ISC", "dependencies": { - "ajv": "^8.4.0", + "ajv": "^8.5.0", "axios": "^0.21.1", "bunyan": "^1.8.15", "busboy": "^0.3.1", "express": "^4.17.1", "form-data": "^4.0.0", + "jsrsasign": "^10.3.0", "swagger-ui-express": "^4.1.6", "ts-node": "^9.1.1", "typescript": "^4.2.4", @@ -31,6 +32,7 @@ "@types/bunyan": "^1.8.6", "@types/busboy": "^0.2.3", "@types/express": "^4.17.11", + "@types/jsrsasign": "^8.0.12", "@types/node": "^15.0.3", "@types/swagger-ui-express": "^4.1.2", "@types/ws": "^7.4.4", diff --git a/src/app.ts b/src/app.ts index 37b8061..bd0daab 100644 --- a/src/app.ts +++ b/src/app.ts @@ -67,13 +67,13 @@ export const start = async () => { p2pEventEmitter.addListener('event', event => eventsHandler.queueEvent(event)); blobsEventEmitter.addListener('event', event => eventsHandler.queueEvent(event)); messagesEventEmitter.addListener('event', event => eventsHandler.queueEvent(event)); - + eventsHandler.eventEmitter.addListener('event', event => { - if(delegatedWebSocket !== undefined) { + if (delegatedWebSocket !== undefined) { delegatedWebSocket.send(JSON.stringify(event)); } }); - + const assignWebSocketDelegate = (webSocket: WebSocket) => { delegatedWebSocket = webSocket; const event = eventsHandler.getCurrentEvent(); @@ -96,7 +96,7 @@ export const start = async () => { }; wss.on('connection', (webSocket: WebSocket) => { - if(delegatedWebSocket === undefined) { + if (delegatedWebSocket === undefined) { assignWebSocketDelegate(webSocket); } }); @@ -107,7 +107,7 @@ export const start = async () => { if (req.path === '/') { res.redirect('/swagger'); } else { - if (req.headers['x-api-key'] !== config.apiKey) { + if (config.apiKey !== undefined && req.headers['x-api-key'] !== config.apiKey) { next(new RequestError('Unauthorized', 401)); } else { next(); @@ -123,9 +123,10 @@ export const start = async () => { p2pApp.use('/api/v1', p2pRouter); p2pApp.use(errorHandler); - const apiServerPromise = new Promise(resolve => apiServer.listen(config.apiPort, () => resolve())); - const p2pServerPromise = new Promise(resolve => p2pServer.listen(config.p2pPort, () => resolve())); + const apiServerPromise = new Promise(resolve => apiServer.listen(config.api.port, config.api.hostname, () => resolve())); + const p2pServerPromise = new Promise(resolve => p2pServer.listen(config.p2p.port, config.p2p.hostname, () => resolve())); await Promise.all([apiServerPromise, p2pServerPromise]); - log.info(`Data exchange listening on ports ${config.apiPort} (API) and ${config.p2pPort} (P2P) - log level "${utils.constants.LOG_LEVEL}"`); + log.info(`Data exchange running on http://${config.api.hostname}:${config.api.port} (API) and ` + + `https://${config.p2p.hostname}:${config.p2p.port} (P2P) - log level "${utils.constants.LOG_LEVEL}"`); }; diff --git a/src/custom.d.ts b/src/custom.d.ts index c391773..1ba0bf2 100644 --- a/src/custom.d.ts +++ b/src/custom.d.ts @@ -29,6 +29,7 @@ declare global{ getPeerCertificate: () => { issuer: { O: string + OU: string } } } diff --git a/src/lib/cert.ts b/src/lib/cert.ts index 53bf6b1..03a23f7 100644 --- a/src/lib/cert.ts +++ b/src/lib/cert.ts @@ -24,10 +24,13 @@ const log = createLogger({ name: 'lib/certs.ts', level: utils.constants.LOG_LEVE export let key: string; export let cert: string; export let ca: string[] = []; +export let peerID: string; export const init = async () => { key = (await fs.readFile(path.join(utils.constants.DATA_DIRECTORY, utils.constants.KEY_FILE))).toString(); cert = (await fs.readFile(path.join(utils.constants.DATA_DIRECTORY, utils.constants.CERT_FILE))).toString(); + const certData = utils.getCertData(cert); + peerID = utils.getPeerID(certData.organization, certData.organizationUnit); await loadCAs(); }; diff --git a/src/lib/interfaces.ts b/src/lib/interfaces.ts index f879b59..cc72c50 100644 --- a/src/lib/interfaces.ts +++ b/src/lib/interfaces.ts @@ -15,11 +15,17 @@ // limitations under the License. export interface IConfig { - apiPort: number - p2pPort: number - apiKey: string + api: { + hostname: string + port: number + } + p2p: { + hostname: string + port: number + } + apiKey?: string peers: { - name: string + id: string endpoint: string }[] } @@ -107,7 +113,12 @@ export type BlobTask = { export interface IStatus { messageQueueSize: number peers: { - name: string + id: string available: boolean }[] } + +export interface ICertData { + organization?: string + organizationUnit?: string +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 9f3c56a..78983e4 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -16,11 +16,12 @@ import { Request } from 'express'; import { promises as fs } from 'fs'; -import { IFile } from './interfaces'; +import { ICertData, IFile } from './interfaces'; import RequestError from './request-error'; import Busboy from 'busboy'; import axios, { AxiosRequestConfig } from 'axios'; import { createLogger, LogLevelString } from 'bunyan'; +import { X509 } from 'jsrsasign'; export const constants = { LOG_LEVEL: process.env.LOG_LEVEL || 'info', @@ -115,3 +116,33 @@ export const axiosWithRetry = async (config: AxiosRequestConfig) => { } throw currentError; }; + +export const getPeerID = (organization: string | undefined, organizationUnit: string | undefined) => { + if(organization !== undefined) { + if(organizationUnit !== undefined) { + return `${organization}-${organizationUnit}`; + } else { + return organization; + } + } else if(organizationUnit !== undefined) { + return organizationUnit; + } else { + throw new Error('Invalid peer'); + } +}; + +export const getCertData = (cert: string): ICertData => { + const x509 = new X509(); + x509.readCertPEM(cert); + const subject = x509.getSubjectString(); + const o = subject.match(/O=(.+[^/])/); + let certData: ICertData = {}; + if(o !== null) { + certData.organization = o[1]; + } + const ou = subject.match(/OU=(.+[^/])/); + if(ou !== null) { + certData.organizationUnit = ou[1]; + } + return certData; +}; \ No newline at end of file diff --git a/src/routers/api.ts b/src/routers/api.ts index d279afa..80cd669 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -22,13 +22,25 @@ import RequestError from '../lib/request-error'; import { config, persistConfig } from '../lib/config'; import { IStatus } from '../lib/interfaces'; import https from 'https'; -import { key, cert, ca, loadCAs } from '../lib/cert'; +import { key, cert, ca, loadCAs, peerID } from '../lib/cert'; import * as eventsHandler from '../handlers/events'; import { promises as fs } from 'fs'; import path from 'path'; export const router = Router(); +router.get('/id', async (_req, res, next) => { + try { + res.send({ + id: peerID, + endpoint: `https://${config.p2p.hostname}:${config.p2p.port}`, + cert + }); + } catch (err) { + next(err); + } +}); + router.get('/status', async (_req, res, next) => { try { let status: IStatus = { @@ -48,7 +60,7 @@ router.get('/status', async (_req, res, next) => { let i = 0; for (const peer of config.peers) { status.peers.push({ - name: peer.name, + id: peer.id, available: responses[i++].status === 'fulfilled' }) } @@ -62,7 +74,7 @@ router.get('/peers', (_req, res) => { res.send(config.peers); }); -router.put('/peers/:name', async (req, res, next) => { +router.put('/peers/:id', async (req, res, next) => { try { if (req.body.endpoint === undefined) { throw new RequestError('Missing endpoint', 400); @@ -70,10 +82,10 @@ router.put('/peers/:name', async (req, res, next) => { if (req.body.certificate !== undefined) { await fs.writeFile(path.join(utils.constants.DATA_DIRECTORY, utils.constants.PEER_CERTS_SUBDIRECTORY, `${req.params.name}.pem`), req.body.certificate); } - let peer = config.peers.find(peer => peer.name === req.params.name); + let peer = config.peers.find(peer => peer.id === req.params.id); if (peer === undefined) { peer = { - name: req.params.name, + id: req.params.id, endpoint: req.body.endpoint }; config.peers.push(peer); @@ -86,9 +98,9 @@ router.put('/peers/:name', async (req, res, next) => { } }); -router.delete('/peers/:name', async (req, res, next) => { +router.delete('/peers/:id', async (req, res, next) => { try { - if (!config.peers.some(peer => peer.name === req.params.name)) { + if (!config.peers.some(peer => peer.id === req.params.id)) { throw new RequestError('Peer not found', 404); } try { @@ -98,7 +110,7 @@ router.delete('/peers/:name', async (req, res, next) => { throw new RequestError(`Failed to remove peer certificate`); } } - config.peers = config.peers.filter(peer => peer.name !== req.params.name); + config.peers = config.peers.filter(peer => peer.id !== req.params.id); await persistConfig(); await loadCAs(); res.send({ status: 'removed' }); @@ -115,7 +127,7 @@ router.post('/messages', async (req, res, next) => { if (req.body.recipient === undefined) { throw new RequestError('Missing recipient', 400); } - let recipientURL = config.peers.find(peer => peer.name === req.body.recipient)?.endpoint; + let recipientURL = config.peers.find(peer => peer.id === req.body.recipient)?.endpoint; if (recipientURL === undefined) { throw new RequestError(`Unknown recipient`, 400); } @@ -169,7 +181,7 @@ router.post('/transfers', async (req, res, next) => { if (req.body.recipient === undefined) { throw new RequestError('Missing recipient', 400); } - let recipientURL = config.peers.find(peer => peer.name === req.body.recipient)?.endpoint; + let recipientURL = config.peers.find(peer => peer.id === req.body.recipient)?.endpoint; if (recipientURL === undefined) { throw new RequestError(`Unknown recipient`, 400); } diff --git a/src/routers/p2p.ts b/src/routers/p2p.ts index be68c4d..8d43cea 100644 --- a/src/routers/p2p.ts +++ b/src/routers/p2p.ts @@ -31,7 +31,7 @@ router.head('/ping', (_req, res) => { router.post('/messages', async (req, res, next) => { try { const cert = req.client.getPeerCertificate(); - const sender = cert.issuer.O; + const sender = cert.issuer.O + cert.issuer.OU; const message = await utils.extractMessageFromMultipartForm(req); eventEmitter.emit('event', { type: 'message-received', @@ -47,7 +47,7 @@ router.post('/messages', async (req, res, next) => { router.put('/blobs/*', async (req, res, next) => { try { const cert = req.client.getPeerCertificate(); - const sender = cert.issuer.O; + const sender = cert.issuer.O + cert.issuer.OU; const file = await utils.extractFileFromMultipartForm(req); const blobPath = path.join(utils.constants.RECEIVED_BLOBS_SUBDIRECTORY, sender, req.params[0]); const hash = await blobsHandler.storeBlob(file, blobPath); diff --git a/src/schemas/config.json b/src/schemas/config.json index 85e74db..f029537 100644 --- a/src/schemas/config.json +++ b/src/schemas/config.json @@ -2,14 +2,40 @@ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "required": [ - "apiPort", - "p2pPort", - "apiKey", + "api", + "p2p", "peers" ], "properties": { - "port": { - "type": "integer" + "api": { + "type": "object", + "required": [ + "hostname", + "port" + ], + "properties": { + "hostname": { + "type": "string" + }, + "port": { + "type": "integer" + } + } + }, + "p2p": { + "type": "object", + "required": [ + "hostname", + "port" + ], + "properties": { + "hostname": { + "type": "string" + }, + "port": { + "type": "integer" + } + } }, "apiKey": { "type": "string" @@ -19,7 +45,7 @@ "items": { "type": "object", "required": [ - "name", + "id", "endpoint" ], "properties": { diff --git a/src/swagger.yaml b/src/swagger.yaml index c21984b..08b6bb1 100644 --- a/src/swagger.yaml +++ b/src/swagger.yaml @@ -7,6 +7,24 @@ servers: - url: /api/v1 paths: + /id: + get: + tags: + - ID + description: Peer information + responses: + '200': + description: Peer information + content: + application/json: + schema: + $ref: '#/components/schemas/PeerInformation' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /status: get: tags: @@ -43,18 +61,18 @@ application/json: schema: $ref: '#/components/schemas/Error' - /peers/{name}: + /peers/{id}: + parameters: + - in: path + name: id + required: true + schema: + type: string + description: Peer id put: tags: - Peers description: Add peer - parameters: - - in: path - name: name - required: true - schema: - type: string - description: Peer name responses: '200': description: Peer added @@ -78,13 +96,6 @@ tags: - Peers description: Remove peer - parameters: - - in: path - name: name - required: true - schema: - type: string - description: Peer name responses: '200': description: Peer removed @@ -250,6 +261,19 @@ in: header name: X-API-KEY schemas: + PeerInformation: + type: object + required: + - id + - endpoint + - cert + properties: + id: + type: string + endpoint: + type: string + cert: + type: string Status: type: object required: @@ -263,20 +287,20 @@ items: type: object required: - - name + - id - available properties: - name: + id: type: string available: type: boolean Peer: type: object required: - - name + - id - endpoint properties: - name: + id: type: string endpoint: type: string From 38da42dbf3f152f90bed94280967460615c3c4fa Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Fri, 4 Jun 2021 22:58:53 -0400 Subject: [PATCH 17/19] Optionally bypass API key for websocket connections --- src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index bd0daab..b621860 100644 --- a/src/app.ts +++ b/src/app.ts @@ -56,7 +56,7 @@ export const start = async () => { const wss = new WebSocket.Server({ server: apiServer, verifyClient: (info, cb) => { - if (info.req.headers['x-api-key'] === config.apiKey) { + if (config.api === undefined || info.req.headers['x-api-key'] === config.apiKey) { cb(true); } else { cb(false, 401, 'Unauthorized'); From 44271deaa670ee1170c5da4896f3e2c79ec97a92 Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Sat, 5 Jun 2021 09:59:52 -0400 Subject: [PATCH 18/19] Include peer endpoint in status API --- src/lib/interfaces.ts | 1 + src/routers/api.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/lib/interfaces.ts b/src/lib/interfaces.ts index cc72c50..176a750 100644 --- a/src/lib/interfaces.ts +++ b/src/lib/interfaces.ts @@ -114,6 +114,7 @@ export interface IStatus { messageQueueSize: number peers: { id: string + endpoint: string available: boolean }[] } diff --git a/src/routers/api.ts b/src/routers/api.ts index 80cd669..9dcd9c2 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -61,6 +61,7 @@ router.get('/status', async (_req, res, next) => { for (const peer of config.peers) { status.peers.push({ id: peer.id, + endpoint: peer.endpoint, available: responses[i++].status === 'fulfilled' }) } From 6d85f4557d36c0fa2e825e59dfdee3462690c352 Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Mon, 7 Jun 2021 21:04:19 -0400 Subject: [PATCH 19/19] Auto generate requestID if not provided --- data/member-b/config.json | 4 +- package-lock.json | 1130 ++++++++++++++++++++++++++++++++++++- package.json | 2 + src/routers/api.ts | 7 +- src/swagger.yaml | 5 +- 5 files changed, 1139 insertions(+), 9 deletions(-) diff --git a/data/member-b/config.json b/data/member-b/config.json index 4b4701c..86aec8f 100644 --- a/data/member-b/config.json +++ b/data/member-b/config.json @@ -8,10 +8,10 @@ "hostname": "localhost", "port": 4001 }, - "apiKey": "xxxxx", + "apiKey": "yyyyy", "peers": [ { - "id": "org-b", + "id": "org-a", "endpoint": "https://localhost:3001" } ] diff --git a/package-lock.json b/package-lock.json index 545f039..8dda44b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,8 +1,1125 @@ { "name": "blob-exchange", "version": "1.0.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "blob-exchange", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "ajv": "^8.5.0", + "axios": "^0.21.1", + "bunyan": "^1.8.15", + "busboy": "^0.3.1", + "express": "^4.17.1", + "form-data": "^4.0.0", + "jsrsasign": "^10.3.0", + "swagger-ui-express": "^4.1.6", + "ts-node": "^9.1.1", + "typescript": "^4.2.4", + "uuid": "^8.3.2", + "ws": "^7.4.6", + "yamljs": "^0.3.0" + }, + "devDependencies": { + "@types/bunyan": "^1.8.6", + "@types/busboy": "^0.2.3", + "@types/express": "^4.17.11", + "@types/jsrsasign": "^8.0.12", + "@types/node": "^15.0.3", + "@types/swagger-ui-express": "^4.1.2", + "@types/uuid": "^8.3.0", + "@types/ws": "^7.4.4", + "@types/yamljs": "^0.2.31" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bunyan": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.6.tgz", + "integrity": "sha512-YiozPOOsS6bIuz31ilYqR5SlLif4TBWsousN2aCWLi5233nZSX19tFbcQUPdR7xJ8ypPyxkCGNxg0CIV5n9qxQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/busboy": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-0.2.3.tgz", + "integrity": "sha1-ZpetKYcyRsUw8Jo/9aQIYYJCMNU=", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", + "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.11.tgz", + "integrity": "sha512-no+R6rW60JEc59977wIxreQVsIEOAYwgCqldrA/vkpCnbD7MqTefO97lmoBe4WE0F156bC4uLSP1XHDOySnChg==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.19.tgz", + "integrity": "sha512-DJOSHzX7pCiSElWaGR8kCprwibCB/3yW6vcT8VG3P0SJjnv19gnWG/AZMfM60Xj/YJIp/YCaDHyvzsFVeniARA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/jsrsasign": { + "version": "8.0.12", + "resolved": "https://registry.npmjs.org/@types/jsrsasign/-/jsrsasign-8.0.12.tgz", + "integrity": "sha512-FLXKbwbB+4fsJECYOpIiYX2GSqSHYnkO/UnrFqlZn6crpyyOtk4LRab+G1HC7dTbT1NB7spkHecZRQGXoCWiJQ==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "dev": true + }, + "node_modules/@types/node": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.3.tgz", + "integrity": "sha512-/WbxFeBU+0F79z9RdEOXH4CsDga+ibi5M8uEYr91u3CkT/pdWcV8MCook+4wDPnZBexRdwWS+PiVZ2xJviAzcQ==", + "dev": true + }, + "node_modules/@types/qs": { + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz", + "integrity": "sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", + "dev": true + }, + "node_modules/@types/serve-static": { + "version": "1.13.9", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz", + "integrity": "sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.2.tgz", + "integrity": "sha512-t9teFTU8dKe69rX9EwL6OM2hbVquYdFM+sQ0REny4RalPlxAm+zyP04B12j4c7qEuDS6CnlwICywqWStPA3v4g==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/uuid": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==", + "dev": true + }, + "node_modules/@types/ws": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.4.tgz", + "integrity": "sha512-d/7W23JAXPodQNbOZNXvl2K+bqAQrCMwlh/nuQsPSQk6Fq0opHoPrUw43aHsvSbIiQPr8Of2hkFbnz1XBFVyZQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yamljs": { + "version": "0.2.31", + "resolved": "https://registry.npmjs.org/@types/yamljs/-/yamljs-0.2.31.tgz", + "integrity": "sha512-QcJ5ZczaXAqbVD3o8mw/mEBhRvO5UAdTtbvgwL/OgoWubvNBh6/MxLBAigtcgIFaq3shon9m3POIxQaLQt4fxQ==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dependencies": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.5.0.tgz", + "integrity": "sha512-Y2l399Tt1AguU3BPRP9Fn4eN+Or+StUGWCUpbnFyXSo8NZ9S4uj+AG2pjs5apK+ZMOwYOz1+a+VKvKH7CudXgQ==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "node_modules/axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "dependencies": { + "follow-redirects": "^1.10.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dependencies": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "node_modules/bunyan": { + "version": "1.8.15", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz", + "integrity": "sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==", + "engines": [ + "node >=0.10.0" + ], + "bin": { + "bunyan": "bin/bunyan" + }, + "optionalDependencies": { + "dtrace-provider": "~0.8", + "moment": "^2.19.3", + "mv": "~2", + "safe-json-stringify": "~1" + } + }, + "node_modules/busboy": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.3.1.tgz", + "integrity": "sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw==", + "dependencies": { + "dicer": "0.3.0" + }, + "engines": { + "node": ">=4.5.0" + } + }, + "node_modules/bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "node_modules/dicer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", + "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==", + "dependencies": { + "streamsearch": "0.1.2" + }, + "engines": { + "node": ">=4.5.0" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dtrace-provider": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", + "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", + "optional": true, + "dependencies": { + "nan": "^2.14.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dependencies": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", + "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "node_modules/glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "optional": true, + "dependencies": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/jsrsasign": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-10.3.0.tgz", + "integrity": "sha512-irDIKKFW++EAELgP3fjFi5/Fn0XEyfuQTTgpbeFwCGkV6tRIYZl3uraRea2HTXWCstcSZuDaCbdAhU1n+075Bg==" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz", + "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.30", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz", + "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==", + "dependencies": { + "mime-db": "1.47.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "optional": true + }, + "node_modules/mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "optional": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/moment": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "optional": true, + "dependencies": { + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "optional": true + }, + "node_modules/ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "optional": true, + "bin": { + "ncp": "bin/ncp" + } + }, + "node_modules/negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "node_modules/proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "dependencies": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dependencies": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "optional": true, + "dependencies": { + "glob": "^6.0.1" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/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==" + }, + "node_modules/safe-json-stringify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", + "optional": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "node_modules/serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/swagger-ui-dist": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.49.0.tgz", + "integrity": "sha512-R1+eT16XNP1bBLfacISifZAkFJlpwvWsS2vVurF5pbIFZnmCasD/hj+9r/q7urYdQyb0B6v11mDnuYU7rUpfQg==" + }, + "node_modules/swagger-ui-express": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz", + "integrity": "sha512-Xs2BGGudvDBtL7RXcYtNvHsFtP1DBFPMJFRxHe5ez/VG/rzVOEjazJOOSc/kSCyxreCTKfJrII6MJlL9a6t8vw==", + "dependencies": { + "swagger-ui-dist": "^3.18.1" + }, + "engines": { + "node": ">= v0.10.32" + } + }, + "node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-node": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", + "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "dependencies": { + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "dependencies": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + }, + "bin": { + "json2yaml": "bin/json2yaml", + "yaml2json": "bin/yaml2json" + } + }, + "node_modules/yamljs/node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "engines": { + "node": ">=6" + } + } + }, "dependencies": { "@types/body-parser": { "version": "1.19.0", @@ -114,6 +1231,12 @@ "@types/serve-static": "*" } }, + "@types/uuid": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==", + "dev": true + }, "@types/ws": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.4.tgz", @@ -825,6 +1948,11 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 22ebc40..9196a9a 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "swagger-ui-express": "^4.1.6", "ts-node": "^9.1.1", "typescript": "^4.2.4", + "uuid": "^8.3.2", "ws": "^7.4.6", "yamljs": "^0.3.0" }, @@ -35,6 +36,7 @@ "@types/jsrsasign": "^8.0.12", "@types/node": "^15.0.3", "@types/swagger-ui-express": "^4.1.2", + "@types/uuid": "^8.3.0", "@types/ws": "^7.4.4", "@types/yamljs": "^0.2.31" } diff --git a/src/routers/api.ts b/src/routers/api.ts index 9dcd9c2..f69a3bd 100644 --- a/src/routers/api.ts +++ b/src/routers/api.ts @@ -26,6 +26,7 @@ import { key, cert, ca, loadCAs, peerID } from '../lib/cert'; import * as eventsHandler from '../handlers/events'; import { promises as fs } from 'fs'; import path from 'path'; +import { v4 as uuidV4 } from 'uuid'; export const router = Router(); @@ -132,12 +133,12 @@ router.post('/messages', async (req, res, next) => { if (recipientURL === undefined) { throw new RequestError(`Unknown recipient`, 400); } - let requestID: string | undefined = undefined; + let requestID = uuidV4(); if(typeof req.body.requestID === 'string') { requestID = req.body.requestID; } messagesHandler.sendMessage(req.body.message, req.body.recipient, recipientURL, requestID); - res.send({ status: 'submitted' }); + res.send({ requestID }); } catch (err) { next(err); } @@ -186,7 +187,7 @@ router.post('/transfers', async (req, res, next) => { if (recipientURL === undefined) { throw new RequestError(`Unknown recipient`, 400); } - let requestID: string | undefined = undefined; + let requestID = uuidV4(); if(typeof req.body.requestID === 'string') { requestID = req.body.requestID; } diff --git a/src/swagger.yaml b/src/swagger.yaml index 08b6bb1..c6c0c64 100644 --- a/src/swagger.yaml +++ b/src/swagger.yaml @@ -358,11 +358,10 @@ Submitted: type: object required: - - status + - requestID properties: - status: + requestID: type: string - enum: ['submitted'] Error: type: object required: