diff --git a/.eslintrc.cjs b/.eslintrc.cjs index d5029b3..43990d5 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -14,6 +14,7 @@ module.exports = { }, rules: { 'max-len': ['error', { code: 150, ignoreStrings: true }], + 'brace-style': ['error', '1tbs', { 'allowSingleLine': false }], curly: ['error', 'all'], 'no-console': 'off', '@typescript-eslint/explicit-function-return-type': ['error'], diff --git a/Dockerfile b/Dockerfile index 1b24833..4bc86c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,6 @@ -FROM node:18-buster +# NOTE: `bookworm` is 2024-06-29 v12.6 release of Debian OS +# `slim` is a leaner image, optimized for production. +FROM node:20-bookworm-slim ARG DS_PORT ENV DS_PORT=${DS_PORT:-3000} diff --git a/README.md b/README.md index cf5eca0..2140a0c 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,14 @@ Exposes a multi-tenanted DWN (aka Decentralized Web Node) through a JSON-RPC API over `http:` and `ws:` - [Supported DBs](#supported-dbs) +- [Running online environment](#running-online-environment) - [Installation](#installation) - [Package usage](#package-usage) -- [Running The Server](#running-the-server) +- [Running the server](#running-the-server) + - [Running via docker](#running-via-docker) + - [Running a specific version](#running-a-specific-version) + - [Running Locally for Development](#running-locally-for-development) + - [Building a docker image locally](#building-a-docker-image-locally) - [JSON-RPC API](#json-rpc-api) - [Available Methods](#available-methods) - [`dwn.processMessage`](#dwnprocessmessage) @@ -15,9 +20,17 @@ Exposes a multi-tenanted DWN (aka Decentralized Web Node) through a JSON-RPC API - [Example Error Response](#example-error-response) - [Transporting large amounts of data](#transporting-large-amounts-of-data) - [Receiving large amounts of data](#receiving-large-amounts-of-data) -- [npm scripts](#npm-scripts) +- [Hosting your own DWN-server](#hosting-your-own-dwn-server) + - [Running on render.com](#running-on-rendercom) + - [Running with ngrok](#running-with-ngrok) + - [Running with cloudflared](#running-with-cloudflared) + - [Running on GCP](#running-on-gcp) +- [`npm` scripts](#npm-scripts) - [Configuration](#configuration) - [Storage Options](#storage-options) + - [Plugins](#plugins) +- [Registration Requirements](#registration-requirements) +- [Server info](#server-info) ## Supported DBs @@ -276,24 +289,28 @@ cloudflared tunnel --url http://localhost:3000 Configuration can be set using environment variables -| Env Var | Description | Default | -| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------- | -| `DS_PORT` | Port that the server listens on | `3000` | -| `DS_MAX_RECORD_DATA_SIZE` | Maximum size for `RecordsWrite` data. use `b`, `kb`, `mb`, `gb` for value | `1gb` | -| `DS_WEBSOCKET_SERVER` | Whether to enable listening over `ws:`. values: `on`,`off` | `on` | -| `DWN_REGISTRATION_STORE_URL` | URL to use for storage of registered DIDs. Leave unset to if DWN does not require registration (ie. open for all) | unset | -| `DWN_REGISTRATION_PROOF_OF_WORK_SEED` | Seed to generate the challenge nonce from, this allows all DWN instances in a cluster to generate the same challenge. | unset | -| `DWN_REGISTRATION_PROOF_OF_WORK_ENABLED` | Require new users to complete a proof-of-work challenge | `false` | -| `DWN_REGISTRATION_PROOF_OF_WORK_INITIAL_MAX_HASH` | Initial maximum allowed hash in 64 char HEX string. The more leading zeros (smaller number) the higher the difficulty. | `false` | -| `DWN_TERMS_OF_SERVICE_FILE_PATH` | Required terms of service agreement if set. Value is path to the terms of service file. | unset | -| `DWN_STORAGE` | URL to use for storage by default. See [Storage Options](#storage-options) for details | `level://data` | -| `DWN_STORAGE_MESSAGES` | URL to use for storage of messages. | value of `DWN_STORAGE` | -| `DWN_STORAGE_DATA` | URL to use for data storage | value of `DWN_STORAGE` | -| `DWN_STORAGE_EVENTS` | URL to use for event storage | value of `DWN_STORAGE` | +| Env Var | Description | Default | +| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ---------------------- | +| `DS_PORT` | Port that the server listens on | `3000` | +| `DS_MAX_RECORD_DATA_SIZE` | Maximum size for `RecordsWrite` data. use `b`, `kb`, `mb`, `gb` for value | `1gb` | +| `DS_WEBSOCKET_SERVER` | Whether to enable listening over `ws:`. values: `on`,`off` | `on` | +| `DWN_BASE_URL` | Base external URL of this DWN. Used to construct URL paths such as the `Request URI` for the Web5 Connect flow. | `http://localhost` | +| `DWN_EVENT_STREAM_PLUGIN_PATH` | Path to DWN Event Stream plugin to use. Default single-node implementation will be used if left empty. | unset | +| `DWN_REGISTRATION_STORE_URL` | URL to use for storage of registered DIDs. Leave unset to if DWN does not require registration (ie. open for all) | unset | +| `DWN_REGISTRATION_PROOF_OF_WORK_SEED` | Optional seed to generate the challenge nonce from, this allows all DWN instances in a cluster to generate the same challenge. | unset | +| `DWN_REGISTRATION_PROOF_OF_WORK_ENABLED` | Require new users to complete a proof-of-work challenge | `false` | +| `DWN_REGISTRATION_PROOF_OF_WORK_INITIAL_MAX_HASH` | Initial maximum allowed hash in 64 char HEX string. The more leading zeros (smaller number) the higher the difficulty. | `000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF` | +| `DWN_STORAGE` | URL to use for storage by default. See [Storage Options](#storage-options) for details | `level://data` | +| `DWN_STORAGE_MESSAGES` | Connection URL or file path to custom plugin to use for the message store. | value of `DWN_STORAGE` | +| `DWN_STORAGE_DATA` | Connection URL or file path to custom plugin to use for the data store. | value of `DWN_STORAGE` | +| `DWN_STORAGE_RESUMABLE_TASKS` | Connection URL or file path to custom plugin to use for the resumable task store. | value of `DWN_STORAGE` | +| `DWN_STORAGE_EVENTS` | Connection URL or file path to custom plugin to use for the event store. | value of `DWN_STORAGE` | +| `DWN_TERMS_OF_SERVICE_FILE_PATH` | Required terms of service agreement if set. Value is path to the terms of service file. | unset | +| `DWN_TTL_CACHE_URL` | URL of the TTL cache used by the DWN. Currently only supports SQL databases. | `sqlite://` | ### Storage Options -Several storage formats are supported, and may be configured with the `DWN_STORAGE_*` environment variables: +Several built storage options are supported, and may be configured with the `DWN_STORAGE_*` environment variables: | Database | Example | Notes | | ---------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -302,6 +319,25 @@ Several storage formats are supported, and may be configured with the `DWN_STORA | MySQL | `mysql://user:pass@host/db?debug=true&timezone=-0700` | [all URL options documented here](https://github.com/mysqljs/mysql#connection-options) | | PostgreSQL | `postgres:///dwn` | any options other than the URL scheme (`postgres://`) may also be specified via [standard environment variables](https://node-postgres.com/features/connecting#environment-variables) | +### Plugins + +In some scenarios, you may want to provide a custom implementation of a pluggable module for the DWN Server. The following interfaces defined in `dwn-sdk-js` package are supported: + +- `DataStore` +- `MessageStore` +- `ResumableDataStore` +- `EventLog` +- `EventStream` + +To load your custom plugin, specify the absolute path to the `.js` file of your custom implementation using the corresponding environment variable. For instance, use `DWN_STORAGE_DATA` for a custom DWN Data Store. + +Refer to the `tests/plugins/*.ts` files for examples of plugin implementations. In summary, you need to: + +- Implement the corresponding interface from the `dwn-sdk-js` package. For example, implement the `DataStore` interface for a DWN Data Store. +- Ensure that the built `.js` file that will be referenced by the DWN Server config environment variable contains a class that: + 1. Is a default export. This is how DWN Server locates the correct class for instantiation. + 1. Has a public constructor that does not take any arguments. This is how DWN Server instantiates the plugin. + ## Registration Requirements There are multiple optional registration gates, each of which can be enabled (all are disabled by default). Tenants (DIDs) must comply with whatever diff --git a/package-lock.json b/package-lock.json index e290752..e21bdb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@web5/dwn-server", - "version": "0.2.2", + "version": "0.4.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@web5/dwn-server", - "version": "0.2.2", + "version": "0.4.4", "dependencies": { - "@tbd54566975/dwn-sdk-js": "0.3.2", - "@tbd54566975/dwn-sql-store": "0.4.2", + "@tbd54566975/dwn-sdk-js": "0.4.4", + "@tbd54566975/dwn-sql-store": "0.6.4", "better-sqlite3": "^8.5.0", "body-parser": "^1.20.2", "bytes": "3.1.2", @@ -27,7 +27,7 @@ "readable-stream": "4.4.2", "response-time": "2.3.2", "uuid": "9.0.0", - "ws": "8.12.0" + "ws": "8.18.0" }, "bin": { "dwn-server": "dist/esm/src/main.js" @@ -42,18 +42,18 @@ "@types/readable-stream": "4.0.6", "@types/sinon": "17.0.3", "@types/supertest": "2.0.12", - "@types/ws": "8.5.4", - "@typescript-eslint/eslint-plugin": "5.59.0", - "@typescript-eslint/parser": "5.59.0", - "@web5/dids": "1.0.1", + "@types/ws": "8.5.10", + "@typescript-eslint/eslint-plugin": "^7.8.0", + "@typescript-eslint/parser": "^7.8.0", + "@web5/dids": "1.1.0", "c8": "8.0.1", "chai": "4.3.6", "chai-as-promised": "7.1.1", "crypto-browserify": "^3.12.0", "esbuild": "0.16.17", - "eslint": "8.33.0", - "eslint-plugin-import": "2.26.0", - "eslint-plugin-mocha": "10.1.0", + "eslint": "8.56.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-mocha": "^10.4.3", "eslint-plugin-todo-plz": "^1.3.0", "http-proxy": "^1.18.1", "husky": "^8.0.0", @@ -63,7 +63,7 @@ "karma-mocha": "^2.0.1", "lint-staged": "^14.0.1", "mocha": "^10.2.0", - "puppeteer": "^21.4.0", + "puppeteer": "^22.11.2", "sinon": "17.0.1", "stream-browserify": "^3.0.0", "supertest": "6.3.3", @@ -85,36 +85,37 @@ "integrity": "sha512-HazVq9zwTVwGmqdwYzu7WyQ6FQVZ7SwET0KKQuKm55jD0IfUpZgN0OPIiZG3zV1iSrVYcN0bdwLRXI/VNCYsUA==" }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" @@ -136,15 +137,14 @@ } }, "node_modules/@decentralized-identity/ion-sdk": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@decentralized-identity/ion-sdk/-/ion-sdk-1.0.1.tgz", - "integrity": "sha512-+P+DXcRSFjsEsI5KIqUmVjpzgUT28B2lWpTO+IxiBcfibAN/1Sg20NebGTO/+serz2CnSZf95N2a1OZ6eXypGQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@decentralized-identity/ion-sdk/-/ion-sdk-1.0.4.tgz", + "integrity": "sha512-pOWrlTH5ChxUKRHOgfG2ZeTioWEFJXADyErCQOJ0BqYNDKfP+CM09Vss+9ei6PNOABQlcDn0mEDFZtpO+DXl8A==", "dependencies": { "@noble/ed25519": "^2.0.0", "@noble/secp256k1": "^2.0.0", "canonicalize": "^2.0.0", - "multiformats": "^12.0.1", - "multihashes": "^4.0.3", + "multiformats": "^12.1.3", "uri-js": "^4.4.1" } }, @@ -210,14 +210,14 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", - "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.4.0", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -248,37 +248,19 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "node_modules/@eslint/js": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", "dev": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { @@ -416,11 +398,6 @@ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==" }, - "node_modules/@multiformats/base-x": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@multiformats/base-x/-/base-x-4.0.1.tgz", - "integrity": "sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw==" - }, "node_modules/@multiformats/murmur3": { "version": "2.1.7", "resolved": "https://registry.npmjs.org/@multiformats/murmur3/-/murmur3-2.1.7.tgz", @@ -531,16 +508,17 @@ } }, "node_modules/@puppeteer/browsers": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.8.0.tgz", - "integrity": "sha512-TkRHIV6k2D8OlUe8RtG+5jgOF/H98Myx0M6AOafC8DdNVOFiBSFa5cpRDtpm8LXOa9sVwe0+e6Q3FC56X/DZfg==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.2.3.tgz", + "integrity": "sha512-bJ0UBsk0ESOs6RFcLXOt99a3yTDcOKlzfjad+rhFwdaG1Lu/Wzq58GHYCDTlZ9z6mldf4g+NTb+TXEfe0PpnsQ==", "dev": true, "dependencies": { "debug": "4.3.4", "extract-zip": "2.0.1", "progress": "2.0.3", - "proxy-agent": "6.3.1", - "tar-fs": "3.0.4", + "proxy-agent": "6.4.0", + "semver": "7.6.0", + "tar-fs": "3.0.5", "unbzip2-stream": "1.4.3", "yargs": "17.7.2" }, @@ -548,24 +526,27 @@ "browsers": "lib/cjs/main-cli.js" }, "engines": { - "node": ">=16.3.0" + "node": ">=18" } }, "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", - "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.5.tgz", + "integrity": "sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==", "dev": true, "dependencies": { - "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" } }, "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz", - "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", "dev": true, "dependencies": { "b4a": "^1.6.4", @@ -624,15 +605,17 @@ "dev": true }, "node_modules/@tbd54566975/dwn-sdk-js": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sdk-js/-/dwn-sdk-js-0.3.2.tgz", - "integrity": "sha512-MMBau0Snkfnw4pCyBAzOneniUPVOBD1+m/Wj5rVgkDJNPIRkbFMcJ7auEmc4Pm0w+7pgm/ToPXDaSix+2Qob1w==", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sdk-js/-/dwn-sdk-js-0.4.4.tgz", + "integrity": "sha512-i4jBrKOn6ypaFQJLNG8Kc9AvR1nmZrq3A+9M4O9xiDbH60+QMPf2HsNuVOTu6Ve9V7IU8NorI/GoSryD0TknpQ==", "dependencies": { "@ipld/dag-cbor": "9.0.3", "@js-temporal/polyfill": "0.4.4", + "@noble/ciphers": "0.5.3", + "@noble/curves": "1.4.2", "@noble/ed25519": "2.0.0", "@noble/secp256k1": "2.0.0", - "@web5/dids": "1.0.1", + "@web5/dids": "1.1.0", "abstract-level": "1.0.3", "ajv": "8.12.0", "blockstore-core": "4.2.0", @@ -649,6 +632,7 @@ "multiformats": "11.0.2", "randombytes": "2.1.0", "readable-stream": "4.5.2", + "uint8arrays": "5.1.0", "ulidx": "2.1.0", "uuid": "8.3.2", "varint": "6.0.0" @@ -657,6 +641,36 @@ "node": ">= 18" } }, + "node_modules/@tbd54566975/dwn-sdk-js/node_modules/@noble/ciphers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz", + "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@tbd54566975/dwn-sdk-js/node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@tbd54566975/dwn-sdk-js/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@tbd54566975/dwn-sdk-js/node_modules/readable-stream": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", @@ -672,6 +686,19 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@tbd54566975/dwn-sdk-js/node_modules/uint8arrays": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-5.1.0.tgz", + "integrity": "sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==", + "dependencies": { + "multiformats": "^13.0.0" + } + }, + "node_modules/@tbd54566975/dwn-sdk-js/node_modules/uint8arrays/node_modules/multiformats": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.1.3.tgz", + "integrity": "sha512-CZPi9lFZCM/+7oRolWYsvalsyWQGFo+GpdaTmjxXXomC+nP/W1Rnxb9sUgjvmNmRZ5bOPqRAl4nuK+Ydw/4tGw==" + }, "node_modules/@tbd54566975/dwn-sdk-js/node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -681,12 +708,12 @@ } }, "node_modules/@tbd54566975/dwn-sql-store": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sql-store/-/dwn-sql-store-0.4.2.tgz", - "integrity": "sha512-KdLk5PUaAuONr/xLOQN6LSVwVV/JQ3kn7ozlj74f9MRoBjL2iIiowCocCe6sAhzKCYmrT+xSdYP+M3e9moJ6Yw==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sql-store/-/dwn-sql-store-0.6.4.tgz", + "integrity": "sha512-ivQt83MMMYsXC4qSD61lKINuJ5KnGIz4p0H61oh/zqd4ivg7ofrTnnVZBdKopvchREZ1ozxI5hcRRuBLkuLbkw==", "dependencies": { - "@ipld/dag-cbor": "^9.0.5", - "@tbd54566975/dwn-sdk-js": "0.3.2", + "@ipld/dag-cbor": "9.0.5", + "@tbd54566975/dwn-sdk-js": "0.4.4", "kysely": "0.26.3", "multiformats": "12.0.1", "readable-stream": "4.4.2" @@ -696,9 +723,9 @@ } }, "node_modules/@tbd54566975/dwn-sql-store/node_modules/@ipld/dag-cbor": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@ipld/dag-cbor/-/dag-cbor-9.0.6.tgz", - "integrity": "sha512-3kNab5xMppgWw6DVYx2BzmFq8t7I56AGWfp5kaU1fIPkwHVpBRglJJTYsGtbVluCi/s/q97HZM3bC+aDW4sxbQ==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@ipld/dag-cbor/-/dag-cbor-9.0.5.tgz", + "integrity": "sha512-TyqgtxEojc98rvxg4NGM+73JzQeM4+tK2VQes/in2mdyhO+1wbGuBijh1tvi9BErQ/dEblxs9v4vEQSX8mFCIw==", "dependencies": { "cborg": "^4.0.0", "multiformats": "^12.0.1" @@ -709,9 +736,9 @@ } }, "node_modules/@tbd54566975/dwn-sql-store/node_modules/cborg": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/cborg/-/cborg-4.0.5.tgz", - "integrity": "sha512-q8TAjprr8pn9Fp53rOIGp/UFDdFY6os2Nq62YogPSIzczJD9M6g2b6igxMkpCiZZKJ0kn/KzDLDvG+EqBIEeCg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/cborg/-/cborg-4.2.1.tgz", + "integrity": "sha512-LSdnRagOTx1QZ3/ECLEOMc5fYHaDBjjQkBeBGtZ9KkGa78Opb5UzUxJeuxhmYTZm1DUzdBjj9JT3fcQNRL9ZBg==", "bin": { "cborg": "lib/bin.js" } @@ -881,9 +908,9 @@ } }, "node_modules/@types/semver": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz", - "integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, "node_modules/@types/send": { @@ -942,9 +969,9 @@ } }, "node_modules/@types/ws": { - "version": "8.5.4", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", - "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", "dev": true, "dependencies": { "@types/node": "*" @@ -961,32 +988,33 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.0.tgz", - "integrity": "sha512-p0QgrEyrxAWBecR56gyn3wkG15TJdI//eetInP3zYRewDh0XS+DhB3VUAd3QqvziFsfaQIoIuZMxZRB7vXYaYw==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz", + "integrity": "sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.0", - "@typescript-eslint/type-utils": "5.59.0", - "@typescript-eslint/utils": "5.59.0", + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.8.0", + "@typescript-eslint/type-utils": "7.8.0", + "@typescript-eslint/utils": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -995,25 +1023,26 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.0.tgz", - "integrity": "sha512-qK9TZ70eJtjojSUMrrEwA9ZDQ4N0e/AuoOIgXuNBorXYcBDk397D2r5MIe1B3cok/oCtdNC5j+lUUpVB+Dpb+w==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.8.0.tgz", + "integrity": "sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.59.0", - "@typescript-eslint/types": "5.59.0", - "@typescript-eslint/typescript-estree": "5.59.0", + "@typescript-eslint/scope-manager": "7.8.0", + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/typescript-estree": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", "debug": "^4.3.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -1022,16 +1051,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.0.tgz", - "integrity": "sha512-tsoldKaMh7izN6BvkK6zRMINj4Z2d6gGhO2UsI8zGZY3XhLq1DndP3Ycjhi1JwdwPRwtLMW4EFPgpuKhbCGOvQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz", + "integrity": "sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.0", - "@typescript-eslint/visitor-keys": "5.59.0" + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1039,25 +1068,25 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.0.tgz", - "integrity": "sha512-d/B6VSWnZwu70kcKQSCqjcXpVH+7ABKH8P1KNn4K7j5PXXuycZTPXF44Nui0TEm6rbWGi8kc78xRgOC4n7xFgA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz", + "integrity": "sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.59.0", - "@typescript-eslint/utils": "5.59.0", + "@typescript-eslint/typescript-estree": "7.8.0", + "@typescript-eslint/utils": "7.8.0", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -1066,12 +1095,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.0.tgz", - "integrity": "sha512-yR2h1NotF23xFFYKHZs17QJnB51J/s+ud4PYU4MqdZbzeNxpgUr05+dNeCN/bb6raslHvGdd6BFCkVhpPk/ZeA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz", + "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1079,21 +1108,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.0.tgz", - "integrity": "sha512-sUNnktjmI8DyGzPdZ8dRwW741zopGxltGs/SAPgGL/AAgDpiLsCFLcMNSpbfXfmnNeHmK9h3wGmCkGRGAoUZAg==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz", + "integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.0", - "@typescript-eslint/visitor-keys": "5.59.0", + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1105,49 +1135,78 @@ } } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/utils": { - "version": "5.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.0.tgz", - "integrity": "sha512-GGLFd+86drlHSvPgN/el6dRQNYYGOvRSDVydsUaQluwIW3HvbXuxyuD5JETvBt/9qGYe+lOrDk6gRrWOHb/FvA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz", + "integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==", "dev": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.0", - "@typescript-eslint/types": "5.59.0", - "@typescript-eslint/typescript-estree": "5.59.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.15", + "@types/semver": "^7.5.8", + "@typescript-eslint/scope-manager": "7.8.0", + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/typescript-estree": "7.8.0", + "semver": "^7.6.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.0.tgz", - "integrity": "sha512-qZ3iXxQhanchCeaExlKPV3gDQFxMUmU35xfd5eCXB6+kUw1TUAbIy2n7QIrwz9s98DQLzNWyHp61fY0da4ZcbA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz", + "integrity": "sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.0", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "7.8.0", + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/@web5/common": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@web5/common/-/common-1.0.0.tgz", @@ -1185,18 +1244,18 @@ } }, "node_modules/@web5/dids": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@web5/dids/-/dids-1.0.1.tgz", - "integrity": "sha512-bAc+zwTDPvtFtd8T25XD0oUmSOBmeTpYSZyBz9w/EqZPKtZOFSc5oFS5qLtrh4YDkkcBqTG5ENQlE9fXs56zIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@web5/dids/-/dids-1.1.0.tgz", + "integrity": "sha512-d9pKf/DW+ziUiV5g3McC71utyAhQyT1tYGPbQSYWt2ji6FHGNC6tffHMfLXXK/W+vbwV3eNTn06JqTXRaYhxBA==", "dependencies": { - "@decentralized-identity/ion-sdk": "1.0.1", + "@decentralized-identity/ion-sdk": "1.0.4", "@dnsquery/dns-packet": "6.1.1", "@web5/common": "1.0.0", "@web5/crypto": "1.0.0", "abstract-level": "1.0.4", "bencode": "4.0.0", "buffer": "6.0.3", - "level": "8.0.0", + "level": "8.0.1", "ms": "2.1.3" }, "engines": { @@ -1220,6 +1279,23 @@ "node": ">=12" } }, + "node_modules/@web5/dids/node_modules/level": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/level/-/level-8.0.1.tgz", + "integrity": "sha512-oPBGkheysuw7DmzFQYyFe8NAia5jFLAgEnkgWnK3OXAuJr8qFT+xBQIwokAZPME2bhPFzS8hlYcL16m8UZrtwQ==", + "dependencies": { + "abstract-level": "^1.0.4", + "browser-level": "^1.0.1", + "classic-level": "^1.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/level" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -1261,9 +1337,9 @@ } }, "node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -1282,9 +1358,9 @@ } }, "node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, "dependencies": { "debug": "^4.3.4" @@ -1373,13 +1449,16 @@ "dev": true }, "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1418,6 +1497,26 @@ "node": ">=8" } }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.flat": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", @@ -1436,18 +1535,37 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", "dev": true, "dependencies": { - "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.2", "define-properties": "^1.2.0", "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", "is-shared-array-buffer": "^1.0.2" }, "engines": { @@ -1509,10 +1627,13 @@ "dev": true }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -1521,9 +1642,9 @@ } }, "node_modules/b4a": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", - "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==", + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", "dev": true }, "node_modules/balanced-match": { @@ -1532,6 +1653,52 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bare-events": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", + "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==", + "dev": true, + "optional": true + }, + "node_modules/bare-fs": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.1.tgz", + "integrity": "sha512-W/Hfxc/6VehXlsgFtbB5B4xFcsCl+pAh30cYhoFyXErf6oGrwjh8SwiPAdHgpmWonKuYpZgGywN0SXt7dgsADA==", + "dev": true, + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "bare-stream": "^2.0.0" + } + }, + "node_modules/bare-os": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.0.tgz", + "integrity": "sha512-v8DTT08AS/G0F9xrhyLtepoo9EJBJ85FRSMbu1pQUlAf6A8T0tEEQGMVObWeqpjhSPXsE0VGlluFBJu2fdoTNg==", + "dev": true, + "optional": true + }, + "node_modules/bare-path": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", + "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "dev": true, + "optional": true, + "dependencies": { + "bare-os": "^2.1.0" + } + }, + "node_modules/bare-stream": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.1.3.tgz", + "integrity": "sha512-tiDAH9H/kP+tvNO5sczyn9ZAA7utrSMobyDchsnyyXBuUe2FSQWbxhtuHB8jwpHYYevVo2UJpcmvvjrbHboUUQ==", + "dev": true, + "optional": true, + "dependencies": { + "streamx": "^2.18.0" + } + }, "node_modules/base64-arraybuffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", @@ -1569,9 +1736,9 @@ } }, "node_modules/basic-ftp": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.3.tgz", - "integrity": "sha512-QHX8HLlncOLpy54mh+k/sWIFd0ThmRqwe9ZjELybGZK+tZ8rUb9VO0saKJUROTbE+KhzDUT7xziGpGrW8Kmd+g==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", "dev": true, "engines": { "node": ">=10.0.0" @@ -1711,12 +1878,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1927,13 +2094,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2082,13 +2254,14 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "node_modules/chromium-bidi": { - "version": "0.4.33", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.33.tgz", - "integrity": "sha512-IxoFM5WGQOIAd95qrSXzJUv4eXIrh+RvU3rwwqIiwYuvfE7U/Llj4fejbsJnjJMUYCuGtVQsY2gv7oGl4aTNSQ==", + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.5.23.tgz", + "integrity": "sha512-1o/gLU9wDqbN5nL2MtfjykjOuighGXc3/hnWueO1haiEoFgX8h5vbvcA4tgdQfjw1mkZ1OEF4x/+HVeqEX6NoA==", "dev": true, "dependencies": { "mitt": "3.0.1", - "urlpattern-polyfill": "9.0.0" + "urlpattern-polyfill": "10.0.0", + "zod": "3.23.8" }, "peerDependencies": { "devtools-protocol": "*" @@ -2443,15 +2616,15 @@ } }, "node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "dependencies": { + "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" + "parse-json": "^5.2.0" }, "engines": { "node": ">=14" @@ -2588,6 +2761,57 @@ "node": ">= 12" } }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/date-format": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", @@ -2671,16 +2895,19 @@ "dev": true }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-properties": { @@ -2767,9 +2994,9 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1203626", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1203626.tgz", - "integrity": "sha512-nEzHZteIUZfGCZtTiS1fRpC8UZmsfD1SiyPvaUNvS13dvKf666OAm8YTi0+Ca3n1nLEyu49Cy4+dPWpaHFJk9g==", + "version": "0.0.1299070", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1299070.tgz", + "integrity": "sha512-+qtL3eX50qsJ7c+qVyagqi7AWMoQCBGNfoyJZMwm/NSXVqLYbuitrWEEIzxfUmTNy7//Xe8yhMmQ+elj3uAqSg==", "dev": true }, "node_modules/dezalgo": { @@ -2918,9 +3145,9 @@ } }, "node_modules/engine.io": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.3.tgz", - "integrity": "sha512-IML/R4eG/pUS5w7OfcDE0jKrljWS9nwnEfsxWCIJF5eO6AHo6+Hlv+lQbdlAYsiJPHzUthLm1RUjnBzWOs45cw==", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", + "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", "dev": true, "dependencies": { "@types/cookie": "^0.4.1", @@ -2932,7 +3159,7 @@ "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", - "ws": "~8.11.0" + "ws": "~8.17.1" }, "engines": { "node": ">=10.2.0" @@ -2957,16 +3184,16 @@ } }, "node_modules/engine.io/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -2983,6 +3210,15 @@ "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", "dev": true }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/err-code": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/err-code/-/err-code-3.0.1.tgz", @@ -2998,50 +3234,57 @@ } }, "node_modules/es-abstract": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", - "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.5", - "es-set-tostringtag": "^2.0.1", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.2", - "get-symbol-description": "^1.0.0", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", "globalthis": "^1.0.3", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", - "hasown": "^2.0.0", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", + "is-shared-array-buffer": "^1.0.3", "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", + "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", "object-inspect": "^1.13.1", "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" + "which-typed-array": "^1.1.15" }, "engines": { "node": ">= 0.4" @@ -3050,15 +3293,46 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-set-tostringtag": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", - "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -3171,69 +3445,49 @@ "source-map": "~0.6.1" } }, - "node_modules/escodegen/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/escodegen/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==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/eslint": { - "version": "8.33.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.33.0.tgz", - "integrity": "sha512-WjOpFQgKK8VrCnAtl8We0SUOy/oVZ5NHykyMiagV1M9r8IFpIJX7DduK6n1mpfhlG7T1NLWm2SuD8QB7KFySaA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, "dependencies": { - "@eslint/eslintrc": "^1.4.1", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.4.0", - "esquery": "^1.4.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "regexpp": "^3.2.0", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { @@ -3293,24 +3547,28 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", - "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", "dev": true, "dependencies": { - "array-includes": "^3.1.4", - "array.prototype.flat": "^1.2.5", - "debug": "^2.6.9", + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.3", - "has": "^1.0.3", - "is-core-module": "^2.8.1", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.values": "^1.1.5", - "resolve": "^1.22.0", - "tsconfig-paths": "^3.14.1" + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" @@ -3320,12 +3578,12 @@ } }, "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "dependencies": { - "ms": "2.0.0" + "ms": "^2.1.1" } }, "node_modules/eslint-plugin-import/node_modules/doctrine": { @@ -3340,20 +3598,24 @@ "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-import/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } }, "node_modules/eslint-plugin-mocha": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.1.0.tgz", - "integrity": "sha512-xLqqWUF17llsogVOC+8C6/jvQ+4IoOREbN7ZCHuOHuD6cT5cDD4h7f2LgsZuzMAiwswWE21tO7ExaknHVDrSkw==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.4.3.tgz", + "integrity": "sha512-emc4TVjq5Ht0/upR+psftuz6IBG5q279p+1dSRDeHf+NS9aaerBi3lXKo1SEzwC29hFIW21gO89CEWSvRsi8IQ==", "dev": true, "dependencies": { "eslint-utils": "^3.0.0", - "rambda": "^7.1.0" + "globals": "^13.24.0", + "rambda": "^7.4.0" }, "engines": { "node": ">=14.0.0" @@ -3374,19 +3636,6 @@ "eslint": ">=7.3.0" } }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/eslint-utils": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", @@ -3519,30 +3768,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/eslint/node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3570,18 +3795,6 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -3624,15 +3837,6 @@ "node": ">=0.10" } }, - "node_modules/esquery/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -3645,7 +3849,7 @@ "node": ">=4.0" } }, - "node_modules/esrecurse/node_modules/estraverse": { + "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", @@ -3654,15 +3858,6 @@ "node": ">=4.0" } }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3972,9 +4167,9 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -4257,15 +4452,19 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4283,13 +4482,14 @@ } }, "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" }, "engines": { "node": ">= 0.4" @@ -4299,29 +4499,64 @@ } }, "node_modules/get-uri": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.2.tgz", - "integrity": "sha512-5KLucCJobh8vBY1K07EFV4+cPZH3mrV9YeAruUseCQKHB58SGjjT2l9/eA9LD082IiuMjSlFJEcdJ27TXvbZNw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", "dev": true, "dependencies": { "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.0", + "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4", - "fs-extra": "^8.1.0" + "fs-extra": "^11.2.0" }, "engines": { "node": ">= 14" } }, "node_modules/get-uri/node_modules/data-uri-to-buffer": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.1.tgz", - "integrity": "sha512-MZd3VlchQkp8rdend6vrx7MmVDJzSNTBvghvKjirLkD+WTChA3KUf0jkE68Q4UyctNqI11zZO9/x2Yx+ub5Cvg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", "dev": true, "engines": { "node": ">= 14" } }, + "node_modules/get-uri/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/get-uri/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/get-uri/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -4359,6 +4594,33 @@ "node": ">=10.13.0" } }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globalthis": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", @@ -4411,10 +4673,10 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, - "node_modules/grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, "node_modules/hamt-sharding": { @@ -4430,15 +4692,6 @@ "npm": ">=7.0.0" } }, - "node_modules/has": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", - "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -4458,20 +4711,20 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "engines": { "node": ">= 0.4" }, @@ -4491,12 +4744,12 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -4564,9 +4817,9 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "^1.1.2" }, @@ -4639,9 +4892,9 @@ } }, "node_modules/http-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", - "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "dependencies": { "agent-base": "^7.1.0", @@ -4652,9 +4905,9 @@ } }, "node_modules/https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", "dev": true, "dependencies": { "agent-base": "^7.0.2", @@ -4719,9 +4972,9 @@ ] }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, "engines": { "node": ">= 4" @@ -4795,12 +5048,12 @@ } }, "node_modules/internal-slot": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", - "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2", + "es-errors": "^1.3.0", "hasown": "^2.0.0", "side-channel": "^1.0.4" }, @@ -4808,11 +5061,18 @@ "node": ">= 0.4" } }, - "node_modules/ip": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", - "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==", - "dev": true + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } }, "node_modules/ipaddr.js": { "version": "1.9.1", @@ -4892,14 +5152,16 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4997,6 +5259,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-date-object": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", @@ -5046,9 +5323,9 @@ } }, "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "engines": { "node": ">= 0.4" @@ -5121,12 +5398,15 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5175,12 +5455,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", "dev": true, "dependencies": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -5394,16 +5674,6 @@ "npm": ">=7.0.0" } }, - "node_modules/js-sdsl": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.2.tgz", - "integrity": "sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5427,6 +5697,12 @@ "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.0.tgz", "integrity": "sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g==" }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -5549,15 +5825,6 @@ "esbuild": ">=0.8.45" } }, - "node_modules/karma-esbuild/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==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/karma-mocha": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/karma-mocha/-/karma-mocha-2.0.1.tgz", @@ -5626,15 +5893,6 @@ "node": ">=8" } }, - "node_modules/karma/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==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/karma/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -6530,19 +6788,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, - "node_modules/multibase": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/multibase/-/multibase-4.0.6.tgz", - "integrity": "sha512-x23pDe5+svdLz/k5JPGCVdfn7Q5mZVMBETiC+ORfO+sor9Sgs0smJzAjfTbM5tckeCqnaUuMYoz+k3RXMmJClQ==", - "deprecated": "This module has been superseded by the multiformats module", - "dependencies": { - "@multiformats/base-x": "^4.0.1" - }, - "engines": { - "node": ">=12.0.0", - "npm": ">=6.0.0" - } - }, "node_modules/multiformats": { "version": "11.0.2", "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-11.0.2.tgz", @@ -6552,38 +6797,6 @@ "npm": ">=7.0.0" } }, - "node_modules/multihashes": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/multihashes/-/multihashes-4.0.3.tgz", - "integrity": "sha512-0AhMH7Iu95XjDLxIeuCOOE4t9+vQZsACyKZ9Fxw2pcsRmlX4iCn1mby0hS0bb+nQOVpdQYWPpnyusw4da5RPhA==", - "dependencies": { - "multibase": "^4.0.1", - "uint8arrays": "^3.0.0", - "varint": "^5.0.2" - }, - "engines": { - "node": ">=12.0.0", - "npm": ">=6.0.0" - } - }, - "node_modules/multihashes/node_modules/multiformats": { - "version": "9.9.0", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", - "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" - }, - "node_modules/multihashes/node_modules/uint8arrays": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.1.tgz", - "integrity": "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==", - "dependencies": { - "multiformats": "^9.4.2" - } - }, - "node_modules/multihashes/node_modules/varint": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.2.tgz", - "integrity": "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow==" - }, "node_modules/murmurhash3js-revisited": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/murmurhash3js-revisited/-/murmurhash3js-revisited-3.0.0.tgz", @@ -6593,9 +6806,9 @@ } }, "node_modules/mysql2": { - "version": "3.9.7", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz", - "integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.10.1.tgz", + "integrity": "sha512-6zo1T3GILsXMCex3YEu7hCz2OXLUarxFsxvFcUHWMpkPtmZLeTTWgRdc1gWyNJiYt6AxITmIf9bZDRy/jAfWew==", "dependencies": { "denque": "^2.1.0", "generate-function": "^2.3.1", @@ -6676,12 +6889,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -6783,9 +6990,9 @@ } }, "node_modules/node-gyp-build": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", - "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", + "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", @@ -6854,13 +7061,13 @@ } }, "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, @@ -6871,6 +7078,38 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/object.values": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", @@ -7039,13 +7278,12 @@ } }, "node_modules/pac-resolver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.0.tgz", - "integrity": "sha512-Fd9lT9vJbHYRACT8OhCbZBbxr6KRSawSovFpy8nDGshaK99S/EBhVIHp9+crhxrsZOuvLpgL1n23iyPg6Rl2hg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", "dev": true, "dependencies": { "degenerator": "^5.0.0", - "ip": "^1.1.8", "netmask": "^2.0.2" }, "engines": { @@ -7277,6 +7515,12 @@ "split2": "^4.1.0" } }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -7301,6 +7545,15 @@ "node": ">=0.10" } }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -7429,15 +7682,15 @@ } }, "node_modules/proxy-agent": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz", - "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", "dev": true, "dependencies": { "agent-base": "^7.0.2", "debug": "^4.3.4", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.2", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.0.1", "proxy-from-env": "^1.1.0", @@ -7500,41 +7753,67 @@ } }, "node_modules/puppeteer": { - "version": "21.5.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-21.5.0.tgz", - "integrity": "sha512-prvy9rdauyIaaEgefQRcw9zhQnYQbl8O1Gj5VJazKJ7kwNx703+Paw/1bwA+b96jj/S+r55hrmF5SfiEG5PUcg==", + "version": "22.11.2", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.11.2.tgz", + "integrity": "sha512-8fjdQSgW0sq7471ftca24J7sXK+jXZ7OW7Gx+NEBFNyXrcTiBfukEI46gNq6hiMhbLEDT30NeylK/1ZoPdlKSA==", "dev": true, "hasInstallScript": true, "dependencies": { - "@puppeteer/browsers": "1.8.0", - "cosmiconfig": "8.3.6", - "puppeteer-core": "21.5.0" + "@puppeteer/browsers": "2.2.3", + "cosmiconfig": "9.0.0", + "devtools-protocol": "0.0.1299070", + "puppeteer-core": "22.11.2" + }, + "bin": { + "puppeteer": "lib/esm/puppeteer/node/cli.js" }, "engines": { - "node": ">=16.3.0" + "node": ">=18" } }, "node_modules/puppeteer-core": { - "version": "21.5.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-21.5.0.tgz", - "integrity": "sha512-qG0RJ6qKgFz09UUZxDB9IcyTJGypQXMuE8WmEoHk7kgjutmRiOVv5RgsyUkY67AxDdBWx21bn1PHHRJnO/6b4A==", + "version": "22.11.2", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.11.2.tgz", + "integrity": "sha512-vQo+YDuePyvj+92Z9cdtxi/HalKf+k/R4tE80nGtQqJRNqU81eHaHkbVfnLszdaLlvwFF5tipnnSCzqWlEddtw==", "dev": true, "dependencies": { - "@puppeteer/browsers": "1.8.0", - "chromium-bidi": "0.4.33", - "cross-fetch": "4.0.0", - "debug": "4.3.4", - "devtools-protocol": "0.0.1203626", - "ws": "8.14.2" + "@puppeteer/browsers": "2.2.3", + "chromium-bidi": "0.5.23", + "debug": "4.3.5", + "devtools-protocol": "0.0.1299070", + "ws": "8.17.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" }, "engines": { - "node": ">=16.3.0" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, + "node_modules/puppeteer-core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, "node_modules/puppeteer-core/node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "engines": { "node": ">=10.0.0" @@ -7744,14 +8023,15 @@ } }, "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -7760,18 +8040,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -7968,13 +8236,13 @@ } }, "node_modules/safe-array-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", - "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", "has-symbols": "^1.0.3", "isarray": "^2.0.5" }, @@ -7991,15 +8259,18 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", "is-regex": "^1.1.4" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8010,9 +8281,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -8110,28 +8381,31 @@ } }, "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "dependencies": { - "define-data-property": "^1.0.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -8361,25 +8635,26 @@ } }, "node_modules/socket.io-adapter": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", - "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", "dev": true, "dependencies": { - "ws": "~8.11.0" + "debug": "~4.3.4", + "ws": "~8.17.1" } }, "node_modules/socket.io-adapter/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -8404,26 +8679,26 @@ } }, "node_modules/socks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", "dev": true, "dependencies": { - "ip": "^2.0.0", + "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.13.0", + "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, "node_modules/socks-proxy-agent": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz", - "integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", + "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", "dev": true, "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.1", "debug": "^4.3.4", "socks": "^2.7.1" }, @@ -8431,11 +8706,14 @@ "node": ">= 14" } }, - "node_modules/socks/node_modules/ip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", - "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", - "dev": true + "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==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, "node_modules/sparse-array": { "version": "1.3.2", @@ -8450,6 +8728,12 @@ "node": ">= 10.x" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, "node_modules/sqlstring": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", @@ -8505,13 +8789,17 @@ } }, "node_modules/streamx": { - "version": "2.15.2", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.2.tgz", - "integrity": "sha512-b62pAV/aeMjUoRN2C/9F0n+G8AfcJjNC0zw/ZmOHeFsIe4m4GzjVW9m6VHXVjk536NbdU9JRwKMJRfkc+zUFTg==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", + "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", "dev": true, "dependencies": { - "fast-fifo": "^1.1.0", - "queue-tick": "^1.0.1" + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" } }, "node_modules/string_decoder": { @@ -8595,14 +8883,15 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", - "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -8612,28 +8901,31 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", - "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", - "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8836,6 +9128,15 @@ "node": ">=8" } }, + "node_modules/text-decoder": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.0.tgz", + "integrity": "sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -8885,10 +9186,22 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/tsconfig-paths": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", - "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, "dependencies": { "@types/json5": "^0.0.29", @@ -8902,27 +9215,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -8980,29 +9272,30 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -9012,16 +9305,17 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -9031,14 +9325,20 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9081,9 +9381,9 @@ } }, "node_modules/uint8-util": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/uint8-util/-/uint8-util-2.2.4.tgz", - "integrity": "sha512-uEI5lLozmKQPYEevfEhP9LY3Je5ZmrQhaWXrzTVqrLNQl36xsRh8NiAxYwB9J+2BAt99TRbmCkROQB2ZKhx4UA==", + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/uint8-util/-/uint8-util-2.2.5.tgz", + "integrity": "sha512-/QxVQD7CttWpVUKVPz9znO+3Dd4BdTSnFQ7pv/4drVhC9m4BaL2LFHTkJn6EsYoxT79VDq/2Gg8L0H22PrzyMw==", "dependencies": { "base64-arraybuffer": "^1.0.2" } @@ -9203,9 +9503,9 @@ } }, "node_modules/urlpattern-polyfill": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-9.0.0.tgz", - "integrity": "sha512-WHN8KDQblxd32odxeIgo83rdVDE2bvdkb86it7bMhYZwWKJz0+O0RK/eZiHYnM+zgt/U7hAHOlCQGfjjvSkw2g==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", "dev": true }, "node_modules/utf8-codec": { @@ -9324,16 +9624,16 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -9410,9 +9710,9 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.0.tgz", - "integrity": "sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "engines": { "node": ">=10.0.0" }, @@ -9552,6 +9852,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 4ce651d..e4fadf4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@web5/dwn-server", "type": "module", - "version": "0.2.2", + "version": "0.4.4", "files": [ "dist", "src" @@ -26,8 +26,8 @@ "url": "https://github.com/TBD54566975/dwn-server/issues" }, "dependencies": { - "@tbd54566975/dwn-sdk-js": "0.3.2", - "@tbd54566975/dwn-sql-store": "0.4.2", + "@tbd54566975/dwn-sdk-js": "0.4.4", + "@tbd54566975/dwn-sql-store": "0.6.4", "better-sqlite3": "^8.5.0", "body-parser": "^1.20.2", "bytes": "3.1.2", @@ -45,7 +45,7 @@ "readable-stream": "4.4.2", "response-time": "2.3.2", "uuid": "9.0.0", - "ws": "8.12.0" + "ws": "8.18.0" }, "devDependencies": { "@types/bytes": "3.1.1", @@ -57,18 +57,18 @@ "@types/readable-stream": "4.0.6", "@types/sinon": "17.0.3", "@types/supertest": "2.0.12", - "@types/ws": "8.5.4", - "@typescript-eslint/eslint-plugin": "5.59.0", - "@typescript-eslint/parser": "5.59.0", - "@web5/dids": "1.0.1", + "@types/ws": "8.5.10", + "@typescript-eslint/eslint-plugin": "^7.8.0", + "@typescript-eslint/parser": "^7.8.0", + "@web5/dids": "1.1.0", "c8": "8.0.1", "chai": "4.3.6", "chai-as-promised": "7.1.1", "crypto-browserify": "^3.12.0", "esbuild": "0.16.17", - "eslint": "8.33.0", - "eslint-plugin-import": "2.26.0", - "eslint-plugin-mocha": "10.1.0", + "eslint": "8.56.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-mocha": "^10.4.3", "eslint-plugin-todo-plz": "^1.3.0", "http-proxy": "^1.18.1", "husky": "^8.0.0", @@ -78,7 +78,7 @@ "karma-mocha": "^2.0.1", "lint-staged": "^14.0.1", "mocha": "^10.2.0", - "puppeteer": "^21.4.0", + "puppeteer": "^22.11.2", "sinon": "17.0.1", "stream-browserify": "^3.0.0", "supertest": "6.3.3", @@ -105,4 +105,4 @@ "eslint --fix" ] } -} +} \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index ee1f895..389dd3a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -10,6 +10,28 @@ export const config = { * 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', + + /** + * The base external URL of this DWN. + * This is used to construct URL paths such as the `Request URI` in the Web5 Connect flow. + */ + baseUrl: process.env.DWN_BASE_URL || 'http://localhost', + + /** + * Port that server listens on. + */ + port: parseInt(process.env.DS_PORT || '3000'), + + /** + * The URL of the TTL cache used by the DWN. + * NOTE: Used for session/state keeping, thus requires the cache to be commonly addressable by nodes in a cloud cluster environment. + * + * Currently only supports SQL databases, e.g. + * Postgres: 'postgres://root:dwn@localhost:5432/dwn' + * MySQL: 'mysql://root:dwn@localhost:3306/dwn' + */ + ttlCacheUrl: process.env.DWN_TTL_CACHE_URL || 'sqlite://', + /** * Used to populate the `version` and `sdkVersion` properties returned by the `/info` endpoint. * @@ -21,14 +43,20 @@ export const config = { 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 - port: parseInt(process.env.DS_PORT || '3000'), + // whether to enable 'ws:' webSocketSupport: { on: true, off: false }[process.env.DS_WEBSOCKET_SERVER] ?? true, + + /** + * Path to DWN Event Stream plugin to use. Default single-node implementation will be used if left empty. + */ + eventStreamPluginPath: process.env.DWN_EVENT_STREAM_PLUGIN_PATH, + // where to store persistent data messageStore: process.env.DWN_STORAGE_MESSAGES || process.env.DWN_STORAGE || 'level://data', dataStore: process.env.DWN_STORAGE_DATA || process.env.DWN_STORAGE || 'level://data', eventLog: process.env.DWN_STORAGE_EVENTS || process.env.DWN_STORAGE || 'level://data', + resumableTaskStore: process.env.DWN_STORAGE_RESUMABLE_TASKS || process.env.DWN_STORAGE || 'level://data', // tenant registration feature configuration registrationStoreUrl: process.env.DWN_REGISTRATION_STORE_URL || process.env.DWN_STORAGE, diff --git a/src/dwn-server.ts b/src/dwn-server.ts index cd606ce..316e078 100644 --- a/src/dwn-server.ts +++ b/src/dwn-server.ts @@ -1,26 +1,54 @@ +import type { DidResolver } from '@web5/dids'; import type { EventStream } from '@tbd54566975/dwn-sdk-js'; -import { Dwn, EventEmitterStream } from '@tbd54566975/dwn-sdk-js'; - +import type { ProcessHandlers } from './process-handlers.js'; import type { Server } from 'http'; +import type { WebSocketServer } from 'ws'; +import type { DwnServerConfig } from './config.js'; + import log from 'loglevel'; import prefix from 'loglevel-plugin-prefix'; -import { type WebSocketServer } from 'ws'; - +import { config as defaultConfig } from './config.js'; +import { getDwnConfig } from './storage.js'; import { HttpServerShutdownHandler } from './lib/http-server-shutdown-handler.js'; - -import { type DwnServerConfig, config as defaultConfig } from './config.js'; import { HttpApi } from './http-api.js'; -import { setProcessHandlers } from './process-handlers.js'; -import { getDWNConfig } from './storage.js'; -import { WsApi } from './ws-api.js'; +import { PluginLoader } from './plugin-loader.js'; import { RegistrationManager } from './registration/registration-manager.js'; +import { WsApi } from './ws-api.js'; +import { Dwn, EventEmitterStream } from '@tbd54566975/dwn-sdk-js'; +import { removeProcessHandlers, setProcessHandlers } from './process-handlers.js'; +/** + * Options for the DwnServer constructor. + * This is different to DwnServerConfig in that the DwnServerConfig defines configuration that come from environment variables so (more) user facing. + * Where as DwnServerOptions wraps DwnServerConfig with additional overrides that can be used for testing. + */ export type DwnServerOptions = { + /** + * A custom DID resolver to use in the DWN. + * Mainly for testing purposes. Ignored if `dwn` is provided. + */ + didResolver?: DidResolver; dwn?: Dwn; config?: DwnServerConfig; }; +/** + * State of the DwnServer, either Stopped or Started, to help short-circuit start and stop logic. + */ +enum DwnServerState { + Stopped, + Started +} + export class DwnServer { + serverState = DwnServerState.Stopped; + processHandlers: ProcessHandlers; + + /** + * A custom DID resolver to use in the DWN. + * Mainly for testing purposes. Ignored if `dwn` is provided. + */ + didResolver?: DidResolver; dwn?: Dwn; config: DwnServerConfig; #httpServerShutdownHandler: HttpServerShutdownHandler; @@ -32,6 +60,8 @@ export class DwnServer { */ constructor(options: DwnServerOptions = {}) { this.config = options.config ?? defaultConfig; + + this.didResolver = options.didResolver; this.dwn = options.dwn; log.setLevel(this.config.logLevel as log.LogLevelDesc); @@ -40,9 +70,17 @@ export class DwnServer { prefix.apply(log); } + /** + * Starts the DWN server. + */ async start(): Promise { + if (this.serverState === DwnServerState.Started) { + return; + } + await this.#setupServer(); - setProcessHandlers(this); + this.processHandlers = setProcessHandlers(this); + this.serverState = DwnServerState.Started; } /** @@ -63,22 +101,27 @@ export class DwnServer { let eventStream: EventStream | undefined; if (this.config.webSocketSupport) { - // setting `EventEmitterStream` as default the default `EventStream - // if an alternate implementation is needed, instantiate a `Dwn` with a custom `EventStream` and add it to server options. - eventStream = new EventEmitterStream(); + // If Even Stream plugin is not specified, use `EventEmitterStream` implementation as default. + if (this.config.eventStreamPluginPath === undefined || this.config.eventStreamPluginPath === '') { + eventStream = new EventEmitterStream(); + } else { + eventStream = await PluginLoader.loadPlugin(this.config.eventStreamPluginPath); + } + } - this.dwn = await Dwn.create(getDWNConfig(this.config, { + const dwnConfig = await getDwnConfig(this.config, { + didResolver: this.didResolver, tenantGate: registrationManager, eventStream, - })); + }) + this.dwn = await Dwn.create(dwnConfig); } - this.#httpApi = new HttpApi(this.config, this.dwn, registrationManager); + this.#httpApi = await HttpApi.create(this.config, this.dwn, registrationManager); - await this.#httpApi.start(this.config.port, () => { - log.info(`HttpServer listening on port ${this.config.port}`); - }); + await this.#httpApi.start(this.config.port); + log.info(`HttpServer listening on port ${this.config.port}`); this.#httpServerShutdownHandler = new HttpServerShutdownHandler( this.#httpApi.server, @@ -91,8 +134,31 @@ export class DwnServer { } } - stop(callback: () => void): void { - this.#httpServerShutdownHandler.stop(callback); + /** + * Stops the DWN server. + */ + async stop(): Promise { + if (this.serverState === DwnServerState.Stopped) { + return; + } + + await this.dwn.close(); + await this.#httpApi.close(); + + // close WebSocket server if it was initialized + if (this.#wsApi !== undefined) { + await this.#wsApi.close(); + } + + await new Promise((resolve) => { + this.#httpServerShutdownHandler.stop(() => { + resolve(); + }); + }); + + removeProcessHandlers(this.processHandlers); + + this.serverState = DwnServerState.Stopped; } get httpServer(): Server { diff --git a/src/http-api.ts b/src/http-api.ts index efce968..7629237 100644 --- a/src/http-api.ts +++ b/src/http-api.ts @@ -1,4 +1,4 @@ -import { type Dwn, RecordsRead, RecordsQuery } from '@tbd54566975/dwn-sdk-js'; +import { type Dwn, DateSort, RecordsRead, RecordsQuery, ProtocolsQuery } from '@tbd54566975/dwn-sdk-js'; import cors from 'cors'; import type { Express, Request, Response } from 'express'; @@ -12,14 +12,15 @@ import { v4 as uuidv4 } from 'uuid'; import type { RequestContext } from './lib/json-rpc-router.js'; import type { JsonRpcRequest } from './lib/json-rpc.js'; -import { createJsonRpcErrorResponse, JsonRpcErrorCodes } from './lib/json-rpc.js'; import type { DwnServerConfig } from './config.js'; +import type { DwnServerError } from './dwn-error.js'; +import type { RegistrationManager } from './registration/registration-manager.js'; import { config } from './config.js'; -import { type DwnServerError } from './dwn-error.js'; import { jsonRpcRouter } from './json-rpc-api.js'; +import { Web5ConnectServer } from './web5-connect/web5-connect-server.js'; +import { createJsonRpcErrorResponse, JsonRpcErrorCodes } from './lib/json-rpc.js'; import { requestCounter, responseHistogram } from './metrics.js'; -import type { RegistrationManager } from './registration/registration-manager.js'; export class HttpApi { @@ -27,36 +28,49 @@ export class HttpApi { #packageInfo: { version?: string, sdkVersion?: string, server: string }; #api: Express; #server: http.Server; + web5ConnectServer: Web5ConnectServer; registrationManager: RegistrationManager; dwn: Dwn; - constructor(config: DwnServerConfig, dwn: Dwn, registrationManager?: RegistrationManager) { - console.log(config); + private constructor() { } - this.#packageInfo = { + public static async create(config: DwnServerConfig, dwn: Dwn, registrationManager?: RegistrationManager): Promise { + const httpApi = new HttpApi(); + + log.info(config); + + httpApi.#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; + httpApi.#packageInfo.version = packageJson.version; + httpApi.#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); - this.dwn = dwn; + httpApi.#config = config; + httpApi.#api = express(); + httpApi.#server = http.createServer(httpApi.#api); + httpApi.dwn = dwn; if (registrationManager !== undefined) { - this.registrationManager = registrationManager; + httpApi.registrationManager = registrationManager; } - this.#setupMiddleware(); - this.#setupRoutes(); + // create the Web5 Connect Server + httpApi.web5ConnectServer = await Web5ConnectServer.create({ + baseUrl: `${config.baseUrl}:${config.port}`, + sqlTtlCacheUrl: config.ttlCacheUrl, + }); + + httpApi.#setupMiddleware(); + httpApi.#setupRoutes(); + + return httpApi; } get server(): http.Server { @@ -70,7 +84,7 @@ export class HttpApi { #setupMiddleware(): void { this.#api.use(cors({ exposedHeaders: 'dwn-response' })); this.#api.use(express.json()); - + this.#api.use(express.urlencoded({ extended: true })); // formdata middleware this.#api.use( responseTime((req: Request, res: Response, time) => { const url = req.url === '/' ? '/jsonrpc' : req.url; @@ -90,6 +104,29 @@ export class HttpApi { * Configures the HTTP server's request handlers. */ #setupRoutes(): void { + + const leadTailSlashRegex = /^\/|\/$/; + + function readReplyHandler(res, reply): any { + if (reply.status.code === 200) { + if (reply?.record?.data) { + const stream = reply.record.data; + delete reply.record.data; + + res.setHeader('content-type', reply.record.descriptor.dataFormat); + res.setHeader('dwn-response', JSON.stringify(reply)); + + return stream.pipe(res); + } else { + return res.sendStatus(400); + } + } else if (reply.status.code === 401) { + return res.sendStatus(404); + } else { + return res.status(reply.status.code).send(reply); + } + } + this.#api.get('/health', (_req, res) => { // return 200 ok return res.json({ ok: true }); @@ -104,28 +141,83 @@ export class HttpApi { } }); - this.#api.get('/:did/records/:id', async (req, res) => { - const record = await RecordsRead.create({ - filter: { recordId: req.params.id }, + // Returns the data for the most recently published record under a given protocol path collection, if one is present + this.#api.get('/:did/read/protocols/:protocol/*', async (req, res) => { + if (!req.params[0]) { + return res.status(400).send('protocol path is required'); + } + + const protocolPath = req.params[0].replace(leadTailSlashRegex, ''); + const protocol = req.params.protocol; + + const query = await RecordsQuery.create({ + filter: { + protocol, + protocolPath, + }, + pagination: { limit: 1 }, + dateSort: DateSort.PublishedDescending }); - const reply = await this.dwn.processMessage(req.params.did, record.message); - if (reply.status.code === 200) { - if (reply?.record?.data) { - const stream = reply.record.data; - delete reply.record.data; + const { entries, status } = await this.dwn.processMessage(req.params.did, query.message); - res.setHeader('content-type', reply.record.descriptor.dataFormat); - res.setHeader('dwn-response', JSON.stringify(reply)); + if (status.code === 200) { + if (entries[0]) { + const record = await RecordsRead.create({ + filter: { recordId: entries[0].recordId }, + }); + const reply = await this.dwn.processMessage(req.params.did, record.toJSON()); + return readReplyHandler(res, reply); + } else { + return res.sendStatus(404); + } + } else if (status.code === 401) { + return res.sendStatus(404); + } else { + return res.sendStatus(status.code); + } + }) - return stream.pipe(res); + this.#api.get('/:did/read/protocols/:protocol', async (req, res) => { + const query = await ProtocolsQuery.create({ + filter: { protocol: req.params.protocol } + }); + const { entries, status } = await this.dwn.processMessage(req.params.did, query.message); + if (status.code === 200) { + if (entries.length) { + res.status(status.code); + res.json(entries[0]); } else { - return res.sendStatus(400); + return res.sendStatus(404); } - } else if (reply.status.code === 401) { + } else if (status.code === 401) { return res.sendStatus(404); } else { - return res.status(reply.status.code).send(reply); + return res.sendStatus(status.code); + } + }) + + const recordsReadHandler = async (req, res): Promise => { + const record = await RecordsRead.create({ + filter: { recordId: req.params.id }, + }); + const reply = await this.dwn.processMessage(req.params.did, record.message); + return readReplyHandler(res, reply); + } + + this.#api.get('/:did/read/records/:id', recordsReadHandler); + this.#api.get('/:did/records/:id', recordsReadHandler); + + this.#api.get('/:did/query/protocols', async (req, res) => { + const query = await ProtocolsQuery.create({}); + const { entries, status } = await this.dwn.processMessage(req.params.did, query.message); + if (status.code === 200) { + res.status(status.code); + res.json(entries); + } else if (status.code === 401) { + return res.sendStatus(404); + } else { + return res.sendStatus(status.code); } }); @@ -133,7 +225,7 @@ export class HttpApi { try { // builds a nested object from flat keys with dot notation which may share the same parent path - // e.g. "filter.protocol=foo&filter.protocolPath=bar" becomes + // e.g. "did:dht:123/query?filter.protocol=foo&filter.protocolPath=bar" becomes // { // filter: { // protocol: 'foo', @@ -235,6 +327,7 @@ export class HttpApi { } res.json({ + url : config.baseUrl, server : this.#packageInfo.server, maxFileSize : config.maxRecordDataSize, registrationRequirements : registrationRequirements, @@ -243,10 +336,8 @@ export class HttpApi { webSocketSupport : config.webSocketSupport, }); }); - } - #listen(port: number, callback?: () => void): void { - this.#server.listen(port, callback); + this.#setupWeb5ConnectServerRoutes(); } #setupRegistrationRoutes(): void { @@ -264,7 +355,7 @@ export class HttpApi { if (this.#config.registrationStoreUrl !== undefined) { this.#api.post('/registration', async (req: Request, res: Response) => { const requestBody = req.body; - console.log('Registration request:', requestBody); + log.info('Registration request:', requestBody); try { await this.registrationManager.handleRegistrationRequest(requestBody); @@ -275,7 +366,7 @@ export class HttpApi { if (dwnServerError.code !== undefined) { res.status(400).json(dwnServerError); } else { - console.log('Error handling registration request:', error); + log.info('Error handling registration request:', error); res.status(500).json({ success: false }); } } @@ -283,8 +374,140 @@ export class HttpApi { } } - async start(port: number, callback?: () => void): Promise { - this.#listen(port, callback); - return this.#server; + #setupWeb5ConnectServerRoutes(): void { + /** + * Endpoint allows a Client app (RP) to submit an Authorization Request. + * The Authorization Request is stored on the server, and a unique `request_uri` is returned to the Client app. + * The Client app can then provide this `request_uri` to the Provider app (wallet). + * The Provider app uses the `request_uri` to retrieve the stored Authorization Request. + */ + this.#api.post('/connect/par', async (req, res) => { + log.info('Storing Pushed Authorization Request (PAR) request...'); + + // TODO: Add validation for request too large HTTP 413: https://github.com/TBD54566975/dwn-server/issues/146 + // TODO: Add validation for too many requests HTTP 429: https://github.com/TBD54566975/dwn-server/issues/147 + + if (!req.body.request) { + return res.status(400).json({ + ok: false, + status: { + code: 400, + message: "Bad Request: Missing 'request' parameter", + }, + }); + } + + // Validate that `request_uri` was NOT provided + if (req.body?.request?.request_uri) { + return res.status(400).json({ + ok: false, + status: { + code: 400, + message: "Bad Request: 'request_uri' parameter is not allowed in PAR", + }, + }); + } + + const result = await this.web5ConnectServer.setWeb5ConnectRequest(req.body.request); + res.status(201).json(result); + }); + + /** + * Endpoint for the Provider to retrieve the Authorization Request from the request_uri + */ + this.#api.get('/connect/authorize/:requestId.jwt', async (req, res) => { + log.info(`Retrieving Web5 Connect Request object of ID: ${req.params.requestId}...`); + + // Look up the request object based on the requestId. + const requestObjectJwt = await this.web5ConnectServer.getWeb5ConnectRequest(req.params.requestId); + + if (!requestObjectJwt) { + res.status(404).json({ + ok : false, + status : { code: 404, message: 'Not Found' } + }); + } else { + res.set('Content-Type', 'application/jwt'); + res.send(requestObjectJwt); + } + }); + + /** + * Endpoint that the Provider sends the Authorization Response to + */ + this.#api.post('/connect/callback', async (req, res) => { + log.info('Storing Identity Provider (wallet) pushed response with ID token...'); + + // Store the ID token. + const idToken = req.body.id_token; + const state = req.body.state; + + if (idToken !== undefined && state != undefined) { + + await this.web5ConnectServer.setWeb5ConnectResponse(state, idToken); + + res.status(201).json({ + ok : true, + status : { code: 201, message: 'Created' } + }); + + } else { + res.status(400).json({ + ok : false, + status : { code: 400, message: 'Bad Request' } + }); + } + }); + + /** + * Endpoint for the connecting Client to retrieve the Authorization Response + */ + this.#api.get('/connect/token/:state.jwt', async (req, res) => { + log.info(`Retrieving ID token for state: ${req.params.state}...`); + + // Look up the ID token. + const idToken = await this.web5ConnectServer.getWeb5ConnectResponse(req.params.state); + + if (!idToken) { + res.status(404).json({ + ok : false, + status : { code: 404, message: 'Not Found' } + }); + } else { + res.set('Content-Type', 'application/jwt'); + res.send(idToken); + } + }); + } + + /** + * Starts the HTTP API endpoint on the given port. + * @returns The HTTP server instance. + */ + async start(port: number): Promise { + // promisify http.Server.listen() and await on it + await new Promise((resolve) => { + this.#server.listen(port, () => { + resolve(); + }); + }); + } + + /** + * Stops the HTTP API endpoint. + */ + async close(): Promise { + // promisify http.Server.close() and await on it + await new Promise((resolve, reject) => { + this.#server.close((err?: Error) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + + this.server.closeAllConnections(); } } diff --git a/src/index.ts b/src/index.ts index 77b43b5..9b22c69 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,5 +2,5 @@ export { DwnServerConfig } from './config.js'; export { DwnServer, DwnServerOptions } from './dwn-server.js'; export { HttpApi } from './http-api.js'; export { jsonRpcRouter } from './json-rpc-api.js'; -export { EStoreType, BackendTypes, StoreType } from './storage.js'; +export { StoreType, BackendTypes, DwnStore } from './storage.js'; export { WsApi } from './ws-api.js'; diff --git a/src/json-rpc-handlers/dwn/process-message.ts b/src/json-rpc-handlers/dwn/process-message.ts index cac2090..6d66c66 100644 --- a/src/json-rpc-handlers/dwn/process-message.ts +++ b/src/json-rpc-handlers/dwn/process-message.ts @@ -79,12 +79,17 @@ export const handleDwnProcessMessage: JsonRpcHandler = async ( subscriptionHandler: subscriptionRequest?.subscriptionHandler, }); - const { record } = reply; - // RecordsRead messages return record data as a stream to for accommodate large amounts of data + + const { record, entry } = reply; + // RecordsRead or MessagesRead messages optionally return data as a stream to accommodate large amounts of data + // we remove the data stream from the reply that will be serialized and return it as a separate property in the response payload. let recordDataStream: IsomorphicReadable; if (record !== undefined && record.data !== undefined) { recordDataStream = reply.record.data; delete reply.record.data; // not serializable via JSON + } else if (entry !== undefined && entry.data !== undefined) { + recordDataStream = entry.data; + delete reply.entry.data; // not serializable via JSON } if (subscriptionRequest && reply.subscription) { @@ -107,15 +112,15 @@ export const handleDwnProcessMessage: JsonRpcHandler = async ( } return responsePayload; - } catch (e) { + } catch (error) { const jsonRpcResponse = createJsonRpcErrorResponse( requestId, JsonRpcErrorCodes.InternalError, - e.message, + error.message, ); // log the unhandled error response - log.error('handleDwnProcessMessage error', jsonRpcResponse, dwnRequest, e); + log.error('handleDwnProcessMessage error', jsonRpcResponse, dwnRequest, error); return { jsonRpcResponse } as HandlerResponse; } }; diff --git a/src/lib/json-rpc-router.ts b/src/lib/json-rpc-router.ts index 8fd1408..9866452 100644 --- a/src/lib/json-rpc-router.ts +++ b/src/lib/json-rpc-router.ts @@ -1,4 +1,4 @@ -import type { Dwn, EventSubscriptionHandler } from '@tbd54566975/dwn-sdk-js'; +import type { Dwn, MessageSubscriptionHandler } from '@tbd54566975/dwn-sdk-js'; import type { Readable } from 'node:stream'; @@ -14,7 +14,7 @@ export type RequestContext = { /** The JsonRpcId of the subscription handler */ id: JsonRpcId; /** The `MessageEvent` handler associated with a subscription request, only used in `ws` requests */ - subscriptionHandler: EventSubscriptionHandler; + subscriptionHandler: MessageSubscriptionHandler; } /** The `Readable` stream associated with a `RecordsWrite` request only used in `http` requests */ dataStream?: Readable; diff --git a/src/plugin-loader.ts b/src/plugin-loader.ts new file mode 100644 index 0000000..44a3089 --- /dev/null +++ b/src/plugin-loader.ts @@ -0,0 +1,17 @@ +/** + * A utility class for dynamically loading plugins from file paths. + */ +export class PluginLoader { + /** + * Dynamically loads a plugin from a file path by invoking the argument-less constructor of the default exported class. + */ + public static async loadPlugin(filePath: string): Promise { + try { + const module = await import(filePath); + const instance: T = new module.default() as T; + return instance; + } catch (error) { + throw new Error(`Failed to load component at ${filePath}: ${error.message}`); + } + } +} diff --git a/src/process-handlers.ts b/src/process-handlers.ts index d458cec..554fcaf 100644 --- a/src/process-handlers.ts +++ b/src/process-handlers.ts @@ -1,36 +1,65 @@ import type { DwnServer } from './dwn-server.js'; -export const gracefulShutdown = (dwnServer: DwnServer): void => { - dwnServer.stop(() => { - console.log('http server stopped.. exiting'); - process.exit(0); - }); +export const gracefulShutdown = async (dwnServer: DwnServer): Promise => { + await dwnServer.stop(); + console.log('http server stopped.. exiting'); + process.exit(0); }; -export const setProcessHandlers = (dwnServer: DwnServer): void => { - process.on('unhandledRejection', (reason, promise) => { +export type ProcessHandlers = { + unhandledRejectionHandler: (reason: any, promise: Promise) => void, + uncaughtExceptionHandler: (err: Error) => void, + sigintHandler: () => Promise, + sigtermHandler: () => Promise +}; + +export const setProcessHandlers = (dwnServer: DwnServer): ProcessHandlers => { + const unhandledRejectionHandler = (reason: any, promise: Promise): void => { console.error( `Unhandled promise rejection. Reason: ${reason}. Promise: ${JSON.stringify( promise, )}`, ); - }); + }; - process.on('uncaughtException', (err) => { + const uncaughtExceptionHandler = (err: Error): void => { console.error('Uncaught exception:', err.stack || err); - }); + }; - // triggered by ctrl+c with no traps in between - process.on('SIGINT', async () => { + const sigintHandler = async (): Promise => { console.log('exit signal received [SIGINT]. starting graceful shutdown'); + await gracefulShutdown(dwnServer); + }; - gracefulShutdown(dwnServer); - }); - - // triggered by docker, tiny etc. - process.on('SIGTERM', async () => { + const sigtermHandler = async (): Promise => { console.log('exit signal received [SIGTERM]. starting graceful shutdown'); + await gracefulShutdown(dwnServer); + }; - gracefulShutdown(dwnServer); - }); + process.on('unhandledRejection', unhandledRejectionHandler); + process.on('uncaughtException', uncaughtExceptionHandler); + process.on('SIGINT', sigintHandler); + process.on('SIGTERM', sigtermHandler); + + // Store handlers to be able to remove them later + return { + unhandledRejectionHandler, + uncaughtExceptionHandler, + sigintHandler, + sigtermHandler + }; }; + +export const removeProcessHandlers = (handlers: ProcessHandlers): void => { + const { + unhandledRejectionHandler, + uncaughtExceptionHandler, + sigintHandler, + sigtermHandler + } = handlers; + + process.removeListener('unhandledRejection', unhandledRejectionHandler); + process.removeListener('uncaughtException', uncaughtExceptionHandler); + process.removeListener('SIGINT', sigintHandler); + process.removeListener('SIGTERM', sigtermHandler); +}; \ No newline at end of file diff --git a/src/registration/registration-manager.ts b/src/registration/registration-manager.ts index d7c221e..518d884 100644 --- a/src/registration/registration-manager.ts +++ b/src/registration/registration-manager.ts @@ -5,7 +5,7 @@ import type { RegistrationData, RegistrationRequest } from "./registration-types import type { ProofOfWorkChallengeModel } from "./proof-of-work-types.js"; import { DwnServerError, DwnServerErrorCode } from "../dwn-error.js"; import type { ActiveTenantCheckResult, TenantGate } from "@tbd54566975/dwn-sdk-js"; -import { getDialectFromURI } from "../storage.js"; +import { getDialectFromUrl } from "../storage.js"; import { readFileSync } from "fs"; /** @@ -77,7 +77,7 @@ export class RegistrationManager implements TenantGate { }); // Initialize RegistrationStore. - const sqlDialect = getDialectFromURI(new URL(registrationStoreUrl)); + const sqlDialect = getDialectFromUrl(new URL(registrationStoreUrl)); const registrationStore = await RegistrationStore.create(sqlDialect); registrationManager.registrationStore = registrationStore; diff --git a/src/storage.ts b/src/storage.ts index a4eea8c..087797c 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -1,39 +1,44 @@ -import * as fs from 'fs'; - -import { - DataStoreLevel, - EventLogLevel, - MessageStoreLevel, -} from '@tbd54566975/dwn-sdk-js'; +import type { DidResolver } from '@web5/dids'; import type { DataStore, DwnConfig, EventLog, EventStream, MessageStore, + ResumableTaskStore, TenantGate, } from '@tbd54566975/dwn-sdk-js'; import type { Dialect } from '@tbd54566975/dwn-sql-store'; +import type { DwnServerConfig } from './config.js'; + +import * as fs from 'fs'; +import Cursor from 'pg-cursor'; +import Database from 'better-sqlite3'; +import pg from 'pg'; +import { createPool as MySQLCreatePool } from 'mysql2'; +import { PluginLoader } from './plugin-loader.js'; + +import { + DataStoreLevel, + EventLogLevel, + MessageStoreLevel, + ResumableTaskStoreLevel, +} from '@tbd54566975/dwn-sdk-js'; import { DataStoreSql, EventLogSql, MessageStoreSql, MysqlDialect, PostgresDialect, + ResumableTaskStoreSql, SqliteDialect, } from '@tbd54566975/dwn-sql-store'; -import Database from 'better-sqlite3'; -import { createPool as MySQLCreatePool } from 'mysql2'; -import pg from 'pg'; -import Cursor from 'pg-cursor'; - -import type { DwnServerConfig } from './config.js'; - -export enum EStoreType { +export enum StoreType { DataStore, MessageStore, EventLog, + ResumableTaskStore, } export enum BackendTypes { @@ -43,79 +48,91 @@ export enum BackendTypes { POSTGRES = 'postgres', } -export type StoreType = DataStore | EventLog | MessageStore; +export type DwnStore = DataStore | EventLog | MessageStore | ResumableTaskStore; -export function getDWNConfig( +export async function getDwnConfig( config : DwnServerConfig, options : { + didResolver? : DidResolver, tenantGate? : TenantGate, eventStream? : EventStream, } -): DwnConfig { - const { tenantGate, eventStream } = options; - const dataStore: DataStore = getStore(config.dataStore, EStoreType.DataStore); - const eventLog: EventLog = getStore(config.eventLog, EStoreType.EventLog); - const messageStore: MessageStore = getStore( - config.messageStore, - EStoreType.MessageStore, - ); - - return { eventStream, eventLog, dataStore, messageStore, tenantGate }; +): Promise { + const { tenantGate, eventStream, didResolver } = options; + const dataStore: DataStore = await getStore(config.dataStore, StoreType.DataStore); + const eventLog: EventLog = await getStore(config.eventLog, StoreType.EventLog); + const messageStore: MessageStore = await getStore(config.messageStore, StoreType.MessageStore); + const resumableTaskStore: ResumableTaskStore = await getStore(config.resumableTaskStore, StoreType.ResumableTaskStore); + + return { didResolver, eventStream, eventLog, dataStore, messageStore, resumableTaskStore, tenantGate }; } function getLevelStore( storeURI: URL, - storeType: EStoreType, -): DataStore | MessageStore | EventLog { + storeType: StoreType, +): DwnStore { switch (storeType) { - case EStoreType.DataStore: + case StoreType.DataStore: return new DataStoreLevel({ blockstoreLocation: storeURI.host + storeURI.pathname + '/DATASTORE', }); - case EStoreType.MessageStore: + case StoreType.MessageStore: return new MessageStoreLevel({ blockstoreLocation: storeURI.host + storeURI.pathname + '/MESSAGESTORE', indexLocation: storeURI.host + storeURI.pathname + '/INDEX', }); - case EStoreType.EventLog: + case StoreType.EventLog: return new EventLogLevel({ location: storeURI.host + storeURI.pathname + '/EVENTLOG', }); + case StoreType.ResumableTaskStore: + return new ResumableTaskStoreLevel({ + location: storeURI.host + storeURI.pathname + '/RESUMABLE-TASK-STORE', + }); default: throw new Error('Unexpected level store type'); } } -function getDBStore( - db: Dialect, - storeType: EStoreType, -): DataStore | MessageStore | EventLog { +function getSqlStore( + connectionUrl: URL, + storeType: StoreType, +): DwnStore { + const dialect = getDialectFromUrl(connectionUrl); + switch (storeType) { - case EStoreType.DataStore: - return new DataStoreSql(db); - case EStoreType.MessageStore: - return new MessageStoreSql(db); - case EStoreType.EventLog: - return new EventLogSql(db); + case StoreType.DataStore: + return new DataStoreSql(dialect); + case StoreType.MessageStore: + return new MessageStoreSql(dialect); + case StoreType.EventLog: + return new EventLogSql(dialect); + case StoreType.ResumableTaskStore: + return new ResumableTaskStoreSql(dialect); default: - throw new Error('Unexpected db store type'); + throw new Error(`Unsupported store type ${storeType} for SQL store.`); } } -function getStore( - storeString: string, - storeType: EStoreType.DataStore, -): DataStore; -function getStore( - storeString: string, - storeType: EStoreType.EventLog, -): EventLog; -function getStore( - storeString: string, - storeType: EStoreType.MessageStore, -): MessageStore; -function getStore(storeString: string, storeType: EStoreType): StoreType { - const storeURI = new URL(storeString); +/** + * Check if the given string is a file path. + */ +function isFilePath(configString: string): boolean { + const filePathPrefixes = ['/', './', '../']; + return filePathPrefixes.some(prefix => configString.startsWith(prefix)); +} + +async function getStore(storeString: string, storeType: StoreType.DataStore): Promise; +async function getStore(storeString: string, storeType: StoreType.EventLog): Promise; +async function getStore(storeString: string, storeType: StoreType.MessageStore): Promise; +async function getStore(storeString: string, storeType: StoreType.ResumableTaskStore): Promise; +async function getStore(storeConfigString: string, storeType: StoreType): Promise { + if (isFilePath(storeConfigString)) { + return await loadStoreFromFilePath(storeConfigString, storeType); + } + // else treat the `storeConfigString` as a connection string + + const storeURI = new URL(storeConfigString); switch (storeURI.protocol.slice(0, -1)) { case BackendTypes.LEVEL: @@ -124,22 +141,43 @@ function getStore(storeString: string, storeType: EStoreType): StoreType { case BackendTypes.SQLITE: case BackendTypes.MYSQL: case BackendTypes.POSTGRES: - return getDBStore(getDialectFromURI(storeURI), storeType); + return getSqlStore(storeURI, storeType); default: throw invalidStorageSchemeMessage(storeURI.protocol); } } -export function getDialectFromURI(u: URL): Dialect { - switch (u.protocol.slice(0, -1)) { +/** + * Loads a DWN store plugin of the given type from the given file path. + */ +async function loadStoreFromFilePath( + filePath: string, + storeType: StoreType, +): Promise { + switch (storeType) { + case StoreType.DataStore: + return await PluginLoader.loadPlugin(filePath); + case StoreType.EventLog: + return await PluginLoader.loadPlugin(filePath); + case StoreType.MessageStore: + return await PluginLoader.loadPlugin(filePath); + case StoreType.ResumableTaskStore: + return await PluginLoader.loadPlugin(filePath); + default: + throw new Error(`Loading store for unsupported store type ${storeType} from path ${filePath}`); + } +} + +export function getDialectFromUrl(connectionUrl: URL): Dialect { + switch (connectionUrl.protocol.slice(0, -1)) { case BackendTypes.SQLITE: - const path = u.host + u.pathname; + const path = connectionUrl.host + connectionUrl.pathname; console.log('SQL-lite relative path:', path ? path : undefined); // NOTE, using ? for lose equality comparison - if (u.host && !fs.existsSync(u.host)) { - console.log('SQL-lite directory does not exist, creating:', u.host); - fs.mkdirSync(u.host, { recursive: true }); + if (connectionUrl.host && !fs.existsSync(connectionUrl.host)) { + console.log('SQL-lite directory does not exist, creating:', connectionUrl.host); + fs.mkdirSync(connectionUrl.host, { recursive: true }); } return new SqliteDialect({ @@ -147,11 +185,11 @@ export function getDialectFromURI(u: URL): Dialect { }); case BackendTypes.MYSQL: return new MysqlDialect({ - pool: async () => MySQLCreatePool(u.toString()), + pool: async () => MySQLCreatePool(connectionUrl.toString()), }); case BackendTypes.POSTGRES: return new PostgresDialect({ - pool: async () => new pg.Pool({ u }), + pool: async () => new pg.Pool({ connectionString: connectionUrl.toString() }), cursor: Cursor, }); } diff --git a/src/web5-connect/sql-ttl-cache.ts b/src/web5-connect/sql-ttl-cache.ts new file mode 100644 index 0000000..697b3cb --- /dev/null +++ b/src/web5-connect/sql-ttl-cache.ts @@ -0,0 +1,137 @@ +import type { Dialect } from '@tbd54566975/dwn-sql-store'; +import { Kysely } from 'kysely'; + +/** + * The SqlTtlCache is responsible for storing and retrieving cache data with TTL (Time-to-Live). + */ +export class SqlTtlCache { + private static readonly cacheTableName = 'cacheEntries'; + private static readonly cleanupIntervalInSeconds = 60; + + private sqlDialect: Dialect; + private db: Kysely; + private cleanupTimer: NodeJS.Timeout; + + private constructor(sqlDialect: Dialect) { + this.db = new Kysely({ dialect: sqlDialect }); + this.sqlDialect = sqlDialect; + } + + /** + * Creates a new SqlTtlCache instance. + */ + public static async create(sqlDialect: Dialect): Promise { + const cacheManager = new SqlTtlCache(sqlDialect); + + await cacheManager.initialize(); + + return cacheManager; + } + + private async initialize(): Promise { + + // create table if it doesn't exist + const tableExists = await this.sqlDialect.hasTable(this.db, SqlTtlCache.cacheTableName); + if (!tableExists) { + await this.db.schema + .createTable(SqlTtlCache.cacheTableName) + .ifNotExists() // kept to show supported by all dialects in contrast to ifNotExists() below, though not needed due to tableExists check above + // 512 chars to accommodate potentially large `state` in Web5 Connect flow + .addColumn('key', 'varchar(512)', (column) => column.primaryKey()) + .addColumn('value', 'text', (column) => column.notNull()) + .addColumn('expiry', 'integer', (column) => column.notNull()) + .execute(); + + await this.db.schema + .createIndex('index_expiry') + // .ifNotExists() // intentionally kept commented out code to show that it is not supported by all dialects (ie. MySQL) + .on(SqlTtlCache.cacheTableName) + .column('expiry') + .execute(); + } + + // Start the cleanup timer + this.startCleanupTimer(); + } + + /** + * Starts a timer to periodically clean up expired cache entries. + */ + private startCleanupTimer(): void { + this.cleanupTimer = setInterval(async () => { + await this.cleanUpExpiredEntries(); + }, SqlTtlCache.cleanupIntervalInSeconds * 1000); + } + + /** + * Inserts a cache entry. + * @param ttl The time-to-live in seconds. + */ + public async insert(key: string, value: object, ttl: number): Promise { + const expiry = Date.now() + (ttl * 1000); + + const objectString = JSON.stringify(value); + + await this.db + .insertInto(SqlTtlCache.cacheTableName) + .values({ key, value: objectString, expiry }) + .execute(); + } + + /** + * Retrieves a cache entry if it is not expired and cleans up expired entries. + */ + public async get(key: string): Promise { + const result = await this.db + .selectFrom(SqlTtlCache.cacheTableName) + .select('key') + .select('value') + .select('expiry') + .where('key', '=', key) + .execute(); + + if (result.length === 0) { + return undefined; + } + + const entry = result[0]; + + // if the entry is expired, don't return it and delete it + if (Date.now() >= entry.expiry) { + this.delete(key); // no need to await + return undefined; + } + + return JSON.parse(entry.value); + } + + /** + * Deletes a cache entry. + */ + public async delete(key: string): Promise { + await this.db + .deleteFrom(SqlTtlCache.cacheTableName) + .where('key', '=', key) + .execute(); + } + + /** + * Cleans up expired cache entries. + */ + public async cleanUpExpiredEntries(): Promise { + await this.db + .deleteFrom(SqlTtlCache.cacheTableName) + .where('expiry', '<', Date.now()) + .execute(); + } +} + +interface CacheEntry { + key: string; + value: string; + expiry: number; +} + +interface CacheDatabase { + cacheEntries: CacheEntry; +} diff --git a/src/web5-connect/web5-connect-server.ts b/src/web5-connect/web5-connect-server.ts new file mode 100644 index 0000000..72ff1e6 --- /dev/null +++ b/src/web5-connect/web5-connect-server.ts @@ -0,0 +1,122 @@ +import { getDialectFromUrl } from "../storage.js"; +import { randomUuid } from '@web5/crypto/utils'; +import { SqlTtlCache } from "./sql-ttl-cache.js"; + +/** + * The Web5 Connect Request object. + */ +export type Web5ConnectRequest = any; // TODO: define type in common repo for reuse (https://github.com/TBD54566975/dwn-server/issues/138) + +/** + * The Web5 Connect Response object, which is also an OIDC ID token + */ +export type Web5ConnectResponse = any; // TODO: define type in common repo for reuse (https://github.com/TBD54566975/dwn-server/issues/138) + +/** + * The result of the setWeb5ConnectRequest() method. + */ +export type SetWeb5ConnectRequestResult = { + /** + * The Request URI that the wallet should use to retrieve the request object. + */ + request_uri: string; + + /** + * The time in seconds that the Request URI is valid for. + */ + expires_in: number; +} + +/** + * The Web5 Connect Server is responsible for handling the Web5 Connect flow. + */ +export class Web5ConnectServer { + public static readonly ttlInSeconds = 600; + + private baseUrl: string; + private cache: SqlTtlCache; + + /** + * Creates a new instance of the Web5 Connect Server. + * @param params.baseUrl The the base URL of the connect server including the port. + * This is given to the Identity Provider (wallet) to fetch the Web5 Connect Request object. + * @param params.sqlTtlCacheUrl The URL of the SQL database to use as the TTL cache. + */ + public static async create({ baseUrl, sqlTtlCacheUrl }: { + baseUrl: string; + sqlTtlCacheUrl: string; + }): Promise { + const web5ConnectServer = new Web5ConnectServer({ baseUrl }); + + // Initialize TTL cache. + const sqlDialect = getDialectFromUrl(new URL(sqlTtlCacheUrl)); + web5ConnectServer.cache = await SqlTtlCache.create(sqlDialect); + + return web5ConnectServer; + } + + private constructor({ baseUrl }: { + baseUrl: string; + }) { + this.baseUrl = baseUrl; + } + + /** + * Stores the given Web5 Connect Request object, which is also an OAuth 2 Pushed Authorization Request (PAR) object. + * This is the initial call to the connect server to start the Web5 Connect flow. + */ + public async setWeb5ConnectRequest(request: Web5ConnectRequest): Promise { + // Generate a request URI + const requestId = randomUuid(); + const request_uri = `${this.baseUrl}/connect/authorize/${requestId}.jwt`; + + // Store the Request Object. + this.cache.insert(`request:${requestId}`, request, Web5ConnectServer.ttlInSeconds); + + return { + request_uri, + expires_in : Web5ConnectServer.ttlInSeconds, + }; + } + + /** + * Returns the Web5 Connect Request object. The request ID can only be used once. + */ + public async getWeb5ConnectRequest(requestId: string): Promise { + const request = await this.cache.get(`request:${requestId}`); + + // Delete the Request Object from cache once it has been retrieved. + // IMPORTANT: only delete if the object exists, otherwise there could be a race condition + // where the object does not exist in this call but becomes available immediately after, + // we would end up deleting it before it is successfully retrieved. + if (request !== undefined) { + this.cache.delete(`request:${requestId}`); + } + + return request; + } + + /** + * Sets the Web5 Connect Response object, which is also an OIDC ID token. + */ + public async setWeb5ConnectResponse(state: string, response: Web5ConnectResponse): Promise { + this.cache.insert(`response:${state}`, response, Web5ConnectServer.ttlInSeconds); + } + + /** + * Gets the Web5 Connect Response object. The `state` string can only be used once. + */ + public async getWeb5ConnectResponse(state: string): Promise { + const response = await this.cache.get(`response:${state}`); + + // Delete the Response object from the cache once it has been retrieved. + // IMPORTANT: only delete if the object exists, otherwise there could be a race condition + // where the object does not exist in this call but becomes available immediately after, + // we would end up deleting it before it is successfully retrieved. + if (response !== undefined) { + this.cache.delete(`response:${state}`); + } + + return response; + } +} \ No newline at end of file diff --git a/src/ws-api.ts b/src/ws-api.ts index 426aadc..2cfbd88 100644 --- a/src/ws-api.ts +++ b/src/ws-api.ts @@ -34,9 +34,8 @@ export class WsApi { this.#wsServer.on('close', () => this.#connectionManager.closeAll()); } - start(): WebSocketServer { + start(): void { this.#setupWebSocket(); - return this.#wsServer; } async close(): Promise { diff --git a/tests/common-scenario-validator.ts b/tests/common-scenario-validator.ts new file mode 100644 index 0000000..4d755b2 --- /dev/null +++ b/tests/common-scenario-validator.ts @@ -0,0 +1,142 @@ +import type { JsonRpcSuccessResponse } from '../src/lib/json-rpc.js'; +import type { Persona } from '@tbd54566975/dwn-sdk-js'; +import type { Readable } from 'readable-stream'; + +import chaiAsPromised from 'chai-as-promised'; +import chai, { expect } from 'chai'; +import fetch from 'node-fetch'; + +import { createJsonRpcRequest } from '../src/lib/json-rpc.js'; +import { getFileAsReadStream } from './utils.js'; +import { v4 as uuidv4 } from 'uuid'; +import { webcrypto } from 'node:crypto'; + +import { Cid, DwnConstant, Jws, ProtocolsConfigure, RecordsRead, RecordsWrite, TestDataGenerator } from '@tbd54566975/dwn-sdk-js'; + +// node.js 18 and earlier needs globalThis.crypto polyfill +if (!globalThis.crypto) { + // @ts-ignore + globalThis.crypto = webcrypto; +} + +chai.use(chaiAsPromised); + +/** + * Validator of common scenarios. + */ +export default class CommonScenarioValidator { + /** + * Sanity test RecordsWrite and RecordsRead on the DWN instance. + */ + public static async sanityTestDwnReadWrite(dwnUrl: string, persona?: Persona): Promise { + const alice = persona || await TestDataGenerator.generateDidKeyPersona(); + const aliceSigner = Jws.createSigner(alice); + + // install minimal protocol on Alice's DWN + const protocolDefinition = { + protocol: 'http://minimal.xyz', + published: false, + types: { + foo: {} + }, + structure: { + foo: {} + } + }; + + const protocolsConfig = await ProtocolsConfigure.create({ + signer: aliceSigner, + definition: protocolDefinition + }); + + const protocolConfigureRequestId = uuidv4(); + const protocolConfigureRequest = createJsonRpcRequest(protocolConfigureRequestId, 'dwn.processMessage', { + target: alice.did, + message: protocolsConfig.message, + }); + const protocolConfigureResponse = await fetch(dwnUrl, { + method: 'POST', + headers: { + 'dwn-request': JSON.stringify(protocolConfigureRequest), + } + }); + const protocolConfigureResponseBody = await protocolConfigureResponse.json() as JsonRpcSuccessResponse; + + expect(protocolConfigureResponse.status).to.equal(200); + expect(protocolConfigureResponseBody.result.reply.status.code).to.equal(202); + + // Alice writing a file larger than max data size allowed to be encoded directly in the DWN Message Store. + const filePath = './fixtures/test.jpeg'; + const { + cid: dataCid, + size: dataSize, + stream + } = await getFileAsReadStream(filePath); + expect(dataSize).to.be.greaterThan(DwnConstant.maxDataSizeAllowedToBeEncoded); + + const recordsWrite = await RecordsWrite.create({ + signer: aliceSigner, + dataFormat: 'image/jpeg', + dataCid, + dataSize + }); + + const recordsWriteRequestId = uuidv4(); + const recordsWriteRequest = createJsonRpcRequest(recordsWriteRequestId, 'dwn.processMessage', { + target: alice.did, + message: recordsWrite.message, + }); + const recordsWriteResponse = await fetch(dwnUrl, { + method: 'POST', + headers: { + 'dwn-request': JSON.stringify(recordsWriteRequest), + }, + body: stream + }); + const recordsWriteResponseBody = await recordsWriteResponse.json() as JsonRpcSuccessResponse; + + expect(recordsWriteResponse.status).to.equal(200); + expect(recordsWriteResponseBody.result.reply.status.code).to.equal(202); + + // Alice reading the file back out. + const recordsRead = await RecordsRead.create({ + signer: aliceSigner, + filter: { + recordId: recordsWrite.message.recordId, + }, + }); + + const recordsReadRequestId = uuidv4(); + const recordsReadRequest = createJsonRpcRequest(recordsReadRequestId, 'dwn.processMessage', { + target: alice.did, + message: recordsRead.message + }); + + const recordsReadResponse = await fetch(dwnUrl, { + method: 'POST', + headers: { + 'dwn-request': JSON.stringify(recordsReadRequest), + }, + }); + + expect(recordsReadResponse.status).to.equal(200); + + const { headers } = recordsReadResponse; + const contentType = headers.get('content-type'); + expect(contentType).to.not.be.undefined; + expect(contentType).to.equal('application/octet-stream'); + + const recordsReadDwnResponse = headers.get('dwn-response'); + expect(recordsReadDwnResponse).to.not.be.undefined; + + const recordsReadJsonRpcResponse = JSON.parse(recordsReadDwnResponse) as JsonRpcSuccessResponse; + expect(recordsReadJsonRpcResponse.id).to.equal(recordsReadRequestId); + expect(recordsReadJsonRpcResponse.error).to.not.exist; + expect(recordsReadJsonRpcResponse.result.reply.status.code).to.equal(200); + expect(recordsReadJsonRpcResponse.result.reply.record).to.exist; + + // can't get response as stream from supertest :( + const cid = await Cid.computeDagPbCidFromStream(recordsReadResponse.body as Readable); + expect(cid).to.equal(dataCid); + } +} diff --git a/tests/connection/connection-manager.spec.ts b/tests/connection/connection-manager.spec.ts index ba6850b..616f7ee 100644 --- a/tests/connection/connection-manager.spec.ts +++ b/tests/connection/connection-manager.spec.ts @@ -8,7 +8,6 @@ import { getTestDwn } from '../test-dwn.js'; import { InMemoryConnectionManager } from '../../src/connection/connection-manager.js'; import { config } from '../../src/config.js'; import { WsApi } from '../../src/ws-api.js'; -import type { Server } from 'http'; import { HttpApi } from '../../src/http-api.js'; import { JsonRpcSocket } from '../../src/json-rpc-socket.js'; @@ -17,24 +16,23 @@ chai.use(chaiAsPromised); describe('InMemoryConnectionManager', () => { let dwn: Dwn; let connectionManager: InMemoryConnectionManager; - let server: Server + let httpApi: HttpApi; let wsApi: WsApi; beforeEach(async () => { dwn = await getTestDwn({ withEvents: true }); connectionManager = new InMemoryConnectionManager(dwn); - const httpApi = new HttpApi(config, dwn); - server = await httpApi.start(9002); - wsApi = new WsApi(server, dwn, connectionManager); + httpApi = await HttpApi.create(config, dwn); + await httpApi.start(9002); + wsApi = new WsApi(httpApi.server, dwn, connectionManager); wsApi.start(); }); afterEach(async () => { await connectionManager.closeAll(); await dwn.close(); + await httpApi.close(); await wsApi.close(); - server.close(); - server.closeAllConnections(); sinon.restore(); }); @@ -48,6 +46,7 @@ describe('InMemoryConnectionManager', () => { }); it('closes all connections on `closeAll`', async () => { + await JsonRpcSocket.connect('ws://127.0.0.1:9002'); expect((connectionManager as any).connections.size).to.equal(1); @@ -56,5 +55,5 @@ describe('InMemoryConnectionManager', () => { await connectionManager.closeAll(); expect((connectionManager as any).connections.size).to.equal(0); - }); + }) }); \ No newline at end of file diff --git a/tests/cors.spec.ts b/tests/cors.spec.ts index 9b9649c..1ce7363 100644 --- a/tests/cors.spec.ts +++ b/tests/cors.spec.ts @@ -72,10 +72,9 @@ class CorsProxySetup { resolve(null); }); }); + // shutdown dwn - await new Promise((resolve) => { - dwnServer.stop(resolve); - }); + await dwnServer.stop(); } } diff --git a/tests/dwn-process-message.spec.ts b/tests/dwn-process-message.spec.ts index fe4f3d1..d65c264 100644 --- a/tests/dwn-process-message.spec.ts +++ b/tests/dwn-process-message.spec.ts @@ -7,7 +7,7 @@ import type { RequestContext } from '../src/lib/json-rpc-router.js'; import { JsonRpcErrorCodes, createJsonRpcRequest } from '../src/lib/json-rpc.js'; import { getTestDwn } from './test-dwn.js'; import { createRecordsWriteMessage } from './utils.js'; -import { TestDataGenerator } from '@tbd54566975/dwn-sdk-js'; +import { DataStream, Jws, Message, MessagesRead, RecordsRead, TestDataGenerator } from '@tbd54566975/dwn-sdk-js'; describe('handleDwnProcessMessage', function () { it('returns a JSON RPC Success Response when DWN returns a 2XX status code', async function () { @@ -64,6 +64,106 @@ describe('handleDwnProcessMessage', function () { await dwn.close(); }); + it('should extract data stream from DWN response and return it as a separate property in the JSON RPC response for RecordsRead', async function () { + // scenario: Write a record with some data, and then read the record to get the data back + const alice = await TestDataGenerator.generateDidKeyPersona(); + + // Write a record to later read + const { recordsWrite, dataStream, dataBytes } = await TestDataGenerator.generateRecordsWrite({ author: alice }); + const requestId = uuidv4(); + const dwnRequest = createJsonRpcRequest(requestId, 'dwn.processMessage', { + message: recordsWrite.toJSON(), + target: alice.did, + }); + + const dwn = await getTestDwn(); + const context: RequestContext = { dwn, transport: 'http', dataStream }; + + const { jsonRpcResponse } = await handleDwnProcessMessage( + dwnRequest, + context, + ); + + expect(jsonRpcResponse.error).to.not.exist; + const { reply } = jsonRpcResponse.result; + expect(reply.status.code).to.equal(202); + + + // Read the record to get the data back + const readRequestId = uuidv4(); + const recordsRead = await RecordsRead.create({ + signer: Jws.createSigner(alice), + filter: { recordId: recordsWrite.message.recordId }, + }); + + const readRequest = createJsonRpcRequest(readRequestId, 'dwn.processMessage', { + message: recordsRead.toJSON(), + target: alice.did, + }); + + const { jsonRpcResponse: recordsReadResponse, dataStream: responseDataStream } = await handleDwnProcessMessage(readRequest, { dwn, transport: 'http' }); + expect(recordsReadResponse.error).to.not.exist; + const { reply: readReply } = recordsReadResponse.result; + expect(readReply.status.code).to.equal(200); + expect(responseDataStream).to.not.be.undefined; + + // Compare the data stream bytes to ensure they are the same + const responseDataBytes = await DataStream.toBytes(responseDataStream as any) + expect(responseDataBytes).to.deep.equal(dataBytes); + await dwn.close(); + }); + + it('should extract data stream from DWN response and return it as a separate property in the JSON RPC response for MessagesRead', async function () { + // scenario: Write a record with some data, and then read the message to get the data back + + const alice = await TestDataGenerator.generateDidKeyPersona(); + + // Create a record to read + const { recordsWrite, dataStream, dataBytes } = await TestDataGenerator.generateRecordsWrite({ author: alice }); + const requestId = uuidv4(); + const dwnRequest = createJsonRpcRequest(requestId, 'dwn.processMessage', { + message: recordsWrite.toJSON(), + target: alice.did, + }); + + const dwn = await getTestDwn(); + const context: RequestContext = { dwn, transport: 'http', dataStream }; + + const { jsonRpcResponse } = await handleDwnProcessMessage( + dwnRequest, + context, + ); + + expect(jsonRpcResponse.error).to.not.exist; + const { reply } = jsonRpcResponse.result; + expect(reply.status.code).to.equal(202); + + const messageCid = await Message.getCid(recordsWrite.message); + + // read the message + const readRequestId = uuidv4(); + const messageRead = await MessagesRead.create({ + signer: Jws.createSigner(alice), + messageCid, + }); + + const readRequest = createJsonRpcRequest(readRequestId, 'dwn.processMessage', { + message: messageRead.toJSON(), + target: alice.did, + }); + + const { jsonRpcResponse: recordsReadResponse, dataStream: responseDataStream } = await handleDwnProcessMessage(readRequest, { dwn, transport: 'http' }); + expect(recordsReadResponse.error).to.not.exist; + const { reply: readReply } = recordsReadResponse.result; + expect(readReply.status.code).to.equal(200); + expect(responseDataStream).to.not.be.undefined; + + // Compare the data stream bytes to ensure they are the same + const responseDataBytes = await DataStream.toBytes(responseDataStream as any) + expect(responseDataBytes).to.deep.equal(dataBytes); + await dwn.close(); + }); + it('should fail if no subscriptionRequest context exists for a `Subscribe` message', async function () { const requestId = uuidv4(); const dwnRequest = createJsonRpcRequest(requestId, 'dwn.processMessage', { diff --git a/tests/dwn-server.spec.ts b/tests/dwn-server.spec.ts index 1f33890..fc6a765 100644 --- a/tests/dwn-server.spec.ts +++ b/tests/dwn-server.spec.ts @@ -16,7 +16,7 @@ describe('DwnServer', function () { const dwnServer = new DwnServer({ config: dwnServerConfig, dwn }); await dwnServer.start(); - dwnServer.stop(() => console.log('server Stop')); + await dwnServer.stop(); expect(dwnServer.httpServer.listening).to.be.false; }); @@ -34,7 +34,9 @@ describe('DwnServer', function () { await withoutSocketServer.start(); expect(withoutSocketServer.httpServer.listening).to.be.true; expect(withoutSocketServer.wsServer).to.be.undefined; - withoutSocketServer.stop(() => console.log('server Stop')); + + await withoutSocketServer.stop(); + console.log('server Stop'); expect(withoutSocketServer.httpServer.listening).to.be.false; }); @@ -50,7 +52,9 @@ describe('DwnServer', function () { await withSocketServer.start(); expect(withSocketServer.wsServer).to.not.be.undefined; - withSocketServer.stop(() => console.log('server Stop')); + + await withSocketServer.stop(); + console.log('server Stop'); expect(withSocketServer.httpServer.listening).to.be.false; }); }); diff --git a/tests/http-api.spec.ts b/tests/http-api.spec.ts index ee906a2..c49a3a4 100644 --- a/tests/http-api.spec.ts +++ b/tests/http-api.spec.ts @@ -1,18 +1,16 @@ // node.js 18 and earlier, needs globalThis.crypto polyfill import sinon from 'sinon'; import { - Cid, DataStream, DwnErrorCode, + ProtocolsConfigure, RecordsQuery, - RecordsRead, TestDataGenerator, Time, } from '@tbd54566975/dwn-sdk-js'; -import type { Dwn, DwnError, Persona, RecordsQueryReply } from '@tbd54566975/dwn-sdk-js'; +import type { Dwn, DwnError, Persona, ProtocolsConfigureMessage, RecordsQueryReply } from '@tbd54566975/dwn-sdk-js'; import { expect } from 'chai'; -import type { Server } from 'http'; import fetch from 'node-fetch'; import { webcrypto } from 'node:crypto'; import { useFakeTimers } from 'sinon'; @@ -33,10 +31,11 @@ import { import { getTestDwn } from './test-dwn.js'; import { createRecordsWriteMessage, + getDwnResponse, getFileAsReadStream, - streamHttpRequest, } from './utils.js'; import { RegistrationManager } from '../src/registration/registration-manager.js'; +import CommonScenarioValidator from './common-scenario-validator.js'; if (!globalThis.crypto) { // @ts-ignore @@ -45,7 +44,6 @@ if (!globalThis.crypto) { describe('http api', function () { let httpApi: HttpApi; - let server: Server; let alice: Persona; let registrationManager: RegistrationManager; let dwn: Dwn; @@ -53,7 +51,7 @@ describe('http api', function () { before(async function () { clock = useFakeTimers({ shouldAdvanceTime: true }); - + // TODO: Remove direct use of default config to avoid changes bleed/pollute between tests - https://github.com/TBD54566975/dwn-server/issues/144 config.registrationStoreUrl = 'sqlite://'; config.registrationProofOfWorkEnabled = true; config.termsOfServiceFilePath = './tests/fixtures/terms-of-service.txt'; @@ -67,20 +65,21 @@ describe('http api', function () { dwn = await getTestDwn({ tenantGate: registrationManager }); - httpApi = new HttpApi(config, dwn, registrationManager); + httpApi = await HttpApi.create(config, dwn, registrationManager); - alice = await TestDataGenerator.generateDidKeyPersona(); - await registrationManager.recordTenantRegistration({ did: alice.did, termsOfServiceHash: registrationManager.getTermsOfServiceHash()}); }); beforeEach(async function () { sinon.restore(); - server = await httpApi.start(3000); + await httpApi.start(3000); + + // generate a new persona for each test to avoid state pollution + alice = await TestDataGenerator.generateDidKeyPersona(); + await registrationManager.recordTenantRegistration({ did: alice.did, termsOfServiceHash: registrationManager.getTermsOfServiceHash()}); }); afterEach(async function () { - server.close(); - server.closeAllConnections(); + await httpApi.close(); }); after(function () { @@ -198,44 +197,14 @@ describe('http api', function () { }); }); - describe('RecordsWrite', function () { - it('handles RecordsWrite with request body', async function () { - const filePath = './fixtures/test.jpeg'; - const { cid, size, stream } = await getFileAsReadStream(filePath); - - const { recordsWrite } = await createRecordsWriteMessage(alice, { - dataCid: cid, - dataSize: size, - }); - - const requestId = uuidv4(); - const dwnRequest = createJsonRpcRequest(requestId, 'dwn.processMessage', { - message: recordsWrite.toJSON(), - target: alice.did, - }); - - const resp = await streamHttpRequest( - 'http://localhost:3000', - { - method: 'POST', - headers: { - 'content-type': 'application/octet-stream', - 'dwn-request': JSON.stringify(dwnRequest), - }, - }, - stream, - ); - - expect(resp.status).to.equal(200); - - const body = JSON.parse(resp.body) as JsonRpcResponse; - expect(body.id).to.equal(requestId); - expect(body.error).to.not.exist; - - const { reply } = body.result; - expect(reply.status.code).to.equal(202); + describe('P0 Scenarios', function () { + it('should be able to read and write a protocol record', async function () { + const dwnUrl = `${config.baseUrl}:${config.port}`; + await CommonScenarioValidator.sanityTestDwnReadWrite(dwnUrl, alice) }); + }); + describe('RecordsWrite', function () { it('handles RecordsWrite overwrite that does not mutate data', async function () { // First RecordsWrite that creates the record. const { recordsWrite: initialWrite, dataStream } = @@ -331,8 +300,8 @@ describe('http api', function () { }); }); - describe('RecordsRead', function () { - it('returns message in response header and data in body', async function () { + describe('/:did/records/:id', function () { + it('returns record data if record is published', async function () { const filePath = './fixtures/test.jpeg'; const { cid: expectedCid, @@ -343,10 +312,11 @@ describe('http api', function () { const { recordsWrite } = await createRecordsWriteMessage(alice, { dataCid: expectedCid, dataSize: size, + published: true, }); - let requestId = uuidv4(); - let dwnRequest = createJsonRpcRequest(requestId, 'dwn.processMessage', { + const requestId = uuidv4(); + const dwnRequest = createJsonRpcRequest(requestId, 'dwn.processMessage', { message: recordsWrite.toJSON(), target: alice.did, }); @@ -368,53 +338,85 @@ describe('http api', function () { const { reply } = body.result; expect(reply.status.code).to.equal(202); - const recordsRead = await RecordsRead.create({ - signer: alice.signer, - filter: { - recordId: recordsWrite.message.recordId, - }, + response = await fetch( + `http://localhost:3000/${alice.did}/records/${recordsWrite.message.recordId}`, + ); + const blob = await response.blob(); + + expect(blob.size).to.equal(size); + }); + + it('returns a 404 if an unpublished record is requested', async function () { + const filePath = './fixtures/test.jpeg'; + const { + cid: expectedCid, + size, + stream, + } = await getFileAsReadStream(filePath); + + const { recordsWrite } = await createRecordsWriteMessage(alice, { + dataCid: expectedCid, + dataSize: size, }); - requestId = uuidv4(); - dwnRequest = createJsonRpcRequest(requestId, 'dwn.processMessage', { + const requestId = uuidv4(); + const dwnRequest = createJsonRpcRequest(requestId, 'dwn.processMessage', { + message: recordsWrite.toJSON(), target: alice.did, - message: recordsRead.toJSON(), }); - response = await fetch('http://localhost:3000', { + let response = await fetch('http://localhost:3000', { method: 'POST', headers: { 'dwn-request': JSON.stringify(dwnRequest), }, + body: stream, }); expect(response.status).to.equal(200); - const { headers } = response; + const body = (await response.json()) as JsonRpcResponse; + expect(body.id).to.equal(requestId); + expect(body.error).to.not.exist; - const contentType = headers.get('content-type'); - expect(contentType).to.not.be.undefined; - expect(contentType).to.equal('application/octet-stream'); + const { reply } = body.result; + expect(reply.status.code).to.equal(202); - const dwnResponse = headers.get('dwn-response'); - expect(dwnResponse).to.not.be.undefined; + response = await fetch( + `http://localhost:3000/${alice.did}/records/${recordsWrite.message.recordId}`, + ); - const jsonRpcResponse = JSON.parse(dwnResponse) as JsonRpcResponse; + expect(response.status).to.equal(404); + }); + + it('returns a 404 if record does not exist', async function () { + const { recordsWrite } = await createRecordsWriteMessage(alice); + + const response = await fetch( + `http://localhost:3000/${alice.did}/records/${recordsWrite.message.recordId}`, + ); + expect(response.status).to.equal(404); + }); - expect(jsonRpcResponse.id).to.equal(requestId); - expect(jsonRpcResponse.error).to.not.exist; + it('returns a 404 for invalid or unauthorized did', async function () { + const unauthorized = await TestDataGenerator.generateDidKeyPersona(); + const { recordsWrite } = await createRecordsWriteMessage(unauthorized); - const { reply: recordsReadReply } = jsonRpcResponse.result; - expect(recordsReadReply.status.code).to.equal(200); - expect(recordsReadReply.record).to.exist; + const response = await fetch( + `http://localhost:3000/${unauthorized.did}/records/${recordsWrite.message.recordId}`, + ); + expect(response.status).to.equal(404); + }); - // can't get response as stream from supertest :( - const cid = await Cid.computeDagPbCidFromStream(response.body as any); - expect(cid).to.equal(expectedCid); + it('returns a 404 for invalid record id', async function () { + const response = await fetch( + `http://localhost:3000/${alice.did}/records/kaka`, + ); + expect(response.status).to.equal(404); }); }); - describe('/:did/records/:id', function () { + describe('/:did/read/records/:id', function () { it('returns record data if record is published', async function () { const filePath = './fixtures/test.jpeg'; const { @@ -453,7 +455,7 @@ describe('http api', function () { expect(reply.status.code).to.equal(202); response = await fetch( - `http://localhost:3000/${alice.did}/records/${recordsWrite.message.recordId}`, + `http://localhost:3000/${alice.did}/read/records/${recordsWrite.message.recordId}`, ); const blob = await response.blob(); @@ -497,7 +499,7 @@ describe('http api', function () { expect(reply.status.code).to.equal(202); response = await fetch( - `http://localhost:3000/${alice.did}/records/${recordsWrite.message.recordId}`, + `http://localhost:3000/${alice.did}/read/records/${recordsWrite.message.recordId}`, ); expect(response.status).to.equal(404); @@ -507,7 +509,7 @@ describe('http api', function () { const { recordsWrite } = await createRecordsWriteMessage(alice); const response = await fetch( - `http://localhost:3000/${alice.did}/records/${recordsWrite.message.recordId}`, + `http://localhost:3000/${alice.did}/read/records/${recordsWrite.message.recordId}`, ); expect(response.status).to.equal(404); }); @@ -517,19 +519,349 @@ describe('http api', function () { const { recordsWrite } = await createRecordsWriteMessage(unauthorized); const response = await fetch( - `http://localhost:3000/${unauthorized.did}/records/${recordsWrite.message.recordId}`, + `http://localhost:3000/${unauthorized.did}/read/records/${recordsWrite.message.recordId}`, ); expect(response.status).to.equal(404); }); it('returns a 404 for invalid record id', async function () { const response = await fetch( - `http://localhost:3000/${alice.did}/records/kaka`, + `http://localhost:3000/${alice.did}/read/records/kaka`, ); expect(response.status).to.equal(404); }); }); + describe('/:did/read/protocols/:protocol', function () { + it('returns protocol definition if protocol is published', async function () { + // Create and publish a protocol + const protocolConfigure = await ProtocolsConfigure.create({ + definition : { + protocol : 'http://example.com/protocol', + published : true, + types : { + foo: {}, + }, + structure: { + foo: {} + } + }, + signer : alice.signer, + }); + + const requestId = uuidv4(); + const dwnRequest = createJsonRpcRequest(requestId, 'dwn.processMessage', { + message: protocolConfigure.toJSON(), + target: alice.did, + }); + + const response = await fetch('http://localhost:3000', { + method: 'POST', + headers: { + 'dwn-request': JSON.stringify(dwnRequest), + }, + }); + expect(response.status).to.equal(200); + + + // Fetch the protocol definition using the HTTP API + const urlEncodedProtocol = encodeURIComponent(protocolConfigure.message.descriptor.definition.protocol); + const protocolUrl = `http://localhost:3000/${alice.did}/read/protocols/${urlEncodedProtocol}`; + const protocolQueryResponse = await fetch(protocolUrl); + expect(protocolQueryResponse.status).to.equal(200); + + // get the JSON response + const protocolConfigureReply = await protocolQueryResponse.json() as ProtocolsConfigureMessage; + expect(protocolConfigureReply.descriptor).to.deep.equal(protocolConfigure.message.descriptor); + }); + + it('returns a 404 if protocol is not published', async function () { + // Create a not-published protocol + const protocolConfigure = await ProtocolsConfigure.create({ + definition : { + protocol : 'http://example.com/protocol', + published : false, + types : { + foo: {}, + }, + structure: { + foo: {} + } + }, + signer : alice.signer, + }); + + const requestId = uuidv4(); + const dwnRequest = createJsonRpcRequest(requestId, 'dwn.processMessage', { + message: protocolConfigure.toJSON(), + target: alice.did, + }); + + const response = await fetch('http://localhost:3000', { + method: 'POST', + headers: { + 'dwn-request': JSON.stringify(dwnRequest), + }, + }); + expect(response.status).to.equal(200); + + + // Fetch the protocol definition using the HTTP API + const urlEncodedProtocol = encodeURIComponent(protocolConfigure.message.descriptor.definition.protocol); + const protocolUrl = `http://localhost:3000/${alice.did}/read/protocols/${urlEncodedProtocol}`; + const protocolQueryResponse = await fetch(protocolUrl); + expect(protocolQueryResponse.status).to.equal(404); + }); + }); + + describe('/:did/query/protocols', function () { + it('returns protocol definition if protocol is published', async function () { + // create two protocol definitions, one published and one not + const protocolConfigurePublished = await ProtocolsConfigure.create({ + definition : { + protocol : 'http://example.com/protocol', + published : true, + types : { + foo: {}, + }, + structure: { + foo: {} + } + }, + signer : alice.signer, + }); + + const requestId = uuidv4(); + const dwnRequest = createJsonRpcRequest(requestId, 'dwn.processMessage', { + message: protocolConfigurePublished.toJSON(), + target: alice.did, + }); + + const response = await fetch('http://localhost:3000', { + method: 'POST', + headers: { + 'dwn-request': JSON.stringify(dwnRequest), + }, + }); + expect(response.status).to.equal(200); + + const protocolConfigureNotPublished = await ProtocolsConfigure.create({ + definition : { + protocol : 'http://example.com/protocol2', + published : false, + types : { + foo: {}, + }, + structure: { + foo: {} + } + }, + signer : alice.signer, + }); + + const requestId2 = uuidv4(); + const dwnRequest2 = createJsonRpcRequest(requestId2, 'dwn.processMessage', { + message: protocolConfigureNotPublished.toJSON(), + target: alice.did, + }); + + const response2 = await fetch('http://localhost:3000', { + method: 'POST', + headers: { + 'dwn-request': JSON.stringify(dwnRequest2), + }, + }); + + expect(response2.status).to.equal(200); + + // now query for a list of protocols + const protocolQueryUrl = `http://localhost:3000/${alice.did}/query/protocols`; + const protocolQueryResponse = await fetch(protocolQueryUrl); + expect(protocolQueryResponse.status).to.equal(200); + + // get the JSON response + const protocolQueryReply = await protocolQueryResponse.json() as ProtocolsConfigureMessage[]; + expect(protocolQueryReply).to.have.lengthOf(1); + + // check that the published protocol is returned + expect(protocolQueryReply[0].descriptor).to.deep.equal(protocolConfigurePublished.message.descriptor); + }); + }); + + describe('/:did/read/protocols/:protocol/*', function () { + it('returns record for a given protocol and protocolPath that is published', async function () { + // Create and publish a protocol + const protocolConfigure = await ProtocolsConfigure.create({ + definition : { + protocol : 'http://example.com/protocol', + published : true, + types : { + foo: {}, + }, + structure: { + foo: {} + } + }, + signer : alice.signer, + }); + + const requestId = uuidv4(); + const dwnRequest = createJsonRpcRequest(requestId, 'dwn.processMessage', { + message: protocolConfigure.toJSON(), + target: alice.did, + }); + + const response = await fetch('http://localhost:3000', { + method: 'POST', + headers: { + 'dwn-request': JSON.stringify(dwnRequest), + }, + }); + expect(response.status).to.equal(200); + + // Create a foo record + const filePath = './fixtures/test.jpeg'; + const { + cid: expectedCid, + size, + stream, + } = await getFileAsReadStream(filePath); + + const { recordsWrite } = await createRecordsWriteMessage(alice, { + dataCid : expectedCid, + dataSize : size, + published : true, + protocol : protocolConfigure.message.descriptor.definition.protocol, + protocolPath : 'foo', + }); + + const recordsWriteRequestId = uuidv4(); + const recordsWriteDwnRequest = createJsonRpcRequest(recordsWriteRequestId, 'dwn.processMessage', { + message: recordsWrite.toJSON(), + target: alice.did, + }); + + const recordsWriteResponse = await fetch('http://localhost:3000', { + method: 'POST', + headers: { + 'dwn-request': JSON.stringify(recordsWriteDwnRequest), + }, + body: stream, + }); + expect(recordsWriteResponse.status).to.equal(200); + const responseJson = await recordsWriteResponse.json() as JsonRpcResponse; + expect(responseJson.result.reply.status.code).to.equal(202); + + // Fetch the record using the HTTP API + const urlEncodedProtocol = encodeURIComponent(protocolConfigure.message.descriptor.definition.protocol); + const protocolUrl = `http://localhost:3000/${alice.did}/read/protocols/${urlEncodedProtocol}/foo`; + const recordReadResponse = await fetch(protocolUrl); + expect(recordReadResponse.status).to.equal(200); + + // get the data response + const blob = await recordReadResponse.blob(); + expect(blob.size).to.equal(size); + + // get dwn message response + const { status, record } = getDwnResponse(recordReadResponse); + expect(status.code).to.equal(200); + expect(record).to.exist; + expect(record.recordId).to.equal(recordsWrite.message.recordId); + }); + + it('removes the trailing slash from the protocol path', async function () { + const recordsQueryCreateSpy = sinon.spy(RecordsQuery, 'create'); + + const urlEncodedProtocol = encodeURIComponent('http://example.com/protocol'); + const protocolUrl = `http://localhost:3000/${alice.did}/read/protocols/${urlEncodedProtocol}/foo/`; // trailing slash + const recordReadResponse = await fetch(protocolUrl); + expect(recordReadResponse.status).to.equal(404); + + expect(recordsQueryCreateSpy.calledOnce).to.be.true; + const recordsQueryFilter = recordsQueryCreateSpy.getCall(0).args[0].filter; + expect(recordsQueryFilter.protocolPath).to.equal('foo'); + }); + + it('returns a 404 if record for a given protocol and protocolPath is not published', async function () { + // Create and publish a protocol + const protocolConfigure = await ProtocolsConfigure.create({ + definition : { + protocol : 'http://example.com/protocol', + published : true, + types : { + foo: {}, + }, + structure: { + foo: {} + } + }, + signer : alice.signer, + }); + + const requestId = uuidv4(); + const dwnRequest = createJsonRpcRequest(requestId, 'dwn.processMessage', { + message: protocolConfigure.toJSON(), + target: alice.did, + }); + + const response = await fetch('http://localhost:3000', { + method: 'POST', + headers: { + 'dwn-request': JSON.stringify(dwnRequest), + }, + }); + expect(response.status).to.equal(200); + + // Create a foo record + const filePath = './fixtures/test.jpeg'; + const { + cid: expectedCid, + size, + stream, + } = await getFileAsReadStream(filePath); + + const { recordsWrite } = await createRecordsWriteMessage(alice, { + dataCid : expectedCid, + dataSize : size, + published : false, // not published + protocol : protocolConfigure.message.descriptor.definition.protocol, + protocolPath : 'foo', + }); + + const recordsWriteRequestId = uuidv4(); + const recordsWriteDwnRequest = createJsonRpcRequest(recordsWriteRequestId, 'dwn.processMessage', { + message: recordsWrite.toJSON(), + target: alice.did, + }); + + const recordsWriteResponse = await fetch('http://localhost:3000', { + method: 'POST', + headers: { + 'dwn-request': JSON.stringify(recordsWriteDwnRequest), + }, + body: stream, + }); + expect(recordsWriteResponse.status).to.equal(200); + const responseJson = await recordsWriteResponse.json() as JsonRpcResponse; + expect(responseJson.result.reply.status.code).to.equal(202); + + // Fetch the record using the HTTP API + const urlEncodedProtocol = encodeURIComponent(protocolConfigure.message.descriptor.definition.protocol); + const protocolUrl = `http://localhost:3000/${alice.did}/read/protocols/${urlEncodedProtocol}/foo`; + const recordReadResponse = await fetch(protocolUrl); + expect(recordReadResponse.status).to.equal(404); + }); + + it('returns a 400 if protocol path is not provided', async function () { + // Fetch a protocol record without providing a protocol path + const urlEncodedProtocol = encodeURIComponent('http://example.com/protocol'); + const protocolUrl = `http://localhost:3000/${alice.did}/read/protocols/${urlEncodedProtocol}/`; // missing protocol path + const recordReadResponse = await fetch(protocolUrl); + expect(recordReadResponse.status).to.equal(400); + expect(await recordReadResponse.text()).to.equal('protocol path is required'); + }); + }); + describe('/:did/query', function () { it('returns record data if record is published', async function () { const filePath = './fixtures/test.jpeg'; @@ -592,6 +924,7 @@ describe('http api', function () { expect(resp.status).to.equal(200); const info = await resp.json(); + expect(info['url']).to.equal('http://localhost'); expect(info['server']).to.equal('@web5/dwn-server'); expect(info['registrationRequirements']).to.include('terms-of-service'); expect(info['registrationRequirements']).to.include( @@ -609,12 +942,11 @@ describe('http api', function () { // start server without websocket support enabled - server.close(); - server.closeAllConnections(); + await httpApi.close(); config.webSocketSupport = false; - httpApi = new HttpApi(config, dwn, registrationManager); - server = await httpApi.start(3000); + httpApi = await HttpApi.create(config, dwn, registrationManager); + await httpApi.start(3000); resp = await fetch(`http://localhost:3000/info`); expect(resp.status).to.equal(200); @@ -628,8 +960,7 @@ describe('http api', function () { }); it('verify /info still returns when package.json file does not exist', async function () { - server.close(); - server.closeAllConnections(); + await httpApi.close(); // set up spy to check for an error log by the server const logSpy = sinon.spy(log, 'error'); @@ -637,8 +968,8 @@ describe('http api', function () { // 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); + httpApi = await HttpApi.create(config, dwn, registrationManager); + await httpApi.start(3000); const resp = await fetch(`http://localhost:3000/info`); const info = await resp.json(); @@ -660,14 +991,13 @@ describe('http api', function () { }); it('verify /info returns server name from config', async function () { - server.close(); - server.closeAllConnections(); + await httpApi.close(); // 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); + httpApi = await HttpApi.create(config, dwn, registrationManager); + await httpApi.start(3000); const resp = await fetch(`http://localhost:3000/info`); const info = await resp.json(); diff --git a/tests/plugins/data-store-sqlite.ts b/tests/plugins/data-store-sqlite.ts new file mode 100644 index 0000000..9f46c68 --- /dev/null +++ b/tests/plugins/data-store-sqlite.ts @@ -0,0 +1,26 @@ +import type { DataStore } from "@tbd54566975/dwn-sdk-js"; +import { DataStoreSql } from "@tbd54566975/dwn-sql-store"; +import { getDialectFromUrl } from "../../src/storage.js"; + +/** + * An example of a plugin that is used for testing. + * The points to note are: + * - The class must be a default export. + * - The constructor must not take any arguments. + */ +export default class DataStoreSqlite extends DataStoreSql implements DataStore { + constructor() { + const sqliteDialect = getDialectFromUrl(new URL('sqlite://')); + super(sqliteDialect); + + // NOTE: the following line is added purely to test the constructor invocation. + DataStoreSqlite.spyingTheConstructor(); + } + + /** + * NOTE: This method is introduced purely to indirectly test/spy invocation of the constructor. + * As I was unable to find an easy way to directly spy the constructor. + */ + public static spyingTheConstructor(): void { + } +} \ No newline at end of file diff --git a/tests/plugins/event-log-sqlite.ts b/tests/plugins/event-log-sqlite.ts new file mode 100644 index 0000000..c3078b1 --- /dev/null +++ b/tests/plugins/event-log-sqlite.ts @@ -0,0 +1,26 @@ +import type { EventLog } from "@tbd54566975/dwn-sdk-js"; +import { EventLogSql } from "@tbd54566975/dwn-sql-store"; +import { getDialectFromUrl } from "../../src/storage.js"; + +/** + * An example of a plugin. Used for testing. + * The points to note are: + * - The class must be a default export. + * - The constructor must not take any arguments. + */ +export default class EventLogSqlite extends EventLogSql implements EventLog { + constructor() { + const sqliteDialect = getDialectFromUrl(new URL('sqlite://')); + super(sqliteDialect); + + // NOTE: the following line is added purely to test the constructor invocation. + EventLogSqlite.spyingTheConstructor(); + } + + /** + * NOTE: This method is introduced purely to indirectly test/spy invocation of the constructor. + * As I was unable to find an easy way to directly spy the constructor. + */ + public static spyingTheConstructor(): void { + } +} \ No newline at end of file diff --git a/tests/plugins/event-stream-in-memory.ts b/tests/plugins/event-stream-in-memory.ts new file mode 100644 index 0000000..7653ed5 --- /dev/null +++ b/tests/plugins/event-stream-in-memory.ts @@ -0,0 +1,24 @@ +import type { EventStream } from "@tbd54566975/dwn-sdk-js"; +import { EventEmitterStream } from "@tbd54566975/dwn-sdk-js"; + +/** + * An example of a plugin that is used for testing. + * The points to note are: + * - The class must be a default export. + * - The constructor must not take any arguments. + */ +export default class EventStreamInMemory extends EventEmitterStream implements EventStream { + constructor() { + super(); + + // NOTE: the following line is added purely to test the constructor invocation. + EventStreamInMemory.spyingTheConstructor(); + } + + /** + * NOTE: This method is introduced purely to indirectly test/spy invocation of the constructor. + * As I was unable to find an easy way to directly spy the constructor. + */ + public static spyingTheConstructor(): void { + } +} \ No newline at end of file diff --git a/tests/plugins/message-store-sqlite.ts b/tests/plugins/message-store-sqlite.ts new file mode 100644 index 0000000..7cb65c6 --- /dev/null +++ b/tests/plugins/message-store-sqlite.ts @@ -0,0 +1,26 @@ +import type { MessageStore } from "@tbd54566975/dwn-sdk-js"; +import { MessageStoreSql } from "@tbd54566975/dwn-sql-store"; +import { getDialectFromUrl } from "../../src/storage.js"; + +/** + * An example of a plugin. Used for testing. + * The points to note are: + * - The class must be a default export. + * - The constructor must not take any arguments. + */ +export default class MessageStoreSqlite extends MessageStoreSql implements MessageStore { + constructor() { + const sqliteDialect = getDialectFromUrl(new URL('sqlite://')); + super(sqliteDialect); + + // NOTE: the following line is added purely to test the constructor invocation. + MessageStoreSqlite.spyingTheConstructor(); + } + + /** + * NOTE: This method is introduced purely to indirectly test/spy invocation of the constructor. + * As I was unable to find an easy way to directly spy the constructor. + */ + public static spyingTheConstructor(): void { + } +} \ No newline at end of file diff --git a/tests/plugins/resumable-task-store-sqlite.ts b/tests/plugins/resumable-task-store-sqlite.ts new file mode 100644 index 0000000..474e8a7 --- /dev/null +++ b/tests/plugins/resumable-task-store-sqlite.ts @@ -0,0 +1,26 @@ +import type { ResumableTaskStore } from "@tbd54566975/dwn-sdk-js"; +import { ResumableTaskStoreSql } from "@tbd54566975/dwn-sql-store"; +import { getDialectFromUrl } from "../../src/storage.js"; + +/** + * An example of a plugin that is used for testing. + * The points to note are: + * - The class must be a default export. + * - The constructor must not take any arguments. + */ +export default class ResumableTaskStoreSqlite extends ResumableTaskStoreSql implements ResumableTaskStore { + constructor() { + const sqliteDialect = getDialectFromUrl(new URL('sqlite://')); + super(sqliteDialect); + + // NOTE: the following line is added purely to test the constructor invocation. + ResumableTaskStoreSqlite.spyingTheConstructor(); + } + + /** + * NOTE: This method is introduced purely to indirectly test/spy invocation of the constructor. + * As I was unable to find an easy way to directly spy the constructor. + */ + public static spyingTheConstructor(): void { + } +} \ No newline at end of file diff --git a/tests/process-handler.spec.ts b/tests/process-handler.spec.ts index 97dfe4d..082c87c 100644 --- a/tests/process-handler.spec.ts +++ b/tests/process-handler.spec.ts @@ -1,49 +1,76 @@ -import { expect } from 'chai'; -import sinon from 'sinon'; +import type { Dwn } from '@tbd54566975/dwn-sdk-js'; +import sinon from 'sinon'; import { config } from '../src/config.js'; import { DwnServer } from '../src/dwn-server.js'; +import { expect } from 'chai'; import { getTestDwn } from './test-dwn.js'; +import { Poller } from '@tbd54566975/dwn-sdk-js'; describe('Process Handlers', function () { + let dwn: Dwn; let dwnServer: DwnServer; let processExitStub: sinon.SinonStub; beforeEach(async function () { - const testDwn = await getTestDwn(); - dwnServer = new DwnServer({ dwn: testDwn, config: config }); + const dwn = await getTestDwn(); + dwnServer = new DwnServer({ dwn, config: config }); await dwnServer.start(); processExitStub = sinon.stub(process, 'exit'); }); - afterEach(async function () { - dwnServer.stop(() => console.log('server stop in Process Handlers tests')); - process.removeAllListeners('SIGINT'); - process.removeAllListeners('SIGTERM'); - process.removeAllListeners('uncaughtException'); + afterEach(async () => { + await dwnServer.stop(); processExitStub.restore(); }); + it('should stop when SIGINT is emitted', async function () { process.emit('SIGINT'); - expect(dwnServer.httpServer.listening).to.be.false; - expect(processExitStub.called).to.be.false; // Ensure process.exit is not called + + Poller.pollUntilSuccessOrTimeout(async () => { + expect(dwnServer.httpServer.listening).to.be.false; + expect(processExitStub.called).to.be.false; // Ensure process.exit is not called + }); }); it('should stop when SIGTERM is emitted', async function () { process.emit('SIGTERM'); - expect(dwnServer.httpServer.listening).to.be.false; - expect(processExitStub.called).to.be.false; // Ensure process.exit is not called + + Poller.pollUntilSuccessOrTimeout(async () => { + expect(dwnServer.httpServer.listening).to.be.false; + expect(processExitStub.called).to.be.false; // Ensure process.exit is not called + }); }); - it('should log an error for an uncaught exception', function () { + it('should log an error for an uncaught exception', async () => { + + // IMPORTANT: this test is a bit tricky to write because + // existing process `uncaughtException` listener/handler will result will trigger an error when we force an `uncaughtException` event + // causing the test to fail. So we need to remove the existing listener and add them back after the test. + // To be in full control of the test, we also create the DWN server (which adds it's own `uncaughtException` listener) + // AFTER removing the existing listener. + await dwnServer.stop(); + + // storing then removing existing listeners and adding back at the very end of the test + const existingUncaughtExceptionListeners = [...process.listeners('uncaughtException')]; + process.removeAllListeners('uncaughtException'); + + dwnServer = new DwnServer({ dwn, config: config }); + await dwnServer.start(); + const consoleErrorStub = sinon.stub(console, 'error'); // Stub console.error const errorMessage = 'Test uncaught exception'; const error = new Error(errorMessage); process.emit('uncaughtException', error); + // Ensure console.error was called with the expected error message + console.log('console.error call count', consoleErrorStub.callCount); expect(consoleErrorStub.calledOnce).to.be.true; // Restore the original console.error consoleErrorStub.restore(); + + // add back original listeners + existingUncaughtExceptionListeners.forEach(listener => process.on('uncaughtException', listener)); }); }); diff --git a/tests/scenarios/dynamic-plugin-loading.spec.ts b/tests/scenarios/dynamic-plugin-loading.spec.ts new file mode 100644 index 0000000..6bdbb36 --- /dev/null +++ b/tests/scenarios/dynamic-plugin-loading.spec.ts @@ -0,0 +1,88 @@ +import chaiAsPromised from 'chai-as-promised'; +import chai, { expect } from 'chai'; +import DataStoreSqlite from '../plugins/data-store-sqlite.js'; +import EventLogSqlite from '../plugins/event-log-sqlite.js'; +import EventStreamInMemory from '../plugins/event-stream-in-memory.js'; +import sinon from 'sinon'; + +import { config } from '../../src/config.js'; +import { DwnServer } from '../../src/dwn-server.js'; + +import { DidDht, DidKey, UniversalResolver } from '@web5/dids'; +import CommonScenarioValidator from '../common-scenario-validator.js'; +import MessageStoreSqlite from '../plugins/message-store-sqlite.js'; +import ResumableTaskStoreSqlite from '../plugins/resumable-task-store-sqlite.js'; + +// node.js 18 and earlier needs globalThis.crypto polyfill +if (!globalThis.crypto) { + // @ts-ignore + globalThis.crypto = webcrypto; +} + +chai.use(chaiAsPromised); + +describe('Dynamic DWN plugin loading', function () { + let dwnServer: DwnServer; + + afterEach(async () => { + if (dwnServer !== undefined) { + await dwnServer.stop(); + } + }); + + it('should fail dynamically loading a non-existent plugin', async () => { + const dwnServerConfigCopy = { ...config }; // not touching the original config + dwnServerConfigCopy.dataStore = './non-existent-plugin.js'; + + const invalidDwnServer = new DwnServer({ config: dwnServerConfigCopy }); + await expect(invalidDwnServer.start()).to.be.rejectedWith('Failed to load component at ./non-existent-plugin.js'); + }); + + it('should be able to dynamically load and use custom data store implementation', async () => { + // Scenario: + // 1. Configure DWN to load a custom data store plugin. + // 2. Validate that the constructor of the plugin is called. + // 3. Validate that the DWN instance is using the custom data store plugin. + + // NOTE: was not able to spy on constructor directly, so spying on a method that is called in the constructor + const customMessageStoreConstructorSpy = sinon.spy(MessageStoreSqlite, 'spyingTheConstructor'); + const customDataStoreConstructorSpy = sinon.spy(DataStoreSqlite, 'spyingTheConstructor'); + const customResumableTaskStoreConstructorSpy = sinon.spy(ResumableTaskStoreSqlite, 'spyingTheConstructor'); + const customEventLogConstructorSpy = sinon.spy(EventLogSqlite, 'spyingTheConstructor'); + const customEventStreamConstructorSpy = sinon.spy(EventStreamInMemory, 'spyingTheConstructor'); + + // 1. Configure DWN to load a custom data store plugin. + const dwnServerConfigCopy = { ...config }; // not touching the original config + + // TODO: remove below after https://github.com/TBD54566975/dwn-server/issues/144 is resolved + // The default config is not reliable because other tests modify it. + dwnServerConfigCopy.registrationStoreUrl = undefined; // allow all traffic + + dwnServerConfigCopy.messageStore = '../tests/plugins/message-store-sqlite.js'; + dwnServerConfigCopy.dataStore = '../tests/plugins/data-store-sqlite.js'; + dwnServerConfigCopy.resumableTaskStore = '../tests/plugins/resumable-task-store-sqlite.js'; + dwnServerConfigCopy.eventLog = '../tests/plugins/event-log-sqlite.js'; + dwnServerConfigCopy.eventStreamPluginPath = '../tests/plugins/event-stream-in-memory.js'; + + // 2. Validate that the constructor of the plugin is called. + // CRITICAL: We need to create a custom DID resolver that does not use a LevelDB based cache (which is the default cache used in `DWN`) + // otherwise we will receive a `Database is not open` coming from LevelDB. + // This is likely due to the fact that LevelDB is the default cache used in `DWN`, and we have tests creating default DWN instances, + // so here we have to create a DWN that does not use the same LevelDB cache to avoid hitting LevelDB locked issues. + // Long term we should investigate and unify approach of DWN instantiation taken by tests to avoid this "workaround" entirely. + const didResolver = new UniversalResolver({ + didResolvers : [DidDht, DidKey], + }); + dwnServer = new DwnServer({ config: dwnServerConfigCopy, didResolver }); + await dwnServer.start(); + expect(customMessageStoreConstructorSpy.calledOnce).to.be.true; + expect(customDataStoreConstructorSpy.calledOnce).to.be.true; + expect(customResumableTaskStoreConstructorSpy.calledOnce).to.be.true; + expect(customEventLogConstructorSpy.calledOnce).to.be.true; + expect(customEventStreamConstructorSpy.calledOnce).to.be.true; + + // 3. Validate that the DWN instance is using the custom data store plugin. + const dwnUrl = `${dwnServerConfigCopy.baseUrl}:${dwnServerConfigCopy.port}`; + await CommonScenarioValidator.sanityTestDwnReadWrite(dwnUrl); + }); +}); diff --git a/tests/scenarios/registration.spec.ts b/tests/scenarios/registration.spec.ts index df3e9c7..aa8e032 100644 --- a/tests/scenarios/registration.spec.ts +++ b/tests/scenarios/registration.spec.ts @@ -1,35 +1,28 @@ -// node.js 18 and earlier, needs globalThis.crypto polyfill -import { DataStream, TestDataGenerator } from '@tbd54566975/dwn-sdk-js'; +import type { DwnServerConfig } from '../../src/config.js'; import type { Persona } from '@tbd54566975/dwn-sdk-js'; +import type { ProofOfWorkChallengeModel } from '../../src/registration/proof-of-work-types.js'; +import type { RegistrationManager } from '../../src/registration/registration-manager.js'; +import type { JsonRpcRequest, JsonRpcResponse } from '../../src/lib/json-rpc.js'; +import type { RegistrationData, RegistrationRequest } from '../../src/registration/registration-types.js'; -import { expect } from 'chai'; -import { readFileSync } from 'fs'; import fetch from 'node-fetch'; -import { webcrypto } from 'node:crypto'; -import { useFakeTimers } from 'sinon'; -import { v4 as uuidv4 } from 'uuid'; - -import type { DwnServerConfig } from '../../src/config.js'; import { config } from '../../src/config.js'; -import type { - JsonRpcRequest, - JsonRpcResponse, -} from '../../src/lib/json-rpc.js'; -import { - createJsonRpcRequest, -} from '../../src/lib/json-rpc.js'; -import { ProofOfWork } from '../../src/registration/proof-of-work.js'; -import { - createRecordsWriteMessage, -} from '../utils.js'; -import type { ProofOfWorkChallengeModel } from '../../src/registration/proof-of-work-types.js'; -import type { RegistrationData, RegistrationRequest } from '../../src/registration/registration-types.js'; -import type { RegistrationManager } from '../../src/registration/registration-manager.js'; +import { createJsonRpcRequest} from '../../src/lib/json-rpc.js'; +import { createRecordsWriteMessage } from '../utils.js'; +import { DwnServer } from '../../src/dwn-server.js'; import { DwnServerErrorCode } from '../../src/dwn-error.js'; +import { expect } from 'chai'; +import { ProofOfWork } from '../../src/registration/proof-of-work.js'; import { ProofOfWorkManager } from '../../src/registration/proof-of-work-manager.js'; -import { DwnServer } from '../../src/dwn-server.js'; import { randomBytes } from 'crypto'; +import { readFileSync } from 'fs'; +import { useFakeTimers } from 'sinon'; +import { v4 as uuidv4 } from 'uuid'; +import { webcrypto } from 'node:crypto'; +import { DataStream, TestDataGenerator } from '@tbd54566975/dwn-sdk-js'; +import { DidDht, DidKey, UniversalResolver } from '@web5/dids'; +// node.js 18 and earlier, needs globalThis.crypto polyfill if (!globalThis.crypto) { // @ts-ignore globalThis.crypto = webcrypto; @@ -41,6 +34,7 @@ describe('Registration scenarios', function () { const proofOfWorkEndpoint = 'http://localhost:3000/registration/proof-of-work'; const registrationEndpoint = 'http://localhost:3000/registration'; + // let didResolverCache = new DidResolverCacheLevel({ location: 'RESOLVERCACHE' }); let alice: Persona; let registrationManager: RegistrationManager; let clock; @@ -56,6 +50,7 @@ describe('Registration scenarios', function () { // and dwn-server.spec.ts already uses LevelDB. dwnServerConfig.messageStore = 'sqlite://', dwnServerConfig.dataStore = 'sqlite://', + dwnServerConfig.resumableTaskStore = 'sqlite://', dwnServerConfig.eventLog = 'sqlite://', // registration config @@ -64,24 +59,24 @@ describe('Registration scenarios', function () { dwnServerConfig.termsOfServiceFilePath = './tests/fixtures/terms-of-service.txt'; dwnServerConfig.registrationProofOfWorkInitialMaxHash = '0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'; // 1 in 16 chance of solving - dwnServer = new DwnServer({ config: dwnServerConfig }); + // CRITICAL: We need to create a custom DID resolver that does not use a LevelDB based cache (which is the default cache used in `DWN`) + // otherwise we will receive a `Database is not open` coming from LevelDB. + // This is likely due to the fact that LevelDB is the default cache used in `DWN`, and we have tests creating default DWN instances, + // so here we have to create a DWN that does not use the same LevelDB cache to avoid hitting LevelDB locked issues. + // Long term we should investigate and unify approach of DWN instantiation taken by tests to avoid this "workaround" entirely. + const didResolver = new UniversalResolver({ + didResolvers : [DidDht, DidKey], + }); + + dwnServer = new DwnServer({ config: dwnServerConfig, didResolver }); await dwnServer.start(); registrationManager = dwnServer.registrationManager; }); - after(function () { - dwnServer.stop(() => { }); + after(async () => { clock.restore(); }); - beforeEach(function () { - dwnServer.start(); - }); - - afterEach(function () { - dwnServer.stop(() => {}); - }); - it('should facilitate tenant registration with terms-of-service and proof-or-work turned on', async () => { // Scenario: // 1. Alice fetches the terms-of-service. @@ -532,13 +527,8 @@ describe('Registration scenarios', function () { }); - /** - * NOTE: The tests below instantiate their own server configs and should should take care to stop the `dwnServer` - * This is done to avoid LevelDB locking for the default `DidResolver` cache. - */ - it('should initialize ProofOfWorkManager with challenge nonce seed if given.', async function () { - dwnServer.stop(() => {}); + await dwnServer.stop(); const registrationProofOfWorkSeed = randomBytes(32).toString('hex'); const configWithProofOfWorkSeed: DwnServerConfig = { @@ -554,7 +544,16 @@ describe('Registration scenarios', function () { }); it('should allow tenant registration to be turned off to allow all DWN messages through.', async () => { - dwnServer.stop(() => {}); + await dwnServer.stop(); + + // CRITICAL: We need to create a custom DID resolver that does not use a LevelDB based cache (which is the default cache used in `DWN`) + // otherwise we will receive a `Database is not open` coming from LevelDB. + // This is likely due to the fact that LevelDB is the default cache used in `DWN`, and we have tests creating default DWN instances, + // so here we have to create a DWN that does not use the same LevelDB cache to avoid hitting LevelDB locked issues. + // Long term we should investigate and unify approach of DWN instantiation taken by tests to avoid this "workaround" entirely. + const didResolver = new UniversalResolver({ + didResolvers : [DidDht, DidKey], + }); // Scenario: // 1. There is a DWN that does not require tenant registration. @@ -566,17 +565,19 @@ describe('Registration scenarios', function () { registrationProofOfWorkEnabled: false, termsOfServiceFilePath: undefined, }; - dwnServer = new DwnServer({ config: configClone }); + dwnServer = new DwnServer({ config: configClone, didResolver }); await dwnServer.start(); const { jsonRpcRequest, dataBytes } = await generateRecordsWriteJsonRpcRequest(alice); - const writeResponse = await fetch(dwnMessageEndpoint, { + + const writeResponse = await fetch('http://localhost:3002', { method: 'POST', headers: { 'dwn-request': JSON.stringify(jsonRpcRequest), }, body: new Blob([dataBytes]), }); + const writeResponseBody = await writeResponse.json() as JsonRpcResponse; expect(writeResponse.status).to.equal(200); expect(writeResponseBody.result.reply.status.code).to.equal(202); diff --git a/tests/scenarios/web5-connect.spec.ts b/tests/scenarios/web5-connect.spec.ts new file mode 100644 index 0000000..3533600 --- /dev/null +++ b/tests/scenarios/web5-connect.spec.ts @@ -0,0 +1,160 @@ +import fetch from 'node-fetch'; +import sinon from 'sinon'; +import { config } from '../../src/config.js'; +import { DwnServer } from '../../src/dwn-server.js'; +import { expect } from 'chai'; +import { Poller } from '@tbd54566975/dwn-sdk-js'; +import { useFakeTimers } from 'sinon'; +import { Web5ConnectServer } from '../../src/web5-connect/web5-connect-server.js'; +import { randomUUID, webcrypto } from 'node:crypto'; + +// node.js 18 and earlier needs globalThis.crypto polyfill +if (!globalThis.crypto) { + // @ts-ignore + globalThis.crypto = webcrypto; +} + +describe('Web5 Connect scenarios', function () { + const web5ConnectBaseUrl = 'http://localhost:3000'; + + let clock: sinon.SinonFakeTimers; + let dwnServer: DwnServer; + const dwnServerConfig = { ...config } // not touching the original config + + before(async function () { + + // NOTE: using SQL to workaround an issue where multiple instances of DwnServer cannot be started using LevelDB in the same test run, + // and dwn-server.spec.ts already uses LevelDB. + dwnServerConfig.messageStore = 'sqlite://', + dwnServerConfig.dataStore = 'sqlite://', + dwnServerConfig.resumableTaskStore = 'sqlite://', + dwnServerConfig.eventLog = 'sqlite://', + + dwnServer = new DwnServer({ config: dwnServerConfig }); + }); + + after(async () => { + await dwnServer.stop(); + }); + + beforeEach(async () => { + sinon.restore(); // wipe all previous stubs/spies/mocks/fakes/clock + + // IMPORTANT: MUST be called AFTER `sinon.restore()` because `sinon.restore()` resets fake timers + clock = useFakeTimers({ shouldAdvanceTime: true }); + await dwnServer.start(); + }); + + afterEach(async () => { + clock.restore(); + await dwnServer.stop(); + }); + + it('should be able to set and get Web5 Connect Request & Response objects', async () => { + // Scenario: + // 1. App sends the Web5 Connect Request object to the Web5 Connect server. + // 2. Identity Provider (wallet) fetches the Web5 Connect Request object from the Web5 Connect server. + // 3. Should receive 404 if fetching the same Web5 Connect Request again + // 4. Identity Provider (wallet) should receive 400 if sending an incomplete response. + // 5. Identity Provider (wallet) sends the Web5 Connect Response object to the Web5 Connect server. + // 6. App fetches the Web5 Connect Response object from the Web5 Connect server. + // 7. Should receive 404 if fetching the same Web5 Connect Response object again. + + // 1. App sends the Web5 Connect Request object to the Web5 Connect server. + const requestBody = { request: { dummyProperty: 'dummyValue' } }; + const postWeb5ConnectRequestResult = await fetch(`${web5ConnectBaseUrl}/connect/par`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(requestBody), + }); + expect(postWeb5ConnectRequestResult.status).to.equal(201); + + // 2. Identity Provider (wallet) fetches the Web5 Connect Request object from the Web5 Connect server. + const requestUrl = (await postWeb5ConnectRequestResult.json() as any).request_uri; + + let getWeb5ConnectRequestResult; + await Poller.pollUntilSuccessOrTimeout(async () => { + console.log('Polling for Web5 Connect Request object...') + getWeb5ConnectRequestResult = await fetch(requestUrl, { method: 'GET' }); + expect(getWeb5ConnectRequestResult.status).to.equal(200); + }); + + const fetchedRequest = await getWeb5ConnectRequestResult.json(); + expect(fetchedRequest).to.deep.equal(requestBody.request); + + // 3. Should receive 404 if fetching the same Web5 Connect Request again + await Poller.pollUntilSuccessOrTimeout(async () => { + const getWeb5ConnectRequestResult2 = await fetch(requestUrl, { method: 'GET' }); + expect(getWeb5ConnectRequestResult2.status).to.equal(404); + }); + + // 4. Identity Provider (wallet) should receive 400 if sending an incomplete response. + const incompleteResponseBody = { + id_token : { dummyToken: 'dummyToken' }, + // state : 'dummyState', // intentionally missing + }; + const postIncompleteWeb5ConnectResponseResult = await fetch(`${web5ConnectBaseUrl}/connect/callback`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(incompleteResponseBody), + }); + expect(postIncompleteWeb5ConnectResponseResult.status).to.equal(400); + + const state = `dummyState-${randomUUID()}`; + // 5. Identity Provider (wallet) sends the Web5 Connect Response object to the Web5 Connect server. + const web5ConnectResponseBody = { + id_token : { dummyToken: 'dummyToken' }, + state + }; + const postWeb5ConnectResponseResult = await fetch(`${web5ConnectBaseUrl}/connect/callback`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(web5ConnectResponseBody), + }); + expect(postWeb5ConnectResponseResult.status).to.equal(201); + + // 6. App fetches the Web5 Connect Response object from the Web5 Connect server. + const web5ConnectResponseUrl = `${web5ConnectBaseUrl}/connect/token/${web5ConnectResponseBody.state}.jwt`; + + let getWeb5ConnectResponseResult; + await Poller.pollUntilSuccessOrTimeout(async () => { + getWeb5ConnectResponseResult = await fetch(web5ConnectResponseUrl, { method: 'GET' }); + expect(getWeb5ConnectResponseResult.status).to.equal(200); + }); + + const fetchedResponse = await getWeb5ConnectResponseResult.json(); + expect(fetchedResponse).to.deep.equal(web5ConnectResponseBody.id_token); + + // 7. Should receive 404 if fetching the same Web5 Connect Response object again. + await Poller.pollUntilSuccessOrTimeout(async () => { + const getWeb5ConnectResponseResult2 = await fetch(web5ConnectResponseUrl, { method: 'GET' }); + expect(getWeb5ConnectResponseResult2.status).to.equal(404); + }); + }); + + it('should clean up objects that are expired', async () => { + // Scenario: + // 1. App sends the Web5 Connect Request object to the Web5 Connect server. + // 2. Time passes and the Web5 Connect Request object is expired. + // 3. Should receive 404 when fetching Web5 Connect Request. + + // 1. App sends the Web5 Connect Request object to the Web5 Connect server. + const requestBody = { request: { dummyProperty: 'dummyValue' } }; + const postWeb5ConnectRequestResult = await fetch(`${web5ConnectBaseUrl}/connect/par`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(requestBody), + }); + expect(postWeb5ConnectRequestResult.status).to.equal(201); + + // 2. Time passes and the Web5 Connect Request object is expired. + await clock.tickAsync(Web5ConnectServer.ttlInSeconds * 1000); + + // 3. Should receive 404 when fetching the expired Web5 Connect Request. + const requestUrl = (await postWeb5ConnectRequestResult.json() as any).request_uri; + const getWeb5ConnectRequestResult = await fetch(requestUrl, { + method: 'GET', + }); + expect(getWeb5ConnectRequestResult.status).to.equal(404); + }); +}); diff --git a/tests/test-dwn.ts b/tests/test-dwn.ts index 27d7744..8ce33d7 100644 --- a/tests/test-dwn.ts +++ b/tests/test-dwn.ts @@ -1,23 +1,25 @@ import type { TenantGate } from '@tbd54566975/dwn-sdk-js'; -import { Dwn, EventEmitterStream } from '@tbd54566975/dwn-sdk-js'; + +import { getDialectFromUrl } from '../src/storage.js'; import { DataStoreSql, EventLogSql, MessageStoreSql, + ResumableTaskStoreSql, } from '@tbd54566975/dwn-sql-store'; - -import { getDialectFromURI } from '../src/storage.js'; import { DidDht, DidIon, DidKey, UniversalResolver } from '@web5/dids'; +import { Dwn, EventEmitterStream } from '@tbd54566975/dwn-sdk-js'; export async function getTestDwn(options: { tenantGate?: TenantGate, withEvents?: boolean, } = {}): Promise { const { tenantGate, withEvents = false } = options; - const db = getDialectFromURI(new URL('sqlite://')); - const dataStore = new DataStoreSql(db); - const eventLog = new EventLogSql(db); - const messageStore = new MessageStoreSql(db); + const dialect = getDialectFromUrl(new URL('sqlite://')); + const dataStore = new DataStoreSql(dialect); + const eventLog = new EventLogSql(dialect); + const messageStore = new MessageStoreSql(dialect); + const resumableTaskStore = new ResumableTaskStoreSql(dialect); const eventStream = withEvents ? new EventEmitterStream() : undefined; // NOTE: no resolver cache used here to avoid locking LevelDB @@ -31,6 +33,7 @@ export async function getTestDwn(options: { eventLog, dataStore, messageStore, + resumableTaskStore, eventStream, tenantGate, didResolver diff --git a/tests/utils.ts b/tests/utils.ts index af10b0a..33f67d4 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,9 +1,8 @@ import type { GenericMessage, Persona, UnionMessageReply } from '@tbd54566975/dwn-sdk-js'; +import type { Response } from 'node-fetch'; import { Cid, DataStream, RecordsWrite } from '@tbd54566975/dwn-sdk-js'; -import type { ReadStream } from 'node:fs'; import fs from 'node:fs'; -import http from 'node:http'; import path from 'path'; import { v4 as uuidv4 } from 'uuid'; import fetch from 'node-fetch'; @@ -25,6 +24,8 @@ export type CreateRecordsWriteOverrides = dateCreated?: string; published?: boolean; recordId?: string; + protocol?: string; + protocolPath?: string; } & { data?: never }) | ({ dataCid?: never; @@ -32,9 +33,11 @@ export type CreateRecordsWriteOverrides = dateCreated?: string; published?: boolean; recordId?: string; + protocol?: string; + protocolPath?: string; } & { data?: Uint8Array }); -export type GenerateProtocolsConfigureOutput = { +export type GenerateRecordsWriteOutput = { recordsWrite: RecordsWrite; dataStream: Readable | undefined; }; @@ -42,7 +45,7 @@ export type GenerateProtocolsConfigureOutput = { export async function createRecordsWriteMessage( signer: Persona, overrides: CreateRecordsWriteOverrides = {}, -): Promise { +): Promise { if (!overrides.dataCid && !overrides.data) { overrides.data = randomBytes(32); } @@ -98,50 +101,8 @@ export async function getFileAsReadStream( }); } -type HttpResponse = { - status: number; - headers: http.IncomingHttpHeaders; - body?: any; -}; - -export function streamHttpRequest( - url: string, - opts: http.RequestOptions, - bodyStream: ReadStream, -): Promise { - return new Promise((resolve, reject) => { - const request = http.request(url, opts, (rawResponse) => { - rawResponse.setEncoding('utf8'); - - const response: HttpResponse = { - status: rawResponse.statusCode, - headers: rawResponse.headers, - }; - - let body = ''; - rawResponse.on('data', (chunk) => { - body += chunk; - }); - - rawResponse.on('end', () => { - if (body) { - response.body = body; - } - - return resolve(response); - }); - }); - - request.on('error', (e) => { - return reject(e); - }); - - bodyStream.on('end', () => { - request.end(); - }); - - bodyStream.pipe(request); - }); +export function getDwnResponse(response: Response): UnionMessageReply { + return JSON.parse(response.headers.get('dwn-response') as string) as UnionMessageReply; } export async function sendHttpMessage(options: { diff --git a/tests/ws-api.spec.ts b/tests/ws-api.spec.ts index 34a2150..19251b0 100644 --- a/tests/ws-api.spec.ts +++ b/tests/ws-api.spec.ts @@ -2,8 +2,6 @@ import type { Dwn, MessageEvent } from '@tbd54566975/dwn-sdk-js'; import { DataStream, Message, TestDataGenerator } from '@tbd54566975/dwn-sdk-js'; -import type { Server } from 'http'; - import { expect } from 'chai'; import { base64url } from 'multiformats/bases/base64'; import type { SinonFakeTimers } from 'sinon'; @@ -24,7 +22,6 @@ import { JsonRpcSocket } from '../src/json-rpc-socket.js'; describe('websocket api', function () { - let server: Server; let httpApi: HttpApi; let wsApi: WsApi; let dwn: Dwn; @@ -40,16 +37,15 @@ describe('websocket api', function () { beforeEach(async function () { dwn = await getTestDwn({ withEvents: true }); - httpApi = new HttpApi(config, dwn); - server = await httpApi.start(9002); - wsApi = new WsApi(server, dwn); + httpApi = await HttpApi.create(config, dwn); + await httpApi.start(9002); + wsApi = new WsApi(httpApi.server, dwn); wsApi.start(); }); afterEach(async function () { await wsApi.close(); - server.close(); - server.closeAllConnections(); + await httpApi.close(); await dwn.close(); });