diff --git a/README.md b/README.md index b5b50e1..cf5eca0 100644 --- a/README.md +++ b/README.md @@ -321,6 +321,10 @@ the server exposes information about itself via the `/info` endpoint, which retu "maxFileSize": 1073741824, "registrationRequirements": ["proof-of-work-sha256-v0", "terms-of-service"], "version": "0.1.5", - "sdkVersion": "0.2.6" + "sdkVersion": "0.2.6", + "webSocketSupport": "true" } ``` + +- `server` is read from the `process.env.npm_package_name` variable that `npm` provides. If that does not exist, it will check for a `DWN_SERVER_PACKAGE_NAME` environment variable set by the user, or otherwise it will default to `@web5/dwn-server`. +- `version` and `sdkVersion` are read from the `package.json` file. It will locate the file's path either from the `process.env.npm_package_json` variable that `npm` provides. If that does not exist, it will check for a `DWN_SERVER_PACKAGE_JSON` environment variable set by the user, or otherwise it will default to `/dwn-server/package.json` which is the path within the default Docker container build. diff --git a/package-lock.json b/package-lock.json index bd5ee4a..3ec8af0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@web5/dwn-server", - "version": "0.1.12", + "version": "0.1.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@web5/dwn-server", - "version": "0.1.12", + "version": "0.1.13", "dependencies": { "@tbd54566975/dwn-sdk-js": "0.2.18", "@tbd54566975/dwn-sql-store": "0.2.10", diff --git a/package.json b/package.json index 20bad08..7025a03 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@web5/dwn-server", "type": "module", - "version": "0.1.12", + "version": "0.1.13", "files": [ "dist", "src" diff --git a/src/config.ts b/src/config.ts index 4d9c3f1..ee1f895 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,6 +3,22 @@ import bytes from 'bytes'; export type DwnServerConfig = typeof config; export const config = { + /** + * Used to populate the `server` property returned by the `/info` endpoint. + * + * If running using `npm` the `process.env.npm_package_name` variable exists and we use that, + * otherwise we fall back on the use defined `DWN_SERVER_PACKAGE_NAME` or `@web5/dwn-server`. + */ + serverName: process.env.npm_package_name || process.env.DWN_SERVER_PACKAGE_NAME || '@web5/dwn-server', + /** + * Used to populate the `version` and `sdkVersion` properties returned by the `/info` endpoint. + * + * The `version` and `sdkVersion` are pulled from `package.json` at runtime. + * If running using `npm` the `process.env.npm_package_json` variable exists as the filepath, so we use that. + * Otherwise we check to see if a specific `DWN_SERVER_PACKAGE_JSON` exists, if it does we use that. + * Finally if both of those options don't exist we resort to the path within the docker server image, located at `/dwn-server/package.json` + */ + packageJsonPath: process.env.npm_package_json || process.env.DWN_SERVER_PACKAGE_JSON || '/dwn-server/package.json', // max size of data that can be provided with a RecordsWrite maxRecordDataSize: bytes(process.env.MAX_RECORD_DATA_SIZE || '1gb'), // port that server listens on diff --git a/src/http-api.ts b/src/http-api.ts index 19b4680..8abfa61 100644 --- a/src/http-api.ts +++ b/src/http-api.ts @@ -21,10 +21,10 @@ import { jsonRpcRouter } from './json-rpc-api.js'; import { requestCounter, responseHistogram } from './metrics.js'; import type { RegistrationManager } from './registration/registration-manager.js'; -const packageJson = process.env.npm_package_json ? JSON.parse(readFileSync(process.env.npm_package_json).toString()) : {}; export class HttpApi { #config: DwnServerConfig; + #packageInfo: { version?: string, sdkVersion?: string, server: string }; #api: Express; #server: http.Server; registrationManager: RegistrationManager; @@ -33,6 +33,19 @@ export class HttpApi { constructor(config: DwnServerConfig, dwn: Dwn, registrationManager?: RegistrationManager) { console.log(config); + this.#packageInfo = { + server: config.serverName, + }; + + try { + // We populate the `version` and `sdkVersion` properties from the `package.json` file. + const packageJson = JSON.parse(readFileSync(config.packageJsonPath).toString()); + this.#packageInfo.version = packageJson.version; + this.#packageInfo.sdkVersion = packageJson.dependencies ? packageJson.dependencies['@tbd54566975/dwn-sdk-js'] : undefined; + } catch (error: any) { + log.error('could not read `package.json` for version info', error); + } + this.#config = config; this.#api = express(); this.#server = http.createServer(this.#api); @@ -185,11 +198,11 @@ export class HttpApi { } res.json({ - server : process.env.npm_package_name, + server : this.#packageInfo.server, maxFileSize : config.maxRecordDataSize, registrationRequirements : registrationRequirements, - version : packageJson.version, - sdkVersion : packageJson.dependencies['@tbd54566975/dwn-sdk-js'], + version : this.#packageInfo.version, + sdkVersion : this.#packageInfo.sdkVersion, webSocketSupport : config.webSocketSupport, }); }); diff --git a/src/json-rpc-handlers/dwn/process-message.ts b/src/json-rpc-handlers/dwn/process-message.ts index bed6db3..cac2090 100644 --- a/src/json-rpc-handlers/dwn/process-message.ts +++ b/src/json-rpc-handlers/dwn/process-message.ts @@ -115,7 +115,7 @@ export const handleDwnProcessMessage: JsonRpcHandler = async ( ); // log the unhandled error response - log.error('handleDwnProcessMessage error', jsonRpcResponse, e); + log.error('handleDwnProcessMessage error', jsonRpcResponse, dwnRequest, e); return { jsonRpcResponse } as HandlerResponse; } }; diff --git a/tests/http-api.spec.ts b/tests/http-api.spec.ts index 621660d..eabd370 100644 --- a/tests/http-api.spec.ts +++ b/tests/http-api.spec.ts @@ -1,4 +1,5 @@ // node.js 18 and earlier, needs globalThis.crypto polyfill +import sinon from 'sinon'; import { Cid, DataStream, @@ -18,6 +19,7 @@ import request from 'supertest'; import { v4 as uuidv4 } from 'uuid'; import { config } from '../src/config.js'; +import log from 'loglevel'; import { HttpApi } from '../src/http-api.js'; import type { JsonRpcErrorResponse, @@ -71,6 +73,7 @@ describe('http api', function () { }); beforeEach(async function () { + sinon.restore(); server = await httpApi.start(3000); }); @@ -80,6 +83,7 @@ describe('http api', function () { }); after(function () { + sinon.restore(); clock.restore(); }); @@ -561,6 +565,66 @@ describe('http api', function () { info = await resp.json(); expect(info['server']).to.equal('@web5/dwn-server'); expect(info['webSocketSupport']).to.equal(false); + + // restore old config value + config.webSocketSupport = true; + }); + + it('verify /info still returns when package.json file does not exist', async function () { + server.close(); + server.closeAllConnections(); + + // set up spy to check for an error log by the server + const logSpy = sinon.spy(log, 'error'); + + // set the config to an invalid file path + const packageJsonConfig = config.packageJsonPath; + config.packageJsonPath = '/some/invalid/file.json'; + httpApi = new HttpApi(config, dwn, registrationManager); + server = await httpApi.start(3000); + + const resp = await fetch(`http://localhost:3000/info`); + const info = await resp.json(); + expect(resp.status).to.equal(200); + + // check that server name exists in the info object + expect(info['server']).to.equal('@web5/dwn-server'); + + // check that `sdkVersion` and `version` are undefined as they were not abel to be retrieved from the invalid file. + expect(info['sdkVersion']).to.be.undefined; + expect(info['version']).to.be.undefined; + + // check the logSpy was called + expect(logSpy.callCount).to.equal(1); + expect(logSpy.args[0][0]).to.contain('could not read `package.json` for version info'); + + // restore old config path + config.packageJsonPath = packageJsonConfig; + }); + + it('verify /info returns server name from config', async function () { + server.close(); + server.closeAllConnections(); + + // set a custom name for the `serverName` + const serverName = config.serverName; + config.serverName = '@web5/dwn-server-2' + httpApi = new HttpApi(config, dwn, registrationManager); + server = await httpApi.start(3000); + + const resp = await fetch(`http://localhost:3000/info`); + const info = await resp.json(); + expect(resp.status).to.equal(200); + + // verify that the custom server name was passed to the info endpoint + expect(info['server']).to.equal('@web5/dwn-server-2'); + + // verify that `sdkVersion` and `version` exist. + expect(info['sdkVersion']).to.not.be.undefined; + expect(info['version']).to.not.be.undefined; + + // restore server name config + config.serverName = serverName; }); }); });