diff --git a/packages/headless-web3-provider/CHANGELOG.md b/packages/headless-web3-provider/CHANGELOG.md new file mode 100644 index 000000000..a3ce99779 --- /dev/null +++ b/packages/headless-web3-provider/CHANGELOG.md @@ -0,0 +1,21 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.14.1](https://github.com/Sphereon-Opensource/SSI-SDK/compare/v0.14.0...v0.14.1) (2023-07-31) + +**Note:** Version bump only for package @sphereon/ssi-sdk.express-support + + + + + +# [0.14.0](https://github.com/Sphereon-Opensource/SSI-SDK/compare/v0.13.0...v0.14.0) (2023-07-30) + + +### Features + +* Add express builder, cors configurer, passport authentication and casbin authorization support for APIs. ([cb04fe8](https://github.com/Sphereon-Opensource/SSI-SDK/commit/cb04fe8b84ce6f4c840afef43d628f23cb8e9e36)) +* Add global web resolution provider. Add json error handler ([f19d1d1](https://github.com/Sphereon-Opensource/SSI-SDK/commit/f19d1d135a9944a6c9e4c6040c58e7563c4442f2)) +* Allow objects for error response. Improve json handling in error responses ([4151c73](https://github.com/Sphereon-Opensource/SSI-SDK/commit/4151c73b4cdeb931c0deb8b8f34ed9c215efe5ba)) diff --git a/packages/headless-web3-provider/LICENSE b/packages/headless-web3-provider/LICENSE new file mode 100644 index 000000000..8a21446d8 --- /dev/null +++ b/packages/headless-web3-provider/LICENSE @@ -0,0 +1,201 @@ + 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 [2022] [Sphereon BV] + + 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. diff --git a/packages/headless-web3-provider/README.md b/packages/headless-web3-provider/README.md new file mode 100644 index 000000000..78c242e58 --- /dev/null +++ b/packages/headless-web3-provider/README.md @@ -0,0 +1,25 @@ + +

+
+ Sphereon +
Express Support +
+

+ +This plugin adds Express REST API support, used in multiple plugins. It allows to configure multiple express options, and provides defaults. +It also add support for Passport based authentication, allowing integration with many different strategies/solutions, including OpenID Connect. +Lastly it has an integration with Azure/Entra Active Directory + +Note: Be aware that this plugin only runs in NodeJS environments! + +## Installation + +```shell +pnpm add @sphereon/ssi-sdk.express-support +``` + +## Build + +```shell +pnpm build +``` diff --git a/packages/headless-web3-provider/api-extractor.json b/packages/headless-web3-provider/api-extractor.json new file mode 100644 index 000000000..94c2c6a9f --- /dev/null +++ b/packages/headless-web3-provider/api-extractor.json @@ -0,0 +1,3 @@ +{ + "extends": "../include/api-extractor-base.json" +} diff --git a/packages/headless-web3-provider/package.json b/packages/headless-web3-provider/package.json new file mode 100644 index 000000000..e90dbf70c --- /dev/null +++ b/packages/headless-web3-provider/package.json @@ -0,0 +1,59 @@ +{ + "name": "@sphereon/ssi-sdk-web3.headless-provider", + "version": "0.14.1", + "source": "src/index.ts", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc --build", + "build:clean": "tsc --build --clean && tsc --build" + }, + "dependencies": { + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/abstract-provider": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/random": "^5.7.0", + "@ethersproject/signing-key": "^5.7.0", + "@ethersproject/strings": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/wallet": "^5.7.0", + "@metamask/eth-sig-util": "^6.0.0", + "@veramo/core": "4.2.0", + "@veramo/key-manager": "4.2.0", + "uint8arrays": "3.1.1", + "casbin": "^5.26.1", + "cors": "^2.8.5", + "dotenv-flow": "^3.2.0", + "express": "^4.18.2", + "express-session": "^1.17.3", + "passport": "^0.6.0", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@types/body-parser": "^1.19.2", + "@types/cors": "^2.8.13", + "@types/dotenv-flow": "^3.2.0", + "@types/express": "^4.17.17", + "@types/express-serve-static-core": "^4.17.35", + "@types/express-session": "^1.17.7", + "@types/passport": "^1.0.12", + "typescript": "4.9.5" + }, + "files": [ + "dist/**/*", + "src/**/*", + "README.md", + "LICENSE" + ], + "publishConfig": { + "access": "public" + }, + "repository": "git@github.com:Sphereon-Opensource/SSI-SDK.git", + "author": "Sphereon ", + "license": "Apache-2.0", + "keywords": [ + "Sphereon", + "SSI", + "Agent" + ] +} diff --git a/packages/headless-web3-provider/src/auth-utils.ts b/packages/headless-web3-provider/src/auth-utils.ts new file mode 100644 index 000000000..326b06995 --- /dev/null +++ b/packages/headless-web3-provider/src/auth-utils.ts @@ -0,0 +1,85 @@ +import express, { NextFunction } from 'express' +import passport from 'passport' +import { EndpointArgs } from './types' + +export const checkUserIsInRole = (opts: { roles: string | string[] }) => (req: express.Request, res: express.Response, next: NextFunction) => { + if (!opts?.roles || opts.roles.length === 0) { + return next() + } + const roles = Array.isArray(opts.roles) ? opts.roles : [opts.roles] + if (!req?.user || !('role' in req.user)) { + return res.status(401).end() + } + + // @ts-ignore + const hasRole = roles.find((role) => req.user.role.toLowerCase() === role.toLowerCase()) + if (!hasRole) { + return res.status(403).end() + } + + return next() +} + +const checkAuthenticationImpl = (req: express.Request, res: express.Response, opts?: EndpointArgs) => { + if (!opts || !opts.authentication || opts.authentication.enabled === false) { + return + } + if (!opts.authentication.strategy) { + return res.status(401).end() + } + passport.authenticate(opts.authentication.strategy) + + if (typeof req.isAuthenticated !== 'function' || !req.isAuthenticated()) { + return res.status(403).end() + } + return +} +const checkAuthorizationImpl = (req: express.Request, res: express.Response, opts?: EndpointArgs) => { + if (!opts || !opts.authentication || !opts.authorization || opts.authentication.enabled === false || opts?.authorization.enabled === false) { + return + } + const authorization = opts.authorization + + if (!authorization.enforcer && (!authorization.requireUserInRoles || authorization.requireUserInRoles.length === 0)) { + console.log(`Authorization enabled for endpoint, but no enforcer or roles supplied`) + return res.status(401).end() + } + if (authorization.requireUserInRoles && authorization.requireUserInRoles.length > 0) { + checkUserIsInRole({ roles: authorization.requireUserInRoles }) + } + if (authorization.enforcer) { + const enforcer = authorization.enforcer + const permitted = enforcer.enforceSync(req.user, opts.resource, opts.operation) + if (!permitted) { + console.log(`Access to ${opts.resource} and op ${opts.operation} not allowed for ${req.user}`) + return res.status(403).end() + } + } + return +} + +const executeRequestHandlers = (req: express.Request, res: express.Response, next: NextFunction, opts?: EndpointArgs) => { + if (opts?.handlers) { + opts.handlers.forEach((requestHandler) => requestHandler(req, res, next)) + } +} + +export const checkAuthenticationOnly = (opts?: EndpointArgs) => (req: express.Request, res: express.Response, next: NextFunction) => { + executeRequestHandlers(req, res, next, opts) + return checkAuthenticationImpl(req, res, opts) ?? next() +} + +export const checkAuthorizationOnly = (opts?: EndpointArgs) => (req: express.Request, res: express.Response, next: NextFunction) => { + executeRequestHandlers(req, res, next, opts) + return checkAuthorizationImpl(req, res, opts) ?? next() +} +export const checkAuth = (opts?: EndpointArgs) => (req: express.Request, res: express.Response, next: NextFunction) => { + /*const handlers = /!*this._handlers ??*!/ [] + checkAuthenticationImpl(req, res, opts)) + handlers.push(checkAuthorizationImpl(req, res, opts)) + handlers.push(next) + return handlers +*/ + executeRequestHandlers(req, res, next, opts) + return checkAuthenticationImpl(req, res, opts) ?? checkAuthorizationImpl(req, res, opts) ?? next() +} diff --git a/packages/headless-web3-provider/src/builders.ts b/packages/headless-web3-provider/src/builders.ts new file mode 100644 index 000000000..4e7fd295a --- /dev/null +++ b/packages/headless-web3-provider/src/builders.ts @@ -0,0 +1,292 @@ +/** + * @public + */ +import bodyParser from 'body-parser' +import { Enforcer } from 'casbin' +import cors, { CorsOptions } from 'cors' +import express, { Express } from 'express' +import { Application, ApplicationRequestHandler } from 'express-serve-static-core' +import session from 'express-session' +import passport, { InitializeOptions } from 'passport' +import { checkUserIsInRole } from './auth-utils' +import { env, jsonErrorHandler } from './functions' +import { ExpressBuildResult, IExpressServerOpts } from './types' +import * as dotenv from 'dotenv-flow' + +dotenv.config() + +export class ExpressBuilder { + private existingExpress?: Express + private hostnameOrIP?: string + private port?: number + private _handlers?: ApplicationRequestHandler[] = [] + private listenCallback?: () => void + private _startListen?: boolean | undefined = undefined + private readonly envVarPrefix?: string + private _corsConfigurer?: ExpressCorsConfigurer + private _sessionOpts?: session.SessionOptions + private _usePassportAuth?: boolean = false + private _passportInitOpts?: InitializeOptions + private _userIsInRole?: string | string[] + private _enforcer?: Enforcer + + private constructor(opts?: { existingExpress?: Express; envVarPrefix?: string }) { + const { existingExpress, envVarPrefix } = opts ?? {} + if (existingExpress) { + this.withExpress(existingExpress) + } + this.envVarPrefix = envVarPrefix ?? '' + } + + public static fromExistingExpress(opts?: { existingExpress?: Express; envVarPrefix?: string }) { + return new ExpressBuilder(opts ?? {}) + } + + public static fromServerOpts(opts: IExpressServerOpts & { envVarPrefix?: string }) { + const builder = new ExpressBuilder({ existingExpress: opts?.existingExpress, envVarPrefix: opts?.envVarPrefix }) + return builder.withEnableListenOpts(opts) + } + + public enableListen(startOnBuild?: boolean): this { + if (startOnBuild !== undefined) { + this._startListen = startOnBuild + } + return this + } + + public withEnableListenOpts({ + port, + hostnameOrIP, + callback, + startOnBuild, + }: { + port?: number + hostnameOrIP?: string + startOnBuild?: boolean + callback?: () => void + }): this { + port && this.withPort(port) + hostnameOrIP && this.withHostname(hostnameOrIP) + if (typeof callback === 'function') { + this.withListenCallback(callback) + } + this._startListen = startOnBuild !== false + return this + } + + public withPort(port: number): this { + this.port = port + return this + } + + public withHostname(hostnameOrIP: string): this { + this.hostnameOrIP = hostnameOrIP + return this + } + + public withListenCallback(callback: () => void): this { + this.listenCallback = callback + return this + } + + public withExpress(existingExpress: Express): this { + this.existingExpress = existingExpress + this._startListen = false + return this + } + + public withCorsConfigurer(configurer: ExpressCorsConfigurer): this { + this._corsConfigurer = configurer + return this + } + + public withPassportAuth(usePassport: boolean, initializeOptions?: InitializeOptions): this { + this._usePassportAuth = usePassport + this._passportInitOpts = initializeOptions + return this + } + + public withGlobalUserIsInRole(userIsInRole: string | string[]): this { + this._userIsInRole = userIsInRole + return this + } + + public withEnforcer(enforcer: Enforcer): this { + this._enforcer = enforcer + return this + } + + public startListening(express: Express) { + return express.listen(this.getPort(), this.getHostname(), this.listenCallback) + } + + public getHostname(): string { + return this.hostnameOrIP ?? env('HOSTNAME', this.envVarPrefix) ?? '0.0.0.0' + } + + public getPort(): number { + return (this.port ?? env('PORT', this.envVarPrefix) ?? 5000) as number + } + + public setHandlers(handlers: ApplicationRequestHandler | ApplicationRequestHandler[]): this { + if (Array.isArray(handlers)) { + this._handlers = handlers + } else if (handlers) { + if (!this._handlers) { + this._handlers = [] + } + this._handlers.push(handlers) + } else { + this._handlers = [] + } + + return this + } + + public addHandler(handler: ApplicationRequestHandler): this { + if (!this._handlers) { + this._handlers = [] + } + this._handlers.push(handler) + return this + } + + public withSessionOptions(sessionOpts: session.SessionOptions): this { + this._sessionOpts = sessionOpts + return this + } + + public build(opts?: { + express?: Express + startListening?: boolean + handlers?: ApplicationRequestHandler | ApplicationRequestHandler[] + }): ExpressBuildResult { + const express = this.buildExpress(opts) + return { + express, + port: this.getPort(), + hostname: this.getHostname(), + userIsInRole: this._userIsInRole, + startListening: this._startListen !== false, + enforcer: this._enforcer, + } + } + + protected buildExpress(opts?: { + express?: Express + startListening?: boolean + handlers?: ApplicationRequestHandler | ApplicationRequestHandler[] + }): express.Express { + const app: express.Express = opts?.express ?? this.existingExpress ?? express() + if (this._sessionOpts) { + // @ts-ignore + app.use(session(this._sessionOpts)) + } + if (this._usePassportAuth) { + app.use(passport.initialize(this._passportInitOpts)) + if (this._sessionOpts) { + app.use(passport.session()) + } + } + if (this._userIsInRole) { + app.use(checkUserIsInRole({ roles: this._userIsInRole })) + } + if (this._corsConfigurer) { + this._corsConfigurer.configure({ existingExpress: app }) + } + + app.use(jsonErrorHandler) + + // @ts-ignore + this._handlers && this._handlers.length > 0 && app.use(this._handlers) + // @ts-ignore + opts?.handlers && app.use(opts.handlers) + + app.use(bodyParser.urlencoded({ extended: true })) + app.use(bodyParser.json()) + + if (this._startListen !== false) { + this.startListening(app) + } + return app + } +} + +export class ExpressCorsConfigurer { + private _disableCors?: boolean + private _enablePreflightOptions?: boolean + private _allowOrigin?: boolean | string | RegExp | Array + private _allowMethods?: string | string[] + private _allowedHeaders?: string | string[] + private _allowCredentials?: boolean + private readonly _express?: Express + private readonly _envVarPrefix?: string + + constructor({ existingExpress, envVarPrefix }: { existingExpress?: Express; envVarPrefix?: string }) { + this._express = existingExpress + this._envVarPrefix = envVarPrefix + } + + public allowOrigin(value: string | boolean | RegExp | Array): this { + this._allowOrigin = value + return this + } + + public disableCors(value: boolean): this { + this._disableCors = value + return this + } + + public allowMethods(value: string | string[]): this { + this._allowMethods = value + return this + } + + public allowedHeaders(value: string | string[]): this { + this._allowedHeaders = value + return this + } + + public allowCredentials(value: boolean): this { + this._allowCredentials = value + return this + } + + public configure({ existingExpress }: { existingExpress?: Express }) { + const express = existingExpress ?? this._express + if (!express) { + throw Error('No express passed in during construction or configure') + } + + const disableCorsEnv = env('CORS_DISABLE', this._envVarPrefix) + const corsDisabled = this._disableCors ?? (disableCorsEnv ? /true/.test(disableCorsEnv) : false) + if (corsDisabled) { + return + } + const envAllowOriginStr = env('CORS_ALLOW_ORIGIN', this._envVarPrefix) ?? '*' + let envAllowOrigin: string[] | string + if (envAllowOriginStr.includes(',')) { + envAllowOrigin = envAllowOriginStr.split(',') + } else if (envAllowOriginStr.includes(' ')) { + envAllowOrigin = envAllowOriginStr.split(' ') + } else { + envAllowOrigin = envAllowOriginStr + } + if (Array.isArray(envAllowOrigin) && envAllowOrigin.length === 1) { + envAllowOrigin = envAllowOrigin[0] + } + const corsOptions: CorsOptions = { + origin: this._allowOrigin ?? envAllowOrigin, + // todo: env vars + ...(this._allowMethods && { methods: this._allowMethods }), + ...(this._allowedHeaders && { allowedHeaders: this._allowedHeaders }), + ...(this._allowCredentials !== undefined && { credentials: this._allowCredentials }), + optionsSuccessStatus: 204, + } + + if (this._enablePreflightOptions) { + express.options('*', cors(corsOptions)) + } + express.use(cors(corsOptions)) + } +} diff --git a/packages/headless-web3-provider/src/event-emitter.ts b/packages/headless-web3-provider/src/event-emitter.ts new file mode 100644 index 000000000..88420d202 --- /dev/null +++ b/packages/headless-web3-provider/src/event-emitter.ts @@ -0,0 +1,39 @@ +export class EventEmitter { + private readonly listeners: Record void>> = + Object.create(null) + + emit(eventName: string, ...args: any[]): boolean { + this.listeners[eventName]?.forEach((listener) => { + listener(...args) + }) + return true + } + + on(eventName: string, listener: (...args: any[]) => void): this { + this.listeners[eventName] ??= [] + this.listeners[eventName]?.push(listener) + return this + } + + off(eventName: string, listener: (...args: any[]) => void): this { + const listeners = this.listeners[eventName] ?? [] + + for (const [i, listener_] of listeners.entries()) { + if (listener === listener_) { + listeners.splice(i, 1) + break + } + } + + return this + } + + once(eventName: string, listener: (...args: any[]) => void): this { + const cb = (...args: any[]): void => { + this.off(eventName, cb) + listener(...args) + } + + return this.on(eventName, cb) + } +} diff --git a/packages/headless-web3-provider/src/functions.ts b/packages/headless-web3-provider/src/functions.ts new file mode 100644 index 000000000..5eb95bce0 --- /dev/null +++ b/packages/headless-web3-provider/src/functions.ts @@ -0,0 +1,31 @@ +import express, { NextFunction } from 'express' +import process from 'process' + +export function env(key?: string, prefix?: string): string | undefined { + if (!key) { + return + } + return process.env[`${prefix ? prefix.trim() : ''}${key}`] +} + +export function sendErrorResponse(response: express.Response, statusCode: number, message: string | object, error?: Error) { + console.log(message) + if (error) { + console.log(error) + } + response.statusCode = statusCode + if (typeof message === 'string' && !message.startsWith('{')) { + message = { error: message } + } + if (typeof message === 'string' && message.startsWith('{')) { + return response.status(statusCode).end(message) + } + return response.status(statusCode).json(message) +} + +export const jsonErrorHandler = (err: any, req: express.Request, res: express.Response, next: NextFunction) => { + if (res.headersSent) { + return next(err) + } + return sendErrorResponse(res, 500, err.message, err) +} diff --git a/packages/headless-web3-provider/src/headless-web3-provider.ts b/packages/headless-web3-provider/src/headless-web3-provider.ts new file mode 100644 index 000000000..31eb2e499 --- /dev/null +++ b/packages/headless-web3-provider/src/headless-web3-provider.ts @@ -0,0 +1,358 @@ +import {Signer} from "@ethersproject/abstract-signer"; +import { + filter, + firstValueFrom, + BehaviorSubject, + switchMap, + from, + first, + tap, +} from 'rxjs' +import {signTypedData, SignTypedDataVersion} from '@metamask/eth-sig-util' +import assert from 'assert/strict' + +import {Web3RequestKind} from './utils' +import { + ChainDisconnected, + Deny, + Disconnected, + ErrorWithCode, + Unauthorized, + UnsupportedMethod, +} from './errors' +import {ChainConnection, IWeb3Provider, PendingRequest, Web3ProviderConfig} from './types' +import {EventEmitter} from './event-emitter' + + +export class KMSWeb3Provider extends EventEmitter implements IWeb3Provider { + private _pendingRequests = new BehaviorSubject([]) + private _signers: Signer[] = [] + private _activeChainId: number + private _rpc: Record = {} + private _config: { debug: boolean; logger: typeof console.log } + private _authorizedRequests: { [K in Web3RequestKind | string]?: boolean } = {} + + constructor(privateKeys: string[], private readonly chains: ChainConnection[], config: Web3ProviderConfig = {}) { + super() + this._signers = privateKeys.map((key) => new ethers.Wallet(key)) + this._activeChainId = chains[0].chainId + this._config = Object.assign({debug: true, logger: console.log}, config) + } + + request(args: { method: 'eth_accounts'; params: [] }): Promise + request(args: { + method: 'eth_requestAccounts' + params: string[] + }): Promise + request(args: { method: 'net_version'; params: [] }): Promise + request(args: { method: 'eth_chainId'; params: [] }): Promise + request(args: { method: 'personal_sign'; params: string[] }): Promise + request(args: { + method: 'eth_signTypedData' | 'eth_signTypedData_v1' + params: [object[], string] + }): Promise + request(args: { + method: 'eth_signTypedData_v3' | 'eth_signTypedData_v4' + params: string[] + }): Promise + async request({ + method, + params, + }: { + method: string + params: any[] + }): Promise { + if (this._config.debug) { + this._config.logger({method, params}) + } + + switch (method) { + case 'eth_call': + case 'eth_estimateGas': + case 'eth_blockNumber': + case 'eth_getBlockByNumber': + case 'eth_getTransactionByHash': + case 'eth_getTransactionReceipt': + return this.getRpc().send(method, params) + + case 'eth_requestAccounts': + case 'eth_accounts': + return this.waitAuthorization( + {method, params}, + async () => { + const {chainId} = this.getCurrentChain() + this.emit('connect', {chainId}) + return Promise.all( + this._signers.map((wallet) => wallet.getAddress()) + ) + }, + true, + 'eth_requestAccounts' + ) + + case 'eth_chainId': { + const {chainId} = this.getCurrentChain() + return '0x' + chainId.toString(16) + } + + case 'net_version': { + const {chainId} = this.getCurrentChain() + return chainId + } + + case 'eth_sendTransaction': { + return this.waitAuthorization({method, params}, async () => { + const wallet = this.getCurrentWallet() + const rpc = this.getRpc() + const {gas, ...txRequest} = params[0] + const tx = await wallet.connect(rpc).sendTransaction(txRequest) + return tx.hash + }) + } + + case 'wallet_addEthereumChain': { + return this.waitAuthorization({method, params}, async () => { + const chainId = Number(params[0].chainId) + const rpcUrl = params[0].rpcUrl + this.addNetwork(chainId, rpcUrl) + return null + }) + } + + case 'wallet_switchEthereumChain': { + if (this._activeChainId === Number(params[0].chainId)) { + return null + } + return this.waitAuthorization({method, params}, async () => { + const chainId = Number(params[0].chainId) + this.switchNetwork(chainId) + return null + }) + } + + case 'personal_sign': { + return this.waitAuthorization({method, params}, async () => { + const wallet = this.getCurrentWallet() + const address = await wallet.getAddress() + assert.equal(address, ethers.getAddress(params[1])) + const message = toUtf8String(params[0]) + + const signature = await wallet.signMessage(message) + if (this._config.debug) { + this._config.logger('personal_sign', { + message, + signature, + }) + } + + return signature + }) + } + + case 'eth_signTypedData': + case 'eth_signTypedData_v1': { + return this.waitAuthorization({method, params}, async () => { + const wallet = this.getCurrentWallet() as Wallet + const address = await wallet.getAddress() + assert.equal(address, ethers.getAddress(params[1])) + + const msgParams = params[0] + + return signTypedData({ + privateKey: Buffer.from(wallet.privateKey.slice(2), 'hex'), + data: msgParams, + version: SignTypedDataVersion.V1, + }) + }) + } + + case 'eth_signTypedData_v3': + case 'eth_signTypedData_v4': { + return this.waitAuthorization({method, params}, async () => { + const wallet = this.getCurrentWallet() as Wallet + const address = await wallet.getAddress() + assert.equal(address, ethers.getAddress(params[0])) + + const msgParams = JSON.parse(params[1]) + + return signTypedData({ + privateKey: Buffer.from(wallet.privateKey.slice(2), 'hex'), + data: msgParams, + version: + method === 'eth_signTypedData_v4' + ? SignTypedDataVersion.V4 + : SignTypedDataVersion.V3, + }) + }) + } + + default: + throw UnsupportedMethod() + } + } + + getCurrentWallet(): Signer { + const wallet = this._signers[0] + + if (!wallet) { + throw Unauthorized() + } + + return wallet + } + + waitAuthorization( + requestInfo: PendingRequest['requestInfo'], + task: () => Promise, + permanentPermission = false, + methodOverride?: string + ) { + const method = methodOverride ?? requestInfo.method + + if (this._authorizedRequests[method]) { + return task() + } + + return new Promise((resolve, reject) => { + const pendingRequest: PendingRequest = { + requestInfo: requestInfo, + authorize: async () => { + if (permanentPermission) { + this._authorizedRequests[method] = true + } + + resolve(await task()) + }, + reject(err) { + reject(err) + }, + } + + this._pendingRequests.next( + this._pendingRequests.getValue().concat(pendingRequest) + ) + }) + } + + private consumeRequest(requestKind: Web3RequestKind) { + return firstValueFrom( + this._pendingRequests.pipe( + switchMap((a) => from(a)), + filter((request) => { + return request.requestInfo.method === requestKind + }), + first(), + tap((item) => { + this._pendingRequests.next( + without(this._pendingRequests.getValue(), item) + ) + }) + ) + ) + } + + private consumeAllRequests() { + const pendingRequests = this._pendingRequests.getValue() + this._pendingRequests.next([]) + return pendingRequests + } + + getPendingRequests(): PendingRequest['requestInfo'][] { + return this._pendingRequests.getValue().map((pendingRequest) => pendingRequest.requestInfo) + } + + getPendingRequestCount(requestKind?: Web3RequestKind): number { + const pendingRequests = this._pendingRequests.getValue() + if (!requestKind) { + return pendingRequests.length + } + + return pendingRequests.filter( + (pendingRequest) => pendingRequest.requestInfo.method === requestKind + ).length + } + + async authorize(requestKind: Web3RequestKind): Promise { + const pendingRequest = await this.consumeRequest(requestKind) + return pendingRequest.authorize() + } + + async reject( + requestKind: Web3RequestKind, + reason: ErrorWithCode = Deny() + ): Promise { + const pendingRequest = await this.consumeRequest(requestKind) + return pendingRequest.reject(reason) + } + + authorizeAll(): void { + this.consumeAllRequests().forEach((request) => request.authorize()) + } + + rejectAll(reason: ErrorWithCode = Deny()): void { + this.consumeAllRequests().forEach((request) => request.reject(reason)) + } + + async changeAccounts(privateKeys: string[]): Promise { + this._signers = privateKeys.map((key) => new ethers.Wallet(key)) + this.emit( + 'accountsChanged', + await Promise.all(this._signers.map((wallet) => wallet.getAddress())) + ) + } + + private getCurrentChain(): ChainConnection { + const chainConn = this.chains.find( + chainConn => chainConn.chainId === this._activeChainId + ) + if (!chainConn) { + throw Disconnected() + } + return chainConn + } + + private getRpc(): ethers.JsonRpcProvider { + const chainConn = this.getCurrentChain() + let rpc = this._rpc[chainConn.chainId] + + if (!rpc) { + rpc = new ethers.JsonRpcProvider(chainConn.rpcUrl, chainConn.chainId) + this._rpc[chainConn.chainId] = rpc + } + + return rpc + } + + getNetwork(): ChainConnection { + return this.getCurrentChain() + } + + getNetworks(): ChainConnection[] { + return this.chains + } + + addNetwork(chainId: number, rpcUrl: string): void { + this.chains.push({chainId, rpcUrl}) + } + + switchNetwork(chainId: number): void { + const idx = this.chains.findIndex( + connection => connection.chainId === chainId + ) + if (idx < 0) { + throw ChainDisconnected() + } + if (chainId !== this._activeChainId) { + this._activeChainId = chainId + this.emit('chainChanged', chainId) + } + } +} + +function without(list: T[], item: T): T[] { + const idx = list.indexOf(item) + if (idx >= 0) { + return list.slice(0, idx).concat(list.slice(idx + 1)) + } + return list +} diff --git a/packages/headless-web3-provider/src/index.ts b/packages/headless-web3-provider/src/index.ts new file mode 100644 index 000000000..557baa604 --- /dev/null +++ b/packages/headless-web3-provider/src/index.ts @@ -0,0 +1,4 @@ +export * from './auth-utils' +export * from './builders' +export * from './types' +export { sendErrorResponse, jsonErrorHandler } from './functions' diff --git a/packages/headless-web3-provider/src/signer.ts b/packages/headless-web3-provider/src/signer.ts new file mode 100644 index 000000000..0d86e6e38 --- /dev/null +++ b/packages/headless-web3-provider/src/signer.ts @@ -0,0 +1,111 @@ +import {Provider, TransactionRequest} from "@ethersproject/abstract-provider"; +import {Signer, TypedDataDomain, TypedDataField, TypedDataSigner} from "@ethersproject/abstract-signer"; +import {arrayify} from "@ethersproject/bytes"; +import { serialize } from "@ethersproject/transactions"; +import {IKey, ManagedKeyInfo} from "@veramo/core"; +import {AbstractKeyManagementSystem, Eip712Payload} from "@veramo/key-manager"; +import * as u8a from 'uint8arrays' + + +export class Web3KMSSignerBuilder { + private kms?: AbstractKeyManagementSystem + private keyRef?: Pick + private provider?: Provider + + withKms(kms: AbstractKeyManagementSystem): this { + this.kms = kms + return this + } + + withKid(kid: string): this { + this.keyRef = {kid} + return this + } + + withKeyRef(keyRef: Pick | string): this { + if (typeof keyRef === 'string') { + return this.withKid(keyRef) + } + + this.keyRef = keyRef + return this + } + + withProvider(provider: Provider): this { + this.provider = provider + return this + } + + build() { + if (!this.kms) { + throw Error('KMS needs to be provided') + } + if (!this.keyRef) { + throw Error('Keyref needs to be provided') + } + return new Web3KMSSigner({kms: this.kms, keyRef: this.keyRef, provider: this.provider}) + } +} + +/** + * This is a Web3 signer that delegates back to the KMS for the actual signatures. + * This means we do not expose private keys and can use any Secp256k1 key stored in the KMS if we want + * + * Be aware that the provided KeyRef needs to belong to the respective KMS, as it will use a lookup for the key in the KMS to sign + */ +export class Web3KMSSigner extends Signer implements TypedDataSigner { + private readonly kms: AbstractKeyManagementSystem + private readonly keyRef: Pick + + + constructor({provider, kms, keyRef}: { + provider?: Provider, + kms: AbstractKeyManagementSystem, + keyRef: Pick + }) { + super(provider); + this.kms = kms; + this.keyRef = keyRef + } + + + + private async getKey(): Promise { + const keys = await this.kms.listKeys(); + return keys.find(key => key.kid === this.keyRef.kid); + } + + async signTransaction(tx: TransactionRequest): Promise { + return this.kms.sign({ + algorithm: 'eth_signTransaction', + keyRef: this.keyRef, + // @ts-ignore + data: arrayify(serialize(tx)) + }) + } + + async signMessage(message: string | Uint8Array): Promise { + return this.kms.sign({ + algorithm: 'eth_signMessage', + keyRef: this.keyRef, + data: typeof message === 'string' ? u8a.fromString(message) : message + }) + } + + async _signTypedData(domain: TypedDataDomain, types: Record>, value: Record): Promise { + const jsonData: Partial = { + domain, + types, + message: value + } + return this.kms.sign({ + algorithm: 'eth_signTypedData', + keyRef: this.keyRef, + data: u8a.fromString(JSON.stringify(jsonData)) + }) + } + + async getAddress(): Promise { + return `0x${await this.getKey().then(key => key?.publicKeyHex)}` + } +} diff --git a/packages/headless-web3-provider/src/types.ts b/packages/headless-web3-provider/src/types.ts new file mode 100644 index 000000000..1eec6aa22 --- /dev/null +++ b/packages/headless-web3-provider/src/types.ts @@ -0,0 +1,91 @@ +import { Enforcer } from 'casbin' +import { Express, RequestHandler } from 'express' +import { Strategy } from 'passport' + +export interface IWeb3Provider { + isMetaMask?: boolean + + request(args: { method: 'eth_accounts'; params: [] }): Promise + request(args: { + method: 'eth_requestAccounts' + params: [] + }): Promise + request(args: { method: 'net_version'; params: [] }): Promise + request(args: { method: 'eth_chainId'; params: [] }): Promise + request(args: { method: 'personal_sign'; params: string[] }): Promise + request(args: { + method: 'eth_signTypedData' | 'eth_signTypedData_v1' + params: [object[], string] + }): Promise + request(args: { + method: 'eth_signTypedData_v3' | 'eth_signTypedData_v4' + params: string[] + }): Promise + request(args: { method: string; params?: any[] }): Promise + + emit(eventName: string, ...args: any[]): void + on(eventName: string, listener: (eventName: string) => void): void +} + +export interface PendingRequest { + requestInfo: { method: string; params: any[] } + reject: (err: { message?: string; code?: number }) => void + authorize: () => Promise +} + +export interface ChainConnection { + chainId: number + rpcUrl: string +} + +export interface Web3ProviderConfig { + debug?: boolean + logger?: typeof console.log +} + + + +export interface IExpressServerOpts { + port?: number // The port to listen on + cookieSigningKey?: string + hostname?: string // defaults to "0.0.0.0", meaning it will listen on all IP addresses. Can be an IP address or hostname + basePath?: string + existingExpress?: Express + listenCallback?: () => void + startListening?: boolean + // passport?: passport.PassportStatic + // externalBaseUrl?: string // In case an external base URL needs to be exposed +} + +export interface ExpressBuildResult { + express: Express + port: number + hostname: string + userIsInRole?: string | string[] + startListening: boolean + enforcer?: Enforcer +} + +export interface ISingleEndpointOpts { + endpoint?: EndpointArgs + enabled?: boolean + path?: string +} + +export interface GenericAuthArgs { + authentication?: { + enabled?: boolean + strategy?: string | string[] | Strategy + } + authorization?: { + enabled?: boolean + requireUserInRoles?: string | string[] + enforcer?: Enforcer + } +} + +export interface EndpointArgs extends GenericAuthArgs { + resource?: string + operation?: string + handlers?: RequestHandler[] +} diff --git a/packages/headless-web3-provider/tsconfig.json b/packages/headless-web3-provider/tsconfig.json new file mode 100644 index 000000000..730108d34 --- /dev/null +++ b/packages/headless-web3-provider/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig-base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "declarationDir": "dist", + "strictPropertyInitialization": false + } +} diff --git a/packages/tsconfig.json b/packages/tsconfig.json index 5e261c6d7..1e04d7185 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -27,6 +27,7 @@ { "path": "wellknown-did-issuer" }, { "path": "wellknown-did-verifier" }, { "path": "kv-store" }, - { "path": "issuance-branding" } + { "path": "issuance-branding" }, + { "path": "headless-web3-provider" } ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59852758e..168c4ad52 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -332,6 +332,91 @@ importers: specifier: ^1.0.12 version: 1.0.12 + packages/headless-web3-provider: + dependencies: + '@ethersproject/abstract-provider': + specifier: ^5.7.0 + version: 5.7.0 + '@ethersproject/abstract-signer': + specifier: ^5.7.0 + version: 5.7.0 + '@ethersproject/bytes': + specifier: ^5.7.0 + version: 5.7.0 + '@ethersproject/random': + specifier: ^5.7.0 + version: 5.7.0 + '@ethersproject/signing-key': + specifier: ^5.7.0 + version: 5.7.0 + '@ethersproject/strings': + specifier: ^5.7.0 + version: 5.7.0 + '@ethersproject/transactions': + specifier: ^5.7.0 + version: 5.7.0 + '@ethersproject/wallet': + specifier: ^5.7.0 + version: 5.7.0 + '@metamask/eth-sig-util': + specifier: ^6.0.0 + version: 6.0.0 + '@veramo/core': + specifier: 4.2.0 + version: 4.2.0(patch_hash=c5oempznsz4br5w3tcuk2i2mau) + '@veramo/key-manager': + specifier: 4.2.0 + version: 4.2.0 + casbin: + specifier: ^5.26.1 + version: 5.26.1 + cors: + specifier: ^2.8.5 + version: 2.8.5 + dotenv-flow: + specifier: ^3.2.0 + version: 3.2.0 + express: + specifier: ^4.18.2 + version: 4.18.2 + express-session: + specifier: ^1.17.3 + version: 1.17.3 + passport: + specifier: ^0.6.0 + version: 0.6.0 + rxjs: + specifier: ^7.8.1 + version: 7.8.1 + uint8arrays: + specifier: 3.1.1 + version: 3.1.1 + devDependencies: + '@types/body-parser': + specifier: ^1.19.2 + version: 1.19.2 + '@types/cors': + specifier: ^2.8.13 + version: 2.8.13 + '@types/dotenv-flow': + specifier: ^3.2.0 + version: 3.2.0 + '@types/express': + specifier: ^4.17.17 + version: 4.17.17 + '@types/express-serve-static-core': + specifier: ^4.17.35 + version: 4.17.35 + '@types/express-session': + specifier: ^1.17.7 + version: 1.17.7 + '@types/passport': + specifier: ^1.0.12 + version: 1.0.12 + typescript: + specifier: 4.9.5 + version: 4.9.5 + packages/issuance-branding: dependencies: '@sphereon/ssi-sdk.core': @@ -3450,6 +3535,21 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@ethereumjs/rlp@4.0.1: + resolution: {integrity: sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==} + engines: {node: '>=14'} + hasBin: true + dev: false + + /@ethereumjs/util@8.1.0: + resolution: {integrity: sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==} + engines: {node: '>=14'} + dependencies: + '@ethereumjs/rlp': 4.0.1 + ethereum-cryptography: 2.1.2 + micro-ftch: 0.3.1 + dev: false + /@ethersproject/abi@5.7.0: resolution: {integrity: sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA==} dependencies: @@ -4872,6 +4972,18 @@ packages: - supports-color optional: true + /@metamask/eth-sig-util@6.0.0: + resolution: {integrity: sha512-M0ezVz8lirXG1P6rHPzx+9i4zfhebCgVHE8XQT8VWxy/eUWllHQGcBcE8QmOusC7su55M4CMr9AyMIu0lx452g==} + engines: {node: '>=14.0.0'} + dependencies: + '@ethereumjs/util': 8.1.0 + bn.js: 4.12.0 + ethereum-cryptography: 2.1.2 + ethjs-util: 0.1.6 + tweetnacl: 1.0.3 + tweetnacl-util: 0.15.1 + dev: false + /@microsoft/api-extractor-model@7.25.3: resolution: {integrity: sha512-WWxBUq77p2iZ+5VF7Nmrm3y/UtqCh5bYV8ii3khwq3w99+fXWpvfsAhgSLsC7k8XDQc6De4ssMxH6He/qe1pzg==} dependencies: @@ -4915,10 +5027,20 @@ packages: resolution: {integrity: sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw==} dev: true + /@noble/curves@1.1.0: + resolution: {integrity: sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==} + dependencies: + '@noble/hashes': 1.2.0 + dev: false + /@noble/ed25519@1.7.1: resolution: {integrity: sha512-Rk4SkJFaXZiznFyC/t77Q0NKS4FL7TLJJsVG2V2oiEq3kJVeTdxysEe/yRWSpnWMe808XRDJ+VFh5pt/FN5plw==} dev: true + /@noble/hashes@1.2.0: + resolution: {integrity: sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==} + dev: false + /@noble/secp256k1@1.7.0: resolution: {integrity: sha512-kbacwGSsH/CTout0ZnZWxnW1B+jH/7r/WAAKLBtrRJ/+CUH7lgmQzl3GTrQua3SGKWNSDsS6lmjnDpIJ5Dxyaw==} dev: true @@ -5767,6 +5889,21 @@ packages: resolution: {integrity: sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==} dev: false + /@scure/bip32@1.3.1: + resolution: {integrity: sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==} + dependencies: + '@noble/curves': 1.1.0 + '@noble/hashes': 1.2.0 + '@scure/base': 1.1.1 + dev: false + + /@scure/bip39@1.2.1: + resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==} + dependencies: + '@noble/hashes': 1.2.0 + '@scure/base': 1.1.1 + dev: false + /@segment/loosely-validate-event@2.0.0: resolution: {integrity: sha512-ZMCSfztDBqwotkl848ODgVcAmN4OItEWDCkshcKz0/W6gGSQayuuCtWV/MlodFivAZD793d6UgANd6wCXUfrIw==} dependencies: @@ -10885,6 +11022,15 @@ packages: meow: 5.0.0 dev: true + /ethereum-cryptography@2.1.2: + resolution: {integrity: sha512-Z5Ba0T0ImZ8fqXrJbpHcbpAvIswRte2wGNR/KePnu8GbbvgJ47lMxT/ZZPG6i9Jaht4azPDop4HaM00J0J59ug==} + dependencies: + '@noble/curves': 1.1.0 + '@noble/hashes': 1.2.0 + '@scure/bip32': 1.3.1 + '@scure/bip39': 1.2.1 + dev: false + /ethereum-public-key-to-address@0.0.2: resolution: {integrity: sha512-KRd0yrlbgESK3A62L4sHiJRk+b/UPX92Ehd0cCXWa5L7bQaq7z5q4BSRhuUuSZj++LQHQfJQQnJkskuHjDnbCQ==} hasBin: true @@ -10895,6 +11041,14 @@ packages: secp256k1: 3.8.0 dev: true + /ethjs-util@0.1.6: + resolution: {integrity: sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==} + engines: {node: '>=6.5.0', npm: '>=3'} + dependencies: + is-hex-prefixed: 1.0.0 + strip-hex-prefix: 1.0.0 + dev: false + /ethr-did-resolver@8.1.2: resolution: {integrity: sha512-dnbE3GItE1YHp/eavR11KbGDi8Il01H9GeH+wKgoSgE95pKBZufHyHYce/EK2k8VOmj6MJf8u/TIpPvxjCbK+A==} dependencies: @@ -12616,6 +12770,11 @@ packages: dependencies: is-extglob: 2.1.1 + /is-hex-prefixed@1.0.0: + resolution: {integrity: sha512-WvtOiug1VFrE9v1Cydwm+FnXd3+w9GaeVUss5W4v/SLy3UW00vP+6iNF2SdnfiBoLy4bTqVdkftNGTUeOFVsbA==} + engines: {node: '>=6.5.0', npm: '>=3'} + dev: false + /is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} @@ -15171,6 +15330,10 @@ packages: - supports-color - utf-8-validate + /micro-ftch@0.3.1: + resolution: {integrity: sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==} + dev: false + /micromatch@4.0.5: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} engines: {node: '>=8.6'} @@ -18495,6 +18658,13 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + /strip-hex-prefix@1.0.0: + resolution: {integrity: sha512-q8d4ue7JGEiVcypji1bALTos+0pWtyGlivAWyPuTkHzuTCJqrK9sWxYQZUq6Nq3cuyv3bm734IhHvHtGGURU6A==} + engines: {node: '>=6.5.0', npm: '>=3'} + dependencies: + is-hex-prefixed: 1.0.0 + dev: false + /strip-indent@2.0.0: resolution: {integrity: sha512-RsSNPLpq6YUL7QYy44RnPVTn/lcVZtb48Uof3X5JLbF4zD/Gs7ZFDv2HWol+leoQN2mT86LAzSshGfkTlSOpsA==} engines: {node: '>=4'} @@ -19087,9 +19257,12 @@ packages: - supports-color dev: true + /tweetnacl-util@0.15.1: + resolution: {integrity: sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==} + dev: false + /tweetnacl@1.0.3: resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} - dev: true /type-check@0.3.2: resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==}