diff --git a/docs/api/daf-express.agentrouteroptions.md b/docs/api/daf-express.agentrouteroptions.md index a7823944a..abfbb7e71 100644 --- a/docs/api/daf-express.agentrouteroptions.md +++ b/docs/api/daf-express.agentrouteroptions.md @@ -18,4 +18,5 @@ export interface AgentRouterOptions | [exposedMethods](./daf-express.agentrouteroptions.exposedmethods.md) | Array<string> | List of exposed methods | | [extraMethods](./daf-express.agentrouteroptions.extramethods.md) | Array<string> | List of extra methods | | [getAgentForRequest](./daf-express.agentrouteroptions.getagentforrequest.md) | (req: Request) => Promise<[IAgent](./daf-core.iagent.md)> | Function that returns configured agent for specific request | +| [serveSchema](./daf-express.agentrouteroptions.serveschema.md) | boolean | If set to true, router will serve OpenAPI schema JSON on / route | diff --git a/docs/api/daf-express.agentrouteroptions.serveschema.md b/docs/api/daf-express.agentrouteroptions.serveschema.md new file mode 100644 index 000000000..9268f1372 --- /dev/null +++ b/docs/api/daf-express.agentrouteroptions.serveschema.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [daf-express](./daf-express.md) > [AgentRouterOptions](./daf-express.agentrouteroptions.md) > [serveSchema](./daf-express.agentrouteroptions.serveschema.md) + +## AgentRouterOptions.serveSchema property + +If set to `true`, router will serve OpenAPI schema JSON on `/` route + +Signature: + +```typescript +serveSchema?: boolean; +``` diff --git a/packages/daf-cli/package.json b/packages/daf-cli/package.json index d2b9a031b..87db17dd6 100644 --- a/packages/daf-cli/package.json +++ b/packages/daf-cli/package.json @@ -36,6 +36,7 @@ "date-fns": "^2.8.1", "debug": "^4.1.1", "dotenv": "^8.2.0", + "express": "^4.17.1", "graphql": "^15.0.0", "graphql-tools": "^6.0.0", "inquirer": "^7.0.0", diff --git a/packages/daf-cli/src/cli.ts b/packages/daf-cli/src/cli.ts index bfb1176b6..184b2f85f 100644 --- a/packages/daf-cli/src/cli.ts +++ b/packages/daf-cli/src/cli.ts @@ -9,6 +9,7 @@ import './msg' import './version' import './crypto' import './execute' +import './server' import './setup' if (!process.argv.slice(2).length) { diff --git a/packages/daf-cli/src/graphql.ts b/packages/daf-cli/src/graphql.ts index f791e3b4b..4b6e18537 100644 --- a/packages/daf-cli/src/graphql.ts +++ b/packages/daf-cli/src/graphql.ts @@ -7,7 +7,6 @@ program .command('graphql') .description('GraphQL server') .option('-p, --port ', 'Port') - .option('-i, --interval ', 'Poll for new messages with interval of ') .action(async (cmd) => { const agent = getAgent(program.config) const { typeDefs, resolvers } = createSchema({ diff --git a/packages/daf-cli/src/server.ts b/packages/daf-cli/src/server.ts new file mode 100644 index 000000000..686c12eb6 --- /dev/null +++ b/packages/daf-cli/src/server.ts @@ -0,0 +1,100 @@ +import express from 'express' +import program from 'commander' +import { AgentRouter } from 'daf-express' +import { getAgent } from './setup' + +program + .command('server') + .description('Launch OpenAPI server') + .option('--port ', 'Port', '3332') + .option('--hostname ', 'Server hostname', 'localhost') + .option('--https ', 'Use https instead of http', true) + .option('--createDefaultIdentity ', 'Should the agent create default web did', true) + .option('--messagingServiceEndpoint ', 'Path of the messaging service endpoint', '/messaging') + .option( + '--exposedMethods ', + 'Comma separated list of exposed agent methods (example: "resolveDid,handleMessage")', + ) + .action(async (options) => { + const app = express() + const agent = getAgent(program.config) + + const exposedMethods = options.exposedMethods + ? options.exposedMethods.split(',') + : agent.availableMethods() + + const agentRouter = AgentRouter({ + getAgentForRequest: async (req) => agent, + exposedMethods, + serveSchema: true, + }) + + app.use('/', agentRouter) + + if (options.createDefaultIdentity) { + let serverIdentity = await agent.identityManagerGetOrCreateIdentity({ + provider: 'did:web', + alias: options.hostname, + }) + console.log('Created:', serverIdentity.did) + + if (options.messagingServiceEndpoint) { + const serviceEndpoint = + (options.https ? 'https://' : 'http://') + options.hostname + options.messagingServiceEndpoint + + await agent.identityManagerAddService({ + did: serverIdentity.did, + service: { + id: 'msg', + type: 'Messaging', + serviceEndpoint, + }, + }) + console.log('Added endpoint', serviceEndpoint) + + app.post(serviceEndpoint, express.text({ type: '*/*' }), async (req, res) => { + try { + const message = await agent.handleMessage({ raw: req.body, save: true }) + res.json({ id: message.id }) + } catch (e) { + console.log(e) + res.send(e.message) + } + }) + } + + app.get('/.well-known/did.json', async (req, res) => { + serverIdentity = await agent.identityManagerGetOrCreateIdentity({ + provider: 'did:web', + alias: options.hostname, + }) + + const didDoc = { + '@context': 'https://w3id.org/did/v1', + id: serverIdentity.did, + publicKey: serverIdentity.keys.map((key) => ({ + id: serverIdentity.did + '#' + key.kid, + type: key.type === 'Secp256k1' ? 'Secp256k1VerificationKey2018' : 'Ed25519VerificationKey2018', + owner: serverIdentity.did, + publicKeyHex: key.publicKeyHex, + })), + authentication: serverIdentity.keys.map((key) => ({ + type: + key.type === 'Secp256k1' + ? 'Secp256k1SignatureAuthentication2018' + : 'Ed25519SignatureAuthentication2018', + publicKey: serverIdentity.did + '#' + key.kid, + })), + service: serverIdentity.services, + } + + res.json(didDoc) + }) + } + + app.listen(options.port, () => { + console.log(`🚀 Server ready at http://${options.hostname}:${options.port}`) + console.log('Enabled agent methods', JSON.stringify(agent.availableMethods())) + console.log('Exposed methods', JSON.stringify(exposedMethods)) + }) + }) diff --git a/packages/daf-cli/tsconfig.json b/packages/daf-cli/tsconfig.json index d787d0299..b69ba16f6 100644 --- a/packages/daf-cli/tsconfig.json +++ b/packages/daf-cli/tsconfig.json @@ -17,6 +17,7 @@ { "path": "../daf-resolver-universal" }, { "path": "../daf-resolver" }, { "path": "../daf-rest" }, + { "path": "../daf-express" }, { "path": "../daf-selective-disclosure" }, { "path": "../daf-typeorm" }, { "path": "../daf-url" }, diff --git a/packages/daf-express/src/index.ts b/packages/daf-express/src/index.ts index 927c3d03b..e845da9ab 100644 --- a/packages/daf-express/src/index.ts +++ b/packages/daf-express/src/index.ts @@ -22,7 +22,7 @@ import { IAgent } from 'daf-core' import { Request, Response, NextFunction, Router, json } from 'express' -import { supportedMethods } from 'daf-rest' +import { supportedMethods, openApiSchema } from 'daf-rest' import Debug from 'debug' interface RequestWithAgent extends Request { @@ -47,6 +47,11 @@ export interface AgentRouterOptions { * List of extra methods */ extraMethods?: Array + + /** + * If set to `true`, router will serve OpenAPI schema JSON on `/` route + */ + serveSchema?: boolean } /** @@ -65,10 +70,15 @@ export const AgentRouter = (options: AgentRouterOptions): Router => { const allMethods: Array = supportedMethods.concat(options.extraMethods ? options.extraMethods : []) + const publicApi = { ...openApiSchema } + publicApi.paths = {} + for (const exposedMethod of options.exposedMethods) { if (!allMethods.includes(exposedMethod)) throw Error('Method not supported: ' + exposedMethod) Debug('daf:express:initializing')(exposedMethod) + publicApi.paths['/' + exposedMethod] = openApiSchema.paths['/' + exposedMethod] + router.post('/' + exposedMethod, async (req: RequestWithAgent, res: Response, next: NextFunction) => { if (!req.agent) throw Error('Agent not available') try { @@ -80,5 +90,11 @@ export const AgentRouter = (options: AgentRouterOptions): Router => { }) } + if (options.serveSchema) { + router.get('/', (req, res) => { + res.json(publicApi) + }) + } + return router } diff --git a/report/daf-express.api.md b/report/daf-express.api.md index 979b9e392..2a80254f9 100644 --- a/report/daf-express.api.md +++ b/report/daf-express.api.md @@ -16,6 +16,7 @@ export interface AgentRouterOptions { exposedMethods: Array; extraMethods?: Array; getAgentForRequest: (req: Request_2) => Promise; + serveSchema?: boolean; }