diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index fe82db1d..dce3cb1e 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -15,11 +15,11 @@ jobs: uses: actions/setup-node@v3 with: node-version: lts/* - - name: Install dependencies + - name: Install dependencies run: npm ci - name: Build packages run: npm run build - + test: runs-on: ubuntu-latest needs: build @@ -31,9 +31,9 @@ jobs: uses: actions/setup-node@v3 with: node-version: lts/* - - name: Install dependencies + - name: Install dependencies run: npm ci - - name: Install playwright test browsers + - name: Install playwright test browsers run: npx playwright install --with-deps - name: Run all tests run: npm test \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 39490e77..720526dc 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -14,8 +14,7 @@ name: "CodeQL" on: push: branches: [ "master" ] - pull_request: - branches: [ "master" ] + workflow_call: schedule: - cron: '22 3 * * 6' @@ -68,7 +67,7 @@ jobs: # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality config: | - paths-ignore: + paths-ignore: - docs - '**/__testData__/**' - '**/*.test.ts' diff --git a/.github/workflows/devskim.yml b/.github/workflows/devskim.yml index 472a3092..ee44723f 100644 --- a/.github/workflows/devskim.yml +++ b/.github/workflows/devskim.yml @@ -8,8 +8,7 @@ name: DevSkim on: push: branches: [ "master" ] - pull_request: - branches: [ "master" ] + workflow_call: schedule: - cron: '22 15 * * 3' diff --git a/.github/workflows/njsscan.yml b/.github/workflows/njsscan.yml index 4ffeea7c..f10804ab 100644 --- a/.github/workflows/njsscan.yml +++ b/.github/workflows/njsscan.yml @@ -11,9 +11,7 @@ name: njsscan sarif on: push: branches: [ "master" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "master" ] + workflow_call: schedule: - cron: '38 6 * * 5' diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 5e1638c5..8e5a5486 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -5,6 +5,7 @@ on: branches: - main - master + - full-id-service merge_group: workflow_dispatch: @@ -13,3 +14,15 @@ jobs: name: Build And Test uses: ./.github/workflows/build-and-test.yml secrets: inherit + analyze: + name: CodeQL + uses: ./.github/workflows/codeql.yml + secrets: inherit + lint: + name: DevSkim + uses: ./.github/workflows/devskim.yml + secrets: inherit + njsscan: + name: njsscan + uses: ./.github/workflows/njsscan.yml + secrets: inherit diff --git a/.github/workflows/update-docs.yml b/.github/workflows/update-docs.yml index 1c83b440..5ae754dc 100644 --- a/.github/workflows/update-docs.yml +++ b/.github/workflows/update-docs.yml @@ -2,7 +2,13 @@ name: update-docs on: pull_request: + branches: + - main + - master merge_group: + branches: + - main + - master jobs: update: diff --git a/.gitignore b/.gitignore index f334b453..259784db 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules/ packages/*/example/*.js* *.map .nx/ +packages/matrix-identity-server/matrix-server/synapse-data diff --git a/DOCKER-VARIABLES.md b/DOCKER-VARIABLES.md index fdec1f89..de0fecfd 100644 --- a/DOCKER-VARIABLES.md +++ b/DOCKER-VARIABLES.md @@ -19,10 +19,10 @@ * `JITSI_BASE_URL`: example `https://jitsi.linagora.com` * 5 strings to set if Jitsi is strictly reserved for Twake users: * `JITSI_JWT_ALGORITHM`: example: `HS256` - * `JITSI_JWT_ISSUER`: - * `JITSI_SECRET`: - * `JITSI_PREFERRED_DOMAIN`: - * `JITSI_USE_JWT`: + * `JITSI_JWT_ISSUER`: + * `JITSI_SECRET`: + * `JITSI_PREFERRED_DOMAIN`: + * `JITSI_USE_JWT`: * `MATRIX_SERVER`: Matrix server. Example: `matrix.company.com` * `MATRIX_DATABASE_ENGINE`: `sqlite` or `pg` * `MATRIX_DATABASE_HOST`: diff --git a/Dockerfile b/Dockerfile index dd15f3c3..85c9a874 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,45 @@ -FROM node:18 +# Base for final image +FROM debian:bookworm-slim as node-minimal -env BASE_URL= \ +RUN apt update && \ + apt -y dist-upgrade && \ + apt -y install nodejs && \ + apt autoremove -y && \ + apt clean && \ + rm -rf /var/lib/apt/lists/* + +# Temporary image to build app +FROM debian:bookworm-slim as builder + +RUN apt update && \ + apt -y dist-upgrade && \ + apt -y install nodejs npm && \ + apt autoremove -y && \ + apt clean && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /usr/src/app + +# COPIES +# 1. Files +COPY package*.json .njsscan *.js *.json *.mjs LICENSE ./ + +# 2. Directories +COPY .husky ./.husky/ +COPY packages ./packages/ +COPY landing /usr/src/app/landing +#COPY node_modules ./node_modules/ + +# Build and clean + +RUN npm install && npm run build && \ + rm -rf node_modules */*/node_modules && \ + npm install --production --ignore-scripts && \ + npm cache clean --force + +FROM node-minimal as tom-server + +ENV BASE_URL= \ CRON_SERVICE= \ CROWDSEC_URI= \ CROWDSEC_KEY= \ @@ -54,15 +93,9 @@ env BASE_URL= \ RATE_LIMITING_NB_REQUESTS= \ TRUSTED_PROXIES= -RUN apt update && apt -y dist-upgrade +COPY --from=1 /usr/src/app /usr/src/app/ WORKDIR /usr/src/app -COPY package*.json ./ - -COPY . . - -RUN npm install && npm run build && npm cache clean --force - EXPOSE 3000 CMD [ "node", "/usr/src/app/server.mjs" ] diff --git a/README.md b/README.md index 2a1f6002..7d10bdc2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,26 @@ -# Twake-Chat server repository +# Twake-Chat Matrix extension server + +
+
+ + + + + + + +

+ Website + • + View Demo + • + Report Bug + • + Translate Twake> +

+
+ +--- This repository is a multi-packages repository. See [Modules](#modules) for details. @@ -27,12 +49,15 @@ REST API Endpoints documentation is available on https://linagora.github.io/ToM- * [@twake/matrix-identity-server](./packages/matrix-identity-server): [Matrix Identity Service](https://spec.matrix.org/v1.6/identity-service-api/) implementation for Node.js +* [@twake/matrix-client-server](./packages/matrix-client-server/): + [Matrix Client-Server](https://spec.matrix.org/v1.11/client-server-api/) implementation for Node.js * [@twake/matrix-invite](./packages/matrix-invite): matrix invitation web application * [@twake/server](./packages/tom-server): the main Twake Chat Server, extends [@twake/matrix-identity-server](./packages/matrix-identity-server) * [@twake/federated-identity-service](./packages/federated-identity-service): Twake Federated Identity Service * [@twake/config-parser](./packages/config-parser): simple file parser that uses also environment variables * [@twake/crypto](./packages/crypto): cryptographic methods for Twake Chat * [@twake/logger](./packages/logger): logger for Twake +* [@twake/utils](.packages/utils): utilitary methods for Twake Chat * [@twake/matrix-application-server](./packages/matrix-application-server): implements [Matrix Application Service API](https://spec.matrix.org/v1.6/application-service-api/) * [matrix-resolve](./packages/matrix-resolve): resolve a Matrix "server name" into base URL following diff --git a/docs/arch.dot b/docs/arch.dot index bed39357..56accb93 100644 --- a/docs/arch.dot +++ b/docs/arch.dot @@ -1,4 +1,4 @@ -digraph { +digraph { nodesep=1 subgraph cluster_external { style=dotted diff --git a/docs/openapi.json b/docs/openapi.json index f6c04052..2394db39 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1 +1 @@ -{"openapi":"3.0.0","info":{"title":"Twake on Matrix APIs documentation","version":"0.0.1","description":"This is The documentation of all available APIs of this repository"},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"MatrixError":{"type":"object","properties":{"errcode":{"type":"string","description":"A Matrix error code"},"error":{"type":"string","description":"A human-readable error message"}},"required":["error"]},"MutualRooms":{"type":"array","items":{"type":"object","properties":{"roomId":{"type":"string","description":"the room id"},"name":{"type":"string","description":"the room name"},"topic":{"type":"string","description":"the room topic"},"room_type":{"type":"string","description":"the room type"}}}},"PrivateNote":{"type":"object","properties":{"id":{"type":"string","description":"The private note id"},"content":{"type":"string","description":"The private note content"},"authorId":{"type":"string","description":"The author user id"},"targetId":{"type":"string","description":"The target user id"}}},"CreatePrivateNote":{"type":"object","properties":{"content":{"type":"string","description":"The private note content"},"authorId":{"type":"string","description":"The author user id"},"targetId":{"type":"string","description":"The target user id"}}},"UpdatePrivateNote":{"type":"object","properties":{"id":{"type":"string","description":"The private note id"},"content":{"type":"string","description":"The private note content"}}},"RoomTags":{"type":"object","properties":{"tags":{"description":"the room tags list","type":"array","items":{"type":"string"}}}},"RoomTagCreation":{"type":"object","properties":{"content":{"type":"array","description":"the room tags strings","items":{"type":"string"}},"roomId":{"type":"string","description":"the room id"}}},"RoomTagsUpdate":{"type":"object","properties":{"content":{"type":"array","description":"the room tags strings","items":{"type":"string"}}}},"sms":{"type":"object","properties":{"to":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"text":{"type":"string"}}},"UserInfo":{"type":"object","properties":{"uid":{"type":"string","description":"the user id"},"givenName":{"type":"string","description":"the user given name"},"sn":{"type":"string","description":"the user surname"}}}},"responses":{"InternalServerError":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"The message describing the internal error"}}}}}},"Unauthorized":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_UNAUTHORIZED","error":"Unauthorized"}}}},"BadRequest":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_MISSING_PARAMS","error":"Properties are missing in the request body"}}}},"Forbidden":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_FORBIDDEN","error":"Forbidden"}}}},"Conflict":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"error":"Conflict"}}}},"PermanentRedirect":{"description":"Permanent Redirect","headers":{"Location":{"schema":{"type":"string","description":"URL to use for recdirect"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_UNKNOWN","error":"This non-standard endpoint has been removed"}}}},"NotFound":{"description":"Private note not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_NOT_FOUND","error":"Not Found"}}}},"Unrecognized":{"description":"Unrecognized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_UNRECOGNIZED","error":"Unrecognized"}}}},"InternalError":{"description":"Internal error"},"Created":{"description":"Created"},"NoContent":{"description":"operation successful and no content returned"}},"parameters":{"target_userid":{"name":"target_userid","in":"path","required":true,"description":"the target user id","schema":{"type":"string"}},"user_id":{"name":"user_id","in":"query","description":"the author user id","required":true,"schema":{"type":"string"}},"target_user_id":{"name":"target_user_id","in":"query","description":"the target user id","required":true,"schema":{"type":"string"}},"private_note_id":{"name":"private_note_id","in":"path","description":"the private note id","required":true,"schema":{"type":"string"}},"roomId":{"in":"path","name":"roomId","description":"the room id","required":true,"schema":{"type":"string"}},"userId":{"in":"path","name":"userId","description":"the user id","required":true,"schema":{"type":"string"}}}},"security":[{"bearerAuth":[]}],"paths":{"/_matrix/identity/v2":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2"}},"/_matrix/identity/v2/hash_details":{"get":{"tags":["Federated identity service"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2hash_details"}},"/_matrix/identity/v2/lookup":{"post":{"tags":["Federated identity service"],"description":"Extends https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2lookup to display inactive users and 3PID users","requestBody":{"description":"Object containing hashes of mails/phones to search","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"addresses":{"type":"array","items":{"type":"string","description":"List of (hashed) addresses to lookup"}},"algorithm":{"type":"string","description":"Algorithm the client is using to encode the addresses"},"pepper":{"type":"string","description":"Pepper from '/hash_details'"}},"required":["addresses","algorithm","pepper"]},"example":{"addresses":["4kenr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","nlo35_T5fzSGZzJApqu8lgIudJvmOQtDaHtr-I4rU7I"],"algorithm":"sha256","pepper":"matrixrocks"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"mappings":{"type":"object","additionalProperties":{"type":"string"},"description":"List of active accounts"},"inactive_mappings":{"type":"object","additionalProperties":{"type":"string"},"description":"List of inactive accounts"},"third_party_mappings":{"type":"object","description":"List of hashed addresses by identity server hostname","properties":{"hostname":{"type":"object","properties":{"actives":{"type":"array","items":{"type":"string","description":"List of (hashed) active accounts addresses matching request body addresses"}},"inactives":{"type":"array","items":{"type":"string","description":"List of (hashed) inactive accounts addresses matching request body addresses"}}}}}}}},"example":{"mappings":{"4kenr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc":"@dwho:company.com"},"inactive_mappings":{"nlo35_T5fzSGZzJApqu8lgIudJvmOQtDaHtr-I4rU7I":"@rtyler:company.com"},"third_party_mappings":{"identity1.example.com":{"actives":["78jnr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","gtr42_T5fzSGZzJAmlp5lgIudJvmOQtDaHtr-I4rU7I"],"inactives":["qfgt57N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","lnbc8_T5fzSGZzJAmlp5lgIudJvmOQtDaHtr-I4rU7I"]}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/identity/v2/account":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2account"}},"/_matrix/identity/v2/account/register":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2accountregister"}},"/_matrix/identity/v2/account/logout":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2accountlogout"}},"/_matrix/identity/v2/terms":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2terms"}},"/_matrix/identity/v2/validate/email/requestToken":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2validateemailrequesttoken"}},"/_matrix/identity/v2/validate/email/submitToken":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2validateemailsubmittoken"}},"/_matrix/identity/versions":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityversions"}},"/_twake/identity/v1/lookup/match":{"post":{"tags":["Identity server"],"description":"Looks up the Organization User IDs which match value sent","requestBody":{"description":"Object containing detail for the search and the returned data","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"scope":{"type":"array","items":{"type":"string","description":"List of fields to search in (uid, mail,...)"}},"fields":{"type":"array","items":{"type":"string","description":"List of fields to return for matching users (uid, mail, mobile, displayName, givenName, cn, sn)"}},"val":{"type":"string","description":"Optional value to search"},"limit":{"type":"integer","description":"Optional max number of result to return (default 30)"},"offset":{"type":"integer","description":"Optional offset for pagination"}},"required":["scope","fields"]},"example":{"scope":["mail","uid"],"fields":["uid","displayName","sn","givenName","mobile"],"val":"rtyler","limit":3}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"matches":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string","description":"Matrix address"},"uid":{"type":"string","description":"id of a matching user"},"mail":{"type":"string","description":"email address of a matching user"}}},"description":"List of users that match"}}},"example":{"matches":[{"uid":"dwho","mail":"dwho@badwolf.com"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/_twake/identity/v1/lookup/diff":{"post":{"tags":["Identity server"],"description":"Looks up the Organization User IDs updated since X","requestBody":{"description":"Object containing the timestamp","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"since":{"type":"integer","description":"timestamp"},"fields":{"type":"array","items":{"type":"string","description":"List of fields to return for matching users"}},"limit":{"type":"integer","description":"Optional max number of result to return (default 30)"},"offset":{"type":"integer","description":"Optional offset for pagination"}}},"example":{"since":1685074279,"fields":["uid","mail"],"limit":3}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"matches":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string","description":"Matrix address"},"timestamp":{"type":"integer","description":"current server timestamp"},"uid":{"type":"string","description":"id of a matching user"},"mail":{"type":"string","description":"email address of a matching user"}}},"description":"List of users that match"}}},"example":{"matches":[{"uid":"dwho","mail":"dwho@badwolf.com"}]}}}}}}},"/_twake/recoveryWords":{"get":{"tags":["Vault API"],"description":"Allow for the connected user to retrieve its recovery words","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"words":{"type":"string","description":"Recovery words of the connected user"}}},"example":{"words":"This is the recovery sentence of rtyler"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Connected user has no recovery sentence"}}},"example":{"error":"User has no recovery sentence"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Connected user has multiple recovery sentence"}}},"example":{"error":"User has more than one recovery sentence"}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}},"post":{"tags":["Vault API"],"description":"Store connected user recovery words in database","requestBody":{"description":"Object containing the recovery words of the connected user","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"words":{"type":"string","description":"The recovery words of the connected user"}},"required":["words"]},"example":{"words":"This is the recovery sentence of rtyler"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Message indicating that words have been successfully saved"}},"example":{"message":"Saved recovery words sucessfully"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/InternalServerError"}}},"delete":{"tags":["Vault API"],"description":"Delete the user recovery words in the database","responses":{"204":{"description":"Delete success"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Connected user has no recovery sentence"}}},"example":{"error":"User has no recovery sentence"}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/.well-knwon/matrix/client":{"get":{"tags":["Auto configuration"],"description":"Get server metadata for auto configuration","responses":{"200":{"description":"Give server metadata","content":{"application/json":{"schema":{"type":"object","properties":{"m.homeserver":{"type":"object","properties":{"base_url":{"type":"string","description":"Base URL of Matrix server"}}},"m.identity_server":{"type":"object","properties":{"base_url":{"type":"string","description":"Base URL of Identity server"}}},"m.federated_identity_services":{"type":"object","properties":{"base_urls":{"type":"array","items":{"type":"string","description":"Base URL of Federated identity service"},"description":"Available Federated identity services Base URL list"}}},"t.server":{"type":"object","properties":{"base_url":{"type":"string","description":"Base URL of Identity server"},"server_name":{"type":"string","description":"Domain handled by Matrix server"}}},"m.integrations":{"type":"object","properties":{"jitsi":{"type":"object","properties":{"preferredDomain":{"type":"string","description":"Jitsi's preffered domain"},"baseUrl":{"type":"string","description":"URL of Jitsi server"},"useJwt":{"type":"boolean","description":"True if Jitsi server requires a JWT"},"jwt":{"type":"object","properties":{"algorithm":{"type":"string","description":"algorithm used to generate JWT"},"secret":{"type":"string","description":"password of JWTs"},"issuer":{"type":"string","description":"issuer of JWTs"}}}}}}},"m.authentication":{"type":"object","properties":{"issuer":{"type":"string","description":"URL of OIDC issuer"}}}}},"example":{"m.homeserver":{"base_url":"matrix.example.com"},"m.identity_server":{"base_url":"global-id-server.twake.app"},"m.federated_identity_services":{"base_urls":["global-federated_identity_service.twake.app","other-federated-identity-service.twake.app"]},"m.integrations":{"jitsi":{"baseUrl":"https://jitsi.example.com/","preferredDomain":"jitsi.example.com","useJwt":false}},"m.authentication":{"issuer":"https://auth.example.com"},"t.server":{"base_url":"https://tom.example.com","server_name":"example.com"}}}}}}}},"/_matrix/identity/v2/lookups":{"post":{"tags":["Federated identity service"],"description":"Implements https://github.com/guimard/matrix-spec-proposals/blob/unified-identity-service/proposals/4004-unified-identity-service-view.md","requestBody":{"description":"Object containing hashes to store in federated identity service database","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mappings":{"type":"object","description":"List of hashed addresses by identity server hostname","properties":{"hostname":{"type":"array","items":{"type":"object","properties":{"hash":{"type":"string"},"active":{"type":"number"}}}}}},"algorithm":{"type":"string","description":"Algorithm the client is using to encode the addresses"},"pepper":{"type":"string","description":"Pepper from '/hash_details'"}},"required":["addresses","algorithm","pepper"]},"example":{"mappings":{"identity1.example.com":[{"hash":"4kenr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","active":1},{"hash":"nlo35_T5fzSGZzJApqu8lgIudJvmOQtDaHtr-I4rU7I","active":0}]},"algorithm":"sha256","pepper":"matrixrocks"}}}},"responses":{"201":{"description":"Success"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/app/v1/transactions/{txnId}":{"put":{"parameters":[{"in":"path","name":"txnId","required":true,"schema":{"type":"integer"},"description":"The transaction id"}],"tags":["Application server"],"description":"Implements https://spec.matrix.org/v1.6/application-service-api/#put_matrixappv1transactionstxnid","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"308":{"$ref":"#/components/responses/PermanentRedirect"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object"}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/app/v1/users/{userId}":{"get":{"parameters":[{"in":"path","name":"userId","required":true,"schema":{"type":"integer"},"description":"The user id"}],"tags":["Application server"],"description":"Implements https://spec.matrix.org/v1.6/application-service-api/#get_matrixappv1usersuserid","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/app/v1/rooms/{roomAlias}":{"get":{"parameters":[{"in":"path","name":"roomAlias","required":true,"schema":{"type":"integer"},"description":"The room alias"}],"tags":["Application server"],"description":"Implements https://spec.matrix.org/v1.6/application-service-api/#get_matrixappv1roomsroomalias","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/app/v1/rooms":{"post":{"tags":["Application server"],"description":"Implements https://www.notion.so/Automatic-channels-89ba6f97bc90474ca482a28cf3228d3e","requestBody":{"description":"Object containing room's details","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ldapFilter":{"type":"object","additionalProperties":true,"description":"An object containing keys/values to build a ldap filter"},"aliasName":{"type":"string","description":"The desired room alias local part. If aliasName is equal to foo the complete room alias will be"},"name":{"type":"string","description":"The room name"},"topic":{"type":"string","description":"A short message detailing what is currently being discussed in the room."},"visibility":{"type":"string","enum":["public","private"],"description":"visibility values:\n * `public` - The room will be shown in the published room list\n * `private` - Hide the room from the published room list\n"}},"required":["ldapFilter","aliasName"]},"example":{"ldapFilter":{"mail":"example@test.com","cn":"example"},"aliasName":"exp","name":"Example","topic":"This is an example of a room topic","visibility":"public"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"errcode":{"type":"string"},"error":{"type":"string"}},"additionalProperties":{"type":"string"},"description":"List of users uid not added to the new room due to an error"},"example":[{"uid":"test1","errcode":"M_FORBIDDEN","error":"The user has been banned from the room"},{"uid":"test2","errcode":"M_UNKNOWN","error":"Internal server error"}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"examples":{"example1":{"value":{"error":"Error field: Invalid value (property: name)"}},"example2":{"value":{"errcode":"M_NOT_JSON","error":"Not_json"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"examples":{"example1":{"value":{"error":"This room already exits in Twake database"}},"example2":{"value":{"errcode":"M_ROOM_IN_USE","error":"A room with alias foo already exists in Matrix database"}}}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/mutual_rooms/{target_userid}":{"get":{"tags":["Mutual Rooms"],"description":"Get the list of mutual rooms between two users","parameters":[{"$ref":"#/components/parameters/target_userid"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MutualRooms"}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"},"500":{"description":"Internal error"}}}},"/_twake/private_note":{"get":{"tags":["Private Note"],"description":"Get the private note made by the user for a target user","parameters":[{"$ref":"#/components/parameters/user_id"},{"$ref":"#/components/parameters/target_user_id"}],"responses":{"200":{"description":"Private note found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrivateNote"}}}},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"404":{"description":"Private note not found"},"500":{"description":"Internal error"}}},"post":{"tags":["Private Note"],"description":"Create a private note for a target user","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePrivateNote"}}}},"responses":{"201":{"description":"Private note created"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}},"put":{"tags":["Private Note"],"description":"Update a private note","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePrivateNote"}}}},"responses":{"204":{"description":"Private note created"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/private_note/{private_note_id}":{"delete":{"tags":["Private Note"],"description":"Delete a private note","parameters":[{"$ref":"#/components/parameters/private_note_id"}],"responses":{"204":{"description":"Private note deleted"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/v1/room_tags/{roomId}":{"get":{"tags":["Room tags"],"description":"Get room tags","parameters":[{"$ref":"#/components/parameters/roomId"}],"responses":{"200":{"description":"Room tags found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomTags"}}}},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}},"put":{"tags":["Room tags"],"description":"Update room tags","parameters":[{"$ref":"#/components/parameters/roomId"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomTagsUpdate"}}}},"responses":{"204":{"description":"Room tags updated"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}},"delete":{"tags":["Room tags"],"description":"delete tags for a room","parameters":[{"$ref":"#/components/parameters/roomId"}],"responses":{"204":{"description":"Room tags deleted"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/v1/room_tags":{"post":{"tags":["Room tags"],"description":"Create room tags","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomTagCreation"}}}},"responses":{"201":{"description":"Room tags created"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/app/v1/search":{"post":{"tags":["Search Engine"],"description":"Search performs with OpenSearch on Tchat messages and rooms","requestBody":{"description":"Object containing search query details","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"searchValue":{"type":"string","description":"Value used to perform the search on rooms and messages data"}},"required":["searchValue"]},"example":{"searchValue":"hello"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"rooms":{"type":"array","description":"List of rooms whose name contains the search value","items":{"type":"object","properties":{"room_id":{"type":"string"},"name":{"type":"string"},"avatar_url":{"type":"string","description":"Url of the room's avatar"}}}},"messages":{"type":"array","description":"List of messages whose content or/and sender display name contain the search value","items":{"type":"object","properties":{"room_id":{"type":"string"},"event_id":{"type":"string","description":"Id of the message"},"content":{"type":"string"},"display_name":{"type":"string","description":"Sender display name"},"avatar_url":{"type":"string","description":"Sender's avatar url if it is a direct chat, otherwise it is the room's avatar url"},"room_name":{"type":"string","description":"Room's name in case of the message is not part of a direct chat"}}}},"mails":{"type":"array","description":"List of mails from Tmail whose meta or content contain the search value","items":{"type":"object","properties":{"attachments":{"type":"array","items":{"type":"object","properties":{"contentDisposition":{"type":"string"},"fileExtension":{"type":"string"},"fileName":{"type":"string"},"mediaType":{"type":"string"},"subtype":{"type":"string"},"textContent":{"type":"string"}}}},"bcc":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"cc":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"date":{"type":"string"},"from":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"hasAttachment":{"type":"boolean"},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}}}},"htmlBody":{"type":"string"},"isAnswered":{"type":"boolean"},"isDeleted":{"type":"boolean"},"isDraft":{"type":"boolean"},"isFlagged":{"type":"boolean"},"isRecent":{"type":"boolean"},"isUnread":{"type":"boolean"},"mailboxId":{"type":"string"},"mediaType":{"type":"string"},"messageId":{"type":"string"},"mimeMessageID":{"type":"string"},"modSeq":{"type":"number"},"saveDate":{"type":"string"},"sentDate":{"type":"string"},"size":{"type":"number"},"subject":{"type":"array","items":{"type":"string"}},"subtype":{"type":"string"},"textBody":{"type":"string"},"threadId":{"type":"string"},"to":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"uid":{"type":"number"},"userFlags":{"type":"array","items":{"type":"string"}}}}}}},"example":{"rooms":[{"room_id":"!dYqMpBXVQgKWETVAtJ:example.com","name":"Hello world room","avatar_url":"mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR"},{"room_id":"!dugSgNYwppGGoeJwYB:example.com","name":"Worldwide room","avatar_url":null}],"messages":[{"room_id":"!dYqMpBXVQgKWETVAtJ:example.com","event_id":"$c0hW6db_GUjk0NRBUuO12IyMpi48LE_tQK6sH3dkd1U","content":"Hello world","display_name":"Anakin Skywalker","avatar_url":"mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR","room_name":"Hello world room"},{"room_id":"!ftGqINYwppGGoeJwYB:example.com","event_id":"$IUzFofxHCvvoHJ-k2nfx7OlWOO8AuPvlHHqkeJLzxJ8","content":"Hello world my friends in direct chat","display_name":"Luke Skywalker","avatar_url":"mxc://matrix.org/wefh34uihSDRGhw34"}],"mails":[{"id":"message1","attachments":[{"contentDisposition":"attachment","fileExtension":"jpg","fileName":"image1.jpg","mediaType":"image/jpeg","textContent":"A beautiful galaxy far, far away."}],"bcc":[{"address":"okenobi@example.com","domain":"example.com","name":"Obi-Wan Kenobi"}],"cc":[{"address":"pamidala@example.com","domain":"example.com","name":"Padme Amidala"}],"date":"2024-02-24T10:15:00Z","from":[{"address":"dmaul@example.com","domain":"example.com","name":"Dark Maul"}],"hasAttachment":true,"headers":[{"name":"Header5","value":"Value5"},{"name":"Header6","value":"Value6"}],"htmlBody":"

A beautiful galaxy far, far away.

","isAnswered":true,"isDeleted":false,"isDraft":false,"isFlagged":true,"isRecent":true,"isUnread":false,"mailboxId":"mailbox3","mediaType":"image/jpeg","messageId":"message3","mimeMessageID":"mimeMessageID3","modSeq":98765,"saveDate":"2024-02-24T10:15:00Z","sentDate":"2024-02-24T10:15:00Z","size":4096,"subject":["Star Wars Message 3"],"subtype":"subtype3","textBody":"A beautiful galaxy far, far away.","threadId":"thread3","to":[{"address":"kren@example.com","domain":"example.com","name":"Kylo Ren"}],"uid":987654,"userFlags":["Flag4","Flag5"]}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/app/v1/opensearch/restore":{"post":{"tags":["Search Engine"],"description":"Restore OpenSearch indexes using Matrix homeserver database","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/sms":{"post":{"requestBody":{"description":"SMS object","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sms"}}}},"tags":["SMS"],"description":"Send an SMS to a phone number","responses":{"200":{"description":"SMS sent successfully"},"400":{"description":"Invalid request"},"401":{"description":"Unauthorized"},"500":{"description":"Internal server error"}}}},"/_twake/v1/user_info/{userId}":{"get":{"tags":["User Info"],"description":"Get user info","parameters":[{"$ref":"#/components/parameters/userId"}],"responses":{"200":{"description":"User info found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInfo"}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"404":{"description":"User info not found"},"500":{"description":"Internal server error"}}}}},"tags":[]} \ No newline at end of file +{"openapi":"3.0.0","info":{"title":"Twake on Matrix APIs documentation","version":"0.0.1","description":"This is The documentation of all available APIs of this repository"},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"MatrixError":{"type":"object","properties":{"errcode":{"type":"string","description":"A Matrix error code"},"error":{"type":"string","description":"A human-readable error message"}},"required":["error"]},"ActiveContacts":{"type":"object","description":"the list of active contacts","properties":{"contacts":{"type":"string","description":"active contacts"}}},"MutualRooms":{"type":"array","items":{"type":"object","properties":{"roomId":{"type":"string","description":"the room id"},"name":{"type":"string","description":"the room name"},"topic":{"type":"string","description":"the room topic"},"room_type":{"type":"string","description":"the room type"}}}},"PrivateNote":{"type":"object","properties":{"id":{"type":"string","description":"The private note id"},"content":{"type":"string","description":"The private note content"},"authorId":{"type":"string","description":"The author user id"},"targetId":{"type":"string","description":"The target user id"}}},"CreatePrivateNote":{"type":"object","properties":{"content":{"type":"string","description":"The private note content"},"authorId":{"type":"string","description":"The author user id"},"targetId":{"type":"string","description":"The target user id"}}},"UpdatePrivateNote":{"type":"object","properties":{"id":{"type":"string","description":"The private note id"},"content":{"type":"string","description":"The private note content"}}},"RoomTags":{"type":"object","properties":{"tags":{"description":"the room tags list","type":"array","items":{"type":"string"}}}},"RoomTagCreation":{"type":"object","properties":{"content":{"type":"array","description":"the room tags strings","items":{"type":"string"}},"roomId":{"type":"string","description":"the room id"}}},"RoomTagsUpdate":{"type":"object","properties":{"content":{"type":"array","description":"the room tags strings","items":{"type":"string"}}}},"sms":{"type":"object","properties":{"to":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"text":{"type":"string"}}},"UserInfo":{"type":"object","properties":{"uid":{"type":"string","description":"the user id"},"givenName":{"type":"string","description":"the user given name"},"sn":{"type":"string","description":"the user surname"}}}},"responses":{"InternalServerError":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"The message describing the internal error"}}}}}},"Unauthorized":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_UNAUTHORIZED","error":"Unauthorized"}}}},"BadRequest":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_MISSING_PARAMS","error":"Properties are missing in the request body"}}}},"Forbidden":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_FORBIDDEN","error":"Forbidden"}}}},"Conflict":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"error":"Conflict"}}}},"PermanentRedirect":{"description":"Permanent Redirect","headers":{"Location":{"schema":{"type":"string","description":"URL to use for recdirect"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_UNKNOWN","error":"This non-standard endpoint has been removed"}}}},"NotFound":{"description":"Private note not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_NOT_FOUND","error":"Not Found"}}}},"Unrecognized":{"description":"Unrecognized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_UNRECOGNIZED","error":"Unrecognized"}}}},"Created":{"description":"Created"},"NoContent":{"description":"operation successful and no content returned"},"InternalError":{"description":"Internal error"}},"parameters":{"target_userid":{"name":"target_userid","in":"path","required":true,"description":"the target user id","schema":{"type":"string"}},"user_id":{"name":"user_id","in":"query","description":"the author user id","required":true,"schema":{"type":"string"}},"target_user_id":{"name":"target_user_id","in":"query","description":"the target user id","required":true,"schema":{"type":"string"}},"private_note_id":{"name":"private_note_id","in":"path","description":"the private note id","required":true,"schema":{"type":"string"}},"roomId":{"in":"path","name":"roomId","description":"the room id","required":true,"schema":{"type":"string"}},"userId":{"in":"path","name":"userId","description":"the user id","required":true,"schema":{"type":"string"}}}},"security":[{"bearerAuth":[]}],"paths":{"/_matrix/identity/v2":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2"}},"/_matrix/identity/v2/hash_details":{"get":{"tags":["Federated identity service"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2hash_details"}},"/_matrix/identity/v2/lookup":{"post":{"tags":["Federated identity service"],"description":"Extends https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2lookup to display inactive users and 3PID users","requestBody":{"description":"Object containing hashes of mails/phones to search","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"addresses":{"type":"array","items":{"type":"string","description":"List of (hashed) addresses to lookup"}},"algorithm":{"type":"string","description":"Algorithm the client is using to encode the addresses"},"pepper":{"type":"string","description":"Pepper from '/hash_details'"}},"required":["addresses","algorithm","pepper"]},"example":{"addresses":["4kenr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","nlo35_T5fzSGZzJApqu8lgIudJvmOQtDaHtr-I4rU7I"],"algorithm":"sha256","pepper":"matrixrocks"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"mappings":{"type":"object","additionalProperties":{"type":"string"},"description":"List of active accounts"},"inactive_mappings":{"type":"object","additionalProperties":{"type":"string"},"description":"List of inactive accounts"},"third_party_mappings":{"type":"object","description":"List of hashed addresses by identity server hostname","properties":{"hostname":{"type":"object","properties":{"actives":{"type":"array","items":{"type":"string","description":"List of (hashed) active accounts addresses matching request body addresses"}},"inactives":{"type":"array","items":{"type":"string","description":"List of (hashed) inactive accounts addresses matching request body addresses"}}}}}}}},"example":{"mappings":{"4kenr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc":"@dwho:company.com"},"inactive_mappings":{"nlo35_T5fzSGZzJApqu8lgIudJvmOQtDaHtr-I4rU7I":"@rtyler:company.com"},"third_party_mappings":{"identity1.example.com":{"actives":["78jnr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","gtr42_T5fzSGZzJAmlp5lgIudJvmOQtDaHtr-I4rU7I"],"inactives":["qfgt57N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","lnbc8_T5fzSGZzJAmlp5lgIudJvmOQtDaHtr-I4rU7I"]}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/identity/v2/account":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2account"}},"/_matrix/identity/v2/account/register":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2accountregister"}},"/_matrix/identity/v2/account/logout":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2accountlogout"}},"/_matrix/identity/v2/terms":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2terms"}},"/_matrix/identity/v2/validate/email/requestToken":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2validateemailrequesttoken"}},"/_matrix/identity/v2/validate/email/submitToken":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2validateemailsubmittoken"}},"/_matrix/identity/versions":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityversions"}},"/_twake/identity/v1/lookup/match":{"post":{"tags":["Identity server"],"description":"Looks up the Organization User IDs which match value sent","requestBody":{"description":"Object containing detail for the search and the returned data","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"scope":{"type":"array","items":{"type":"string","description":"List of fields to search in (uid, mail,...)"}},"fields":{"type":"array","items":{"type":"string","description":"List of fields to return for matching users (uid, mail, mobile, displayName, givenName, cn, sn)"}},"val":{"type":"string","description":"Optional value to search"},"limit":{"type":"integer","description":"Optional max number of result to return (default 30)"},"offset":{"type":"integer","description":"Optional offset for pagination"}},"required":["scope","fields"]},"example":{"scope":["mail","uid"],"fields":["uid","displayName","sn","givenName","mobile"],"val":"rtyler","limit":3}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"matches":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string","description":"Matrix address"},"uid":{"type":"string","description":"id of a matching user"},"mail":{"type":"string","description":"email address of a matching user"}}},"description":"List of users that match"}}},"example":{"matches":[{"uid":"dwho","mail":"dwho@badwolf.com"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/_twake/identity/v1/lookup/diff":{"post":{"tags":["Identity server"],"description":"Looks up the Organization User IDs updated since X","requestBody":{"description":"Object containing the timestamp","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"since":{"type":"integer","description":"timestamp"},"fields":{"type":"array","items":{"type":"string","description":"List of fields to return for matching users"}},"limit":{"type":"integer","description":"Optional max number of result to return (default 30)"},"offset":{"type":"integer","description":"Optional offset for pagination"}}},"example":{"since":1685074279,"fields":["uid","mail"],"limit":3}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"matches":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string","description":"Matrix address"},"timestamp":{"type":"integer","description":"current server timestamp"},"uid":{"type":"string","description":"id of a matching user"},"mail":{"type":"string","description":"email address of a matching user"}}},"description":"List of users that match"}}},"example":{"matches":[{"uid":"dwho","mail":"dwho@badwolf.com"}]}}}}}}},"/_twake/recoveryWords":{"get":{"tags":["Vault API"],"description":"Allow for the connected user to retrieve its recovery words","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"words":{"type":"string","description":"Recovery words of the connected user"}}},"example":{"words":"This is the recovery sentence of rtyler"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Connected user has no recovery sentence"}}},"example":{"error":"User has no recovery sentence"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Connected user has multiple recovery sentence"}}},"example":{"error":"User has more than one recovery sentence"}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}},"post":{"tags":["Vault API"],"description":"Store connected user recovery words in database","requestBody":{"description":"Object containing the recovery words of the connected user","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"words":{"type":"string","description":"The recovery words of the connected user"}},"required":["words"]},"example":{"words":"This is the recovery sentence of rtyler"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Message indicating that words have been successfully saved"}},"example":{"message":"Saved recovery words sucessfully"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/InternalServerError"}}},"delete":{"tags":["Vault API"],"description":"Delete the user recovery words in the database","responses":{"204":{"description":"Delete success"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Connected user has no recovery sentence"}}},"example":{"error":"User has no recovery sentence"}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}},"put":{"tags":["Vault API"],"description":"Update stored connected user recovery words in database","requestBody":{"description":"Object containing the recovery words of the connected user","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"words":{"type":"string","description":"The new recovery words of the connected user"}},"required":["words"]},"example":{"words":"This is the updated recovery sentence of rtyler"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Message indicating that words have been successfully updated"}},"example":{"message":"Updated recovery words sucessfully"}}}}},"400":{"description":"Bad request"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/.well-knwon/matrix/client":{"get":{"tags":["Auto configuration"],"description":"Get server metadata for auto configuration","responses":{"200":{"description":"Give server metadata","content":{"application/json":{"schema":{"type":"object","properties":{"m.homeserver":{"type":"object","properties":{"base_url":{"type":"string","description":"Base URL of Matrix server"}}},"m.identity_server":{"type":"object","properties":{"base_url":{"type":"string","description":"Base URL of Identity server"}}},"m.federated_identity_services":{"type":"object","properties":{"base_urls":{"type":"array","items":{"type":"string","description":"Base URL of Federated identity service"},"description":"Available Federated identity services Base URL list"}}},"t.server":{"type":"object","properties":{"base_url":{"type":"string","description":"Base URL of Identity server"},"server_name":{"type":"string","description":"Domain handled by Matrix server"}}},"m.integrations":{"type":"object","properties":{"jitsi":{"type":"object","properties":{"preferredDomain":{"type":"string","description":"Jitsi's preffered domain"},"baseUrl":{"type":"string","description":"URL of Jitsi server"},"useJwt":{"type":"boolean","description":"True if Jitsi server requires a JWT"},"jwt":{"type":"object","properties":{"algorithm":{"type":"string","description":"algorithm used to generate JWT"},"secret":{"type":"string","description":"password of JWTs"},"issuer":{"type":"string","description":"issuer of JWTs"}}}}}}},"m.authentication":{"type":"object","properties":{"issuer":{"type":"string","description":"URL of OIDC issuer"}}}}},"example":{"m.homeserver":{"base_url":"matrix.example.com"},"m.identity_server":{"base_url":"global-id-server.twake.app"},"m.federated_identity_services":{"base_urls":["global-federated_identity_service.twake.app","other-federated-identity-service.twake.app"]},"m.integrations":{"jitsi":{"baseUrl":"https://jitsi.example.com/","preferredDomain":"jitsi.example.com","useJwt":false}},"m.authentication":{"issuer":"https://auth.example.com"},"t.server":{"base_url":"https://tom.example.com","server_name":"example.com"}}}}}}}},"/_matrix/identity/v2/lookups":{"post":{"tags":["Federated identity service"],"description":"Implements https://github.com/guimard/matrix-spec-proposals/blob/unified-identity-service/proposals/4004-unified-identity-service-view.md","requestBody":{"description":"Object containing hashes to store in federated identity service database","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mappings":{"type":"object","description":"List of hashed addresses by identity server hostname","properties":{"hostname":{"type":"array","items":{"type":"object","properties":{"hash":{"type":"string"},"active":{"type":"number"}}}}}},"algorithm":{"type":"string","description":"Algorithm the client is using to encode the addresses"},"pepper":{"type":"string","description":"Pepper from '/hash_details'"}},"required":["addresses","algorithm","pepper"]},"example":{"mappings":{"identity1.example.com":[{"hash":"4kenr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","active":1},{"hash":"nlo35_T5fzSGZzJApqu8lgIudJvmOQtDaHtr-I4rU7I","active":0}]},"algorithm":"sha256","pepper":"matrixrocks"}}}},"responses":{"201":{"description":"Success"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/app/v1/transactions/{txnId}":{"put":{"parameters":[{"in":"path","name":"txnId","required":true,"schema":{"type":"integer"},"description":"The transaction id"}],"tags":["Application server"],"description":"Implements https://spec.matrix.org/v1.6/application-service-api/#put_matrixappv1transactionstxnid","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"308":{"$ref":"#/components/responses/PermanentRedirect"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object"}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/app/v1/users/{userId}":{"get":{"parameters":[{"in":"path","name":"userId","required":true,"schema":{"type":"integer"},"description":"The user id"}],"tags":["Application server"],"description":"Implements https://spec.matrix.org/v1.6/application-service-api/#get_matrixappv1usersuserid","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/app/v1/rooms/{roomAlias}":{"get":{"parameters":[{"in":"path","name":"roomAlias","required":true,"schema":{"type":"integer"},"description":"The room alias"}],"tags":["Application server"],"description":"Implements https://spec.matrix.org/v1.6/application-service-api/#get_matrixappv1roomsroomalias","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/v1/activecontacts":{"get":{"tags":["Active contacts"],"description":"Get the list of active contacts","responses":{"200":{"description":"Active contacts found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActiveContacts"}}}},"401":{"description":"user is unauthorized"},"404":{"description":"Active contacts not found"},"500":{"description":"Internal error"}}},"post":{"tags":["Active contacts"],"description":"Create or update the list of active contacts","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActiveContacts"}}}},"responses":{"201":{"description":"Active contacts saved"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}},"delete":{"tags":["Active contacts"],"description":"Delete the list of active contacts","responses":{"200":{"description":"Active contacts deleted"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error/"}}}},"/_twake/app/v1/rooms":{"post":{"tags":["Application server"],"description":"Implements https://www.notion.so/Automatic-channels-89ba6f97bc90474ca482a28cf3228d3e","requestBody":{"description":"Object containing room's details","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ldapFilter":{"type":"object","additionalProperties":true,"description":"An object containing keys/values to build a ldap filter"},"aliasName":{"type":"string","description":"The desired room alias local part. If aliasName is equal to foo the complete room alias will be"},"name":{"type":"string","description":"The room name"},"topic":{"type":"string","description":"A short message detailing what is currently being discussed in the room."},"visibility":{"type":"string","enum":["public","private"],"description":"visibility values:\n * `public` - The room will be shown in the published room list\n * `private` - Hide the room from the published room list\n"}},"required":["ldapFilter","aliasName"]},"example":{"ldapFilter":{"mail":"example@test.com","cn":"example"},"aliasName":"exp","name":"Example","topic":"This is an example of a room topic","visibility":"public"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"errcode":{"type":"string"},"error":{"type":"string"}},"additionalProperties":{"type":"string"},"description":"List of users uid not added to the new room due to an error"},"example":[{"uid":"test1","errcode":"M_FORBIDDEN","error":"The user has been banned from the room"},{"uid":"test2","errcode":"M_UNKNOWN","error":"Internal server error"}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"examples":{"example1":{"value":{"error":"Error field: Invalid value (property: name)"}},"example2":{"value":{"errcode":"M_NOT_JSON","error":"Not_json"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"examples":{"example1":{"value":{"error":"This room already exits in Twake database"}},"example2":{"value":{"errcode":"M_ROOM_IN_USE","error":"A room with alias foo already exists in Matrix database"}}}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/mutual_rooms/{target_userid}":{"get":{"tags":["Mutual Rooms"],"description":"Get the list of mutual rooms between two users","parameters":[{"$ref":"#/components/parameters/target_userid"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MutualRooms"}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"},"500":{"description":"Internal error"}}}},"/_twake/private_note":{"get":{"tags":["Private Note"],"description":"Get the private note made by the user for a target user","parameters":[{"$ref":"#/components/parameters/user_id"},{"$ref":"#/components/parameters/target_user_id"}],"responses":{"200":{"description":"Private note found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrivateNote"}}}},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"404":{"description":"Private note not found"},"500":{"description":"Internal error"}}},"post":{"tags":["Private Note"],"description":"Create a private note for a target user","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePrivateNote"}}}},"responses":{"201":{"description":"Private note created"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}},"put":{"tags":["Private Note"],"description":"Update a private note","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePrivateNote"}}}},"responses":{"204":{"description":"Private note created"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/private_note/{private_note_id}":{"delete":{"tags":["Private Note"],"description":"Delete a private note","parameters":[{"$ref":"#/components/parameters/private_note_id"}],"responses":{"204":{"description":"Private note deleted"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/v1/room_tags/{roomId}":{"get":{"tags":["Room tags"],"description":"Get room tags","parameters":[{"$ref":"#/components/parameters/roomId"}],"responses":{"200":{"description":"Room tags found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomTags"}}}},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}},"put":{"tags":["Room tags"],"description":"Update room tags","parameters":[{"$ref":"#/components/parameters/roomId"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomTagsUpdate"}}}},"responses":{"204":{"description":"Room tags updated"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}},"delete":{"tags":["Room tags"],"description":"delete tags for a room","parameters":[{"$ref":"#/components/parameters/roomId"}],"responses":{"204":{"description":"Room tags deleted"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/v1/room_tags":{"post":{"tags":["Room tags"],"description":"Create room tags","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomTagCreation"}}}},"responses":{"201":{"description":"Room tags created"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/app/v1/search":{"post":{"tags":["Search Engine"],"description":"Search performs with OpenSearch on Tchat messages and rooms","requestBody":{"description":"Object containing search query details","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"searchValue":{"type":"string","description":"Value used to perform the search on rooms and messages data"}},"required":["searchValue"]},"example":{"searchValue":"hello"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"rooms":{"type":"array","description":"List of rooms whose name contains the search value","items":{"type":"object","properties":{"room_id":{"type":"string"},"name":{"type":"string"},"avatar_url":{"type":"string","description":"Url of the room's avatar"}}}},"messages":{"type":"array","description":"List of messages whose content or/and sender display name contain the search value","items":{"type":"object","properties":{"room_id":{"type":"string"},"event_id":{"type":"string","description":"Id of the message"},"content":{"type":"string"},"display_name":{"type":"string","description":"Sender display name"},"avatar_url":{"type":"string","description":"Sender's avatar url if it is a direct chat, otherwise it is the room's avatar url"},"room_name":{"type":"string","description":"Room's name in case of the message is not part of a direct chat"}}}},"mails":{"type":"array","description":"List of mails from Tmail whose meta or content contain the search value","items":{"type":"object","properties":{"attachments":{"type":"array","items":{"type":"object","properties":{"contentDisposition":{"type":"string"},"fileExtension":{"type":"string"},"fileName":{"type":"string"},"mediaType":{"type":"string"},"subtype":{"type":"string"},"textContent":{"type":"string"}}}},"bcc":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"cc":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"date":{"type":"string"},"from":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"hasAttachment":{"type":"boolean"},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}}}},"htmlBody":{"type":"string"},"isAnswered":{"type":"boolean"},"isDeleted":{"type":"boolean"},"isDraft":{"type":"boolean"},"isFlagged":{"type":"boolean"},"isRecent":{"type":"boolean"},"isUnread":{"type":"boolean"},"mailboxId":{"type":"string"},"mediaType":{"type":"string"},"messageId":{"type":"string"},"mimeMessageID":{"type":"string"},"modSeq":{"type":"number"},"saveDate":{"type":"string"},"sentDate":{"type":"string"},"size":{"type":"number"},"subject":{"type":"array","items":{"type":"string"}},"subtype":{"type":"string"},"textBody":{"type":"string"},"threadId":{"type":"string"},"to":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"uid":{"type":"number"},"userFlags":{"type":"array","items":{"type":"string"}}}}}}},"example":{"rooms":[{"room_id":"!dYqMpBXVQgKWETVAtJ:example.com","name":"Hello world room","avatar_url":"mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR"},{"room_id":"!dugSgNYwppGGoeJwYB:example.com","name":"Worldwide room","avatar_url":null}],"messages":[{"room_id":"!dYqMpBXVQgKWETVAtJ:example.com","event_id":"$c0hW6db_GUjk0NRBUuO12IyMpi48LE_tQK6sH3dkd1U","content":"Hello world","display_name":"Anakin Skywalker","avatar_url":"mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR","room_name":"Hello world room"},{"room_id":"!ftGqINYwppGGoeJwYB:example.com","event_id":"$IUzFofxHCvvoHJ-k2nfx7OlWOO8AuPvlHHqkeJLzxJ8","content":"Hello world my friends in direct chat","display_name":"Luke Skywalker","avatar_url":"mxc://matrix.org/wefh34uihSDRGhw34"}],"mails":[{"id":"message1","attachments":[{"contentDisposition":"attachment","fileExtension":"jpg","fileName":"image1.jpg","mediaType":"image/jpeg","textContent":"A beautiful galaxy far, far away."}],"bcc":[{"address":"okenobi@example.com","domain":"example.com","name":"Obi-Wan Kenobi"}],"cc":[{"address":"pamidala@example.com","domain":"example.com","name":"Padme Amidala"}],"date":"2024-02-24T10:15:00Z","from":[{"address":"dmaul@example.com","domain":"example.com","name":"Dark Maul"}],"hasAttachment":true,"headers":[{"name":"Header5","value":"Value5"},{"name":"Header6","value":"Value6"}],"htmlBody":"

A beautiful galaxy far, far away.

","isAnswered":true,"isDeleted":false,"isDraft":false,"isFlagged":true,"isRecent":true,"isUnread":false,"mailboxId":"mailbox3","mediaType":"image/jpeg","messageId":"message3","mimeMessageID":"mimeMessageID3","modSeq":98765,"saveDate":"2024-02-24T10:15:00Z","sentDate":"2024-02-24T10:15:00Z","size":4096,"subject":["Star Wars Message 3"],"subtype":"subtype3","textBody":"A beautiful galaxy far, far away.","threadId":"thread3","to":[{"address":"kren@example.com","domain":"example.com","name":"Kylo Ren"}],"uid":987654,"userFlags":["Flag4","Flag5"]}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/app/v1/opensearch/restore":{"post":{"tags":["Search Engine"],"description":"Restore OpenSearch indexes using Matrix homeserver database","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/sms":{"post":{"requestBody":{"description":"SMS object","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sms"}}}},"tags":["SMS"],"description":"Send an SMS to a phone number","responses":{"200":{"description":"SMS sent successfully"},"400":{"description":"Invalid request"},"401":{"description":"Unauthorized"},"500":{"description":"Internal server error"}}}},"/_twake/v1/user_info/{userId}":{"get":{"tags":["User Info"],"description":"Get user info","parameters":[{"$ref":"#/components/parameters/userId"}],"responses":{"200":{"description":"User info found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInfo"}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"404":{"description":"User info not found"},"500":{"description":"Internal server error"}}}}},"tags":[]} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b0c264eb..a44057c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,9 @@ ], "dependencies": { "@remix-run/express": "^1.16.0", - "express": "^4.18.2" + "express": "^4.18.2", + "tweetnacl": "^1.0.3", + "tweetnacl-util": "^0.15.1" }, "devDependencies": { "@crowdsec/express-bouncer": "^0.1.0", @@ -39,6 +41,7 @@ "@types/validator": "^13.11.7", "@typescript-eslint/eslint-plugin": "^5.54.1", "docker-compose": "^0.24.3", + "esbuild": "^0.20.2", "eslint": "^8.35.0", "eslint-config-prettier": "^8.8.0", "eslint-config-standard-with-typescript": "^34.0.0", @@ -66,6 +69,10 @@ "toad-cache": "^3.3.0", "ts-jest": "^29.1.0", "typescript": "^4.9.5" + }, + "optionalDependencies": { + "@nx/nx-darwin-arm64": "^19.4.0", + "@nx/nx-linux-x64-gnu": "^19.4.0" } }, "landing": { @@ -104,9 +111,8 @@ }, "landing/node_modules/typescript": { "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -131,7 +137,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -188,7 +193,6 @@ "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.24.7", "picocolors": "^1.0.0" @@ -198,30 +202,28 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", - "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", - "dev": true, + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", + "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", - "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", - "dev": true, + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helpers": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -240,15 +242,14 @@ "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/@babel/eslint-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.24.7.tgz", - "integrity": "sha512-SO5E3bVxDuxyNxM5agFv480YA2HO6ohZbGxbazZdIk3KQOPOGVNw6q78I9/lbviIf95eq6tPozeYnJLbjnC8IA==", + "version": "7.25.1", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.25.1.tgz", + "integrity": "sha512-Y956ghgTT4j7rKesabkh5WeqgSFZVFwaPR0IWFm7KFHFmmJ4afbG49SmfW4S+GyRPx0Dy5jxEWA5t0rpxfElWg==", "dev": true, "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", @@ -263,6 +264,15 @@ "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" } }, + "node_modules/@babel/eslint-parser/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/@babel/eslint-parser/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -273,12 +283,11 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", - "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", - "dev": true, + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", + "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", "dependencies": { - "@babel/types": "^7.24.7", + "@babel/types": "^7.25.6", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -287,18 +296,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/generator/node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/helper-annotate-as-pure": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", @@ -325,14 +322,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", - "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", - "dev": true, + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", "dependencies": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -344,25 +340,22 @@ "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/@babel/helper-create-class-features-plugin": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz", - "integrity": "sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.4.tgz", + "integrity": "sha512-ro/bFs3/84MDgDmMwbcHgDa8/E6J3QKNTk4xJJnVeFtGE+tL0K26E3pNxhYz2b67fJpt7Aphw5XcploKXuCvCQ==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.8", "@babel/helper-optimise-call-expression": "^7.24.7", - "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-replace-supers": "^7.25.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/traverse": "^7.25.4", "semver": "^6.3.1" }, "engines": { @@ -382,9 +375,9 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz", - "integrity": "sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.2.tgz", + "integrity": "sha512-+wqVGP+DFmqwFD3EH6TMTfUNeqDehV3E/dl+Sd54eaXqm17tEUNbEIn4sVivVowbvUpOtIGxdo3GoXyDH9N/9g==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.24.7", @@ -423,51 +416,14 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", - "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", - "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", - "dev": true, - "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", - "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz", - "integrity": "sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz", + "integrity": "sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==", "dev": true, "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -477,7 +433,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", - "dev": true, "dependencies": { "@babel/traverse": "^7.24.7", "@babel/types": "^7.24.7" @@ -487,16 +442,14 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", - "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", - "dev": true, + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", "@babel/helper-module-imports": "^7.24.7", "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" }, "engines": { "node": ">=6.9.0" @@ -518,23 +471,22 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", - "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", - "dev": true, + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz", - "integrity": "sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.0.tgz", + "integrity": "sha512-NhavI2eWEIz/H9dbrG0TuOicDhNexze43i5z7lEqwYm0WEZVTwnPpA0EafUTP7+6/W79HWIP2cTe3Z5NiSTVpw==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-wrap-function": "^7.24.7" + "@babel/helper-wrap-function": "^7.25.0", + "@babel/traverse": "^7.25.0" }, "engines": { "node": ">=6.9.0" @@ -544,14 +496,14 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz", - "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz", + "integrity": "sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.7", - "@babel/helper-optimise-call-expression": "^7.24.7" + "@babel/helper-member-expression-to-functions": "^7.24.8", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/traverse": "^7.25.0" }, "engines": { "node": ">=6.9.0" @@ -564,7 +516,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", - "dev": true, "dependencies": { "@babel/traverse": "^7.24.7", "@babel/types": "^7.24.7" @@ -586,23 +537,10 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", - "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", - "dev": true, + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "engines": { "node": ">=6.9.0" } @@ -611,43 +549,39 @@ "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/helper-validator-option": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", - "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", - "dev": true, + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz", - "integrity": "sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.0.tgz", + "integrity": "sha512-s6Q1ebqutSiZnEjaofc/UKDyC4SbzV5n5SrA2Gq8UawLycr3i04f1dX4OzoQVnexm6aOCh37SQNYlJ/8Ku+PMQ==", "dev": true, "dependencies": { - "@babel/helper-function-name": "^7.24.7", - "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.0", + "@babel/types": "^7.25.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", - "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", - "dev": true, + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz", + "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==", "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6" }, "engines": { "node": ">=6.9.0" @@ -657,7 +591,6 @@ "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.24.7", "chalk": "^2.4.2", @@ -672,7 +605,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -684,7 +616,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -698,7 +629,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -706,14 +636,12 @@ "node_modules/@babel/highlight/node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/@babel/highlight/node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, "engines": { "node": ">=0.8.0" } @@ -722,7 +650,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, "engines": { "node": ">=4" } @@ -731,7 +658,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -740,10 +666,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", - "dev": true, + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "dependencies": { + "@babel/types": "^7.25.6" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -752,13 +680,28 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz", - "integrity": "sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.3.tgz", + "integrity": "sha512-wUrcsxZg6rqBXG05HG1FPYgsP6EvwF4WpBbxIpWIIYnH8wG0gzx3yZY3dtEHas4sTAOGkbTsc9EGPxwff8lRoA==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/traverse": "^7.25.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.0.tgz", + "integrity": "sha512-Bm4bH2qsX880b/3ziJ8KD711LT7z4u8CFudmjqle65AZj/HNUFhEf90dqYv6O86buWvSBmeQDjv0Tn2aF/bIBA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -768,12 +711,12 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.7.tgz", - "integrity": "sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.0.tgz", + "integrity": "sha512-lXwdNZtTmeVOOFtwM/WDe7yg1PL8sYhRk/XH0FzbR2HDQ0xC+EnQ/JHeoMYSavtU115tnUk0q9CDyq8si+LMAA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -800,13 +743,13 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.7.tgz", - "integrity": "sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.0.tgz", + "integrity": "sha512-tggFrk1AIShG/RUQbEwt2Tr/E+ObkfwrPjR6BjbRvsx24+PSjK8zrq0GWPNCjo8qpRx4DuJzlcvWJqlm+0h3kw==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/traverse": "^7.25.0" }, "engines": { "node": ">=6.9.0" @@ -831,7 +774,6 @@ "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -843,7 +785,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -855,7 +796,6 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, @@ -867,7 +807,6 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -903,12 +842,12 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", - "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.6.tgz", + "integrity": "sha512-aABl0jHw9bZ2karQ/uUD6XP4u0SG22SJrOHFoL6XB1R7dTovOP4TzTlsxOYC5yQ1pdscVK2JTUnF6QL3ARoAiQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -918,12 +857,11 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", - "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", - "dev": true, + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.6.tgz", + "integrity": "sha512-sXaDXaJN9SNLymBdlWFA+bjzBhFD617ZaFiY13dGt7TVslVvVgA6fkZOP7Ki3IGElC45lwHdOTrCtKZGVAWeLQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -936,7 +874,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -948,7 +885,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -960,7 +896,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.24.7" }, @@ -975,7 +910,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -987,7 +921,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -999,7 +932,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -1011,7 +943,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1023,7 +954,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1035,7 +965,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1047,7 +976,6 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -1062,7 +990,6 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -1074,12 +1001,11 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", - "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", - "dev": true, + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.4.tgz", + "integrity": "sha512-uMOCoHVU52BsSWxPOMVv5qKRdeSlPuImUCB2dlPuBSU+W2/ROE7/Zg8F2Kepbk+8yBa68LlRKxO+xgEVWorsDg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1120,15 +1046,15 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.7.tgz", - "integrity": "sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.4.tgz", + "integrity": "sha512-jz8cV2XDDTqjKPwVPJBIjORVEmSGYhdRa8e5k5+vN+uwcjSrSxUaebBRa4ko1jqNF2uxyg8G6XYk30Jv285xzg==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-remap-async-to-generator": "^7.24.7", - "@babel/plugin-syntax-async-generators": "^7.8.4" + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-remap-async-to-generator": "^7.25.0", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/traverse": "^7.25.4" }, "engines": { "node": ">=6.9.0" @@ -1170,12 +1096,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz", - "integrity": "sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.0.tgz", + "integrity": "sha512-yBQjYoOjXlFv9nlXb3f1casSHOZkWr29NX+zChVanLg5Nc157CrbEX9D7hxxtTpuFy7Q0YzmmWfJxzvps4kXrQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1185,13 +1111,13 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz", - "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.4.tgz", + "integrity": "sha512-nZeZHyCWPfjkdU5pA/uHiTaDAFUEqkpzf1YoQT2NeSynCGYq9rxfyI3XpQbfx/a0hSnFH6TGlEXvae5Vi7GD8g==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-class-features-plugin": "^7.25.4", + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1218,18 +1144,16 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz", - "integrity": "sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.4.tgz", + "integrity": "sha512-oexUfaQle2pF/b6E0dwsxQtAol9TLSO88kQvym6HHBWFliV2lGdrPieX+WgMRLSJDVzdYywk7jXbLPuO2KLTLg==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-replace-supers": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-replace-supers": "^7.25.0", + "@babel/traverse": "^7.25.4", "globals": "^11.1.0" }, "engines": { @@ -1239,6 +1163,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-classes/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", @@ -1256,12 +1189,12 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz", - "integrity": "sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz", + "integrity": "sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1301,6 +1234,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.0.tgz", + "integrity": "sha512-YLpb4LlYSc3sCUa35un84poXoraOiQucUTTu8X1j18JV+gNa8E0nyUf/CjZ171IRGr4jEguF+vzJU66QZhn29g==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.0", + "@babel/helper-plugin-utils": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-transform-dynamic-import": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz", @@ -1366,14 +1315,14 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz", - "integrity": "sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==", + "version": "7.25.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.1.tgz", + "integrity": "sha512-TVVJVdW9RKMNgJJlLtHsKDTydjZAbwIsn6ySBPQaEAUU5+gVvlJt/9nRmqVbsV/IBanRjzWoaAQKLoamWVOUuA==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/traverse": "^7.25.1" }, "engines": { "node": ">=6.9.0" @@ -1399,12 +1348,12 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz", - "integrity": "sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.2.tgz", + "integrity": "sha512-HQI+HcTbm9ur3Z2DkO+jgESMAMcYLuN/A7NRw9juzxAezN9AvqvUTnpKP/9kkYANz6u7dFlAyOu44ejuGySlfw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1461,13 +1410,13 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz", - "integrity": "sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz", + "integrity": "sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-simple-access": "^7.24.7" }, "engines": { @@ -1478,15 +1427,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz", - "integrity": "sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz", + "integrity": "sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw==", "dev": true, "dependencies": { - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" + "@babel/helper-module-transforms": "^7.25.0", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.0" }, "engines": { "node": ">=6.9.0" @@ -1625,12 +1574,12 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz", - "integrity": "sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz", + "integrity": "sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, @@ -1657,13 +1606,13 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz", - "integrity": "sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.4.tgz", + "integrity": "sha512-ao8BG7E2b/URaUQGqN3Tlsg+M3KlHY6rJ1O1gXAEUnZoyNQnvKyH87Kfg+FoxSeyWUB8ISZZsC91C44ZuBFytw==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-class-features-plugin": "^7.25.4", + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1721,16 +1670,16 @@ } }, "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.24.7.tgz", - "integrity": "sha512-+Dj06GDZEFRYvclU6k4bme55GKBEWUmByM/eoKuqg4zTNQHiApWRhQph5fxQB2wAEFvRzL1tOEj1RJ19wJrhoA==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.2.tgz", + "integrity": "sha512-KQsqEAVBpU82NM/B/N9j9WOdphom1SZH3R+2V7INrQUH+V9EBFwZsEJl8eBIVeQE62FxJCc70jzEZwqU7RcVqA==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.24.7", "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.8", "@babel/plugin-syntax-jsx": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/types": "^7.25.2" }, "engines": { "node": ">=6.9.0" @@ -1863,12 +1812,12 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz", - "integrity": "sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz", + "integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1878,14 +1827,15 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.7.tgz", - "integrity": "sha512-iLD3UNkgx2n/HrjBesVbYX6j0yqn/sJktvbtKKgcaLIQ4bTTQ8obAypc1VpyHPD2y4Phh9zHOaAt8e/L14wCpw==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.2.tgz", + "integrity": "sha512-lBwRvjSmqiMYe/pS0+1gggjJleUJi7NzjvQ1Fkqtt69hBa/0t1YuW/MLQMAPixfwaQOHUXsd6jeU3Z+vdGv3+A==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.25.0", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", "@babel/plugin-syntax-typescript": "^7.24.7" }, "engines": { @@ -1943,13 +1893,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz", - "integrity": "sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.4.tgz", + "integrity": "sha512-qesBxiWkgN1Q+31xUE9RcMk79eOXXDCv6tfyGMRSs4RGlioSg2WVyQAm07k726cSE56pa+Kb0y9epX2qaXzTvA==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.2", + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1959,19 +1909,20 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.7.tgz", - "integrity": "sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.7", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.7", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.4.tgz", + "integrity": "sha512-W9Gyo+KmcxjGahtt3t9fb14vFRWvPpu5pT6GBlovAK6BTBcxgjfVMSQCfJl4oi35ODrxP6xx2Wr8LNST57Mraw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.25.4", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.3", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.0", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.0", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.0", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", @@ -1992,29 +1943,30 @@ "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.24.7", - "@babel/plugin-transform-async-generator-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoped-functions": "^7.24.7", - "@babel/plugin-transform-block-scoping": "^7.24.7", - "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-class-static-block": "^7.24.7", - "@babel/plugin-transform-classes": "^7.24.7", + "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", - "@babel/plugin-transform-destructuring": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-dotall-regex": "^7.24.7", "@babel/plugin-transform-duplicate-keys": "^7.24.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.0", "@babel/plugin-transform-dynamic-import": "^7.24.7", "@babel/plugin-transform-exponentiation-operator": "^7.24.7", "@babel/plugin-transform-export-namespace-from": "^7.24.7", "@babel/plugin-transform-for-of": "^7.24.7", - "@babel/plugin-transform-function-name": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", "@babel/plugin-transform-json-strings": "^7.24.7", - "@babel/plugin-transform-literals": "^7.24.7", + "@babel/plugin-transform-literals": "^7.25.2", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-member-expression-literals": "^7.24.7", "@babel/plugin-transform-modules-amd": "^7.24.7", - "@babel/plugin-transform-modules-commonjs": "^7.24.7", - "@babel/plugin-transform-modules-systemjs": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-modules-systemjs": "^7.25.0", "@babel/plugin-transform-modules-umd": "^7.24.7", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-new-target": "^7.24.7", @@ -2023,9 +1975,9 @@ "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-object-super": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", - "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.25.4", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-property-literals": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", @@ -2034,16 +1986,16 @@ "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-template-literals": "^7.24.7", - "@babel/plugin-transform-typeof-symbol": "^7.24.7", + "@babel/plugin-transform-typeof-symbol": "^7.24.8", "@babel/plugin-transform-unicode-escapes": "^7.24.7", "@babel/plugin-transform-unicode-property-regex": "^7.24.7", "@babel/plugin-transform-unicode-regex": "^7.24.7", - "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.4", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-corejs3": "^0.10.6", "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.31.0", + "core-js-compat": "^3.37.1", "semver": "^6.3.1" }, "engines": { @@ -2122,9 +2074,9 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", - "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", + "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -2134,33 +2086,28 @@ } }, "node_modules/@babel/template": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", - "dev": true, + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", "dependencies": { "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", - "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", - "dev": true, + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", + "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", "dependencies": { "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7", + "@babel/generator": "^7.25.6", + "@babel/parser": "^7.25.6", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -2168,13 +2115,20 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", - "dev": true, + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", "dependencies": { - "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-string-parser": "^7.24.8", "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, @@ -2191,8 +2145,7 @@ "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" }, "node_modules/@colors/colors": { "version": "1.6.0", @@ -2226,9 +2179,9 @@ } }, "node_modules/@emotion/hash": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", - "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", "dev": true }, "node_modules/@esbuild/aix-ppc64": { @@ -2248,9 +2201,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.6.tgz", - "integrity": "sha512-bSC9YVUjADDy1gae8RrioINU6e1lCkg3VGVwm0QQ2E1CWcC4gnMce9+B6RpxuSsrsXsk1yojn7sp1fnG8erE2g==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", "cpu": [ "arm" ], @@ -2264,9 +2217,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.6.tgz", - "integrity": "sha512-YnYSCceN/dUzUr5kdtUzB+wZprCafuD89Hs0Aqv9QSdwhYQybhXTaSTcrl6X/aWThn1a/j0eEpUBGOE7269REg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", "cpu": [ "arm64" ], @@ -2280,9 +2233,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.6.tgz", - "integrity": "sha512-MVcYcgSO7pfu/x34uX9u2QIZHmXAB7dEiLQC5bBl5Ryqtpj9lT2sg3gNDEsrPEmimSJW2FXIaxqSQ501YLDsZQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", "cpu": [ "x64" ], @@ -2296,9 +2249,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.6.tgz", - "integrity": "sha512-bsDRvlbKMQMt6Wl08nHtFz++yoZHsyTOxnjfB2Q95gato+Yi4WnRl13oC2/PJJA9yLCoRv9gqT/EYX0/zDsyMA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", "cpu": [ "arm64" ], @@ -2312,9 +2265,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.6.tgz", - "integrity": "sha512-xh2A5oPrYRfMFz74QXIQTQo8uA+hYzGWJFoeTE8EvoZGHb+idyV4ATaukaUvnnxJiauhs/fPx3vYhU4wiGfosg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", "cpu": [ "x64" ], @@ -2328,9 +2281,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.6.tgz", - "integrity": "sha512-EnUwjRc1inT4ccZh4pB3v1cIhohE2S4YXlt1OvI7sw/+pD+dIE4smwekZlEPIwY6PhU6oDWwITrQQm5S2/iZgg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", "cpu": [ "arm64" ], @@ -2344,9 +2297,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.6.tgz", - "integrity": "sha512-Uh3HLWGzH6FwpviUcLMKPCbZUAFzv67Wj5MTwK6jn89b576SR2IbEp+tqUHTr8DIl0iDmBAf51MVaP7pw6PY5Q==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", "cpu": [ "x64" ], @@ -2360,9 +2313,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.6.tgz", - "integrity": "sha512-7YdGiurNt7lqO0Bf/U9/arrPWPqdPqcV6JCZda4LZgEn+PTQ5SMEI4MGR52Bfn3+d6bNEGcWFzlIxiQdS48YUw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", "cpu": [ "arm" ], @@ -2376,9 +2329,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.6.tgz", - "integrity": "sha512-bUR58IFOMJX523aDVozswnlp5yry7+0cRLCXDsxnUeQYJik1DukMY+apBsLOZJblpH+K7ox7YrKrHmJoWqVR9w==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", "cpu": [ "arm64" ], @@ -2392,9 +2345,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.6.tgz", - "integrity": "sha512-ujp8uoQCM9FRcbDfkqECoARsLnLfCUhKARTP56TFPog8ie9JG83D5GVKjQ6yVrEVdMie1djH86fm98eY3quQkQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", "cpu": [ "ia32" ], @@ -2408,9 +2361,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.6.tgz", - "integrity": "sha512-y2NX1+X/Nt+izj9bLoiaYB9YXT/LoaQFYvCkVD77G/4F+/yuVXYCWz4SE9yr5CBMbOxOfBcy/xFL4LlOeNlzYQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", "cpu": [ "loong64" ], @@ -2424,9 +2377,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.6.tgz", - "integrity": "sha512-09AXKB1HDOzXD+j3FdXCiL/MWmZP0Ex9eR8DLMBVcHorrWJxWmY8Nms2Nm41iRM64WVx7bA/JVHMv081iP2kUA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", "cpu": [ "mips64el" ], @@ -2440,9 +2393,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.6.tgz", - "integrity": "sha512-AmLhMzkM8JuqTIOhxnX4ubh0XWJIznEynRnZAVdA2mMKE6FAfwT2TWKTwdqMG+qEaeyDPtfNoZRpJbD4ZBv0Tg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", "cpu": [ "ppc64" ], @@ -2456,9 +2409,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.6.tgz", - "integrity": "sha512-Y4Ri62PfavhLQhFbqucysHOmRamlTVK10zPWlqjNbj2XMea+BOs4w6ASKwQwAiqf9ZqcY9Ab7NOU4wIgpxwoSQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", "cpu": [ "riscv64" ], @@ -2472,9 +2425,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.6.tgz", - "integrity": "sha512-SPUiz4fDbnNEm3JSdUW8pBJ/vkop3M1YwZAVwvdwlFLoJwKEZ9L98l3tzeyMzq27CyepDQ3Qgoba44StgbiN5Q==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", "cpu": [ "s390x" ], @@ -2488,9 +2441,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.6.tgz", - "integrity": "sha512-a3yHLmOodHrzuNgdpB7peFGPx1iJ2x6m+uDvhP2CKdr2CwOaqEFMeSqYAHU7hG+RjCq8r2NFujcd/YsEsFgTGw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", "cpu": [ "x64" ], @@ -2504,9 +2457,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.6.tgz", - "integrity": "sha512-EanJqcU/4uZIBreTrnbnre2DXgXSa+Gjap7ifRfllpmyAU7YMvaXmljdArptTHmjrkkKm9BK6GH5D5Yo+p6y5A==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", "cpu": [ "x64" ], @@ -2520,9 +2473,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.6.tgz", - "integrity": "sha512-xaxeSunhQRsTNGFanoOkkLtnmMn5QbA0qBhNet/XLVsc+OVkpIWPHcr3zTW2gxVU5YOHFbIHR9ODuaUdNza2Vw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", "cpu": [ "x64" ], @@ -2536,9 +2489,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.6.tgz", - "integrity": "sha512-gnMnMPg5pfMkZvhHee21KbKdc6W3GR8/JuE0Da1kjwpK6oiFU3nqfHuVPgUX2rsOx9N2SadSQTIYV1CIjYG+xw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", "cpu": [ "x64" ], @@ -2552,9 +2505,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.6.tgz", - "integrity": "sha512-G95n7vP1UnGJPsVdKXllAJPtqjMvFYbN20e8RK8LVLhlTiSOH1sd7+Gt7rm70xiG+I5tM58nYgwWrLs6I1jHqg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", "cpu": [ "arm64" ], @@ -2568,9 +2521,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.6.tgz", - "integrity": "sha512-96yEFzLhq5bv9jJo5JhTs1gI+1cKQ83cUpyxHuGqXVwQtY5Eq54ZEsKs8veKtiKwlrNimtckHEkj4mRh4pPjsg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", "cpu": [ "ia32" ], @@ -2584,9 +2537,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.6.tgz", - "integrity": "sha512-n6d8MOyUrNp6G4VSpRcgjs5xj4A91svJSaiwLIDWVWEsZtpN5FA9NlBbZHDmAJc2e8e6SF4tkBD3HAvPF+7igA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", "cpu": [ "x64" ], @@ -2614,22 +2567,10 @@ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", - "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -2658,55 +2599,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/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/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "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==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@eslint/js": { "version": "8.57.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", @@ -2735,6 +2627,7 @@ "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^2.0.2", @@ -2745,28 +2638,6 @@ "node": ">=10.10.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -2784,6 +2655,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true }, "node_modules/@hutson/parse-repository-url": { @@ -2836,6 +2708,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -2889,7 +2767,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", @@ -2905,7 +2782,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, "dependencies": { "sprintf-js": "~1.0.2" } @@ -2914,7 +2790,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -2927,7 +2802,6 @@ "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -2940,7 +2814,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -2952,7 +2825,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, "dependencies": { "p-try": "^2.0.0" }, @@ -2967,7 +2839,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -2979,7 +2850,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, "engines": { "node": ">=8" } @@ -2987,14 +2857,12 @@ "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, "engines": { "node": ">=8" } @@ -3003,7 +2871,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -3020,7 +2887,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "dev": true, "dependencies": { "@jest/console": "^29.7.0", "@jest/reporters": "^29.7.0", @@ -3067,7 +2933,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dev": true, "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", @@ -3082,7 +2947,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, "dependencies": { "expect": "^29.7.0", "jest-snapshot": "^29.7.0" @@ -3095,7 +2959,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, "dependencies": { "jest-get-type": "^29.6.3" }, @@ -3107,7 +2970,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "dev": true, "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", @@ -3124,7 +2986,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -3139,7 +3000,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "dev": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@jest/console": "^29.7.0", @@ -3178,22 +3038,11 @@ } } }, - "node_modules/@jest/reporters/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/@jest/reporters/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -3209,23 +3058,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@jest/reporters/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, "dependencies": { "@sinclair/typebox": "^0.27.8" }, @@ -3237,7 +3073,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", @@ -3251,7 +3086,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, "dependencies": { "@jest/console": "^29.7.0", "@jest/types": "^29.6.3", @@ -3266,7 +3100,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, "dependencies": { "@jest/test-result": "^29.7.0", "graceful-fs": "^4.2.9", @@ -3281,7 +3114,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", @@ -3307,7 +3139,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", @@ -3324,7 +3155,6 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dev": true, "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -3338,7 +3168,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -3347,7 +3176,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -3363,16 +3191,14 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -3480,16 +3306,6 @@ "node": ">=16.0.0" } }, - "node_modules/@lerna/create/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/@lerna/create/node_modules/chalk": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", @@ -3546,20 +3362,6 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/@lerna/create/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/@lerna/create/node_modules/get-stream": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz", @@ -3572,6 +3374,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@lerna/create/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@lerna/create/node_modules/is-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", @@ -3622,35 +3436,6 @@ "node": ">=8" } }, - "node_modules/@lerna/create/node_modules/ssri": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", - "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", - "dev": true, - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/@lerna/create/node_modules/tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", - "dev": true, - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/@lerna/create/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -3693,12 +3478,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@lerna/create/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@lerna/create/node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -3761,14 +3540,26 @@ "node": ">= 8" } }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "engines": { + "node": ">=12.4.0" + } + }, "node_modules/@npmcli/fs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", - "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", - "devOptional": true, + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, "dependencies": { - "@gar/promisify": "^1.0.1", + "@gar/promisify": "^1.1.3", "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/@npmcli/git": { @@ -3852,27 +3643,17 @@ } }, "node_modules/@npmcli/move-file": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", - "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", "deprecated": "This functionality has been moved to @npmcli/fs", - "devOptional": true, + "dev": true, "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" }, "engines": { - "node": ">=10" - } - }, - "node_modules/@npmcli/move-file/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "devOptional": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/@npmcli/move-file/node_modules/glob": { @@ -3880,7 +3661,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "devOptional": true, + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -3896,24 +3677,12 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@npmcli/move-file/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "devOptional": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@npmcli/move-file/node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", - "devOptional": true, + "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -4079,15 +3848,6 @@ "node": ">=10" } }, - "node_modules/@nx/devkit/node_modules/tmp": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", - "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", - "dev": true, - "engines": { - "node": ">=14.14" - } - }, "node_modules/@nx/devkit/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -4095,13 +3855,12 @@ "dev": true }, "node_modules/@nx/nx-darwin-arm64": { - "version": "16.10.0", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-16.10.0.tgz", - "integrity": "sha512-YF+MIpeuwFkyvM5OwgY/rTNRpgVAI/YiR0yTYCZR+X3AAvP775IVlusNgQ3oedTBRUzyRnI4Tknj1WniENFsvQ==", + "version": "19.6.4", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-19.6.4.tgz", + "integrity": "sha512-kRn2FLvhwJA/TJrNlsCSqqQTrguNZLmiRsiXhvjkfUMbUKwyQfVMgJlvkZ+KoqraUSG+Qyb0FmrGur1I/Mld0Q==", "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -4191,13 +3950,12 @@ } }, "node_modules/@nx/nx-linux-x64-gnu": { - "version": "16.10.0", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-16.10.0.tgz", - "integrity": "sha512-134PW/u/arNFAQKpqMJniC7irbChMPz+W+qtyKPAUXE0XFKPa7c1GtlI/wK2dvP9qJDZ6bKf0KtA0U/m2HMUOA==", + "version": "19.6.4", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-19.6.4.tgz", + "integrity": "sha512-AUMPvLs9KeCUuWD5DdlpbP3VfVsiD0IlptS2b3ul336rsQ7LwwdvE7jTVO5CixFOsiRZxP72fKJhaEargMn5Aw==", "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -4452,14 +4210,14 @@ } }, "node_modules/@opensearch-project/opensearch": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opensearch-project/opensearch/-/opensearch-2.9.0.tgz", - "integrity": "sha512-BXPWSBME1rszZ8OvtBVQ9F6kLiZSENDSFPawbPa1fv0GouuQfWxkKSI9TcnfGLp869fgLTEIfeC5Qexd4RbAYw==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opensearch-project/opensearch/-/opensearch-2.11.0.tgz", + "integrity": "sha512-G+SZwtWRDv90IrtTSNnCt0MQjHVyqrcIXcpwN68vjHnfbun2+RHn+ux4K7dnG+s/KwWzVKIpPFoRjg2gfFX0Mw==", "dependencies": { "aws4": "^1.11.0", "debug": "^4.3.1", "hpagent": "^1.2.0", - "json11": "^1.0.4", + "json11": "^1.1.2", "ms": "^2.1.3", "secure-json-parse": "^2.4.0" }, @@ -4502,18 +4260,18 @@ } }, "node_modules/@playwright/test": { - "version": "1.44.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz", - "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==", + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.1.tgz", + "integrity": "sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA==", "dev": true, "dependencies": { - "playwright": "1.44.1" + "playwright": "1.46.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/@polka/url": { @@ -4531,9 +4289,9 @@ } }, "node_modules/@redis/client": { - "version": "1.5.16", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.16.tgz", - "integrity": "sha512-X1a3xQ5kEMvTib5fBrHKh6Y+pXbeKXqziYuxOUo1ojQNECg4M5Etd1qqyhMap+lFUOAh8S7UYevgJHOm4A+NOg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", + "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -4557,25 +4315,25 @@ } }, "node_modules/@redis/json": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.6.tgz", - "integrity": "sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", "peerDependencies": { "@redis/client": "^1.0.0" } }, "node_modules/@redis/search": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.6.tgz", - "integrity": "sha512-mZXCxbTYKBQ3M2lZnEddwEAks0Kc7nauire8q20oA0oA/LoA+E/b5Y5KZn232ztPb1FkIGqo12vh3Lf+Vw5iTw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", "peerDependencies": { "@redis/client": "^1.0.0" } }, "node_modules/@redis/time-series": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.5.tgz", - "integrity": "sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", "peerDependencies": { "@redis/client": "^1.0.0" } @@ -4664,98 +4422,809 @@ } } }, - "node_modules/@remix-run/dev/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "node_modules/@remix-run/dev/node_modules/@esbuild/android-arm": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.6.tgz", + "integrity": "sha512-bSC9YVUjADDy1gae8RrioINU6e1lCkg3VGVwm0QQ2E1CWcC4gnMce9+B6RpxuSsrsXsk1yojn7sp1fnG8erE2g==", + "cpu": [ + "arm" + ], "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "node": ">=12" } }, - "node_modules/@remix-run/eslint-config": { - "version": "1.19.3", - "resolved": "https://registry.npmjs.org/@remix-run/eslint-config/-/eslint-config-1.19.3.tgz", - "integrity": "sha512-8yvPtsJj1hBKp6ypTTDhHmJ2ftoPmBV1xPPOIqhMLDQ1fwmzocwnkz9RHTeMYkdNSzUuqGnpGaMTHgrT3WfckQ==", + "node_modules/@remix-run/dev/node_modules/@esbuild/android-arm64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.6.tgz", + "integrity": "sha512-YnYSCceN/dUzUr5kdtUzB+wZprCafuD89Hs0Aqv9QSdwhYQybhXTaSTcrl6X/aWThn1a/j0eEpUBGOE7269REg==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "@babel/core": "^7.21.8", - "@babel/eslint-parser": "^7.21.8", - "@babel/preset-react": "^7.18.6", - "@rushstack/eslint-patch": "^1.2.0", - "@typescript-eslint/eslint-plugin": "^5.59.0", - "@typescript-eslint/parser": "^5.59.0", - "eslint-import-resolver-node": "0.3.7", - "eslint-import-resolver-typescript": "^3.5.4", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jest": "^26.9.0", - "eslint-plugin-jest-dom": "^4.0.3", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-react": "^7.32.2", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-testing-library": "^5.10.2" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^8.0.0", - "react": "^17.0.0 || ^18.0.0", - "typescript": "^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=12" } }, - "node_modules/@remix-run/express": { - "version": "1.19.3", - "resolved": "https://registry.npmjs.org/@remix-run/express/-/express-1.19.3.tgz", - "integrity": "sha512-jeWZ9xFyaarKSbpN/sQWx53QApGs16IiB8XC7CkOAEVDtLfOhXkJ9jOZNScOFUn6JXPx2oAwBBRRdbwOmryScQ==", - "dependencies": { - "@remix-run/node": "1.19.3" - }, + "node_modules/@remix-run/dev/node_modules/@esbuild/android-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.6.tgz", + "integrity": "sha512-MVcYcgSO7pfu/x34uX9u2QIZHmXAB7dEiLQC5bBl5Ryqtpj9lT2sg3gNDEsrPEmimSJW2FXIaxqSQ501YLDsZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "express": "^4.17.1" + "node": ">=12" } }, - "node_modules/@remix-run/node": { - "version": "1.19.3", - "resolved": "https://registry.npmjs.org/@remix-run/node/-/node-1.19.3.tgz", - "integrity": "sha512-z5qrVL65xLXIUpU4mkR4MKlMeKARLepgHAk4W5YY3IBXOreRqOGUC70POViYmY7x38c2Ia1NwqL80H+0h7jbMw==", - "dependencies": { - "@remix-run/server-runtime": "1.19.3", - "@remix-run/web-fetch": "^4.3.6", - "@remix-run/web-file": "^3.0.3", - "@remix-run/web-stream": "^1.0.4", - "@web3-storage/multipart-parser": "^1.0.0", - "abort-controller": "^3.0.0", - "cookie-signature": "^1.1.0", - "source-map-support": "^0.5.21", - "stream-slice": "^0.1.2" - }, + "node_modules/@remix-run/dev/node_modules/@esbuild/darwin-arm64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.6.tgz", + "integrity": "sha512-bsDRvlbKMQMt6Wl08nHtFz++yoZHsyTOxnjfB2Q95gato+Yi4WnRl13oC2/PJJA9yLCoRv9gqT/EYX0/zDsyMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=14.0.0" + "node": ">=12" } }, - "node_modules/@remix-run/react": { - "version": "1.19.3", + "node_modules/@remix-run/dev/node_modules/@esbuild/darwin-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.6.tgz", + "integrity": "sha512-xh2A5oPrYRfMFz74QXIQTQo8uA+hYzGWJFoeTE8EvoZGHb+idyV4ATaukaUvnnxJiauhs/fPx3vYhU4wiGfosg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@remix-run/dev/node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.6.tgz", + "integrity": "sha512-EnUwjRc1inT4ccZh4pB3v1cIhohE2S4YXlt1OvI7sw/+pD+dIE4smwekZlEPIwY6PhU6oDWwITrQQm5S2/iZgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@remix-run/dev/node_modules/@esbuild/freebsd-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.6.tgz", + "integrity": "sha512-Uh3HLWGzH6FwpviUcLMKPCbZUAFzv67Wj5MTwK6jn89b576SR2IbEp+tqUHTr8DIl0iDmBAf51MVaP7pw6PY5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@remix-run/dev/node_modules/@esbuild/linux-arm": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.6.tgz", + "integrity": "sha512-7YdGiurNt7lqO0Bf/U9/arrPWPqdPqcV6JCZda4LZgEn+PTQ5SMEI4MGR52Bfn3+d6bNEGcWFzlIxiQdS48YUw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@remix-run/dev/node_modules/@esbuild/linux-arm64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.6.tgz", + "integrity": "sha512-bUR58IFOMJX523aDVozswnlp5yry7+0cRLCXDsxnUeQYJik1DukMY+apBsLOZJblpH+K7ox7YrKrHmJoWqVR9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@remix-run/dev/node_modules/@esbuild/linux-ia32": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.6.tgz", + "integrity": "sha512-ujp8uoQCM9FRcbDfkqECoARsLnLfCUhKARTP56TFPog8ie9JG83D5GVKjQ6yVrEVdMie1djH86fm98eY3quQkQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@remix-run/dev/node_modules/@esbuild/linux-loong64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.6.tgz", + "integrity": "sha512-y2NX1+X/Nt+izj9bLoiaYB9YXT/LoaQFYvCkVD77G/4F+/yuVXYCWz4SE9yr5CBMbOxOfBcy/xFL4LlOeNlzYQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@remix-run/dev/node_modules/@esbuild/linux-mips64el": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.6.tgz", + "integrity": "sha512-09AXKB1HDOzXD+j3FdXCiL/MWmZP0Ex9eR8DLMBVcHorrWJxWmY8Nms2Nm41iRM64WVx7bA/JVHMv081iP2kUA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@remix-run/dev/node_modules/@esbuild/linux-ppc64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.6.tgz", + "integrity": "sha512-AmLhMzkM8JuqTIOhxnX4ubh0XWJIznEynRnZAVdA2mMKE6FAfwT2TWKTwdqMG+qEaeyDPtfNoZRpJbD4ZBv0Tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@remix-run/dev/node_modules/@esbuild/linux-riscv64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.6.tgz", + "integrity": "sha512-Y4Ri62PfavhLQhFbqucysHOmRamlTVK10zPWlqjNbj2XMea+BOs4w6ASKwQwAiqf9ZqcY9Ab7NOU4wIgpxwoSQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@remix-run/dev/node_modules/@esbuild/linux-s390x": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.6.tgz", + "integrity": "sha512-SPUiz4fDbnNEm3JSdUW8pBJ/vkop3M1YwZAVwvdwlFLoJwKEZ9L98l3tzeyMzq27CyepDQ3Qgoba44StgbiN5Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@remix-run/dev/node_modules/@esbuild/linux-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.6.tgz", + "integrity": "sha512-a3yHLmOodHrzuNgdpB7peFGPx1iJ2x6m+uDvhP2CKdr2CwOaqEFMeSqYAHU7hG+RjCq8r2NFujcd/YsEsFgTGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@remix-run/dev/node_modules/@esbuild/netbsd-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.6.tgz", + "integrity": "sha512-EanJqcU/4uZIBreTrnbnre2DXgXSa+Gjap7ifRfllpmyAU7YMvaXmljdArptTHmjrkkKm9BK6GH5D5Yo+p6y5A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@remix-run/dev/node_modules/@esbuild/openbsd-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.6.tgz", + "integrity": "sha512-xaxeSunhQRsTNGFanoOkkLtnmMn5QbA0qBhNet/XLVsc+OVkpIWPHcr3zTW2gxVU5YOHFbIHR9ODuaUdNza2Vw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@remix-run/dev/node_modules/@esbuild/sunos-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.6.tgz", + "integrity": "sha512-gnMnMPg5pfMkZvhHee21KbKdc6W3GR8/JuE0Da1kjwpK6oiFU3nqfHuVPgUX2rsOx9N2SadSQTIYV1CIjYG+xw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@remix-run/dev/node_modules/@esbuild/win32-arm64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.6.tgz", + "integrity": "sha512-G95n7vP1UnGJPsVdKXllAJPtqjMvFYbN20e8RK8LVLhlTiSOH1sd7+Gt7rm70xiG+I5tM58nYgwWrLs6I1jHqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@remix-run/dev/node_modules/@esbuild/win32-ia32": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.6.tgz", + "integrity": "sha512-96yEFzLhq5bv9jJo5JhTs1gI+1cKQ83cUpyxHuGqXVwQtY5Eq54ZEsKs8veKtiKwlrNimtckHEkj4mRh4pPjsg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@remix-run/dev/node_modules/@esbuild/win32-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.6.tgz", + "integrity": "sha512-n6d8MOyUrNp6G4VSpRcgjs5xj4A91svJSaiwLIDWVWEsZtpN5FA9NlBbZHDmAJc2e8e6SF4tkBD3HAvPF+7igA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@remix-run/dev/node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "dev": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@remix-run/dev/node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@remix-run/dev/node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@remix-run/dev/node_modules/esbuild": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.6.tgz", + "integrity": "sha512-TKFRp9TxrJDdRWfSsSERKEovm6v30iHnrjlcGhLBOtReE28Yp1VSBRfO3GTaOFMoxsNerx4TjrhzSuma9ha83Q==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.6", + "@esbuild/android-arm64": "0.17.6", + "@esbuild/android-x64": "0.17.6", + "@esbuild/darwin-arm64": "0.17.6", + "@esbuild/darwin-x64": "0.17.6", + "@esbuild/freebsd-arm64": "0.17.6", + "@esbuild/freebsd-x64": "0.17.6", + "@esbuild/linux-arm": "0.17.6", + "@esbuild/linux-arm64": "0.17.6", + "@esbuild/linux-ia32": "0.17.6", + "@esbuild/linux-loong64": "0.17.6", + "@esbuild/linux-mips64el": "0.17.6", + "@esbuild/linux-ppc64": "0.17.6", + "@esbuild/linux-riscv64": "0.17.6", + "@esbuild/linux-s390x": "0.17.6", + "@esbuild/linux-x64": "0.17.6", + "@esbuild/netbsd-x64": "0.17.6", + "@esbuild/openbsd-x64": "0.17.6", + "@esbuild/sunos-x64": "0.17.6", + "@esbuild/win32-arm64": "0.17.6", + "@esbuild/win32-ia32": "0.17.6", + "@esbuild/win32-x64": "0.17.6" + } + }, + "node_modules/@remix-run/dev/node_modules/esbuild-plugins-node-modules-polyfill": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/esbuild-plugins-node-modules-polyfill/-/esbuild-plugins-node-modules-polyfill-1.6.6.tgz", + "integrity": "sha512-0wDvliv65SCaaGtmoITnmXqqiUzU+ggFupnOgkEo2B9cQ+CUt58ql2+EY6dYoEsoqiHRu2NuTrFUJGMJEgMmLw==", + "dev": true, + "dependencies": { + "@jspm/core": "^2.0.1", + "local-pkg": "^0.5.0", + "resolve.exports": "^2.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.14.0 ^0.23.0" + } + }, + "node_modules/@remix-run/dev/node_modules/fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@remix-run/dev/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@remix-run/dev/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@remix-run/dev/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@remix-run/dev/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@remix-run/dev/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@remix-run/dev/node_modules/local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "dev": true, + "dependencies": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@remix-run/dev/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@remix-run/dev/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@remix-run/dev/node_modules/minimatch/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/@remix-run/dev/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@remix-run/dev/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@remix-run/dev/node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "dev": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remix-run/dev/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@remix-run/dev/node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/@remix-run/dev/node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "node_modules/@remix-run/dev/node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@remix-run/dev/node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/@remix-run/dev/node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/@remix-run/dev/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@remix-run/eslint-config": { + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/@remix-run/eslint-config/-/eslint-config-1.19.3.tgz", + "integrity": "sha512-8yvPtsJj1hBKp6ypTTDhHmJ2ftoPmBV1xPPOIqhMLDQ1fwmzocwnkz9RHTeMYkdNSzUuqGnpGaMTHgrT3WfckQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.21.8", + "@babel/eslint-parser": "^7.21.8", + "@babel/preset-react": "^7.18.6", + "@rushstack/eslint-patch": "^1.2.0", + "@typescript-eslint/eslint-plugin": "^5.59.0", + "@typescript-eslint/parser": "^5.59.0", + "eslint-import-resolver-node": "0.3.7", + "eslint-import-resolver-typescript": "^3.5.4", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jest": "^26.9.0", + "eslint-plugin-jest-dom": "^4.0.3", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-testing-library": "^5.10.2" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^8.0.0", + "react": "^17.0.0 || ^18.0.0", + "typescript": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@remix-run/eslint-config/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@remix-run/eslint-config/node_modules/eslint-import-resolver-node": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", + "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.11.0", + "resolve": "^1.22.1" + } + }, + "node_modules/@remix-run/express": { + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/@remix-run/express/-/express-1.19.3.tgz", + "integrity": "sha512-jeWZ9xFyaarKSbpN/sQWx53QApGs16IiB8XC7CkOAEVDtLfOhXkJ9jOZNScOFUn6JXPx2oAwBBRRdbwOmryScQ==", + "dependencies": { + "@remix-run/node": "1.19.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "express": "^4.17.1" + } + }, + "node_modules/@remix-run/node": { + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/@remix-run/node/-/node-1.19.3.tgz", + "integrity": "sha512-z5qrVL65xLXIUpU4mkR4MKlMeKARLepgHAk4W5YY3IBXOreRqOGUC70POViYmY7x38c2Ia1NwqL80H+0h7jbMw==", + "dependencies": { + "@remix-run/server-runtime": "1.19.3", + "@remix-run/web-fetch": "^4.3.6", + "@remix-run/web-file": "^3.0.3", + "@remix-run/web-stream": "^1.0.4", + "@web3-storage/multipart-parser": "^1.0.0", + "abort-controller": "^3.0.0", + "cookie-signature": "^1.1.0", + "source-map-support": "^0.5.21", + "stream-slice": "^0.1.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@remix-run/react": { + "version": "1.19.3", "resolved": "https://registry.npmjs.org/@remix-run/react/-/react-1.19.3.tgz", "integrity": "sha512-iP37MZ+oG1n4kv4rX77pKT/knra51lNwKo5tinPPF0SuNJhF3+XjWo5nwEjvisKTXLZ/OHeicinhgX2JHHdDvA==", "dependencies": { @@ -4986,9 +5455,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", - "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.2.tgz", + "integrity": "sha512-fSuPrt0ZO8uXeS+xP3b+yYTCBUd05MoSp2N/MFOgjhhUhMmchXlpTQrTpI8T+YAwAQuK7MafsCOxW7VrPMrJcg==", "cpu": [ "arm" ], @@ -4999,9 +5468,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", - "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.2.tgz", + "integrity": "sha512-xGU5ZQmPlsjQS6tzTTGwMsnKUtu0WVbl0hYpTPauvbRAnmIvpInhJtgjj3mcuJpEiuUw4v1s4BimkdfDWlh7gA==", "cpu": [ "arm64" ], @@ -5012,9 +5481,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", - "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.2.tgz", + "integrity": "sha512-99AhQ3/ZMxU7jw34Sq8brzXqWH/bMnf7ZVhvLk9QU2cOepbQSVTns6qoErJmSiAvU3InRqC2RRZ5ovh1KN0d0Q==", "cpu": [ "arm64" ], @@ -5025,9 +5494,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", - "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.2.tgz", + "integrity": "sha512-ZbRaUvw2iN/y37x6dY50D8m2BnDbBjlnMPotDi/qITMJ4sIxNY33HArjikDyakhSv0+ybdUxhWxE6kTI4oX26w==", "cpu": [ "x64" ], @@ -5038,9 +5507,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", - "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.2.tgz", + "integrity": "sha512-ztRJJMiE8nnU1YFcdbd9BcH6bGWG1z+jP+IPW2oDUAPxPjo9dverIOyXz76m6IPA6udEL12reYeLojzW2cYL7w==", "cpu": [ "arm" ], @@ -5051,9 +5520,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", - "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.2.tgz", + "integrity": "sha512-flOcGHDZajGKYpLV0JNc0VFH361M7rnV1ee+NTeC/BQQ1/0pllYcFmxpagltANYt8FYf9+kL6RSk80Ziwyhr7w==", "cpu": [ "arm" ], @@ -5064,9 +5533,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", - "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.2.tgz", + "integrity": "sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw==", "cpu": [ "arm64" ], @@ -5077,9 +5546,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", - "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.2.tgz", + "integrity": "sha512-48pD/fJkTiHAZTnZwR0VzHrao70/4MlzJrq0ZsILjLW/Ab/1XlVUStYyGt7tdyIiVSlGZbnliqmult/QGA2O2w==", "cpu": [ "arm64" ], @@ -5090,9 +5559,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", - "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.2.tgz", + "integrity": "sha512-cZdyuInj0ofc7mAQpKcPR2a2iu4YM4FQfuUzCVA2u4HI95lCwzjoPtdWjdpDKyHxI0UO82bLDoOaLfpZ/wviyQ==", "cpu": [ "ppc64" ], @@ -5103,9 +5572,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", - "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.2.tgz", + "integrity": "sha512-RL56JMT6NwQ0lXIQmMIWr1SW28z4E4pOhRRNqwWZeXpRlykRIlEpSWdsgNWJbYBEWD84eocjSGDu/XxbYeCmwg==", "cpu": [ "riscv64" ], @@ -5116,9 +5585,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", - "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.2.tgz", + "integrity": "sha512-PMxkrWS9z38bCr3rWvDFVGD6sFeZJw4iQlhrup7ReGmfn7Oukrr/zweLhYX6v2/8J6Cep9IEA/SmjXjCmSbrMQ==", "cpu": [ "s390x" ], @@ -5129,9 +5598,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", - "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.2.tgz", + "integrity": "sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw==", "cpu": [ "x64" ], @@ -5142,9 +5611,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", - "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.2.tgz", + "integrity": "sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg==", "cpu": [ "x64" ], @@ -5155,9 +5624,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", - "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.2.tgz", + "integrity": "sha512-9rRero0E7qTeYf6+rFh3AErTNU1VCQg2mn7CQcI44vNUWM9Ze7MSRS/9RFuSsox+vstRt97+x3sOhEey024FRQ==", "cpu": [ "arm64" ], @@ -5168,9 +5637,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", - "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.2.tgz", + "integrity": "sha512-5rA4vjlqgrpbFVVHX3qkrCo/fZTj1q0Xxpg+Z7yIo3J2AilW7t2+n6Q8Jrx+4MrYpAnjttTYF8rr7bP46BPzRw==", "cpu": [ "ia32" ], @@ -5181,9 +5650,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", - "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.2.tgz", + "integrity": "sha512-6UUxd0+SKomjdzuAcp+HAmxw1FlGBnl1v2yEPSabtx4lBfdXHDVsW7+lQkgz9cNFJGY3AWR7+V8P5BqkD9L9nA==", "cpu": [ "x64" ], @@ -5194,9 +5663,9 @@ ] }, "node_modules/@rushstack/eslint-patch": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz", - "integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz", + "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==", "dev": true }, "node_modules/@sigstore/bundle": { @@ -5246,6 +5715,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/@sigstore/sign/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/@sigstore/sign/node_modules/cacache": { "version": "17.1.4", "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz", @@ -5300,23 +5778,21 @@ } }, "node_modules/@sigstore/sign/node_modules/glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -5365,6 +5841,21 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/@sigstore/sign/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@sigstore/sign/node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -5461,8 +5952,7 @@ "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" }, "node_modules/@sindresorhus/is": { "version": "4.6.0", @@ -5480,7 +5970,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, "dependencies": { "type-detect": "4.0.8" } @@ -5489,7 +5978,6 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, "dependencies": { "@sinonjs/commons": "^3.0.0" } @@ -5703,17 +6191,41 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/@tufjs/models": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-1.0.4.tgz", - "integrity": "sha512-qaGV9ltJP0EO25YfFUPhxRVK0evXFIAGicsVXuRim4Ed9cjPxYhNnNJ49SFmbeLgtxpslIkX317IgpfcHPVj/A==", + "node_modules/@tufjs/models": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-1.0.4.tgz", + "integrity": "sha512-qaGV9ltJP0EO25YfFUPhxRVK0evXFIAGicsVXuRim4Ed9cjPxYhNnNJ49SFmbeLgtxpslIkX317IgpfcHPVj/A==", + "dev": true, + "dependencies": { + "@tufjs/canonical-json": "1.0.0", + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models/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/@tufjs/models/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { - "@tufjs/canonical-json": "1.0.0", - "minimatch": "^9.0.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@twake/config-parser": { @@ -5740,6 +6252,10 @@ "resolved": "packages/matrix-application-server", "link": true }, + "node_modules/@twake/matrix-client-server": { + "resolved": "packages/matrix-client-server", + "link": true + }, "node_modules/@twake/matrix-identity-server": { "resolved": "packages/matrix-identity-server", "link": true @@ -5756,6 +6272,10 @@ "resolved": "packages/tom-server", "link": true }, + "node_modules/@twake/utils": { + "resolved": "packages/utils", + "link": true + }, "node_modules/@types/acorn": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz", @@ -5775,7 +6295,6 @@ "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -5788,823 +6307,1330 @@ "version": "7.6.8", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", - "dev": true, "dependencies": { "@babel/types": "^7.0.0" } }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/chai": { + "version": "4.3.19", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.19.tgz", + "integrity": "sha512-2hHHvQBVE2FiSK4eN0Br6snX9MtolHaTo/batnLjlGRhoQzlCL61iVpxoqO7SfFyOw+P/pwv+0zNHzKoGWz9Cw==", + "dev": true + }, + "node_modules/@types/chai-subset": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.5.tgz", + "integrity": "sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, + "node_modules/@types/compression": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.5.tgz", + "integrity": "sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/dockerode": { + "version": "3.3.31", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.31.tgz", + "integrity": "sha512-42R9eoVqJDSvVspV89g7RwRqfNExgievLNWoHkg7NoWIqAmavIbgQBb4oc0qRtHkxE+I3Xxvqv7qVXFABKPBTg==", + "dev": true, + "dependencies": { + "@types/docker-modem": "*", + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/estree-jsx": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-0.0.1.tgz", + "integrity": "sha512-gcLAYiMfQklDCPjQegGn0TBAn9it05ISEsEhlKQUddIk7o2XDokOcTN7HBO8tznM0D9dGezvHEfRZBfZf6me0A==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/events": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz", + "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==", + "dev": true + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "dev": true, + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/js-nacl": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/js-nacl/-/js-nacl-1.3.4.tgz", + "integrity": "sha512-4W/wLwOA5gSRvrwBGQEhuv7a2j2SDBBOsfB5CIZIM+hWEwH0egETDFul2nDJfXrtGc+QdmQoGCzceOuFpZGiKA==", + "dev": true + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true }, - "node_modules/@types/babel__traverse": { - "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", - "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", "dev": true, "dependencies": { - "@babel/types": "^7.20.7" + "@types/node": "*" } }, - "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "node_modules/@types/ldapjs": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-2.2.5.tgz", + "integrity": "sha512-Lv/nD6QDCmcT+V1vaTRnEKE8UgOilVv5pHcQuzkU1LcRe4mbHHuUo/KHi0LKrpdHhQY8FJzryF38fcVdeUIrzg==", "dev": true, "dependencies": { - "@types/connect": "*", "@types/node": "*" } }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "node_modules/@types/lodash": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "dev": true + }, + "node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", "dev": true, "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" + "@types/unist": "^2" } }, - "node_modules/@types/chai": { - "version": "4.3.16", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.16.tgz", - "integrity": "sha512-PatH4iOdyh3MyWtmHVFXLWCCIhUbopaltqddG9BzB+gMIzee2MJrvd+jouii9Z3wzQJruGWAm7WOMjgfG8hQlQ==", + "node_modules/@types/mdurl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz", + "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==", "dev": true }, - "node_modules/@types/chai-subset": { + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true + }, + "node_modules/@types/mime": { "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.5.tgz", - "integrity": "sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true + }, + "node_modules/@types/morgan": { + "version": "1.9.9", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.9.tgz", + "integrity": "sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==", "dev": true, "dependencies": { - "@types/chai": "*" + "@types/node": "*" } }, - "node_modules/@types/compression": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.5.tgz", - "integrity": "sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==", - "dev": true, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true + }, + "node_modules/@types/node": { + "version": "22.5.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.1.tgz", + "integrity": "sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==", "dependencies": { - "@types/express": "*" + "undici-types": "~6.19.2" } }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "node_modules/@types/node-cron": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", + "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", + "dev": true + }, + "node_modules/@types/nodemailer": { + "version": "6.4.15", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.15.tgz", + "integrity": "sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==", "dev": true, "dependencies": { "@types/node": "*" } }, - "node_modules/@types/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" - }, - "node_modules/@types/cookiejar": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", - "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "node_modules/@types/pg": { + "version": "8.11.8", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.8.tgz", + "integrity": "sha512-IqpCf8/569txXN/HoP5i1LjXfKZWL76Yr2R77xgeIICUbAYHeoaEZFhYHo2uDftecLWrTJUq63JvQu8q3lnDyA==", "dev": true, "dependencies": { - "@types/ms": "*" + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" } }, - "node_modules/@types/docker-modem": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", - "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "dev": true + }, + "node_modules/@types/pug": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", + "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", + "dev": true + }, + "node_modules/@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.3.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz", + "integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==", "dev": true, "dependencies": { - "@types/node": "*", - "@types/ssh2": "*" + "@types/prop-types": "*", + "csstype": "^3.0.2" } }, - "node_modules/@types/dockerode": { - "version": "3.3.29", - "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.29.tgz", - "integrity": "sha512-5PRRq/yt5OT/Jf77ltIdz4EiR9+VLnPF+HpU4xGFwUqmV24Co2HKBNW3w+slqZ1CYchbcDeqJASHDYWzZCcMiQ==", + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", "dev": true, "dependencies": { - "@types/docker-modem": "*", - "@types/node": "*", - "@types/ssh2": "*" + "@types/react": "*" } }, - "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true }, - "node_modules/@types/estree-jsx": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-0.0.1.tgz", - "integrity": "sha512-gcLAYiMfQklDCPjQegGn0TBAn9it05ISEsEhlKQUddIk7o2XDokOcTN7HBO8tznM0D9dGezvHEfRZBfZf6me0A==", + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", "dev": true, "dependencies": { - "@types/estree": "*" + "@types/node": "*" } }, - "node_modules/@types/events": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz", - "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==", + "node_modules/@types/retry": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz", + "integrity": "sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==", "dev": true }, - "node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "node_modules/@types/semver": { + "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": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", "dev": true, "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" + "@types/mime": "^1", + "@types/node": "*" } }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.3", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.3.tgz", - "integrity": "sha512-KOzM7MhcBFlmnlr/fzISFF5vGWVSvN6fTd4T+ExOt08bA/dA5kpSzY52nMsI1KDFmUREpJelPYyuslLRSjjgCg==", + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", "dev": true, "dependencies": { + "@types/http-errors": "*", "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", "@types/send": "*" } }, - "node_modules/@types/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "node_modules/@types/ssh2": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.1.tgz", + "integrity": "sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA==", + "dev": true, + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2-streams": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.12.tgz", + "integrity": "sha512-Sy8tpEmCce4Tq0oSOYdfqaBpA3hDM8SoxoFh5vzFsu2oL+znzGz8oVWW7xb4K920yYMUY+PIG31qZnFMfPWNCg==", "dev": true, "dependencies": { - "@types/minimatch": "*", "@types/node": "*" } }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.47.tgz", + "integrity": "sha512-1f7dB3BL/bpd9tnDJrrHb66Y+cVrhxSOTGorRNdHwYTUlTay3HuTDPKo9a/4vX9pMQkhYBcAbL4jQdNlhCFP9A==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", "dev": true, "dependencies": { - "@types/node": "*" + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" } }, - "node_modules/@types/hast": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", - "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "node_modules/@types/supertest": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.16.tgz", + "integrity": "sha512-6c2ogktZ06tr2ENoZivgm7YnprnhYE4ZoXGMY+oA7IuAf17M8FWvujXZGmxLv8y0PTyts4x5A+erSwVUFA8XSg==", "dev": true, "dependencies": { - "@types/unist": "^2" + "@types/superagent": "*" } }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", - "dev": true + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" }, - "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "dev": true }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "node_modules/@types/validator": { + "version": "13.12.1", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.1.tgz", + "integrity": "sha512-w0URwf7BQb0rD/EuiG12KP0bailHKHP5YVviJG9zw3ykAokL0TuxU2TUqMB7EwZ59bDHYdeTIvjI5m0S7qHfOA==", "dev": true }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dependencies": { - "@types/istanbul-lib-coverage": "*" + "@types/yargs-parser": "*" } }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", "dev": true, "dependencies": { - "@types/istanbul-lib-report": "*" + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.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" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@types/jest": { - "version": "29.5.12", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", - "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@types/js-nacl": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@types/js-nacl/-/js-nacl-1.3.4.tgz", - "integrity": "sha512-4W/wLwOA5gSRvrwBGQEhuv7a2j2SDBBOsfB5CIZIM+hWEwH0egETDFul2nDJfXrtGc+QdmQoGCzceOuFpZGiKA==", - "dev": true - }, - "node_modules/@types/js-yaml": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", - "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", - "dev": true + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", "dev": true, "dependencies": { - "@types/node": "*" + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@types/ldapjs": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-2.2.5.tgz", - "integrity": "sha512-Lv/nD6QDCmcT+V1vaTRnEKE8UgOilVv5pHcQuzkU1LcRe4mbHHuUo/KHi0LKrpdHhQY8FJzryF38fcVdeUIrzg==", + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", "dev": true, "dependencies": { - "@types/node": "*" + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } - }, - "node_modules/@types/lodash": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==", - "dev": true - }, - "node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", "dev": true, "dependencies": { - "@types/unist": "^2" + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@types/mdurl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz", - "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==", - "dev": true - }, - "node_modules/@types/methods": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", - "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", - "dev": true - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true - }, - "node_modules/@types/minimatch": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", - "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", - "dev": true - }, - "node_modules/@types/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "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/@types/morgan": { - "version": "1.9.9", - "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.9.tgz", - "integrity": "sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==", + "node_modules/@vanilla-extract/babel-plugin-debug-ids": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@vanilla-extract/babel-plugin-debug-ids/-/babel-plugin-debug-ids-1.0.6.tgz", + "integrity": "sha512-C188vUEYmw41yxg3QooTs8r1IdbDQQ2mH7L5RkORBnHx74QlmsNfqVmKwAVTgrlYt8JoRaWMtPfGm/Ql0BNQrA==", "dev": true, "dependencies": { - "@types/node": "*" + "@babel/core": "^7.23.9" } }, - "node_modules/@types/ms": { - "version": "0.7.34", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", - "dev": true - }, - "node_modules/@types/node": { - "version": "20.14.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", - "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", + "node_modules/@vanilla-extract/css": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@vanilla-extract/css/-/css-1.15.5.tgz", + "integrity": "sha512-N1nQebRWnXvlcmu9fXKVUs145EVwmWtMD95bpiEKtvehHDpUhmO1l2bauS7FGYKbi3dU1IurJbGpQhBclTr1ng==", "dev": true, "dependencies": { - "undici-types": "~5.26.4" + "@emotion/hash": "^0.9.0", + "@vanilla-extract/private": "^1.0.6", + "css-what": "^6.1.0", + "cssesc": "^3.0.0", + "csstype": "^3.0.7", + "dedent": "^1.5.3", + "deep-object-diff": "^1.1.9", + "deepmerge": "^4.2.2", + "lru-cache": "^10.4.3", + "media-query-parser": "^2.0.2", + "modern-ahocorasick": "^1.0.0", + "picocolors": "^1.0.0" } }, - "node_modules/@types/node-cron": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", - "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", + "node_modules/@vanilla-extract/css/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true }, - "node_modules/@types/nodemailer": { - "version": "6.4.15", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.15.tgz", - "integrity": "sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==", + "node_modules/@vanilla-extract/integration": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@vanilla-extract/integration/-/integration-6.5.0.tgz", + "integrity": "sha512-E2YcfO8vA+vs+ua+gpvy1HRqvgWbI+MTlUpxA8FvatOvybuNcWAY0CKwQ/Gpj7rswYKtC6C7+xw33emM6/ImdQ==", "dev": true, "dependencies": { - "@types/node": "*" + "@babel/core": "^7.20.7", + "@babel/plugin-syntax-typescript": "^7.20.0", + "@vanilla-extract/babel-plugin-debug-ids": "^1.0.4", + "@vanilla-extract/css": "^1.14.0", + "esbuild": "npm:esbuild@~0.17.6 || ~0.18.0 || ~0.19.0", + "eval": "0.1.8", + "find-up": "^5.0.0", + "javascript-stringify": "^2.0.1", + "lodash": "^4.17.21", + "mlly": "^1.4.2", + "outdent": "^0.8.0", + "vite": "^5.0.11", + "vite-node": "^1.2.0" } }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dev": true + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@types/pg": { - "version": "8.11.6", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.6.tgz", - "integrity": "sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==", + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], "dev": true, - "dependencies": { - "@types/node": "*", - "pg-protocol": "*", - "pg-types": "^4.0.1" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@types/prop-types": { - "version": "15.7.12", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "dev": true + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@types/pug": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", - "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", - "dev": true + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@types/qs": { - "version": "6.9.15", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", - "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", - "dev": true + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@types/react": { - "version": "18.3.3", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", - "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@types/react-dom": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", - "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@types/react": "*" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", - "dev": true + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "@types/node": "*" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@types/retry": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz", - "integrity": "sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==", - "dev": true - }, - "node_modules/@types/semver": { - "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": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], "dev": true, - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], "dev": true, - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@types/ssh2": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.0.tgz", - "integrity": "sha512-YcT8jP5F8NzWeevWvcyrrLB3zcneVjzYY9ZDSMAMboI+2zR1qYWFhwsyOFVzT7Jorn67vqxC0FRiw8YyG9P1ww==", + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], "dev": true, - "dependencies": { - "@types/node": "^18.11.18" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@types/ssh2-streams": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.12.tgz", - "integrity": "sha512-Sy8tpEmCce4Tq0oSOYdfqaBpA3hDM8SoxoFh5vzFsu2oL+znzGz8oVWW7xb4K920yYMUY+PIG31qZnFMfPWNCg==", + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], "dev": true, - "dependencies": { - "@types/node": "*" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@types/ssh2/node_modules/@types/node": { - "version": "18.19.34", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.34.tgz", - "integrity": "sha512-eXF4pfBNV5DAMKGbI02NnDtWrQ40hAN558/2vvS4gMpMIxaf6JmD7YjnZbq0Q9TDSSkKBamime8ewRoomHdt4g==", + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], "dev": true, - "dependencies": { - "undici-types": "~5.26.4" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true - }, - "node_modules/@types/superagent": { - "version": "8.1.7", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.7.tgz", - "integrity": "sha512-NmIsd0Yj4DDhftfWvvAku482PZum4DBW7U51OvS8gvOkDDY0WT1jsVyDV3hK+vplrsYw8oDwi9QxOM7U68iwww==", + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], "dev": true, - "dependencies": { - "@types/cookiejar": "^2.1.5", - "@types/methods": "^1.1.4", - "@types/node": "*" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@types/supertest": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.16.tgz", - "integrity": "sha512-6c2ogktZ06tr2ENoZivgm7YnprnhYE4ZoXGMY+oA7IuAf17M8FWvujXZGmxLv8y0PTyts4x5A+erSwVUFA8XSg==", + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@types/superagent": "*" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" - }, - "node_modules/@types/unist": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", - "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", - "dev": true - }, - "node_modules/@types/validator": { - "version": "13.11.10", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.10.tgz", - "integrity": "sha512-e2PNXoXLr6Z+dbfx5zSh9TRlXJrELycxiaXznp4S5+D2M3b9bqJEitNHA5923jhnB2zzFiZHa2f0SI1HoIahpg==", - "dev": true - }, - "node_modules/@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@types/yargs-parser": "*" + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.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" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=12" } }, - "node_modules/@typescript-eslint/parser": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "debug": "^4.3.4" - }, + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=12" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], "dev": true, - "dependencies": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=12" } }, - "node_modules/@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], "dev": true, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=12" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "node_modules/@vanilla-extract/integration/node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=12" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" } }, - "node_modules/@typescript-eslint/utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "node_modules/@vanilla-extract/integration/node_modules/rollup": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.2.tgz", + "integrity": "sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw==", "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.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "@types/estree": "1.0.5" }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "bin": { + "rollup": "dist/bin/rollup" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.21.2", + "@rollup/rollup-android-arm64": "4.21.2", + "@rollup/rollup-darwin-arm64": "4.21.2", + "@rollup/rollup-darwin-x64": "4.21.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.21.2", + "@rollup/rollup-linux-arm-musleabihf": "4.21.2", + "@rollup/rollup-linux-arm64-gnu": "4.21.2", + "@rollup/rollup-linux-arm64-musl": "4.21.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.21.2", + "@rollup/rollup-linux-riscv64-gnu": "4.21.2", + "@rollup/rollup-linux-s390x-gnu": "4.21.2", + "@rollup/rollup-linux-x64-gnu": "4.21.2", + "@rollup/rollup-linux-x64-musl": "4.21.2", + "@rollup/rollup-win32-arm64-msvc": "4.21.2", + "@rollup/rollup-win32-ia32-msvc": "4.21.2", + "@rollup/rollup-win32-x64-msvc": "4.21.2", + "fsevents": "~2.3.2" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "node_modules/@vanilla-extract/integration/node_modules/vite": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz", + "integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" + "esbuild": "^0.21.3", + "postcss": "^8.4.41", + "rollup": "^4.20.0" }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "bin": { + "vite": "bin/vite.js" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.0.0 || >=20.0.0" }, "funding": { - "url": "https://opencollective.com/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/@vanilla-extract/babel-plugin-debug-ids": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@vanilla-extract/babel-plugin-debug-ids/-/babel-plugin-debug-ids-1.0.6.tgz", - "integrity": "sha512-C188vUEYmw41yxg3QooTs8r1IdbDQQ2mH7L5RkORBnHx74QlmsNfqVmKwAVTgrlYt8JoRaWMtPfGm/Ql0BNQrA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.23.9" - } - }, - "node_modules/@vanilla-extract/css": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@vanilla-extract/css/-/css-1.15.2.tgz", - "integrity": "sha512-Bi61iCAtojCuqvV+FYaF5i69vBjuMQJpHPdpgKYyQvx+e2Hp79V0ELglyYOdcyg9Wh0k0MFwgCDipVd7EloTXQ==", - "dev": true, - "dependencies": { - "@emotion/hash": "^0.9.0", - "@vanilla-extract/private": "^1.0.5", - "css-what": "^6.1.0", - "cssesc": "^3.0.0", - "csstype": "^3.0.7", - "dedent": "^1.5.3", - "deep-object-diff": "^1.1.9", - "deepmerge": "^4.2.2", - "media-query-parser": "^2.0.2", - "modern-ahocorasick": "^1.0.0", - "picocolors": "^1.0.0" + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } } }, - "node_modules/@vanilla-extract/integration": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@vanilla-extract/integration/-/integration-6.5.0.tgz", - "integrity": "sha512-E2YcfO8vA+vs+ua+gpvy1HRqvgWbI+MTlUpxA8FvatOvybuNcWAY0CKwQ/Gpj7rswYKtC6C7+xw33emM6/ImdQ==", + "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "dependencies": { - "@babel/core": "^7.20.7", - "@babel/plugin-syntax-typescript": "^7.20.0", - "@vanilla-extract/babel-plugin-debug-ids": "^1.0.4", - "@vanilla-extract/css": "^1.14.0", - "esbuild": "npm:esbuild@~0.17.6 || ~0.18.0 || ~0.19.0", - "eval": "0.1.8", - "find-up": "^5.0.0", - "javascript-stringify": "^2.0.1", - "lodash": "^4.17.21", - "mlly": "^1.4.2", - "outdent": "^0.8.0", - "vite": "^5.0.11", - "vite-node": "^1.2.0" + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@vanilla-extract/integration/node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], @@ -6617,10 +7643,10 @@ "node": ">=12" } }, - "node_modules/@vanilla-extract/integration/node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], @@ -6633,10 +7659,10 @@ "node": ">=12" } }, - "node_modules/@vanilla-extract/integration/node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], @@ -6649,10 +7675,10 @@ "node": ">=12" } }, - "node_modules/@vanilla-extract/integration/node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], @@ -6665,10 +7691,10 @@ "node": ">=12" } }, - "node_modules/@vanilla-extract/integration/node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], @@ -6680,11 +7706,11 @@ "engines": { "node": ">=12" } - }, - "node_modules/@vanilla-extract/integration/node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + }, + "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], @@ -6697,10 +7723,10 @@ "node": ">=12" } }, - "node_modules/@vanilla-extract/integration/node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], @@ -6713,10 +7739,10 @@ "node": ">=12" } }, - "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], @@ -6729,10 +7755,10 @@ "node": ">=12" } }, - "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], @@ -6745,10 +7771,10 @@ "node": ">=12" } }, - "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], @@ -6761,10 +7787,10 @@ "node": ">=12" } }, - "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], @@ -6777,10 +7803,10 @@ "node": ">=12" } }, - "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], @@ -6793,10 +7819,10 @@ "node": ">=12" } }, - "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], @@ -6809,10 +7835,10 @@ "node": ">=12" } }, - "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], @@ -6825,10 +7851,10 @@ "node": ">=12" } }, - "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], @@ -6841,10 +7867,10 @@ "node": ">=12" } }, - "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], @@ -6857,10 +7883,10 @@ "node": ">=12" } }, - "node_modules/@vanilla-extract/integration/node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], @@ -6873,10 +7899,10 @@ "node": ">=12" } }, - "node_modules/@vanilla-extract/integration/node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], @@ -6889,10 +7915,10 @@ "node": ">=12" } }, - "node_modules/@vanilla-extract/integration/node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], @@ -6905,10 +7931,10 @@ "node": ">=12" } }, - "node_modules/@vanilla-extract/integration/node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], @@ -6921,10 +7947,10 @@ "node": ">=12" } }, - "node_modules/@vanilla-extract/integration/node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], @@ -6937,10 +7963,10 @@ "node": ">=12" } }, - "node_modules/@vanilla-extract/integration/node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], @@ -6953,100 +7979,10 @@ "node": ">=12" } }, - "node_modules/@vanilla-extract/integration/node_modules/rollup": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", - "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", - "dev": true, - "dependencies": { - "@types/estree": "1.0.5" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.18.0", - "@rollup/rollup-android-arm64": "4.18.0", - "@rollup/rollup-darwin-arm64": "4.18.0", - "@rollup/rollup-darwin-x64": "4.18.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", - "@rollup/rollup-linux-arm-musleabihf": "4.18.0", - "@rollup/rollup-linux-arm64-gnu": "4.18.0", - "@rollup/rollup-linux-arm64-musl": "4.18.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", - "@rollup/rollup-linux-riscv64-gnu": "4.18.0", - "@rollup/rollup-linux-s390x-gnu": "4.18.0", - "@rollup/rollup-linux-x64-gnu": "4.18.0", - "@rollup/rollup-linux-x64-musl": "4.18.0", - "@rollup/rollup-win32-arm64-msvc": "4.18.0", - "@rollup/rollup-win32-ia32-msvc": "4.18.0", - "@rollup/rollup-win32-x64-msvc": "4.18.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/@vanilla-extract/integration/node_modules/vite": { - "version": "5.2.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz", - "integrity": "sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA==", - "dev": true, - "dependencies": { - "esbuild": "^0.20.1", - "postcss": "^8.4.38", - "rollup": "^4.13.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, "node_modules/@vanilla-extract/integration/node_modules/vite/node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, "bin": { @@ -7056,35 +7992,35 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/@vanilla-extract/private": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@vanilla-extract/private/-/private-1.0.5.tgz", - "integrity": "sha512-6YXeOEKYTA3UV+RC8DeAjFk+/okoNz/h88R+McnzA2zpaVqTR/Ep+vszkWYlGBcMNO7vEkqbq5nT/JMMvhi+tw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@vanilla-extract/private/-/private-1.0.6.tgz", + "integrity": "sha512-ytsG/JLweEjw7DBuZ/0JCN4WAQgM9erfSTdS1NQY778hFQSZ6cfCDEZZ0sgVm4k54uNz6ImKB33AYvSR//fjxw==", "dev": true }, "node_modules/@web3-storage/multipart-parser": { @@ -7174,12 +8110,6 @@ "node": ">=6.5" } }, - "node_modules/abstract-logging": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", - "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", - "optional": true - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -7193,9 +8123,9 @@ } }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -7214,10 +8144,13 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", + "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, "engines": { "node": ">=0.4.0" } @@ -7294,7 +8227,6 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, "dependencies": { "type-fest": "^0.21.3" }, @@ -7305,11 +8237,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "devOptional": true, "engines": { "node": ">=8" } @@ -7318,7 +8260,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -7339,7 +8280,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -7355,115 +8295,183 @@ "devOptional": true }, "node_modules/archiver": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", - "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", "dev": true, "dependencies": { - "archiver-utils": "^2.1.0", + "archiver-utils": "^5.0.2", "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", "readdir-glob": "^1.1.2", - "tar-stream": "^2.2.0", - "zip-stream": "^4.1.0" + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" }, "engines": { - "node": ">= 10" + "node": ">= 14" } }, "node_modules/archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", "dev": true, "dependencies": { - "glob": "^7.1.4", + "glob": "^10.0.0", "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", + "lodash": "^4.17.15", "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" + "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/archiver-utils/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "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", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" + } + }, + "node_modules/archiver-utils/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" } }, "node_modules/archiver-utils/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/archiver-utils/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "node_modules/archiver-utils/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } }, - "node_modules/archiver-utils/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" }, "engines": { - "node": "*" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/archiver-utils/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "node_modules/archiver/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" } }, - "node_modules/archiver-utils/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", "dev": true, "dependencies": { - "safe-buffer": "~5.1.0" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/tar-stream": { + "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", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" } }, "node_modules/are-we-there-yet": { @@ -7641,18 +8649,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.toreversed": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz", - "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - } - }, "node_modules/array.prototype.tosorted": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", @@ -7715,15 +8711,6 @@ "safer-buffer": "~2.1.0" } }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "optional": true, - "engines": { - "node": ">=0.8" - } - }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -7752,18 +8739,18 @@ "dev": true }, "node_modules/astring": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/astring/-/astring-1.8.6.tgz", - "integrity": "sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", "dev": true, "bin": { "astring": "bin/astring" } }, "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" }, "node_modules/async-lock": { "version": "1.4.1", @@ -7778,9 +8765,9 @@ "dev": true }, "node_modules/autoprefixer": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", "dev": true, "funding": [ { @@ -7797,11 +8784,11 @@ } ], "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001599", + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -7829,23 +8816,23 @@ } }, "node_modules/aws4": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.0.tgz", - "integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==" }, "node_modules/axe-core": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", - "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.0.tgz", + "integrity": "sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz", + "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==", "dev": true, "dependencies": { "follow-redirects": "^1.15.6", @@ -7854,12 +8841,12 @@ } }, "node_modules/axobject-query": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", - "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", + "integrity": "sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==", "dev": true, "dependencies": { - "dequal": "^2.0.3" + "deep-equal": "^2.0.5" } }, "node_modules/b4a": { @@ -7872,7 +8859,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", @@ -7893,7 +8879,6 @@ "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", @@ -7909,7 +8894,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", @@ -7925,7 +8909,6 @@ "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" } @@ -7934,7 +8917,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", @@ -7969,13 +8951,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", - "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.1", - "core-js-compat": "^3.36.1" + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -7994,23 +8976,25 @@ } }, "node_modules/babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0" @@ -8020,7 +9004,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" @@ -8032,18 +9015,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/backoff": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", - "integrity": "sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA==", - "optional": true, - "dependencies": { - "precond": "0.2" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -8057,20 +9028,19 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "devOptional": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/bare-events": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.3.1.tgz", - "integrity": "sha512-sJnSOTVESURZ61XgEleqmP255T6zTYwHPwE4r6SssIh0U9/uDvfpdoJYpVUerJJZH2fueO+CdT8ZT+OC/7aZDA==", + "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==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.3.tgz", + "integrity": "sha512-7RYKL+vZVCyAsMLi5SPu7QGauGGT8avnP/HO571ndEuV4MYdGXvLhtW67FuLPeEI8EiIY7zbbRR9x7x7HU0kgw==", "dev": true, "optional": true, "dependencies": { @@ -8080,9 +9050,9 @@ } }, "node_modules/bare-os": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.3.0.tgz", - "integrity": "sha512-oPb8oMM1xZbhRQBngTgpcQ5gXw6kjOaRsSWsIeNyRxGed2w/ARyP7ScBYpWR1qfX2E5rS3gBw6OWcSQo+s+kUg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.2.tgz", + "integrity": "sha512-HZoJwzC+rZ9lqEemTMiO0luOePoGYNBgsLLgegKR/cljiJvcDNhDZQkzC+NC5Oh0aHbdBNSOHpghwMuB5tqhjg==", "dev": true, "optional": true }, @@ -8097,9 +9067,9 @@ } }, "node_modules/bare-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.0.1.tgz", - "integrity": "sha512-ubLyoDqPnUf5o0kSFp709HC0WRZuxVuh4pbte5eY95Xvx5bdvz07c2JFmXBfqqe60q+9PJ8S4X5GRvmcNSKMxg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.2.0.tgz", + "integrity": "sha512-+o9MG5bPRRBlkVSpfFlMag3n7wMaIZb4YZasU2+/96f+3HTQ4F9DKQeu3K/Sjz1W0umu6xvVq1ON0ipWdMlr3A==", "dev": true, "optional": true, "dependencies": { @@ -8137,6 +9107,11 @@ "node": ">= 0.8" } }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/basic-ftp": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", @@ -8155,6 +9130,12 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/bcrypt-pbkdf/node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true + }, "node_modules/before-after-hook": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", @@ -8225,14 +9206,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/body-parser/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/body-parser/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -8247,19 +9220,18 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "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, + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/braces": { "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.1.1" }, @@ -8277,10 +9249,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", - "dev": true, + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", "funding": [ { "type": "opencollective", @@ -8296,10 +9267,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" @@ -8324,7 +9295,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, "dependencies": { "node-int64": "^0.4.0" } @@ -8354,12 +9324,12 @@ } }, "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", "dev": true, "engines": { - "node": "*" + "node": ">=8.0.0" } }, "node_modules/buffer-from": { @@ -8417,9 +9387,9 @@ } }, "node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "engines": { "node": ">= 0.8" } @@ -8434,87 +9404,41 @@ } }, "node_modules/cacache": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", - "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", - "devOptional": true, + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, "dependencies": { - "@npmcli/fs": "^1.0.0", - "@npmcli/move-file": "^1.0.1", + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", "infer-owner": "^1.0.4", - "lru-cache": "^6.0.0", - "minipass": "^3.1.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^1.0.3", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", - "ssri": "^8.0.1", - "tar": "^6.0.2", - "unique-filename": "^1.1.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/cacache/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "devOptional": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "devOptional": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" }, "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/cacache/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "devOptional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cacache/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "devOptional": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, "engines": { - "node": "*" + "node": ">=12" } }, "node_modules/cacache/node_modules/rimraf": { @@ -8522,7 +9446,7 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", - "devOptional": true, + "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -8533,11 +9457,26 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/cacache/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "devOptional": true + "node_modules/cacache/node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/cacheable-lookup": { "version": "5.0.4", @@ -8581,16 +9520,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cacheable-request/node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -8619,7 +9548,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "engines": { "node": ">=6" } @@ -8628,7 +9556,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, "engines": { "node": ">=6" } @@ -8660,10 +9587,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001629", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001629.tgz", - "integrity": "sha512-c3dl911slnQhmxUIT4HhYzT7wnBK/XYpGnYLOj4nJBaRiw52Ibe7YxlDaAeRECvA786zCuExhxIUJ2K7nHMrBw==", - "dev": true, + "version": "1.0.30001655", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001655.tgz", + "integrity": "sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg==", "funding": [ { "type": "opencollective", @@ -8680,9 +9606,9 @@ ] }, "node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", @@ -8691,17 +9617,25 @@ "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "type-detect": "^4.1.0" }, "engines": { "node": ">=4" } }, + "node_modules/chai/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -8717,7 +9651,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, "engines": { "node": ">=10" } @@ -8804,6 +9737,18 @@ "fsevents": "~2.3.2" } }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -8817,7 +9762,6 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, "funding": [ { "type": "github", @@ -8829,10 +9773,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", - "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", - "dev": true + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.0.tgz", + "integrity": "sha512-N1NGmowPlGBLsOZLPvm48StN04V4YvQRL0i6b7ctrVY3epjP/ct7hFLOItz6pDIvRjwpfPxi52a2UWV2ziir8g==" }, "node_modules/clean-stack": { "version": "2.2.0", @@ -8880,7 +9823,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -8894,7 +9836,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -8954,6 +9895,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/clone-response/node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -8975,7 +9925,6 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, "engines": { "iojs": ">= 1.0.0", "node": ">= 0.12.0" @@ -8984,8 +9933,7 @@ "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==" }, "node_modules/color": { "version": "3.2.1", @@ -9000,7 +9948,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -9123,18 +10070,59 @@ } }, "node_modules/compress-commons": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", - "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", "dev": true, "dependencies": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.2", + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" + "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 10" + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dev": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/compressible": { @@ -9165,6 +10153,14 @@ "node": ">= 0.8.0" } }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/compression/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -9178,11 +10174,15 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "devOptional": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/concat-stream": { "version": "2.0.0", @@ -9222,25 +10222,6 @@ "node": ">= 0.6" } }, - "node_modules/content-disposition/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/content-type": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", @@ -9368,8 +10349,7 @@ "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/cookie": { "version": "0.4.2", @@ -9394,12 +10374,12 @@ "dev": true }, "node_modules/core-js-compat": { - "version": "3.37.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", - "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", + "version": "3.38.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", + "integrity": "sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==", "dev": true, "dependencies": { - "browserslist": "^4.23.0" + "browserslist": "^4.23.3" }, "funding": { "type": "opencollective", @@ -9466,23 +10446,62 @@ } }, "node_modules/crc32-stream": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", - "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", "dev": true, "dependencies": { "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" + "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 10" + "node": ">= 14" + } + }, + "node_modules/crc32-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dev": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", @@ -9665,9 +10684,9 @@ "optional": true }, "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dependencies": { "ms": "2.1.2" }, @@ -9742,23 +10761,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "devOptional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", - "dev": true, "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, @@ -9837,7 +10843,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -9988,7 +10993,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, "engines": { "node": ">=8" } @@ -10028,7 +11032,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -10098,16 +11101,6 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true }, - "node_modules/dockerode/node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/dockerode/node_modules/tar-fs": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", @@ -10151,15 +11144,15 @@ } }, "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.2.tgz", + "integrity": "sha512-HTlk5nmhkm8F6JcdXvHIzaorzCoziNQT9mGxLPVXW8wJF1TiGSL60ZGB4gHWabHOaMmWmhvk2/lPHfnBiT78AQ==", "dev": true, "engines": { "node": ">=12" }, "funding": { - "url": "https://dotenvx.com" + "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, "node_modules/dotenv-expand": { @@ -10210,6 +11203,12 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/duplexify/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "node_modules/duplexify/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -10246,16 +11245,14 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.792", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.792.tgz", - "integrity": "sha512-rkg5/N3L+Y844JyfgPUyuKK0Hk0efo3JNxUDKvz3HgP6EmN4rNGhr2D8boLsfTV/hGo7ZGAL8djw+jlg99zQyA==", - "dev": true + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz", + "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==" }, "node_modules/emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, "engines": { "node": ">=12" }, @@ -10264,10 +11261,9 @@ } }, "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/emojis-list": { "version": "3.0.0", @@ -10322,9 +11318,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.17.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", - "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -10377,7 +11373,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, "dependencies": { "is-arrayish": "^0.2.1" } @@ -10565,9 +11560,9 @@ "dev": true }, "node_modules/esbuild": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.6.tgz", - "integrity": "sha512-TKFRp9TxrJDdRWfSsSERKEovm6v30iHnrjlcGhLBOtReE28Yp1VSBRfO3GTaOFMoxsNerx4TjrhzSuma9ha83Q==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", "dev": true, "hasInstallScript": true, "bin": { @@ -10577,52 +11572,35 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.17.6", - "@esbuild/android-arm64": "0.17.6", - "@esbuild/android-x64": "0.17.6", - "@esbuild/darwin-arm64": "0.17.6", - "@esbuild/darwin-x64": "0.17.6", - "@esbuild/freebsd-arm64": "0.17.6", - "@esbuild/freebsd-x64": "0.17.6", - "@esbuild/linux-arm": "0.17.6", - "@esbuild/linux-arm64": "0.17.6", - "@esbuild/linux-ia32": "0.17.6", - "@esbuild/linux-loong64": "0.17.6", - "@esbuild/linux-mips64el": "0.17.6", - "@esbuild/linux-ppc64": "0.17.6", - "@esbuild/linux-riscv64": "0.17.6", - "@esbuild/linux-s390x": "0.17.6", - "@esbuild/linux-x64": "0.17.6", - "@esbuild/netbsd-x64": "0.17.6", - "@esbuild/openbsd-x64": "0.17.6", - "@esbuild/sunos-x64": "0.17.6", - "@esbuild/win32-arm64": "0.17.6", - "@esbuild/win32-ia32": "0.17.6", - "@esbuild/win32-x64": "0.17.6" - } - }, - "node_modules/esbuild-plugins-node-modules-polyfill": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/esbuild-plugins-node-modules-polyfill/-/esbuild-plugins-node-modules-polyfill-1.6.4.tgz", - "integrity": "sha512-x3MCOvZrKDGAfqAYS/pZUUSwiN+XH7x84A+Prup0CZBJKuGfuGkTAC4g01D6JPs/GCM9wzZVfd8bmiy+cP/iXA==", - "dev": true, - "dependencies": { - "@jspm/core": "^2.0.1", - "local-pkg": "^0.5.0", - "resolve.exports": "^2.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "esbuild": "^0.14.0 || ^0.15.0 || ^0.16.0 || ^0.17.0 || ^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0" + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "dev": true, + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "engines": { "node": ">=6" } @@ -10665,6 +11643,15 @@ "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", @@ -10788,14 +11775,14 @@ } }, "node_modules/eslint-import-resolver-node": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", - "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==", + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, "dependencies": { "debug": "^3.2.7", - "is-core-module": "^2.11.0", - "resolve": "^1.22.1" + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" } }, "node_modules/eslint-import-resolver-node/node_modules/debug": { @@ -10808,17 +11795,18 @@ } }, "node_modules/eslint-import-resolver-typescript": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz", - "integrity": "sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==", - "dev": true, - "dependencies": { - "debug": "^4.3.4", - "enhanced-resolve": "^5.12.0", - "eslint-module-utils": "^2.7.4", - "fast-glob": "^3.3.1", - "get-tsconfig": "^4.5.0", - "is-core-module": "^2.11.0", + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.3.tgz", + "integrity": "sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA==", + "dev": true, + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.3.5", + "enhanced-resolve": "^5.15.0", + "eslint-module-utils": "^2.8.1", + "fast-glob": "^3.3.2", + "get-tsconfig": "^4.7.5", + "is-bun-module": "^1.0.2", "is-glob": "^4.0.3" }, "engines": { @@ -10829,29 +11817,22 @@ }, "peerDependencies": { "eslint": "*", - "eslint-plugin-import": "*" - } - }, - "node_modules/eslint-import-resolver-typescript/node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" }, - "engines": { - "node": ">=8.6.0" + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } } }, "node_modules/eslint-module-utils": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", - "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.2.tgz", + "integrity": "sha512-3XnC5fDyc8M4J2E8pt8pmSVRX2M+5yWMCfI/kDZwauQeFgzQOuhcRBFKjTeJagqgk4sFKxe1mvNVnaWwImx/Tg==", "dev": true, "dependencies": { "debug": "^3.2.7" @@ -10948,16 +11929,6 @@ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" } }, - "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/eslint-plugin-import/node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", @@ -10979,41 +11950,6 @@ "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-import/node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-plugin-import/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint-plugin-import/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -11023,27 +11959,6 @@ "semver": "bin/semver.js" } }, - "node_modules/eslint-plugin-import/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-plugin-import/node_modules/tsconfig-paths": { - "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", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, "node_modules/eslint-plugin-jest": { "version": "26.9.0", "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-26.9.0.tgz", @@ -11088,27 +12003,27 @@ } }, "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz", - "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.9.0.tgz", + "integrity": "sha512-nOFOCaJG2pYqORjK19lqPqxMO/JpvdCZdPtNdxY3kvom3jTvkAbOvQvD8wuD0G8BYR0IGAGYDlzqWJOh/ybn2g==", "dev": true, "dependencies": { - "@babel/runtime": "^7.23.2", - "aria-query": "^5.3.0", - "array-includes": "^3.1.7", + "aria-query": "~5.1.3", + "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", - "axe-core": "=4.7.0", - "axobject-query": "^3.2.1", + "axe-core": "^4.9.1", + "axobject-query": "~3.1.1", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", - "es-iterator-helpers": "^1.0.15", - "hasown": "^2.0.0", + "es-iterator-helpers": "^1.0.19", + "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", - "object.entries": "^1.1.7", - "object.fromentries": "^2.0.7" + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.0" }, "engines": { "node": ">=4.0" @@ -11117,36 +12032,11 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" } }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } + "node_modules/eslint-plugin-jsx-a11y/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true }, "node_modules/eslint-plugin-n": { "version": "15.7.0", @@ -11173,28 +12063,6 @@ "eslint": ">=7.0.0" } }, - "node_modules/eslint-plugin-n/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-n/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint-plugin-node": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", @@ -11215,16 +12083,6 @@ "eslint": ">=5.16.0" } }, - "node_modules/eslint-plugin-node/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/eslint-plugin-node/node_modules/eslint-plugin-es": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", @@ -11268,18 +12126,6 @@ "node": ">=4" } }, - "node_modules/eslint-plugin-node/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint-plugin-node/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -11290,9 +12136,9 @@ } }, "node_modules/eslint-plugin-promise": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.2.0.tgz", - "integrity": "sha512-QmAqwizauvnKOlifxyDj2ObfULpHQawlg/zQdgEixur9vl0CvZGv/LCJV2rtj3210QCoeGBzVMfMXqGAOr/4fA==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.6.0.tgz", + "integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -11305,35 +12151,35 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.34.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.2.tgz", - "integrity": "sha512-2HCmrU+/JNigDN6tg55cRDKCQWicYAPB38JGSFDQt95jDm8rrvSUo7YPkOIm5l6ts1j1zCvysNcasvfTMQzUOw==", + "version": "7.35.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz", + "integrity": "sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==", "dev": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.2", - "array.prototype.toreversed": "^1.1.2", - "array.prototype.tosorted": "^1.1.3", + "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.0.19", "estraverse": "^5.3.0", + "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.8", "object.fromentries": "^2.0.8", - "object.hasown": "^1.1.4", "object.values": "^1.2.0", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.11" + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "node_modules/eslint-plugin-react-hooks": { @@ -11348,16 +12194,6 @@ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" } }, - "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/eslint-plugin-react/node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -11370,16 +12206,13 @@ "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/eslint-plugin-react/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, - "dependencies": { - "brace-expansion": "^1.1.7" - }, "engines": { - "node": "*" + "node": ">=4.0" } }, "node_modules/eslint-plugin-react/node_modules/resolve": { @@ -11447,15 +12280,6 @@ "node": ">=8.0.0" } }, - "node_modules/eslint-scope/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/eslint-utils": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", @@ -11474,42 +12298,16 @@ "eslint": ">=5" } }, - "node_modules/eslint-visitor-keys": { + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true, "engines": { - "node": ">=10" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=10" } }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { + "node_modules/eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", @@ -11521,55 +12319,29 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "dependencies": { - "type-fest": "^0.20.2" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">=8" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "url": "https://opencollective.com/eslint" } }, - "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==", + "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": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4.0" } }, "node_modules/esm-env": { @@ -11595,23 +12367,10 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -11621,9 +12380,9 @@ } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "dependencies": { "estraverse": "^5.1.0" @@ -11632,6 +12391,15 @@ "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", @@ -11644,7 +12412,7 @@ "node": ">=4.0" } }, - "node_modules/estraverse": { + "node_modules/esrecurse/node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", @@ -11653,6 +12421,15 @@ "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/estree-util-attach-comments": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-2.1.1.tgz", @@ -11828,7 +12605,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -11851,7 +12627,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, "engines": { "node": ">= 0.8.0" } @@ -11881,7 +12656,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", @@ -11941,9 +12715,9 @@ } }, "node_modules/express-rate-limit": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.3.0.tgz", - "integrity": "sha512-ZPfWlcQQ1PsZonB/vqksOsBQV74z5osi/QcdoBCyKJXl/wOVjS1yRDmvkpMM52KJeLbiF2+djwVEnEgVCDdvtw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.0.tgz", + "integrity": "sha512-v1204w3cXu5gCDmAvgvzI6qjzZzoMWKnyVDk3ACgfswTQLYiGen+r8w0VnXnGMmzEN/g8fwIQ4JrFFd4ZP6ssg==", "engines": { "node": ">= 16" }, @@ -11955,9 +12729,9 @@ } }, "node_modules/express-validator": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.1.0.tgz", - "integrity": "sha512-ePn6NXjHRZiZkwTiU1Rl2hy6aUqmi6Cb4/s8sfUsKH7j2yYl9azSpl8xEHcOj1grzzQ+UBEoLWtE1s6FDxW++g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.0.tgz", + "integrity": "sha512-I2ByKD8panjtr8Y05l21Wph9xk7kk64UMyvJCl/fFM/3CTJq8isXYPLeKW/aZBCdb/LYNv63PwhY8khw8VWocA==", "dependencies": { "lodash": "^4.17.21", "validator": "~13.12.0" @@ -11992,25 +12766,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/express/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -12031,14 +12786,17 @@ "node": ">=4" } }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "engines": [ - "node >=0.6.0" - ], - "optional": true + "node_modules/external-editor/node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } }, "node_modules/fast-deep-equal": { "version": "3.1.3", @@ -12053,9 +12811,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -12068,11 +12826,22 @@ "node": ">=8.6.0" } }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -12112,7 +12881,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, "dependencies": { "bser": "2.1.1" } @@ -12203,6 +12971,15 @@ "minimatch": "^5.0.1" } }, + "node_modules/filelist/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/filelist/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -12219,7 +12996,6 @@ "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" }, @@ -12296,16 +13072,6 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/flat-cache/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/flat-cache/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -12327,18 +13093,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/flat-cache/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/flat-cache/node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -12395,9 +13149,9 @@ } }, "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, "dependencies": { "cross-spawn": "^7.0.0", @@ -12507,9 +13261,9 @@ "devOptional": true }, "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "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", @@ -12517,7 +13271,7 @@ "universalify": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=14.14" } }, "node_modules/fs-minipass": { @@ -12535,14 +13289,12 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "devOptional": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -12628,7 +13380,6 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -12637,7 +13388,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -12673,7 +13423,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, "engines": { "node": ">=8.0.0" } @@ -12758,7 +13507,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, "engines": { "node": ">=10" }, @@ -12784,9 +13532,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.7.5", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.5.tgz", - "integrity": "sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.0.tgz", + "integrity": "sha512-Pgba6TExTZ0FJAn1qkJAjIeKoDJ3CsI2ChuLohJnZl/tTU8MVrq3b+2t5UOPfRa4RMsorClBjJALkJUMjG1PAw==", "dev": true, "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -12819,20 +13567,6 @@ "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/git-hooks-list": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/git-hooks-list/-/git-hooks-list-1.0.3.tgz", @@ -12952,15 +13686,24 @@ } }, "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "dependencies": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 6" + "node": ">=10.13.0" + } + }, + "node_modules/glob/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/glob/node_modules/minimatch": { @@ -12976,12 +13719,18 @@ } }, "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "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": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globalthis": { @@ -13071,8 +13820,7 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "devOptional": true + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/graphemer": { "version": "1.4.0", @@ -13149,7 +13897,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -13324,8 +14071,7 @@ "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" }, "node_modules/http-cache-semantics": { "version": "4.1.1", @@ -13404,7 +14150,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, "engines": { "node": ">=10.17.0" } @@ -13477,9 +14222,9 @@ ] }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "engines": { "node": ">= 4" @@ -13503,6 +14248,15 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/ignore-walk/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/ignore-walk/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -13532,10 +14286,9 @@ } }, "node_modules/import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", - "dev": true, + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" @@ -13564,7 +14317,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "devOptional": true, "engines": { "node": ">=0.8.19" } @@ -13589,7 +14341,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "devOptional": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -13785,8 +14536,7 @@ "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, "node_modules/is-async-function": { "version": "2.0.0", @@ -13881,6 +14631,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-bun-module": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.1.0.tgz", + "integrity": "sha512-4mTAVPlrXpaN3jtF0lsnPCMGnq4+qZjVIKq0HCpfcqf8OC1SM5oATCIAPM5V5FN05qp2NNnFndphmdZS9CV3hA==", + "dev": true, + "dependencies": { + "semver": "^7.6.3" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -13905,12 +14664,14 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -14002,7 +14763,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "devOptional": true, "engines": { "node": ">=8" } @@ -14011,7 +14771,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, "engines": { "node": ">=6" } @@ -14110,7 +14869,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -14420,16 +15178,14 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, "engines": { "node": ">=8" } }, "node_modules/istanbul-lib-instrument": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", - "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", - "dev": true, + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dependencies": { "@babel/core": "^7.23.9", "@babel/parser": "^7.23.9", @@ -14445,7 +15201,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", @@ -14459,7 +15214,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, "dependencies": { "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", @@ -14473,7 +15227,6 @@ "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" } @@ -14482,7 +15235,6 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", - "dev": true, "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -14505,16 +15257,13 @@ } }, "node_modules/jackspeak": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz", - "integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=14" - }, "funding": { "url": "https://github.com/sponsors/isaacs" }, @@ -14523,9 +15272,9 @@ } }, "node_modules/jake": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", - "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", "dev": true, "dependencies": { "async": "^3.2.3", @@ -14540,28 +15289,6 @@ "node": ">=10" } }, - "node_modules/jake/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/jake/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/javascript-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz", @@ -14572,7 +15299,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "dev": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -14598,7 +15324,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "dev": true, "dependencies": { "execa": "^5.0.0", "jest-util": "^29.7.0", @@ -14612,7 +15337,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "dev": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -14643,7 +15367,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/test-result": "^29.7.0", @@ -14676,7 +15399,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "dev": true, "dependencies": { "@babel/core": "^7.11.6", "@jest/test-sequencer": "^29.7.0", @@ -14717,22 +15439,11 @@ } } }, - "node_modules/jest-config/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/jest-config/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -14748,23 +15459,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jest-config/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/jest-diff": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", @@ -14779,7 +15477,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "dev": true, "dependencies": { "detect-newline": "^3.0.0" }, @@ -14791,7 +15488,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "dev": true, "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", @@ -14807,7 +15503,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "dev": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -14824,7 +15519,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -14833,7 +15527,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", @@ -14858,7 +15551,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "dev": true, "dependencies": { "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" @@ -14871,7 +15563,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", @@ -14886,7 +15577,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", @@ -14906,7 +15596,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "dev": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -14920,7 +15609,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, "engines": { "node": ">=6" }, @@ -14937,7 +15625,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -14946,7 +15633,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", @@ -14966,7 +15652,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, "dependencies": { "jest-regex-util": "^29.6.3", "jest-snapshot": "^29.7.0" @@ -14979,7 +15664,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "dev": true, "dependencies": { "@jest/console": "^29.7.0", "@jest/environment": "^29.7.0", @@ -15011,7 +15695,6 @@ "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" } @@ -15020,7 +15703,6 @@ "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -15030,7 +15712,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -15059,22 +15740,11 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-runtime/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/jest-runtime/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -15090,23 +15760,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jest-runtime/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/jest-snapshot": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", @@ -15137,7 +15794,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -15154,7 +15810,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "dev": true, "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", @@ -15171,7 +15826,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, "engines": { "node": ">=10" }, @@ -15183,7 +15837,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, "dependencies": { "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", @@ -15202,7 +15855,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "dev": true, "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", @@ -15217,7 +15869,6 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -15229,9 +15880,9 @@ } }, "node_modules/jiti": { - "version": "1.21.3", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.3.tgz", - "integrity": "sha512-uy2bNX5zQ+tESe+TiC7ilGRz8AtRGmnJH55NC5S0nSUjvvvM2hJHmefHErugGXN4pNv4Qx7vLsnNw9qJ9mtIsw==", + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", "dev": true, "bin": { "jiti": "bin/jiti.js" @@ -15267,15 +15918,14 @@ "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" }, "node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "dev": true, + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=6" + "node": ">=4" } }, "node_modules/json-buffer": { @@ -15293,8 +15943,7 @@ "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -15326,7 +15975,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "bin": { "json5": "lib/cli.js" }, @@ -15475,6 +16123,12 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "node_modules/lazystream/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -15484,38 +16138,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/ldap-filter": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz", - "integrity": "sha512-/tFkx5WIn4HuO+6w9lsfxq4FN3O+fDZeO9Mek8dCD8rTUpqzRa766BOBO7BcGkn3X86m5+cBm1/2S/Shzz7gMg==", - "optional": true, - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/ldapjs": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.3.tgz", - "integrity": "sha512-75QiiLJV/PQqtpH+HGls44dXweviFwQ6SiIK27EqzKQ5jU/7UFrl2E5nLdQ3IYRBzJ/AVFJI66u0MZ0uofKYwg==", - "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", - "optional": true, - "dependencies": { - "abstract-logging": "^2.0.0", - "asn1": "^0.2.4", - "assert-plus": "^1.0.0", - "backoff": "^2.5.0", - "ldap-filter": "^0.3.3", - "once": "^1.4.0", - "vasync": "^2.2.0", - "verror": "^1.8.1" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/lerna": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/lerna/-/lerna-7.4.2.tgz", @@ -15605,16 +16227,6 @@ "node": ">=16.0.0" } }, - "node_modules/lerna/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/lerna/node_modules/chalk": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", @@ -15671,27 +16283,44 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/lerna/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==", + "node_modules/lerna/node_modules/get-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz", + "integrity": "sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lerna/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "is-glob": "^4.0.1" }, "engines": { - "node": ">=14.14" + "node": ">= 6" } }, - "node_modules/lerna/node_modules/get-stream": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz", - "integrity": "sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==", + "node_modules/lerna/node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -15747,35 +16376,6 @@ "node": ">=8" } }, - "node_modules/lerna/node_modules/ssri": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", - "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", - "dev": true, - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/lerna/node_modules/tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", - "dev": true, - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/lerna/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -15818,12 +16418,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/lerna/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/lerna/node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -15846,7 +16440,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, "engines": { "node": ">=6" } @@ -16004,16 +16597,18 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.7.tgz", + "integrity": "sha512-x2xON4/Qg2bRIS11KIN9yCNYUjhtiEjNyptjX0mX+pyKHecxuJVLIpfX1lq9ZD6CrC/rB+y4GBi18c6CEcUR+A==" + }, "node_modules/lilconfig": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", - "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", "dev": true, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" + "node": ">=10" } }, "node_modules/lines-and-columns": { @@ -16059,14 +16654,10 @@ } }, "node_modules/local-pkg": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", - "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", "dev": true, - "dependencies": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" - }, "engines": { "node": ">=14" }, @@ -16106,24 +16697,6 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "dev": true - }, - "node_modules/lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", - "dev": true - }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", - "dev": true - }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -16142,12 +16715,6 @@ "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", "dev": true }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true - }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -16166,12 +16733,6 @@ "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", "dev": true }, - "node_modules/lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", - "dev": true - }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -16189,9 +16750,9 @@ } }, "node_modules/logform": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz", - "integrity": "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.1.tgz", + "integrity": "sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==", "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", @@ -16247,7 +16808,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, "dependencies": { "yallist": "^3.0.2" } @@ -16262,19 +16822,18 @@ } }, "node_modules/magic-string": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", "dev": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, "dependencies": { "semver": "^7.5.3" }, @@ -16318,72 +16877,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/make-fetch-happen/node_modules/@npmcli/fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", - "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", - "dev": true, - "dependencies": { - "@gar/promisify": "^1.1.3", - "semver": "^7.3.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/@npmcli/move-file": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", - "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", - "deprecated": "This functionality has been moved to @npmcli/fs", - "dev": true, - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/make-fetch-happen/node_modules/cacache": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", - "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", - "dev": true, - "dependencies": { - "@npmcli/fs": "^2.1.0", - "@npmcli/move-file": "^2.0.0", - "chownr": "^2.0.0", - "fs-minipass": "^2.1.0", - "glob": "^8.0.1", - "infer-owner": "^1.0.4", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "mkdirp": "^1.0.4", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^9.0.0", - "tar": "^6.1.11", - "unique-filename": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/make-fetch-happen/node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", @@ -16393,96 +16886,10 @@ "node": ">=12" } }, - "node_modules/make-fetch-happen/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/make-fetch-happen/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/make-fetch-happen/node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/make-fetch-happen/node_modules/ssri": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", - "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", - "dev": true, - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/unique-filename": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", - "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", - "dev": true, - "dependencies": { - "unique-slug": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/unique-slug": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", - "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, "dependencies": { "tmpl": "1.0.5" } @@ -16927,8 +17334,7 @@ "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" }, "node_modules/merge2": { "version": "1.4.1", @@ -17570,10 +17976,9 @@ ] }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", - "dev": true, + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -17616,18 +18021,20 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, "engines": { "node": ">=6" } }, "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "devOptional": true, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/min-indent": { @@ -17640,18 +18047,14 @@ } }, "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, + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "*" } }, "node_modules/minimist": { @@ -17731,9 +18134,9 @@ } }, "node_modules/minipass-json-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", - "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.2.tgz", + "integrity": "sha512-myxeeTm57lYs8pH2nxPzmEEg8DGIgW+9mv6D4JZD2pa81I/OBjeU7PtICXV6c9eRGTA5JMDsuIPUZRCyBMYNhg==", "dev": true, "dependencies": { "jsonparse": "^1.3.1", @@ -17931,28 +18334,6 @@ "node": ">=8" } }, - "node_modules/multimatch/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/multimatch/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -17971,9 +18352,9 @@ } }, "node_modules/nan": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", - "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==", + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", "dev": true, "optional": true }, @@ -18004,8 +18385,7 @@ "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, "node_modules/natural-compare-lite": { "version": "1.4.0", @@ -18043,9 +18423,9 @@ "dev": true }, "node_modules/node-abi": { - "version": "3.63.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.63.0.tgz", - "integrity": "sha512-vAszCsOUrUxjGAmdnM/pq7gUgie0IRteCQMX6d4A534fQCR93EJU5qgzBvU6EkFfK27s0T3HEV3BOyJIr7OMYw==", + "version": "3.67.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.67.0.tgz", + "integrity": "sha512-bLn/fU/ALVBE9wj+p4Y21ZJWYFjUXLXPi/IewyLZkx3ApxKDNBWCKdReeKOtD8dWpOdDCeMyLh6ZewzcLsG2Nw==", "optional": true, "dependencies": { "semver": "^7.3.5" @@ -18190,9 +18570,9 @@ } }, "node_modules/node-gyp-build": { - "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==", + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.2.tgz", + "integrity": "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==", "dev": true, "bin": { "node-gyp-build": "bin.js", @@ -18200,16 +18580,6 @@ "node-gyp-build-test": "build-test.js" } }, - "node_modules/node-gyp/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/node-gyp/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -18231,18 +18601,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/node-gyp/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/node-gyp/node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -18262,8 +18620,7 @@ "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" }, "node_modules/node-machine-id": { "version": "1.1.12", @@ -18272,23 +18629,22 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" }, "node_modules/nodemailer": { - "version": "6.9.13", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.13.tgz", - "integrity": "sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==", + "version": "6.9.14", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.14.tgz", + "integrity": "sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==", "engines": { "node": ">=6.0.0" } }, "node_modules/nodemon": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.3.tgz", - "integrity": "sha512-m4Vqs+APdKzDFpuaL9F9EVOF85+h070FnkHVEoU4+rmT6Vw0bmNl7s61VEkY/cJkL7RCv1p4urnUDUMrS5rk2w==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz", + "integrity": "sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==", "dev": true, "dependencies": { "chokidar": "^3.5.2", @@ -18303,24 +18659,14 @@ "undefsafe": "^2.0.5" }, "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" } }, "node_modules/nodemon/node_modules/has-flag": { @@ -18332,18 +18678,6 @@ "node": ">=4" } }, - "node_modules/nodemon/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/nodemon/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -18390,7 +18724,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -18610,6 +18943,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/npm-registry-fetch/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/npm-registry-fetch/node_modules/cacache": { "version": "17.1.4", "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz", @@ -18664,23 +19006,21 @@ } }, "node_modules/npm-registry-fetch/node_modules/glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -18741,6 +19081,21 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/npm-registry-fetch/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/npm-registry-fetch/node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -18873,16 +19228,6 @@ "node": ">=4" } }, - "node_modules/npm-run-all/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/npm-run-all/node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -18946,18 +19291,6 @@ "node": ">=4" } }, - "node_modules/npm-run-all/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/npm-run-all/node_modules/path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", @@ -19037,7 +19370,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, "dependencies": { "path-key": "^3.0.0" }, @@ -19163,20 +19495,38 @@ "node": ">=10" } }, - "node_modules/nx-cloud/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==", + "node_modules/nx-cloud/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nx-cloud/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "dev": true, "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" }, "engines": { - "node": ">=14.14" + "node": ">=10" } }, + "node_modules/nx-cloud/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/nx-cloud/node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", @@ -19186,40 +19536,36 @@ "node": ">=12" } }, - "node_modules/nx/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/nx/node_modules/dotenv": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.2.tgz", - "integrity": "sha512-HTlk5nmhkm8F6JcdXvHIzaorzCoziNQT9mGxLPVXW8wJF1TiGSL60ZGB4gHWabHOaMmWmhvk2/lPHfnBiT78AQ==", + "node_modules/nx/node_modules/@nx/nx-darwin-arm64": { + "version": "16.10.0", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-16.10.0.tgz", + "integrity": "sha512-YF+MIpeuwFkyvM5OwgY/rTNRpgVAI/YiR0yTYCZR+X3AAvP775IVlusNgQ3oedTBRUzyRnI4Tknj1WniENFsvQ==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" + "node": ">= 10" } }, - "node_modules/nx/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==", + "node_modules/nx/node_modules/@nx/nx-linux-x64-gnu": { + "version": "16.10.0", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-16.10.0.tgz", + "integrity": "sha512-134PW/u/arNFAQKpqMJniC7irbChMPz+W+qtyKPAUXE0XFKPa7c1GtlI/wK2dvP9qJDZ6bKf0KtA0U/m2HMUOA==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14.14" + "node": ">= 10" } }, "node_modules/nx/node_modules/glob": { @@ -19279,13 +19625,27 @@ "node": ">=10" } }, - "node_modules/nx/node_modules/tmp": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", - "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "node_modules/nx/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, "engines": { - "node": ">=14.14" + "node": ">=4" + } + }, + "node_modules/nx/node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, "node_modules/nx/node_modules/yallist": { @@ -19322,9 +19682,12 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -19418,23 +19781,6 @@ "node": ">= 0.4" } }, - "node_modules/object.hasown": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz", - "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==", - "dev": true, - "dependencies": { - "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.values": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", @@ -19481,7 +19827,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "devOptional": true, "dependencies": { "wrappy": "1" } @@ -19498,7 +19843,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, "dependencies": { "mimic-fn": "^2.1.0" }, @@ -19622,7 +19966,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -19725,7 +20068,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, "engines": { "node": ">=6" } @@ -19746,9 +20088,9 @@ } }, "node_modules/pac-proxy-agent": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.1.tgz", - "integrity": "sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", + "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==", "dev": true, "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", @@ -19756,9 +20098,9 @@ "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.2", - "pac-resolver": "^7.0.0", - "socks-proxy-agent": "^8.0.2" + "https-proxy-agent": "^7.0.5", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.4" }, "engines": { "node": ">= 14" @@ -19790,9 +20132,9 @@ } }, "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, "dependencies": { "agent-base": "^7.0.2", @@ -19803,14 +20145,14 @@ } }, "node_modules/pac-proxy-agent/node_modules/socks-proxy-agent": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", - "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", "dev": true, "dependencies": { "agent-base": "^7.1.1", "debug": "^4.3.4", - "socks": "^2.7.1" + "socks": "^2.8.3" }, "engines": { "node": ">= 14" @@ -19829,6 +20171,12 @@ "node": ">= 14" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, "node_modules/pacote": { "version": "15.2.0", "resolved": "https://registry.npmjs.org/pacote/-/pacote-15.2.0.tgz", @@ -19873,6 +20221,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/pacote/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/pacote/node_modules/cacache": { "version": "17.1.4", "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz", @@ -19927,23 +20284,21 @@ } }, "node_modules/pacote/node_modules/glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -19990,6 +20345,21 @@ "node": ">=12" } }, + "node_modules/pacote/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/pacote/node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -20113,7 +20483,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -20130,8 +20499,7 @@ "node_modules/parse-json/node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/parse-ms": { "version": "2.1.0", @@ -20172,7 +20540,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "engines": { "node": ">=8" } @@ -20181,7 +20548,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -20197,8 +20563,7 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { "version": "1.11.1", @@ -20217,13 +20582,10 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true }, "node_modules/path-scurry/node_modules/minipass": { "version": "7.1.2", @@ -20469,14 +20831,12 @@ "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 + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -20512,7 +20872,6 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, "engines": { "node": ">= 6" } @@ -20521,7 +20880,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, "dependencies": { "find-up": "^4.0.0" }, @@ -20533,7 +20891,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -20546,7 +20903,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -20558,7 +20914,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, "dependencies": { "p-try": "^2.0.0" }, @@ -20573,7 +20928,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -20582,44 +20936,44 @@ } }, "node_modules/pkg-types": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.1.tgz", - "integrity": "sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.0.tgz", + "integrity": "sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==", "dev": true, "dependencies": { "confbox": "^0.1.7", - "mlly": "^1.7.0", + "mlly": "^1.7.1", "pathe": "^1.1.2" } }, "node_modules/playwright": { - "version": "1.44.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz", - "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==", + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.1.tgz", + "integrity": "sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==", "dev": true, "dependencies": { - "playwright-core": "1.44.1" + "playwright-core": "1.46.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" }, "optionalDependencies": { "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.44.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", - "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==", + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.1.tgz", + "integrity": "sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==", "dev": true, "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/playwright/node_modules/fsevents": { @@ -20645,9 +20999,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", "dev": true, "funding": [ { @@ -20665,7 +21019,7 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "source-map-js": "^1.2.0" }, "engines": { @@ -20755,6 +21109,18 @@ } } }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/postcss-modules": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-6.0.0.tgz", @@ -20834,28 +21200,34 @@ } }, "node_modules/postcss-nested": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", - "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "dependencies": { - "postcss-selector-parser": "^6.0.11" + "postcss-selector-parser": "^6.1.1" }, "engines": { "node": ">=12.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, "peerDependencies": { "postcss": "^8.2.14" } }, "node_modules/postcss-selector-parser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", - "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -20942,23 +21314,22 @@ "node": ">=10" } }, - "node_modules/prebuild-install/node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "optional": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } + "node_modules/prebuild-install/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "optional": true }, - "node_modules/precond": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", - "integrity": "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==", + "node_modules/prebuild-install/node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", "optional": true, - "engines": { - "node": ">= 0.6" + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" } }, "node_modules/prelude-ls": { @@ -20999,7 +21370,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -21013,7 +21383,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, "engines": { "node": ">=10" }, @@ -21045,6 +21414,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -21074,7 +21452,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" @@ -21087,7 +21464,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, "engines": { "node": ">=6" } @@ -21242,9 +21618,9 @@ } }, "node_modules/proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, "dependencies": { "agent-base": "^7.0.2", @@ -21264,14 +21640,14 @@ } }, "node_modules/proxy-agent/node_modules/socks-proxy-agent": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", - "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", "dev": true, "dependencies": { "agent-base": "^7.1.1", "debug": "^4.3.4", - "socks": "^2.7.1" + "socks": "^2.8.3" }, "engines": { "node": ">= 14" @@ -21290,10 +21666,10 @@ "dev": true }, "node_modules/pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "devOptional": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -21310,6 +21686,16 @@ "pump": "^2.0.0" } }, + "node_modules/pumpify/node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -21323,7 +21709,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, "funding": [ { "type": "individual", @@ -21415,14 +21800,6 @@ "node": ">= 0.8" } }, - "node_modules/raw-body/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -21473,8 +21850,7 @@ "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, "node_modules/react-refresh": { "version": "0.14.2", @@ -21601,24 +21977,31 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/read-package-json/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/read-package-json/node_modules/glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -21653,6 +22036,21 @@ "node": ">=12" } }, + "node_modules/read-package-json/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/read-package-json/node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -21896,6 +22294,15 @@ "minimatch": "^5.1.0" } }, + "node_modules/readdir-glob/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/readdir-glob/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -21970,19 +22377,16 @@ } }, "node_modules/redis": { - "version": "4.6.14", - "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.14.tgz", - "integrity": "sha512-GrNg/e33HtsQwNXL7kJT+iNFPSwE1IPmd7wzV3j4f2z0EYxZfZE7FVTmUysgAtqQQtg5NXF5SNLR9OdO/UHOfw==", - "workspaces": [ - "./packages/*" - ], + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.0.tgz", + "integrity": "sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==", "dependencies": { "@redis/bloom": "1.2.0", - "@redis/client": "1.5.16", + "@redis/client": "1.6.0", "@redis/graph": "1.1.1", - "@redis/json": "1.0.6", - "@redis/search": "1.1.6", - "@redis/time-series": "1.0.5" + "@redis/json": "1.0.7", + "@redis/search": "1.2.0", + "@redis/time-series": "1.1.0" } }, "node_modules/reflect.getprototypeof": { @@ -22173,7 +22577,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -22200,7 +22603,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -22223,7 +22625,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, "dependencies": { "resolve-from": "^5.0.0" }, @@ -22235,7 +22636,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, "engines": { "node": ">=8" } @@ -22262,7 +22662,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", - "dev": true, "engines": { "node": ">=10" } @@ -22329,6 +22728,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/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/rimraf/node_modules/glob": { "version": "9.3.5", "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", @@ -22402,16 +22810,6 @@ "rollup": "> 1.0" } }, - "node_modules/rollup-plugin-cleaner/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/rollup-plugin-cleaner/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -22433,18 +22831,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rollup-plugin-cleaner/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/rollup-plugin-cleaner/node_modules/rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -22530,9 +22916,23 @@ } }, "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, "node_modules/safe-regex-test": { "version": "1.0.3", @@ -22552,9 +22952,9 @@ } }, "node_modules/safe-stable-stringify": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", - "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", "engines": { "node": ">=10" } @@ -22576,16 +22976,6 @@ "rimraf": "^2.5.2" } }, - "node_modules/sander/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/sander/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -22607,18 +22997,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/sander/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/sander/node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -22658,10 +23036,9 @@ "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "devOptional": true, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -22740,9 +23117,9 @@ "devOptional": true }, "node_modules/set-cookie-parser": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", - "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.0.tgz", + "integrity": "sha512-lXLOiqpkUumhRdFF3k1osNXCy9akgx/dyPZ5p8qAg9seJzXr5ZrlqZuWIMuY6ejOsVLE6flJ5/h3lsn57fQ/PQ==" }, "node_modules/set-function-length": { "version": "1.2.2", @@ -22840,8 +23217,7 @@ "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "devOptional": true + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "node_modules/sigstore": { "version": "1.9.0", @@ -22874,6 +23250,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/sigstore/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/sigstore/node_modules/cacache": { "version": "17.1.4", "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz", @@ -22928,23 +23313,21 @@ } }, "node_modules/sigstore/node_modules/glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -22993,6 +23376,21 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/sigstore/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/sigstore/node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -23169,14 +23567,12 @@ "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, "engines": { "node": ">=8" } @@ -23245,13 +23641,13 @@ "devOptional": true }, "node_modules/sorcery": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz", - "integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.1.tgz", + "integrity": "sha512-o7npfeJE6wi6J9l0/5LKshFzZ2rMatRiCDwYeDQaOzqdzRJwALhX7mk/A/ecg6wjMu7wdZbmXfD2S/vpOg0bdQ==", "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.14", - "buffer-crc32": "^0.2.5", + "buffer-crc32": "^1.0.0", "minimist": "^1.2.0", "sander": "^0.5.0" }, @@ -23294,16 +23690,6 @@ "sort-package-json": "cli.js" } }, - "node_modules/sort-package-json/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/sort-package-json/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -23353,18 +23739,6 @@ "node": ">=8" } }, - "node_modules/sort-package-json/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -23436,9 +23810,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.18", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", - "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", "dev": true }, "node_modules/split": { @@ -23498,6 +23872,30 @@ } } }, + "node_modules/sqlite3/node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/sqlite3/node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/sqlite3/node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -23507,14 +23905,33 @@ "node": ">= 6" } }, - "node_modules/sqlite3/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/sqlite3/node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", "optional": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" } }, "node_modules/sqlite3/node_modules/glob": { @@ -23591,18 +24008,6 @@ "node": ">= 10" } }, - "node_modules/sqlite3/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "optional": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/sqlite3/node_modules/minipass-fetch": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", @@ -23621,13 +24026,10 @@ } }, "node_modules/sqlite3/node_modules/node-addon-api": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", - "integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==", - "optional": true, - "engines": { - "node": "^16 || ^18 || >= 20" - } + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "optional": true }, "node_modules/sqlite3/node_modules/node-gyp": { "version": "8.4.1", @@ -23698,6 +24100,36 @@ "node": ">= 10" } }, + "node_modules/sqlite3/node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/sqlite3/node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/sqlite3/node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, "node_modules/sqlite3/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -23743,15 +24175,15 @@ } }, "node_modules/ssri": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", - "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", - "devOptional": true, + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, "dependencies": { "minipass": "^3.1.1" }, "engines": { - "node": ">= 8" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/stack-trace": { @@ -23766,7 +24198,6 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, "dependencies": { "escape-string-regexp": "^2.0.0" }, @@ -23778,7 +24209,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, "engines": { "node": ">=8" } @@ -23815,9 +24245,9 @@ "integrity": "sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==" }, "node_modules/streamx": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", - "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.19.0.tgz", + "integrity": "sha512-5z6CNR4gtkPbwlxyEqoDGDmWIzoNJqCBt4Eac1ICP9YaIT08ct712cFj0u1rx4F8luAuL+3Qc+RFIdI4OX00kg==", "dev": true, "dependencies": { "fast-fifo": "^1.3.2", @@ -23836,25 +24266,6 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/string-hash": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", @@ -23865,7 +24276,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" @@ -23878,7 +24288,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "devOptional": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -23903,17 +24312,15 @@ "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/string-width/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "devOptional": true + "node_modules/string.prototype.includes": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.0.tgz", + "integrity": "sha512-E34CkBgyeqNDcrbU76cDjL5JLcVrtSdYq0MEh/B10r17pRP4ciHLwTgnuLV8Ay6cgEMLkcBkFCKyFZ43YldYzg==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } }, "node_modules/string.prototype.matchall": { "version": "4.0.11", @@ -23959,6 +24366,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -24026,7 +24443,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "devOptional": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -24051,7 +24467,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, "engines": { "node": ">=8" } @@ -24060,7 +24475,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, "engines": { "node": ">=6" } @@ -24081,7 +24495,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, "engines": { "node": ">=8" }, @@ -24149,6 +24562,15 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/sucrase/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/sucrase/node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -24159,23 +24581,21 @@ } }, "node_modules/sucrase/node_modules/glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -24186,6 +24606,21 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/sucrase/node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -24246,7 +24681,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -24258,7 +24692,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -24276,15 +24709,13 @@ } }, "node_modules/svelte-check": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.8.0.tgz", - "integrity": "sha512-7Nxn+3X97oIvMzYJ7t27w00qUf1Y52irE2RU2dQAd5PyvfGp4E7NLhFKVhb6PV2fx7dCRMpNKDIuazmGthjpSQ==", + "version": "3.8.6", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.8.6.tgz", + "integrity": "sha512-ij0u4Lw/sOTREP13BdWZjiXD/BlHE6/e2e34XzmVmsp5IN4kVa3PWP65NM32JAgwjZlwBg/+JtiNV1MM8khu0Q==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.17", "chokidar": "^3.4.1", - "fast-glob": "^3.2.7", - "import-fresh": "^3.2.1", "picocolors": "^1.0.0", "sade": "^1.7.4", "svelte-preprocess": "^5.1.3", @@ -24298,9 +24729,9 @@ } }, "node_modules/svelte-check/node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -24416,16 +24847,6 @@ "node": ">=12.0.0" } }, - "node_modules/swagger-jsdoc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/swagger-jsdoc/node_modules/glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", @@ -24447,18 +24868,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/swagger-jsdoc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/swagger-jsdoc/node_modules/yaml": { "version": "2.0.0-1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", @@ -24487,9 +24896,9 @@ "dev": true }, "node_modules/tailwindcss": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", - "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz", + "integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==", "dev": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -24523,55 +24932,6 @@ "node": ">=14.0.0" } }, - "node_modules/tailwindcss/node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/tailwindcss/node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/tailwindcss/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/tailwindcss/node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -24582,48 +24942,45 @@ } }, "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", "devOptional": true, "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", + "minipass": "^3.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" }, "engines": { - "node": ">=10" + "node": ">= 10" } }, "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "devOptional": true, + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dev": true, "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", - "tar-stream": "^2.1.4" + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" } }, - "node_modules/tar-fs/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "devOptional": true - }, - "node_modules/tar-fs/node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "devOptional": true, + "node_modules/tar-fs/node_modules/tar-stream": { + "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": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" } }, "node_modules/tar-stream": { @@ -24642,15 +24999,6 @@ "node": ">=6" } }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "devOptional": true, - "engines": { - "node": ">=8" - } - }, "node_modules/tar/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -24667,9 +25015,9 @@ } }, "node_modules/terser": { - "version": "5.31.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.1.tgz", - "integrity": "sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg==", + "version": "5.31.6", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz", + "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -24694,7 +25042,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", @@ -24704,22 +25051,11 @@ "node": ">=8" } }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/test-exclude/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -24735,109 +25071,33 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/testcontainers": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.9.0.tgz", - "integrity": "sha512-LN+cKAOd61Up9SVMJW+3VFVGeVQG8JBqZhEQo2U0HBfIsAynyAXcsLBSo+KZrOfy9SBz7pGHctWN/KabLDbNFA==", + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.12.0.tgz", + "integrity": "sha512-KEtFj7VvfZPZuyugYJe5aYC/frFN2LRHwQVOVbdZf1vYYGDa4VQt6d0/bM3PcgTE1BOAY6cWBD/S41yu4JQ1Kg==", "dev": true, "dependencies": { "@balena/dockerignore": "^1.0.2", - "@types/dockerode": "^3.3.24", - "archiver": "^5.3.2", + "@types/dockerode": "^3.3.29", + "archiver": "^7.0.1", "async-lock": "^1.4.1", "byline": "^5.0.0", - "debug": "^4.3.4", - "docker-compose": "^0.24.6", + "debug": "^4.3.5", + "docker-compose": "^0.24.8", "dockerode": "^3.3.5", "get-port": "^5.1.1", - "node-fetch": "^2.7.0", "proper-lockfile": "^4.1.2", "properties-reader": "^2.3.0", "ssh-remote-port-forward": "^1.0.4", - "tar-fs": "^3.0.5", - "tmp": "^0.2.1" - } - }, - "node_modules/testcontainers/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/testcontainers/node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/testcontainers/node_modules/tar-fs": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", - "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", - "dev": true, - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - }, - "optionalDependencies": { - "bare-fs": "^2.1.1", - "bare-path": "^2.1.0" - } - }, - "node_modules/testcontainers/node_modules/tar-stream": { - "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", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/testcontainers/node_modules/tmp": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", - "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", - "dev": true, - "engines": { - "node": ">=14.14" + "tar-fs": "^3.0.6", + "tmp": "^0.2.3", + "undici": "^5.28.4" } }, "node_modules/text-decoder": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.0.tgz", - "integrity": "sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.1.tgz", + "integrity": "sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==", "dev": true, "dependencies": { "b4a": "^1.6.4" @@ -24921,6 +25181,12 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/through2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "node_modules/through2/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -24947,9 +25213,9 @@ "devOptional": true }, "node_modules/tinybench": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", - "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true }, "node_modules/tinypool": { @@ -24971,28 +25237,23 @@ } }, "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", "dev": true, - "dependencies": { - "os-tmpdir": "~1.0.2" - }, "engines": { - "node": ">=0.6.0" + "node": ">=14.14" } }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==" }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, "engines": { "node": ">=4" } @@ -25001,7 +25262,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -25089,19 +25349,20 @@ "dev": true }, "node_modules/ts-jest": { - "version": "29.1.4", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.4.tgz", - "integrity": "sha512-YiHwDhSvCiItoAgsKtoLFCuakDzDsJ1DLDnSouTaTmdOcOwIkSzbLXduaQ6M5DRVhuZC/NYaaZ/mtHbWMv/S6Q==", + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", "dev": true, "dependencies": { - "bs-logger": "0.x", - "fast-json-stable-stringify": "2.x", + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", "jest-util": "^29.0.0", "json5": "^2.2.3", - "lodash.memoize": "4.x", - "make-error": "1.x", - "semver": "^7.5.3", - "yargs-parser": "^21.0.1" + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" }, "bin": { "ts-jest": "cli.js" @@ -25145,17 +25406,27 @@ } }, "node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "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": { - "json5": "^2.2.2", + "@types/json5": "^0.0.29", + "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" }, - "engines": { - "node": ">=6" + "bin": { + "json5": "lib/cli.js" } }, "node_modules/tsconfig-paths/node_modules/strip-bom": { @@ -25168,9 +25439,9 @@ } }, "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "dev": true }, "node_modules/tsutils": { @@ -25220,6 +25491,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/tuf-js/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/tuf-js/node_modules/cacache": { "version": "17.1.4", "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz", @@ -25274,23 +25554,21 @@ } }, "node_modules/tuf-js/node_modules/glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -25339,6 +25617,21 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/tuf-js/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/tuf-js/node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -25432,10 +25725,14 @@ } }, "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "dev": true + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + }, + "node_modules/tweetnacl-util": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", + "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==" }, "node_modules/type-check": { "version": "0.4.0", @@ -25453,15 +25750,14 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, "engines": { "node": ">=4" } }, "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "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" @@ -25575,15 +25871,15 @@ } }, "node_modules/ufo": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", - "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", "dev": true }, "node_modules/uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", "dev": true, "optional": true, "bin": { @@ -25627,10 +25923,9 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", @@ -25704,21 +25999,27 @@ } }, "node_modules/unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "devOptional": true, + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, "dependencies": { - "unique-slug": "^2.0.0" + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "devOptional": true, + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, "dependencies": { "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/unist-builder": { @@ -25873,10 +26174,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", "funding": [ { "type": "opencollective", @@ -25974,10 +26274,9 @@ "dev": true }, "node_modules/v8-to-istanbul": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", - "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", - "dev": true, + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", @@ -26025,58 +26324,6 @@ "node": ">= 0.8" } }, - "node_modules/vasync": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.1.tgz", - "integrity": "sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==", - "engines": [ - "node >=0.6.0" - ], - "optional": true, - "dependencies": { - "verror": "1.10.0" - } - }, - "node_modules/vasync/node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "optional": true - }, - "node_modules/vasync/node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "engines": [ - "node >=0.6.0" - ], - "optional": true, - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "optional": true, - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/verror/node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "optional": true - }, "node_modules/vfile": { "version": "5.3.7", "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", @@ -26184,10 +26431,26 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/vite-node/node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], @@ -26199,11 +26462,11 @@ "engines": { "node": ">=12" } - }, - "node_modules/vite-node/node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], @@ -26217,9 +26480,9 @@ } }, "node_modules/vite-node/node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], @@ -26233,9 +26496,9 @@ } }, "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], @@ -26249,9 +26512,9 @@ } }, "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], @@ -26265,9 +26528,9 @@ } }, "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], @@ -26281,9 +26544,9 @@ } }, "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], @@ -26297,9 +26560,9 @@ } }, "node_modules/vite-node/node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], @@ -26313,9 +26576,9 @@ } }, "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], @@ -26329,9 +26592,9 @@ } }, "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], @@ -26345,9 +26608,9 @@ } }, "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], @@ -26361,9 +26624,9 @@ } }, "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], @@ -26377,9 +26640,9 @@ } }, "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], @@ -26393,9 +26656,9 @@ } }, "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], @@ -26409,9 +26672,9 @@ } }, "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], @@ -26425,9 +26688,9 @@ } }, "node_modules/vite-node/node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], @@ -26441,9 +26704,9 @@ } }, "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], @@ -26457,9 +26720,9 @@ } }, "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], @@ -26473,9 +26736,9 @@ } }, "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], @@ -26489,9 +26752,9 @@ } }, "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], @@ -26505,9 +26768,9 @@ } }, "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], @@ -26521,9 +26784,9 @@ } }, "node_modules/vite-node/node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], @@ -26537,9 +26800,9 @@ } }, "node_modules/vite-node/node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, "bin": { @@ -26549,35 +26812,35 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/vite-node/node_modules/rollup": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", - "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.2.tgz", + "integrity": "sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -26590,34 +26853,34 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.18.0", - "@rollup/rollup-android-arm64": "4.18.0", - "@rollup/rollup-darwin-arm64": "4.18.0", - "@rollup/rollup-darwin-x64": "4.18.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", - "@rollup/rollup-linux-arm-musleabihf": "4.18.0", - "@rollup/rollup-linux-arm64-gnu": "4.18.0", - "@rollup/rollup-linux-arm64-musl": "4.18.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", - "@rollup/rollup-linux-riscv64-gnu": "4.18.0", - "@rollup/rollup-linux-s390x-gnu": "4.18.0", - "@rollup/rollup-linux-x64-gnu": "4.18.0", - "@rollup/rollup-linux-x64-musl": "4.18.0", - "@rollup/rollup-win32-arm64-msvc": "4.18.0", - "@rollup/rollup-win32-ia32-msvc": "4.18.0", - "@rollup/rollup-win32-x64-msvc": "4.18.0", + "@rollup/rollup-android-arm-eabi": "4.21.2", + "@rollup/rollup-android-arm64": "4.21.2", + "@rollup/rollup-darwin-arm64": "4.21.2", + "@rollup/rollup-darwin-x64": "4.21.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.21.2", + "@rollup/rollup-linux-arm-musleabihf": "4.21.2", + "@rollup/rollup-linux-arm64-gnu": "4.21.2", + "@rollup/rollup-linux-arm64-musl": "4.21.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.21.2", + "@rollup/rollup-linux-riscv64-gnu": "4.21.2", + "@rollup/rollup-linux-s390x-gnu": "4.21.2", + "@rollup/rollup-linux-x64-gnu": "4.21.2", + "@rollup/rollup-linux-x64-musl": "4.21.2", + "@rollup/rollup-win32-arm64-msvc": "4.21.2", + "@rollup/rollup-win32-ia32-msvc": "4.21.2", + "@rollup/rollup-win32-x64-msvc": "4.21.2", "fsevents": "~2.3.2" } }, "node_modules/vite-node/node_modules/vite": { - "version": "5.2.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz", - "integrity": "sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz", + "integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==", "dev": true, "dependencies": { - "esbuild": "^0.20.1", - "postcss": "^8.4.38", - "rollup": "^4.13.0" + "esbuild": "^0.21.3", + "postcss": "^8.4.41", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -26636,6 +26899,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -26653,6 +26917,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -27122,18 +27389,6 @@ } } }, - "node_modules/vitest/node_modules/local-pkg": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", - "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/vitest/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -27147,7 +27402,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, "dependencies": { "makeerror": "1.0.12" } @@ -27233,13 +27487,13 @@ } }, "node_modules/which-builtin-type": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", - "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz", + "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==", "dev": true, "dependencies": { - "function.prototype.name": "^1.1.5", - "has-tostringtag": "^1.0.0", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.0.5", "is-finalizationregistry": "^1.0.2", @@ -27248,8 +27502,8 @@ "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.15" }, "engines": { "node": ">= 0.4" @@ -27304,15 +27558,15 @@ } }, "node_modules/winston": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.13.0.tgz", - "integrity": "sha512-rwidmA1w3SE4j0E5MuIufFhyJPBDG7Nu71RkZor1p2+qHvJSZ9GYDA81AyleQcZbh/+V6HjeBdfnTZJm9rSeQQ==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.14.2.tgz", + "integrity": "sha512-CO8cdpBB2yqzEf8v895L+GNKYJiEq8eKlHU38af3snQBQ+sdAIUepjMSguOIJC7ICbzm0ZI+Af2If4vIJrtmOg==", "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", - "logform": "^2.4.0", + "logform": "^2.6.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", @@ -27350,12 +27604,12 @@ } }, "node_modules/winston-transport": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.0.tgz", - "integrity": "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.1.tgz", + "integrity": "sha512-wQCXXVgfv/wUPOfb2x0ruxzwkcZfxcktz6JIMUaPLmcNhO4bZTwA/WtDWK74xV3F2dKu8YadrFv0qhwYjVEwhA==", "dependencies": { - "logform": "^2.3.2", - "readable-stream": "^3.6.0", + "logform": "^2.6.1", + "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" }, "engines": { @@ -27412,14 +27666,12 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "devOptional": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/write-file-atomic": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" @@ -27520,9 +27772,9 @@ } }, "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, "engines": { "node": ">=8.3.0" @@ -27641,7 +27893,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "engines": { "node": ">=10" } @@ -27649,13 +27900,12 @@ "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/yaml": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.3.tgz", - "integrity": "sha512-sntgmxj8o7DE7g/Qi60cqpLBA3HG3STcDA0kO+WfB05jEKhZMbY7umNm2rBpQvsmZ16/lPXCJGW2672dgOUkrg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", + "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", "dev": true, "bin": { "yaml": "bin.mjs" @@ -27668,7 +27918,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -27695,7 +27944,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "engines": { "node": ">=12" } @@ -27704,7 +27952,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "engines": { "node": ">=10" }, @@ -27743,81 +27990,57 @@ } }, "node_modules/zip-stream": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", - "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", - "dev": true, - "dependencies": { - "archiver-utils": "^3.0.4", - "compress-commons": "^4.1.2", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/zip-stream/node_modules/archiver-utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", - "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", "dev": true, "dependencies": { - "glob": "^7.2.3", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 10" - } - }, - "node_modules/zip-stream/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "node": ">= 14" } }, - "node_modules/zip-stream/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "node_modules/zip-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" } }, - "node_modules/zip-stream/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" }, "engines": { - "node": "*" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/zwitch": { @@ -27852,6 +28075,7 @@ "@twake/crypto": "*", "@twake/logger": "*", "@twake/matrix-identity-server": "*", + "@twake/utils": "*", "express-validator": "^7.0.1", "ip-address": "^9.0.5", "lodash": "^4.17.21" @@ -27862,8 +28086,7 @@ }, "packages/federated-identity-service/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==", + "license": "MIT", "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" @@ -27900,6 +28123,20 @@ "js-yaml": "^4.1.0" } }, + "packages/matrix-client-server": { + "name": "@twake/matrix-client-server", + "version": "0.0.1", + "license": "AGPL-3.0-or-later", + "dependencies": { + "@twake/config-parser": "*", + "@twake/logger": "*", + "@twake/matrix-identity-server": "*", + "@twake/utils": "*", + "express": "^4.19.2", + "libphonenumber-js": "^1.11.4", + "node-fetch": "^3.3.0" + } + }, "packages/matrix-identity-server": { "name": "@twake/matrix-identity-server", "version": "0.0.1", @@ -27908,9 +28145,11 @@ "@twake/config-parser": "*", "@twake/crypto": "*", "@twake/logger": "*", + "@twake/utils": "*", "express": "^4.19.2", "express-rate-limit": "^7.2.0", "generic-pool": "^3.9.0", + "jest": "^29.7.0", "matrix-resolve": "^1.0.1", "node-cron": "^3.0.2", "node-fetch": "^3.3.0", @@ -27925,6 +28164,24 @@ "sqlite3": "^5.1.6" } }, + "packages/matrix-identity-server/node_modules/ldapjs": { + "version": "2.3.3", + "license": "MIT", + "optional": true, + "dependencies": { + "abstract-logging": "^2.0.0", + "asn1": "^0.2.4", + "assert-plus": "^1.0.0", + "backoff": "^2.5.0", + "ldap-filter": "^0.3.3", + "once": "^1.4.0", + "vasync": "^2.2.0", + "verror": "^1.8.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, "packages/matrix-invite": { "name": "@twake/matrix-invite", "version": "0.0.3", @@ -27953,9 +28210,8 @@ }, "packages/matrix-invite/node_modules/typescript": { "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -27987,8 +28243,7 @@ }, "packages/retry-promise/node_modules/retry": { "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", "engines": { "node": ">= 4" } @@ -28001,6 +28256,7 @@ "@opensearch-project/opensearch": "^2.5.0", "@twake/matrix-application-server": "*", "@twake/matrix-identity-server": "*", + "@twake/utils": "*", "lodash": "^4.17.21", "redis": "^4.6.6", "validator": "^13.11.0" @@ -28011,6 +28267,32 @@ "pg": "^8.10.0", "sqlite3": "^5.1.6" } + }, + "packages/tom-server/node_modules/ldapjs": { + "version": "2.3.3", + "license": "MIT", + "optional": true, + "dependencies": { + "abstract-logging": "^2.0.0", + "asn1": "^0.2.4", + "assert-plus": "^1.0.0", + "backoff": "^2.5.0", + "ldap-filter": "^0.3.3", + "once": "^1.4.0", + "vasync": "^2.2.0", + "verror": "^1.8.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "packages/utils": { + "name": "@twake/utils", + "version": "0.0.1", + "license": "AGPL-3.0-or-later", + "dependencies": { + "@twake/logger": "*" + } } } } diff --git a/package.json b/package.json index bc27765e..1a0c255f 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,10 @@ "test": "lerna run test" }, "dependencies": { + "@remix-run/express": "^1.16.0", "express": "^4.18.2", - "@remix-run/express": "^1.16.0" + "tweetnacl": "^1.0.3", + "tweetnacl-util": "^0.15.1" }, "devDependencies": { "@crowdsec/express-bouncer": "^0.1.0", @@ -63,6 +65,7 @@ "@types/validator": "^13.11.7", "@typescript-eslint/eslint-plugin": "^5.54.1", "docker-compose": "^0.24.3", + "esbuild": "^0.20.2", "eslint": "^8.35.0", "eslint-config-prettier": "^8.8.0", "eslint-config-standard-with-typescript": "^34.0.0", @@ -90,5 +93,9 @@ "toad-cache": "^3.3.0", "ts-jest": "^29.1.0", "typescript": "^4.9.5" + }, + "optionalDependencies": { + "@nx/nx-darwin-arm64": "^19.4.0", + "@nx/nx-linux-x64-gnu": "^19.4.0" } -} \ No newline at end of file +} diff --git a/packages/crypto/src/index.test.ts b/packages/crypto/src/index.test.ts index 7a685e54..2b723bae 100644 --- a/packages/crypto/src/index.test.ts +++ b/packages/crypto/src/index.test.ts @@ -1,4 +1,10 @@ -import { Hash, randomString } from './index' +import { + Hash, + randomString, + generateKeyPair, + canonicalJson, + signJson +} from './index' const sha256Results: Record = { 'alice@example.com email matrixrocks': @@ -39,3 +45,187 @@ test('randomString', () => { const res = randomString(64) expect(res).toMatch(/^[a-zA-Z0-9]{64}$/) }) + +describe('generateKeyPair', () => { + it('should refuse an invalid algorithm', () => { + expect(() => + generateKeyPair( + 'invalid_algorithm' as unknown as 'ed25519' | 'curve25519' + ) + ).toThrow('Unsupported algorithm') + }) + it('should generate a valid Ed25519 key pair and key ID', () => { + const { publicKey, privateKey, keyId } = generateKeyPair('ed25519') + expect(publicKey).toMatch(/^[A-Za-z0-9_-]+$/) // Unpadded Base64 URL encoded string + expect(privateKey).toMatch(/^[A-Za-z0-9_-]+$/) // Unpadded Base64 URL encoded string + expect(keyId).toMatch(/^ed25519:[A-Za-z0-9_-]+$/) // Key ID format + }) + + it('should generate a valid Curve25519 key pair and key ID', () => { + const { publicKey, privateKey, keyId } = generateKeyPair('curve25519') + expect(publicKey).toMatch(/^[A-Za-z0-9_-]+$/) // Unpadded Base64 URL encoded string + expect(privateKey).toMatch(/^[A-Za-z0-9_-]+$/) // Unpadded Base64 URL encoded string + expect(keyId).toMatch(/^curve25519:[A-Za-z0-9_-]+$/) // Key ID format + }) +}) + +describe('canonicalJson', () => { + test('should handle empty object', () => { + const input = {} + const expectedOutput = '{}' + expect(canonicalJson(input)).toEqual(expectedOutput) + }) + + test('should handle simple object with different key types', () => { + const input = { one: 1, two: 'Two' } + const expectedOutput = '{"one":1,"two":"Two"}' + expect(canonicalJson(input)).toEqual(expectedOutput) + }) + + test('should handle object with keys in reverse order', () => { + const input = { b: '2', a: '1' } + const expectedOutput = '{"a":"1","b":"2"}' + expect(canonicalJson(input)).toEqual(expectedOutput) + }) + + test('should handle nested objects with arrays', () => { + const input = { + auth: { + success: true, + mxid: '@john.doe:example.com', + profile: { + display_name: 'John Doe', + three_pids: [ + { + medium: 'email', + address: 'john.doe@example.org' + }, + { + medium: 'msisdn', + address: '123456789' + } + ] + } + } + } + const expectedOutput = + '{"auth":{"mxid":"@john.doe:example.com","profile":{"display_name":"John Doe","three_pids":[{"address":"john.doe@example.org","medium":"email"},{"address":"123456789","medium":"msisdn"}]},"success":true}}' + expect(canonicalJson(input)).toEqual(expectedOutput) + }) + + test('should handle object with non-ASCII characters', () => { + const input = { a: '日本語' } + const expectedOutput = '{"a":"日本語"}' + expect(canonicalJson(input)).toEqual(expectedOutput) + }) + + test('should handle object with non-ASCII keys', () => { + const input = { 本: 2, 日: 1 } + const expectedOutput = '{"日":1,"本":2}' + expect(canonicalJson(input)).toEqual(expectedOutput) + }) + + test('should handle object with unicode escape sequences', () => { + const input = { a: '\u65E5' } + const expectedOutput = '{"a":"日"}' + expect(canonicalJson(input)).toEqual(expectedOutput) + }) + + test('should handle object with null values', () => { + const input = { a: null } + const expectedOutput = '{"a":null}' + expect(canonicalJson(input)).toEqual(expectedOutput) + }) + + test('should handle object with special numeric values', () => { + const input = { a: -0, b: 1e10 } + const expectedOutput = '{"a":0,"b":10000000000}' + expect(canonicalJson(input)).toEqual(expectedOutput) + }) +}) + +describe('signJson', () => { + const testKey = generateKeyPair('ed25519') + const signingKey = testKey.privateKey + const signingName: string = 'matrix.org' + const keyId = testKey.keyId + + it('should add signature to a simple object', () => { + const jsonObj = { a: 1, b: 'string' } + const result = signJson(jsonObj, signingKey, signingName, keyId) + + expect(result).toHaveProperty('signatures') + expect(result.signatures?.[signingName]).toBeDefined() + expect(result.signatures?.[signingName]?.[keyId]).toBeDefined() + expect(result.signatures?.[signingName]?.[keyId]).toMatch( + /^[A-Za-z0-9+/]+$/ + ) + expect(result.signatures?.[signingName]).toHaveProperty(keyId) + expect(result).toMatchObject({ + a: 1, + b: 'string', + signatures: expect.any(Object) + }) + }) + + it('should preserve existing signatures', () => { + const jsonObj = { + a: 1, + b: 'string', + signatures: { + existingSignature: { + existingKeyId: 'existingSignatureValue' + } + } + } + const result = signJson(jsonObj, signingKey, signingName, keyId) + + expect(result.signatures).toHaveProperty('existingSignature') + expect(result.signatures?.existingSignature).toHaveProperty('existingKeyId') + expect(result.signatures?.[signingName]).toBeDefined() + expect(result.signatures?.[signingName]).toHaveProperty(keyId) + }) + + it('should handle unsigned field correctly', () => { + const jsonObj = { a: 1, b: 'string', unsigned: { c: 2 } } + const result = signJson(jsonObj, signingKey, signingName, keyId) + + expect(result).toHaveProperty('unsigned') + expect(result.unsigned).toEqual({ c: 2 }) + }) + + it('should not include `unsigned` field if not present', () => { + const jsonObj = { a: 1, b: 'string' } + const result = signJson(jsonObj, signingKey, signingName, keyId) + + expect(result).not.toHaveProperty('unsigned') + }) + + it('should handle complex nested objects', () => { + const jsonObj = { a: { b: { c: 1 } }, d: ['e', 'f', { g: 'h' }] } + const result = signJson(jsonObj, signingKey, signingName, keyId) + + expect(result).toHaveProperty('signatures') + expect(result.signatures?.[signingName]).toBeDefined() + expect(result.signatures?.[signingName]).toHaveProperty(keyId) + expect(result).toMatchObject({ + a: { b: { c: 1 } }, + d: ['e', 'f', { g: 'h' }], + signatures: expect.any(Object) + }) + }) + + it('should handle objects with null values', () => { + const jsonObj = { a: null, b: 'string' } + const result = signJson(jsonObj, signingKey, signingName, keyId) + + expect(result).toHaveProperty('signatures') + expect(result.signatures?.[signingName]).toBeDefined() + expect(result.signatures?.[signingName]).toHaveProperty(keyId) + expect(result).toMatchObject({ + a: null, + b: 'string', + signatures: expect.any(Object) + }) + }) +}) diff --git a/packages/crypto/src/index.ts b/packages/crypto/src/index.ts index 58a4310c..d405852a 100644 --- a/packages/crypto/src/index.ts +++ b/packages/crypto/src/index.ts @@ -1,4 +1,6 @@ import _nacl, { type Nacl } from 'js-nacl' +import nacl from 'tweetnacl' +import * as naclUtil from 'tweetnacl-util' // export const supportedHashes = ['sha256', 'sha512'] export const supportedHashes = ['sha256'] @@ -50,3 +52,135 @@ export const randomString = (n: number): string => { } return res } + +// Function to generate KeyId +function generateKeyId(algorithm: string, identifier: string): string { + return `${algorithm}:${identifier}` +} + +// Function to convert a Base64 string to unpadded Base64 URL encoded string +export function toBase64Url(base64: string): string { + return base64.replace(/=+$/, '').replace(/\//g, '_').replace(/\+/g, '-') +} + +// Function to convert an unpadded Base64 URL encoded string to Base64 string +function fromBase64Url(base64Url: string): string { + let base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/') + while (base64.length % 4 !== 0) { + base64 += '=' + } + return base64 +} + +// Function to generate Ed25519 key pair and KeyId +function generateEdKeyPair(): { + publicKey: string + privateKey: string + keyId: string +} { + // Generate an Ed25519 key pair + const keyPair = nacl.sign.keyPair() + + // Generate a unique identifier for the KeyId + const identifier = nacl.randomBytes(8) // Generate 8 random bytes + let identifierHex = naclUtil.encodeBase64(identifier) + // Convert to unpadded Base64 URL encoded form + identifierHex = toBase64Url(identifierHex) + + const algorithm = 'ed25519' + const _keyId = generateKeyId(algorithm, identifierHex) + + return { + publicKey: toBase64Url(naclUtil.encodeBase64(keyPair.publicKey)), + privateKey: toBase64Url(naclUtil.encodeBase64(keyPair.secretKey)), + keyId: _keyId + } +} + +// Function to generate Curve25519 key pair and KeyId +function generateCurveKeyPair(): { + publicKey: string + privateKey: string + keyId: string +} { + // Generate a Curve25519 key pair + const keyPair = nacl.box.keyPair() + + // Generate a unique identifier for the KeyId + const identifier = nacl.randomBytes(8) // Generate 8 random bytes + let identifierHex = naclUtil.encodeBase64(identifier) + // Convert to unpadded Base64 URL encoded form + identifierHex = toBase64Url(identifierHex) + + const algorithm = 'curve25519' + const _keyId = generateKeyId(algorithm, identifierHex) + + return { + publicKey: toBase64Url(naclUtil.encodeBase64(keyPair.publicKey)), + privateKey: toBase64Url(naclUtil.encodeBase64(keyPair.secretKey)), + keyId: _keyId + } +} + +export const generateKeyPair = ( + algorithm: 'ed25519' | 'curve25519' +): { publicKey: string; privateKey: string; keyId: string } => { + if (algorithm === 'ed25519') { + return generateEdKeyPair() + } else if (algorithm === 'curve25519') { + return generateCurveKeyPair() + } else { + throw new Error('Unsupported algorithm') + } +} + +export const canonicalJson = (value: any): string => { + return JSON.stringify(value, (key, val) => + typeof val === 'object' && val !== null && !Array.isArray(val) + ? Object.keys(val) + .sort() + .reduce((sorted, key) => { + sorted[key] = val[key] + return sorted + }, {}) + : val + ).replace(/[\u007f-\uffff]/g, function (c) { + return c + }) +} + +interface JsonObject { + [key: string]: any + signatures?: Record> + unsigned?: any +} + +export const signJson = ( + jsonObj: JsonObject, + signingKey: string, + signingName: string, + keyId: string +): JsonObject => { + const signatures = + jsonObj.signatures ?? ({} as Record>) + const unsigned = jsonObj.unsigned + delete jsonObj.signatures + delete jsonObj.unsigned + const signed = nacl.sign( + naclUtil.decodeUTF8(canonicalJson(jsonObj)), + naclUtil.decodeBase64(fromBase64Url(signingKey)) + ) + const signatureBase64 = Buffer.from(signed).toString('base64') + + signatures[signingName] = { + ...signatures[signingName], + [keyId]: signatureBase64 + } + + jsonObj.signatures = signatures + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (unsigned) { + jsonObj.unsigned = unsigned + } + return jsonObj +} diff --git a/packages/federated-identity-service/Dockerfile b/packages/federated-identity-service/Dockerfile index d0cbf499..4c3fc0aa 100644 --- a/packages/federated-identity-service/Dockerfile +++ b/packages/federated-identity-service/Dockerfile @@ -28,6 +28,7 @@ COPY ./packages/matrix-resolve ./packages/matrix-resolve COPY ./packages/matrix-identity-server ./packages/matrix-identity-server COPY ./packages/config-parser ./packages/config-parser COPY ./packages/federated-identity-service ./packages/federated-identity-service +COPY ./packages/utils ./packages/utils COPY .husky .husky COPY lerna.json ./ COPY tsconfig-build.json ./ @@ -70,4 +71,4 @@ COPY --from=1 /usr/src/app /usr/src/app/ WORKDIR /usr/src/app EXPOSE 3000 -CMD [ "node", "/usr/src/app/server.mjs" ] \ No newline at end of file +CMD [ "node", "/usr/src/app/server.mjs" ] diff --git a/packages/federated-identity-service/package.json b/packages/federated-identity-service/package.json index 1a02120e..e64c56bd 100644 --- a/packages/federated-identity-service/package.json +++ b/packages/federated-identity-service/package.json @@ -37,12 +37,13 @@ "build:example": "rollup -p @rollup/plugin-typescript -e express,@twake/federated-identity-service -m -o example/federated-identity-service.js example/federated-identity-service.ts", "build:lib": "rollup -c", "start": "node example/federated-identity-service.js", - "test": "jest" + "test": "LOG_TRANSPORTS=File LOG_FILE=/dev/null jest" }, "dependencies": { "@twake/config-parser": "*", "@twake/crypto": "*", "@twake/logger": "*", + "@twake/utils": "*", "@twake/matrix-identity-server": "*", "express-validator": "^7.0.1", "ip-address": "^9.0.5", diff --git a/packages/federated-identity-service/rollup.config.js b/packages/federated-identity-service/rollup.config.js index eac935a8..0cf03c61 100644 --- a/packages/federated-identity-service/rollup.config.js +++ b/packages/federated-identity-service/rollup.config.js @@ -5,6 +5,7 @@ export default config([ '@twake/logger', '@twake/crypto', '@twake/config-parser', + '@twake/utils', 'express', 'express-validator', 'ip-address', diff --git a/packages/federated-identity-service/src/__testData__/build-userdb.ts b/packages/federated-identity-service/src/__testData__/build-userdb.ts index 2620c30d..fec66571 100644 --- a/packages/federated-identity-service/src/__testData__/build-userdb.ts +++ b/packages/federated-identity-service/src/__testData__/build-userdb.ts @@ -13,14 +13,14 @@ interface UserDBSQLite { const logger: TwakeLogger = getLogger() -const createUsersTable = 'CREATE TABLE users (uid varchar(32), cn varchar(32), sn varchar(32), mail varchar(32), mobile varchar(12))' -const insertLskywalker = "INSERT INTO users VALUES('lskywalker', 'Luke Skywalker', 'Lskywalker', 'lskywalker@example.com', '')" -const insertOkenobi = "INSERT INTO users VALUES('okenobi', 'Obi-Wan Kenobi', 'Okenobi', 'okenobi@example.com', '')" -const insertAskywalker = "INSERT INTO users VALUES('askywalker', 'Anakin Skywalker', 'Askywalker', 'askywalker@example.com', '')" -const insertQjinn = "INSERT INTO users VALUES('qjinn', 'Qgonjinn', 'Qjinn', 'qjinn@example.com', '')" -const insertChewbacca = "INSERT INTO users VALUES('chewbacca', 'Chewbacca', 'Chewbacca', 'chewbacca@example.com', '')" +const createUsersTable = 'CREATE TABLE IF NOT EXISTS users (uid varchar(255), mobile text, mail test)' +const insertLskywalker = "INSERT INTO users VALUES('lskywalker', '', 'lskywalker@example.com')" +const insertOkenobi = "INSERT INTO users VALUES('okenobi', '', 'okenobi@example.com')" +const insertAskywalker = "INSERT INTO users VALUES('askywalker', '', 'askywalker@example.com')" +const insertQjinn = "INSERT INTO users VALUES('qjinn', '', 'qjinn@example.com')" +const insertChewbacca = "INSERT INTO users VALUES('chewbacca', '', 'chewbacca@example.com')" -const mCreateUsersTable = 'CREATE TABLE users (name text)' +const mCreateUsersTable = 'CREATE TABLE IF NOT EXISTS users (name text)' const mInsertChewbacca = "INSERT INTO users VALUES('@chewbacca:example.com')" // eslint-disable-next-line @typescript-eslint/promise-function-async diff --git a/packages/federated-identity-service/src/__testData__/docker-compose.yml b/packages/federated-identity-service/src/__testData__/docker-compose.yml index db9dd28e..b1c72e5d 100644 --- a/packages/federated-identity-service/src/__testData__/docker-compose.yml +++ b/packages/federated-identity-service/src/__testData__/docker-compose.yml @@ -31,7 +31,7 @@ services: - ./nginx/ssl/9da13359.0:/etc/ssl/certs/9da13359.0 depends_on: - auth - environment: + environment: - UID=${MYUID} - VIRTUAL_PORT=8008 - VIRTUAL_HOST=matrix.example.com @@ -47,7 +47,7 @@ services: synapse-1: <<: *synapse_template container_name: synapse-1 - environment: + environment: - UID=${MYUID} - VIRTUAL_PORT=8008 - VIRTUAL_HOST=matrix1.example.com @@ -56,7 +56,7 @@ services: synapse-2: <<: *synapse_template container_name: synapse-2 - environment: + environment: - UID=${MYUID} - VIRTUAL_PORT=8008 - VIRTUAL_HOST=matrix2.example.com @@ -65,7 +65,7 @@ services: synapse-3: <<: *synapse_template container_name: synapse-3 - environment: + environment: - UID=${MYUID} - VIRTUAL_PORT=8008 - VIRTUAL_HOST=matrix3.example.com @@ -157,11 +157,11 @@ services: volumes: - ./nginx/ssl/ca.pem:/etc/ssl/certs/ca.pem - ./identity-server/conf/identity-server-2.conf:/etc/twake/identity-server.conf - environment: + environment: - NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca.pem - VIRTUAL_PORT=3000 - VIRTUAL_HOST=identity2.example.com - + identity-server-3: <<: *identity-server-template container_name: identity-server-3 diff --git a/packages/federated-identity-service/src/__testData__/identity-server/Dockerfile b/packages/federated-identity-service/src/__testData__/identity-server/Dockerfile index 4d3ad418..47c7e405 100644 --- a/packages/federated-identity-service/src/__testData__/identity-server/Dockerfile +++ b/packages/federated-identity-service/src/__testData__/identity-server/Dockerfile @@ -11,6 +11,7 @@ COPY ./packages/logger ./packages/logger COPY ./packages/crypto ./packages/crypto COPY ./packages/config-parser ./packages/config-parser COPY ./packages/matrix-resolve ./packages/matrix-resolve +COPY ./packages/utils ./packages/utils COPY ./.husky .husky COPY ./lerna.json ./ COPY ./tsconfig-build.json ./ diff --git a/packages/federated-identity-service/src/__testData__/ldap/Dockerfile b/packages/federated-identity-service/src/__testData__/ldap/Dockerfile index 83adb618..86d55944 100644 --- a/packages/federated-identity-service/src/__testData__/ldap/Dockerfile +++ b/packages/federated-identity-service/src/__testData__/ldap/Dockerfile @@ -3,7 +3,7 @@ LABEL maintainer Linagora ENV DEBIAN_FRONTEND=noninteractive -# Update system and install dependencies +# Update system and install dependencies RUN apt-get update && \ apt-get install -y --no-install-recommends \ apt-transport-https \ @@ -16,7 +16,7 @@ RUN apt-get update && \ apt-get update && \ apt-get install -y openldap-ltb openldap-ltb-contrib-overlays openldap-ltb-mdb-utils ldap-utils && \ apt-get clean && \ - rm -rf /var/lib/apt/lists/* + rm -rf /var/lib/apt/lists/* # Copy configuration files COPY ./ldif/config-20230322180123.ldif /var/backups/openldap/ diff --git a/packages/federated-identity-service/src/__testData__/llng/lmConf-1.json b/packages/federated-identity-service/src/__testData__/llng/lmConf-1.json index cfe373f9..c56a0a82 100644 --- a/packages/federated-identity-service/src/__testData__/llng/lmConf-1.json +++ b/packages/federated-identity-service/src/__testData__/llng/lmConf-1.json @@ -538,4 +538,3 @@ "yubikey2fSelfRegistration": 0, "yubikey2fUserCanRemoveKey": 1 } - \ No newline at end of file diff --git a/packages/federated-identity-service/src/controllers/controllers.ts b/packages/federated-identity-service/src/controllers/controllers.ts index 100ab79a..1e073c24 100644 --- a/packages/federated-identity-service/src/controllers/controllers.ts +++ b/packages/federated-identity-service/src/controllers/controllers.ts @@ -1,23 +1,18 @@ import { supportedHashes } from '@twake/crypto' import { type TwakeLogger } from '@twake/logger' -import { MatrixErrors, type DbGetResult } from '@twake/matrix-identity-server' +import { type DbGetResult } from '@twake/matrix-identity-server' +import { errCodes } from '@twake/utils' import lodash from 'lodash' -import { hashByServer } from '../db' import { FederatedIdentityServiceError, validationErrorHandler } from '../middlewares/errors' -import { - type Config, - type IdentityServerDb, - type expressAppHandler -} from '../types' +import { type Config, type FdServerDb, type expressAppHandler } from '../types' const { groupBy, mapValues } = lodash -export const lookup = ( - conf: Config, - db: IdentityServerDb -): expressAppHandler => { +export const hashByServer = 'hashByServer' + +export const lookup = (conf: Config, db: FdServerDb): expressAppHandler => { return (req, res, next) => { const mappings: Record = {} const inactives: Record = {} @@ -71,14 +66,14 @@ export const lookup = ( next( new FederatedIdentityServiceError({ message: e, - code: MatrixErrors.errCodes.unknown + code: errCodes.unknown }) ) }) } } -export const lookups = (db: IdentityServerDb): expressAppHandler => { +export const lookups = (db: FdServerDb): expressAppHandler => { return (req, res, next) => { validationErrorHandler(req) const pepper = req.body.pepper @@ -118,7 +113,7 @@ export const lookups = (db: IdentityServerDb): expressAppHandler => { next( new FederatedIdentityServiceError({ message: e, - code: MatrixErrors.errCodes.unknown + code: errCodes.unknown }) ) }) @@ -132,7 +127,7 @@ interface HashDetailsObject { } export const hashDetails = ( - db: IdentityServerDb, + db: FdServerDb, logger: TwakeLogger ): expressAppHandler => { return (req, res, next) => { @@ -157,7 +152,7 @@ export const hashDetails = ( next( new FederatedIdentityServiceError({ message: e, - code: MatrixErrors.errCodes.unknown + code: errCodes.unknown }) ) }) diff --git a/packages/federated-identity-service/src/db/index.ts b/packages/federated-identity-service/src/db/index.ts deleted file mode 100644 index 1499c3f3..00000000 --- a/packages/federated-identity-service/src/db/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { type TwakeLogger } from '@twake/logger' -import { - createTables, - type Pg, - type SQLite -} from '@twake/matrix-identity-server' -import { type Collections, type Config, type IdentityServerDb } from '../types' - -export const hashByServer = 'hashByServer' as Collections - -// eslint-disable-next-line @typescript-eslint/promise-function-async -const initializeDb = ( - db: IdentityServerDb, - conf: Config, - logger: TwakeLogger -): Promise => { - return new Promise((resolve, reject) => { - switch (conf.database_engine) { - case 'sqlite': - case 'pg': - createTables( - db.db as SQLite.default | Pg, - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - { - hashByServer: - 'hash varchar(48), server text, pepper text, PRIMARY KEY (hash, server, pepper)' - } as unknown as Record, - {}, - {}, - logger, - resolve, - reject - ) - break - default: - /* istanbul ignore next */ throw new Error( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `Unsupported DB type ${conf.database_engine}` - ) - } - }) -} - -export default initializeDb diff --git a/packages/federated-identity-service/src/index.test.ts b/packages/federated-identity-service/src/index.test.ts index f7bf3c27..32d8b79f 100644 --- a/packages/federated-identity-service/src/index.test.ts +++ b/packages/federated-identity-service/src/index.test.ts @@ -18,7 +18,7 @@ import FederatedIdentityService from '.' import JEST_PROCESS_ROOT_PATH from '../jest.globals' import { buildMatrixDb, buildUserDB } from './__testData__/build-userdb' import defaultConfig from './__testData__/config.json' -import { hashByServer } from './db' +import { hashByServer } from './controllers/controllers' import { type Config } from './types' // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -367,9 +367,9 @@ describe('Federated identity service', () => { { mappings: {}, inactive_mappings: {}, - third_party_mappings: { + third_party_mappings: { 'identity2.example.com:443': ['gxkUW11GNrH5YASQhG_I7ijwdUBoMpqqSCc_OtbpOm0'] - } + } } } */ diff --git a/packages/federated-identity-service/src/index.ts b/packages/federated-identity-service/src/index.ts index 9ad08eaf..a2b14034 100644 --- a/packages/federated-identity-service/src/index.ts +++ b/packages/federated-identity-service/src/index.ts @@ -4,15 +4,20 @@ import MatrixIdentityServer from '@twake/matrix-identity-server' import { Router } from 'express' import fs from 'fs' import defaultConfig from './config.json' -import initializeDb from './db' import { Authenticate } from './middlewares/auth' import Routes from './routes/routes' -import { type Config } from './types' +import { type Config, type FdServerDb, type fdDbCollections } from './types' import { isIpLiteral, isNetwork } from './utils/ip-address' -export default class FederatedIdentityService extends MatrixIdentityServer { +const tables = { + hashByServer: + 'hash varchar(48), server text, pepper text, PRIMARY KEY (hash, server, pepper)' +} + +export default class FederatedIdentityService extends MatrixIdentityServer { routes = Router() declare conf: Config + declare db: FdServerDb constructor( conf?: Partial, confDesc?: ConfigDescription, @@ -30,7 +35,7 @@ export default class FederatedIdentityService extends MatrixIdentityServer { ? conf : undefined ) as Config - super(serverConf, confDesc, logger) + super(serverConf, confDesc, logger, tables) this.conf.trusted_servers_addresses = typeof this.conf.trusted_servers_addresses === 'string' ? (this.conf.trusted_servers_addresses as string) @@ -57,10 +62,6 @@ export default class FederatedIdentityService extends MatrixIdentityServer { const superReady = this.ready this.ready = new Promise((resolve, reject) => { superReady - // eslint-disable-next-line @typescript-eslint/promise-function-async - .then(() => { - return initializeDb(this.db, this.conf, this.logger) - }) .then(() => { this.routes = Routes( this.api, diff --git a/packages/federated-identity-service/src/middlewares/auth.ts b/packages/federated-identity-service/src/middlewares/auth.ts index 441b0514..a957dfcd 100644 --- a/packages/federated-identity-service/src/middlewares/auth.ts +++ b/packages/federated-identity-service/src/middlewares/auth.ts @@ -1,17 +1,14 @@ import { type TwakeLogger } from '@twake/logger' -import { - MatrixErrors, - Utils, - type tokenContent -} from '@twake/matrix-identity-server' +import { errMsg, send } from '@twake/utils' +import { Utils, type tokenContent } from '@twake/matrix-identity-server' import { type NextFunction, type Response } from 'express' -import { type AuthRequest, type IdentityServerDb } from '../types' +import { type AuthRequest, type FdServerDb } from '../types' import { convertToIPv6 } from '../utils/ip-address' const tokenTrustedServer = 'TOKEN_TRUSTED_SERVER' export const Authenticate = ( - db: IdentityServerDb, + db: FdServerDb, trustedServersList: string[], trustXForwardedForHeader: boolean, logger: TwakeLogger @@ -56,7 +53,7 @@ export const Authenticate = ( token = re[1] } // @ts-expect-error req.query exists - } else if (req.query != null) { + } else if (req.query && Object.keys(req.query).length > 0) { // @ts-expect-error req.query.access_token may be null token = req.query.access_token } @@ -66,10 +63,10 @@ export const Authenticate = ( callbackMethod(JSON.parse(rows[0].data as string), token) }) .catch((e) => { - Utils.send(res, 401, MatrixErrors.errMsg('unAuthorized')) + send(res, 401, errMsg('unAuthorized')) }) } else { - Utils.send(res, 401, MatrixErrors.errMsg('unAuthorized')) + send(res, 401, errMsg('unAuthorized')) } } } catch (error) { @@ -82,7 +79,7 @@ export const Authenticate = ( httpMethod: request.method, endpointPath: request.originalUrl }) - Utils.send(res, 401, MatrixErrors.errMsg('unAuthorized')) + send(res, 401, errMsg('unAuthorized')) } } } diff --git a/packages/federated-identity-service/src/middlewares/errors.ts b/packages/federated-identity-service/src/middlewares/errors.ts index ecf351b0..a90ae4af 100644 --- a/packages/federated-identity-service/src/middlewares/errors.ts +++ b/packages/federated-identity-service/src/middlewares/errors.ts @@ -1,4 +1,4 @@ -import { MatrixErrors } from '@twake/matrix-identity-server' +import { defaultMsg, errCodes } from '@twake/utils' import { type Request } from 'express' import { validationResult, type ValidationError } from 'express-validator' import { @@ -24,7 +24,7 @@ export class FederatedIdentityServiceError extends Error { if (error.message != null) { errorMessage = error.message } else if (error.code != null) { - errorMessage = MatrixErrors.defaultMsg(error.code) + errorMessage = defaultMsg(error.code) } super(errorMessage) if (error.code != null) { @@ -73,7 +73,7 @@ export const validationErrorHandler = (req: Request): void => { throw new FederatedIdentityServiceError({ status: 400, message: errorMessage, - code: MatrixErrors.errCodes.invalidParam + code: errCodes.invalidParam }) } } diff --git a/packages/federated-identity-service/src/middlewares/utils.ts b/packages/federated-identity-service/src/middlewares/utils.ts index a3003ad1..3daac507 100644 --- a/packages/federated-identity-service/src/middlewares/utils.ts +++ b/packages/federated-identity-service/src/middlewares/utils.ts @@ -1,4 +1,4 @@ -import { MatrixErrors } from '@twake/matrix-identity-server' +import { errCodes } from '@twake/utils' import { type expressAppHandler } from '../types' import { FederatedIdentityServiceError } from './errors' @@ -18,13 +18,13 @@ export const allowCors: expressAppHandler = (req, res, next) => { export const methodNotAllowed: expressAppHandler = (req, res, next) => { throw new FederatedIdentityServiceError({ status: 405, - code: MatrixErrors.errCodes.unrecognized + code: errCodes.unrecognized }) } export const methodNotFound: expressAppHandler = (req, res, next) => { throw new FederatedIdentityServiceError({ status: 404, - code: MatrixErrors.errCodes.notFound + code: errCodes.notFound }) } diff --git a/packages/federated-identity-service/src/routes/routes.ts b/packages/federated-identity-service/src/routes/routes.ts index ac985518..82a761c6 100644 --- a/packages/federated-identity-service/src/routes/routes.ts +++ b/packages/federated-identity-service/src/routes/routes.ts @@ -15,8 +15,8 @@ import { lookupsValidator } from '../middlewares/validation' import { + type FdServerDb, type Config, - type IdentityServerDb, type expressAppHandler, type middlewaresList } from '../types' @@ -33,7 +33,7 @@ export default ( post: IdServerAPI put?: IdServerAPI }, - db: IdentityServerDb, + db: FdServerDb, authenticate: Utils.AuthenticationFunction, conf: Config, logger: TwakeLogger diff --git a/packages/federated-identity-service/src/types.ts b/packages/federated-identity-service/src/types.ts index 99265daf..6b10f910 100644 --- a/packages/federated-identity-service/src/types.ts +++ b/packages/federated-identity-service/src/types.ts @@ -1,8 +1,8 @@ import { - type Config as MConfig, - type IdentityServerDb as MIdentityServerDb, - type MatrixErrors + type IdentityServerDb, + type Config as MConfig } from '@twake/matrix-identity-server' +import { type errCodes } from '@twake/utils' import { type NextFunction, type Request, type Response } from 'express' export type expressAppHandler = ( @@ -24,7 +24,7 @@ export interface AuthRequest extends Request { } export type federatedIdentityServiceErrorCode = - (typeof MatrixErrors.errCodes)[keyof typeof MatrixErrors.errCodes] + (typeof errCodes)[keyof typeof errCodes] export interface ErrorResponseBody { error: string @@ -37,6 +37,6 @@ export type Config = MConfig & { trusted_servers_addresses: string[] } -export type IdentityServerDb = MIdentityServerDb.default +export type fdDbCollections = 'hashByServer' -export type Collections = MIdentityServerDb.Collections +export type FdServerDb = IdentityServerDb diff --git a/packages/federated-identity-service/templates/3pidInvitation.tpl b/packages/federated-identity-service/templates/3pidInvitation.tpl new file mode 100644 index 00000000..e5bbad27 --- /dev/null +++ b/packages/federated-identity-service/templates/3pidInvitation.tpl @@ -0,0 +1,67 @@ +Date: __date__ +From: __from__ +To: __to__ +Message-ID: __messageid__ +Subject: Invitation to join a Matrix room +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="__multipart_boundary__" + +--__multipart_boundary__ +Content-Type: text/plain; charset=UTF-8 +Content-Disposition: inline +Hello, + +You have been invited to join a Matrix room by __inviter_name__. If you possess a Matrix account, please consider binding this email address to your account in order to accept the invitation. + + +About Matrix: +Matrix is an open standard for interoperable, decentralised, real-time communication over IP. It can be used to power Instant Messaging, VoIP/WebRTC signalling, Internet of Things communication - or anywhere you need a standard HTTP API for publishing and subscribing to data whilst tracking the conversation history. + +Matrix defines the standard, and provides open source reference implementations of Matrix-compatible Servers, Clients, Client SDKs and Application Services to help you create new communication solutions or extend the capabilities and reach of existing ones. + +--__multipart_boundary__ +Content-Type: text/html; charset=UTF-8 +Content-Disposition: inline + + + + + +Invitation to join a Matrix room + + + +

Hello,

+ +

You have been invited to join a Matrix room by __inviter_name__. If you possess a Matrix account, please consider binding this email address to your account in order to accept the invitation.

+ +

If your client requires a code, the code is __token__

+ +
+

Invitation Details:

+
    +
  • Inviter: __inviter_name__ (display name: __inviter_display_name__)
  • +
  • Room Name: __room_name__
  • +
  • Room Type: __room_type__
  • +
  • Room Avatar: Room Avatar
  • +
+ +
+

About Matrix:

+ +

Matrix is an open standard for interoperable, decentralised, real-time communication over IP. It can be used to power Instant Messaging, VoIP/WebRTC signalling, Internet of Things communication - or anywhere you need a standard HTTP API for publishing and subscribing to data whilst tracking the conversation history.

+ +

Matrix defines the standard, and provides open source reference implementations of Matrix-compatible Servers, Clients, Client SDKs and Application Services to help you create new communication solutions or extend the capabilities and reach of existing ones.

+ + + + +--__multipart_boundary__-- + diff --git a/packages/federated-identity-service/templates/mailVerification.tpl b/packages/federated-identity-service/templates/mailVerification.tpl index 08fbe672..6a5c4740 100644 --- a/packages/federated-identity-service/templates/mailVerification.tpl +++ b/packages/federated-identity-service/templates/mailVerification.tpl @@ -15,7 +15,6 @@ We have received a request to use this email address with a matrix.org identity server. If this was you who made this request, you may use the following link to complete the verification of your email address: __link__ -If your client requires a code, the code is __token__ If you aren't aware of making such a request, please disregard this email. About Matrix: Matrix is an open standard for interoperable, decentralised, real-time communication diff --git a/packages/logger/README.md b/packages/logger/README.md index ec34fe67..b8d2096a 100644 --- a/packages/logger/README.md +++ b/packages/logger/README.md @@ -21,7 +21,7 @@ LOG_FILE=etc/twake/winston.log ### Configuration File -All winston's [core configuration properties](https://github.com/winstonjs/winston#logging) except `format` and `levels` can be set in a JSON configuration file. The property `transports` is set through the field `log_transports` which is detailed in the following part. +All winston's [core configuration properties](https://github.com/winstonjs/winston#logging) except `format` and `levels` can be set in a JSON configuration file. The property `transports` is set through the field `log_transports` which is detailed in the following part. There are three more available properties: * `default_meta`: javascript object containing metadata that should be displayed in the log message * `exception_handlers`: array containing transports which specify where uncaughtException events should be displayed (see [winston documention](https://github.com/winstonjs/winston#exceptions)) @@ -43,7 +43,7 @@ NB: Winston `level` property is named `log_level` #### Format -This loggger has a predefined format which is: +This loggger has a predefined format which is: `LEVEL | Date in ISO format | log message` Between date and log message it is possible to add request details: @@ -56,17 +56,17 @@ Between date and log message it is possible to add request details: Any other detail can be added and it will be displayed after `log message ` -Aditionnal details are displayed in the following order: +Aditionnal details are displayed in the following order: `LEVEL | Date in ISO format | ip | matrixUserId | httpMethod | requestURL | endpointPath | status | log message | additionnal details` #### Transports In this module, logger's `log_transports` field is set to an array of objects which contain two properties: * type: winston transport that the logger should use. "Console" and "File" transports listed in this [winston documentation](https://github.com/winstonjs/winston/blob/master/docs/transports.md#built-in-to-winston) are available. The field's value must be the transport name and must start with a capital letter ("Console", "File"). -* options: object containing selected transport options, they are all detailed on this [page](https://github.com/winstonjs/winston/blob/master/docs/transports.md#built-in-to-winston). +* options: object containing selected transport options, they are all detailed on this [page](https://github.com/winstonjs/winston/blob/master/docs/transports.md#built-in-to-winston). NB: It is not specified in winston documentation but transport options can also contain the following properties: -* level: a string that specifies which logger level is associated with this transport +* level: a string that specifies which logger level is associated with this transport * silent: boolean flag indicating whether to suppress output * handleExceptions: boolean flag indicating that transport should log uncaughtException events * handleRejections: boolean flag indicating that transport should log uncaughtRejection events @@ -114,8 +114,8 @@ This module enables to configure transports based on the [winston-daily-rotate-f ### Default values -All default values are defined in the configuration description file `src/config.json` or your custom description object. -For the following properties: log_level, silent, exit_on_error if they are `null` or `undefined` both in configuration and description files then the default values will come from winston library. +All default values are defined in the configuration description file `src/config.json` or your custom description object. +For the following properties: log_level, silent, exit_on_error if they are `null` or `undefined` both in configuration and description files then the default values will come from winston library. Transports options default values will come from winston library too, except for `filename` option #### Logs in file @@ -325,7 +325,7 @@ logger.silly( ) // Output: SILLY | 2028-12-08T21:36:22.011Z | @dwho:example.com | GET | /example/how/to/use/logger | 200 | This is a silly message -// Methods won't crash if they are called with unsupported additionnal detail +// Methods won't crash if they are called with unsupported additionnal detail logger.debug( 'This is an debug message', { diff --git a/packages/matrix-application-server/tsconfig.json b/packages/matrix-application-server/tsconfig.json index 27b0cac4..ff758b0f 100644 --- a/packages/matrix-application-server/tsconfig.json +++ b/packages/matrix-application-server/tsconfig.json @@ -5,4 +5,3 @@ }, "include": ["src/**/*"] } - \ No newline at end of file diff --git a/packages/matrix-client-server/README.md b/packages/matrix-client-server/README.md new file mode 100644 index 00000000..dfaf5ba2 --- /dev/null +++ b/packages/matrix-client-server/README.md @@ -0,0 +1,25 @@ +# @twake/matrix-client-server + +Node.js library that implements +[Matrix Client Server API](https://spec.matrix.org/v1.10/client-server-api/). + +## Synopsis + +Example using [express](https://www.npmjs.com/package/express): + +``js +// Add example which will be similar to the one inside packages/matrix-identity-server/README.md + +``` + +## Configuration file + +Configuration file is a JSON file. The default values are +in [src/config.json](./src/config.json). + +## Copyright and license + +Copyright (c) 2023-present Linagora + +License: [GNU AFFERO GENERAL PUBLIC LICENSE](https://ci.linagora.com/publicgroup/oss/twake/tom-server/-/blob/master/LICENSE) +``` diff --git a/packages/matrix-client-server/example/client-server.ts b/packages/matrix-client-server/example/client-server.ts new file mode 100644 index 00000000..04f147c7 --- /dev/null +++ b/packages/matrix-client-server/example/client-server.ts @@ -0,0 +1,25 @@ +import express from 'express' + +import ClientServer from '@twake/matrix-client-server' + +const clientServer = new ClientServer({ + database_host: ':memory:' +}) + +const app = express() + +clientServer.ready + .then(() => { + Object.keys(clientServer.api.get).forEach((k) => { + app.get(k, clientServer.api.get[k]) + }) + Object.keys(clientServer.api.post).forEach((k) => { + app.post(k, clientServer.api.post[k]) + }) + const port = process.argv[2] != null ? parseInt(process.argv[2]) : 3000 + console.log(`Listening on port ${port}`) + app.listen(port) + }) + .catch((e) => { + throw e + }) diff --git a/packages/matrix-client-server/jest.config.js b/packages/matrix-client-server/jest.config.js new file mode 100644 index 00000000..0d6e4c0a --- /dev/null +++ b/packages/matrix-client-server/jest.config.js @@ -0,0 +1,6 @@ +import jestConfigBase from '../../jest-base.config.js' + +export default { + ...jestConfigBase, + testTimeout: 30000 +} diff --git a/packages/matrix-client-server/package.json b/packages/matrix-client-server/package.json new file mode 100644 index 00000000..64d3d13a --- /dev/null +++ b/packages/matrix-client-server/package.json @@ -0,0 +1,65 @@ +{ + "name": "@twake/matrix-client-server", + "version": "0.0.1", + "description": "Matrix Client Server", + "keywords": [ + "matrix", + "twake" + ], + "homepage": "https://ci.linagora.com/publicgroup/oss/twake/tom-server", + "bugs": { + "url": "https://ci.linagora.com/publicgroup/oss/twake/tom-server/-/issues" + }, + "repository": { + "type": "git", + "url": "https://ci.linagora.com/publicgroup/oss/twake/tom-server.git" + }, + "license": "AGPL-3.0-or-later", + "authors": [ + { + "name": "Xavier Guimard", + "email": "yadd@debian.org" + }, + { + "name": "Mathias Perez", + "email": "mathias.perez.2022@polytechnique.org" + }, + { + "name": "Hippolyte Wallaert", + "email": "hippolyte.wallaert.2022@polytechnique.org" + }, + { + "name": "Amine Chraibi", + "email": "amine.chraibi.2022@polytechnique.org" + } + ], + "type": "module", + "exports": { + "import": "./dist/index.js" + }, + "main": "dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "package.json", + "dist", + "example/client-server.js", + "templates", + "*.md" + ], + "scripts": { + "build": "npm run build:lib && npm run build:example", + "build:example": "rollup -p @rollup/plugin-typescript -e express,@twake/matrix-client-server -m -o example/client-server.js example/client-server.ts", + "build:lib": "rollup -c", + "start": "node server.mjs", + "test": "jest --passWithNoTests" + }, + "dependencies": { + "@twake/config-parser": "*", + "@twake/logger": "*", + "@twake/matrix-identity-server": "*", + "@twake/utils": "*", + "express": "^4.19.2", + "libphonenumber-js": "^1.11.4", + "node-fetch": "^3.3.0" + } +} diff --git a/packages/matrix-client-server/rollup.config.js b/packages/matrix-client-server/rollup.config.js new file mode 100644 index 00000000..f215c209 --- /dev/null +++ b/packages/matrix-client-server/rollup.config.js @@ -0,0 +1,9 @@ +import config from '../../rollup-template.js' + +export default config([ + '@twake/config-parser', + '@twake/logger', + '@twake/matrix-identity-server', + '@twake/utils', + 'fs' +]) diff --git a/packages/matrix-client-server/server.mjs b/packages/matrix-client-server/server.mjs new file mode 100644 index 00000000..fe794c25 --- /dev/null +++ b/packages/matrix-client-server/server.mjs @@ -0,0 +1,112 @@ +import MatrixClientServer from '@twake/matrix-client-server' +import express from 'express' +import path from 'node:path' +import { fileURLToPath } from 'url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const conf = { + base_url: process.env.BASE_URL, + additional_features: process.env.ADDITIONAL_FEATURES || false, + cron_service: process.env.CRON_SERVICE ?? true, + database_engine: process.env.DATABASE_ENGINE || 'sqlite', + database_host: process.env.DATABASE_HOST || './tokens.db', + database_name: process.env.DATABASE_NAME, + database_user: process.env.DATABASE_USER, + database_ssl: process.env.DATABASE_SSL + ? JSON.parse(process.env.DATABASE_SSL) + : false, + database_password: process.env.DATABASE_PASSWORD, + federated_identity_services: process.env.FEDERATED_IDENTITY_SERVICES + ? process.env.FEDERATED_IDENTITY_SERVICES.split(/[,\s]+/) + : [], + hashes_rate_limit: process.env.HASHES_RATE_LIMIT, + is_federated_identity_service: false, + ldap_base: process.env.LDAP_BASE, + ldap_filter: process.env.LDAP_FILTER, + ldap_user: process.env.LDAP_USER, + ldap_password: process.env.LDAP_PASSWORD, + ldap_uri: process.env.LDAP_URI, + matrix_database_engine: process.env.MATRIX_DATABASE_ENGINE, + matrix_database_host: process.env.MATRIX_DATABASE_HOST, + matrix_database_name: process.env.MATRIX_DATABASE_NAME, + matrix_database_password: process.env.MATRIX_DATABASE_PASSWORD, + matrix_database_user: process.env.MATRIX_DATABASE_USER, + matrix_database_ssl: process.env.MATRIX_DATABASE_SSL + ? JSON.parse(process.env.MATRIX_DATABASE_SSL) + : false, + pepperCron: process.env.PEPPER_CRON || '9 1 * * *', + rate_limiting_window: process.env.RATE_LIMITING_WINDOW || 600000, + rate_limiting_nb_requests: process.env.RATE_LIMITING_NB_REQUESTS || 100, + redis_uri: process.env.REDIS_URI, + server_name: process.env.SERVER_NAME, + smtp_password: process.env.SMTP_PASSWORD, + smtp_tls: process.env.SMTP_TLS ?? true, + smtp_user: process.env.SMTP_USER, + smtp_verify_certificate: process.env.SMTP_VERIFY_CERTIFICATE, + smtp_sender: process.env.SMTP_SENDER ?? '', + smtp_server: process.env.SMTP_SERVER || 'localhost', + smtp_port: process.env.SMTP_PORT || 25, + template_dir: process.env.TEMPLATE_DIR || path.join(__dirname, 'templates'), + update_federated_identity_hashes_cron: + process.env.UPDATE_FEDERATED_IDENTITY_HASHES_CRON || '*/10 * * * *', + update_users_cron: process.env.UPDATE_USERS_CRON || '*/10 * * * *', + userdb_engine: process.env.USERDB_ENGINE || 'sqlite', + userdb_host: process.env.USERDB_HOST || './users.db', + userdb_name: process.env.USERDB_NAME, + userdb_password: process.env.USERDB_PASSWORD, + userdb_ssl: process.env.USERDB_SSL + ? JSON.parse(process.env.USERDB_SSL) + : false, + userdb_user: process.env.USERDB_USER +} + +const app = express() +const trustProxy = process.env.TRUSTED_PROXIES + ? process.env.TRUSTED_PROXIES.split(/\s+/) + : [] +if (trustProxy.length > 0) { + conf.trust_x_forwarded_for = true + app.set('trust proxy', ...trustProxy) +} +const matrixClientServer = new MatrixClientServer(conf) +const promises = [matrixClientServer.ready] + +if (process.env.CROWDSEC_URI) { + if (!process.env.CROWDSEC_KEY) { + throw new Error('Missing CROWDSEC_KEY') + } + promises.push( + new Promise((resolve, reject) => { + import('@crowdsec/express-bouncer') + .then((m) => + m.default({ + url: process.env.CROWDSEC_URI, + apiKey: process.env.CROWDSEC_KEY + }) + ) + .then((crowdsecMiddleware) => { + app.use(crowdsecMiddleware) + resolve() + }) + .catch(reject) + }) + ) +} + +Promise.all(promises) + .then(() => { + Object.keys(matrixClientServer.api.get).forEach((k) => { + app.get(k, matrixClientServer.api.get[k]) + }) + Object.keys(matrixClientServer.api.post).forEach((k) => { + app.post(k, matrixClientServer.api.post[k]) + }) + const port = process.argv[2] != null ? parseInt(process.argv[2]) : 3000 + console.log(`Listening on port ${port}`) + app.listen(port) + }) + .catch((e) => { + throw new Error(e) + }) diff --git a/packages/matrix-client-server/src/__testData__/buildUserDB.ts b/packages/matrix-client-server/src/__testData__/buildUserDB.ts new file mode 100644 index 00000000..57b48aec --- /dev/null +++ b/packages/matrix-client-server/src/__testData__/buildUserDB.ts @@ -0,0 +1,184 @@ +/* istanbul ignore file */ +import { getLogger, type TwakeLogger } from '@twake/logger' +import sqlite3 from 'sqlite3' +import { type Config } from '../types' +import { + type UserDBPg, + type UserDBSQLite, + UserDB +} from '@twake/matrix-identity-server' + +const logger: TwakeLogger = getLogger() + +let created = false +let matrixDbCreated = false + +const createQuery = + 'CREATE TABLE IF NOT EXISTS users (uid varchar(8), mobile varchar(12), mail varchar(32))' +const insertQuery = + "INSERT INTO users VALUES('dwho', '33612345678', 'dwho@company.com')" +const insertQuery2 = + "INSERT INTO users VALUES('rtyler', '33687654321', 'rtyler@company.com')" + +const matrixDbQueries = [ + 'CREATE TABLE IF NOT EXISTS profiles( user_id TEXT NOT NULL, displayname TEXT, avatar_url TEXT, UNIQUE(user_id) )', + 'CREATE TABLE IF NOT EXISTS users( name TEXT, password_hash TEXT, creation_ts BIGINT, admin SMALLINT DEFAULT 0 NOT NULL, upgrade_ts BIGINT, is_guest SMALLINT DEFAULT 0 NOT NULL, appservice_id TEXT, consent_version TEXT, consent_server_notice_sent TEXT, user_type TEXT DEFAULT NULL, deactivated SMALLINT DEFAULT 0 NOT NULL, shadow_banned INT DEFAULT 0, consent_ts bigint, UNIQUE(name) )', + 'CREATE TABLE IF NOT EXISTS user_ips ( user_id TEXT NOT NULL, access_token TEXT NOT NULL, device_id TEXT, ip TEXT NOT NULL, user_agent TEXT NOT NULL, last_seen BIGINT NOT NULL)', + 'CREATE TABLE IF NOT EXISTS registration_tokens (token TEXT NOT NULL, uses_allowed INT, pending INT NOT NULL, completed INT NOT NULL, expiry_time BIGINT,UNIQUE (token))', + 'CREATE TABLE IF NOT EXISTS events( stream_ordering INTEGER PRIMARY KEY, topological_ordering BIGINT NOT NULL, event_id TEXT NOT NULL, type TEXT NOT NULL, room_id TEXT NOT NULL, content TEXT, unrecognized_keys TEXT, processed INTEGER NOT NULL, outlier INTEGER NOT NULL, depth BIGINT DEFAULT 0 NOT NULL, origin_server_ts BIGINT, received_ts BIGINT, sender TEXT, contains_url INT, instance_name TEXT, state_key TEXT DEFAULT NULL, rejection_reason TEXT DEFAULT NULL, UNIQUE (event_id) )', + 'CREATE TABLE IF NOT EXISTS room_memberships( event_id TEXT NOT NULL, user_id TEXT NOT NULL, sender TEXT NOT NULL, room_id TEXT NOT NULL, membership TEXT NOT NULL, forgotten INTEGER DEFAULT 0, display_name TEXT, avatar_url TEXT, UNIQUE (event_id) )', + 'CREATE TABLE IF NOT EXISTS devices (user_id TEXT NOT NULL, device_id TEXT NOT NULL, display_name TEXT, last_seen BIGINT, ip TEXT, user_agent TEXT, hidden INT DEFAULT 0,CONSTRAINT device_uniqueness UNIQUE (user_id, device_id))', + 'CREATE TABLE IF NOT EXISTS account_data( user_id TEXT NOT NULL, account_data_type TEXT NOT NULL, stream_id BIGINT NOT NULL, content TEXT NOT NULL, instance_name TEXT, CONSTRAINT account_data_uniqueness UNIQUE (user_id, account_data_type))', + 'CREATE TABLE IF NOT EXISTS room_account_data( user_id TEXT NOT NULL, room_id TEXT NOT NULL, account_data_type TEXT NOT NULL, stream_id BIGINT NOT NULL, content TEXT NOT NULL, instance_name TEXT, CONSTRAINT room_account_data_uniqueness UNIQUE (user_id, room_id, account_data_type) )', + 'CREATE TABLE IF NOT EXISTS profiles( user_id TEXT NOT NULL, displayname TEXT, avatar_url TEXT, UNIQUE(user_id) )', + 'CREATE TABLE IF NOT EXISTS local_current_membership (room_id TEXT NOT NULL, user_id TEXT NOT NULL, event_id TEXT NOT NULL, membership TEXT NOT NULL)', + 'CREATE TABLE IF NOT EXISTS room_stats_state (room_id TEXT NOT NULL,name TEXT,canonical_alias TEXT,join_rules TEXT,history_visibility TEXT,encryption TEXT,avatar TEXT,guest_access TEXT,is_federatable INT,topic TEXT, room_type TEXT)', + 'CREATE TABLE IF NOT EXISTS room_aliases( room_alias TEXT NOT NULL, room_id TEXT NOT NULL, creator TEXT, UNIQUE (room_alias) )', + 'CREATE TABLE IF NOT EXISTS rooms( room_id TEXT PRIMARY KEY NOT NULL, is_public INTEGER, creator TEXT , room_version TEXT, has_auth_chain_index INT)', + 'CREATE TABLE IF NOT EXISTS room_tags( user_id TEXT NOT NULL, room_id TEXT NOT NULL, tag TEXT NOT NULL, content TEXT NOT NULL, CONSTRAINT room_tag_uniqueness UNIQUE (user_id, room_id, tag) )', + 'CREATE TABLE IF NOT EXISTS "user_threepids" ( user_id TEXT NOT NULL, medium TEXT NOT NULL, address TEXT NOT NULL, validated_at BIGINT NOT NULL, added_at BIGINT NOT NULL, CONSTRAINT medium_address UNIQUE (medium, address) )', + 'CREATE TABLE IF NOT EXISTS threepid_validation_session (session_id TEXT PRIMARY KEY,medium TEXT NOT NULL,address TEXT NOT NULL,client_secret TEXT NOT NULL,last_send_attempt BIGINT NOT NULL,validated_at BIGINT)', + 'CREATE TABLE IF NOT EXISTS threepid_validation_token (token TEXT PRIMARY KEY,session_id TEXT NOT NULL,next_link TEXT,expires BIGINT NOT NULL)', + 'CREATE TABLE IF NOT EXISTS presence (user_id TEXT NOT NULL, state VARCHAR(20), status_msg TEXT, mtime BIGINT, UNIQUE (user_id))', + 'CREATE TABLE IF NOT EXISTS open_id_tokens ( token TEXT NOT NULL PRIMARY KEY, ts_valid_until_ms bigint NOT NULL, user_id TEXT NOT NULL, UNIQUE (token) )', + 'CREATE TABLE IF NOT EXISTS user_threepid_id_server ( user_id TEXT NOT NULL, medium TEXT NOT NULL, address TEXT NOT NULL, id_server TEXT NOT NULL )', + 'CREATE TABLE IF NOT EXISTS "access_tokens" (id BIGINT PRIMARY KEY, user_id TEXT NOT NULL, device_id TEXT, token TEXT NOT NULL,valid_until_ms BIGINT,puppets_user_id TEXT,last_validated BIGINT, refresh_token_id BIGINT REFERENCES refresh_tokens (id) ON DELETE CASCADE, used INTEGEREAN,UNIQUE(token))', + 'CREATE TABLE IF NOT EXISTS refresh_tokens (id BIGINT PRIMARY KEY,user_id TEXT NOT NULL,device_id TEXT NOT NULL,token TEXT NOT NULL,next_token_id BIGINT REFERENCES refresh_tokens (id) ON DELETE CASCADE, expiry_ts BIGINT DEFAULT NULL, ultimate_session_expiry_ts BIGINT DEFAULT NULL,UNIQUE(token))', + 'CREATE TABLE IF NOT EXISTS current_state_events (event_id text NOT NULL,room_id text NOT NULL,type text NOT NULL,state_key text NOT NULL,membership text)', + 'CREATE TABLE IF NOT EXISTS "user_filters" ( user_id TEXT NOT NULL, filter_id BIGINT NOT NULL, filter_json BYTEA NOT NULL )', + 'CREATE TABLE IF NOT EXISTS ui_auth_sessions(session_id TEXT NOT NULL,creation_time BIGINT NOT NULL, serverdict TEXT NOT NULL, clientdict TEXT NOT NULL,uri TEXT NOT NULL, method TEXT NOT NULL, description TEXT NOT NULL, UNIQUE (session_id))', + 'CREATE TABLE IF NOT EXISTS ui_auth_sessions_credentials(session_id TEXT NOT NULL, stage_type TEXT NOT NULL, result TEXT NOT NULL, UNIQUE (session_id, stage_type),FOREIGN KEY (session_id) REFERENCES ui_auth_sessions (session_id))', + 'CREATE TABLE IF NOT EXISTS ui_auth_sessions_ips(session_id TEXT NOT NULL,ip TEXT NOT NULL,user_agent TEXT NOT NULL,UNIQUE (session_id, ip, user_agent), FOREIGN KEY (session_id)REFERENCES ui_auth_sessions (session_id))', + 'CREATE TABLE IF NOT EXISTS current_state_events (event_id text NOT NULL,room_id text NOT NULL,type text NOT NULL,state_key text NOT NULL,membership text)', + 'CREATE TABLE IF NOT EXISTS users_in_public_rooms ( user_id TEXT NOT NULL, room_id TEXT NOT NULL )', + 'CREATE TABLE IF NOT EXISTS users_who_share_private_rooms ( user_id TEXT NOT NULL, other_user_id TEXT NOT NULL, room_id TEXT NOT NULL )', + 'CREATE UNIQUE INDEX users_who_share_private_rooms_u_idx ON users_who_share_private_rooms(user_id, other_user_id, room_id)', + 'CREATE TABLE IF NOT EXISTS "user_directory" ( user_id TEXT NOT NULL, room_id TEXT, display_name TEXT, avatar_url TEXT )', + 'CREATE INDEX user_directory_room_idx ON user_directory(room_id)', + 'CREATE UNIQUE INDEX user_directory_user_idx ON user_directory(user_id)', + 'CREATE VIRTUAL TABLE user_directory_search USING fts4 ( user_id, value )', + 'CREATE TABLE IF NOT EXISTS "deleted_pushers" ( stream_id BIGINT NOT NULL, app_id TEXT NOT NULL, pushkey TEXT NOT NULL, user_id TEXT NOT NULL )', + 'CREATE TABLE IF NOT EXISTS "pushers" ( id BIGINT PRIMARY KEY, user_name TEXT NOT NULL, access_token BIGINT DEFAULT NULL, profile_tag TEXT NOT NULL, kind TEXT NOT NULL, app_id TEXT NOT NULL, app_display_name TEXT NOT NULL, device_display_name TEXT NOT NULL, pushkey TEXT NOT NULL, ts BIGINT NOT NULL, lang TEXT, data TEXT, last_stream_ordering INTEGER, last_success BIGINT, failing_since BIGINT, UNIQUE (app_id, pushkey, user_name) )', + 'CREATE TABLE IF NOT EXISTS erased_users ( user_id TEXT NOT NULL )', + 'CREATE TABLE IF NOT EXISTS event_expiry (event_id TEXT PRIMARY KEY,expiry_ts BIGINT NOT NULL)', + 'CREATE TABLE IF NOT EXISTS account_validity ( user_id TEXT PRIMARY KEY, expiration_ts_ms BIGINT NOT NULL, email_sent INTEGEREAN NOT NULL, renewal_token TEXT , token_used_ts_ms BIGINT)', + 'CREATE TABLE IF NOT EXISTS push_rules ( id BIGINT PRIMARY KEY, user_name TEXT NOT NULL, rule_id TEXT NOT NULL, priority_class SMALLINT NOT NULL, priority INTEGER NOT NULL DEFAULT 0, conditions TEXT NOT NULL, actions TEXT NOT NULL, UNIQUE(user_name, rule_id) )', + 'CREATE TABLE IF NOT EXISTS push_rules_enable ( id BIGINT PRIMARY KEY, user_name TEXT NOT NULL, rule_id TEXT NOT NULL, enabled SMALLINT, UNIQUE(user_name, rule_id) )', + 'CREATE TABLE IF NOT EXISTS push_rules_stream( stream_id BIGINT NOT NULL, event_stream_ordering BIGINT NOT NULL, user_id TEXT NOT NULL, rule_id TEXT NOT NULL, op TEXT NOT NULL, priority_class SMALLINT, priority INTEGER, conditions TEXT, actions TEXT )', + 'CREATE TABLE IF NOT EXISTS ignored_users( ignorer_user_id TEXT NOT NULL, ignored_user_id TEXT NOT NULL )', + 'CREATE TABLE IF NOT EXISTS "e2e_room_keys" ( user_id TEXT NOT NULL, room_id TEXT NOT NULL, session_id TEXT NOT NULL, version BIGINT NOT NULL, first_message_index INT, forwarded_count INT, is_verified INTEGEREAN, session_data TEXT NOT NULL )', + 'CREATE TABLE IF NOT EXISTS "e2e_room_keys_versions" ( user_id TEXT NOT NULL, version BIGINT NOT NULL, algorithm TEXT NOT NULL, auth_data TEXT NOT NULL, deleted SMALLINT DEFAULT 0 NOT NULL , etag BIGINT)', + 'CREATE TABLE IF NOT EXISTS event_json( event_id TEXT NOT NULL, room_id TEXT NOT NULL, internal_metadata TEXT NOT NULL, json TEXT NOT NULL, format_version INTEGER, UNIQUE (event_id))', + 'CREATE TABLE IF NOT EXISTS device_auth_providers (user_id TEXT NOT NULL,device_id TEXT NOT NULL,auth_provider_id TEXT NOT NULL,auth_provider_session_id TEXT NOT NULL)', + 'CREATE TABLE IF NOT EXISTS e2e_device_keys_json ( user_id TEXT NOT NULL, device_id TEXT NOT NULL, ts_added_ms BIGINT NOT NULL, key_json TEXT NOT NULL, CONSTRAINT e2e_device_keys_json_uniqueness UNIQUE (user_id, device_id) )', + 'CREATE TABLE IF NOT EXISTS e2e_one_time_keys_json ( user_id TEXT NOT NULL, device_id TEXT NOT NULL, algorithm TEXT NOT NULL, key_id TEXT NOT NULL, ts_added_ms BIGINT NOT NULL, key_json TEXT NOT NULL, CONSTRAINT e2e_one_time_keys_json_uniqueness UNIQUE (user_id, device_id, algorithm, key_id) )', + 'CREATE TABLE IF NOT EXISTS e2e_fallback_keys_json (user_id TEXT NOT NULL, device_id TEXT NOT NULL, algorithm TEXT NOT NULL, key_id TEXT NOT NULL, key_json TEXT NOT NULL, used BOOLEAN NOT NULL DEFAULT FALSE, CONSTRAINT e2e_fallback_keys_json_uniqueness UNIQUE (user_id, device_id, algorithm))', + 'CREATE TABLE IF NOT EXISTS dehydrated_devices(user_id TEXT NOT NULL PRIMARY KEY,device_id TEXT NOT NULL,device_data TEXT NOT NULL)', + 'CREATE TABLE IF NOT EXISTS device_inbox ( user_id TEXT NOT NULL, device_id TEXT NOT NULL, stream_id BIGINT NOT NULL, message_json TEXT NOT NULL , instance_name TEXT)' +] + +// eslint-disable-next-line @typescript-eslint/promise-function-async +const runQueries = ( + db: sqlite3.Database | any, + queries: string[], + isSqlite: boolean +): Promise => { + return new Promise((resolve, reject) => { + const runNextQuery = (index: number): void => { + if (index >= queries.length) { + resolve() + } else { + if (isSqlite) { + db.run(queries[index], (err: Error | null) => { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (err) { + reject(err) + } else { + runNextQuery(index + 1) + } + }) + } else { + db.query(queries[index], (err: Error | null) => { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (err) { + reject(err) + } else { + runNextQuery(index + 1) + } + }) + } + } + } + runNextQuery(0) + }) +} + +// eslint-disable-next-line @typescript-eslint/promise-function-async +export const buildUserDB = (conf: Config): Promise => { + if (created) return Promise.resolve() + const userDb = new UserDB(conf, logger) + return new Promise((resolve, reject) => { + /* istanbul ignore else */ + if (conf.userdb_engine === 'sqlite') { + userDb.ready + .then(() => { + ;(userDb.db as UserDBSQLite).db?.run(createQuery, () => { + ;(userDb.db as UserDBSQLite).db?.run(insertQuery, () => { + ;(userDb.db as UserDBSQLite).db + ?.run(insertQuery2) + .close((err) => { + /* istanbul ignore if */ + if (err != null) { + reject(err) + } else { + logger.close() + created = true + resolve() + } + }) + }) + }) + }) + .catch(reject) + } else { + ;(userDb.db as UserDBPg).db?.query(createQuery, () => { + ;(userDb.db as UserDBPg).db?.query(insertQuery, () => { + logger.close() + created = true + resolve() + }) + }) + } + }) +} + +// eslint-disable-next-line @typescript-eslint/promise-function-async +export const buildMatrixDb = (conf: Config): Promise => { + if (matrixDbCreated) return Promise.resolve() + const matrixDb = new sqlite3.Database(conf.matrix_database_host as string) + return new Promise((resolve, reject) => { + if (conf.matrix_database_engine === 'sqlite') { + runQueries(matrixDb, matrixDbQueries, true) + .then(() => { + matrixDb.close((err) => { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (err) { + reject(err) + } else { + matrixDbCreated = true + resolve() + } + }) + }) + .catch((err) => { + matrixDb.close(() => { + reject(err) + }) + }) + } else { + matrixDb.close(() => { + reject(new Error('only SQLite is implemented here')) + }) + } + }) +} diff --git a/packages/matrix-client-server/src/__testData__/pg/Dockerfile b/packages/matrix-client-server/src/__testData__/pg/Dockerfile new file mode 100644 index 00000000..70d27497 --- /dev/null +++ b/packages/matrix-client-server/src/__testData__/pg/Dockerfile @@ -0,0 +1,12 @@ +FROM postgres:13-bullseye + +LABEL maintainer="Yadd yadd@debian.org>" \ + name="yadd/twke-test-server" \ + version="v1.0" + +ENV PG_DATABASE=test \ + PG_USER=twake \ + PG_PASSWORD=twake \ + PG_TABLE=test + +COPY install / diff --git a/packages/matrix-client-server/src/__testData__/pg/install/docker-entrypoint-initdb.d/init-user-db.sh b/packages/matrix-client-server/src/__testData__/pg/install/docker-entrypoint-initdb.d/init-user-db.sh new file mode 100644 index 00000000..2d5d404b --- /dev/null +++ b/packages/matrix-client-server/src/__testData__/pg/install/docker-entrypoint-initdb.d/init-user-db.sh @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +DATABASE=${PG_DATABASE:-twake} +USER=${PG_USER:-twake} +PASSWORD=${PG_PASSWORD:-twake} + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + CREATE USER $USER PASSWORD '$PASSWORD'; + CREATE DATABASE $DATABASE; + GRANT ALL PRIVILEGES ON DATABASE $DATABASE TO $USER; +EOSQL diff --git a/packages/matrix-client-server/src/__testData__/registerConf.json b/packages/matrix-client-server/src/__testData__/registerConf.json new file mode 100644 index 00000000..890636e8 --- /dev/null +++ b/packages/matrix-client-server/src/__testData__/registerConf.json @@ -0,0 +1,60 @@ +{ + "cron_service": false, + "database_engine": "sqlite", + "database_host": "./src/__testData__/test.db", + "matrix_database_engine": "sqlite", + "matrix_database_host": "./src/__testData__/testMatrix.db", + "database_vacuum_delay": 7200, + "invitation_server_name": "matrix.to", + "is_federated_identity_service": false, + "key_delay": 3600, + "keys_depth": 5, + "mail_link_delay": 7200, + "rate_limiting_window": 10000, + "server_name": "example.com", + "smtp_sender": "yadd@debian.org", + "smtp_server": "localhost", + "template_dir": "./templates", + "userdb_engine": "sqlite", + "userdb_host": "./src/__testData__/test.db", + "login_flows": { + "flows": [ + { + "type": "m.login.password" + }, + { + "get_login_token": true, + "type": "m.login.token" + } + ] + }, + "application_services": [ + { + "id": "test", + "hs_token": "hsTokenTestwdakZQunWWNe3DZitAerw9aNqJ2a6HVp0sJtg7qTJWXcHnBjgN0NL", + "as_token": "as_token_test", + "url": "http://localhost:3000", + "sender_localpart": "sender_localpart_test", + "namespaces": { + "users": [ + { + "exclusive": true, + "regex": "@_irc_bridge_.*" + } + ] + } + } + ], + "sms_folder": "./src/__testData__/sms", + "is_registration_enabled": true, + "is_email_login_enabled": true, + "is_registration_token_login_enabled": true, + "is_terms_login_enabled": true, + "is_recaptcha_login_enabled": true, + "is_password_login_enabled": true, + "is_sso_login_enabled": true, + "is_msisdn_login_enabled": true, + "registration_required_3pid": [], + "capabilities": {}, + "open_id_token_lifetime": 3600000 +} diff --git a/packages/matrix-client-server/src/__testData__/setupTokens.ts b/packages/matrix-client-server/src/__testData__/setupTokens.ts new file mode 100644 index 00000000..18139a38 --- /dev/null +++ b/packages/matrix-client-server/src/__testData__/setupTokens.ts @@ -0,0 +1,170 @@ +import { Hash, randomString } from '@twake/crypto' +import { epoch } from '@twake/utils' +import type MatrixClientServer from '..' // Adjust the import path as necessary +import { type TwakeLogger } from '@twake/logger' + +export let validToken: string +export let validToken1: string +export let validToken2: string +export let validToken3: string +export let validToken4 : string +export let validRefreshToken1: string +export let validRefreshToken2: string +export let validRefreshToken3: string +export async function setupTokens( + clientServer: MatrixClientServer, + logger: TwakeLogger +): Promise { + validToken = randomString(64) + validToken1 = randomString(64) + validToken2 = randomString(64) + validToken3 = randomString(64) + validToken4 = randomString(64) + const validRefreshTokenId1 = randomString(64) + const validRefreshTokenId2 = randomString(64) + const validRefreshTokenId3 = randomString(64) + validRefreshToken1 = randomString(64) + validRefreshToken2 = randomString(64) + validRefreshToken3 = randomString(64) + + try { + await clientServer.matrixDb.insert('user_ips', { + user_id: '@testuser:example.com', + device_id: 'testdevice', + access_token: validToken, + ip: '127.0.0.1', + user_agent: 'curl/7.31.0-DEV', + last_seen: 1411996332123 + }) + + const hash = new Hash() + await hash.ready + await clientServer.matrixDb.insert('users', { + name: '@testuser:example.com', + password_hash: hash.sha256( + '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK' + ) + }) + + await clientServer.matrixDb.insert('user_ips', { + user_id: '@testuser2:example.com', + device_id: 'testdevice2', + access_token: validToken2, + ip: '137.0.0.1', + user_agent: 'curl/7.31.0-DEV', + last_seen: 1411996332123 + }) + + await clientServer.matrixDb.insert('user_ips', { + user_id: '@testuser3:example.com', + device_id: 'testdevice3', + access_token: validToken3, + ip: '147.0.0.1', + user_agent: 'curl/7.31.0-DEV', + last_seen: 1411996332123 + }) + + await clientServer.matrixDb.insert('refresh_tokens', { + id: validRefreshTokenId3, + user_id: '@seconduser:example.com', + device_id: 'seconddevice', + token: validRefreshToken3 + }) + + await clientServer.matrixDb.insert('refresh_tokens', { + id: validRefreshTokenId2, + user_id: '@seconduser:example.com', + device_id: 'seconddevice', + token: validRefreshToken2 + }) + + await clientServer.matrixDb.insert('refresh_tokens', { + id: validRefreshTokenId1, + user_id: '@firstuser:example.com', + device_id: 'firstdevice', + token: validRefreshToken1, + next_token_id: validRefreshTokenId2 + }) + + await clientServer.matrixDb.insert('access_tokens', { + id: randomString(64), + user_id: '@firstuser:example.com', + device_id: 'firstdevice', + token: validToken1, + valid_until_ms: epoch() + 64000, + refresh_token_id: validRefreshTokenId1 + }) + + await clientServer.matrixDb.insert('access_tokens', { + id: randomString(64), + user_id: '@testuser:example.com', + device_id: 'testdevice', + token: validToken, + valid_until_ms: epoch() + 64000 + }) + + await clientServer.matrixDb.insert('access_tokens', { + user_id: '@testuser2:example.com', + device_id: 'testdevice2', + token: validToken2, + valid_until_ms: epoch() + 64000 + }) + + await clientServer.matrixDb.insert('threepid_validation_session', { + session_id: 'validatedSession2', + medium: 'email', + address: 'validated@example.com', + client_secret: 'validatedSecret2', + last_send_attempt: 1, + validated_at: epoch() + }) + + await clientServer.matrixDb.insert('user_threepids', { + user_id: '@validated:example.com', + medium: 'email', + address: 'validated@example.com', + validated_at: epoch(), + added_at: epoch() + }) + + await clientServer.matrixDb.insert('threepid_validation_session', { + session_id: 'validatedSession', + medium: 'msisdn', + address: '0612938719', + client_secret: 'validatedSecret', + last_send_attempt: 1, + validated_at: epoch() + }) + + await clientServer.matrixDb.insert('user_threepids', { + user_id: '@validated:example.com', + medium: 'msisdn', + address: '0612938719', + validated_at: epoch(), + added_at: epoch() + }) + await clientServer.matrixDb.insert('access_tokens', { + id: randomString(64), + user_id: '@validated:example.com', + device_id: 'thirddevice', + token: validToken4 + }) + await clientServer.matrixDb.insert('access_tokens', { + id: randomString(64), + user_id: '@thirduser:example.com', + device_id: 'thirddevice', + token: validToken3, + refresh_token_id: validRefreshTokenId3, + valid_until_ms: epoch() + 64000 + }) + + await clientServer.matrixDb.insert('access_tokens', { + id: 0, + user_id: 'wrongUserId', + token: 'wrongUserAccessToken' + }) + } catch (e) { + // istanbul ignore next + logger.error('Error creating tokens for authentication', e) + } +} diff --git a/packages/matrix-client-server/src/__testData__/termsConf.json b/packages/matrix-client-server/src/__testData__/termsConf.json new file mode 100644 index 00000000..f46c77ea --- /dev/null +++ b/packages/matrix-client-server/src/__testData__/termsConf.json @@ -0,0 +1,42 @@ +{ + "cron_service": false, + "database_engine": "sqlite", + "database_host": "./src/__testData__/terms.db", + "database_vacuum_delay": 7200, + "invitation_server_name": "matrix.to", + "is_federated_identity_service": false, + "key_delay": 3600, + "keys_depth": 5, + "mail_link_delay": 7200, + "rate_limiting_window": 10000, + "server_name": "matrix.org", + "smtp_sender": "yadd@debian.org", + "smtp_server": "localhost", + "policies": { + "privacy_policy": { + "en": { + "name": "Privacy Policy", + "url": "https://example.org/somewhere/privacy-1.2-en.html" + }, + "fr": { + "name": "Politique de confidentialité", + "url": "https://example.org/somewhere/privacy-1.2-fr.html" + }, + "version": "1.2" + }, + "terms_of_service": { + "en": { + "name": "Terms of Service", + "url": "https://example.org/somewhere/terms-2.0-en.html" + }, + "fr": { + "name": "Conditions d'utilisation", + "url": "https://example.org/somewhere/terms-2.0-fr.html" + }, + "version": "2.0" + } + }, + "template_dir": "./templates", + "userdb_engine": "sqlite", + "userdb_host": "./src/__testData__/terms.db" +} diff --git a/packages/matrix-client-server/src/account/3pid/3pid.test.ts b/packages/matrix-client-server/src/account/3pid/3pid.test.ts new file mode 100644 index 00000000..4e4be4fc --- /dev/null +++ b/packages/matrix-client-server/src/account/3pid/3pid.test.ts @@ -0,0 +1,976 @@ +import fs from 'fs' +import request from 'supertest' +import express from 'express' +import ClientServer from '../../index' +import fetch from 'node-fetch' +import { buildMatrixDb, buildUserDB } from '../../__testData__/buildUserDB' +import { type Config } from '../../types' +import defaultConfig from '../../__testData__/registerConf.json' +import { getLogger, type TwakeLogger } from '@twake/logger' +import { + setupTokens, + validToken, + validToken2 +} from '../../__testData__/setupTokens' +import { epoch } from '@twake/utils' + +jest.mock('node-fetch', () => jest.fn()) +const sendMailMock = jest.fn() +jest.mock('nodemailer', () => ({ + createTransport: jest.fn().mockImplementation(() => ({ + sendMail: sendMailMock + })) +})) +const sendSMSMock = jest.fn() +jest.mock('../../utils/smsSender', () => { + return jest.fn().mockImplementation(() => { + return { + sendSMS: sendSMSMock + } + }) +}) + +let conf: Config +let clientServer: ClientServer +let app: express.Application + +const logger: TwakeLogger = getLogger() + +beforeAll((done) => { + // @ts-expect-error TS doesn't understand that the config is valid + conf = { + ...defaultConfig, + cron_service: false, + database_engine: 'sqlite', + base_url: 'http://example.com/', + userdb_engine: 'sqlite', + matrix_database_engine: 'sqlite', + matrix_database_host: './src/__testData__/testMatrixThreepid.db', + database_host: './src/__testData__/testThreepid.db', + userdb_host: './src/__testData__/testThreepid.db' + } + if (process.env.TEST_PG === 'yes') { + conf.database_engine = 'pg' + conf.userdb_engine = 'pg' + conf.database_host = process.env.PG_HOST ?? 'localhost' + conf.database_user = process.env.PG_USER ?? 'twake' + conf.database_password = process.env.PG_PASSWORD ?? 'twake' + conf.database_name = process.env.PG_DATABASE ?? 'test' + } + buildUserDB(conf) + .then(() => { + buildMatrixDb(conf) + .then(() => { + done() + }) + .catch((e) => { + logger.error('Error while building matrix db:', e) + done(e) + }) + }) + .catch((e) => { + logger.error('Error while building user db:', e) + done(e) + }) +}) + +afterAll(() => { + fs.unlinkSync('src/__testData__/testThreepid.db') + fs.unlinkSync('src/__testData__/testMatrixThreepid.db') +}) + +describe('Use configuration file', () => { + beforeAll((done) => { + clientServer = new ClientServer(conf) + app = express() + clientServer.ready + .then(() => { + Object.keys(clientServer.api.get).forEach((k) => { + app.get(k, clientServer.api.get[k]) + }) + Object.keys(clientServer.api.post).forEach((k) => { + app.post(k, clientServer.api.post[k]) + }) + Object.keys(clientServer.api.put).forEach((k) => { + app.put(k, clientServer.api.put[k]) + }) + Object.keys(clientServer.api.delete).forEach((k) => { + app.delete(k, clientServer.api.delete[k]) + }) + done() + }) + .catch((e) => { + done(e) + }) + }) + + afterAll(() => { + clientServer.cleanJobs() + }) + + beforeEach(() => { + jest.clearAllMocks() + jest.mock('node-fetch', () => jest.fn()) + }) + + describe('Endpoints with authentication', () => { + beforeAll(async () => { + await setupTokens(clientServer, logger) + }) + describe('/_matrix/client/v3/account/3pid/add', () => { + let session: string + describe('User Interactive Authentication', () => { + it('should refuse to validate a userId that does not match the regex', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/add') + .set('Accept', 'application/json') + .set('Authorization', `Bearer wrongUserAccessToken`) + .send({ + sid: 'sid', + client_secret: 'clientsecret' + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid user ID') + }) + it('should refuse to authenticate a user with a password if he does not have one registered', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/add') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken2}`) + .send({ + sid: 'sid', + client_secret: 'clientsecret' + }) + expect(response.statusCode).toBe(401) + session = response.body.session + const response1 = await request(app) + .post('/_matrix/client/v3/account/3pid/add') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken2}`) + .send({ + sid: 'sid', + client_secret: 'clientsecret', + auth: { + type: 'm.login.password', + session, + password: 'password', + identifier: { + type: 'm.id.user', + user: '@testuser2:example.com' + } + } + }) + expect(response1.statusCode).toBe(401) + expect(response1.body).toHaveProperty('errcode', 'M_FORBIDDEN') + expect(response1.body).toHaveProperty( + 'error', + 'The user does not have a password registered or the provided password is wrong.' + ) + }) + }) + let sid: string + let token: string + it('should refuse an invalid secret', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/add') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + sid: 'sid', + client_secret: 'my', + auth: { + type: 'm.login.password', + session: 'session', + password: + '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK', + identifier: { type: 'm.id.user', user: '@testuser:example.com' } + } + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid client_secret') + }) + it('should refuse an invalid session ID', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/add') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + sid: '$;!', + client_secret: 'mysecret', + auth: { + type: 'm.login.password', + session: 'session', + password: + '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK', + identifier: { type: 'm.id.user', user: '@testuser:example.com' } + } + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid session ID') + }) + it('should refuse an invalid auth', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/add') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + sid: 'sid', + client_secret: 'mysecret', + auth: { + type: 'invalidtype' + } + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty( + 'error', + 'Invalid authentication data' + ) + }) + it('should return 400 for a wrong combination of client secret and session ID', async () => { + const response1 = await request(app) + .post('/_matrix/client/v3/account/3pid/add') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({}) + expect(response1.statusCode).toBe(401) + expect(response1.body).toHaveProperty('session') + session = response1.body.session + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/add') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + sid: 'wrongSid', + client_secret: 'mysecret', + auth: { + type: 'm.login.password', + session, + password: + '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK', + identifier: { type: 'm.id.user', user: '@testuser:example.com' } + } + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_NO_VALID_SESSION') + }) + it('should refuse to add a 3pid if the session has not been validated', async () => { + const requesTokenResponse = await request(app) + .post('/_matrix/client/v3/register/email/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + email: 'xg@xnr.fr', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(requesTokenResponse.statusCode).toBe(200) + expect(sendMailMock.mock.calls[0][0].to).toBe('xg@xnr.fr') + expect(sendMailMock.mock.calls[0][0].raw).toMatch( + /token=([a-zA-Z0-9]{64})&client_secret=mysecret&sid=([a-zA-Z0-9]{64})/ + ) + token = RegExp.$1 + sid = RegExp.$2 + const response1 = await request(app) + .post('/_matrix/client/v3/account/3pid/add') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + sid: 'sid', + client_secret: 'mysecret' + }) + expect(response1.statusCode).toBe(401) + session = response1.body.session + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/add') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + sid, + client_secret: 'mysecret', + auth: { + type: 'm.login.password', + session, + password: + '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK', + identifier: { type: 'm.id.user', user: '@testuser:example.com' } + } + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty( + 'errcode', + 'M_SESSION_NOT_VALIDATED' + ) + }) + it('should accept to add a 3pid if the session has been validated', async () => { + const submitTokenResponse = await request(app) + .post('/_matrix/client/v3/register/email/submitToken') + .send({ + token, + client_secret: 'mysecret', + sid + }) + .set('Accept', 'application/json') + expect(submitTokenResponse.statusCode).toBe(200) + const response1 = await request(app) + .post('/_matrix/client/v3/account/3pid/add') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + sid: 'sid', + client_secret: 'mysecret' + }) + expect(response1.statusCode).toBe(401) + session = response1.body.session + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/add') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + sid, + client_secret: 'mysecret', + auth: { + type: 'm.login.password', + session, + password: + '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK', + identifier: { type: 'm.id.user', user: '@testuser:example.com' } + } + }) + expect(response.statusCode).toBe(200) + }) + it('should refuse adding a 3pid already associated to another user', async () => { + const response1 = await request(app) + .post('/_matrix/client/v3/account/3pid/add') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + sid: 'sid', + client_secret: 'mysecret' + }) + expect(response1.statusCode).toBe(401) + session = response1.body.session + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/add') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + sid, + client_secret: 'mysecret', + auth: { + type: 'm.login.password', + session, + password: + '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK', + identifier: { type: 'm.id.user', user: '@testuser:example.com' } + } + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_THREEPID_IN_USE') + }) + it('should refuse to add a 3pid if the user is not an admin and the server does not allow it', async () => { + clientServer.conf.capabilities.enable_3pid_changes = false + const response1 = await request(app) + .post('/_matrix/client/v3/account/3pid/add') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + sid: 'sid', + client_secret: 'mysecret' + }) + expect(response1.statusCode).toBe(401) + session = response1.body.session + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/add') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + sid, + client_secret: 'mysecret', + auth: { + type: 'm.login.password', + session, + password: + '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK', + identifier: { type: 'm.id.user', user: '@testuser:example.com' } + } + }) + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + expect(response.body).toHaveProperty( + 'error', + 'Cannot add 3pid as it is not allowed by server' + ) + delete clientServer.conf.capabilities.enable_3pid_changes + }) + // Used to work but not anymore since we only check UI Auth with m.login.password or m.login.sso + // it('should refuse adding a userId that is not of the right format', async () => { + // const response = await request(app) + // .post('/_matrix/client/v3/account/3pid/add') + // .set('Accept', 'application/json') + // .set('Authorization', `Bearer ${validToken}`) + // .send({ + // sid, + // client_secret: 'mysecret', + // auth: { + // type: 'm.login.password', + // session: 'authSession7', + // password: + // '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK', + // identifier: { type: 'm.id.user', user: '@testuser:example.com' } + // } + // }) + // expect(response.statusCode).toBe(400) + // expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + // }) + }) + describe('/_matrix/client/v3/account/3pid/delete', () => { + it('should return 403 if the user is not an admin and the server does not allow it', async () => { + clientServer.conf.capabilities.enable_3pid_changes = false + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/delete') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + medium: 'email', + address: 'testuser@example.com' + }) + + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + expect(response.body).toHaveProperty( + 'error', + 'Cannot add 3pid as it is not allowed by server' + ) + delete clientServer.conf.capabilities.enable_3pid_changes + }) + it('should refuse an invalid medium', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/delete') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + medium: 'wrongmedium', + address: 'testuser@example.com' + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty( + 'error', + 'Invalid medium, medium must be either email or msisdn' + ) + }) + it('should refuse an invalid email', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/delete') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + medium: 'email', + address: 'testuser' + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid email address') + }) + it('should refuse an invalid phone number', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/delete') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + medium: 'msisdn', + address: 'testuser' + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid phone number') + }) + it('should unbind from the id server provided in the request body', async () => { + const mockResolveResponse = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + email: 'dwho@example.com', + 'm.server': 'matrix.example.com:8448' + }) + }) + + const mockRegisterResponse = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + token: 'validToken' + }) + }) + const mockUnbindResponse = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + address: 'testuser@example.com', + medium: 'email' + }) + }) + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockResolveResponse) + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockRegisterResponse) + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockUnbindResponse) + + await clientServer.matrixDb.insert('user_threepid_id_server', { + address: 'testuser@example.com', + medium: 'email', + user_id: '@testuser:example.com', + id_server: 'matrix.example.com' + }) + + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/delete') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + medium: 'email', + address: 'testuser@example.com', + id_server: 'matrix.example.com' + }) + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty( + 'id_server_unbind_result', + 'success' + ) + }) + it('should unbind from the identity server that was used to bind the threepid if no id_server is provided', async () => { + await clientServer.matrixDb.insert('user_threepid_id_server', { + address: 'testuser@example.com', + medium: 'email', + user_id: '@testuser:example.com', + id_server: 'matrix.example.com' + }) + + await clientServer.matrixDb.insert('user_threepids', { + address: 'testuser@example.com', + medium: 'email', + user_id: '@testuser:example.com', + validated_at: epoch(), + added_at: epoch() + }) + + const mockResolveResponse = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + email: 'dwho@example.com', + 'm.server': 'matrix.example.com:8448' + }) + }) + + const mockRegisterResponse = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + token: 'validToken' + }) + }) + const mockUnbindResponse = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + address: 'testuser@example.com', + medium: 'email' + }) + }) + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockResolveResponse) + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockRegisterResponse) + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockUnbindResponse) + + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/delete') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + medium: 'email', + address: 'testuser@example.com' + }) + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty( + 'id_server_unbind_result', + 'success' + ) + + // added this user so as not to mess up future tests if validToken is used again + await clientServer.matrixDb.insert('user_threepids', { + address: 'testuser@example.com', + medium: 'email', + user_id: '@testuser:example.com', + validated_at: epoch(), + added_at: epoch() + }) + }) + it('should return an error if the user was not previously bound to any id server', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/delete') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + medium: 'email', + address: 'testuser@example.com' + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty( + 'id_server_unbind_result', + 'no-support' + ) + }) + it('should return an error if the unbind was unsuccessful on the id-server', async () => { + const mockResolveResponse = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + email: 'dwho@example.com', + 'm.server': 'matrix.example.com:8448' + }) + }) + + const mockRegisterResponse = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + token: 'validToken' + }) + }) + const mockUnbindResponse = Promise.resolve({ + ok: false, + status: 403, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + error: 'invalid session ID or client_secret', + errcode: 'M_INVALID_PARAM' + }) + }) + + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockResolveResponse) + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockRegisterResponse) + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockUnbindResponse) + + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/delete') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + medium: 'email', + address: 'testuser@example.com', + id_server: 'matrix.example.com' + }) + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty( + 'id_server_unbind_result', + 'no-support' + ) + }) + }) + + describe('/_matrix/client/v3/account/3pid/bind', () => { + it('should return 200 on a successful bind', async () => { + const mockResolveResponse = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + email: 'dwho@example.com', + 'm.server': 'matrix.example.com:8448' + }) + }) + + const mockBindResponse = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => { + return { + medium: 'email', + address: 'localhost@example.com', + mxid: '@testuser:example.com', + not_after: 1234567890, + not_before: 1234567890, + signatures: {}, + ts: 1234567890 + } + } + }) + + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockResolveResponse) + + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockBindResponse) + + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/bind') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + id_access_token: 'myaccesstoken', + id_server: 'matrix.example.com', + sid: 'mysid' + }) + + expect(response.statusCode).toBe(200) + }) + + it('should return an error if bind fails', async () => { + const mockResolveResponse = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + email: 'dwho@example.com', + 'm.server': 'matrix.example.com:8448' + }) + }) + + const mockBindResponse = Promise.resolve({ + ok: false, + status: 400, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => { + return { + errcode: 'M_SESSION_NOT_VALIDATED', + error: 'This validation session has not yet been completed' + } + } + }) + + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockResolveResponse) + + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockBindResponse) + + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/bind') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + id_access_token: 'myaccesstoken', + id_server: 'matrix.example.com', + sid: 'mysid' + }) + + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty( + 'errcode', + 'M_SESSION_NOT_VALIDATED' + ) + expect(response.body).toHaveProperty( + 'error', + 'This validation session has not yet been completed' + ) + }) + it('should return a 400 error if the medium is incorrect', async () => { + const mockResolveResponse = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + email: 'dwho@example.com', + 'm.server': 'matrix.example.com:8448' + }) + }) + + const mockBindResponse = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => { + return { + medium: 'wrongmedium', + address: 'localhost@example.com', + mxid: '@testuser:example.com', + not_after: 1234567890, + not_before: 1234567890, + signatures: {}, + ts: 1234567890 + } + } + }) + + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockResolveResponse) + + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockBindResponse) + + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/bind') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + id_access_token: 'myaccesstoken', + id_server: 'matrix.example.com', + sid: 'mysid' + }) + + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty( + 'error', + 'Medium must be one of "email" or "msisdn"' + ) + }) + it('should return a 400 error if the email is incorrect', async () => { + const mockResolveResponse = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + email: 'dwho@example.com', + 'm.server': 'matrix.example.com:8448' + }) + }) + + const mockBindResponse = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => { + return { + medium: 'email', + address: '05934903', + mxid: '@testuser:example.com', + not_after: 1234567890, + not_before: 1234567890, + signatures: {}, + ts: 1234567890 + } + } + }) + + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockResolveResponse) + + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockBindResponse) + + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/bind') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + id_access_token: 'myaccesstoken', + id_server: 'matrix.example.com', + sid: 'mysid' + }) + + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid email') + }) + it('should return a 400 error if the phone number is incorrect', async () => { + const mockResolveResponse = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + email: 'dwho@example.com', + 'm.server': 'matrix.example.com:8448' + }) + }) + + const mockBindResponse = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => { + return { + medium: 'msisdn', + address: 'localhost@example.com', + mxid: '@testuser:example.com', + not_after: 1234567890, + not_before: 1234567890, + signatures: {}, + ts: 1234567890 + } + } + }) + + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockResolveResponse) + + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockBindResponse) + + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/bind') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + id_access_token: 'myaccesstoken', + id_server: 'matrix.example.com', + sid: 'mysid' + }) + + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid phone number') + }) + it('should return 403 if the user is not an admin and the server does not allow it', async () => { + clientServer.conf.capabilities.enable_3pid_changes = false + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/bind') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + id_access_token: 'myaccesstoken', + id_server: 'matrix.example.com', + sid: 'mysid' + }) + + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + expect(response.body).toHaveProperty( + 'error', + 'Cannot bind 3pid to user account as it is not allowed by server' + ) + delete clientServer.conf.capabilities.enable_3pid_changes + }) + }) + }) +}) diff --git a/packages/matrix-client-server/src/account/3pid/add.ts b/packages/matrix-client-server/src/account/3pid/add.ts new file mode 100644 index 00000000..4ddb5621 --- /dev/null +++ b/packages/matrix-client-server/src/account/3pid/add.ts @@ -0,0 +1,204 @@ +import { + epoch, + errMsg, + isClientSecretValid, + isSidValid, + jsonContent, + send, + validateParameters, + type expressAppHandler +} from '@twake/utils' +import { type AuthenticationData } from '../../types' +import type MatrixClientServer from '../..' +import { validateUserWithUIAuthentication } from '../../utils/userInteractiveAuthentication' +import { isAdmin } from '../../utils/utils' +import { verifyAuthenticationData } from '../../typecheckers' + +interface RequestBody { + auth?: AuthenticationData + client_secret: string + sid: string +} + +const schema = { + auth: false, + client_secret: true, + sid: true +} + +const add = (clientServer: MatrixClientServer): expressAppHandler => { + return (req, res) => { + clientServer.authenticate(req, res, (data, token) => { + jsonContent(req, res, clientServer.logger, (obj) => { + const body = obj as unknown as RequestBody + if ( + body.auth != null && + body.auth !== undefined && + !verifyAuthenticationData(body.auth) + ) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid authentication data'), + clientServer.logger + ) + return + } + if (!isClientSecretValid(body.client_secret)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid client_secret'), + clientServer.logger + ) + return + } + if (!isSidValid(body.sid)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid session ID'), + clientServer.logger + ) + return + } + validateUserWithUIAuthentication( + clientServer, + req, + res, + data.sub, + 'add a 3pid to a user account', + obj, + (obj, userId) => { + validateParameters( + res, + schema, + obj, + clientServer.logger, + // eslint-disable-next-line @typescript-eslint/no-misused-promises + async (obj) => { + const body = obj as RequestBody + const byAdmin = await isAdmin(clientServer, userId as string) + const allowed = + clientServer.conf.capabilities.enable_3pid_changes ?? true + if (!byAdmin && !allowed) { + send( + res, + 403, + errMsg( + 'forbidden', + 'Cannot add 3pid as it is not allowed by server' + ), + clientServer.logger + ) + return + } + clientServer.matrixDb + .get( + 'threepid_validation_session', + ['address', 'medium', 'validated_at'], + { + // Get the address from the validation session. This API has to be called after /requestToken, else it will send error 400 + client_secret: body.client_secret, + session_id: body.sid + } + ) + .then((sessionRows) => { + if (sessionRows.length === 0) { + send( + res, + 400, + errMsg('noValidSession'), + clientServer.logger + ) + return + } + if ( + sessionRows[0].validated_at === null || + sessionRows[0].validated_at === undefined + ) { + send( + res, + 400, + errMsg('sessionNotValidated'), + clientServer.logger + ) + return + } + clientServer.matrixDb + .get('user_threepids', ['user_id'], { + address: sessionRows[0].address + }) + .then((rows) => { + if (rows.length > 0) { + send( + res, + 400, + errMsg('threepidInUse'), + clientServer.logger + ) + } else { + clientServer.matrixDb + .insert('user_threepids', { + user_id: userId as string, + address: sessionRows[0].address as string, + medium: sessionRows[0].medium as string, + validated_at: sessionRows[0] + .validated_at as number, + added_at: epoch() + }) + .then(() => { + send(res, 200, {}, clientServer.logger) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error( + 'Error while inserting user_threepids' + ) + // istanbul ignore next + send( + res, + 400, + errMsg('unknown', e.toString()), + clientServer.logger + ) + }) + } + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error( + 'Error while getting user_threepids' + ) + // istanbul ignore next + send( + res, + 500, + errMsg('unknown', e.toString()), + clientServer.logger + ) + }) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error( + 'Error while getting threepid_validation_session' + ) + // istanbul ignore next + send( + res, + 500, + errMsg('unknown', e.toString()), + clientServer.logger + ) + }) + } + ) + } + ) + }) + }) + } +} + +export default add diff --git a/packages/matrix-client-server/src/account/3pid/bind.ts b/packages/matrix-client-server/src/account/3pid/bind.ts new file mode 100644 index 00000000..286a4160 --- /dev/null +++ b/packages/matrix-client-server/src/account/3pid/bind.ts @@ -0,0 +1,161 @@ +import { + errMsg, + type expressAppHandler, + jsonContent, + send, + validateParameters, + isEmailValid, + isPhoneNumberValid +} from '@twake/utils' +import type MatrixClientServer from '../..' +import { type TokenContent } from '../../utils/authenticate' +import fetch from 'node-fetch' +import { MatrixResolve } from 'matrix-resolve' +import { isAdmin } from '../../utils/utils' + +interface RequestBody { + client_secret: string + id_access_token: string + id_server: string + sid: string +} + +interface ResponseBody { + address: string + medium: string + mxid: string + not_after: number + not_before: number + ts: number +} + +const schema = { + client_secret: true, + id_access_token: true, + id_server: true, + sid: true +} + +const bind = (clientServer: MatrixClientServer): expressAppHandler => { + return (req, res) => { + clientServer.authenticate(req, res, (data: TokenContent) => { + jsonContent(req, res, clientServer.logger, (obj) => { + validateParameters( + res, + schema, + obj, + clientServer.logger, + // eslint-disable-next-line @typescript-eslint/no-misused-promises + async (obj) => { + const byAdmin = await isAdmin(clientServer, data.sub) + const allowed = + clientServer.conf.capabilities.enable_3pid_changes ?? true + if (!byAdmin && !allowed) { + send( + res, + 403, + errMsg( + 'forbidden', + 'Cannot bind 3pid to user account as it is not allowed by server' + ), + clientServer.logger + ) + return + } + const requestBody = obj as RequestBody + const matrixResolve = new MatrixResolve({ + cache: 'toad-cache' + }) + matrixResolve + .resolve(requestBody.id_server) + .then(async (baseUrl: string | string[]) => { + // istanbul ignore next + if (typeof baseUrl === 'object') baseUrl = baseUrl[0] + const response = await fetch( + encodeURI(`${baseUrl}_matrix/identity/v2/3pid/bind`), + { + method: 'POST', + headers: { + Authorization: `Bearer ${requestBody.id_access_token}`, + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + sid: requestBody.sid, + client_secret: requestBody.client_secret, + mxid: data.sub + }) + } + ) + const responseBody = (await response.json()) as ResponseBody + if (response.status === 200) { + if (!['email', 'msisdn'].includes(responseBody.medium)) { + send( + res, + 400, + errMsg( + 'invalidParam', + 'Medium must be one of "email" or "msisdn"' + ) + ) + return + } + if ( + responseBody.medium === 'email' && + !isEmailValid(responseBody.address) + ) { + send(res, 400, errMsg('invalidParam', 'Invalid email')) + return + } + if ( + responseBody.medium === 'msisdn' && + !isPhoneNumberValid(responseBody.address) + ) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid phone number') + ) + return + } + // We don't test the format of id_server since it is already tested in the matrix-resolve package + clientServer.matrixDb + .insert('user_threepid_id_server', { + user_id: data.sub, + id_server: requestBody.id_server, + medium: responseBody.medium, + address: responseBody.address + }) + .then(() => { + send(res, 200, {}) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error( + 'Error while inserting data into the Matrix database', + e + ) + // istanbul ignore next + send(res, 500, {}) + }) + } else { + send(res, response.status, responseBody) + } + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.warn( + `Unable to resolve matrix server ${requestBody.id_server}`, + e + ) + // istanbul ignore next + send(res, 400, 'Invalid server') + }) + } + ) + }) + }) + } +} + +export default bind diff --git a/packages/matrix-client-server/src/account/3pid/delete.ts b/packages/matrix-client-server/src/account/3pid/delete.ts new file mode 100644 index 00000000..b57218c8 --- /dev/null +++ b/packages/matrix-client-server/src/account/3pid/delete.ts @@ -0,0 +1,229 @@ +import { + errMsg, + isEmailValid, + isPhoneNumberValid, + jsonContent, + send, + validateParameters, + type expressAppHandler +} from '@twake/utils' +import type MatrixClientServer from '../..' +import fetch from 'node-fetch' +import { MatrixResolve } from 'matrix-resolve' +import { isAdmin } from '../../utils/utils' +import { insertOpenIdToken } from '../../user/openid/requestToken' +import { randomString } from '@twake/crypto' + +interface RequestBody { + address: string + id_server?: string + medium: string +} + +interface RegisterResponseBody { + token: string +} + +export interface DeleteResponse { + success: boolean + status?: number +} + +const schema = { + address: true, + id_server: false, + medium: true +} + +export const delete3pid = async ( + address: string, + medium: string, + clientServer: MatrixClientServer, + userId: string, + potentialIdServer?: string + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type +): Promise => { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + let idServer: string + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (potentialIdServer) { + idServer = potentialIdServer + } else { + const rows = await clientServer.matrixDb.get( + 'user_threepid_id_server', + ['id_server'], + { + user_id: userId, + medium, + address + } + ) + if (rows.length === 0) { + return { success: false, status: 400 } + } else { + idServer = rows[0].id_server as string + } + } + + const openIDRows = await insertOpenIdToken( + clientServer, + userId, + randomString(64) + ) + const matrixResolve = new MatrixResolve({ + cache: 'toad-cache' + }) + const baseUrl: string | string[] = await matrixResolve.resolve(idServer) + const registerResponse = await fetch( + `https://${baseUrl as string}/_matrix/identity/v2/account/register`, + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + access_token: openIDRows[0].token, + expires_in: clientServer.conf.open_id_token_lifetime, + matrix_server_name: clientServer.conf.server_name, + token_type: 'Bearer' + }) + } + ) + const validToken = ((await registerResponse.json()) as RegisterResponseBody) + .token + const UnbindResponse = await fetch( + `https://${baseUrl as string}/_matrix/identity/v2/3pid/unbind`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${validToken}`, + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + address, + medium + }) + } + ) + if (UnbindResponse.ok) { + const deleteAdd = clientServer.matrixDb.deleteWhere('user_threepids', [ + { field: 'address', value: address, operator: '=' }, + { field: 'medium', value: medium, operator: '=' }, + { field: 'user_id', value: userId, operator: '=' } + ]) + const deleteBind = clientServer.matrixDb.deleteWhere( + 'user_threepid_id_server', + [ + { field: 'address', value: address, operator: '=' }, + { field: 'medium', value: medium, operator: '=' }, + { field: 'user_id', value: userId, operator: '=' }, + { field: 'id_server', value: idServer, operator: '=' } + ] + ) + await Promise.all([deleteAdd, deleteBind]) + return { success: true } + } else { + // istanbul ignore next + return { success: false, status: UnbindResponse.status } + } +} + +const delete3pidHandler = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + clientServer.authenticate(req, res, (data, token) => { + jsonContent(req, res, clientServer.logger, (obj) => { + validateParameters( + res, + schema, + obj, + clientServer.logger, + // eslint-disable-next-line @typescript-eslint/no-misused-promises + async (obj) => { + const body = obj as RequestBody + const byAdmin = await isAdmin(clientServer, data.sub) + const allowed = + clientServer.conf.capabilities.enable_3pid_changes ?? true + if (!byAdmin && !allowed) { + send( + res, + 403, + errMsg( + 'forbidden', + 'Cannot add 3pid as it is not allowed by server' + ), + clientServer.logger + ) + return + } + if (!['email', 'msisdn'].includes(body.medium)) { + send( + res, + 400, + errMsg( + 'invalidParam', + 'Invalid medium, medium must be either email or msisdn' + ), + clientServer.logger + ) + return + } + if (body.medium === 'email' && !isEmailValid(body.address)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid email address'), + clientServer.logger + ) + return + } + if (body.medium === 'msisdn' && !isPhoneNumberValid(body.address)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid phone number'), + clientServer.logger + ) + return + } + delete3pid( + body.address, + body.medium, + clientServer, + data.sub, + body.id_server + ) + .then((response) => { + if (response.success) { + send(res, 200, { id_server_unbind_result: 'success' }) + } else { + send(res, response.status as number, { + id_server_unbind_result: 'no-support' + }) + } + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error( + 'Error while deleting user_threepids', + e + ) + // istanbul ignore next + send( + res, + 500, + errMsg('unknown', e.toString()), + clientServer.logger + ) + }) + } + ) + }) + }) + } +} + +export default delete3pidHandler diff --git a/packages/matrix-client-server/src/account/account.test.ts b/packages/matrix-client-server/src/account/account.test.ts new file mode 100644 index 00000000..fb988d5c --- /dev/null +++ b/packages/matrix-client-server/src/account/account.test.ts @@ -0,0 +1,635 @@ +import fs from 'fs' +import request from 'supertest' +import express from 'express' +import ClientServer from '../index' +import { buildMatrixDb, buildUserDB } from '../__testData__/buildUserDB' +import { type Config } from '../types' +import fetch from 'node-fetch' +import defaultConfig from '../__testData__/registerConf.json' +import { getLogger, type TwakeLogger } from '@twake/logger' +import { + setupTokens, + validRefreshToken3, + validToken, + validToken3 +} from '../__testData__/setupTokens' +import { Hash, randomString } from '@twake/crypto' +import { epoch } from '@twake/utils' + +jest.mock('node-fetch', () => jest.fn()) + +let conf: Config +let clientServer: ClientServer +let app: express.Application + +const logger: TwakeLogger = getLogger() + +beforeAll((done) => { + // @ts-expect-error TS doesn't understand that the config is valid + conf = { + ...defaultConfig, + cron_service: false, + database_engine: 'sqlite', + base_url: 'http://example.com/', + userdb_engine: 'sqlite', + matrix_database_engine: 'sqlite', + matrix_database_host: './src/__testData__/testMatrixAccount.db', + database_host: './src/__testData__/testAccount.db', + userdb_host: './src/__testData__/testAccount.db' + } + if (process.env.TEST_PG === 'yes') { + conf.database_engine = 'pg' + conf.userdb_engine = 'pg' + conf.database_host = process.env.PG_HOST ?? 'localhost' + conf.database_user = process.env.PG_USER ?? 'twake' + conf.database_password = process.env.PG_PASSWORD ?? 'twake' + conf.database_name = process.env.PG_DATABASE ?? 'test' + } + buildUserDB(conf) + .then(() => { + buildMatrixDb(conf) + .then(() => { + done() + }) + .catch((e) => { + logger.error('Error while building matrix db:', e) + done(e) + }) + }) + .catch((e) => { + logger.error('Error while building user db:', e) + done(e) + }) +}) + +afterAll(() => { + fs.unlinkSync('src/__testData__/testAccount.db') + fs.unlinkSync('src/__testData__/testMatrixAccount.db') +}) + +describe('Use configuration file', () => { + beforeAll((done) => { + clientServer = new ClientServer(conf) + app = express() + clientServer.ready + .then(() => { + Object.keys(clientServer.api.get).forEach((k) => { + app.get(k, clientServer.api.get[k]) + }) + Object.keys(clientServer.api.post).forEach((k) => { + app.post(k, clientServer.api.post[k]) + }) + Object.keys(clientServer.api.put).forEach((k) => { + app.put(k, clientServer.api.put[k]) + }) + Object.keys(clientServer.api.delete).forEach((k) => { + app.delete(k, clientServer.api.delete[k]) + }) + done() + }) + .catch((e) => { + done(e) + }) + }) + + afterAll(() => { + clientServer.cleanJobs() + }) + + beforeEach(() => { + jest.clearAllMocks() + jest.mock('node-fetch', () => jest.fn()) + }) + + describe('Endpoints with authentication', () => { + beforeAll(async () => { + await setupTokens(clientServer, logger) + }) + describe('/_matrix/client/v3/account/whoami', () => { + it('should refuse a request with a used access token', async () => { + const response = await request(app) + .post('/_matrix/client/v3/refresh') + .send({ refresh_token: validRefreshToken3 }) + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('access_token') + expect(response.body).toHaveProperty('refresh_token') + const response1 = await request(app) + .get('/_matrix/client/v3/account/whoami') + .set('Authorization', `Bearer ${validToken3}`) + .set('Accept', 'application/json') + expect(response1.statusCode).toBe(401) + }) + }) + describe('/_matrix/client/v3/account/whoami', () => { + let asToken: string + it('should reject missing token (', async () => { + const response = await request(app) + .get('/_matrix/client/v3/account/whoami') + .set('Accept', 'application/json') + expect(response.statusCode).toBe(401) + }) + it('should reject token that mismatch regex', async () => { + const response = await request(app) + .get('/_matrix/client/v3/account/whoami') + .set('Authorization', 'Bearer zzzzzzz') + .set('Accept', 'application/json') + expect(response.statusCode).toBe(401) + }) + it('should reject expired or invalid token', async () => { + const response = await request(app) + .get('/_matrix/client/v3/account/whoami') + .set('Authorization', `Bearer ${randomString(64)}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(401) + }) + it('should accept valid token', async () => { + const response = await request(app) + .get('/_matrix/client/v3/account/whoami') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + }) + it('should accept a valid appservice authentication', async () => { + asToken = conf.application_services[0].as_token + const registerResponse = await request(app) + .post('/_matrix/client/v3/register') + .query({ kind: 'user' }) + .send({ + auth: { + type: 'm.login.application_service', + username: '_irc_bridge_' + }, + username: '_irc_bridge_' + }) + .set('Authorization', `Bearer ${asToken}`) + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '127.10.00') + .set('Accept', 'application/json') + expect(registerResponse.statusCode).toBe(200) + const response = await request(app) + .get('/_matrix/client/v3/account/whoami') + .query({ user_id: '@_irc_bridge_:example.com' }) + .set('Authorization', `Bearer ${asToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body.user_id).toBe('@_irc_bridge_:example.com') + }) + it('should refuse an appservice authentication with a user_id not registered in the appservice', async () => { + const response = await request(app) + .get('/_matrix/client/v3/account/whoami') + .query({ user_id: '@testuser:example.com' }) + .set('Authorization', `Bearer ${asToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(403) + }) + it('should ensure a normal user cannot access the account of an appservice', async () => { + const response = await request(app) + .get('/_matrix/client/v3/account/whoami') + .query({ user_id: '@_irc_bridge_:example.com' }) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.body).toHaveProperty('user_id', '@testuser:example.com') // not _irc_bridge_ (appservice account) + }) + }) + describe('/_matrix/client/v3/account/deactivate', () => { + let session: string + it('should refuse to deactivate an account if the server does not allow it', async () => { + clientServer.conf.capabilities.enable_3pid_changes = false + const response1 = await request(app) + .post('/_matrix/client/v3/account/deactivate') + .set('Accept', 'application/json') + .send({}) + expect(response1.statusCode).toBe(401) + expect(response1.body).toHaveProperty('session') + session = response1.body.session + const response = await request(app) + .post('/_matrix/client/v3/account/deactivate') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + auth: { + type: 'm.login.password', + session, + password: + '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK', + identifier: { type: 'm.id.user', user: '@testuser:example.com' } + } + }) + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + expect(response.body).toHaveProperty( + 'error', + 'Cannot add 3pid as it is not allowed by server' + ) + clientServer.conf.capabilities.enable_3pid_changes = true + }) + it('should refuse to erase all user data if the server does not allow it', async () => { + clientServer.conf.capabilities.enable_set_avatar_url = false + const response1 = await request(app) + .post('/_matrix/client/v3/account/deactivate') + .set('Accept', 'application/json') + .send({}) + expect(response1.statusCode).toBe(401) + expect(response1.body).toHaveProperty('session') + session = response1.body.session + const response = await request(app) + .post('/_matrix/client/v3/account/deactivate') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + auth: { + type: 'm.login.password', + session, + password: + '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK', + identifier: { type: 'm.id.user', user: '@testuser:example.com' } + }, + erase: true + }) + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + expect(response.body).toHaveProperty( + 'error', + 'Cannot erase account as it is not allowed by server' + ) + clientServer.conf.capabilities.enable_set_avatar_url = true + }) + it('should refuse an invalid auth', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/deactivate') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + auth: { + type: 'm.login.password', + session: 'session', + password: 'wrongpassword', + identifier: { type: 'wrongtype', user: '@testuser:example.com' } + } + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid auth') + }) + it('should refuse an invalid id_server', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/deactivate') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + id_server: 42 + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid id_server') + }) + it('should refuse an invalid erase', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/deactivate') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + erase: 'true' + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid erase') + }) + it('should deactivate a user account who authenticated with a token', async () => { + const response1 = await request(app) + .post('/_matrix/client/v3/account/deactivate') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({}) + expect(response1.statusCode).toBe(401) + expect(response1.body).toHaveProperty('session') + session = response1.body.session + + const response = await request(app) + .post('/_matrix/client/v3/account/deactivate') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + auth: { + type: 'm.login.password', + session, + password: + '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK', + identifier: { type: 'm.id.user', user: '@testuser:example.com' } + } + }) + expect(response.statusCode).toBe(200) + }) + it('should deactivate a user account who authenticated without an access token', async () => { + const accessToken = randomString(64) + const hash = new Hash() + await hash.ready + + // Setup the database with the user to deactivate's information + await clientServer.matrixDb.insert('users', { + name: '@usertodeactivate:example.com', + password_hash: hash.sha256('password') + }) + + await clientServer.matrixDb.insert('user_ips', { + user_id: '@usertodeactivate:example.com', + device_id: 'devicetoremove', + access_token: accessToken, + ip: '137.0.0.1', + user_agent: 'curl/7.31.0-DEV', + last_seen: 1411996332123 + }) + await clientServer.matrixDb.insert('access_tokens', { + id: randomString(64), + user_id: '@usertodeactivate:example.com', + device_id: 'devicetodeactivate', + token: accessToken + }) + await clientServer.matrixDb.insert('user_threepids', { + user_id: '@usertodeactivate:example.com', + medium: 'email', + address: 'usertodeactivate@example.com', + validated_at: epoch(), + added_at: epoch() + }) + await clientServer.matrixDb.insert('user_threepids', { + user_id: '@usertodeactivate:example.com', + medium: 'msisdn', + address: '0678912765', + validated_at: epoch(), + added_at: epoch() + }) + await clientServer.matrixDb.insert('devices', { + user_id: '@usertodeactivate:example.com', + device_id: 'devicetodeactivate' + }) + await clientServer.matrixDb.insert('pushers', { + id: randomString(64), + user_name: '@usertodeactivate:example.com', + access_token: accessToken, + profile_tag: 'profile_tag', + kind: 'user', + app_id: 'app_id', + app_display_name: 'app_display_name', + device_display_name: 'device_display_name', + pushkey: 'pushkey', + ts: epoch() + }) + await clientServer.matrixDb.insert('current_state_events', { + event_id: '$eventid:example.com', + room_id: '!roomid:example.com', + type: 'm.room.member', + state_key: '@usertodeactivate:example.com', + membership: 'join' + }) + await clientServer.matrixDb.insert('events', { + stream_ordering: 0, + topological_ordering: 0, + event_id: '$eventid:example.com', + room_id: '!roomid:example.com', + type: 'm.room.member', + state_key: '@usertodeactivate:example.com', + content: '{"random_key": "random_value"}', + processed: 0, + outlier: 0, + depth: 1, + sender: '@sender:example.com', + origin_server_ts: 1411996332123 + }) + await clientServer.matrixDb.insert('room_memberships', { + user_id: '@usertodeactivate:example.com', + room_id: '!roomid:example.com', + membership: 'join', + event_id: '$eventid:example.com', + sender: '@sender:example.com' + }) + + // Mock the response of the identity server from the delete3pid function for the first 3pid + + const mockResolveResponse = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + email: 'dwho@example.com', + 'm.server': 'matrix.example.com:8448' + }) + }) + + const mockRegisterResponse = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + token: 'validToken' + }) + }) + + const mockUnbindResponse = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + address: 'usertodeactivate@example.com', + medium: 'email' + }) + }) + + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockResolveResponse) + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockRegisterResponse) + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockUnbindResponse) + + // Mock the response of the identity server from the delete3pid function for the second 3pid + + const mockResolveResponse2 = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + email: 'dwho@example.com', + 'm.server': 'matrix.example.com:8448' + }) + }) + + const mockRegisterResponse2 = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + token: 'validToken' + }) + }) + + const mockUnbindResponse2 = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + address: '0678912765', + medium: 'msisdn' + }) + }) + + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockResolveResponse2) + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockRegisterResponse2) + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockUnbindResponse2) + + const response1 = await request(app) + .post('/_matrix/client/v3/account/deactivate') + .set('Accept', 'application/json') + .send({}) + expect(response1.statusCode).toBe(401) + expect(response1.body).toHaveProperty('session') + session = response1.body.session + + const response = await request(app) + .post('/_matrix/client/v3/account/deactivate') + .set('Accept', 'application/json') + .send({ + auth: { + type: 'm.login.password', + session, + password: 'password', + identifier: { + type: 'm.id.user', + user: '@usertodeactivate:example.com' + } + }, + erase: true, + id_server: 'matrix.example.com' + }) + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty( + 'id_server_unbind_result', + 'success' + ) + }) + it('should refuse to deactivate an account that was already deactivated', async () => { + const response1 = await request(app) + .post('/_matrix/client/v3/account/deactivate') + .set('Accept', 'application/json') + .send({}) + expect(response1.statusCode).toBe(401) + expect(response1.body).toHaveProperty('session') + session = response1.body.session + + const response = await request(app) + .post('/_matrix/client/v3/account/deactivate') + .set('Accept', 'application/json') + .send({ + auth: { + type: 'm.login.password', + session, + password: 'password', + identifier: { + type: 'm.id.user', + user: '@usertodeactivate:example.com' + } + }, + erase: true, + id_server: 'matrix.example.com' + }) + expect(response.statusCode).toBe(401) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + expect(response.body).toHaveProperty( + 'error', + 'The user does not have a password registered or the provided password is wrong.' + ) // Error from UI Authentication since the password was deleted upon deactivation of the account + }) + it('should send a no-support response if the identity server did not unbind the 3pid association', async () => { + const mockResolveResponse = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + email: 'dwho@example.com', + 'm.server': 'matrix.example.com:8448' + }) + }) + + const mockRegisterResponse = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + token: 'validToken' + }) + }) + + const mockUnbindResponse = Promise.resolve({ + ok: false, + status: 403, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + error: 'invalid session ID or client_secret', + errcode: 'M_INVALID_PARAM' + }) + }) + + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockResolveResponse) + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockRegisterResponse) + // @ts-expect-error mock is unknown + fetch.mockImplementationOnce(async () => await mockUnbindResponse) + + const response1 = await request(app) + .post('/_matrix/client/v3/account/deactivate') + .set('Accept', 'application/json') + .send({}) + expect(response1.statusCode).toBe(401) + expect(response1.body).toHaveProperty('session') + session = response1.body.session + const hash = new Hash() + await hash.ready + await clientServer.matrixDb.insert('users', { + name: '@newusertodeactivate:example.com', + password_hash: hash.sha256('password') + }) + await clientServer.matrixDb.insert('user_threepids', { + user_id: '@newusertodeactivate:example.com', + medium: 'email', + address: 'newusertodeactivate@example.com', + validated_at: epoch(), + added_at: epoch() + }) + const response = await request(app) + .post('/_matrix/client/v3/account/deactivate') + .set('Accept', 'application/json') + .send({ + auth: { + type: 'm.login.password', + session, + password: 'password', + identifier: { + type: 'm.id.user', + user: '@newusertodeactivate:example.com' + } + }, + erase: true, + id_server: 'matrix.example.com' + }) + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty( + 'id_server_unbind_result', + 'no-support' + ) + }) + }) + }) +}) diff --git a/packages/matrix-client-server/src/account/deactivate.ts b/packages/matrix-client-server/src/account/deactivate.ts new file mode 100644 index 00000000..ab409d09 --- /dev/null +++ b/packages/matrix-client-server/src/account/deactivate.ts @@ -0,0 +1,596 @@ +/* eslint-disable @typescript-eslint/promise-function-async */ +import { + errMsg, + type expressAppHandler, + getAccessToken, + jsonContent, + send +} from '@twake/utils' +import type MatrixClientServer from '..' +import { type TokenContent } from '../utils/authenticate' +import { + getParams, + validateUserWithUIAuthentication +} from '../utils/userInteractiveAuthentication' +import { + type AuthenticationFlowContent, + type AuthenticationData, + Membership, + RoomEventTypes, + type ClientEvent +} from '../types' +import type { ServerResponse } from 'http' +import type e from 'express' +import { isAdmin } from '../utils/utils' +import { SafeClientEvent } from '../utils/event' +import { delete3pid, type DeleteResponse } from './3pid/delete' +import { randomString } from '@twake/crypto' +import pLimit from 'p-limit' +import { + verifyAuthenticationData, + verifyBoolean, + verifyString +} from '../typecheckers' + +const maxPromisesToExecuteConcurrently = 10 +const limit = pLimit(maxPromisesToExecuteConcurrently) + +interface RequestBody { + auth?: AuthenticationData + erase?: boolean + id_server?: string +} + +const allowedFlows: AuthenticationFlowContent = { + // Those can be changed. Synapse's implementation only includes m.login.email.identity but + // I think it's relevant to also include m.login.msisdn and m.login.password + flows: [ + { + stages: ['m.login.email.identity'] + }, + { + stages: ['m.login.msisdn'] + }, + { + stages: ['m.login.password'] + } + ], + params: { + 'm.login.email.identity': getParams('m.login.email.identity'), + 'm.login.msisdn': getParams('m.login.msisdn'), + 'm.login.password': getParams('m.login.password') + } +} + +// We return an array of promises wrapped in a limiter so that at most maxPromisesToExecuteConcurrently promises are executed +// at the same time in the promise.all in realMethod +const deleteUserDirectory = ( + clientServer: MatrixClientServer, + userId: string +): Array> => { + const deleteFromDirectory = clientServer.matrixDb.deleteEqual( + 'user_directory', + 'user_id', + userId + ) + const deleteDirectorySearch = clientServer.matrixDb.deleteEqual( + 'user_directory_search', + 'user_id', + userId + ) + const deletePublicRooms = clientServer.matrixDb.deleteEqual( + 'users_in_public_rooms', + 'user_id', + userId + ) + const deletePrivateRooms = clientServer.matrixDb.deleteEqual( + 'users_who_share_private_rooms', + 'user_id', + userId + ) + const deletePrivateRooms2 = clientServer.matrixDb.deleteEqual( + 'users_who_share_private_rooms', + 'other_user_id', + userId + ) + return [ + limit(() => deleteFromDirectory), + limit(() => deleteDirectorySearch), + limit(() => deletePublicRooms), + limit(() => deletePrivateRooms), + limit(() => deletePrivateRooms2) + ] +} + +// We return an array of promises wrapped in a limiter so that at most maxPromisesToExecuteConcurrently promises are executed +// at the same time in the promise.all in realMethod +const deleteAllPushers = async ( + clientServer: MatrixClientServer, + userId: string +): Promise>> => { + const pushers = await clientServer.matrixDb.get( + 'pushers', + ['app_id', 'pushkey'], + { + user_name: userId + } + ) + + await clientServer.matrixDb.deleteEqual('pushers', 'user_name', userId) + + const insertDeletedPushersPromises = pushers.map(async (pusher) => { + await limit(() => + clientServer.matrixDb.insert('deleted_pushers', { + stream_id: randomString(64), // TODO: Update when stream ordering is implemented since the stream_id has to keep track of the order of operations + app_id: pusher.app_id as string, + pushkey: pusher.pushkey as string, + user_id: userId + }) + ) + }) + + return insertDeletedPushersPromises +} + +// We return an array of promises wrapped in a limiter so that at most maxPromisesToExecuteConcurrently promises are executed +// at the same time in the promise.all in realMethod +const deleteAllRooms = async ( + clientServer: MatrixClientServer, + userId: string, + shouldErase: boolean = false +): Promise>> => { + const rooms = await clientServer.matrixDb.get( + 'current_state_events', + ['room_id'], + { + state_key: userId, + type: RoomEventTypes.Member, + membership: Membership.JOIN + } + ) + const deleteRoomsPromises: Array> = [] + for (const room of rooms) { + if (shouldErase) { + // Delete the expiry timestamp associated with this event from the database. + const membershipEventIds = await clientServer.matrixDb.get( + 'room_memberships', + ['event_id'], + { room_id: room.room_id, user_id: userId } + ) + + membershipEventIds.forEach((membershipEventId) => { + deleteRoomsPromises.push( + limit(async () => { + const eventRows = await clientServer.matrixDb.get('events', ['*'], { + event_id: membershipEventId.event_id + }) + if (eventRows.length === 0) { + // istanbul ignore next + throw new Error('Event not found') + } + await clientServer.matrixDb.deleteEqual( + 'event_expiry', + 'event_id', + membershipEventId.event_id as string + ) + // Redact the Event + // I added default values for the fields that don't have the NOT NULL constraint in the db but are required in the event object + const event: ClientEvent = { + content: + eventRows[0].content !== null && + eventRows[0].content !== undefined + ? JSON.parse(eventRows[0].content as string) + : {}, + event_id: eventRows[0].event_id as string, + origin_server_ts: + eventRows[0].origin_server_ts !== null && + eventRows[0].origin_server_ts !== undefined + ? (eventRows[0].origin_server_ts as number) + : 0, // TODO : Discuss default value + room_id: eventRows[0].room_id as string, + sender: + eventRows[0] !== null && eventRows[0] !== undefined + ? (eventRows[0].sender as string) + : '', // TODO : Discuss default value + state_key: eventRows[0].state_key as string, + type: eventRows[0].type as string, + unsigned: + eventRows[0].unsigned !== null && + eventRows[0].unsigned !== undefined + ? JSON.parse(eventRows[0].unsigned as string) + : undefined + } + const safeEvent = new SafeClientEvent(event) + safeEvent.redact() + const redactedEvent = safeEvent.getEvent() + // Update the event_json table with the redacted event + await clientServer.matrixDb.updateWithConditions( + 'event_json', + { json: JSON.stringify(redactedEvent.content) }, + [ + { + field: 'event_id', + value: membershipEventId.event_id as string + } + ] + ) + }) + ) + }) + } + // await updateMembership(room, userId, Membership.LEAVE) + // TODO : Replace this after implementing method to update room membership from the spec + // https://spec.matrix.org/v1.11/client-server-api/#mroommember + // or after implementing the endpoint '/_matrix/client/v3/rooms/{roomId}/leave' + } + return deleteRoomsPromises +} + +const rejectPendingInvitesAndKnocks = async ( + clientServer: MatrixClientServer, + userId: string +): Promise => { + // TODO : Implement this after implementing endpoint '/_matrix/client/v3/rooms/{roomId}/leave' from the spec at : https://spec.matrix.org/v1.11/client-server-api/#post_matrixclientv3roomsroomidleave +} + +// We return an array of promises wrapped in a limiter so that at most maxPromisesToExecuteConcurrently promises are executed +// at the same time in the promise.all in realMethod +const purgeAccountData = ( + clientServer: MatrixClientServer, + userId: string +): Array> => { + const deleteAccountData = clientServer.matrixDb.deleteEqual( + 'account_data', + 'user_id', + userId + ) + const deleteRoomAccountData = clientServer.matrixDb.deleteEqual( + 'room_account_data', + 'user_id', + userId + ) + // We have never used all below tables in other endpoints as of yet, so these promises are useless for now, but we can keep them for future use + // As they are present in Synapse's implementation + const deleteIgnoredUsers = clientServer.matrixDb.deleteEqual( + 'ignored_users', + 'ignorer_user_id', + userId + ) // We only delete the users that were ignored by the deactivated user, so that when the user is reactivated he is still ignored by the users who wanted to ignore him. + const deletePushRules = clientServer.matrixDb.deleteEqual( + 'push_rules', + 'user_name', + userId + ) + const deletePushRulesEnable = clientServer.matrixDb.deleteEqual( + 'push_rules_enable', + 'user_name', + userId + ) + const deletePushRulesStream = clientServer.matrixDb.deleteEqual( + 'push_rules_stream', + 'user_id', + userId + ) + return [ + limit(() => deleteAccountData), + limit(() => deleteRoomAccountData), + limit(() => deleteIgnoredUsers), + limit(() => deletePushRules), + limit(() => deletePushRulesEnable), + limit(() => deletePushRulesStream) + ] +} + +const deleteDevices = ( + clientServer: MatrixClientServer, + userId: string +): Array> => { + const deleteDevicesPromise = limit(() => + clientServer.matrixDb.deleteWhere('devices', [ + { field: 'user_id', value: userId, operator: '=' } + ]) + ) + const deleteDevicesAuthProvidersPromise = limit(() => + clientServer.matrixDb.deleteWhere('device_auth_providers', [ + { field: 'user_id', value: userId, operator: '=' } + ]) + ) + const deleteEndToEndDeviceKeys = limit(() => + clientServer.matrixDb.deleteWhere('e2e_device_keys_json', [ + { field: 'user_id', value: userId, operator: '=' } + ]) + ) + const deleteEndToEndOneTimeKeys = limit(() => + clientServer.matrixDb.deleteWhere('e2e_one_time_keys_json', [ + { field: 'user_id', value: userId, operator: '=' } + ]) + ) + const deleteDehydratedDevices = limit(() => + clientServer.matrixDb.deleteWhere('dehydrated_devices', [ + { field: 'user_id', value: userId, operator: '=' } + ]) + ) + const deleteEndToEndFallbackKeys = limit(() => + clientServer.matrixDb.deleteWhere('e2e_fallback_keys_json', [ + { field: 'user_id', value: userId, operator: '=' } + ]) + ) + return [ + deleteDevicesPromise, + deleteDevicesAuthProvidersPromise, + deleteEndToEndDeviceKeys, + deleteEndToEndOneTimeKeys, + deleteDehydratedDevices, + deleteEndToEndFallbackKeys + ] +} +// Main method to deactivate the account. Uses several submethods to delete the user's data from multiple places in the database. +// Some of those submethods are yet to be implemented as they would require implementing other endpoints from the spec +// We don't remove the user from the user_ips table here since Synapse don't do it. Maybe this is intentional but it isn't removed elsewhere +// Even though we fill this table in the /register endpoint +const realMethod = async ( + res: e.Response | ServerResponse, + clientServer: MatrixClientServer, + body: RequestBody, + userId: string +): Promise => { + const byAdmin = await isAdmin(clientServer, userId) + let allowed = clientServer.conf.capabilities.enable_3pid_changes ?? true + if (!byAdmin && !allowed) { + send( + res, + 403, + errMsg('forbidden', 'Cannot add 3pid as it is not allowed by server'), + clientServer.logger + ) + return + } + allowed = clientServer.conf.capabilities.enable_set_avatar_url ?? true + if ((body.erase ?? false) && !byAdmin && !allowed) { + send( + res, + 403, + errMsg( + 'forbidden', + 'Cannot erase account as it is not allowed by server' + ), + clientServer.logger + ) + return + } + const threepidRows = await clientServer.matrixDb.get( + 'user_threepids', + ['medium', 'address'], + { user_id: userId } + ) + + const threepidDeletePromises: Array> = [] + threepidRows.forEach((row) => { + threepidDeletePromises.push( + limit(() => + delete3pid( + row.address as string, + row.medium as string, + clientServer, + userId, + body.id_server + ) + ) + ) + }) + const deleteDevicesPromises = deleteDevices(clientServer, userId) + const deleteAccessTokensPromise = limit(() => + clientServer.matrixDb.deleteWhere('access_tokens', [ + { field: 'user_id', value: userId, operator: '=' } + ]) + ) + const deleteRefreshTokensPromise = limit(() => + clientServer.matrixDb.deleteWhere('refresh_tokens', [ + { field: 'user_id', value: userId, operator: '=' } + ]) + ) + const removePasswordPromise = limit(() => + clientServer.matrixDb.updateWithConditions( + 'users', + { password_hash: null }, + [{ field: 'name', value: userId }] + ) + ) + const deleteUserDirectoryPromises = deleteUserDirectory(clientServer, userId) + const deleteAllPushersPromises = await deleteAllPushers(clientServer, userId) + // TODO : Check that this doesn't pose a problem + // Synapse's implementation first populates the "user_pending_deactivation" table, parts the user from joined rooms then deletes the user from that table + // Maybe this is because they have many workers and they want to prevent concurrent workers accessing the db at the same time + // If that's the case then we can just directly deleteAllRooms at the same time as all other operations in Promise.all + // And don't need to worry about the "user_pending_deactivation" and the order of operations + const deleteAllRoomsPromise = await deleteAllRooms( + clientServer, + userId, + body.erase + ) + const rejectPendingInvitesAndKnocksPromise = rejectPendingInvitesAndKnocks( + clientServer, + userId + ) + const purgeAccountDataPromises = purgeAccountData(clientServer, userId) + const deleteRoomKeysPromise = limit(() => + clientServer.matrixDb.deleteEqual('e2e_room_keys', 'user_id', userId) + ) + const deleteRoomKeysVersionsPromise = limit(() => + clientServer.matrixDb.deleteEqual( + 'e2e_room_keys_versions', + 'user_id', + userId + ) + ) + + const promisesToExecute = [ + ...threepidDeletePromises, // We put the threepid delete promises first so that we can check if all threepids were successfully unbound from the associated id-servers + ...deleteDevicesPromises, + deleteAccessTokensPromise, + deleteRefreshTokensPromise, + removePasswordPromise, + rejectPendingInvitesAndKnocksPromise, + deleteRoomKeysPromise, + deleteRoomKeysVersionsPromise, + ...deleteAllRoomsPromise, + ...deleteAllPushersPromises, + ...deleteUserDirectoryPromises, + ...purgeAccountDataPromises + ] + if (body.erase ?? false) { + promisesToExecute.push( + limit(() => + clientServer.matrixDb.updateWithConditions( + 'profiles', + { avatar_url: '', displayname: '' }, + [{ field: 'user_id', value: userId }] + ) + ) + ) + promisesToExecute.push( + limit(() => + clientServer.matrixDb.insert('erased_users', { user_id: userId }) + ) + ) + } + // This is not present in the spec but is present in Synapse's implementation so I included it for more flexibility + if (clientServer.conf.capabilities.enable_account_validity ?? true) { + promisesToExecute.push( + limit(() => + clientServer.matrixDb.deleteEqual('account_validity', 'user_id', userId) + ) + ) + } + Promise.all(promisesToExecute) + .then(async (rows) => { + // Synapse's implementation calls a callback function not specified in the spec before sending the response. I didn't include it here since it is not in the spec + // eslint-disable-next-line @typescript-eslint/naming-convention + let id_server_unbind_result = 'success' + for (let i = 0; i < threepidDeletePromises.length; i++) { + // Check if all threepids were successfully unbound from the associated id-servers + const success = (rows[i] as DeleteResponse).success + if (!success) { + id_server_unbind_result = 'no-support' + break + } + } + // Mark the user deactivated after all operations are successful + await clientServer.matrixDb.updateWithConditions( + 'users', + { deactivated: 1 }, + [{ field: 'name', value: userId }] + ) + send(res, 200, { id_server_unbind_result }) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error('Error while deleting user 3pids') + // istanbul ignore next + send(res, 500, errMsg('unknown', e.toString()), clientServer.logger) + }) +} + +// There should be a method to reactivate the account to match this one but it isn't implemented yet +const deactivate = (clientServer: MatrixClientServer): expressAppHandler => { + return (req, res) => { + jsonContent(req, res, clientServer.logger, (obj) => { + const body = obj as unknown as RequestBody + if ( + body.auth !== null && + body.auth !== undefined && + !verifyAuthenticationData(body.auth) + ) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid auth'), + clientServer.logger + ) + return + } else if ( + body.id_server !== null && + body.id_server !== undefined && + !verifyString(body.id_server) + ) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid id_server'), + clientServer.logger + ) + return + } else if ( + body.erase !== null && + body.erase !== undefined && + !verifyBoolean(body.erase) + ) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid erase'), + clientServer.logger + ) + return + } + const token = getAccessToken(req) + if (token != null) { + clientServer.authenticate(req, res, (data: TokenContent) => { + validateUserWithUIAuthentication( + clientServer, + req, + res, + data.sub, + 'deactivate your account', + obj, + (obj, userId) => { + realMethod( + res, + clientServer, + obj as RequestBody, + userId as string + ).catch((e) => { + // istanbul ignore next + clientServer.logger.error('Error while deactivating account') + // istanbul ignore next + send( + res, + 500, + errMsg('unknown', e.toString()), + clientServer.logger + ) + }) + } + ) + }) + } else { + clientServer.uiauthenticate( + req, + res, + allowedFlows, + 'deactivate your account', + obj, + (obj, userId) => { + realMethod( + res, + clientServer, + obj as RequestBody, + userId as string + ).catch((e) => { + // istanbul ignore next + clientServer.logger.error('Error while changing password') + // istanbul ignore next + send( + res, + 500, + errMsg('unknown', e.toString()), + clientServer.logger + ) + }) + } + ) + } + }) + } +} +export default deactivate diff --git a/packages/matrix-client-server/src/account/password/email/requestToken.ts b/packages/matrix-client-server/src/account/password/email/requestToken.ts new file mode 100644 index 00000000..9db38057 --- /dev/null +++ b/packages/matrix-client-server/src/account/password/email/requestToken.ts @@ -0,0 +1,197 @@ +import fs from 'fs' +import { + errMsg, + isValidUrl, + jsonContent, + send, + validateParameters, + type expressAppHandler +} from '@twake/utils' +import type MatrixClientServer from '../../../index' +import Mailer from '../../../utils/mailer' +import { + fillTableAndSend, + getSubmitUrl, + preConfigureTemplate +} from '../../../register/email/requestToken' +import { randomString } from '@twake/crypto' + +interface RequestTokenArgs { + client_secret: string + email: string + next_link?: string + send_attempt: number + id_server?: string + id_access_token?: string +} + +const schema = { + client_secret: true, + email: true, + next_link: false, + send_attempt: true, + id_server: false, + id_access_token: false +} + +const clientSecretRe = /^[0-9a-zA-Z.=_-]{6,255}$/ +const validEmailRe = /^\w[+.-\w]*\w@\w[.-\w]*\w\.\w{2,6}$/ +const maxAttemps = 1000000000 + +const RequestToken = (clientServer: MatrixClientServer): expressAppHandler => { + const transport = new Mailer(clientServer.conf) + const verificationTemplate = preConfigureTemplate( + fs + .readFileSync(`${clientServer.conf.template_dir}/mailVerification.tpl`) + .toString(), + clientServer.conf, + transport + ) + return (req, res) => { + jsonContent(req, res, clientServer.logger, (obj) => { + validateParameters(res, schema, obj, clientServer.logger, (obj) => { + const clientSecret = (obj as RequestTokenArgs).client_secret + const sendAttempt = (obj as RequestTokenArgs).send_attempt + const dst = (obj as RequestTokenArgs).email + const nextLink = (obj as RequestTokenArgs).next_link + if (!clientSecretRe.test(clientSecret)) { + send( + res, + 400, + errMsg('invalidParam', 'invalid client_secret'), + clientServer.logger + ) + } else if (!validEmailRe.test(dst)) { + send(res, 400, errMsg('invalidEmail'), clientServer.logger) + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + } else if (nextLink && !isValidUrl(nextLink)) { + send( + res, + 400, + errMsg('invalidParam', 'invalid next_link'), + clientServer.logger + ) + } else if ( + typeof sendAttempt !== 'number' || + sendAttempt > maxAttemps + ) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid send attempt'), + clientServer.logger + ) + } else { + clientServer.matrixDb + .get('user_threepids', ['user_id'], { address: dst }) + .then((rows) => { + if (rows.length === 0) { + send(res, 400, errMsg('threepidNotFound'), clientServer.logger) + } else { + clientServer.matrixDb + .get( + 'threepid_validation_session', + ['last_send_attempt', 'session_id'], + { + client_secret: clientSecret, + address: dst + } + ) + .then((rows) => { + if (rows.length > 0) { + if (sendAttempt === rows[0].last_send_attempt) { + send( + res, + 200, + { + sid: rows[0].session_id, + submit_url: getSubmitUrl(clientServer.conf) + }, + clientServer.logger + ) + } else { + clientServer.matrixDb + .deleteWhere('threepid_validation_session', [ + { + field: 'client_secret', + value: clientSecret, + operator: '=' + }, + { + field: 'session_id', + value: rows[0].session_id as string, + operator: '=' + } + ]) + .then(() => { + fillTableAndSend( + // The calls to send are made in this function + clientServer, + dst, + clientSecret, + sendAttempt, + verificationTemplate, + transport, + res, + rows[0].session_id as string, + nextLink + ) + }) + .catch((err) => { + // istanbul ignore next + clientServer.logger.error('Deletion error') + // istanbul ignore next + send( + res, + 500, + errMsg('unknown', err.toString()), + clientServer.logger + ) + }) + } + } else { + fillTableAndSend( + // The calls to send are made in this function + clientServer, + dst, + clientSecret, + sendAttempt, + verificationTemplate, + transport, + res, + randomString(64), + nextLink + ) + } + }) + .catch((err) => { + /* istanbul ignore next */ + clientServer.logger.error('Send_attempt error') + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', err.toString()), + clientServer.logger + ) + }) + } + }) + .catch((err) => { + /* istanbul ignore next */ + clientServer.logger.error('Error getting userID') + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', err.toString()), + clientServer.logger + ) + }) + } + }) + }) + } +} + +export default RequestToken diff --git a/packages/matrix-client-server/src/account/password/index.ts b/packages/matrix-client-server/src/account/password/index.ts new file mode 100644 index 00000000..f1962e24 --- /dev/null +++ b/packages/matrix-client-server/src/account/password/index.ts @@ -0,0 +1,263 @@ +import { + errMsg, + type expressAppHandler, + getAccessToken, + jsonContent, + send, + validateParameters +} from '@twake/utils' +import type MatrixClientServer from '../..' +import { + getParams, + validateUserWithUIAuthentication +} from '../../utils/userInteractiveAuthentication' +import { + type AuthenticationData, + type AuthenticationFlowContent +} from '../../types' +import type { ServerResponse } from 'http' +import type e from 'express' +import { Hash } from '@twake/crypto' +import { type TokenContent } from '../../utils/authenticate' +import { isAdmin } from '../../utils/utils' +import { + verifyAuthenticationData, + verifyBoolean, + verifyString +} from '../../typecheckers' + +interface RequestBody { + auth?: AuthenticationData + logout_devices?: boolean + new_password: string +} + +const schema = { + auth: false, + logout_devices: false, + new_password: true +} + +const allowedFlows: AuthenticationFlowContent = { + // TODO : Make sure those are the flows we want + // Those can be changed. Synapse's implementation only includes m.login.email.identity but + // I think it's relevant to also include m.login.msisdn and m.login.password + flows: [ + { + stages: ['m.login.email.identity'] + }, + { + stages: ['m.login.msisdn'] + }, + { + stages: ['m.login.password'] + } + ], + params: { + 'm.login.email.identity': getParams('m.login.email.identity'), + 'm.login.msisdn': getParams('m.login.msisdn'), + 'm.login.password': getParams('m.login.password') + } +} + +const revokeTokenAndDevicesAndSend = ( + res: e.Response | ServerResponse, + clientServer: MatrixClientServer, + userId: string, + accessToken?: string, + deviceId?: string +): void => { + const deleteDevicesPromise = clientServer.matrixDb.deleteWhere('devices', [ + { field: 'user_id', value: userId, operator: '=' }, + { field: 'device_id', value: deviceId as string, operator: '!=' } + ]) + const deleteTokenPromise = clientServer.matrixDb.deleteWhere( + 'access_tokens', + [ + { field: 'user_id', value: userId, operator: '=' }, + { field: 'token', value: accessToken as string, operator: '!=' } + ] + ) + Promise.all([deleteDevicesPromise, deleteTokenPromise]) + .then(() => { + send(res, 200, {}) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error('Error while deleting devices and token') + // istanbul ignore next + send(res, 500, errMsg('unknown', e.toString()), clientServer.logger) + }) +} + +const realMethod = async ( + res: e.Response | ServerResponse, + clientServer: MatrixClientServer, + body: RequestBody, + userId: string, + deviceId?: string, + accessToken?: string +): Promise => { + const byAdmin = await isAdmin(clientServer, userId) + const allowed = clientServer.conf.capabilities.enable_change_password ?? true + if (!byAdmin && !allowed) { + // To comply with spec : https://spec.matrix.org/v1.11/client-server-api/#mchange_password-capability + send( + res, + 403, + errMsg( + 'forbidden', + 'Cannot change password as it is not allowed by server' + ), + clientServer.logger + ) + return + } + const hash = new Hash() + hash.ready + .then(() => { + const hashedPassword = hash.sha256(body.new_password) // TODO : Handle other algorithms + clientServer.matrixDb + .updateWithConditions('users', { password_hash: hashedPassword }, [ + { field: 'name', value: userId } + ]) + .then(() => { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (body.logout_devices) { + revokeTokenAndDevicesAndSend( + res, + clientServer, + userId, + accessToken, + deviceId + ) + } else { + send(res, 200, {}) + } + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error('Error while updating password') + // istanbul ignore next + send(res, 500, errMsg('unknown', e.toString()), clientServer.logger) + }) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error('Error while hashing password') + // istanbul ignore next + send(res, 500, errMsg('unknown', e.toString()), clientServer.logger) + }) +} +const passwordReset = (clientServer: MatrixClientServer): expressAppHandler => { + return (req, res) => { + jsonContent(req, res, clientServer.logger, (obj) => { + const body = obj as unknown as RequestBody + if ( + body.auth != null && + body.auth !== undefined && + !verifyAuthenticationData(body.auth) + ) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid auth'), + clientServer.logger + ) + return + } else if ( + body.logout_devices != null && + body.logout_devices !== undefined && + !verifyBoolean(body.logout_devices) + ) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid logout_devices'), + clientServer.logger + ) + return + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + } else if (body.new_password && !verifyString(body.new_password)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid new_password'), + clientServer.logger + ) + return + } + const token = getAccessToken(req) + if (token != null) { + clientServer.authenticate(req, res, (data: TokenContent) => { + validateUserWithUIAuthentication( + clientServer, + req, + res, + data.sub, + 'modify your account password', + obj, + (obj, userId) => { + validateParameters( + res, + schema, + obj, + clientServer.logger, + (obj) => { + realMethod( + res, + clientServer, + obj as RequestBody, + userId as string, + data.device_id, + token + ).catch((e) => { + // istanbul ignore next + clientServer.logger.error('Error while changing password') + // istanbul ignore next + send( + res, + 500, + errMsg('unknown', e.toString()), + clientServer.logger + ) + }) + } + ) + } + ) + }) + } else { + clientServer.uiauthenticate( + req, + res, + allowedFlows, + 'modify your account password', + obj, + (obj, userId) => { + validateParameters(res, schema, obj, clientServer.logger, (obj) => { + realMethod( + res, + clientServer, + obj as RequestBody, + userId as string + ).catch((e) => { + // istanbul ignore next + clientServer.logger.error('Error while changing password') + // istanbul ignore next + send( + res, + 500, + errMsg('unknown', e.toString()), + clientServer.logger + ) + }) + }) + } + ) + } + }) + } +} + +export default passwordReset diff --git a/packages/matrix-client-server/src/account/password/msisdn/requestToken.ts b/packages/matrix-client-server/src/account/password/msisdn/requestToken.ts new file mode 100644 index 00000000..3de403aa --- /dev/null +++ b/packages/matrix-client-server/src/account/password/msisdn/requestToken.ts @@ -0,0 +1,214 @@ +import { randomString } from '@twake/crypto' +import fs from 'fs' +import { + errMsg, + isValidUrl, + jsonContent, + send, + validateParameters, + type expressAppHandler, + isClientSecretValid, + isCountryValid, + isPhoneNumberValid +} from '@twake/utils' +import type MatrixClientServer from '../../../index' +import SmsSender from '../../../utils/smsSender' +import { getSubmitUrl } from '../../../register/email/requestToken' +import { + fillTableAndSend, + formatPhoneNumber, + preConfigureTemplate +} from '../../../register/msisdn/requestToken' + +interface RequestTokenArgs { + client_secret: string + country: string + phone_number: string + next_link?: string + send_attempt: number + id_server?: string + id_access_token?: string +} + +const schema = { + client_secret: true, + country: true, + phone_number: true, + next_link: false, + send_attempt: true, + id_server: false, + id_access_token: false +} +const maxAttemps = 1000000000 + +const RequestToken = (clientServer: MatrixClientServer): expressAppHandler => { + const transport = new SmsSender(clientServer.conf) + const verificationTemplate = preConfigureTemplate( + fs + .readFileSync(`${clientServer.conf.template_dir}/smsVerification.tpl`) + .toString(), + clientServer.conf, + transport + ) + return (req, res) => { + jsonContent(req, res, clientServer.logger, (obj) => { + validateParameters(res, schema, obj, clientServer.logger, (obj) => { + const clientSecret = (obj as RequestTokenArgs).client_secret + const sendAttempt = (obj as RequestTokenArgs).send_attempt + const country = (obj as RequestTokenArgs).country + const phoneNumber = (obj as RequestTokenArgs).phone_number + const dst = formatPhoneNumber(phoneNumber, country) + const nextLink = (obj as RequestTokenArgs).next_link + if (!isClientSecretValid(clientSecret)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid client_secret'), + clientServer.logger + ) + } else if (!isCountryValid(country)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid country'), + clientServer.logger + ) + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + } else if (nextLink && !isValidUrl(nextLink)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid next_link'), + clientServer.logger + ) + } else if (!isPhoneNumberValid(dst)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid phone number'), + clientServer.logger + ) + } else if ( + typeof sendAttempt !== 'number' || + sendAttempt > maxAttemps + ) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid send attempt'), + clientServer.logger + ) + } else { + clientServer.matrixDb + .get('user_threepids', ['user_id'], { address: dst }) + .then((rows) => { + if (rows.length === 0) { + send(res, 400, errMsg('threepidNotFound'), clientServer.logger) + } else { + clientServer.matrixDb + .get( + 'threepid_validation_session', + ['last_send_attempt', 'session_id'], + { + client_secret: clientSecret, + address: dst + } + ) + .then((rows) => { + if (rows.length > 0) { + if (sendAttempt === rows[0].last_send_attempt) { + send( + res, + 200, + { + sid: rows[0].session_id, + submit_url: getSubmitUrl(clientServer.conf) + }, + clientServer.logger + ) + } else { + clientServer.matrixDb + .deleteWhere('threepid_validation_session', [ + { + field: 'client_secret', + value: clientSecret, + operator: '=' + }, + { + field: 'session_id', + value: rows[0].session_id as string, + operator: '=' + } + ]) + .then(() => { + fillTableAndSend( + // The calls to send are made in this function + clientServer, + dst, + clientSecret, + sendAttempt, + verificationTemplate, + transport, + res, + rows[0].session_id as string, + nextLink + ) + }) + .catch((err) => { + // istanbul ignore next + clientServer.logger.error('Deletion error:', err) + // istanbul ignore next + send( + res, + 500, + errMsg('unknown', err.toString()), + clientServer.logger + ) + }) + } + } else { + fillTableAndSend( + // The calls to send are made in this function + clientServer, + dst, + clientSecret, + sendAttempt, + verificationTemplate, + transport, + res, + randomString(64), + nextLink + ) + } + }) + .catch((err) => { + /* istanbul ignore next */ + clientServer.logger.error('Send_attempt error:', err) + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', err.toString()), + clientServer.logger + ) + }) + } + }) + .catch((err) => { + /* istanbul ignore next */ + clientServer.logger.error('Error getting userID :', err) + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', err.toString()), + clientServer.logger + ) + }) + } + }) + }) + } +} + +export default RequestToken diff --git a/packages/matrix-client-server/src/account/password/password.test.ts b/packages/matrix-client-server/src/account/password/password.test.ts new file mode 100644 index 00000000..ed0e4e49 --- /dev/null +++ b/packages/matrix-client-server/src/account/password/password.test.ts @@ -0,0 +1,272 @@ +import fs from 'fs' +import express from 'express' +import request from 'supertest' +import ClientServer from '../../index' +import { buildMatrixDb, buildUserDB } from '../../__testData__/buildUserDB' +import { type Config } from '../../types' +import defaultConfig from '../../__testData__/registerConf.json' +import { getLogger, type TwakeLogger } from '@twake/logger' +import { setupTokens, validToken } from '../../__testData__/setupTokens' +import { Hash, randomString } from '@twake/crypto' +import { epoch } from '@twake/utils' + +jest.mock('node-fetch', () => jest.fn()) + +let conf: Config +let clientServer: ClientServer +let app: express.Application + +const logger: TwakeLogger = getLogger() + +beforeAll((done) => { + // @ts-expect-error TS doesn't understand that the config is valid + conf = { + ...defaultConfig, + cron_service: false, + database_engine: 'sqlite', + base_url: 'http://example.com/', + userdb_engine: 'sqlite', + matrix_database_engine: 'sqlite', + matrix_database_host: './src/__testData__/testMatrixPassword.db', + database_host: './src/__testData__/testPassword.db', + userdb_host: './src/__testData__/testPassword.db' + } + if (process.env.TEST_PG === 'yes') { + conf.database_engine = 'pg' + conf.userdb_engine = 'pg' + conf.database_host = process.env.PG_HOST ?? 'localhost' + conf.database_user = process.env.PG_USER ?? 'twake' + conf.database_password = process.env.PG_PASSWORD ?? 'twake' + conf.database_name = process.env.PG_DATABASE ?? 'test' + } + buildUserDB(conf) + .then(() => { + buildMatrixDb(conf) + .then(() => { + done() + }) + .catch((e) => { + logger.error('Error while building matrix db:', e) + done(e) + }) + }) + .catch((e) => { + logger.error('Error while building user db:', e) + done(e) + }) +}) + +afterAll(() => { + fs.unlinkSync('src/__testData__/testPassword.db') + fs.unlinkSync('src/__testData__/testMatrixPassword.db') +}) + +describe('Use configuration file', () => { + beforeAll((done) => { + clientServer = new ClientServer(conf) + app = express() + clientServer.ready + .then(() => { + Object.keys(clientServer.api.get).forEach((k) => { + app.get(k, clientServer.api.get[k]) + }) + Object.keys(clientServer.api.post).forEach((k) => { + app.post(k, clientServer.api.post[k]) + }) + Object.keys(clientServer.api.put).forEach((k) => { + app.put(k, clientServer.api.put[k]) + }) + Object.keys(clientServer.api.delete).forEach((k) => { + app.delete(k, clientServer.api.delete[k]) + }) + done() + }) + .catch((e) => { + done(e) + }) + }) + + afterAll(() => { + clientServer.cleanJobs() + }) + + beforeEach(() => { + jest.clearAllMocks() + jest.mock('node-fetch', () => jest.fn()) + }) + + describe('Endpoints with authentication', () => { + beforeAll(async () => { + await setupTokens(clientServer, logger) + }) + describe('/_matrix/client/v3/account/password', () => { + let session: string + it('should refuse an invalid logout_devices', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/password') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + logout_devices: 'true' + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid logout_devices') + }) + it('should refuse an invalid new_password', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/password') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + new_password: 55 + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid new_password') + }) + it('should refuse an invalid auth', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/password') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + auth: { type: 'wrongtype' } + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid auth') + }) + it('should return 403 if the user is not an admin and the server does not allow it', async () => { + clientServer.conf.capabilities.enable_change_password = false + const response1 = await request(app) + .post('/_matrix/client/v3/account/password') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + sid: 'sid', + client_secret: 'my' + }) + expect(response1.statusCode).toBe(401) + session = response1.body.session + const response = await request(app) + .post('/_matrix/client/v3/account/password') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + new_password: 'newpassword', + auth: { + type: 'm.login.password', + session, + password: + '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK', + identifier: { type: 'm.id.user', user: '@testuser:example.com' } + } + }) + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + expect(response.body).toHaveProperty( + 'error', + 'Cannot change password as it is not allowed by server' + ) + delete clientServer.conf.capabilities.enable_change_password + }) + it('should change password of the user', async () => { + const response1 = await request(app) + .post('/_matrix/client/v3/account/password') + .set('Accept', 'application/json') + .send({ + sid: 'sid', + client_secret: 'my' + }) + expect(response1.statusCode).toBe(401) + session = response1.body.session + const response = await request(app) + .post('/_matrix/client/v3/account/password') + .set('Accept', 'application/json') + .send({ + new_password: 'newpassword', + auth: { + type: 'm.login.password', + session, + password: + '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK', + identifier: { type: 'm.id.user', user: '@testuser:example.com' } + } + }) + expect(response.statusCode).toBe(200) + + const hash = new Hash() + await hash.ready + const hashedPassword = hash.sha256('newpassword') + const updatedPassword = ( + await clientServer.matrixDb.get('users', ['password_hash'], { + name: '@testuser:example.com' + }) + )[0].password_hash + expect(updatedPassword).toBe(hashedPassword) + }) + it('should delete all devices and tokens except the current one if logout_devices is set to true', async () => { + const response1 = await request(app) + .post('/_matrix/client/v3/account/password') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + sid: 'sid', + client_secret: 'my' + }) + expect(response1.statusCode).toBe(401) + session = response1.body.session + + await clientServer.matrixDb.insert('devices', { + user_id: '@testuser:example.com', + device_id: 'testdevice' + }) // device to keep + await clientServer.matrixDb.insert('devices', { + user_id: '@testuser:example.com', + device_id: 'deviceToDelete' + }) + await clientServer.matrixDb.insert('access_tokens', { + id: randomString(64), + user_id: '@testuser:example.com', + device_id: 'testdevice', + token: 'tokenToDelete', + valid_until_ms: epoch() + 64000 + }) + const response = await request(app) + .post('/_matrix/client/v3/account/password') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + new_password: 'newpassword2', + auth: { + type: 'm.login.password', + session, + password: 'newpassword', + identifier: { type: 'm.id.user', user: '@testuser:example.com' } + }, + logout_devices: true + }) + expect(response.statusCode).toBe(200) + const devices = await clientServer.matrixDb.get( + 'devices', + ['device_id'], + { + user_id: '@testuser:example.com' + } + ) + expect(devices).toHaveLength(1) + expect(devices[0].device_id).toBe('testdevice') + const tokens = await clientServer.matrixDb.get( + 'access_tokens', + ['token'], + { + user_id: '@testuser:example.com' + } + ) + expect(tokens).toHaveLength(1) + expect(tokens[0].token).toBe(validToken) + }) + }) + }) +}) diff --git a/packages/matrix-client-server/src/account/whoami.ts b/packages/matrix-client-server/src/account/whoami.ts new file mode 100644 index 00000000..e0e78fdc --- /dev/null +++ b/packages/matrix-client-server/src/account/whoami.ts @@ -0,0 +1,38 @@ +import { errMsg, send, type expressAppHandler } from '@twake/utils' +import type MatrixClientServer from '..' +import { type TokenContent } from '../utils/authenticate' + +interface responseBody { + user_id: string + is_guest: boolean + device_id?: string +} +const whoami = (clientServer: MatrixClientServer): expressAppHandler => { + return (req, res) => { + clientServer.authenticate(req, res, (data: TokenContent) => { + clientServer.matrixDb + .get('users', ['name', 'is_guest'], { name: data.sub }) + .then((rows) => { + // istanbul ignore if // might remove the istanbul ignore if an endpoint other than /register modifies the users table + if (rows.length === 0) { + send(res, 403, errMsg('invalidUsername'), clientServer.logger) + return + } + const isGuest = rows[0].is_guest !== 0 + const body: responseBody = { user_id: data.sub, is_guest: isGuest } + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (data.device_id) { + body.device_id = data.device_id + } + send(res, 200, body, clientServer.logger) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error('Error while fetching user data') + // istanbul ignore next + send(res, 500, errMsg('unknown', e.toString()), clientServer.logger) + }) + }) + } +} +export default whoami diff --git a/packages/matrix-client-server/src/admin/whois.ts b/packages/matrix-client-server/src/admin/whois.ts new file mode 100644 index 00000000..9de0bf5d --- /dev/null +++ b/packages/matrix-client-server/src/admin/whois.ts @@ -0,0 +1,88 @@ +import type MatrixClientServer from '..' +import { type expressAppHandler, send, errMsg } from '@twake/utils' + +interface Parameters { + userId: string +} + +interface connectionsContent { + ip: string + last_seen: number + user_agent: string +} + +interface sessionsContent { + connections: connectionsContent[] +} + +interface deviceContent { + sessions: sessionsContent[] +} + +const whois = (clientServer: MatrixClientServer): expressAppHandler => { + return (req, res) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const parameters: Parameters = req.query as Parameters + if (parameters.userId?.length != null) { + clientServer.authenticate(req, res, (data, id) => { + clientServer.matrixDb + .get( + 'user_ips', + ['device_id', 'ip', 'user_agent', 'last_seen', 'access_token'], + { + user_id: parameters.userId + } + ) + .then((rows) => { + const sessions: Record = {} + const devices: Record = {} + const deviceIds = Array.from( + new Set(rows.map((row) => row.device_id as string)) + ) // Get all unique deviceIds + const mappings: Record = {} // Map deviceIds to access_tokens + rows.forEach((row) => { + mappings[row.device_id as string] = row.access_token as string + }) + rows.forEach((row) => { + if (sessions[row.access_token as string] == null) { + sessions[row.access_token as string] = { connections: [] } + } + sessions[row.access_token as string].connections.push({ + ip: row.ip as string, + last_seen: row.last_seen as number, + user_agent: row.user_agent as string + }) + }) + deviceIds.forEach((deviceId) => { + if (devices[deviceId] == null) { + devices[deviceId] = { sessions: [] } + } + devices[deviceId].sessions.push(sessions[mappings[deviceId]]) + }) + send( + res, + 200, + { + user_id: parameters.userId, + devices + }, + clientServer.logger + ) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error( + 'Error retrieving user informations from the MatrixDB' + ) + // istanbul ignore next + send(res, 500, errMsg('unknown', e.toString()), clientServer.logger) + }) + }) + } else { + send(res, 400, errMsg('missingParams'), clientServer.logger) + } + } +} + +export default whois diff --git a/packages/matrix-client-server/src/capabilities/getCapabilities.ts b/packages/matrix-client-server/src/capabilities/getCapabilities.ts new file mode 100644 index 00000000..c29b8361 --- /dev/null +++ b/packages/matrix-client-server/src/capabilities/getCapabilities.ts @@ -0,0 +1,65 @@ +/** + * Implements the Capabilities negotiation of the Matrix Protocol (Client-Server) + * cf https://spec.matrix.org/latest/client-server-api/#capabilities-negotiation + * + * The capabilities will be stored in the server's configuration file. + * + * To be effectively taken into account, the concerned API's should check the capabilities to ensure it can be used. + * + * For reference, look at how the capabilities are checked in the `changeDisplayname` function. ( ../profiles/changeProfiles.ts ) + * + * TODO : Implement capability checks in the concerned API's for changing password and 3pid changes + * (TODO : Implement capability checks in the concerned API's for user_directory search (not specified in spec)) + */ + +import type MatrixClientServer from '../index' +import { errMsg, send, type expressAppHandler } from '@twake/utils' +import { DEFAULT_ROOM_VERSION, ROOM_VERSIONS } from '../versions' + +const getCapabilities = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + clientServer.authenticate(req, res, (data) => { + let _capabilities: Record + try { + _capabilities = { + 'm.room_versions': { + default: DEFAULT_ROOM_VERSION, + available: { + ...ROOM_VERSIONS + } + }, + 'm.change_password': { + enabled: + clientServer.conf.capabilities.enable_change_password ?? true + }, + 'm.set_displayname': { + enabled: + clientServer.conf.capabilities.enable_set_displayname ?? true + }, + 'm.set_avatar_url': { + enabled: + clientServer.conf.capabilities.enable_set_avatar_url ?? true + }, + 'm.3pid_changes': { + enabled: clientServer.conf.capabilities.enable_3pid_changes ?? true + } + } + } catch (e) { + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', 'Error getting capabilities'), + clientServer.logger + ) + /* istanbul ignore next */ + return + } + send(res, 200, { capabilities: _capabilities }, clientServer.logger) + }) + } +} + +export default getCapabilities diff --git a/packages/matrix-client-server/src/config.json b/packages/matrix-client-server/src/config.json new file mode 100644 index 00000000..43da286f --- /dev/null +++ b/packages/matrix-client-server/src/config.json @@ -0,0 +1,106 @@ +{ + "additional_features": false, + "application_services": [ + { + "id": "test", + "hs_token": "hsTokenTestwdakZQunWWNe3DZitAerw9aNqJ2a6HVp0sJtg7qTJWXcHnBjgN0NL", + "as_token": "as_token_test", + "url": "http://localhost:3000", + "sender_localpart": "sender_localpart_test", + "namespaces": { + "users": [ + { + "exclusive": false, + "regex": "@_irc_bridge_.*" + } + ] + } + } + ], + "base_url": "", + "cache_engine": "", + "capabilities": {}, + "cron_service": true, + "database_engine": "sqlite", + "database_host": "./tokens.db", + "database_name": "", + "database_password": "", + "database_ssl": false, + "database_user": "", + "database_vacuum_delay": 3600, + "federated_identity_services": null, + "hashes_rate_limit": 100, + "invitation_server_name": "matrix.to", + "is_federated_identity_service": false, + "is_registration_enabled": true, + "key_delay": 3600, + "keys_depth": 5, + "ldap_base": "", + "ldap_filter": "(ObjectClass=inetOrgPerson)", + "ldap_password": "", + "ldap_uri": "", + "ldap_user": "", + "ldapjs_opts": {}, + "login_flows": { + "flows": [ + { + "type": "m.login.sso", + "identity_providers": { + "id": "oidc-twake", + "name": "Connect with Twake" + } + }, + { + "type": "m.login.password" + }, + { + "type": "m.login.token" + }, + { + "type": "m.login.application_service" + } + ] + }, + "mail_link_delay": 7200, + "matrix_server": "localhost", + "matrix_database_engine": "sqlite", + "matrix_database_host": "./matrix.db", + "matrix_database_name": null, + "matrix_database_password": null, + "matrix_database_ssl": false, + "matrix_database_user": null, + "pepperCron": "0 0 * * *", + "policies": null, + "rate_limiting_window": 600000, + "rate_limiting_nb_requests": 100, + "redis_uri": "", + "server_name": "localhost", + "sms_folder": "./src/__testData__/sms", + "smtp_password": "", + "smtp_tls": true, + "smtp_user": "", + "smtp_verify_certificate": true, + "smtp_sender": "", + "smtp_server": "localhost", + "smtp_port": 25, + "template_dir": "./templates", + "trust_x_forwarded_for": false, + "update_federated_identity_hashes_cron": "3 3 3 * * *", + "update_users_cron": "*/10 * * * *", + "userdb_engine": "sqlite", + "userdb_host": "./tokens.db", + "userdb_name": "", + "userdb_password": "", + "userdb_ssl": false, + "userdb_user": "", + "user_directory": {}, + "is_email_login_enabled": true, + "is_registration_token_login_enabled": true, + "is_terms_login_enabled": true, + "is_recaptcha_login_enabled": true, + "is_password_login_enabled": true, + "is_sso_login_enabled": true, + "is_msisdn_login_enabled": true, + "registration_required_3pid": [], + "open_id_token_lifetime": 3600000 +} diff --git a/packages/matrix-client-server/src/delete_devices.ts b/packages/matrix-client-server/src/delete_devices.ts new file mode 100644 index 00000000..f38a3227 --- /dev/null +++ b/packages/matrix-client-server/src/delete_devices.ts @@ -0,0 +1,279 @@ +/* eslint-disable @typescript-eslint/promise-function-async */ +import { errMsg, jsonContent, send, type expressAppHandler } from '@twake/utils' +import type MatrixClientServer from '.' +import { validateUserWithUIAuthentication } from './utils/userInteractiveAuthentication' +import { type AuthenticationData } from './types' +import { randomString } from '@twake/crypto' +import pLimit from 'p-limit' +import { verifyArray, verifyAuthenticationData } from './typecheckers' + +const MESSAGES_TO_DELETE_BATCH_SIZE = 10 + +interface RequestBody { + auth?: AuthenticationData + devices: string[] +} +const maxPromisesToExecuteConcurrently = 10 +const limit = pLimit(maxPromisesToExecuteConcurrently) + +const deleteDevices = ( + clientServer: MatrixClientServer, + devices: string[] +): Array> => { + const devicePromises: Array> = [] + for (const deviceId of devices) { + devicePromises.push( + limit(() => + clientServer.matrixDb.deleteWhere('devices', [ + { field: 'device_id', value: deviceId, operator: '=' } + ]) + ) + ) + devicePromises.push( + limit(() => + clientServer.matrixDb.deleteWhere('device_auth_providers', [ + { field: 'device_id', value: deviceId, operator: '=' } + ]) + ) + ) + devicePromises.push( + limit(() => + clientServer.matrixDb.deleteWhere('e2e_device_keys_json', [ + { field: 'device_id', value: deviceId, operator: '=' } + ]) + ) + ) + devicePromises.push( + limit(() => + clientServer.matrixDb.deleteWhere('e2e_one_time_keys_json', [ + { field: 'device_id', value: deviceId, operator: '=' } + ]) + ) + ) + devicePromises.push( + limit(() => + clientServer.matrixDb.deleteWhere('dehydrated_devices', [ + { field: 'device_id', value: deviceId, operator: '=' } + ]) + ) + ) + devicePromises.push( + limit(() => + clientServer.matrixDb.deleteWhere('e2e_fallback_keys_json', [ + { field: 'device_id', value: deviceId, operator: '=' } + ]) + ) + ) + } + return devicePromises +} + +const deletePushers = async ( + clientServer: MatrixClientServer, + devices: string[], + userId: string +): Promise>> => { + let insertDeletedPushersPromises: Array> = [] + for (const deviceId of devices) { + const deviceDisplayNameRow = await clientServer.matrixDb.get( + 'devices', + ['display_name'], + { device_id: deviceId } + ) + // istanbul ignore if + if (deviceDisplayNameRow.length === 0) { + // Since device_display_name has the NOT NULL constraint, we assume that if the device has no display name it has no associated pushers + // Ideally there should be a device_id field in the pushers table to delete by device_id + continue + } + const pushers = await clientServer.matrixDb.get( + 'pushers', + ['app_id', 'pushkey'], + { + user_name: userId, + device_display_name: deviceDisplayNameRow[0].display_name + } + ) + await clientServer.matrixDb.deleteWhere( + // We'd like to delete by device_id but there is no device_id field in the pushers table + 'pushers', + [ + { + field: 'device_display_name', + value: deviceDisplayNameRow[0].display_name as string, + operator: '=' + }, + { field: 'user_name', value: userId, operator: '=' } + ] + ) + insertDeletedPushersPromises = pushers.map(async (pusher) => { + await limit(() => + clientServer.matrixDb.insert('deleted_pushers', { + stream_id: randomString(64), // TODO: Update when stream ordering is implemented since the stream_id has to keep track of the order of operations + app_id: pusher.app_id as string, + pushkey: pusher.pushkey as string, + user_id: userId + }) + ) + }) + } + return insertDeletedPushersPromises +} + +const deleteTokens = ( + clientServer: MatrixClientServer, + devices: string[], + userId: string +): Array> => { + const deleteTokensPromises: Array> = [] + for (const deviceId of devices) { + deleteTokensPromises.push( + limit(() => + clientServer.matrixDb.deleteWhere('access_tokens', [ + { field: 'user_id', value: userId, operator: '=' }, + { field: 'device_id', value: deviceId, operator: '=' } + ]) + ) + ) + deleteTokensPromises.push( + limit(() => + clientServer.matrixDb.deleteWhere('refresh_tokens', [ + { field: 'user_id', value: userId, operator: '=' }, + { field: 'device_id', value: deviceId, operator: '=' } + ]) + ) + ) + } + return deleteTokensPromises +} + +export const deleteMessagesBetweenStreamIds = async ( + clientServer: MatrixClientServer, + userId: string, + deviceId: string, + fromStreamId: number, + upToStreamId: number, + limit: number +): Promise => { + const maxStreamId = await clientServer.matrixDb.getMaxStreamId( + userId, + deviceId, + fromStreamId, + upToStreamId, + limit + ) + if (maxStreamId === null) { + return 0 + } + await clientServer.matrixDb.deleteWhere('device_inbox', [ + { field: 'user_id', value: userId, operator: '=' }, + { field: 'device_id', value: deviceId, operator: '=' }, + { field: 'stream_id', value: maxStreamId, operator: '<=' }, + { field: 'stream_id', value: fromStreamId, operator: '>' } + ]) + return maxStreamId +} +const deleteDeviceInbox = async ( + clientServer: MatrixClientServer, + userId: string, + deviceId: string, + upToStreamId: number +): Promise => { + let fromStreamId = 0 + while (true) { + // Maybe add a counter to prevent infinite loops if the deletion process is broken + const maxStreamId = await deleteMessagesBetweenStreamIds( + clientServer, + userId, + deviceId, + fromStreamId, + upToStreamId, + MESSAGES_TO_DELETE_BATCH_SIZE + ) + if (maxStreamId === 0) { + break + } + fromStreamId = maxStreamId + } +} + +export const deleteDevicesData = async ( + clientServer: MatrixClientServer, + devices: string[], + userId: string + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type +): Promise => { + // In Synapse's implementation, they also delete account data relative to local notification settings according to this MR : https://github.com/matrix-org/matrix-spec-proposals/pull/3890 + // I did not include it since it is not in the spec + const deleteTokensPromises = deleteTokens(clientServer, devices, userId) + const deleteDevicesPromises = deleteDevices(clientServer, devices) + const deletePushersPromises = await deletePushers( + clientServer, + devices, + userId + ) + const deleteDeviceInboxPromises = devices.map((deviceId) => { + return limit(() => deleteDeviceInbox(clientServer, userId, deviceId, 1000)) // TODO : Fix the upToStreamId when stream ordering is implemented. It should be set to avoid deleting non delivered messages + }) + return await Promise.all([ + ...deleteTokensPromises, + ...deleteDevicesPromises, + ...deletePushersPromises, + ...deleteDeviceInboxPromises + ]) +} + +const deleteDevicesHandler = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + clientServer.authenticate(req, res, (data) => { + jsonContent(req, res, clientServer.logger, (obj) => { + const body = obj as unknown as RequestBody + if ( + body.auth != null && + body.auth !== undefined && + !verifyAuthenticationData(body.auth) + ) { + send(res, 400, errMsg('invalidParam', 'Invalid auth')) + return + } else if (!verifyArray(body.devices, 'string')) { + send(res, 400, errMsg('invalidParam', 'Invalid devices')) + return + } + validateUserWithUIAuthentication( + clientServer, + req, + res, + data.sub, + 'remove device(s) from your account', + obj, + // eslint-disable-next-line @typescript-eslint/no-misused-promises + (obj, userId) => { + deleteDevicesData( + clientServer, + (obj as RequestBody).devices, + userId as string + ) + .then(() => { + send(res, 200, {}) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error(`Unable to delete devices`, e) + // istanbul ignore next + send( + res, + 500, + errMsg('unknown', e.toString()), + clientServer.logger + ) + }) + } + ) + }) + }) + } +} + +export default deleteDevicesHandler diff --git a/packages/matrix-client-server/src/devices/changeDevices.ts b/packages/matrix-client-server/src/devices/changeDevices.ts new file mode 100644 index 00000000..7e0c1283 --- /dev/null +++ b/packages/matrix-client-server/src/devices/changeDevices.ts @@ -0,0 +1,90 @@ +/* +This file implements the changeDeviceName functions, which is used to update the display name of a device associated with a user. +These functions are used to provide device management functionality in the Matrix client server : https://spec.matrix.org/v1.11/client-server-api/#get_matrixclientv3devices + +TODO : Add checks to ensure that the user has the rigths to change the device name. +*/ + +import { + errMsg, + type expressAppHandler, + jsonContent, + send, + validateParameters +} from '@twake/utils' +import type MatrixClientServer from '../index' +import { type Request } from 'express' + +const schema = { + display_name: true +} + +interface changeDeviceNameArgs { + display_name: string +} + +export const changeDeviceName = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + const deviceId: string = (req as Request).params.deviceId + clientServer.authenticate(req, res, (data) => { + const userId = data.sub + jsonContent(req, res, clientServer.logger, (obj) => { + validateParameters(res, schema, obj, clientServer.logger, (obj) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const new_display_name = (obj as changeDeviceNameArgs).display_name + + if (new_display_name.length > 255) { + send( + res, + 400, + errMsg( + 'invalidParam', + 'The display name must be less than 255 characters' + ) + ) + return + } + + clientServer.matrixDb + .updateWithConditions( + 'devices', + { display_name: new_display_name }, + [ + { field: 'device_id', value: deviceId }, + { field: 'user_id', value: userId } + ] + ) + .then((rows) => { + if (rows.length === 0) { + send( + res, + 404, + errMsg( + 'notFound', + 'The current user has no device with the given ID' + ), + clientServer.logger + ) + } else { + clientServer.logger.debug('Device Name updated') + send(res, 200, {}, clientServer.logger) + } + }) + .catch((e) => { + /* istanbul ignore next */ + clientServer.logger.error('Error querying profiles:') + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', e.toString()), + clientServer.logger + ) + }) + }) + }) + }) + } +} diff --git a/packages/matrix-client-server/src/devices/deleteDevice.ts b/packages/matrix-client-server/src/devices/deleteDevice.ts new file mode 100644 index 00000000..fd612afe --- /dev/null +++ b/packages/matrix-client-server/src/devices/deleteDevice.ts @@ -0,0 +1,63 @@ +import { errMsg, type expressAppHandler, jsonContent, send } from '@twake/utils' +import type MatrixClientServer from '..' +import { type AuthenticationData } from '../types' +import { validateUserWithUIAuthentication } from '../utils/userInteractiveAuthentication' +import { verifyAuthenticationData, verifyString } from '../typecheckers' +import { deleteDevicesData } from '../delete_devices' + +interface RequestBody { + auth?: AuthenticationData +} + +interface Parameters { + deviceId: string +} +const deleteDevice = (clientServer: MatrixClientServer): expressAppHandler => { + return (req, res) => { + clientServer.authenticate(req, res, (data) => { + jsonContent(req, res, clientServer.logger, (obj) => { + const body = obj as unknown as RequestBody + // @ts-expect-error : request has parameters + const deviceId = (req.params as Parameters).deviceId + if ( + body.auth != null && + body.auth !== undefined && + !verifyAuthenticationData(body.auth) + ) { + send(res, 400, errMsg('invalidParam', 'Invalid auth')) + return + } else if (!verifyString(deviceId)) { + send(res, 400, errMsg('invalidParam', 'Invalid device ID')) + return + } + validateUserWithUIAuthentication( + clientServer, + req, + res, + data.sub, + 'delete device', + obj, + (obj, userId) => { + deleteDevicesData(clientServer, [deviceId], userId as string) + .then(() => { + send(res, 200, {}) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error(`Error while deleting device`, e) + // istanbul ignore next + send( + res, + 500, + errMsg('unknown', e.toString()), + clientServer.logger + ) + }) + } + ) + }) + }) + } +} + +export default deleteDevice diff --git a/packages/matrix-client-server/src/devices/devices.test.ts b/packages/matrix-client-server/src/devices/devices.test.ts new file mode 100644 index 00000000..3c374bdb --- /dev/null +++ b/packages/matrix-client-server/src/devices/devices.test.ts @@ -0,0 +1,387 @@ +import fs from 'fs' +import request from 'supertest' +import express from 'express' +import ClientServer from '../index' +import { buildMatrixDb, buildUserDB } from '../__testData__/buildUserDB' +import { type Config } from '../types' +import defaultConfig from '../__testData__/registerConf.json' +import { getLogger, type TwakeLogger } from '@twake/logger' +import { setupTokens, validToken } from '../__testData__/setupTokens' +import { randomString } from '@twake/crypto' +jest.mock('node-fetch', () => jest.fn()) +const sendMailMock = jest.fn() +jest.mock('nodemailer', () => ({ + createTransport: jest.fn().mockImplementation(() => ({ + sendMail: sendMailMock + })) +})) + +let conf: Config +let clientServer: ClientServer +let app: express.Application + +const logger: TwakeLogger = getLogger() + +beforeAll((done) => { + // @ts-expect-error TS doesn't understand that the config is valid + conf = { + ...defaultConfig, + base_url: 'http://example.com/', + matrix_database_host: 'src/__testData__/devicesTestMatrix.db', + userdb_host: 'src/__testData__/devicesTest.db', + database_host: 'src/__testData__/devicesTest.db', + registration_required_3pid: ['email', 'msisdn'] + } + if (process.env.TEST_PG === 'yes') { + conf.database_engine = 'pg' + conf.userdb_engine = 'pg' + conf.database_host = process.env.PG_HOST ?? 'localhost' + conf.database_user = process.env.PG_USER ?? 'twake' + conf.database_password = process.env.PG_PASSWORD ?? 'twake' + conf.database_name = process.env.PG_DATABASE ?? 'test' + } + buildUserDB(conf) + .then(() => { + buildMatrixDb(conf) + .then(() => { + done() + }) + .catch((e) => { + logger.error('Error while building matrix db:', e) + done(e) + }) + }) + .catch((e) => { + logger.error('Error while building user db:', e) + done(e) + }) +}) + +afterAll(() => { + fs.unlinkSync('src/__testData__/devicesTest.db') + fs.unlinkSync('src/__testData__/devicesTestMatrix.db') +}) + +beforeEach(() => { + jest.clearAllMocks() +}) + +describe('Use configuration file', () => { + beforeAll((done) => { + clientServer = new ClientServer(conf) + app = express() + clientServer.ready + .then(() => { + Object.keys(clientServer.api.get).forEach((k) => { + app.get(k, clientServer.api.get[k]) + }) + Object.keys(clientServer.api.post).forEach((k) => { + app.post(k, clientServer.api.post[k]) + }) + Object.keys(clientServer.api.put).forEach((k) => { + app.put(k, clientServer.api.put[k]) + }) + Object.keys(clientServer.api.delete).forEach((k) => { + app.delete(k, clientServer.api.delete[k]) + }) + done() + }) + .catch((e) => { + done(e) + }) + }) + + afterAll(() => { + clientServer.cleanJobs() + }) + + describe('Endpoints with authentication', () => { + beforeAll(async () => { + await setupTokens(clientServer, logger) + }) + + describe('/_matrix/client/v3/devices', () => { + const testUserId = '@testuser:example.com' + + beforeAll(async () => { + try { + await clientServer.matrixDb.insert('devices', { + user_id: testUserId, + device_id: 'testdevice1', + display_name: 'Test Device 1', + last_seen: 1411996332123, + ip: '127.0.0.1', + user_agent: 'curl/7.31.0-DEV' + }) + logger.info('Test device 1 created') + + await clientServer.matrixDb.insert('devices', { + user_id: testUserId, + device_id: 'testdevice2', + display_name: 'Test Device 2', + last_seen: 14119963321254, + ip: '127.0.0.2', + user_agent: 'curl/7.31.0-DEV' + }) + logger.info('Test device 2 created') + } catch (e) { + logger.error('Error creating devices:', e) + } + }) + + afterAll(async () => { + try { + await clientServer.matrixDb.deleteEqual( + 'devices', + 'device_id', + 'testdevice1' + ) + logger.info('Test device 1 deleted') + + await clientServer.matrixDb.deleteEqual( + 'devices', + 'device_id', + 'testdevice2' + ) + logger.info('Test device 2 deleted') + } catch (e) { + logger.error('Error deleting devices:', e) + } + }) + + it('should return 401 if the user is not authenticated', async () => { + const response = await request(app) + .get('/_matrix/client/v3/devices') + .set('Authorization', 'Bearer invalidToken') + .set('Accept', 'application/json') + expect(response.statusCode).toBe(401) + }) + + it('should return all devices for the current user', async () => { + const response = await request(app) + .get('/_matrix/client/v3/devices') + .set('Authorization', `Bearer ${validToken}`) + + expect(response.statusCode).toBe(200) + + expect(response.body).toHaveProperty('devices') + expect(response.body.devices).toHaveLength(2) + expect(response.body.devices[0]).toHaveProperty('device_id') + expect(response.body.devices[0]).toHaveProperty('display_name') + expect(response.body.devices[0]).toHaveProperty('last_seen_ts') + expect(response.body.devices[0]).toHaveProperty('last_seen_ip') + }) + describe('/_matrix/client/v3/devices/:deviceId', () => { + // eslint-disable-next-line @typescript-eslint/naming-convention + let _device_id: string + beforeAll(async () => { + try { + _device_id = 'testdevice2_id' + await clientServer.matrixDb.insert('devices', { + user_id: '@testuser:example.com', + device_id: _device_id, + display_name: 'testdevice2_name', + last_seen: 12345678, + ip: '127.0.0.1', + user_agent: 'curl/7.31.0-DEV', + hidden: 0 + }) + + await clientServer.matrixDb.insert('devices', { + user_id: '@testuser2:example.com', + device_id: 'another_device_id', + display_name: 'another_name', + last_seen: 12345678, + ip: '127.0.0.1', + user_agent: 'curl/7.31.0-DEV', + hidden: 0 + }) + logger.info('Devices inserted in db') + } catch (e) { + logger.error('Error when inserting devices', e) + } + }) + + afterAll(async () => { + try { + await clientServer.matrixDb.deleteEqual( + 'devices', + 'device_id', + _device_id + ) + await clientServer.matrixDb.deleteEqual( + 'devices', + 'device_id', + 'another_device_id' + ) + logger.info('Devices deleted from db') + } catch (e) { + logger.error('Error when deleting devices', e) + } + }) + + describe('GET', () => { + it('should return the device information for the given device ID', async () => { + const response = await request(app) + .get(`/_matrix/client/v3/devices/${_device_id}`) + .set('Authorization', `Bearer ${validToken}`) + + expect(response.statusCode).toBe(200) + + expect(response.body).toHaveProperty('device_id') + expect(response.body.device_id).toEqual(_device_id) + expect(response.body).toHaveProperty('display_name') + expect(response.body.display_name).toEqual('testdevice2_name') + expect(response.body).toHaveProperty('last_seen_ip') + expect(response.body.last_seen_ip).toEqual('127.0.0.1') + expect(response.body).toHaveProperty('last_seen_ts') + expect(response.body.last_seen_ts).toEqual(12345678) + }) + + it('should return 404 if the device ID does not exist', async () => { + const deviceId = 'NON_EXISTENT_DEVICE_ID' + const response = await request(app) + .get(`/_matrix/client/v3/devices/${deviceId}`) + .set('Authorization', `Bearer ${validToken}`) + + expect(response.statusCode).toBe(404) + }) + + it('should return 404 if the user has no device with the given device Id', async () => { + const response = await request(app) + .get(`/_matrix/client/v3/devices/another_device_id`) + .set('Authorization', `Bearer ${validToken}`) + + expect(response.statusCode).toBe(404) + }) + + it('should return 401 if the user is not authenticated', async () => { + const response = await request(app).get( + `/_matrix/client/v3/devices/${_device_id}` + ) + + expect(response.statusCode).toBe(401) + }) + }) + + describe('PUT', () => { + const updateData = { + display_name: 'updated_device_name' + } + + it('should update the device information for the given device ID', async () => { + // Update the device + const response = await request(app) + .put(`/_matrix/client/v3/devices/${_device_id}`) + .set('Authorization', `Bearer ${validToken}`) + .send(updateData) + expect(response.statusCode).toBe(200) + + // Verify the update in the database + const updatedDevice = await clientServer.matrixDb.get( + 'devices', + ['device_id', 'display_name'], + { device_id: _device_id } + ) + + expect(updatedDevice[0]).toHaveProperty('device_id', _device_id) + expect(updatedDevice[0]).toHaveProperty( + 'display_name', + updateData.display_name + ) + }) + + it('should return 400 if the display_name is too long', async () => { + const response = await request(app) + .put(`/_matrix/client/v3/devices/${_device_id}`) + .set('Authorization', `Bearer ${validToken}`) + .send({ display_name: randomString(257) }) + + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + + it('should return 404 if the device ID does not exist', async () => { + const response = await request(app) + .put('/_matrix/client/v3/devices/NON_EXISTENT_DEVICE_ID') + .set('Authorization', `Bearer ${validToken}`) + .send(updateData) + + expect(response.statusCode).toBe(404) + }) + + it('should return 404 if the user has no device with the given device ID', async () => { + const deviceId = 'another_device_id' + const response = await request(app) + .put(`/_matrix/client/v3/devices/${deviceId}`) + .set('Authorization', `Bearer ${validToken}`) + .send(updateData) + + expect(response.statusCode).toBe(404) + }) + + it('should return 401 if the user is not authenticated', async () => { + const response = await request(app) + .put(`/_matrix/client/v3/devices/${_device_id}`) + .send(updateData) + + expect(response.statusCode).toBe(401) + }) + }) + + describe('DELETE', () => { + it('should refuse an invalid auth token', async () => { + const response = await request(app) + .delete(`/_matrix/client/v3/devices/${_device_id}`) + .set('Authorization', `Bearer ${validToken}`) + .send({ + devices: ['device1', 'device2'], + auth: { invalid: 'auth' } + }) + expect(response.status).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should refuse an invalid deviceId', async () => { + const response = await request(app) + .delete(`/_matrix/client/v3/devices/${randomString(1000)}`) + .set('Authorization', `Bearer ${validToken}`) + .send({ + devices: ['device1', 'device2'] + }) + expect(response.status).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should delete a device', async () => { + const response1 = await request(app) + .delete(`/_matrix/client/v3/devices/${_device_id}`) + .set('Authorization', `Bearer ${validToken}`) + .send({}) + expect(response1.status).toBe(401) + expect(response1.body).toHaveProperty('session') + const session = response1.body.session + const response = await request(app) + .delete(`/_matrix/client/v3/devices/${_device_id}`) + .set('Authorization', `Bearer ${validToken}`) + .send({ + devices: [_device_id], + auth: { + type: 'm.login.password', + identifier: { type: 'm.id.user', user: testUserId }, + password: + '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK', + session + } + }) + expect(response.status).toBe(200) + const devices = await clientServer.matrixDb.get( + 'devices', + ['device_id'], + { device_id: _device_id } + ) + expect(devices).toHaveLength(0) + }) + }) + }) + }) + }) +}) diff --git a/packages/matrix-client-server/src/devices/getDevices.ts b/packages/matrix-client-server/src/devices/getDevices.ts new file mode 100644 index 00000000..b8bbba3b --- /dev/null +++ b/packages/matrix-client-server/src/devices/getDevices.ts @@ -0,0 +1,95 @@ +/* +This file implements the getDevices and getDeviceInfo functions, which are used to retrieve information about devices associated with a user. +The getDevices function returns a list of devices, while the getDeviceInfo function returns information about a specific device. +These functions are used to provide device management functionality in the Matrix client server : https://spec.matrix.org/v1.11/client-server-api/#get_matrixclientv3devices + +One of the main differences between the implementation in the Twake codebase and the equivalent implementation in the Synapse codebase +is that for now we are not updating the last_ip field of a device when it is looked upon by a user (as it is done here). + +It can be done by looking up the ip of the client (stored in the user_ips table) and updating the ip field of the device in the devices table. +*/ + +import { errMsg, type expressAppHandler, send } from '@twake/utils' +import type MatrixClientServer from '../index' +import { type Request } from 'express' + +export const getDevices = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + clientServer.authenticate(req, res, (token) => { + const userId = token.sub + + clientServer.matrixDb + .get('devices', ['device_id', 'display_name', 'last_seen', 'ip'], { + user_id: userId + }) + .then((rows) => { + // returns a list of devices + const _devices = rows.map((row) => { + return { + device_id: row.device_id, + display_name: row.display_name, + last_seen_ip: row.ip, + last_seen_ts: row.last_seen + } + }) + send(res, 200, { devices: _devices }, clientServer.logger) + }) + .catch((e) => { + /* istanbul ignore next */ + clientServer.logger.error('Error querying devices') + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', e.toString()), clientServer.logger) + }) + }) + } +} + +export const getDeviceInfo = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + clientServer.authenticate(req, res, (token) => { + const userId = token.sub + const deviceId: string = (req as Request).params.deviceId + + clientServer.matrixDb + .get('devices', ['display_name', 'last_seen', 'ip'], { + user_id: userId, + device_id: deviceId + }) + .then((rows) => { + if (rows.length === 0) { + send( + res, + 404, + errMsg( + 'notFound', + 'The current user has no device with the given ID' + ), + clientServer.logger + ) + } else { + send( + res, + 200, + { + device_id: deviceId, + display_name: rows[0].display_name, + last_seen_ip: rows[0].ip, + last_seen_ts: rows[0].last_seen + }, + clientServer.logger + ) + } + }) + .catch((e) => { + /* istanbul ignore next */ + clientServer.logger.error('Error querying devices:') + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', e.toString()), clientServer.logger) + }) + }) + } +} diff --git a/packages/matrix-client-server/src/index.test.ts b/packages/matrix-client-server/src/index.test.ts new file mode 100644 index 00000000..356b59c2 --- /dev/null +++ b/packages/matrix-client-server/src/index.test.ts @@ -0,0 +1,1026 @@ +import fs from 'fs' +import request from 'supertest' +import express from 'express' +import ClientServer from './index' +import { buildMatrixDb, buildUserDB } from './__testData__/buildUserDB' +import { type Config } from './types' +import defaultConfig from './__testData__/registerConf.json' +import { getLogger, type TwakeLogger } from '@twake/logger' +import { randomString } from '@twake/crypto' +import { + setupTokens, + validToken, + validRefreshToken1, + validRefreshToken2 +} from './__testData__/setupTokens' + +process.env.TWAKE_CLIENT_SERVER_CONF = './src/__testData__/registerConf.json' +jest.mock('node-fetch', () => jest.fn()) +const sendMailMock = jest.fn() +jest.mock('nodemailer', () => ({ + createTransport: jest.fn().mockImplementation(() => ({ + sendMail: sendMailMock + })) +})) + +let conf: Config +let clientServer: ClientServer +let app: express.Application + +const logger: TwakeLogger = getLogger() + +beforeAll((done) => { + // @ts-expect-error TS doesn't understand that the config is valid + conf = { + ...defaultConfig, + base_url: 'http://example.com/' + } + if (process.env.TEST_PG === 'yes') { + conf.database_engine = 'pg' + conf.userdb_engine = 'pg' + conf.database_host = process.env.PG_HOST ?? 'localhost' + conf.database_user = process.env.PG_USER ?? 'twake' + conf.database_password = process.env.PG_PASSWORD ?? 'twake' + conf.database_name = process.env.PG_DATABASE ?? 'test' + } + buildUserDB(conf) + .then(() => { + buildMatrixDb(conf) + .then(() => { + done() + }) + .catch((e) => { + logger.error('Error while building matrix db:', e) + done(e) + }) + }) + .catch((e) => { + logger.error('Error while building user db:', e) + done(e) + }) +}) + +afterAll(() => { + fs.unlinkSync('src/__testData__/test.db') + fs.unlinkSync('src/__testData__/testMatrix.db') +}) + +beforeEach(() => { + jest.clearAllMocks() +}) + +describe('Error on server start', () => { + process.env.HASHES_RATE_LIMIT = 'falsy_number' + + it('should display message error about hashes rate limit value', () => { + expect(() => { + clientServer = new ClientServer() + }).toThrow( + new Error( + 'hashes_rate_limit must be a number or a string representing a number' + ) + ) + delete process.env.HASHES_RATE_LIMIT + }) +}) + +describe('Use configuration file', () => { + beforeAll((done) => { + clientServer = new ClientServer() + app = express() + clientServer.ready + .then(() => { + Object.keys(clientServer.api.get).forEach((k) => { + app.get(k, clientServer.api.get[k]) + }) + Object.keys(clientServer.api.post).forEach((k) => { + app.post(k, clientServer.api.post[k]) + }) + Object.keys(clientServer.api.put).forEach((k) => { + app.put(k, clientServer.api.put[k]) + }) + Object.keys(clientServer.api.delete).forEach((k) => { + app.delete(k, clientServer.api.delete[k]) + }) + done() + }) + .catch((e) => { + done(e) + }) + }) + + afterAll(() => { + clientServer.cleanJobs() + }) + + test('Reject unimplemented endpoint with 404', async () => { + const response = await request(app).get('/_matrix/unknown') + expect(response.statusCode).toBe(404) + }) + + test('Reject bad method with 405', async () => { + const response = await request(app).post( + '/_matrix/client/v3/account/whoami' + ) + expect(response.statusCode).toBe(405) + }) + + describe('/_matrix/client/versions', () => { + it('sould correctly provide supported versions', async () => { + const response = await request(app).get('/_matrix/client/versions') + expect(response.statusCode).toBe(200) + }) + }) + + it('should return true if provided user is hosted on local server', async () => { + expect(clientServer.isMine('@testuser:example.com')).toBe(true) + }) + + it('should return false if provided user is hosted on remote server', async () => { + expect(clientServer.isMine('@testuser:remote.com')).toBe(false) + }) + + describe('Endpoints with authentication', () => { + beforeAll(async () => { + await setupTokens(clientServer, logger) + }) + describe('/_matrix/client/v3/refresh', () => { + it('should refuse a request without refresh token', async () => { + const response = await request(app) + .post('/_matrix/client/v3/refresh') + .send({}) + expect(response.statusCode).toBe(400) + expect(response.body.errcode).toBe('M_MISSING_PARAMS') + }) + it('should refuse a request with an unknown refresh token', async () => { + const response = await request(app) + .post('/_matrix/client/v3/refresh') + .send({ refresh_token: 'unknownToken' }) + expect(response.statusCode).toBe(400) + expect(response.body.errcode).toBe('M_UNKNOWN_TOKEN') + }) + it('should refuse a request with an expired refresh token', async () => { + await clientServer.matrixDb.insert('refresh_tokens', { + id: 0, + user_id: 'expiredUser', + device_id: 'expiredDevice', + token: 'expiredToken', + expiry_ts: 0 + }) + const response = await request(app) + .post('/_matrix/client/v3/refresh') + .send({ refresh_token: 'expiredToken' }) + expect(response.statusCode).toBe(401) + expect(response.body.errcode).toBe('INVALID_TOKEN') + }) + it('should send the next request token if the token sent in the request has such a field in the DB', async () => { + const response = await request(app) + .post('/_matrix/client/v3/refresh') + .send({ refresh_token: validRefreshToken1 }) + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('access_token') + expect(response.body).toHaveProperty('refresh_token') + expect(response.body.refresh_token).toBe(validRefreshToken2) + }) + it('should generate a new refresh token and access token if there was no next token in the DB', async () => { + const response = await request(app) + .post('/_matrix/client/v3/refresh') + .send({ refresh_token: validRefreshToken2 }) + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('access_token') + expect(response.body).toHaveProperty('refresh_token') + }) + }) + + describe('/_matrix/client/v3/admin/whois', () => { + it('should refuse a request without a userId', async () => { + const response = await request(app) + .get('/_matrix/client/v3/admin/whois') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + }) + it('should send device information for the user being looked-up', async () => { + await clientServer.matrixDb.insert('user_ips', { + user_id: '@testuser:example.com', + device_id: 'testdevice', + access_token: validToken, + ip: '10.0.0.2', + user_agent: + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36', + last_seen: 1411996332123 + }) + const response = await request(app) + .get('/_matrix/client/v3/admin/whois') + .query({ userId: '@testuser:example.com' }) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('user_id', '@testuser:example.com') + expect(response.body).toHaveProperty('devices') + expect(response.body.devices).toHaveProperty('testdevice') + expect(response.body.devices.testdevice).toHaveProperty('sessions') + expect(response.body.devices.testdevice.sessions).toHaveLength(1) + expect(response.body.devices.testdevice.sessions).toEqual([ + { + connections: [ + { + ip: '127.0.0.1', + last_seen: 1411996332123, + user_agent: 'curl/7.31.0-DEV' + }, + { + ip: '10.0.0.2', + last_seen: 1411996332123, + user_agent: + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36' + } + ] + } + ]) + }) + it('should work if the user has multiple devices and with multiple sessions', async () => { + const validTokenbis = randomString(64) + await clientServer.matrixDb.insert('user_ips', { + user_id: '@testuser:example.com', + device_id: 'testdevice2', + access_token: validTokenbis, + ip: '127.0.0.1', + last_seen: 1411996332123, + user_agent: 'curl/7.31.0-DEV' + }) + const response = await request(app) + .get('/_matrix/client/v3/admin/whois') + .query({ userId: '@testuser:example.com' }) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('user_id', '@testuser:example.com') + expect(response.body).toHaveProperty('devices') + expect(response.body.devices).toHaveProperty('testdevice') + expect(response.body.devices).toHaveProperty('testdevice2') + expect(response.body.devices.testdevice2).toHaveProperty('sessions') + expect(response.body.devices.testdevice2.sessions).toHaveLength(1) + expect(response.body.devices.testdevice2.sessions).toEqual([ + { + connections: [ + { + ip: '127.0.0.1', + last_seen: 1411996332123, + user_agent: 'curl/7.31.0-DEV' + } + ] + } + ]) + }) + }) + + describe('/_matrix/client/v3/user/:userId', () => { + describe('/_matrix/client/v3/user/:userId/account_data/:type', () => { + it('should reject invalid userId', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/invalidUserId/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject invalid roomId', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/rooms/invalidRoomId/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject an invalid event type', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/account_data/invalidEventType' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject missing account data', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(404) + expect(response.body).toHaveProperty('errcode', 'M_NOT_FOUND') + }) + it('should refuse to return account data for another user', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@anotheruser:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + }) + it('should return account data', async () => { + await clientServer.matrixDb.insert('account_data', { + user_id: '@testuser:example.com', + account_data_type: 'm.room.message', + stream_id: 1, + content: 'test content' + }) + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body['m.room.message']).toBe('test content') + }) + it('should reject invalid userId', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/invalidUserId/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject an invalid event type', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@testuser:example.com/account_data/invalidEventType' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject missing account data', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@testuser:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_UNKNOWN') // Error code from jsonContent function of @twake/utils + }) + it('should refuse to update account data for another user', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@anotheruser:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ content: 'new content' }) + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + }) + it('should refuse content that is too long', async () => { + let content = '' + for (let i = 0; i < 10000; i++) { + content += 'a' + } + const response = await request(app) + .put( + '/_matrix/client/v3/user/@testuser:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ content }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should update account data', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@testuser:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ content: 'updated content' }) + expect(response.statusCode).toBe(200) + const response2 = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response2.statusCode).toBe(200) + expect(response2.body['m.room.message']).toBe('updated content') + }) + }) + + describe('/_matrix/client/v3/user/:userId/rooms/:roomId/account_data/:type', () => { + describe('GET', () => { + it('should reject invalid userId', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/invalidUserId/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject invalid roomId', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/rooms/invalidRoomId/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject an invalid event type', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/rooms/!roomId:example.com/account_data/invalidEventType' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject missing account data', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(404) + expect(response.body).toHaveProperty('errcode', 'M_NOT_FOUND') + }) + it('should refuse to return account data for another user', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@anotheruser:example.com/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + }) + it('should return account data', async () => { + await clientServer.matrixDb.insert('room_account_data', { + user_id: '@testuser:example.com', + account_data_type: 'm.room.message', + stream_id: 1, + content: 'test content', + room_id: '!roomId:example.com' + }) + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body['m.room.message']).toBe('test content') + }) + }) + describe('PUT', () => { + it('should reject invalid userId', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/invalidUserId/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject invalid roomId', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@testuser:example.com/rooms/invalidRoomId/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject an invalid event type', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@testuser:example.com/rooms/!roomId:example.com/account_data/invalidEventType' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject missing account data', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@testuser:example.com/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_UNKNOWN') // Error code from jsonContent function of @twake/utils + }) + it('should refuse to update account data for another user', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@anotheruser:example.com/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ content: 'new content' }) + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + }) + it('should refuse content that is too long', async () => { + let content = '' + for (let i = 0; i < 10000; i++) { + content += 'a' + } + const response = await request(app) + .put( + '/_matrix/client/v3/user/@testuser:example.com/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ content }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should update account data', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@testuser:example.com/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ content: 'updated content' }) + expect(response.statusCode).toBe(200) + const response2 = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response2.statusCode).toBe(200) + expect(response2.body['m.room.message']).toBe('updated content') + }) + }) + }) + }) + + describe('/_matrix/client/v3/directory/list/room/:roomId', () => { + describe('GET', () => { + const publicRoomId = '!testroomid:example.com' + const privateRoomId = '!private:example.com' + + beforeAll(async () => { + try { + await clientServer.matrixDb.insert('rooms', { + room_id: publicRoomId, + is_public: 1 + }) + + await clientServer.matrixDb.insert('rooms', { + room_id: privateRoomId, + is_public: 0 + }) + + await clientServer.matrixDb.insert('rooms', { + room_id: '!anotherroomid:example.com' + }) + } catch (e) { + logger.error('Error setting up test data:', e) + } + }) + + afterAll(async () => { + try { + await clientServer.matrixDb.deleteEqual( + 'rooms', + 'room_id', + publicRoomId + ) + + await clientServer.matrixDb.deleteEqual( + 'rooms', + 'room_id', + privateRoomId + ) + + await clientServer.matrixDb.deleteEqual( + 'rooms', + 'room_id', + '!anotherroomid:example.com' + ) + } catch (e) { + logger.error('Error tearing down test data:', e) + } + }) + + it('should return the correct visibility for a public room', async () => { + const response = await request(app).get( + `/_matrix/client/v3/directory/list/room/${publicRoomId}` + ) + expect(response.statusCode).toBe(200) + expect(response.body).toEqual({ + visibility: 'public' + }) + }) + + it('should return the correct visibility for a private room', async () => { + const response = await request(app).get( + `/_matrix/client/v3/directory/list/room/${privateRoomId}` + ) + expect(response.statusCode).toBe(200) + expect(response.body).toEqual({ + visibility: 'private' + }) + }) + + it('should return private visibility if no visibility is set', async () => { + const response = await request(app).get( + `/_matrix/client/v3/directory/list/room/!anotherroomid:example.com` + ) + expect(response.statusCode).toBe(200) + expect(response.body).toEqual({ + visibility: 'private' + }) + }) + + it('should return 404 if the room is not found', async () => { + const invalidRoomId = '!invalidroomid:example.com' + const response = await request(app).get( + `/_matrix/client/v3/directory/list/room/${invalidRoomId}` + ) + expect(response.statusCode).toBe(404) + expect(response.body).toEqual({ + errcode: 'M_NOT_FOUND', + error: 'Room not found' + }) + }) + }) + + describe('PUT', () => { + const testRoomId = '!testroomid:example.com' + + beforeAll(async () => { + try { + await clientServer.matrixDb.insert('rooms', { + room_id: testRoomId, + is_public: 1 + }) + } catch (e) { + logger.error('Error setting up test data:', e) + } + }) + + afterAll(async () => { + try { + await clientServer.matrixDb.deleteEqual( + 'rooms', + 'room_id', + testRoomId + ) + } catch (e) { + logger.error('Error tearing down test data:', e) + } + }) + + it('should require authentication', async () => { + const response = await request(app) + .put(`/_matrix/client/v3/directory/list/room/${testRoomId}`) + .set('Authorization', 'Bearer invalidToken') + .set('Accept', 'application/json') + .send({ visibility: 'private' }) + expect(response.statusCode).toBe(401) + }) + + it('should return 400 invalidParams if wrong parameters given', async () => { + const response = await request(app) + .put(`/_matrix/client/v3/directory/list/room/${testRoomId}`) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ visibility: 'wrongParams' }) + expect(response.statusCode).toBe(400) + expect(response.body.errcode).toBe('M_INVALID_PARAM') + }) + + it('should update the visibility of the room', async () => { + const response = await request(app) + .put(`/_matrix/client/v3/directory/list/room/${testRoomId}`) + .set('Authorization', `Bearer ${validToken}`) + .send({ visibility: 'private' }) + expect(response.statusCode).toBe(200) + + const row = await clientServer.matrixDb.get('rooms', ['is_public'], { + room_id: testRoomId + }) + expect(row[0].is_public).toBe(0) + }) + + it('should update the visibility of the room', async () => { + const response = await request(app) + .put(`/_matrix/client/v3/directory/list/room/${testRoomId}`) + .set('Authorization', `Bearer ${validToken}`) + .send({ visibility: 'public' }) + expect(response.statusCode).toBe(200) + + const row = await clientServer.matrixDb.get('rooms', ['is_public'], { + room_id: testRoomId + }) + expect(row[0].is_public).toBe(1) + }) + + it('should return 404 if the room is not found', async () => { + const invalidRoomId = '!invalidroomid:example.com' + const response = await request(app) + .put(`/_matrix/client/v3/directory/list/room/${invalidRoomId}`) + .set('Authorization', `Bearer ${validToken}`) + .send({ visibility: 'private' }) + expect(response.statusCode).toBe(404) + expect(response.body).toEqual({ + errcode: 'M_NOT_FOUND', + error: 'Room not found' + }) + }) + }) + }) + + describe('/_matrix/client/v3/capabilities', () => { + it('should require authentication', async () => { + const response = await request(app) + .get('/_matrix/client/v3/capabilities') + .set('Authorization', 'Bearer invalid_token') + .set('Accept', 'application/json') + expect(response.statusCode).toBe(401) + }) + + it('should return the capabilities of the server', async () => { + const response = await request(app) + .get('/_matrix/client/v3/capabilities') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + + expect(response.status).toBe(200) + expect(response.body).toHaveProperty('capabilities') + // expect(response.body.capabilities).toHaveProperty('m.room_versions') + expect(response.body.capabilities).toHaveProperty(['m.change_password']) + expect(response.body.capabilities).toHaveProperty(['m.set_displayname']) + expect(response.body.capabilities).toHaveProperty(['m.set_avatar_url']) + expect(response.body.capabilities).toHaveProperty(['m.3pid_changes']) + }) + + it('should return rigth format for m.change_password capability', async () => { + const response = await request(app) + .get('/_matrix/client/v3/capabilities') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + + expect(response.status).toBe(200) + expect(response.body.capabilities).toHaveProperty(['m.change_password']) + expect(response.body.capabilities['m.change_password']).toHaveProperty( + 'enabled' + ) + const numKeyValuePairs = Object.keys( + response.body.capabilities['m.change_password'] + ).length + expect(numKeyValuePairs).toBe(1) + }) + + it('should return rigth format for m.set_displayname capability', async () => { + const response = await request(app) + .get('/_matrix/client/v3/capabilities') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + + expect(response.status).toBe(200) + expect(response.body.capabilities).toHaveProperty(['m.set_displayname']) + expect(response.body.capabilities['m.set_displayname']).toHaveProperty( + 'enabled' + ) + const numKeyValuePairs = Object.keys( + response.body.capabilities['m.set_displayname'] + ).length + expect(numKeyValuePairs).toBe(1) + }) + + it('should return rigth format for m.set_avatar_url capability', async () => { + const response = await request(app) + .get('/_matrix/client/v3/capabilities') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + + expect(response.status).toBe(200) + expect(response.body.capabilities).toHaveProperty(['m.set_avatar_url']) + expect(response.body.capabilities['m.set_avatar_url']).toHaveProperty( + 'enabled' + ) + const numKeyValuePairs = Object.keys( + response.body.capabilities['m.set_avatar_url'] + ).length + expect(numKeyValuePairs).toBe(1) + }) + + it('should return rigth format for m.3pid_changes capability', async () => { + const response = await request(app) + .get('/_matrix/client/v3/capabilities') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + + expect(response.status).toBe(200) + expect(response.body.capabilities).toHaveProperty(['m.3pid_changes']) + expect(response.body.capabilities['m.3pid_changes']).toHaveProperty( + 'enabled' + ) + const numKeyValuePairs = Object.keys( + response.body.capabilities['m.3pid_changes'] + ).length + expect(numKeyValuePairs).toBe(1) + }) + + it('should return rigth format for m.room_versions capability', async () => { + const response = await request(app) + .get('/_matrix/client/v3/capabilities') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + + expect(response.status).toBe(200) + expect(response.body.capabilities).toHaveProperty(['m.room_versions']) + expect(response.body.capabilities['m.room_versions']).toHaveProperty( + 'default' + ) + expect(response.body.capabilities['m.room_versions']).toHaveProperty( + 'available' + ) + const numKeyValuePairs = Object.keys( + response.body.capabilities['m.room_versions'] + ).length + expect(numKeyValuePairs).toBe(2) + }) + }) + describe('/_matrix/client/v3/delete_devices', () => { + let session: string + const userId = '@testuser:example.com' + it('should return 400 if devices is not an array of strings', async () => { + const response = await request(app) + .post('/_matrix/client/v3/delete_devices') + .set('Authorization', `Bearer ${validToken}`) + .send({ devices: 'not an array' }) + expect(response.status).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + + it('should return 400 if auth is provided but invalid', async () => { + const response = await request(app) + .post('/_matrix/client/v3/delete_devices') + .set('Authorization', `Bearer ${validToken}`) + .send({ + devices: ['device1', 'device2'], + auth: { invalid: 'auth' } + }) + expect(response.status).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + + it('should successfully delete devices', async () => { + await clientServer.matrixDb.insert('devices', { + device_id: 'device_id', + user_id: userId, + display_name: 'Device to delete' + }) + const response1 = await request(app) + .post('/_matrix/client/v3/delete_devices') + .set('Authorization', `Bearer ${validToken}`) + .send({ devices: ['device_id'] }) + expect(response1.status).toBe(401) + expect(response1.body).toHaveProperty('session') + session = response1.body.session + const response = await request(app) + .post('/_matrix/client/v3/delete_devices') + .set('Authorization', `Bearer ${validToken}`) + .send({ + devices: ['device_id'], + auth: { + type: 'm.login.password', + identifier: { type: 'm.id.user', user: userId }, + password: + '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK', + session + } + }) + expect(response.status).toBe(200) + const devices = await clientServer.matrixDb.get( + 'devices', + ['device_id'], + { user_id: userId } + ) + expect(devices).toHaveLength(0) + }) + it('should delete associated pushers', async () => { + await clientServer.matrixDb.insert('devices', { + device_id: 'device1', + user_id: userId, + display_name: 'Test Device' + }) + await clientServer.matrixDb.insert('pushers', { + user_name: userId, + device_display_name: 'Test Device', + app_id: 'test_app', + pushkey: 'test_pushkey', + profile_tag: 'test_profile_tag', + kind: 'test_kind', + app_display_name: 'test_app_display_name', + ts: 0 + }) + const response1 = await request(app) + .post('/_matrix/client/v3/delete_devices') + .set('Authorization', `Bearer ${validToken}`) + .send({ devices: ['device1'] }) + expect(response1.status).toBe(401) + expect(response1.body).toHaveProperty('session') + session = response1.body.session + const response = await request(app) + .post('/_matrix/client/v3/delete_devices') + .set('Authorization', `Bearer ${validToken}`) + .send({ + devices: ['device1'], + auth: { + type: 'm.login.password', + session, + identifier: { type: 'm.id.user', user: userId }, + password: + '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK' + } + }) + expect(response.status).toBe(200) + + const pushers = await clientServer.matrixDb.get('pushers', ['app_id'], { + user_name: userId + }) + expect(pushers).toHaveLength(0) + + const deletedPushers = await clientServer.matrixDb.get( + 'deleted_pushers', + ['app_id'], + { user_id: userId } + ) + expect(deletedPushers).toHaveLength(1) + expect(deletedPushers[0].app_id).toBe('test_app') + }) + it('should delete messages in batches', async () => { + const deviceId = 'device1' + + await clientServer.matrixDb.insert('devices', { + device_id: deviceId, + user_id: userId + }) + for (let i = 1; i <= 25; i++) { + await clientServer.matrixDb.insert('device_inbox', { + user_id: userId, + device_id: deviceId, + stream_id: i, + message_json: JSON.stringify({ content: `Message ${i}` }) + }) + } + + const response1 = await request(app) + .post('/_matrix/client/v3/delete_devices') + .set('Authorization', `Bearer ${validToken}`) + .send({ devices: ['device1'] }) + expect(response1.status).toBe(401) + expect(response1.body).toHaveProperty('session') + session = response1.body.session + const response = await request(app) + .post('/_matrix/client/v3/delete_devices') + .set('Authorization', `Bearer ${validToken}`) + .send({ + devices: [deviceId], + auth: { + type: 'm.login.password', + identifier: { type: 'm.id.user', user: userId }, + password: + '$2a$10$zQJv3V3Kjw7Jq7Ww1X7z5e1QXsVd1m3JdV9vG6t8Jv7jQz4Z5J1QK', + session + } + }) + expect(response.status).toBe(200) + const remainingMessages = await clientServer.matrixDb.get( + 'device_inbox', + ['stream_id'], + { user_id: userId, device_id: deviceId } + ) + expect(remainingMessages).toHaveLength(0) + }) + }) + }) +}) diff --git a/packages/matrix-client-server/src/index.ts b/packages/matrix-client-server/src/index.ts new file mode 100644 index 00000000..5e58459c --- /dev/null +++ b/packages/matrix-client-server/src/index.ts @@ -0,0 +1,370 @@ +import configParser, { type ConfigDescription } from '@twake/config-parser' +import { type TwakeLogger } from '@twake/logger' +import fs from 'fs' +import defaultConfig from './config.json' +import { + type clientDbCollections, + type ClientServerDb, + type Config +} from './types' +import { type Request, type Response } from 'express' + +// Internal libraries +import MatrixDBmodified from './matrixDb' +import MatrixIdentityServer from '@twake/matrix-identity-server' +import UiAuthenticate, { + type UiAuthFunction +} from './utils/userInteractiveAuthentication' +import { errMsg, send, type expressAppHandler } from '@twake/utils' +import Authenticate from './utils/authenticate' + +// Endpoints +import { + getProfile, + getAvatarUrl, + getDisplayname +} from './user_data/profiles/getProfiles' +import { + changeAvatarUrl, + changeDisplayname +} from './user_data/profiles/changeProfiles' +import whoami from './account/whoami' +import whois from './admin/whois' +import getAccountData from './user/account_data/getAccountData' +import putAccountData from './user/account_data/putAccountData' +import getRoomAccountData from './user/rooms/getRoomAccountData' +import putRoomAccountData from './user/rooms/putRoomAccountData' +import register from './register' +import { getDevices, getDeviceInfo } from './devices/getDevices' +import { changeDeviceName } from './devices/changeDevices' +import GetEventId from './rooms/roomId/getEventId' +import GetJoinedMembers from './rooms/roomId/getJoinedMembers' +import { + getUserRoomTags, + addUserRoomTag, + removeUserRoomTag +} from './rooms/room_information/room_tags' +import { getJoinedRooms } from './rooms/room_information/get_joined_rooms' +import { + getRoomVisibility, + setRoomVisibility +} from './rooms/room_information/room_visibilty' +import { getRoomAliases } from './rooms/room_information/room_aliases' +import RequestTokenPasswordEmail from './account/password/email/requestToken' +import RequestTokenPasswordMsisdn from './account/password/msisdn/requestToken' +import RequestTokenEmail from './register/email/requestToken' +import RequestTokenMsisdn from './register/msisdn/requestToken' +import SubmitTokenEmail from './register/email/submitToken' +import getTimestampToEvent from './rooms/roomId/getTimestampToEvent' +import getStatus from './presence/getStatus' +import putStatus from './presence/putStatus' +import getLogin from './login/getLogin' +import add from './account/3pid/add' +import PostFilter from './user/filter/postFilter' +import GetFilter from './user/filter/getFilter' +import bind from './account/3pid/bind' +import refresh from './refresh' +import openIdRequestToken from './user/openid/requestToken' +import available from './register/available' +import getRoomState from './rooms/roomId/getState' +import getRoomStateEvent, { + getRoomStateEventNoStatekey +} from './rooms/roomId/getStateEvent' +import getCapabilities from './capabilities/getCapabilities' +import getVersions from './versions' +import passwordReset from './account/password' +import delete3pidHandler from './account/3pid/delete' +import userSearch from './user_data/user_directory/search' +import deactivate from './account/deactivate' +import deleteDevicesHandler from './delete_devices' +import deleteDevice from './devices/deleteDevice' + +// const tables = {} // Add tables declaration here to add new tables to this.db + +export default class MatrixClientServer extends MatrixIdentityServer { + api: { + get: Record + post: Record + put: Record + delete: Record + } + + matrixDb: MatrixDBmodified + declare conf: Config + declare db: ClientServerDb + private _uiauthenticate!: UiAuthFunction + + set uiauthenticate(uiauthenticate: UiAuthFunction) { + this._uiauthenticate = (req, res, allowedFlows, description, obj, cb) => { + this.rateLimiter(req as Request, res as Response, () => { + uiauthenticate(req, res, allowedFlows, description, obj, cb) + }) + } + } + + get uiauthenticate(): UiAuthFunction { + return this._uiauthenticate + } + + constructor( + conf?: Partial, + confDesc?: ConfigDescription, + logger?: TwakeLogger + ) { + if (confDesc == null) confDesc = defaultConfig + const serverConf = configParser( + confDesc, + /* istanbul ignore next */ + fs.existsSync('/etc/twake/client-server.conf') + ? '/etc/twake/client-server.conf' + : process.env.TWAKE_CLIENT_SERVER_CONF != null + ? process.env.TWAKE_CLIENT_SERVER_CONF + : conf != null + ? conf + : undefined + ) as Config + super(serverConf, confDesc, logger) // Add tables in here if we add additional tables to this.db in the tables variable above + this.api = { get: {}, post: {}, put: {}, delete: {} } + this.matrixDb = new MatrixDBmodified(serverConf, this.logger) + this.uiauthenticate = UiAuthenticate(this.matrixDb, serverConf, this.logger) + this.authenticate = Authenticate(this.matrixDb, this.logger, this.conf) + this.ready = new Promise((resolve, reject) => { + this.ready + .then(() => { + const badMethod: expressAppHandler = (req, res) => { + send(res, 405, errMsg('unrecognized')) + } + this.api.get = { + '/_matrix/client/v3/account/whoami': whoami(this), + '/_matrix/client/v3/admin/whois': whois(this), + '/_matrix/client/v3/register': badMethod, + '/_matrix/client/v3/profile/:userId': getProfile(this), + '/_matrix/client/v3/profile/:userId/avatar_url': getAvatarUrl(this), + '/_matrix/client/v3/profile/:userId/displayname': + getDisplayname(this), + '/_matrix/client/v3/user/:userId/account_data/:type': + getAccountData(this), + '/_matrix/client/v3/user/:userId/rooms/:roomId/account_data/:type': + getRoomAccountData(this), + '/_matrix/client/v3/devices': getDevices(this), + '/_matrix/client/v3/devices/:deviceId': getDeviceInfo(this), + '/_matrix/client/v3/rooms/:roomId/event/:eventId': GetEventId(this), + '/_matrix/client/v3/rooms/:roomId/joined_members': + GetJoinedMembers(this), + '/_matrix/client/v3/user/:userId/rooms/:roomId/tags': + getUserRoomTags(this), + '/_matrix/client/v3/joined_rooms': getJoinedRooms(this), + '/_matrix/client/v3/directory/list/room/:roomId': + getRoomVisibility(this), + '/_matrix/client/v3/rooms/:roomId/aliases': getRoomAliases(this), + '/_matrix/client/v3/account/password/email/requestToken': badMethod, + '/_matrix/client/v3/account/password/msisdn/requestToken': + badMethod, + '/_matrix/client/v3/register/email/requestToken': badMethod, + '/_matrix/client/v3/register/msisdn/requestToken': badMethod, + '/_matrix/client/v3/account/3pid/email/requestToken ': badMethod, + '/_matrix/client/v3/account/3pid/msisdn/requestToken ': badMethod, + '/_matrix/client/v3/register/email/submitToken': + SubmitTokenEmail(this), + '/_matrix/client/v3/rooms/:roomId/timestamp_to_event': + getTimestampToEvent(this), + '/_matrix/client/v3/presence/:userId/status': getStatus(this), + '/_matrix/client/v3/login': getLogin(this), + '/_matrix/client/v3/account/3pid/bind': badMethod, + '/_matrix/client/v3/account/3pid/add': badMethod, + '/_matrix/client/v3/refresh': badMethod, + '/_matrix/client/v3/user/:userId/openid/request_token': badMethod, + '/_matrix/client/v3/register/available': available(this), + '/_matrix/client/v3/user/:userId/filter': badMethod, + '/_matrix/client/v3/user/:userId/filter/:filterId': GetFilter(this), + '/_matrix/client/v3/rooms/:roomId/state': getRoomState(this), + '/_matrix/client/v3/rooms/:roomId/state/:eventType/:stateKey': + getRoomStateEvent(this), + '/_matrix/client/v3/rooms/:roomId/state/:eventType': + getRoomStateEventNoStatekey(this), + '/_matrix/client/v3/capabilities': getCapabilities(this), + '/_matrix/client/versions': getVersions, + '/_matrix/client/v3/account/password': badMethod, + '/_matrix/client/v3/account/3pid/delete': badMethod, + '/_matrix/client/v3/user_directory/search': badMethod, + '/_matrix/client/v3/account/deactivate': badMethod, + '/_matrix/client/v3/delete_devices': badMethod + } + this.api.post = { + '/_matrix/client/v3/account/whoami': badMethod, + '/_matrix/client/v3/admin/whois': badMethod, + '/_matrix/client/v3/register': register(this), + '/_matrix/client/v3/profile/:userId': badMethod, + '/_matrix/client/v3/profile/:userId/avatar_url': badMethod, + '/_matrix/client/v3/profile/:userId/displayname': badMethod, + '/_matrix/client/v3/user/:userId/account_data/:type': badMethod, + '/_matrix/client/v3/user/:userId/rooms/:roomId/account_data/:type': + badMethod, + '/_matrix/client/v3/devices': badMethod, + '/_matrix/client/v3/devices/:deviceId': badMethod, + '/_matrix/client/v3/rooms/:roomId/event/:eventId': badMethod, + '/_matrix/client/v3/rooms/:roomId/joined_members': badMethod, + '/_matrix/client/v3/user/:userId/rooms/:roomId/tags': badMethod, + '/_matrix/client/v3/joined_rooms': badMethod, + '/_matrix/client/v3/directory/list/room/:roomId': badMethod, + '/_matrix/client/v3/rooms/:roomId/aliases': badMethod, + '/_matrix/client/v3/account/password/email/requestToken': + RequestTokenPasswordEmail(this), + '/_matrix/client/v3/account/password/msisdn/requestToken': + RequestTokenPasswordMsisdn(this), + '/_matrix/client/v3/register/email/requestToken': + RequestTokenEmail(this), + '/_matrix/client/v3/register/msisdn/requestToken': + RequestTokenMsisdn(this), + '/_matrix/client/v3/account/3pid/email/requestToken': + RequestTokenEmail(this), + '/_matrix/client/v3/account/3pid/msisdn/requestToken': + RequestTokenMsisdn(this), + '/_matrix/client/v3/register/email/submitToken': + SubmitTokenEmail(this), + '/_matrix/client/v3/rooms/:roomId/timestamp_to_event': badMethod, + '/_matrix/client/v3/user/:roomId/timestamp_to_event': badMethod, + '/_matrix/client/v3/presence/:userId/status': badMethod, + '/_matrix/client/v3/login': badMethod, + '/_matrix/client/v3/account/3pid/bind': bind(this), + '/_matrix/client/v3/account/3pid/add': add(this), + '/_matrix/client/v3/refresh': refresh(this), + '/_matrix/client/v3/user/:userId/openid/request_token': + openIdRequestToken(this), + '/_matrix/client/v3/register/available': badMethod, + '/_matrix/client/v3/user/:userId/filter': PostFilter(this), + '/_matrix/client/v3/user/:userId/filter/:filterId': badMethod, + '/_matrix/client/v3/rooms/:roomId/state': badMethod, + '/_matrix/client/v3/rooms/:roomId/state/:eventType/:stateKey': + badMethod, + '/_matrix/client/v3/rooms/:roomId/state/:eventType': badMethod, + '/_matrix/client/v3/capabilities': badMethod, + '/_matrix/client/versions': badMethod, + '/_matrix/client/v3/account/password': passwordReset(this), + '/_matrix/client/v3/account/3pid/delete': delete3pidHandler(this), + '/_matrix/client/v3/user_directory/search': userSearch(this), + '/_matrix/client/v3/account/deactivate': deactivate(this), + '/_matrix/client/v3/delete_devices': deleteDevicesHandler(this) + } + this.api.put = { + '/_matrix/client/v3/account/whoami': badMethod, + '/_matrix/client/v3/admin/whois': badMethod, + '/_matrix/client/v3/register': badMethod, + '/_matrix/client/v3/profile/:userId': badMethod, + '/_matrix/client/v3/profile/:userId/avatar_url': + changeAvatarUrl(this), + '/_matrix/client/v3/profile/:userId/displayname': + changeDisplayname(this), + '/_matrix/client/v3/user/:userId/account_data/:type': + putAccountData(this), + '/_matrix/client/v3/user/:userId/rooms/:roomId/account_data/:type': + putRoomAccountData(this), + '/_matrix/client/v3/devices': badMethod, + '/_matrix/client/v3/devices/:deviceId': changeDeviceName(this), + '/_matrix/client/v3/rooms/:roomId/event/:eventId': badMethod, + '/_matrix/client/v3/rooms/:roomId/joined_members': badMethod, + '/_matrix/client/v3/user/:userId/rooms/:roomId/tags': badMethod, + '/_matrix/client/v3/user/:userId/rooms/:roomId/tags/:tag': + addUserRoomTag(this), + '/_matrix/client/v3/joined_rooms': badMethod, + '/_matrix/client/v3/directory/list/room/:roomId': + setRoomVisibility(this), + '/_matrix/client/v3/rooms/:roomId/aliases': badMethod, + '/_matrix/client/v3/account/password/email/requestToken': badMethod, + '/_matrix/client/v3/account/password/msisdn/requestToken': + badMethod, + '/_matrix/client/v3/register/email/requestToken': badMethod, + '/_matrix/client/v3/register/msisdn/requestToken': badMethod, + '/_matrix/client/v3/account/3pid/email/requestToken ': badMethod, + '/_matrix/client/v3/account/3pid/msisdn/requestToken ': badMethod, + '/_matrix/client/v3/register/email/submitToken': badMethod, + '/_matrix/client/v3/rooms/:roomId/timestamp_to_event': badMethod, + '/_matrix/client/v3/user/:roomId/timestamp_to_event': badMethod, + '/_matrix/client/v3/presence/:userId/status': putStatus(this), + '/_matrix/client/v3/login': badMethod, + '/_matrix/client/v3/account/3pid/bind': badMethod, + '/_matrix/client/v3/account/3pid/add': badMethod, + '/_matrix/client/v3/refresh': badMethod, + '/_matrix/client/v3/user/:userId/openid/request_token': badMethod, + '/_matrix/client/v3/register/available': badMethod, + '/_matrix/client/v3/user/:userId/filter': badMethod, + '/_matrix/client/v3/user/:userId/filter/:filterId': badMethod, + '/_matrix/client/v3/rooms/:roomId/state': badMethod, + '/_matrix/client/v3/rooms/:roomId/state/:eventType/:stateKey': + badMethod, + '/_matrix/client/v3/rooms/:roomId/state/:eventType': badMethod, + '/_matrix/client/v3/capabilities': badMethod, + '/_matrix/client/versions': badMethod, + '/_matrix/client/v3/account/password': badMethod, + '/_matrix/client/v3/account/3pid/delete': badMethod, + '/_matrix/client/v3/user_directory/search': badMethod, + '/_matrix/client/v3/account/deactivate': badMethod, + '/_matrix/client/v3/delete_devices': badMethod + } + this.api.delete = { + '/_matrix/client/v3/account/whoami': badMethod, + '/_matrix/client/v3/admin/whois': badMethod, + '/_matrix/client/v3/register': badMethod, + '/_matrix/client/v3/profile/:userId': badMethod, + '/_matrix/client/v3/profile/:userId/avatar_url': badMethod, + '/_matrix/client/v3/profile/:userId/displayname': badMethod, + '/_matrix/client/v3/devices': badMethod, + '/_matrix/client/v3/devices/:deviceId': deleteDevice(this), + '/_matrix/client/v3/user/:userId/rooms/:roomId/tags': badMethod, + '/_matrix/client/v3/user/:userId/rooms/:roomId/tags/:tag': + removeUserRoomTag(this), + '/_matrix/client/v3/joined_rooms': badMethod, + '/_matrix/client/v3/directory/list/room/:roomId': badMethod, + '/_matrix/client/v3/rooms/:roomId/aliases': badMethod, + '/_matrix/client/v3/account/password/email/requestToken': badMethod, + '/_matrix/client/v3/account/password/msisdn/requestToken': + badMethod, + '/_matrix/client/v3/register/email/requestToken': badMethod, + '/_matrix/client/v3/register/msisdn/requestToken': badMethod, + '/_matrix/client/v3/account/3pid/email/requestToken ': badMethod, + '/_matrix/client/v3/account/3pid/msisdn/requestToken ': badMethod, + '/_matrix/client/v3/register/email/submitToken': badMethod, + '/_matrix/client/v3/rooms/:roomId/timestamp_to_event': badMethod, + '/_matrix/client/v3/presence/:userId/status': badMethod, + '/_matrix/client/v3/login': badMethod, + '/_matrix/client/v3/account/3pid/bind': badMethod, + '/_matrix/client/v3/account/3pid/add': badMethod, + '/_matrix/client/v3/refresh': badMethod, + '/_matrix/client/v3/user/:userId/openid/request_token': badMethod, + '/_matrix/client/v3/register/available': badMethod, + '/_matrix/client/v3/user/:userId/filter': badMethod, + '/_matrix/client/v3/user/:userId/filter/:filterId': badMethod, + '/_matrix/client/v3/rooms/:roomId/state': badMethod, + '/_matrix/client/v3/rooms/:roomId/state/:eventType/:stateKey': + badMethod, + '/_matrix/client/v3/rooms/:roomId/state/:eventType': badMethod, + '/_matrix/client/v3/capabilities': badMethod, + '/_matrix/client/versions': badMethod, + '/_matrix/client/v3/account/password': badMethod, + '/_matrix/client/v3/account/3pid/delete': badMethod, + '/_matrix/client/v3/user_directory/search': badMethod, + '/_matrix/client/v3/account/deactivate': badMethod, + '/_matrix/client/v3/delete_devices': badMethod + } + resolve(true) + }) + /* istanbul ignore next */ + .catch(reject) + }) + } + + // Class methods that determiens if a user is hosted in the server or in a remote one + isMine(userId: string): boolean { + const parts = userId.split(':') + return parts[1] === this.conf.server_name + } + + cleanJobs(): void { + clearTimeout(this.db?.cleanJob) + this.cronTasks?.stop() + this.db?.close() + this.userDB.close() + this.logger.close() + this.matrixDb.close() + } +} diff --git a/packages/matrix-client-server/src/login/getLogin.ts b/packages/matrix-client-server/src/login/getLogin.ts new file mode 100644 index 00000000..005b6582 --- /dev/null +++ b/packages/matrix-client-server/src/login/getLogin.ts @@ -0,0 +1,11 @@ +import { type expressAppHandler, send } from '@twake/utils' +import type MatrixClientServer from '..' + +// TODO : Modify default value of sso login in config +const getLogin = (clientServer: MatrixClientServer): expressAppHandler => { + return (req, res) => { + send(res, 200, clientServer.conf.login_flows, clientServer.logger) + } +} + +export default getLogin diff --git a/packages/matrix-client-server/src/login/index.test.ts b/packages/matrix-client-server/src/login/index.test.ts new file mode 100644 index 00000000..49fae1ec --- /dev/null +++ b/packages/matrix-client-server/src/login/index.test.ts @@ -0,0 +1,118 @@ +import fs from 'fs' +import request from 'supertest' +import express from 'express' +import ClientServer from '../index' +import { buildMatrixDb, buildUserDB } from '../__testData__/buildUserDB' +import { type Config } from '../types' +import defaultConfig from '../__testData__/registerConf.json' +import { getLogger, type TwakeLogger } from '@twake/logger' + +jest.mock('node-fetch', () => jest.fn()) + +let conf: Config +let clientServer: ClientServer +let app: express.Application + +const logger: TwakeLogger = getLogger() + +beforeAll((done) => { + // @ts-expect-error TS doesn't understand that the config is valid + conf = { + ...defaultConfig, + cron_service: false, + database_engine: 'sqlite', + base_url: 'http://example.com/', + userdb_engine: 'sqlite', + matrix_database_engine: 'sqlite', + matrix_database_host: './src/__testData__/testMatrixLogin.db', + userdb_host: './src/__testData__/testLogin.db', + database_host: './src/__testData__/testLogin.db' + } + if (process.env.TEST_PG === 'yes') { + conf.database_engine = 'pg' + conf.userdb_engine = 'pg' + conf.database_host = process.env.PG_HOST ?? 'localhost' + conf.database_user = process.env.PG_USER ?? 'twake' + conf.database_password = process.env.PG_PASSWORD ?? 'twake' + conf.database_name = process.env.PG_DATABASE ?? 'test' + } + buildUserDB(conf) + .then(() => { + buildMatrixDb(conf) + .then(() => { + done() + }) + .catch((e) => { + logger.error('Error while building matrix db:', e) + done(e) + }) + }) + .catch((e) => { + logger.error('Error while building user db:', e) + done(e) + }) +}) + +afterAll(() => { + fs.unlinkSync('src/__testData__/testLogin.db') + fs.unlinkSync('src/__testData__/testMatrixLogin.db') +}) + +describe('Use configuration file', () => { + beforeAll((done) => { + clientServer = new ClientServer(conf) + app = express() + clientServer.ready + .then(() => { + Object.keys(clientServer.api.get).forEach((k) => { + app.get(k, clientServer.api.get[k]) + }) + Object.keys(clientServer.api.post).forEach((k) => { + app.post(k, clientServer.api.post[k]) + }) + Object.keys(clientServer.api.put).forEach((k) => { + app.put(k, clientServer.api.put[k]) + }) + Object.keys(clientServer.api.delete).forEach((k) => { + app.delete(k, clientServer.api.delete[k]) + }) + done() + }) + .catch((e) => { + done(e) + }) + }) + + afterAll(() => { + clientServer.cleanJobs() + }) + describe('/_matrix/client/v3/login', () => { + it('should return the login flows', async () => { + const response = await request(app).get('/_matrix/client/v3/login') + expect(response.status).toBe(200) + expect(response.body).toHaveProperty('flows') + expect(response.body.flows).toEqual([ + { type: 'm.login.password' }, + { get_login_token: true, type: 'm.login.token' } + ]) + }) + }) + // let validToken: string + // describe('Endpoints with authentication', () => { + // beforeAll(async () => { + // validToken = randomString(64) + // try { + // await clientServer.matrixDb.insert('user_ips', { + // user_id: '@testuser:example.com', + // device_id: 'testdevice', + // access_token: validToken, + // ip: '127.0.0.1', + // user_agent: 'curl/7.31.0-DEV', + // last_seen: 1411996332123 + // }) + // } catch (e) { + // logger.error('Error creating tokens for authentification', e) + // } + // }) + // }) +}) diff --git a/packages/matrix-client-server/src/matrixDb/index.test.ts b/packages/matrix-client-server/src/matrixDb/index.test.ts new file mode 100644 index 00000000..3feb0f04 --- /dev/null +++ b/packages/matrix-client-server/src/matrixDb/index.test.ts @@ -0,0 +1,364 @@ +/* eslint-disable @typescript-eslint/promise-function-async */ +import MatrixDBmodified from './index' +import { type TwakeLogger, getLogger } from '@twake/logger' +import { type Config, type DbGetResult } from '../types' +import DefaultConfig from '../__testData__/registerConf.json' +import fs from 'fs' +import { randomString } from '@twake/crypto' +import { buildMatrixDb } from '../__testData__/buildUserDB' +import { parseQuerySqlite, parseWordsWithRegex } from '../matrixDb/sql/sqlite' + +jest.mock('node-fetch', () => jest.fn()) + +const logger: TwakeLogger = getLogger() + +// @ts-expect-error TS doesn't understand that the config is valid +const baseConf: Config = { + ...DefaultConfig, + database_engine: 'sqlite', + userdb_engine: 'sqlite', + cron_service: false, + matrix_database_engine: 'sqlite', + matrix_database_host: './src/__testData__/matrixTestdb.db', + sms_folder: './src/__testData__/sms' +} + +describe('Testing auxiliary functions', () => { + describe('parseQuerySqlite', () => { + it('should create a query string with prefix matching', () => { + const result = parseQuerySqlite('test search') + expect(result).toBe('(test* OR test) & (search* OR search)') + }) + + it('should handle mixed case and accented characters', () => { + const result = parseQuerySqlite('TeSt Search') + expect(result).toBe('(test* OR test) & (search* OR search)') + }) + + it('should return an empty string for an empty input', () => { + const result = parseQuerySqlite('') + expect(result).toBe('') + }) + + it('should ignore special characters and only use word-like characters', () => { + const result = parseQuerySqlite('test@# search!') + expect(result).toBe('(test* OR test) & (search* OR search)') + }) + }) + + describe('parseWordsWithRegex', () => { + it('should return an array of words', () => { + const result = parseWordsWithRegex('this is a test') + expect(result).toEqual(['this', 'is', 'a', 'test']) + }) + + it('should return an empty array for a string with no word characters', () => { + const result = parseWordsWithRegex('!!!') + expect(result).toEqual([]) + }) + + it('should handle mixed alphanumeric and special characters', () => { + const result = parseWordsWithRegex('test-search123, more#words') + expect(result).toEqual(['test-search123', 'more', 'words']) + }) + + it('should handle an empty string', () => { + const result = parseWordsWithRegex('') + expect(result).toEqual([]) + }) + }) +}) + +describe('Matrix DB', () => { + let matrixDb: MatrixDBmodified + + beforeAll((done) => { + buildMatrixDb(baseConf) + .then(() => { + done() + }) + .catch((e) => { + logger.error('Error while building matrix db:', e) + done(e) + }) + }) + + afterAll(() => { + fs.unlinkSync('./src/__testData__/matrixTestdb.db') + logger.close() + }) + + it('should have SQLite database initialized', (done) => { + matrixDb = new MatrixDBmodified(baseConf, logger) + matrixDb.ready + .then(() => { + const userId = randomString(64) + matrixDb + .insert('profiles', { user_id: userId }) + .then(() => { + matrixDb + .get('profiles', ['user_id', 'displayname'], { user_id: userId }) + .then((rows) => { + expect(rows.length).toBe(1) + expect(rows[0].user_id).toEqual(userId) + expect(rows[0].displayname).toEqual(null) + matrixDb.close() + done() + }) + .catch((e) => done(e)) + }) + .catch((e) => done(e)) + }) + .catch((e) => done(e)) + }) + + it('should return entry on insert', (done) => { + matrixDb = new MatrixDBmodified(baseConf, logger) + matrixDb.ready + .then(() => { + const userId = randomString(64) + matrixDb + .insert('profiles', { user_id: userId }) + .then((rows) => { + expect(rows.length).toBe(1) + expect(rows[0].user_id).toEqual(userId) + expect(rows[0].displayname).toEqual(null) + matrixDb.close() + done() + }) + .catch((e) => done(e)) + }) + .catch((e) => done(e)) + }) + + it('should update', (done) => { + matrixDb = new MatrixDBmodified(baseConf, logger) + matrixDb.ready + .then(() => { + const userId = randomString(64) + matrixDb + .insert('profiles', { user_id: userId, displayname: 'test' }) + .then(() => { + matrixDb + .updateWithConditions( + 'profiles', + { displayname: 'testUpdated' }, + [{ field: 'user_id', value: userId }] + ) + .then(() => { + matrixDb + .get('profiles', ['user_id', 'displayname'], { + user_id: userId + }) + .then((rows) => { + expect(rows[0].displayname).toEqual('testUpdated') + matrixDb.close() + done() + }) + .catch((e) => done(e)) + }) + .catch((e) => done(e)) + }) + .catch((e) => done(e)) + }) + .catch((e) => done(e)) + }) + + it('should return entry on update', (done) => { + matrixDb = new MatrixDBmodified(baseConf, logger) + matrixDb.ready + .then(() => { + const userId = randomString(64) + matrixDb + .insert('profiles', { user_id: userId, displayname: 'test' }) + .then(() => { + matrixDb + .updateWithConditions( + 'profiles', + { displayname: 'testUpdated' }, + [{ field: 'user_id', value: userId }] + ) + .then((rows) => { + expect(rows.length).toBe(1) + expect(rows[0].user_id).toEqual(userId) + expect(rows[0].displayname).toEqual('testUpdated') + matrixDb.close() + done() + }) + .catch((e) => done(e)) + }) + .catch((e) => done(e)) + }) + .catch((e) => done(e)) + }) + + it('should delete records matching condition', (done) => { + matrixDb = new MatrixDBmodified(baseConf, logger) + matrixDb.ready + .then(() => { + const idsNumber = 8 + const insertsPromises: Array> = [] + for (let index = 0; index < idsNumber; index++) { + insertsPromises[index] = matrixDb.insert('users', { + name: `user${index}`, + password_hash: `hash${index}`, + creation_ts: Date.now(), + admin: 0, + is_guest: 0, + deactivated: 0 + }) + } + + Promise.all(insertsPromises) + .then(() => { + matrixDb + .deleteEqual('users', 'name', 'user0') + .then(() => { + matrixDb + .getAll('users', ['name', 'password_hash']) + .then((rows) => { + expect(rows.length).toBe(idsNumber - 1) + rows.forEach((row) => { + expect(row.name).not.toEqual('user0') + expect(row.password_hash).not.toEqual('hash0') + }) + matrixDb.close() + done() + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + describe('getMaxStreamId', () => { + it('should return the maximum stream ID within the given range', (done) => { + matrixDb = new MatrixDBmodified(baseConf, logger) + matrixDb.ready + .then(() => { + const userId = 'user1' + const deviceId = 'device1' + + const insertsPromises: Array> = [] + for (let streamId = 1; streamId <= 25; streamId++) { + insertsPromises.push( + matrixDb.insert('device_inbox', { + user_id: userId, + device_id: deviceId, + stream_id: streamId, + message_json: JSON.stringify({ content: `Message ${streamId}` }) + }) + ) + } + + return Promise.all(insertsPromises) + }) + .then(() => { + return matrixDb.getMaxStreamId('user1', 'device1', 10, 20, 10) + }) + .then((maxStreamId) => { + expect(maxStreamId).toBe(20) + matrixDb.close() + }) + .then(() => done()) + .catch(done) + }) + + it('should return an empty array if no stream IDs are found', (done) => { + matrixDb = new MatrixDBmodified(baseConf, logger) + matrixDb.ready + .then(() => { + const userId = 'user2' + const deviceId = 'device2' + + return matrixDb.insert('device_inbox', { + user_id: userId, + device_id: deviceId, + stream_id: 1, + message_json: JSON.stringify({ content: 'Message 1' }) + }) + }) + .then(() => { + return matrixDb.getMaxStreamId('user2', 'device2', 50, 100, 10) + }) + .then((maxStreamId) => { + expect(maxStreamId).toBe(null) + matrixDb.close() + }) + .then(() => done()) + .catch(done) + }) + + it('should handle cases where limit is 1', (done) => { + matrixDb = new MatrixDBmodified(baseConf, logger) + matrixDb.ready + .then(() => { + const userId = 'user3' + const deviceId = 'device3' + + const insertsPromises: Array> = [] + for (let streamId = 1; streamId <= 15; streamId++) { + insertsPromises.push( + matrixDb.insert('device_inbox', { + user_id: userId, + device_id: deviceId, + stream_id: streamId, + message_json: JSON.stringify({ content: `Message ${streamId}` }) + }) + ) + } + + return Promise.all(insertsPromises) + }) + .then(() => { + return matrixDb.getMaxStreamId('user3', 'device3', 5, 15, 1) + }) + .then((maxStreamId) => { + expect(maxStreamId).toBe(6) + matrixDb.close() + }) + .then(() => done()) + .catch(done) + }) + + it('should handle cases with special characters in user_id or device_id', (done) => { + matrixDb = new MatrixDBmodified(baseConf, logger) + matrixDb.ready + .then(() => { + const userId = 'user@domain.com' + const deviceId = 'device#1' + + const insertsPromises: Array> = [] + for (let streamId = 1; streamId <= 10; streamId++) { + insertsPromises.push( + matrixDb.insert('device_inbox', { + user_id: userId, + device_id: deviceId, + stream_id: streamId, + message_json: JSON.stringify({ content: `Message ${streamId}` }) + }) + ) + } + + return Promise.all(insertsPromises) + }) + .then(() => { + return matrixDb.getMaxStreamId( + 'user@domain.com', + 'device#1', + 1, + 10, + 10 + ) + }) + .then((maxStreamId) => { + expect(maxStreamId).toBe(10) + matrixDb.close() + }) + .then(() => done()) + .catch(done) + }) + }) +}) diff --git a/packages/matrix-client-server/src/matrixDb/index.ts b/packages/matrix-client-server/src/matrixDb/index.ts new file mode 100644 index 00000000..a4de2873 --- /dev/null +++ b/packages/matrix-client-server/src/matrixDb/index.ts @@ -0,0 +1,524 @@ +import { type TwakeLogger } from '@twake/logger' +import { type Config, type DbGetResult } from '../types' +import MatrixDBPg from './sql/pg' +import MatrixDBSQLite from './sql/sqlite' +import { randomString } from '@twake/crypto' +import { epoch } from '@twake/utils' + +export type Collections = + | 'users' + | 'profiles' + | 'destinations' + | 'events' + | 'state_events' + | 'current_state_events' + | 'event_forward_extremities' + | 'event_backward_extremities' + | 'rooms' + | 'room_memberships' + | 'room_aliases' + | 'room_stats_state' + | 'room_depth' + | 'room_tags' + | 'room_account_data' + | 'local_current_membership' + | 'server_signature_keys' + | 'rejections' + | 'local_media_repository' + | 'redactions' + | 'user_ips' + | 'registration_tokens' + | 'account_data' + | 'devices' + | 'threepid_validation_token' + | 'threepid_validation_session' + | 'user_threepids' + | 'presence' + | 'user_threepid_id_server' + | 'access_tokens' + | 'refresh_tokens' + | 'open_id_tokens' + | 'user_filters' + | 'ui_auth_sessions' + | 'ui_auth_sessions_ips' + | 'ui_auth_sessions_credentials' + | 'users_in_public_rooms' + | 'users_who_share_private_rooms' + | 'user_directory' + | 'user_directory_search' + | 'pushers' + | 'deleted_pushers' + | 'erased_users' + | 'event_expiry' + | 'account_validity' + | 'ignored_users' + | 'push_rules' + | 'push_rules_enable' + | 'push_rules_stream' + | 'e2e_room_keys' + | 'e2e_room_keys_versions' + | 'e2e_device_keys_json' + | 'e2e_one_time_keys_json' + | 'e2e_fallback_keys_json' + | 'event_json' + | 'device_auth_providers' + | 'dehydrated_devices' + | 'device_inbox' + +type sqlComparaisonOperator = '=' | '!=' | '>' | '<' | '>=' | '<=' | '<>' +interface ISQLCondition { + field: string + operator: sqlComparaisonOperator + value: string | number +} + +type Get = ( + table: Collections, + fields: string[], + filterFields: Record>, + order?: string +) => Promise +type Get2 = ( + table: Collections, + fields: string[], + filterFields1: Record>, + filterFields2: Record>, + order?: string +) => Promise +type GetJoin = ( + tables: Collections[], + fields: string[], + filterFields: Record>, + joinFields: Record, + order?: string +) => Promise +type GetMinMax = ( + table: Collections, + targetField: string, + fields: string[], + filterFields: Record>, + order?: string +) => Promise +type GetMinMax2 = ( + table: Collections, + targetField: string, + fields: string[], + filterFields1: Record>, + filterFields2: Record>, + order?: string +) => Promise +type GetMinMaxJoin2 = ( + tables: Collections[], + targetField: string, + fields: string[], + filterFields1: Record>, + filterFields2: Record>, + joinFields: Record, + order?: string +) => Promise + +type GetAll = (table: Collections, fields: string[]) => Promise + +type Insert = ( + table: Collections, + values: Record +) => Promise +type updateWithConditions = ( + table: Collections, + values: Record, + conditions: Array<{ field: string; value: string | number }> +) => Promise +type DeleteEqual = ( + table: Collections, + field: string, + value: string | number +) => Promise +type DeleteWhere = ( + table: Collections, + conditions: ISQLCondition | ISQLCondition[] +) => Promise +type SearchUserDirectory = ( + userId: string, + searchTerm: string, + limit: number, + searchAllUsers: boolean +) => Promise +type GetMaxStreamId = ( + userId: string, + deviceId: string, + lowerBoundStreamId: number, + upperBoundStreamId: number, + limit: number +) => Promise + +export interface MatrixDBmodifiedBackend { + ready: Promise + get: Get + getJoin: GetJoin + getWhereEqualOrDifferent: Get2 + getWhereEqualAndHigher: Get2 + getMaxWhereEqual: GetMinMax + getMaxWhereEqualAndLower: GetMinMax2 + getMinWhereEqualAndHigher: GetMinMax2 + getMaxWhereEqualAndLowerJoin: GetMinMaxJoin2 + getAll: GetAll + insert: Insert + deleteEqual: DeleteEqual + deleteWhere: DeleteWhere + updateWithConditions: updateWithConditions + getMaxStreamId: GetMaxStreamId // This function is only used in the delete_devices function + // The following functions are specific to the user_directory module + searchUserDirectory: SearchUserDirectory + close: () => void +} + +class MatrixDBmodified implements MatrixDBmodifiedBackend { + ready: Promise + db: MatrixDBmodifiedBackend + + constructor(conf: Config, private readonly logger: TwakeLogger) { + let Module + /* istanbul ignore next */ + switch (conf.matrix_database_engine) { + case 'sqlite': { + Module = MatrixDBSQLite + break + } + case 'pg': { + Module = MatrixDBPg + break + } + default: { + throw new Error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Unsupported matrix-database type ${conf.matrix_database_engine}` + ) + } + } + this.db = new Module(conf, this.logger) + this.ready = new Promise((resolve, reject) => { + this.db.ready + .then(() => { + resolve() + }) + /* istanbul ignore next */ + .catch(reject) + }) + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + getAll(table: Collections, fields: string[]) { + return this.db.getAll(table, fields) + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + get( + table: Collections, + fields: string[], + filterFields: Record>, + order?: string + ) { + return this.db.get(table, fields, filterFields, order) + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + getJoin( + table: Collections[], + fields: string[], + filterFields: Record>, + joinFields: Record, + order?: string + ) { + return this.db.getJoin(table, fields, filterFields, joinFields, order) + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + getWhereEqualOrDifferent( + table: Collections, + fields: string[], + filterFields1: Record>, + filterFields2: Record>, + order?: string + ) { + return this.db.getWhereEqualOrDifferent( + table, + fields, + filterFields1, + filterFields2, + order + ) + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + getWhereEqualAndHigher( + table: Collections, + fields: string[], + filterFields1: Record>, + filterFields2: Record>, + order?: string + ) { + return this.db.getWhereEqualAndHigher( + table, + fields, + filterFields1, + filterFields2, + order + ) + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + getMaxWhereEqual( + table: Collections, + targetField: string, + fields: string[], + filterFields: Record>, + order?: string + ) { + return this.db.getMaxWhereEqual( + table, + targetField, + fields, + filterFields, + order + ) + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + getMaxWhereEqualAndLower( + table: Collections, + targetField: string, + fields: string[], + filterFields1: Record>, + filterFields2: Record>, + order?: string + ) { + return this.db.getMaxWhereEqualAndLower( + table, + targetField, + fields, + filterFields1, + filterFields2, + order + ) + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + getMinWhereEqualAndHigher( + table: Collections, + targetField: string, + fields: string[], + filterFields1: Record>, + filterFields2: Record>, + order?: string + ) { + return this.db.getMinWhereEqualAndHigher( + table, + targetField, + fields, + filterFields1, + filterFields2, + order + ) + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + getMaxWhereEqualAndLowerJoin( + tables: Collections[], + targetField: string, + fields: string[], + filterFields1: Record>, + filterFields2: Record>, + joinFields: Record, + order?: string + ) { + return this.db.getMaxWhereEqualAndLowerJoin( + tables, + targetField, + fields, + filterFields1, + filterFields2, + joinFields, + order + ) + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + insert(table: Collections, values: Record) { + return this.db.insert(table, values) + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + deleteEqual(table: Collections, field: string, value: string | number) { + return this.db.deleteEqual(table, field, value) + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + deleteWhere(table: Collections, conditions: ISQLCondition | ISQLCondition[]) { + // Deletes from table where filters correspond to values + // Size of filters and values must be the same + return this.db.deleteWhere(table, conditions) + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + updateWithConditions( + table: Collections, + values: Record, + conditions: Array<{ field: string; value: string | number }> + ) { + return this.db.updateWithConditions(table, values, conditions) + } + + // eslint-disable-next-line @typescript-eslint/promise-function-async + createOneTimeToken( + sessionId: string, + expires?: number, + nextLink?: string + ): Promise { + /* istanbul ignore if */ + if (this.db == null) { + throw new Error('Wait for database to be ready') + } + const token = randomString(64) + // default: expires in 600 s + const expiresForDb = + epoch() + 1000 * (expires != null && expires > 0 ? expires : 600) + return new Promise((resolve, reject) => { + const insertData: Record = { + token, + expires: expiresForDb, + session_id: sessionId + } + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (nextLink) { + insertData.next_link = nextLink + } + + this.db + .insert('threepid_validation_token', insertData) + .then(() => { + resolve(token) + }) + .catch((err) => { + /* istanbul ignore next */ + this.logger.error('Failed to insert token', err) + /* istanbul ignore next */ + reject(err) + }) + }) + } + + // No difference in creation between a token and a one-time-token + // eslint-disable-next-line @typescript-eslint/promise-function-async + createToken(sessionId: string, expires?: number): Promise { + return this.createOneTimeToken(sessionId, expires) + } + + // eslint-disable-next-line @typescript-eslint/promise-function-async + verifyToken(token: string): Promise { + /* istanbul ignore if */ + if (this.db == null) { + throw new Error('Wait for database to be ready') + } + return new Promise((resolve, reject) => { + this.db + .get( + 'threepid_validation_token', + ['session_id', 'expires', 'next_link'], + { token } + ) + .then((rows) => { + /* istanbul ignore else */ + if (rows.length > 0 && (rows[0].expires as number) >= epoch()) { + this.db + .get('threepid_validation_session', ['client_secret'], { + session_id: rows[0].session_id + }) + .then((validationRows) => { + const body: any = {} + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (rows[0].next_link) { + body.next_link = rows[0].next_link + } + resolve({ + ...body, + session_id: rows[0].session_id, + client_secret: validationRows[0].client_secret + }) + }) + .catch((e) => { + // istanbul ignore next + reject(e) + }) + } else { + reject( + new Error( + 'Token expired' + (rows[0].expires as number).toString() + ) + ) + } + }) + .catch((e) => { + reject(e) + }) + }) + } + + // eslint-disable-next-line @typescript-eslint/promise-function-async + deleteToken(token: string): Promise { + /* istanbul ignore if */ + if (this.db == null) { + throw new Error('Wait for database to be ready') + } + return new Promise((resolve, reject) => { + this.db + .deleteEqual('threepid_validation_token', 'token', token) + .then(() => { + resolve() + }) + .catch((e) => { + /* istanbul ignore next */ + this.logger.info(`Token ${token} already deleted`, e) + /* istanbul ignore next */ + resolve() + }) + }) + } + + close(): void { + this.db.close() + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + getMaxStreamId( + userId: string, + deviceId: string, + lowerBoundStreamId: number, + upperBoundStreamId: number, + limit: number + ) { + return this.db.getMaxStreamId( + userId, + deviceId, + lowerBoundStreamId, + upperBoundStreamId, + limit + ) + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + searchUserDirectory( + userId: string, + searchTerm: string, + limit: number, + searchAllUsers: boolean + ) { + return this.db.searchUserDirectory( + userId, + searchTerm, + limit, + searchAllUsers + ) + } +} + +export default MatrixDBmodified diff --git a/packages/matrix-client-server/src/matrixDb/sql/pg.ts b/packages/matrix-client-server/src/matrixDb/sql/pg.ts new file mode 100644 index 00000000..9296e5c4 --- /dev/null +++ b/packages/matrix-client-server/src/matrixDb/sql/pg.ts @@ -0,0 +1,268 @@ +import { type TwakeLogger } from '@twake/logger' +import { type ClientConfig } from 'pg' +import { type Config, type DbGetResult } from '../../types' +import { type MatrixDBmodifiedBackend, type Collections } from '../' +import { Pg } from '@twake/matrix-identity-server' + +class MatrixDBPg extends Pg implements MatrixDBmodifiedBackend { + // eslint-disable-next-line @typescript-eslint/promise-function-async + createDatabases( + conf: Config, + tables: Record, + indexes: Partial>, + initializeValues: Partial< + Record>> + >, + logger: TwakeLogger + ): Promise { + if (this.db != null) return Promise.resolve() + return new Promise((resolve, reject) => { + import('pg') + .then((pg) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error + // @ts-ignore + if (pg.Database == null) pg = pg.default + if ( + conf.matrix_database_host == null || + conf.matrix_database_user == null || + conf.matrix_database_password == null || + conf.matrix_database_name == null + ) { + throw new Error( + 'database_name, database_user and database_password are required when using Postgres' + ) + } + const opts: ClientConfig = { + host: conf.matrix_database_host, + user: conf.matrix_database_user, + password: conf.matrix_database_password, + database: conf.matrix_database_name, + ssl: conf.matrix_database_ssl + } + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (conf.matrix_database_host.match(/^(.*):(\d+)/)) { + opts.host = RegExp.$1 + opts.port = parseInt(RegExp.$2) + } + try { + this.db = new pg.Pool(opts) + resolve() + } catch (e) { + logger.error('Unable to connect to Pg database') + reject(e) + } + }) + .catch((e) => { + logger.error('Unable to load pg module') + reject(e) + }) + }) + } + + // eslint-disable-next-line @typescript-eslint/promise-function-async + updateWithConditions( + table: Collections, + values: Record, + conditions: Array<{ field: string; value: string | number }> + ): Promise { + return new Promise((resolve, reject) => { + if (this.db == null) { + reject(new Error('Wait for database to be ready')) + return + } + + const names = Object.keys(values) + const vals = Object.values(values) + + // Add the values for the conditions to the vals array + conditions.forEach((condition) => { + vals.push(condition.value) + }) + + // Construct the SET clause for the update statement + const setClause = names.map((name, i) => `${name} = $${i + 1}`).join(', ') + + // Construct the WHERE clause for the conditions + const whereClause = conditions + .map((condition, i) => `${condition.field} = $${names.length + i + 1}`) + .join(' AND ') + + const query = `UPDATE ${table} SET ${setClause} WHERE ${whereClause} RETURNING *;` + + this.db.query( + query, + vals, + (err: Error, result: { rows: DbGetResult }) => { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (err) { + reject(err) + } else { + resolve(result.rows) + } + } + ) + }) + } + + // eslint-disable-next-line @typescript-eslint/promise-function-async + getMaxStreamId( + userId: string, + deviceId: string, + fromStreamId: number, + toStreamId: number, + limit: number + ): Promise { + return new Promise((resolve, reject) => { + if (this.db == null) { + reject(new Error('Wait for database to be ready')) + return + } + + const args = [userId, deviceId, fromStreamId, toStreamId, limit] + + const sql = ` + SELECT MAX(stream_id) AS max_stream_id FROM ( + SELECT stream_id FROM device_inbox + WHERE user_id = $1 AND device_id = $2 + AND $3 < stream_id AND stream_id <= $4 + ORDER BY stream_id + LIMIT $5 + ) AS d + ` + + this.db.query( + sql, + args, + ( + err: Error, + result: { rows: Array<{ max_stream_id: number | null }> } + ) => { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (err) { + reject(err) + } else { + resolve(result.rows[0].max_stream_id) + } + } + ) + }) + } + + // eslint-disable-next-line @typescript-eslint/promise-function-async + searchUserDirectory( + userId: string, + searchTerm: string, + limit: number, + searchAllUsers: boolean + ): Promise { + return new Promise((resolve, reject) => { + if (this.db == null) { + reject(new Error('Wait for database to be ready')) + return + } + + let whereClause: string + if (searchAllUsers) { + whereClause = 'user_id != $1' + } else { + whereClause = ` + ( + EXISTS (SELECT 1 FROM users_in_public_rooms WHERE user_id = t.user_id) + OR EXISTS ( + SELECT 1 FROM users_who_share_private_rooms + WHERE user_id = $1 AND other_user_id = t.user_id + ) + ) + ` + } + + const [fullQuery, exactQuery, prefixQuery] = + parseQueryPostgres(searchTerm) + const args = [userId, fullQuery, exactQuery, prefixQuery, limit + 1] + + const sql = ` + WITH matching_users AS ( + SELECT user_id, vector + FROM user_directory_search + WHERE vector @@ to_tsquery('simple', $2) + LIMIT 10000 + ) + SELECT d.user_id AS user_id, display_name, avatar_url + FROM matching_users as t + INNER JOIN user_directory AS d USING (user_id) + LEFT JOIN users AS u ON t.user_id = u.name + WHERE ${whereClause} + ORDER BY + (CASE WHEN d.user_id IS NOT NULL THEN 4.0 ELSE 1.0 END) + * (CASE WHEN display_name IS NOT NULL THEN 1.2 ELSE 1.0 END) + * (CASE WHEN avatar_url IS NOT NULL THEN 1.2 ELSE 1.0 END) + * ( + 3 * ts_rank_cd( + '{0.1, 0.1, 0.9, 1.0}', + vector, + to_tsquery('simple', $3), + 8 + ) + + ts_rank_cd( + '{0.1, 0.1, 0.9, 1.0}', + vector, + to_tsquery('simple', $4), + 8 + ) + ) + DESC, + display_name IS NULL, + avatar_url IS NULL + LIMIT $5 + ` + + this.db.query(sql, args, (err: Error, result: { rows: DbGetResult }) => { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (err) { + reject(err) + } else { + resolve(result.rows) + } + }) + }) + } +} + +function parseQueryPostgres(searchTerm: string): [string, string, string] { + /** + * Takes a plain string from the user and converts it into a form + * that can be passed to the database. + * This function allows us to add prefix matching, which isn't supported by default. + */ + + searchTerm = searchTerm.toLowerCase() + searchTerm = searchTerm.normalize('NKFD') + + const escapedWords: string[] = [] + for (const word of parseWordsWithRegex(searchTerm)) { + const quotedWord = word.replace(/'/g, "''").replace(/\\/g, '\\\\') + escapedWords.push(`'${quotedWord}'`) + } + + const both = escapedWords.map((word) => `(${word}:* | ${word})`).join(' & ') + const exact = escapedWords.join(' & ') + const prefix = escapedWords.map((word) => `${word}:*`).join(' & ') + + return [both, exact, prefix] +} + +function parseWordsWithRegex(searchTerm: string): string[] { + /** + * Break down the search term into words using a regular expression, + * when we don't have ICU available. + */ + const regex = /[\w-]+/g + const matches = searchTerm.match(regex) + + if (matches === null) { + return [] + } + return matches +} + +export default MatrixDBPg diff --git a/packages/matrix-client-server/src/matrixDb/sql/sqlite.ts b/packages/matrix-client-server/src/matrixDb/sql/sqlite.ts new file mode 100644 index 00000000..df7883c6 --- /dev/null +++ b/packages/matrix-client-server/src/matrixDb/sql/sqlite.ts @@ -0,0 +1,242 @@ +import { type Collections, type MatrixDBmodifiedBackend } from '../' +import { type DbGetResult, type Config } from '../../types' +import { SQLite } from '@twake/matrix-identity-server' + +class MatrixDBSQLite + extends SQLite + implements MatrixDBmodifiedBackend +{ + // eslint-disable-next-line @typescript-eslint/promise-function-async + createDatabases( + conf: Config, + tables: Record, + indexes: Partial>, + initializeValues: Partial< + Record>> + > + ): Promise { + /* istanbul ignore if */ + if (this.db != null) return Promise.resolve() + return new Promise((resolve, reject) => { + import('sqlite3') + .then((sqlite3) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error + /* istanbul ignore next */ // @ts-ignore + if (sqlite3.Database == null) sqlite3 = sqlite3.default + const db = (this.db = new sqlite3.Database( + conf.matrix_database_host as string, + sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE // IT SHOULD ALWAYS BE READWRITE as we connect the synapse db + )) + /* istanbul ignore if */ + if (db == null) { + reject(new Error('Database not created')) + } + resolve() + }) + .catch((e) => { + /* istanbul ignore next */ + reject(e) + }) + }) + } + + // eslint-disable-next-line @typescript-eslint/promise-function-async + updateWithConditions( + table: Collections, + values: Record, + conditions: Array<{ field: string; value: string | number }> + ): Promise { + return new Promise((resolve, reject) => { + /* istanbul ignore if */ + if (this.db == null) { + throw new Error('Wait for database to be ready') + } + const names = Object.keys(values) + const vals = Object.values(values) + // Add the values for the conditions to the vals array + conditions.forEach((condition) => { + vals.push(condition.value) + }) + + // Construct the SET clause for the update statement + const setClause = names.map((name) => `${name} = ?`).join(', ') + + // Construct the WHERE clause for the conditions + const whereClause = conditions + .map((condition) => `${condition.field} = ?`) + .join(' AND ') + + const stmt = this.db.prepare( + `UPDATE ${table} SET ${setClause} WHERE ${whereClause} RETURNING *;` + ) + + stmt.all( + vals, + (err: string, rows: Array>) => { + /* istanbul ignore if */ + if (err != null) { + reject(err) + } else { + resolve(rows) + } + } + ) + + stmt.finalize((err) => { + /* istanbul ignore if */ + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (err) { + reject(err) + } + }) + }) + } + + // eslint-disable-next-line @typescript-eslint/promise-function-async + getMaxStreamId( + userId: string, + deviceId: string, + fromStreamId: number, + toStreamId: number, + limit: number + ): Promise { + return new Promise((resolve, reject) => { + /* istanbul ignore if */ + if (this.db == null) { + throw new Error('Wait for database to be ready') + } + + const stmt = this.db.prepare(` + SELECT MAX(stream_id) AS max_stream_id FROM ( + SELECT stream_id FROM device_inbox + WHERE user_id = ? AND device_id = ? + AND ? < stream_id AND stream_id <= ? + ORDER BY stream_id + LIMIT ? + ) AS d + `) + + stmt.get( + [userId, deviceId, fromStreamId, toStreamId, limit], + (err: Error | null, row: { max_stream_id: number | null }) => { + /* istanbul ignore if */ + if (err != null) { + reject(err) + } else { + resolve(row.max_stream_id) + } + } + ) + + stmt.finalize((err: Error | null) => { + /* istanbul ignore if */ + if (err != null) { + reject(err) + } + }) + }) + } + + // eslint-disable-next-line @typescript-eslint/promise-function-async + searchUserDirectory( + userId: string, + searchTerm: string, + limit: number, + searchAllUsers: boolean + ): Promise { + return new Promise((resolve, reject) => { + /* istanbul ignore if */ + if (this.db == null) { + throw new Error('Wait for database to be ready') + } + + let whereClause: string + if (searchAllUsers) { + whereClause = 'user_id != ?' + } else { + whereClause = ` + ( + EXISTS (SELECT 1 FROM users_in_public_rooms WHERE user_id = t.user_id) + OR EXISTS ( + SELECT 1 FROM users_who_share_private_rooms + WHERE user_id = ? AND other_user_id = t.user_id + ) + ) + ` + } + const searchQuery = parseQuerySqlite(searchTerm) + const args = [userId, searchQuery, limit + 1] + + const stmt = this.db.prepare(` + SELECT d.user_id AS user_id, display_name, avatar_url, + matchinfo(user_directory_search) AS match_info + FROM user_directory_search as t + INNER JOIN user_directory AS d USING (user_id) + LEFT JOIN users AS u ON t.user_id = u.name + WHERE ${whereClause} + AND value MATCH ? + ORDER BY + match_info DESC, + display_name IS NULL, + avatar_url IS NULL + LIMIT ? + `) + + stmt.all( + args, + (err: Error | null, rows: Array>) => { + /* istanbul ignore if */ + if (err != null) { + reject(err) + } else { + resolve(rows) + } + } + ) + + stmt.finalize((err: Error | null) => { + /* istanbul ignore if */ + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (err) { + reject(err) + } + }) + }) + } +} + +export function parseQuerySqlite(searchTerm: string): string { + /** + * Takes a plain string from the user and converts it into a form + * that can be passed to the database. + * This function allows us to add prefix matching, which isn't supported by default. + * + * We specifically add both a prefix and non-prefix matching term so that + * exact matches get ranked higher. + */ + + searchTerm = searchTerm.toLowerCase() + searchTerm = searchTerm.normalize('NFKD') + + // Pull out the individual words, discarding any non-word characters. + const results = parseWordsWithRegex(searchTerm) + + // Construct the SQLite query string for full-text search with prefix matching + return results.map((result) => `(${result}* OR ${result})`).join(' & ') +} + +export function parseWordsWithRegex(searchTerm: string): string[] { + /** + * Break down the search term into words using a regular expression, + * when we don't have ICU available. + */ + const regex = /[\w-]+/g + const matches = searchTerm.match(regex) + + if (matches === null) { + return [] + } + return matches +} + +export default MatrixDBSQLite diff --git a/packages/matrix-client-server/src/presence/getStatus.ts b/packages/matrix-client-server/src/presence/getStatus.ts new file mode 100644 index 00000000..0a13bbe1 --- /dev/null +++ b/packages/matrix-client-server/src/presence/getStatus.ts @@ -0,0 +1,65 @@ +import type MatrixClientServer from '..' +import { + errMsg, + type expressAppHandler, + send, + epoch, + isMatrixIdValid +} from '@twake/utils' + +// TODO : Handle error 403 where the user isn't allowed to see this user's presence status, may have to do with the "users_to_send_full_presence_to" table in the matrixDb +const getStatus = (clientServer: MatrixClientServer): expressAppHandler => { + return (req, res) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const userId: string = req.params.userId as string + if (!isMatrixIdValid(userId)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid user ID'), + clientServer.logger + ) + } else { + clientServer.authenticate(req, res, (data, id) => { + clientServer.matrixDb + .get('presence', ['state', 'mtime', 'state', 'status_msg'], { + user_id: userId + }) + .then((rows) => { + if (rows.length === 0) { + send( + res, + 404, + { + errcode: 'M_UNKNOWN', + error: + 'There is no presence state for this user. This user may not exist or isn’t exposing presence information to you.' + }, + clientServer.logger + ) + } else { + send( + res, + 200, + { + currently_active: rows[0].state === 'online', + last_active_ts: epoch() - (rows[0].mtime as number), // TODO : Check if mtime corresponds to last_active_ts, not clear in the spec + state: rows[0].state, + status_msg: rows[0].status_msg + }, + clientServer.logger + ) + } + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error("Error retrieving user's presence state") + // istanbul ignore next + send(res, 500, errMsg('unknown', e.toString()), clientServer.logger) + }) + }) + } + } +} +export default getStatus diff --git a/packages/matrix-client-server/src/presence/presence.test.ts b/packages/matrix-client-server/src/presence/presence.test.ts new file mode 100644 index 00000000..07e43de0 --- /dev/null +++ b/packages/matrix-client-server/src/presence/presence.test.ts @@ -0,0 +1,198 @@ +import fs from 'fs' +import request from 'supertest' +import express from 'express' +import ClientServer from '../index' +import { buildMatrixDb, buildUserDB } from '../__testData__/buildUserDB' +import { type Config } from '../types' +import defaultConfig from '../__testData__/registerConf.json' +import { getLogger, type TwakeLogger } from '@twake/logger' +import { setupTokens, validToken } from '../__testData__/setupTokens' + +jest.mock('node-fetch', () => jest.fn()) + +let conf: Config +let clientServer: ClientServer +let app: express.Application + +const logger: TwakeLogger = getLogger() + +beforeAll((done) => { + // @ts-expect-error TS doesn't understand that the config is valid + conf = { + ...defaultConfig, + base_url: 'http://example.com/', + matrix_database_host: './src/__testData__/testMatrixPresence.db', + userdb_host: './src/__testData__/testPresence.db', + database_host: './src/__testData__/testPresence.db', + is_registration_enabled: false + } + if (process.env.TEST_PG === 'yes') { + conf.database_engine = 'pg' + conf.userdb_engine = 'pg' + conf.database_host = process.env.PG_HOST ?? 'localhost' + conf.database_user = process.env.PG_USER ?? 'twake' + conf.database_password = process.env.PG_PASSWORD ?? 'twake' + conf.database_name = process.env.PG_DATABASE ?? 'test' + } + buildUserDB(conf) + .then(() => { + buildMatrixDb(conf) + .then(() => { + done() + }) + .catch((e) => { + logger.error('Error while building matrix db:', e) + done(e) + }) + }) + .catch((e) => { + logger.error('Error while building user db:', e) + done(e) + }) +}) + +afterAll(() => { + fs.unlinkSync('src/__testData__/testPresence.db') + fs.unlinkSync('src/__testData__/testMatrixPresence.db') +}) + +describe('Use configuration file', () => { + beforeAll((done) => { + clientServer = new ClientServer(conf) + app = express() + clientServer.ready + .then(() => { + Object.keys(clientServer.api.get).forEach((k) => { + app.get(k, clientServer.api.get[k]) + }) + Object.keys(clientServer.api.post).forEach((k) => { + app.post(k, clientServer.api.post[k]) + }) + Object.keys(clientServer.api.put).forEach((k) => { + app.put(k, clientServer.api.put[k]) + }) + Object.keys(clientServer.api.delete).forEach((k) => { + app.delete(k, clientServer.api.delete[k]) + }) + done() + }) + .catch((e) => { + done(e) + }) + }) + + afterAll(() => { + clientServer.cleanJobs() + }) + + describe('Endpoints with authentication', () => { + beforeAll(async () => { + await setupTokens(clientServer, logger) + }) + describe('/_matrix/client/v3/presence/:userId/status', () => { + describe('GET', () => { + it('should return the presence state of a user', async () => { + await clientServer.matrixDb.insert('presence', { + user_id: '@testuser:example.com', + state: 'online', + status_msg: 'I am online', + mtime: Date.now() + }) + const response = await request(app) + .get('/_matrix/client/v3/presence/@testuser:example.com/status') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('currently_active', true) + expect(response.body).toHaveProperty('last_active_ts') + expect(response.body).toHaveProperty('state', 'online') + expect(response.body).toHaveProperty('status_msg', 'I am online') + }) + it('should reject a request made to an uknown user', async () => { + const response = await request(app) + .get('/_matrix/client/v3/presence/@unknownuser:example.com/status') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(404) + }) + it('should reject a request with a userId that does not match the regex', async () => { + const response = await request(app) + .get('/_matrix/client/v3/presence/invalidUserId/status') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + }) + }) + describe('PUT', () => { + it('should set the presence state of a user', async () => { + const response = await request(app) + .put('/_matrix/client/v3/presence/@testuser:example.com/status') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ presence: 'offline', status_msg: 'I am offline' }) + expect(response.statusCode).toBe(200) + const presence = await clientServer.matrixDb.get( + 'presence', + ['state', 'status_msg'], + { user_id: '@testuser:example.com' } + ) + expect(presence).toHaveLength(1) + expect(presence[0].state).toBe('offline') + expect(presence[0].status_msg).toBe('I am offline') + }) + it('should reject a request to set the presence state of another user', async () => { + const response = await request(app) + .put('/_matrix/client/v3/presence/@anotheruser:example.com/status') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ presence: 'offline', status_msg: 'I am offline' }) + expect(response.statusCode).toBe(403) + }) + it('should reject a state that wants to set a wrong presence status', async () => { + const response = await request(app) + .put('/_matrix/client/v3/presence/@testuser:example.com/status') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ presence: 'wrongStatus', status_msg: 'I am offline' }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty( + 'error', + 'Invalid presence state' + ) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject a state that wants to set a status message that is too long', async () => { + let statusMsg = '' + for (let i = 0; i < 2050; i++) { + statusMsg += 'a' + } + const response = await request(app) + .put('/_matrix/client/v3/presence/@testuser:example.com/status') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ presence: 'online', status_msg: statusMsg }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty( + 'error', + 'Status message is too long' + ) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject a request with a userId that does not match the regex', async () => { + const response = await request(app) + .put('/_matrix/client/v3/presence/invalidUserId/status') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + }) + }) + }) + describe('/_matrix/client/v3/register', () => { + // Test put here since presence doesn't need registration so we can modify the config without consequence + it('should return 404 if registration is disabled', async () => { + const response = await request(app).post('/_matrix/client/v3/register') + expect(response.statusCode).toBe(404) + }) + }) + }) +}) diff --git a/packages/matrix-client-server/src/presence/putStatus.ts b/packages/matrix-client-server/src/presence/putStatus.ts new file mode 100644 index 00000000..e00e374f --- /dev/null +++ b/packages/matrix-client-server/src/presence/putStatus.ts @@ -0,0 +1,96 @@ +import type MatrixClientServer from '..' +import { + jsonContent, + validateParameters, + errMsg, + type expressAppHandler, + send, + isMatrixIdValid +} from '@twake/utils' + +interface PutRequestBody { + presence: string + status_msg: string +} + +const schema = { + presence: true, + status_msg: false +} +const statusMsgRegex = /^.{0,2048}$/ + +// If status message is longer than 2048 characters, we refuse it to prevent clients from sending too long messages that could crash the DB. This value is arbitrary and could be changed +// NB : Maybe the function should update the presence_stream table of the matrixDB, +// TODO : reread the code after implementing streams-related endpoints +const putStatus = (clientServer: MatrixClientServer): expressAppHandler => { + return (req, res) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const userId: string = req.params.userId as string + if (!isMatrixIdValid(userId)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid user ID'), + clientServer.logger + ) + } else { + clientServer.authenticate(req, res, (data, id) => { + jsonContent(req, res, clientServer.logger, (obj) => { + validateParameters(res, schema, obj, clientServer.logger, (obj) => { + if (data.sub !== userId) { + clientServer.logger.warn( + 'You cannot set the presence state of another user' + ) + send(res, 403, errMsg('forbidden'), clientServer.logger) + return + } + if ( + (obj as PutRequestBody).presence !== 'offline' && + (obj as PutRequestBody).presence !== 'online' && + (obj as PutRequestBody).presence !== 'unavailable' + ) { + send(res, 400, errMsg('invalidParam', 'Invalid presence state')) + return + } + if (!statusMsgRegex.test((obj as PutRequestBody).status_msg)) { + send( + res, + 400, + errMsg('invalidParam', 'Status message is too long') + ) + return + } + clientServer.matrixDb + .updateWithConditions( + // TODO : Replace with upsert + 'presence', + { + state: (obj as PutRequestBody).presence, + status_msg: (obj as PutRequestBody).status_msg + }, + [{ field: 'user_id', value: userId }] + ) + .then(() => { + send(res, 200, {}, clientServer.logger) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error( + "Error updating user's presence state" + ) + // istanbul ignore next + send( + res, + 500, + errMsg('unknown', e.toString()), + clientServer.logger + ) + }) + }) + }) + }) + } + } +} +export default putStatus diff --git a/packages/matrix-client-server/src/refresh.ts b/packages/matrix-client-server/src/refresh.ts new file mode 100644 index 00000000..37d746a7 --- /dev/null +++ b/packages/matrix-client-server/src/refresh.ts @@ -0,0 +1,193 @@ +import { + epoch, + errMsg, + jsonContent, + send, + validateParameters, + type expressAppHandler +} from '@twake/utils' +import type MatrixClientServer from '.' +import { randomString } from '@twake/crypto' +import { type DbGetResult } from '@twake/matrix-identity-server' + +interface RequestBody { + refresh_token: string +} + +interface RefreshTokenData { + id: number + user_id: string + device_id: string + token: string + next_token_id?: string + expiry_ts?: number + ultimate_session_expiry_ts?: number +} +const schema = { + refresh_token: true +} + +const generateAndSendToken = ( + clientServer: MatrixClientServer, + res: any, + oldRefreshToken: string, + nextRefreshToken: string, + nextRefreshTokenId: string, + currentTimestamp: number, + refreshTokenData: RefreshTokenData, + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + additionalPromises: Array> +): void => { + const newAccessToken = randomString(64) + const insertNewAccessToken = clientServer.matrixDb.insert('access_tokens', { + id: randomString(64), + user_id: refreshTokenData.user_id, + device_id: refreshTokenData.device_id, + token: newAccessToken, + valid_until_ms: currentTimestamp + 64000, // TODO: Set valid_until_ms based on server config, current value is arbitrary + refresh_token_id: nextRefreshTokenId, + used: 0 + }) // TODO : Handle 'puppets_user_id' if relevant + const updateOldAccessToken = clientServer.matrixDb.updateWithConditions( + 'access_tokens', + { used: 1 }, + [{ field: 'refresh_token_id', value: refreshTokenData.id }] + ) // Invalidate the old access token + + Promise.all([ + insertNewAccessToken, + updateOldAccessToken, + ...additionalPromises + ]) + .then(() => { + send(res, 200, { + access_token: newAccessToken, + refresh_token: nextRefreshToken + }) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error('Error generating tokens', e) + // istanbul ignore next + send(res, 500, e) + }) +} + +const refresh = (clientServer: MatrixClientServer): expressAppHandler => { + return (req, res) => { + jsonContent(req, res, clientServer.logger, (obj) => { + validateParameters(res, schema, obj, clientServer.logger, (obj) => { + const refreshToken = (obj as RequestBody).refresh_token + clientServer.matrixDb + .get('refresh_tokens', ['*'], { token: refreshToken }) + .then((rows) => { + if (rows.length === 0) { + clientServer.logger.error('Unknown refresh token', refreshToken) + send(res, 400, errMsg('unknownToken')) + return + } + const refreshTokenData = rows[0] as unknown as RefreshTokenData + const currentTimestamp = epoch() + if ( + refreshTokenData.expiry_ts !== undefined && + refreshTokenData.expiry_ts !== null && + refreshTokenData.expiry_ts < currentTimestamp + ) { + send( + res, + 401, + errMsg('invalidToken', 'Refresh token has expired') + ) + return + } + + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (refreshTokenData.next_token_id) { + clientServer.matrixDb + .get('refresh_tokens', ['token'], { + id: refreshTokenData.next_token_id + }) + .then((nextTokenRows) => { + // istanbul ignore if + if (nextTokenRows.length === 0) { + send( + res, + 500, + errMsg('unknown', 'Failed to retrieve the next token') + ) + return + } + const deleteOldRefreshToken = // Delete the old refresh token when the new one is used, as per the spec + clientServer.matrixDb.deleteWhere('refresh_tokens', { + field: 'token', + value: refreshToken, + operator: '=' + }) + const newRefreshToken = nextTokenRows[0].token as string + generateAndSendToken( + clientServer, + res, + refreshToken, + newRefreshToken, + nextTokenRows[0].id as string, + currentTimestamp, + refreshTokenData, + [deleteOldRefreshToken] + ) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error('Error retrieving next token', e) + // istanbul ignore next + send(res, 500, e) + }) + } else { + const newRefreshToken = randomString(64) // If there is not next_token_id specified, generate a new refresh token + const newRefreshTokenId = randomString(64) + const insertNewRefreshToken = clientServer.matrixDb.insert( + 'refresh_tokens', + { + id: newRefreshTokenId, + user_id: refreshTokenData.user_id, + device_id: refreshTokenData.device_id, + token: newRefreshToken, + expiry_ts: currentTimestamp + 64000 // TODO: Set expiry_ts based on server config, current value is arbitrary and handle ultimate_session_expiry_ts + } + ) + const updateOldRefreshToken = // Link the newly created refresh token to the old one, so that the old one is deleted when the new one is used + clientServer.matrixDb.updateWithConditions( + 'refresh_tokens', + { next_token_id: newRefreshTokenId }, + [{ field: 'token', value: refreshToken }] + ) + generateAndSendToken( + clientServer, + res, + refreshToken, + newRefreshToken, + newRefreshTokenId, + currentTimestamp, + refreshTokenData, + [insertNewRefreshToken, updateOldRefreshToken] + ) + } + }) + .catch((error) => { + // istanbul ignore next + clientServer.logger.error('Error fetching refresh token', error) + // istanbul ignore next + send( + res, + 500, + errMsg( + 'unknown', + 'An error occurred while fetching the refresh token' + ) + ) + }) + }) + }) + } +} + +export default refresh diff --git a/packages/matrix-client-server/src/register/available.ts b/packages/matrix-client-server/src/register/available.ts new file mode 100644 index 00000000..088fcc3b --- /dev/null +++ b/packages/matrix-client-server/src/register/available.ts @@ -0,0 +1,77 @@ +import { + errMsg, + isMatrixIdValid, + send, + type expressAppHandler +} from '@twake/utils' +import type MatrixClientServer from '..' +import { type Request, type Response } from 'express' + +interface Parameters { + username: string +} + +const available = (clientServer: MatrixClientServer): expressAppHandler => { + return (req, res) => { + // @ts-expect-error req has query + const userId = (req.query as Parameters).username + if (!isMatrixIdValid(userId)) { + clientServer.logger.error('Invalid user ID') + send( + res, + 400, + errMsg('invalidParam', 'Invalid user ID'), + clientServer.logger + ) + return + } + for (const appService of clientServer.conf.application_services) { + if ( + // eslint-disable-next-line @typescript-eslint/prefer-optional-chain + appService.namespaces.users !== undefined && + appService.namespaces.users !== null && + appService.namespaces.users.some( + (namespace) => + new RegExp(namespace.regex).test(userId) && namespace.exclusive + ) + ) { + send( + res, + 400, + errMsg( + 'exclusive', + 'The desired username is in the exclusive namespace claimed by an application service.' + ), + clientServer.logger + ) + return + } + clientServer.matrixDb + .get('users', ['name'], { name: userId }) + .then((rows) => { + if (rows.length > 0) { + send(res, 400, errMsg('userInUse'), clientServer.logger) + } else { + send(res, 200, { available: true }, clientServer.logger) + } + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error('Error while checking user availability', e) + // istanbul ignore next + send(res, 500, e, clientServer.logger) + }) + } + } +} + +const rateLimitedAvailable = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + clientServer.rateLimiter(req as Request, res as Response, () => { + available(clientServer)(req, res) + }) + } +} +export default rateLimitedAvailable diff --git a/packages/matrix-client-server/src/register/email/requestToken.ts b/packages/matrix-client-server/src/register/email/requestToken.ts new file mode 100644 index 00000000..3f396c33 --- /dev/null +++ b/packages/matrix-client-server/src/register/email/requestToken.ts @@ -0,0 +1,299 @@ +import { randomString } from '@twake/crypto' +import fs from 'fs' +import { type Config } from '../../types' +import { + errMsg, + isValidUrl, + jsonContent, + send, + validateParameters, + type expressAppHandler +} from '@twake/utils' +import type MatrixClientServer from '../../index' +import Mailer from '../../utils/mailer' + +interface RequestTokenArgs { + client_secret: string + email: string + next_link?: string + send_attempt: number + id_server?: string + id_access_token?: string +} + +const schema = { + client_secret: true, + email: true, + next_link: false, + send_attempt: true, + id_server: false, + id_access_token: false +} + +const clientSecretRe = /^[0-9a-zA-Z.=_-]{6,255}$/ +const validEmailRe = /^\w[+.-\w]*\w@\w[.-\w]*\w\.\w{2,6}$/ +const maxAttemps = 1000000000 + +export const getSubmitUrl = (conf: Config): string => { + return ( + // istanbul ignore next + (conf.base_url != null && conf.base_url.length > 0 + ? conf.base_url.replace(/\/+$/, '') + : `https://${conf.server_name}`) + + '/_matrix/client/v3/register/email/submitToken' + ) +} + +export const preConfigureTemplate = ( + template: string, + conf: Config, + transport: Mailer +): string => { + const mb = randomString(32) + const baseUrl = + /* istanbul ignore next */ + getSubmitUrl(conf) + return ( + template + // initialize "From" + .replace(/__from__/g, transport.from) + // fix multipart stuff + .replace(/__multipart_boundary__/g, mb) + // prepare link + .replace(/__link__/g, `${baseUrl}?__linkQuery__`) + ) +} + +export const mailBody = ( + template: string, + dst: string, + token: string, + secret: string, + sid: string +): string => { + return ( + template + // set "To" + .replace(/__to__/g, dst) + // set date + .replace(/__date__/g, new Date().toUTCString()) + // initialize message id + .replace(/__messageid__/g, randomString(32)) + // set link parameters + .replace( + /__linkQuery__/g, + new URLSearchParams({ + token, + client_secret: secret, + sid + }).toString() + ) + ) +} + +export const fillTableAndSend = ( + clientServer: MatrixClientServer, + dst: string, + clientSecret: string, + sendAttempt: number, + verificationTemplate: string, + transport: Mailer, + res: any, + sid: string, + nextLink?: string +): void => { + clientServer.matrixDb + .createOneTimeToken(sid, clientServer.conf.mail_link_delay, nextLink) + .then((token) => { + void transport.sendMail({ + to: dst, + raw: mailBody(verificationTemplate, dst, token, clientSecret, sid) + }) + clientServer.matrixDb + .insert('threepid_validation_session', { + client_secret: clientSecret, + address: dst, + medium: 'email', + session_id: sid, + last_send_attempt: sendAttempt + }) + .then(() => { + send( + res, + 200, + { sid, submit_url: getSubmitUrl(clientServer.conf) }, + clientServer.logger + ) + }) + .catch((err) => { + // istanbul ignore next + clientServer.logger.error('Insertion error', err) + // istanbul ignore next + send(res, 500, errMsg('unknown', err.toString()), clientServer.logger) + }) + }) + .catch((err) => { + /* istanbul ignore next */ + clientServer.logger.error('Token error', err) + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', err.toString()), clientServer.logger) + }) +} + +const RequestToken = (clientServer: MatrixClientServer): expressAppHandler => { + const transport = new Mailer(clientServer.conf) + const verificationTemplate = preConfigureTemplate( + fs + .readFileSync(`${clientServer.conf.template_dir}/mailVerification.tpl`) + .toString(), + clientServer.conf, + transport + ) + return (req, res) => { + jsonContent(req, res, clientServer.logger, (obj) => { + validateParameters(res, schema, obj, clientServer.logger, (obj) => { + const clientSecret = (obj as RequestTokenArgs).client_secret + const sendAttempt = (obj as RequestTokenArgs).send_attempt + const dst = (obj as RequestTokenArgs).email + const nextLink = (obj as RequestTokenArgs).next_link + if (!clientSecretRe.test(clientSecret)) { + send( + res, + 400, + errMsg('invalidParam', 'invalid client_secret'), + clientServer.logger + ) + } else if (!validEmailRe.test(dst)) { + send(res, 400, errMsg('invalidEmail'), clientServer.logger) + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + } else if (nextLink && !isValidUrl(nextLink)) { + send( + res, + 400, + errMsg('invalidParam', 'invalid next_link'), + clientServer.logger + ) + } else if ( + typeof sendAttempt !== 'number' || + sendAttempt > maxAttemps + ) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid send attempt'), + clientServer.logger + ) + } else { + clientServer.matrixDb + .get('user_threepids', ['user_id'], { address: dst }) + .then((rows) => { + if (rows.length > 0) { + send(res, 400, errMsg('threepidInUse'), clientServer.logger) + } else { + clientServer.matrixDb + .get( + 'threepid_validation_session', + ['last_send_attempt', 'session_id'], + { + client_secret: clientSecret, + address: dst + } + ) + .then((rows) => { + if (rows.length > 0) { + if (sendAttempt === rows[0].last_send_attempt) { + send( + res, + 200, + { + sid: rows[0].session_id, + submit_url: getSubmitUrl(clientServer.conf) + }, + clientServer.logger + ) + } else { + clientServer.matrixDb + .deleteWhere('threepid_validation_session', [ + { + field: 'client_secret', + value: clientSecret, + operator: '=' + }, + { + field: 'session_id', + value: rows[0].session_id as string, + operator: '=' + } + ]) + .then(() => { + fillTableAndSend( + // The calls to send are made in this function + clientServer, + dst, + clientSecret, + sendAttempt, + verificationTemplate, + transport, + res, + rows[0].session_id as string, + nextLink + ) + }) + .catch((err) => { + // istanbul ignore next + clientServer.logger.error('Deletion error', err) + // istanbul ignore next + send( + res, + 500, + errMsg('unknown', err.toString()), + clientServer.logger + ) + }) + } + } else { + fillTableAndSend( + // The calls to send are made in this function + clientServer, + dst, + clientSecret, + sendAttempt, + verificationTemplate, + transport, + res, + randomString(64), + nextLink + ) + } + }) + .catch((err) => { + /* istanbul ignore next */ + clientServer.logger.error('Send_attempt error', err) + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', err.toString()), + clientServer.logger + ) + }) + } + }) + .catch((err) => { + /* istanbul ignore next */ + clientServer.logger.error('Error getting userID :', err) + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', err.toString()), + clientServer.logger + ) + }) + } + }) + }) + } +} + +export default RequestToken diff --git a/packages/matrix-client-server/src/register/email/submitToken.ts b/packages/matrix-client-server/src/register/email/submitToken.ts new file mode 100644 index 00000000..326f91a8 --- /dev/null +++ b/packages/matrix-client-server/src/register/email/submitToken.ts @@ -0,0 +1,134 @@ +import { + epoch, + errMsg, + jsonContent, + send, + type expressAppHandler +} from '@twake/utils' +import type MatrixClientServer from '../..' + +interface Parameters { + client_secret?: string + token?: string + sid?: string +} + +interface Token { + client_secret: string + session_id: string + next_link?: string +} + +// TODO : Redirect to next_link from requestToken if present + +const SubmitToken = (clientServer: MatrixClientServer): expressAppHandler => { + return (req, res) => { + const realMethod = (parameters: Parameters): void => { + if ( + parameters.client_secret?.length != null && + parameters.token?.length != null && + parameters.sid?.length != null + ) { + clientServer.matrixDb + .verifyToken(parameters.token) + .then((data) => { + if ( + (data as Token).session_id === parameters.sid && + (data as Token).client_secret === parameters.client_secret + ) { + clientServer.matrixDb + .deleteToken(parameters.token as string) + .then(() => { + clientServer.matrixDb + .updateWithConditions( + 'threepid_validation_session', + { validated_at: epoch() }, + [ + { + field: 'session_id', + value: (data as Token).session_id + }, + { + field: 'client_secret', + value: (data as Token).client_secret + } + ] + ) + .then(() => { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (req.method === 'GET' && (data as Token).next_link) { + const redirectUrl = new URL( + // @ts-expect-error : We check that next_link is not null beforehand + (data as Token).next_link + ).toString() + + res.writeHead(302, { + Location: redirectUrl + }) + res.end() + return + } + send(res, 200, { success: true }, clientServer.logger) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error( + 'Error while updating the validation session informations', + e + ) + // istanbul ignore next + send(res, 500, e, clientServer.logger) + }) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error('Error while deleting the token', e) + // istanbul ignore next + send(res, 500, e, clientServer.logger) + }) + } else { + /* istanbul ignore next */ + send( + res, + 400, + errMsg('invalidParam', 'sid or secret mismatch'), + clientServer.logger + ) + } + }) + .catch((e) => { + send( + res, + 400, + errMsg( + 'invalidParam', + 'Unknown or expired token ' + (e as string) + ), + clientServer.logger + ) + }) + } else { + send(res, 400, errMsg('missingParams'), clientServer.logger) + } + } + if (req.method === 'GET') { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error + // @ts-ignore + realMethod(req.query as Parameters) + } else if (req.method === 'POST') { + jsonContent(req, res, clientServer.logger, (data) => { + realMethod(data as Parameters) + }) + } else { + /* istanbul ignore next */ + send( + res, + 400, + errMsg('unAuthorized', 'Unauthorized method'), + clientServer.logger + ) + } + } +} + +export default SubmitToken diff --git a/packages/matrix-client-server/src/register/index.ts b/packages/matrix-client-server/src/register/index.ts new file mode 100644 index 00000000..13ecaa41 --- /dev/null +++ b/packages/matrix-client-server/src/register/index.ts @@ -0,0 +1,529 @@ +/* eslint-disable @typescript-eslint/promise-function-async */ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable @typescript-eslint/strict-boolean-expressions */ +import { + jsonContent, + errMsg, + type expressAppHandler, + send, + epoch, + toMatrixId, + isSenderLocalpartValid +} from '@twake/utils' +import { type AuthenticationData } from '../types' +import { Hash, randomString } from '@twake/crypto' +import type MatrixClientServer from '..' +import { + type DbGetResult, + getUrlsFromPolicies, + computePolicy +} from '@twake/matrix-identity-server' +import type { ServerResponse } from 'http' +import type e from 'express' +import { getRegisterAllowedFlows } from '../utils/userInteractiveAuthentication' +import { + verifyAuthenticationData, + verifyBoolean, + verifyString +} from '../typecheckers' + +interface Parameters { + kind: 'guest' | 'user' + guest_access_token?: string // If a guest wants to upgrade his account, from spec : https://spec.matrix.org/v1.11/client-server-api/#guest-access +} + +interface RegisterRequestBody { + auth?: AuthenticationData + device_id?: string + inhibit_login?: boolean + initial_device_display_name?: string + password?: string + refresh_token?: boolean + username?: string +} + +interface InsertedData { + name: string + creation_ts: number + is_guest: number + shadow_banned: number + user_type?: string +} + +const setupPolicies = ( + userId: string, + clientServer: MatrixClientServer, + accepted: number +): Promise => { + const promises: Array> = [] + Object.keys( + getUrlsFromPolicies(computePolicy(clientServer.conf, clientServer.logger)) + ).forEach((policyName) => { + promises.push( + clientServer.db.insert('userPolicies', { + policy_name: policyName, + user_id: userId, + accepted + }) + ) + }) + return Promise.all(promises) +} + +const sendSuccessResponse = ( + body: RegisterRequestBody, + res: e.Response | ServerResponse, + userId: string, + accessToken: string, + refreshToken: string, + deviceId: string +): void => { + if (body.inhibit_login) { + send(res, 200, { user_id: userId }) + } else { + if (!body.refresh_token) { + // No point sending a refresh token to the client if it does not support it + send(res, 200, { + access_token: accessToken, + device_id: deviceId, + user_id: userId + }) + } else { + send(res, 200, { + access_token: accessToken, + device_id: deviceId, + user_id: userId, + refresh_token: refreshToken + }) + } + } +} + +// NB : It might be necessary to fill the "profiles" table with the displayname set as the username given in the request body +// We did not use it yet so we are not sure whether to fill it here or not +const registerAccount = ( + device_display_name: string, + clientServer: MatrixClientServer, + userId: string, + deviceId: string, + ip: string, + userAgent: string, + body: RegisterRequestBody, + res: e.Response | ServerResponse, + kind: string, + password?: string, + upgrade?: boolean +): void => { + const accessToken = randomString(64) + const refreshToken = randomString(64) + const refreshTokenId = randomString(64) + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + const createUserPromise = (): Promise | void => { + const commonUserData: InsertedData = { + name: userId, + creation_ts: epoch(), + is_guest: kind === 'guest' ? 1 : 0, + shadow_banned: 0 + } + if (kind === 'guest') { + commonUserData.user_type = 'guest' // User type is NULL for normal users + } + if (password) { + const hash = new Hash() + return hash.ready.then(() => { + return clientServer.matrixDb.insert('users', { + ...commonUserData, + password_hash: hash.sha256(password) // TODO: Handle other hashing algorithms + }) + }) + } else { + return clientServer.matrixDb.insert('users', { ...commonUserData }) + } + } + const userPromise = createUserPromise() + const userIpPromise = clientServer.matrixDb.insert('user_ips', { + user_id: userId, + access_token: accessToken, + device_id: deviceId, + ip, + user_agent: userAgent, + last_seen: epoch() + }) + const newDevicePromise = clientServer.matrixDb.insert('devices', { + user_id: userId, + device_id: deviceId, + display_name: device_display_name, + last_seen: epoch(), + ip, + user_agent: userAgent + }) + const newDeviceAuthProviderPromise = clientServer.matrixDb.insert( + // TODO : Fill the auth_provider_id and auth_provider_session_id rows with the right values after implementing SSO login + 'device_auth_providers', + { + user_id: userId, + device_id: deviceId, + auth_provider_id: '', + auth_provider_session_id: '' + } + ) + const fillPoliciesPromise = setupPolicies(userId, clientServer, 0) // 0 means the user hasn't accepted the policies yet, used in Identity Server + const refreshTokenPromise = clientServer.matrixDb.insert('refresh_tokens', { + id: refreshTokenId, + user_id: userId, + device_id: deviceId, + token: refreshToken + }) + const accessTokenPromise = clientServer.matrixDb.insert('access_tokens', { + id: randomString(64), // To be fixed later + user_id: userId, + token: accessToken, + device_id: deviceId, + refresh_token_id: refreshTokenId + }) // TODO : replace the id with a correct one, and fill the 'puppets_user_id' row with the right value + const promisesToExecute = body.inhibit_login + ? [userIpPromise, userPromise, fillPoliciesPromise] + : [ + userIpPromise, + userPromise, + fillPoliciesPromise, + refreshTokenPromise, + accessTokenPromise, + newDevicePromise, + newDeviceAuthProviderPromise + ] + Promise.all(promisesToExecute) + .then(() => { + sendSuccessResponse( + body, + res, + userId, + accessToken, + refreshToken, + deviceId + ) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error('Error while registering a user', e) + // istanbul ignore next + send(res, 500, { + error: 'Error while registering a user' + }) + }) +} + +const upgradeGuest = ( + clientServer: MatrixClientServer, + oldUserId: string, + newUserId: string, + accessToken: string, + refreshTokenId: string, + deviceId: string, + body: RegisterRequestBody, + res: e.Response | ServerResponse, + password?: string +): void => { + if (typeof deviceId !== 'string' || deviceId.length > 512) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid device_id'), + clientServer.logger + ) + return + } + const commonUserData = { + is_guest: 0, + user_type: 'user', + name: newUserId + } + const hash = new Hash() + const updateUsersPromise = password + ? hash.ready.then(() => { + return clientServer.matrixDb.updateWithConditions( + 'users', + { + ...commonUserData, + password_hash: hash.sha256(password) // TODO: Handle other hashing algorithms + }, + [{ field: 'name', value: oldUserId }] + ) + }) + : clientServer.matrixDb.updateWithConditions('users', commonUserData, [ + { field: 'name', value: oldUserId } + ]) + + const updateUserIpsPromise = clientServer.matrixDb.updateWithConditions( + 'user_ips', + { user_id: newUserId }, + [ + { + field: 'access_token', + value: accessToken + } + ] + ) + + const updateDevicePromise = clientServer.matrixDb.updateWithConditions( + 'devices', + { user_id: newUserId, device_id: deviceId }, + [{ field: 'user_id', value: oldUserId }] + ) + + const updateDeviceAuthProviderPromise = + clientServer.matrixDb.updateWithConditions( + 'device_auth_providers', + { + user_id: newUserId, + device_id: deviceId + }, + [{ field: 'user_id', value: oldUserId }] + ) + const updateRefreshTokenPromise = clientServer.matrixDb.updateWithConditions( + 'refresh_tokens', + { user_id: newUserId, device_id: deviceId }, + [{ field: 'id', value: refreshTokenId }] + ) + const updateAccessTokenPromise = clientServer.matrixDb.updateWithConditions( + 'access_tokens', + { user_id: newUserId, device_id: deviceId }, + [{ field: 'token', value: accessToken }] + ) + Promise.all([ + updateRefreshTokenPromise, + updateAccessTokenPromise, + updateUsersPromise, + updateUserIpsPromise, + updateDevicePromise, + updateDeviceAuthProviderPromise + ]) + .then((rows) => { + sendSuccessResponse( + body, + res, + newUserId, + accessToken, + rows[0][0].token as string, + deviceId + ) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error("Error while updating guest's informations", e) + // istanbul ignore next + send(res, 500, e) + }) +} + +const register = (clientServer: MatrixClientServer): expressAppHandler => { + if (!clientServer.conf.is_registration_enabled) { + return (req, res) => { + send(res, 404, { error: 'Registration is disabled' }) + } + } + return (req, res) => { + // @ts-expect-error req.query exists + const parameters = req.query as Parameters + const ip = (req as e.Request).ip + // istanbul ignore if + if (ip === undefined) { + // istanbul ignore next + send(res, 500, errMsg('unknown', 'IP address is missing')) + return + } + const userAgent = req.headers['user-agent'] ?? 'undefined' + if (!parameters.kind || parameters.kind === 'user') { + // kind defaults to user + jsonContent(req, res, clientServer.logger, (obj) => { + const body = obj as unknown as RegisterRequestBody + if (body.username && !isSenderLocalpartValid(body.username)) { + send(res, 400, errMsg('invalidUsername', 'Invalid username')) + return + } else if (body.device_id && !verifyString(body.device_id)) { + send(res, 400, errMsg('invalidParam', 'Invalid device_id')) + return + } else if ( + body.initial_device_display_name && + !verifyString(body.initial_device_display_name) + ) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid initial_device_display_name') + ) + return + } else if (body.password && !verifyString(body.password)) { + send(res, 400, errMsg('invalidParam', 'Invalid password')) + return + } else if (body.refresh_token && !verifyBoolean(body.refresh_token)) { + send(res, 400, errMsg('invalidParam', 'Invalid refresh_token')) + return + } else if ( + body.inhibit_login !== null && + body.inhibit_login !== undefined && + !verifyBoolean(body.inhibit_login) + ) { + send(res, 400, errMsg('invalidParam', 'Invalid inhibit_login')) + return + } else if ( + body.auth !== null && + body.auth !== undefined && + !verifyAuthenticationData(body.auth) + ) { + send(res, 400, errMsg('invalidParam', 'Invalid auth')) + return + } + clientServer.uiauthenticate( + req, + res, + getRegisterAllowedFlows(clientServer.conf), + 'register a new account', + obj, + (obj) => { + const body = obj as unknown as RegisterRequestBody + const deviceId = body.device_id ?? randomString(20) // Length chosen arbitrarily + const username = body.username ?? randomString(9) // Length chosen to match the localpart restrictions for a Matrix userId + const userId = toMatrixId(username, clientServer.conf.server_name) // Checks for username validity are done in this function + clientServer.matrixDb + .get('users', ['name'], { + name: userId + }) + .then((rows) => { + if (rows.length > 0) { + send(res, 400, errMsg('userInUse')) + } else { + clientServer.matrixDb + .get('devices', ['display_name', 'user_id'], { + device_id: deviceId + }) + .then((deviceRows) => { + let initial_device_display_name + if (deviceRows.length > 0) { + // TODO : Refresh access tokens using refresh tokens and invalidate the previous access_token associated with the device after implementing the /refresh endpoint + } else { + initial_device_display_name = + body.initial_device_display_name ?? randomString(20) // Length chosen arbitrarily + registerAccount( + initial_device_display_name, + clientServer, + userId, + deviceId, + ip, + userAgent, + body, + res, + 'user', + body.password + ) + } + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error( + 'Error while checking if a device_id is already in use', + e + ) + // istanbul ignore next + send(res, 500, e) + }) + } + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error( + 'Error while checking if a username is already in use', + e + ) + // istanbul ignore next + send(res, 500, e) + }) + } + ) + }) + } else { + // We don't handle the threepid_guest_access_tokens table and give the guest an access token like any user. + // This might be problematic to restrict the endpoints guests have access to as specified in the spec + // TODO : Review this after implementing endpoints not available to guest accounts. Maybe modify the authenticate function. + // Right now we just give the guest an access token like any user, maybe this isn't the best way to handle it + jsonContent(req, res, clientServer.logger, (obj) => { + if (parameters.kind !== 'guest') { + send( + res, + 400, + errMsg('invalidParam', 'Kind must be either "guest" or "user"') + ) + return + } + const body = obj as unknown as RegisterRequestBody + if (parameters.guest_access_token) { + // Case where the guest user wants to upgrade his account : https://spec.matrix.org/v1.11/client-server-api/#guest-access + if (!body.username) { + clientServer.logger.error( + 'Username is required to upgrade a guest account' + ) + send(res, 400, errMsg('missingParams')) + return + } + const username = body.username + const userId = toMatrixId(username, clientServer.conf.server_name) + clientServer.matrixDb + .get( + 'access_tokens', + ['user_id', 'device_id', 'refresh_token_id'], + { + token: parameters.guest_access_token + } + ) + .then((rows) => { + if (rows.length === 0) { + clientServer.logger.error('Unknown guest access token') + send(res, 401, errMsg('unknownToken')) + return + } + const deviceId = body.device_id ?? (rows[0].device_id as string) + upgradeGuest( + clientServer, + rows[0].user_id as string, + userId, + parameters.guest_access_token as string, + rows[0].refresh_token_id as string, + deviceId, + body, + res, + body.password + ) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error( + "Error while getting the guest's old user_id", + e + ) + // istanbul ignore next + send(res, 500, e) + }) + } else { + const deviceId = randomString(20) // Length chosen arbitrarily + const username = randomString(9) // Length chosen to match the localpart restrictions for a Matrix userId + const initial_device_display_name = body.initial_device_display_name + ? body.initial_device_display_name + : randomString(20) // Length chosen arbitrarily + registerAccount( + initial_device_display_name, + clientServer, + toMatrixId(username, clientServer.conf.server_name), + deviceId, + ip, + userAgent, + { initial_device_display_name }, // All parameters must be ignored for guest registration except for initial_device_display_name as per the spec + res, + 'guest' + ) + } + }) + } + } +} + +export default register diff --git a/packages/matrix-client-server/src/register/msisdn/requestToken.ts b/packages/matrix-client-server/src/register/msisdn/requestToken.ts new file mode 100644 index 00000000..25f2ed48 --- /dev/null +++ b/packages/matrix-client-server/src/register/msisdn/requestToken.ts @@ -0,0 +1,314 @@ +import { randomString } from '@twake/crypto' +import fs from 'fs' +import { type Config } from '../../types' +import { + errMsg, + isValidUrl, + jsonContent, + send, + validateParameters, + type expressAppHandler, + isClientSecretValid, + isCountryValid, + isPhoneNumberValid +} from '@twake/utils' +import type MatrixClientServer from '../../index' +import SmsSender from '../../utils/smsSender' +import { getSubmitUrl } from '../email/requestToken' +import parsePhoneNumberFromString, { type CountryCode } from 'libphonenumber-js' + +interface RequestTokenArgs { + client_secret: string + country: string + phone_number: string + next_link?: string + send_attempt: number + id_server?: string + id_access_token?: string +} + +const schema = { + client_secret: true, + country: true, + phone_number: true, + next_link: false, + send_attempt: true, + id_server: false, + id_access_token: false +} +const maxAttemps = 1000000000 + +export const formatPhoneNumber = ( + rawNumber: string, + countryCode: string +): string => { + const phoneNumber = parsePhoneNumberFromString( + rawNumber, + countryCode as CountryCode + ) + // eslint-disable-next-line @typescript-eslint/prefer-optional-chain, @typescript-eslint/strict-boolean-expressions + if (phoneNumber) { + // Remove the leading '+' if it exists according to MSISDN convention + return phoneNumber.number.startsWith('+') + ? phoneNumber.number.slice(1) + : phoneNumber.number + } + return '' +} + +export const preConfigureTemplate = ( + template: string, + conf: Config, + transport: SmsSender +): string => { + const baseUrl = + /* istanbul ignore next */ + getSubmitUrl(conf) + return ( + template + // prepare link + .replace(/__link__/g, `${baseUrl}?__linkQuery__`) + ) +} + +export const smsBody = ( + template: string, + token: string, + secret: string, + sid: string +): string => { + return ( + template + // set link parameters + .replace( + /__linkQuery__/g, + new URLSearchParams({ + token, + client_secret: secret, + sid + }).toString() + ) + // set token + ) +} + +export const fillTableAndSend = ( + clientServer: MatrixClientServer, + dst: string, + clientSecret: string, + sendAttempt: number, + verificationTemplate: string, + transport: SmsSender, + res: any, + sid: string, + nextLink?: string +): void => { + clientServer.matrixDb + .createOneTimeToken(sid, clientServer.conf.mail_link_delay, nextLink) + .then((token) => { + void transport.sendSMS({ + to: dst, + raw: smsBody(verificationTemplate, token, clientSecret, sid) + }) + clientServer.matrixDb + .insert('threepid_validation_session', { + client_secret: clientSecret, + address: dst, + medium: 'msisdn', + session_id: sid, + last_send_attempt: sendAttempt + }) + .then(() => { + send( + res, + 200, + { sid, submit_url: getSubmitUrl(clientServer.conf) }, + clientServer.logger + ) + }) + .catch((err) => { + // istanbul ignore next + clientServer.logger.error('Insertion error:', err) + // istanbul ignore next + send(res, 500, errMsg('unknown', err.toString()), clientServer.logger) + }) + }) + .catch((err) => { + /* istanbul ignore next */ + clientServer.logger.error('Token error:', err) + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', err.toString()), clientServer.logger) + }) +} + +const RequestToken = (clientServer: MatrixClientServer): expressAppHandler => { + const transport = new SmsSender(clientServer.conf) + const verificationTemplate = preConfigureTemplate( + fs + .readFileSync(`${clientServer.conf.template_dir}/smsVerification.tpl`) + .toString(), + clientServer.conf, + transport + ) + return (req, res) => { + jsonContent(req, res, clientServer.logger, (obj) => { + validateParameters(res, schema, obj, clientServer.logger, (obj) => { + const clientSecret = (obj as RequestTokenArgs).client_secret + const sendAttempt = (obj as RequestTokenArgs).send_attempt + const country = (obj as RequestTokenArgs).country + const phoneNumber = (obj as RequestTokenArgs).phone_number + const dst = formatPhoneNumber(phoneNumber, country) + const nextLink = (obj as RequestTokenArgs).next_link + if (!isClientSecretValid(clientSecret)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid client_secret'), + clientServer.logger + ) + } else if (!isCountryValid(country)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid country'), + clientServer.logger + ) + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + } else if (nextLink && !isValidUrl(nextLink)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid next_link'), + clientServer.logger + ) + } else if (!isPhoneNumberValid(dst)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid phone number'), + clientServer.logger + ) + } else if ( + typeof sendAttempt !== 'number' || + sendAttempt > maxAttemps + ) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid send attempt'), + clientServer.logger + ) + } else { + clientServer.matrixDb + .get('user_threepids', ['user_id'], { address: dst }) + .then((rows) => { + if (rows.length > 0) { + send(res, 400, errMsg('threepidInUse'), clientServer.logger) + } else { + clientServer.matrixDb + .get( + 'threepid_validation_session', + ['last_send_attempt', 'session_id'], + { + client_secret: clientSecret, + address: dst + } + ) + .then((rows) => { + if (rows.length > 0) { + if (sendAttempt === rows[0].last_send_attempt) { + send( + res, + 200, + { + sid: rows[0].session_id, + submit_url: getSubmitUrl(clientServer.conf) + }, + clientServer.logger + ) + } else { + clientServer.matrixDb + .deleteWhere('threepid_validation_session', [ + { + field: 'client_secret', + value: clientSecret, + operator: '=' + }, + { + field: 'session_id', + value: rows[0].session_id as string, + operator: '=' + } + ]) + .then(() => { + fillTableAndSend( + // The calls to send are made in this function + clientServer, + dst, + clientSecret, + sendAttempt, + verificationTemplate, + transport, + res, + rows[0].session_id as string, + nextLink + ) + }) + .catch((err) => { + // istanbul ignore next + clientServer.logger.error('Deletion error:', err) + // istanbul ignore next + send( + res, + 500, + errMsg('unknown', err.toString()), + clientServer.logger + ) + }) + } + } else { + fillTableAndSend( + // The calls to send are made in this function + clientServer, + dst, + clientSecret, + sendAttempt, + verificationTemplate, + transport, + res, + randomString(64), + nextLink + ) + } + }) + .catch((err) => { + /* istanbul ignore next */ + clientServer.logger.error('Send_attempt error:', err) + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', err.toString()), + clientServer.logger + ) + }) + } + }) + .catch((err) => { + /* istanbul ignore next */ + clientServer.logger.error('Error getting userID :', err) + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', err.toString()), + clientServer.logger + ) + }) + } + }) + }) + } +} + +export default RequestToken diff --git a/packages/matrix-client-server/src/register/register.test.ts b/packages/matrix-client-server/src/register/register.test.ts new file mode 100644 index 00000000..39b445d4 --- /dev/null +++ b/packages/matrix-client-server/src/register/register.test.ts @@ -0,0 +1,871 @@ +import { getLogger, type TwakeLogger } from '@twake/logger' +import { type Response } from 'supertest' +import ClientServer from '../index' +import { type Config } from '../types' +import express from 'express' +import defaultConfig from '../__testData__/registerConf.json' +import { buildMatrixDb, buildUserDB } from '../__testData__/buildUserDB' +import fs from 'fs' +import request from 'supertest' +import { setupTokens, validToken } from '../__testData__/setupTokens' + +jest.mock('node-fetch', () => jest.fn()) +const sendMailMock = jest.fn() +jest.mock('nodemailer', () => ({ + createTransport: jest.fn().mockImplementation(() => ({ + sendMail: sendMailMock + })) +})) +const sendSMSMock = jest.fn() +jest.mock('../utils/smsSender', () => { + return jest.fn().mockImplementation(() => { + return { + sendSMS: sendSMSMock + } + }) +}) + +let conf: Config +let clientServer: ClientServer +let app: express.Application + +const logger: TwakeLogger = getLogger() + +const policies = { + privacy_policy: { + en: { + name: 'Privacy Policy', + url: 'https://example.org/somewhere/privacy-1.2-en.html' + }, + fr: { + name: 'Politique de confidentialité', + url: 'https://example.org/somewhere/privacy-1.2-fr.html' + }, + version: '1.2' + }, + terms_of_service: { + en: { + name: 'Terms of Service', + url: 'https://example.org/somewhere/terms-2.0-en.html' + }, + fr: { + name: "Conditions d'utilisation", + url: 'https://example.org/somewhere/terms-2.0-fr.html' + }, + version: '2.0' + } +} + +beforeAll((done) => { + // @ts-expect-error TS doesn't understand that the config is valid + conf = { + ...defaultConfig, + cron_service: false, + database_engine: 'sqlite', + base_url: 'http://example.com/', + userdb_engine: 'sqlite', + matrix_database_engine: 'sqlite', + matrix_database_host: 'testMatrixRegister.db', + database_host: 'testRegister.db', + userdb_host: 'testRegister.db', + policies + } + if (process.env.TEST_PG === 'yes') { + conf.database_engine = 'pg' + conf.userdb_engine = 'pg' + conf.database_host = process.env.PG_HOST ?? 'localhost' + conf.database_user = process.env.PG_USER ?? 'twake' + conf.database_password = process.env.PG_PASSWORD ?? 'twake' + conf.database_name = process.env.PG_DATABASE ?? 'test' + } + buildUserDB(conf) + .then(() => { + buildMatrixDb(conf) + .then(() => { + done() + }) + .catch((e) => { + logger.error('Error while building matrix db:', e) + done(e) + }) + }) + .catch((e) => { + logger.error('Error while building user db:', e) + done(e) + }) +}) + +afterAll(() => { + fs.unlinkSync('testRegister.db') + fs.unlinkSync('testMatrixRegister.db') +}) + +beforeEach(() => { + jest.clearAllMocks() +}) + +describe('Use configuration file', () => { + beforeAll((done) => { + clientServer = new ClientServer(conf) + app = express() + app.set('trust proxy', 1) + clientServer.ready + .then(() => { + Object.keys(clientServer.api.get).forEach((k) => { + app.get(k, clientServer.api.get[k]) + }) + Object.keys(clientServer.api.post).forEach((k) => { + app.post(k, clientServer.api.post[k]) + }) + Object.keys(clientServer.api.put).forEach((k) => { + app.put(k, clientServer.api.put[k]) + }) + Object.keys(clientServer.api.delete).forEach((k) => { + app.delete(k, clientServer.api.delete[k]) + }) + done() + }) + .catch((e) => { + done(e) + }) + }) + + afterAll(() => { + clientServer.cleanJobs() + }) + describe('/_matrix/client/v3/register', () => { + let session: string + let guestToken: string + beforeAll(async () => { + await setupTokens(clientServer, logger) + }) + describe('User Interactive Authentication', () => { + let token: string + let sid: string + it('should validate user interactive authentication with a registration_token', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({}) // empty request to get authentication types + session = response.body.session + await clientServer.matrixDb.insert('registration_tokens', { + token: validToken, + uses_allowed: 100, + pending: 0, + completed: 0 + }) + const response2 = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ + auth: { + type: 'm.login.registration_token', + token: validToken, + session + } + }) + expect(response2.statusCode).toBe(200) + expect(response2.body).toHaveProperty('user_id') + expect(response2.body).toHaveProperty('access_token') + expect(response2.body).toHaveProperty('device_id') + }) + it('should invalidate a registration_token after it has been used too many times for user-interactive-authentication', async () => { + await clientServer.matrixDb.insert('registration_tokens', { + token: 'exampleToken', + uses_allowed: 10, + pending: 8, + completed: 4 + }) + const response1 = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ + username: 'new_user', + device_id: 'device_Id', + inhibit_login: true, + initial_device_display_name: 'testdevice' + }) + expect(response1.statusCode).toBe(401) + session = response1.body.session + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ + auth: { + type: 'm.login.registration_token', + token: 'exampleToken', + session + } + }) + expect(response.statusCode).toBe(401) + expect(response.body).toHaveProperty('error') + expect(response.body).toHaveProperty('errcode') + }) + it('should accept authentication with m.login.email.identity', async () => { + const response1 = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ + username: 'new_user', + device_id: 'device_Id', + inhibit_login: true, + initial_device_display_name: 'testdevice' + }) + expect(response1.statusCode).toBe(401) + session = response1.body.session + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ + auth: { + type: 'm.login.email.identity', + session, + threepid_creds: { + sid: 'validatedSession2', + client_secret: 'validatedSecret2' + } + } + }) + expect(response.statusCode).toBe(200) + }) + it('should refuse autenticating an appservice without a token', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .query({ kind: 'user' }) + .send({ + auth: { + type: 'm.login.application_service', + username: '_irc_bridge_' + } + }) + expect(response.statusCode).toBe(401) + expect(response.body).toHaveProperty('error') + expect(response.body).toHaveProperty('errcode', 'M_MISSING_TOKEN') + }) + it('should refuse authenticating an appservice with the wrong token', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('Authorization', `Bearer wrongToken`) + .query({ kind: 'user' }) + .send({ + auth: { + type: 'm.login.application_service', + username: '_irc_bridge_' + } + }) + expect(response.statusCode).toBe(401) + expect(response.body).toHaveProperty('error') + expect(response.body).toHaveProperty('errcode', 'M_UNKNOWN_TOKEN') + }) + it('should refuse authenticating an appservice with a username that is too long', async () => { + const asToken = conf.application_services[0].as_token + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('Authorization', `Bearer ${asToken}`) + .query({ kind: 'user' }) + .send({ + auth: { + type: 'm.login.application_service', + username: 'invalidUser' + } + }) + expect(response.statusCode).toBe(401) + expect(response.body).toHaveProperty('error') + expect(response.body).toHaveProperty('errcode', 'M_INVALID_USERNAME') + }) + it('should refuse authenticating an appservice with a username it has not registered', async () => { + const asToken = conf.application_services[0].as_token + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('Authorization', `Bearer ${asToken}`) + .query({ kind: 'user' }) + .send({ + auth: { + type: 'm.login.application_service', + username: 'user' + } + }) + expect(response.statusCode).toBe(401) + expect(response.body).toHaveProperty('error') + expect(response.body).toHaveProperty('errcode', 'M_INVALID_USERNAME') + }) + it('should validate an authentication after the user has accepted the terms', async () => { + const response1 = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ + username: 'new_user', + device_id: 'device_Id', + inhibit_login: true, + initial_device_display_name: 'testdevice' + }) + expect(response1.statusCode).toBe(401) + session = response1.body.session + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ + auth: { + type: 'm.login.terms', + session + } + }) + expect(response.statusCode).toBe(200) + }) + it('should refuse authenticating a user with an unknown 3pid for UI Auth', async () => { + const response1 = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ password: 'password' }) + expect(response1.statusCode).toBe(401) + session = response1.body.session + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .set('Accept', 'application/json') + .send({ + auth: { + type: 'm.login.msisdn', + session, + threepid_creds: { sid: 'sid', client_secret: 'mysecret' } // Unknown 3pid + } + }) + expect(response.statusCode).toBe(401) + expect(response.body).toHaveProperty('errcode', 'M_NO_VALID_SESSION') + }) + it('should refuse authenticating a user whose session has not been validated', async () => { + const requestTokenResponse = await request(app) + .post('/_matrix/client/v3/register/msisdn/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'secret', + country: 'FR', + phone_number: '000000000', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(requestTokenResponse.statusCode).toBe(200) + expect(sendSMSMock.mock.calls[0][0].raw).toMatch( + /token=([a-zA-Z0-9]{64})&client_secret=secret&sid=([a-zA-Z0-9]{64})/ + ) + token = RegExp.$1 + sid = RegExp.$2 + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('Accept', 'application/json') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .set('Accept', 'application/json') + .send({ + auth: { + type: 'm.login.msisdn', + session, + threepid_creds: { sid, client_secret: 'secret' } + } + }) + expect(response.statusCode).toBe(401) + expect(response.body).toHaveProperty( + 'errcode', + 'M_SESSION_NOT_VALIDATED' + ) + }) + it('should refuse authenticating a user with an email that has not been added to a matrix userId', async () => { + const submitTokenResponse = await request(app) + .post('/_matrix/client/v3/register/email/submitToken') + .send({ + token, + client_secret: 'secret', + sid + }) + .set('Accept', 'application/json') + expect(submitTokenResponse.statusCode).toBe(200) + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('Accept', 'application/json') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .set('Accept', 'application/json') + .send({ + auth: { + type: 'm.login.msisdn', + session, + threepid_creds: { sid, client_secret: 'secret' } + } + }) + expect(response.statusCode).toBe(401) + expect(response.body).toHaveProperty('errcode', 'M_THREEPID_NOT_FOUND') + }) + it('should refuse authenticating with an unknown session Id', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .set('Accept', 'application/json') + .send({ + auth: { + type: 'm.login.msisdn', + session: 'unknownSession', + threepid_creds: { sid, client_secret: 'secret' } + } + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_NO_VALID_SESSION') + }) + it('should refuse authenticating if the uri changes during the process', async () => { + const response1 = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .set('Accept', 'application/json') + .send({}) + expect(response1.statusCode).toBe(401) + session = response1.body.session + const response = await request(app) + .post('/_matrix/client/v3/account/3pid/add') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken}`) + .send({ + sid: 'sid', + client_secret: 'clientsecret', + auth: { + type: 'm.login.email.identity', + threepid_creds: { sid: 'sid', client_secret: 'clientsecret' }, + session + } + }) + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + }) + }) + it('should send the flows for userInteractiveAuthentication', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({}) // Request without auth parameter so that the server sends the authentication flows + expect(response.statusCode).toBe(401) + expect(response.body).toHaveProperty('flows') + expect(response.body).toHaveProperty('session') + session = response.body.session + }) + it('should refuse an invalid password', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ password: 400 }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid password') + }) + it('should refuse an invalid initial_device_display_name', async () => { + let initialDeviceDisplayName = '' + for (let i = 0; i < 1000; i++) { + initialDeviceDisplayName += 'a' + } + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .query({ kind: 'user' }) + .send({ initial_device_display_name: initialDeviceDisplayName }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty( + 'error', + 'Invalid initial_device_display_name' + ) + }) + it('should refuse an invalid username', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ + auth: { + type: 'm.login.dummy', + session: 'session' + }, + username: '@localhost:example.com' + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('error') + expect(response.body).toHaveProperty('errcode', 'M_INVALID_USERNAME') + }) + it('should refuse an invalid deviceId', async () => { + let deviceId = '' + for (let i = 0; i < 1000; i++) { + deviceId += 'a' + } + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ device_id: deviceId }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid device_id') + }) + it('should refuse an invalid inhibit_login', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ inhibit_login: 'true' }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid inhibit_login') + }) + it('should refuse an invalid auth', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ + auth: { type: 'wrongtype' } + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid auth') + }) + it('should refuse an invalid refresh_token', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ + refresh_token: 'notaboolean' + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid refresh_token') + }) + it('should run the register endpoint after authentication was completed', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ + auth: { type: 'm.login.dummy', session }, + username: 'newuser', + password: 'newpassword', + device_id: 'deviceId', + inhibit_login: false, + initial_device_display_name: 'testdevice' + }) + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('user_id') + expect(response.body).toHaveProperty('access_token') + expect(response.body).toHaveProperty('device_id') + }) + it('should refuse an invalid kind', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'wrongkind' }) + .send({}) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty( + 'error', + 'Kind must be either "guest" or "user"' + ) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should refuse an invalid refresh_token', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.1113.195') + .query({ kind: 'user' }) + .send({ + refresh_token: 'notaboolean' + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid refresh_token') + }) + it('should refuse an invalid password', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.1113.195') + .query({ kind: 'user' }) + .send({ password: 400 }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid password') + }) + it('should refuse an invalid initial_device_display_name', async () => { + let initialDeviceDisplayName = '' + for (let i = 0; i < 1000; i++) { + initialDeviceDisplayName += 'a' + } + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.1113.195') + .query({ kind: 'user' }) + .send({ + initial_device_display_name: initialDeviceDisplayName + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty( + 'error', + 'Invalid initial_device_display_name' + ) + }) + it('should refuse an invalid deviceId', async () => { + let deviceId = '' + for (let i = 0; i < 1000; i++) { + deviceId += 'a' + } + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.1113.195') + .query({ kind: 'user' }) + .send({ + device_id: deviceId + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid device_id') + }) + it('should only return the userId when inhibit login is set to true', async () => { + const response1 = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ + username: 'new_user', + device_id: 'device_Id', + inhibit_login: true, + initial_device_display_name: 'testdevice' + }) + expect(response1.statusCode).toBe(401) + session = response1.body.session + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ + auth: { type: 'm.login.dummy', session }, + username: 'new_user', + device_id: 'device_Id', + inhibit_login: true, + initial_device_display_name: 'testdevice' + }) + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('user_id') + expect(response.body).not.toHaveProperty('expires_in_ms') + expect(response.body).not.toHaveProperty('access_token') + expect(response.body).not.toHaveProperty('device_id') + }) + it('should accept guest registration', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'guest' }) + .send({}) + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('user_id') + expect(response.body).toHaveProperty('access_token') + expect(response.body).toHaveProperty('device_id') + guestToken = response.body.access_token + }) + it('should refuse to upgrade a guest account if no username is provided', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'guest', guest_access_token: guestToken }) + .send({}) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('error') + expect(response.body).toHaveProperty('errcode', 'M_MISSING_PARAMS') + }) + it('should refuse to upgrade a guest account with the wrong token', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'guest', guest_access_token: 'wrongToken' }) + .send({ username: 'guest' }) + expect(response.statusCode).toBe(401) + expect(response.body).toHaveProperty('error') + expect(response.body).toHaveProperty('errcode', 'M_UNKNOWN_TOKEN') + }) + it('should upgrade a guest account if all parameters are present', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'guest', guest_access_token: guestToken }) + .send({ + username: 'guest', + password: 'newpassword', + refresh_token: true + }) + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('user_id') + }) + it('should refuse to upgrade a guest account with a wrong deviceId', async () => { + let deviceId = '' + for (let i = 0; i < 1000; i++) { + deviceId += 'b' + } + const response1 = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.1113.195') + .query({ kind: 'guest' }) + .send({}) + expect(response1.statusCode).toBe(200) + expect(response1.body).toHaveProperty('access_token') + guestToken = response1.body.access_token + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.1113.195') + .query({ guest_access_token: guestToken, kind: 'guest' }) + .send({ + username: 'guest2', + device_id: deviceId + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid device_id') + }) + it('should refuse a username that is already in use', async () => { + const response1 = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ + username: 'new_user', + device_id: 'device_Id', + inhibit_login: true, + initial_device_display_name: 'testdevice' + }) + expect(response1.statusCode).toBe(401) + session = response1.body.session + const response = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ + username: 'newuser', + auth: { type: 'm.login.dummy', session } + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('error') + expect(response.body).toHaveProperty('errcode', 'M_USER_IN_USE') + }) + // The following test might be necessary but spec is unclear so it is commented out for now + + // it('should refuse a request without User Agent', async () => { + // const response = await request(app) + // .post('/_matrix/client/v3/register') + // .set('X-Forwarded-For', '203.0.113.195') + // .query({ kind: 'user' }) + // .send({ + // username: 'newuser', + // auth: { type: 'm.login.dummy', session: randomString(20) } + // }) + // expect(response.statusCode).toBe(400) + // expect(response.body).toHaveProperty('error') + // expect(response.body).toHaveProperty('errcode', 'M_MISSING_PARAMS') + // }) + }) + describe('/_matrix/client/v3/register/available', () => { + it('should reject if more than 100 requests are done in less than 10 seconds', async () => { + let response + // eslint-disable-next-line @typescript-eslint/no-for-in-array, @typescript-eslint/no-unused-vars + for (const i in [...Array(101).keys()]) { + response = await request(app) + .get('/_matrix/client/v3/register/available') + .query({ username: `@username${i}:example.org` }) + .set('Accept', 'application/json') + } + expect((response as Response).statusCode).toEqual(429) + await new Promise((resolve) => setTimeout(resolve, 11000)) + }) + it('should refuse an invalid username', async () => { + const response = await request(app) + .get('/_matrix/client/v3/register/available') + .query({ username: 'invalidUsername' }) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('error') + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should refuse a username that is in an exclusive namespace', async () => { + const response = await request(app) + .get('/_matrix/client/v3/register/available') + .query({ username: '@_irc_bridge_:example.com' }) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('error') + expect(response.body).toHaveProperty('errcode', 'M_EXCLUSIVE') + }) + it('should refuse a username that is already in use', async () => { + const response = await request(app) + .get('/_matrix/client/v3/register/available') + .query({ username: '@newuser:example.com' }) // registered in a previous test + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('error') + expect(response.body).toHaveProperty('errcode', 'M_USER_IN_USE') + }) + it('should accept a username that is available', async () => { + const response = await request(app) + .get('/_matrix/client/v3/register/available') + .query({ username: '@newuser2:example.com' }) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('available', true) + }) + }) +}) diff --git a/packages/matrix-client-server/src/requestToken.test.ts b/packages/matrix-client-server/src/requestToken.test.ts new file mode 100644 index 00000000..b3d77176 --- /dev/null +++ b/packages/matrix-client-server/src/requestToken.test.ts @@ -0,0 +1,862 @@ +import fs from 'fs' +import request from 'supertest' +import express from 'express' +import ClientServer from './index' +import { buildMatrixDb, buildUserDB } from './__testData__/buildUserDB' +import { type Config } from './types' +import defaultConfig from './__testData__/registerConf.json' +import { getLogger, type TwakeLogger } from '@twake/logger' +import { epoch } from '@twake/utils' +import { getSubmitUrl } from './register/email/requestToken' + +jest.mock('node-fetch', () => jest.fn()) +const sendMailMock = jest.fn() +jest.mock('nodemailer', () => ({ + createTransport: jest.fn().mockImplementation(() => ({ + sendMail: sendMailMock + })) +})) + +const sendSMSMock = jest.fn() +jest.mock('./utils/smsSender', () => { + return jest.fn().mockImplementation(() => { + return { + sendSMS: sendSMSMock + } + }) +}) + +let conf: Config +let clientServer: ClientServer +let app: express.Application +let token: string +let sid: string + +const logger: TwakeLogger = getLogger() + +beforeAll((done) => { + // @ts-expect-error TS doesn't understand that the config is valid + conf = { + ...defaultConfig, + base_url: 'http://example.com/', + matrix_database_host: 'src/__testData__/testMatrixRequestToken.db', + userdb_host: 'src/__testData__/testRequestToken.db', + database_host: 'src/__testData__/testRequestToken.db' + } + if (process.env.TEST_PG === 'yes') { + conf.database_engine = 'pg' + conf.userdb_engine = 'pg' + conf.database_host = process.env.PG_HOST ?? 'localhost' + conf.database_user = process.env.PG_USER ?? 'twake' + conf.database_password = process.env.PG_PASSWORD ?? 'twake' + conf.database_name = process.env.PG_DATABASE ?? 'test' + } + buildUserDB(conf) + .then(() => { + buildMatrixDb(conf) + .then(() => { + done() + }) + .catch((e) => { + logger.error('Error while building matrix db:', e) + done(e) + }) + }) + .catch((e) => { + logger.error('Error while building user db:', e) + done(e) + }) +}) + +afterAll(() => { + fs.unlinkSync('src/__testData__/testRequestToken.db') + fs.unlinkSync('src/__testData__/testMatrixRequestToken.db') +}) + +beforeEach(() => { + jest.clearAllMocks() +}) + +describe('Use configuration file', () => { + beforeAll((done) => { + clientServer = new ClientServer(conf) + app = express() + clientServer.ready + .then(() => { + Object.keys(clientServer.api.get).forEach((k) => { + app.get(k, clientServer.api.get[k]) + }) + Object.keys(clientServer.api.post).forEach((k) => { + app.post(k, clientServer.api.post[k]) + }) + Object.keys(clientServer.api.put).forEach((k) => { + app.put(k, clientServer.api.put[k]) + }) + Object.keys(clientServer.api.delete).forEach((k) => { + app.delete(k, clientServer.api.delete[k]) + }) + done() + }) + .catch((e) => { + done(e) + }) + }) + + afterAll(() => { + clientServer.cleanJobs() + }) + describe('/_matrix/client/v3/register/email/requestToken', () => { + it('should refuse to register an invalid email', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register/email/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + email: '@yadd:debian.org', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(response.statusCode).toBe(400) + expect(sendMailMock).not.toHaveBeenCalled() + }) + it('should refuse an invalid secret', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register/email/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'my', + email: 'yadd@debian.org', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(response.statusCode).toBe(400) + expect(sendMailMock).not.toHaveBeenCalled() + }) + it('should refuse an invalid next_link', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register/email/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + email: 'yadd@debian.org', + next_link: 'wrong link', + send_attempt: 1 + }) + expect(response.statusCode).toBe(400) + expect(sendMailMock).not.toHaveBeenCalled() + }) + it('should refuse a send_attempt that is not a number', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register/email/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + email: 'yadd@debian.org', + next_link: 'http://localhost:8090', + send_attempt: 'NaN' + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid send attempt') + expect(sendMailMock).not.toHaveBeenCalled() + }) + it('should refuse a send_attempt that is too large', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register/email/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + email: 'yadd@debian.org', + next_link: 'http://localhost:8090', + send_attempt: 999999999999 + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid send attempt') + expect(sendMailMock).not.toHaveBeenCalled() + }) + it('should accept valid email registration query', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register/email/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + email: 'xg@xnr.fr', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(response.statusCode).toBe(200) + expect(sendMailMock.mock.calls[0][0].to).toBe('xg@xnr.fr') + expect(sendMailMock.mock.calls[0][0].raw).toMatch( + /token=([a-zA-Z0-9]{64})&client_secret=mysecret&sid=([a-zA-Z0-9]{64})/ + ) + token = RegExp.$1 + sid = RegExp.$2 + }) + it('should not resend an email for the same attempt', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register/email/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + email: 'xg@xnr.fr', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(response.statusCode).toBe(200) + expect(sendMailMock).not.toHaveBeenCalled() + expect(response.body).toEqual({ + sid, + submit_url: getSubmitUrl(clientServer.conf) + }) + }) + it('should resend an email for a different attempt', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register/email/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + email: 'xg@xnr.fr', + next_link: 'http://localhost:8090', + send_attempt: 2 + }) + expect(response.statusCode).toBe(200) + expect(sendMailMock.mock.calls[0][0].to).toBe('xg@xnr.fr') + expect(sendMailMock.mock.calls[0][0].raw).toMatch( + /token=([a-zA-Z0-9]{64})&client_secret=mysecret&sid=([a-zA-Z0-9]{64})/ + ) + const newSid = RegExp.$2 + expect(response.body).toEqual({ + sid: newSid, + submit_url: getSubmitUrl(clientServer.conf) + }) + expect(sendMailMock).toHaveBeenCalled() + }) + it('should refuse to send an email to an already existing user', async () => { + await clientServer.matrixDb.insert('user_threepids', { + user_id: '@xg:localhost', + medium: 'email', + address: 'xg@localhost.com', + validated_at: epoch(), + added_at: epoch() + }) + const response = await request(app) + .post('/_matrix/client/v3/register/email/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + email: 'xg@localhost.com', + send_attempt: 1 + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_THREEPID_IN_USE') + expect(sendMailMock).not.toHaveBeenCalled() + }) + }) + + describe('/_matrix/client/v3/register/email/submitToken', () => { + it('should reject registration with a missing parameter', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register/email/submitToken') + .send({ + token, + sid + }) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + }) + it('should reject registration with wrong parameters', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register/email/submitToken') + .send({ + token, + client_secret: 'wrongclientsecret', + sid: 'wrongSid' + }) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + }) + it('should accept to register mail after click', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register/email/submitToken') + .send({ + token, + client_secret: 'mysecret', + sid + }) + .set('Accept', 'application/json') + expect(response.body).toEqual({ success: true }) + expect(response.statusCode).toBe(200) + }) + it('should refuse a second registration', async () => { + const response = await request(app) + .get('/_matrix/client/v3/register/email/submitToken') + .query({ + token, + client_secret: 'mysecret', + sid + }) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + }) + it('should redirect to the next_link if it was provided in requestToken with the GET method', async () => { + const requestTokenResponse = await request(app) + .post('/_matrix/client/v3/register/email/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret2', + email: 'abc@abcd.fr', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(requestTokenResponse.statusCode).toBe(200) + expect(sendMailMock.mock.calls[0][0].raw).toMatch( + /token=([a-zA-Z0-9]{64})&client_secret=mysecret2&sid=([a-zA-Z0-9]{64})/ + ) + sid = RegExp.$2 + token = RegExp.$1 + const response = await request(app) + .get('/_matrix/client/v3/register/email/submitToken') + .query({ + client_secret: 'mysecret2', + token, + sid + }) + expect(response.status).toBe(302) + expect(response.headers.location).toBe( + new URL('http://localhost:8090').toString() + ) + }) + }) + + describe('/_matrix/client/v3/register/msisdn/requestToken', () => { + it('should refuse to register an invalid phone number', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register/msisdn/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + phone_number: '@yadd:debian.org', + country: 'FR', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid phone number') + expect(sendSMSMock).not.toHaveBeenCalled() + }) + it('should refuse to register an invalid country', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register/msisdn/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + phone_number: '0618384839', + country: '123', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid country') + expect(sendSMSMock).not.toHaveBeenCalled() + }) + it('should refuse an invalid secret', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register/msisdn/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'my', + phone_number: '0618384839', + country: 'FR', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid client_secret') + expect(sendSMSMock).not.toHaveBeenCalled() + }) + it('should refuse an invalid next_link', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register/msisdn/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + phone_number: '0618384839', + country: 'FR', + next_link: 'wrong link', + send_attempt: 1 + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid next_link') + expect(sendSMSMock).not.toHaveBeenCalled() + }) + it('should refuse a send_attempt that is not a number', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register/msisdn/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + phone_number: '0618384839', + country: 'FR', + next_link: 'http://localhost:8090', + send_attempt: 'NaN' + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid send attempt') + expect(sendSMSMock).not.toHaveBeenCalled() + }) + it('should refuse a send_attempt that is too large', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register/msisdn/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + phone_number: '0618384839', + country: 'FR', + next_link: 'http://localhost:8090', + send_attempt: 999999999999 + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid send attempt') + expect(sendSMSMock).not.toHaveBeenCalled() + }) + // this test is expected to work with the current behaviour of the sendSMS function which is to write in a file, and not to send a real SMS + it('should accept valid phone number registration query', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register/msisdn/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + country: 'GB', + phone_number: '07700900001', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(response.statusCode).toBe(200) + const sentSMS = sendSMSMock.mock.calls[0][0] + expect(sentSMS.to).toBe('447700900001') + const rawMessage = sentSMS.raw + expect(rawMessage).toMatch( + /token=([a-zA-Z0-9]{64})&client_secret=mysecret&sid=([a-zA-Z0-9]{64})/ + ) + const tokenMatch = rawMessage.match(/token=([a-zA-Z0-9]{64})/) + const sidMatch = rawMessage.match(/sid=([a-zA-Z0-9]{64})/) + expect(tokenMatch).not.toBeNull() + expect(sidMatch).not.toBeNull() + if (tokenMatch != null) token = tokenMatch[1] + if (sidMatch != null) sid = sidMatch[1] + }) + it('should not resend an SMS for the same attempt', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register/msisdn/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + country: 'GB', + phone_number: '07700900001', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(response.statusCode).toBe(200) + expect(sendSMSMock).not.toHaveBeenCalled() + expect(response.body).toEqual({ + sid, + submit_url: getSubmitUrl(clientServer.conf) + }) + }) + it('should resend an SMS for a different attempt', async () => { + const response = await request(app) + .post('/_matrix/client/v3/register/msisdn/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + country: 'GB', + phone_number: '07700900001', + next_link: 'http://localhost:8090', + send_attempt: 2 + }) + expect(response.statusCode).toBe(200) + const sentSMS = sendSMSMock.mock.calls[0][0] + expect(sentSMS.to).toBe('447700900001') + const rawMessage = sentSMS.raw + expect(rawMessage).toMatch( + /token=([a-zA-Z0-9]{64})&client_secret=mysecret&sid=([a-zA-Z0-9]{64})/ + ) + const sidMatch = rawMessage.match(/sid=([a-zA-Z0-9]{64})/) + expect(sidMatch).not.toBeNull() + const newSid = sidMatch[1] + expect(response.body).toEqual({ + sid: newSid, + submit_url: getSubmitUrl(clientServer.conf) + }) + expect(sendSMSMock).toHaveBeenCalled() + }) + it('should refuse to send an SMS to an already existing user', async () => { + await clientServer.matrixDb.insert('user_threepids', { + user_id: '@xg:localhost', + medium: 'msisdn', + address: '33648394785', + validated_at: epoch(), + added_at: epoch() + }) + const response = await request(app) + .post('/_matrix/client/v3/register/msisdn/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + phone_number: '0648394785', + country: 'FR', + send_attempt: 1 + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_THREEPID_IN_USE') + expect(sendSMSMock).not.toHaveBeenCalled() + }) + }) + + describe('/_matrix/client/v3/account/password/email/requestToken', () => { + it('should refuse to register an invalid email', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/password/email/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + email: '@yadd:debian.org', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(response.statusCode).toBe(400) + expect(sendMailMock).not.toHaveBeenCalled() + }) + it('should refuse an invalid secret', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/password/email/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'my', + email: 'yadd@debian.org', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(response.statusCode).toBe(400) + expect(sendMailMock).not.toHaveBeenCalled() + }) + it('should refuse an invalid next_link', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/password/email/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + email: 'yadd@debian.org', + next_link: 'wrong link', + send_attempt: 1 + }) + expect(response.statusCode).toBe(400) + expect(sendMailMock).not.toHaveBeenCalled() + }) + it('should refuse a send_attempt that is not a number', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/password/email/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + email: 'yadd@debian.org', + next_link: 'http://localhost:8090', + send_attempt: 'NaN' + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid send attempt') + expect(sendMailMock).not.toHaveBeenCalled() + }) + it('should refuse a send_attempt that is too large', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/password/email/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + email: 'yadd@debian.org', + next_link: 'http://localhost:8090', + send_attempt: 999999999999 + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid send attempt') + expect(sendMailMock).not.toHaveBeenCalled() + }) + it('should accept valid email registration query', async () => { + await clientServer.matrixDb.insert('user_threepids', { + user_id: '@newuser:localhost', + medium: 'email', + address: 'newuser@localhost.com', + validated_at: epoch(), + added_at: epoch() + }) + const response = await request(app) + .post('/_matrix/client/v3/account/password/email/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + email: 'newuser@localhost.com', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(response.statusCode).toBe(200) + expect(sendMailMock.mock.calls[0][0].to).toBe('newuser@localhost.com') + expect(sendMailMock.mock.calls[0][0].raw).toMatch( + /token=([a-zA-Z0-9]{64})&client_secret=mysecret&sid=([a-zA-Z0-9]{64})/ + ) + sid = RegExp.$2 + }) + it('should not resend an email for the same attempt', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/password/email/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + email: 'newuser@localhost.com', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(response.statusCode).toBe(200) + expect(sendMailMock).not.toHaveBeenCalled() + expect(response.body).toEqual({ + sid, + submit_url: getSubmitUrl(clientServer.conf) + }) + }) + it('should resend an email for a different attempt', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/password/email/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + email: 'newuser@localhost.com', + next_link: 'http://localhost:8090', + send_attempt: 2 + }) + expect(response.statusCode).toBe(200) + expect(sendMailMock.mock.calls[0][0].to).toBe('newuser@localhost.com') + expect(sendMailMock.mock.calls[0][0].raw).toMatch( + /token=([a-zA-Z0-9]{64})&client_secret=mysecret&sid=([a-zA-Z0-9]{64})/ + ) + const newSid = RegExp.$2 + expect(response.body).toEqual({ + sid: newSid, + submit_url: getSubmitUrl(clientServer.conf) + }) + expect(sendMailMock).toHaveBeenCalled() + }) + it('should refuse to send an email to a non-existing user', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/password/email/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + email: 'nonexistinguser@localhost.com', + send_attempt: 1 + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_THREEPID_NOT_FOUND') + expect(sendMailMock).not.toHaveBeenCalled() + }) + }) + + describe('/_matrix/client/v3/account/password/msisdn/requestToken', () => { + it('should refuse to register an invalid phone number', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/password/msisdn/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + phone_number: '@yadd:debian.org', + country: 'FR', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid phone number') + expect(sendSMSMock).not.toHaveBeenCalled() + }) + it('should refuse to register an invalid country', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/password/msisdn/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + phone_number: '0618384839', + country: '123', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid country') + expect(sendSMSMock).not.toHaveBeenCalled() + }) + it('should refuse an invalid secret', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/password/msisdn/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'my', + phone_number: '0618384839', + country: 'FR', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid client_secret') + expect(sendSMSMock).not.toHaveBeenCalled() + }) + it('should refuse a send_attempt that is not a number', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/password/msisdn/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + phone_number: '0618384839', + country: 'FR', + next_link: 'http://localhost:8090', + send_attempt: 'NaN' + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid send attempt') + expect(sendSMSMock).not.toHaveBeenCalled() + }) + it('should refuse a send_attempt that is too large', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/password/msisdn/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + phone_number: '0618384839', + country: 'FR', + next_link: 'http://localhost:8090', + send_attempt: 999999999999 + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid send attempt') + expect(sendSMSMock).not.toHaveBeenCalled() + }) + it('should refuse an invalid next_link', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/password/msisdn/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + phone_number: '0618384839', + country: 'FR', + next_link: 'wrong link', + send_attempt: 1 + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + expect(response.body).toHaveProperty('error', 'Invalid next_link') + expect(sendSMSMock).not.toHaveBeenCalled() + }) + // this test is expected to work with the current behaviour of the sendSMS function which is to write in a file, and not to send a real SMS + it('should accept valid phone number registration query', async () => { + await clientServer.matrixDb.insert('user_threepids', { + user_id: '@newphoneuser:localhost', + medium: 'msisdn', + address: '447700900002', + validated_at: epoch(), + added_at: epoch() + }) + + const response = await request(app) + .post('/_matrix/client/v3/account/password/msisdn/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + country: 'GB', + phone_number: '07700900002', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(response.statusCode).toBe(200) + const sentSMS = sendSMSMock.mock.calls[0][0] + expect(sentSMS.to).toBe('447700900002') + const rawMessage = sentSMS.raw + expect(rawMessage).toMatch( + /token=([a-zA-Z0-9]{64})&client_secret=mysecret&sid=([a-zA-Z0-9]{64})/ + ) + const tokenMatch = rawMessage.match(/token=([a-zA-Z0-9]{64})/) + const sidMatch = rawMessage.match(/sid=([a-zA-Z0-9]{64})/) + expect(tokenMatch).not.toBeNull() + expect(sidMatch).not.toBeNull() + if (tokenMatch != null) token = tokenMatch[1] + if (sidMatch != null) sid = sidMatch[1] + }) + it('should not resend an SMS for the same attempt', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/password/msisdn/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + country: 'GB', + phone_number: '07700900002', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(response.statusCode).toBe(200) + expect(sendSMSMock).not.toHaveBeenCalled() + expect(response.body).toEqual({ + sid, + submit_url: getSubmitUrl(clientServer.conf) + }) + }) + it('should resend an SMS for a different attempt', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/password/msisdn/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + country: 'GB', + phone_number: '07700900002', + next_link: 'http://localhost:8090', + send_attempt: 2 + }) + expect(response.statusCode).toBe(200) + const sentSMS = sendSMSMock.mock.calls[0][0] + expect(sentSMS.to).toBe('447700900002') + const rawMessage = sentSMS.raw + expect(rawMessage).toMatch( + /token=([a-zA-Z0-9]{64})&client_secret=mysecret&sid=([a-zA-Z0-9]{64})/ + ) + const sidMatch = rawMessage.match(/sid=([a-zA-Z0-9]{64})/) + expect(sidMatch).not.toBeNull() + const newSid = sidMatch[1] + expect(response.body).toEqual({ + sid: newSid, + submit_url: getSubmitUrl(clientServer.conf) + }) + expect(sendSMSMock).toHaveBeenCalled() + }) + it('should refuse to send an SMS to a non-existing user', async () => { + const response = await request(app) + .post('/_matrix/client/v3/account/password/msisdn/requestToken') + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + phone_number: '0647392301', + country: 'FR', + send_attempt: 1 + }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_THREEPID_NOT_FOUND') + expect(sendSMSMock).not.toHaveBeenCalled() + }) + }) +}) diff --git a/packages/matrix-client-server/src/rooms/roomId/getEventId.ts b/packages/matrix-client-server/src/rooms/roomId/getEventId.ts new file mode 100644 index 00000000..b2f27071 --- /dev/null +++ b/packages/matrix-client-server/src/rooms/roomId/getEventId.ts @@ -0,0 +1,122 @@ +import type MatrixClientServer from '../..' +import { epoch, errMsg, send, type expressAppHandler } from '@twake/utils' +import { type ClientEvent } from '../../types' +import { isRoomIdValid } from '@twake/utils' + +interface parameters { + eventId: string + roomId: string +} + +const GetEventId = (clientServer: MatrixClientServer): expressAppHandler => { + return (req, res) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const prms: parameters = (req as Request).params as parameters + if (!isRoomIdValid(prms.roomId)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid roomId'), + clientServer.logger + ) + return + } + + clientServer.authenticate(req, res, (data) => { + const requesterUid = data.sub + + // TODO : eventually add redirection with federation here + /* istanbul ignore if */ + if (!clientServer.isMine(requesterUid)) { + send(res, 403, errMsg('forbidden', 'User is not hosted on this server')) + return + } + + // Check for authorization + clientServer.matrixDb + .get('local_current_membership', ['membership'], { + user_id: requesterUid, + room_id: prms.roomId + }) + .then((roomsResult) => { + if ( + roomsResult.length === 0 || + roomsResult[0].membership !== 'join' + ) { + send( + res, + 404, + errMsg('forbidden', 'User is not in the room'), + clientServer.logger + ) + } else { + clientServer.matrixDb + .get( + 'events', + [ + 'content', + 'event_id', + 'origin_server_ts', + 'room_id', + 'sender', + 'state_key', + 'type' + ], + { + event_id: prms.eventId, + room_id: prms.roomId + } + ) + .then((rows) => { + if (rows.length === 0) { + send( + res, + 404, + errMsg( + 'notFound', + 'Cannot retrieve event : event not found' + ), + clientServer.logger + ) + return + } + // TODO : eventually serialize the event before sending it to client as done in Synapse implementation + // This is used for bundling extra information. + const event = rows[0] + const response: ClientEvent = { + content: event.content as Record, + event_id: event.event_id as string, + origin_server_ts: event.origin_server_ts as number, + room_id: event.room_id as string, + sender: event.sender as string, + type: event.type as string, + unsigned: { + age: epoch() - (event.origin_server_ts as number) + } + } + if (event.state_key !== null) { + response.state_key = event.state_key as string + } + send(res, 200, response) + }) + .catch((err) => { + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', err.toString()), + clientServer.logger + ) + }) + } + }) + .catch((e) => { + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', e.toString()), clientServer.logger) + }) + }) + } +} + +export default GetEventId diff --git a/packages/matrix-client-server/src/rooms/roomId/getJoinedMembers.ts b/packages/matrix-client-server/src/rooms/roomId/getJoinedMembers.ts new file mode 100644 index 00000000..9a105a05 --- /dev/null +++ b/packages/matrix-client-server/src/rooms/roomId/getJoinedMembers.ts @@ -0,0 +1,82 @@ +import type MatrixClientServer from '../..' +import { errMsg, send, type expressAppHandler } from '@twake/utils' +import { type RoomMember } from '../../types' + +// TODO : Manage the case where it is an Application Service, in which case any of the AS’s users must be in the room for it to work. +// cf https://spec.matrix.org/v1.11/client-server-api/#get_matrixclientv3roomsroomidjoined_members + +interface parameters { + roomId: string +} + +const GetJoinedMembers = ( + ClientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const roomId: string = ((req as Request).params as parameters).roomId + ClientServer.authenticate(req, res, (data) => { + // Check if the user has permission to retrieve this event + const userId: string = data.sub + ClientServer.matrixDb + .get('local_current_membership', ['membership'], { + user_id: userId, + room_id: roomId + }) + .then((rows) => { + if (rows.length === 0 || rows[0].membership !== 'join') { + send( + res, + 403, + errMsg( + 'notFound', + 'User not in the room - cannot retrieve members' + ), + ClientServer.logger + ) + return + } + ClientServer.matrixDb + .getJoin( + ['local_current_membership', 'profiles'], + [ + 'profiles.user_id', + 'profiles.avatar_url', + 'profiles.displayname' + ], + { + 'local_current_membership.room_id': roomId, + 'local_current_membership.membership': 'join' + }, + { 'local_current_membership.user_id': 'profiles.user_id' } + ) + .then((rows) => { + const joined: Record = {} + for (const row of rows) { + joined[row.profiles_user_id as string] = { + avatar_url: row.profiles_avatar_url as string, + display_name: row.profiles_displayname as string + } + } + send(res, 200, { joined }) + }) + .catch((err) => { + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', err.toString()), + ClientServer.logger + ) + }) + }) + .catch((err) => { + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', err.toString()), ClientServer.logger) + }) + }) + } +} + +export default GetJoinedMembers diff --git a/packages/matrix-client-server/src/rooms/roomId/getState.ts b/packages/matrix-client-server/src/rooms/roomId/getState.ts new file mode 100644 index 00000000..ce43237f --- /dev/null +++ b/packages/matrix-client-server/src/rooms/roomId/getState.ts @@ -0,0 +1,176 @@ +/** + * Implements : https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3roomsroomidstate + * + * Following the spec and Synapse implementation of the matrix Protocol, we have decided that when the requesting user + * left the room, he will have access solely to the event of his departure. + * + * TODO : eventually add check for eventType to prevent invalid requests in the database + */ + +import type MatrixClientServer from '../..' +import { + epoch, + errMsg, + isRoomIdValid, + send, + type expressAppHandler +} from '@twake/utils' +import { type Request } from 'express' + +const getRoomState = (clientServer: MatrixClientServer): expressAppHandler => { + return (req, res) => { + const roomId: string = (req as Request).params.roomId + if (!isRoomIdValid(roomId)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid roomId'), + clientServer.logger + ) + return + } + + clientServer.authenticate(req, res, (data) => { + const requesterUid = data.sub + + // Check if requester is currently in the room or was in it before + clientServer.matrixDb + .get('local_current_membership', ['membership', 'event_id'], { + user_id: requesterUid, + room_id: roomId + }) + .then((rows) => { + if ( + rows.length === 0 || + (rows[0].membership !== 'join' && rows[0].membership !== 'leave') + ) { + send( + res, + 403, + errMsg('forbidden', 'User is not and was never part of the room'), + clientServer.logger + ) + } else { + if (rows[0].membership !== 'join') { + // The requester was once part of the room and left it + // We then arbitrarily decided to allow him to get the event of his departure + clientServer.matrixDb + .get( + 'events', + [ + 'content', + 'event_id', + 'origin_server_ts', + 'room_id', + 'type', + 'sender', + 'state_key' + ], + { event_id: rows[0].event_id } + ) + .then((eventResult) => { + /* istanbul ignore if */ + if (eventResult.length === 0) { + send( + res, + 404, + errMsg('unknown', 'Event not found'), + clientServer.logger + ) + return + } + const unsigned = { + age: epoch() - (eventResult[0].origin_server_ts as number), + prev_content: eventResult[0].prev_content + // TODO : Add more unsigned data cf https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3roomsroomidstate + } + + send(res, 200, [ + { + content: eventResult[0].content, + event_id: eventResult[0].event_id, + origin_server_ts: eventResult[0].origin_server_ts, + room_id: eventResult[0].room_id, + sender: eventResult[0].sender, + state_key: eventResult[0].state_key, + type: eventResult[0].type, + unsigned + } + ]) + }) + .catch((err) => { + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', err.toString()), + clientServer.logger + ) + }) + } else { + // The requester is currently in the room + clientServer.matrixDb + .getJoin( + ['events', 'current_state_events'], + [ + 'events.content', + 'events.event_id', + 'events.origin_server_ts', + 'events.room_id', + 'events.type', + 'events.sender', + 'events.state_key' + ], + {}, + { + 'events.event_id': 'current_state_events.event_id', + 'events.room_id': 'current_state_events.room_id', + 'events.type': 'current_state_events.type', + 'events.state_key': 'current_state_events.state_key' + } + ) + .then((eventResult) => { + const state = [] + for (const event of eventResult) { + const unsigned = { + age: + epoch() - + (eventResult[0].events_origin_server_ts as number), + prev_content: eventResult[0].events_prev_content + // TODO : Add more unsigned data cf https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3roomsroomidstate + } + state.push({ + content: event.events_content, + event_id: event.events_event_id, + origin_server_ts: event.events_origin_server_ts, + room_id: event.events_room_id, + sender: event.events_sender, + state_key: event.events_state_key, + type: event.events_type, + unsigned + }) + } + + send(res, 200, state, clientServer.logger) + }) + .catch((err) => { + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', err.toString()), + clientServer.logger + ) + }) + } + } + }) + .catch((err) => { + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', err.toString()), clientServer.logger) + }) + }) + } +} + +export default getRoomState diff --git a/packages/matrix-client-server/src/rooms/roomId/getStateEvent.ts b/packages/matrix-client-server/src/rooms/roomId/getStateEvent.ts new file mode 100644 index 00000000..8a4345bd --- /dev/null +++ b/packages/matrix-client-server/src/rooms/roomId/getStateEvent.ts @@ -0,0 +1,280 @@ +/** + * Implements : https://spec.matrix.org/v1.11/client-server-api/#get_matrixclientv3roomsroomidstateeventtypestatekey + * + * To be noted : This endpoints : /_matrix/client/v3/rooms/{roomId}/state/{eventType}/{stateKey} can be called with no stateKey parameter + * To ensure the same behavior, we have two handlers for this endpoint, one with the stateKey parameter and one without + * In addition, if the stateKey is an empty string, the handler with no stateKey parameter will be called. + * + * Following the spec and Synapse implementation of the matrix Protocol, we have decided that when the requesting user + * left the room, he will have access solely to the event of his departure. + * + * TODO : eventually add check for eventType to prevent invalid requests in the database + */ + +import type MatrixClientServer from '../..' +import { + errMsg, + isRoomIdValid, + send, + type expressAppHandler +} from '@twake/utils' +import { type Request } from 'express' + +const getRoomStateEvent = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + const roomId: string = (req as Request).params.roomId + const eventType = (req as Request).params.eventType + const stateKey = (req as Request).params.stateKey + // TODO : add check for eventType + if (!isRoomIdValid(roomId)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid roomId'), + clientServer.logger + ) + return + } + + clientServer.authenticate(req, res, (data) => { + const requesterUid = data.sub + + // Check if requester is currently in the room or was in it before + clientServer.matrixDb + .get('local_current_membership', ['membership', 'event_id'], { + user_id: requesterUid, + room_id: roomId + }) + .then((rows) => { + if ( + rows.length === 0 || + (rows[0].membership !== 'join' && rows[0].membership !== 'leave') + ) { + send( + res, + 403, + errMsg('forbidden', 'User is not and was never part of the room'), + clientServer.logger + ) + } else { + if (rows[0].membership !== 'join') { + // The requester was once part of the room and left it + // We then arbitrarily decided to allow him to get the event of his departure + clientServer.matrixDb + .get('events', ['content'], { event_id: rows[0].event_id }) + .then((eventResult) => { + /* istanbul ignore if */ + if (eventResult.length === 0) { + send( + res, + 500, + errMsg('unknown', 'Event not found'), + clientServer.logger + ) + return + } + send( + res, + 200, + JSON.parse(eventResult[0].content as string), + clientServer.logger + ) + }) + .catch((err) => { + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', err.toString()), + clientServer.logger + ) + }) + } else { + // The requester is currently in the room + clientServer.matrixDb + .getJoin( + ['events', 'current_state_events'], + ['events.content'], + { + 'events.room_id': roomId, + 'events.type': eventType, + 'events.state_key': stateKey + }, + { + 'events.event_id': 'current_state_events.event_id', + 'events.room_id': 'current_state_events.room_id', + 'events.type': 'current_state_events.type', + 'events.state_key': 'current_state_events.state_key' + } + ) + .then((eventResult) => { + if (eventResult.length === 0) { + send( + res, + 404, + errMsg( + 'notFound', + 'The room has no state with the given type or key.' + ), + clientServer.logger + ) + return + } + send( + res, + 200, + JSON.parse(eventResult[0].events_content as string), + clientServer.logger + ) + }) + .catch((err) => { + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', err.toString()), + clientServer.logger + ) + }) + } + } + }) + .catch((err) => { + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', err.toString()), clientServer.logger) + }) + }) + } +} + +export const getRoomStateEventNoStatekey = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + const roomId: string = (req as Request).params.roomId + const eventType = (req as Request).params.eventType + const stateKey = '' + if (!isRoomIdValid(roomId)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid roomId'), + clientServer.logger + ) + return + } + + clientServer.authenticate(req, res, (data) => { + const requesterUid = data.sub + + // Check if requester is currently in the room or was in it before + clientServer.matrixDb + .get('local_current_membership', ['membership', 'event_id'], { + user_id: requesterUid, + room_id: roomId + }) + .then((rows) => { + if ( + rows.length === 0 || + (rows[0].membership !== 'join' && rows[0].membership !== 'leave') + ) { + send( + res, + 403, + errMsg('forbidden', 'User is not and was never part of the room'), + clientServer.logger + ) + } else { + if (rows[0].membership !== 'join') { + // The requester was once part of the room and left it + // We then arbitrarily decided to allow him to get the event of his departure + clientServer.matrixDb + .get('events', ['content'], { event_id: rows[0].event_id }) + .then((eventResult) => { + /* istanbul ignore if */ + if (eventResult.length === 0) { + send( + res, + 500, + errMsg('unknown', 'Event not found'), + clientServer.logger + ) + return + } + send( + res, + 200, + JSON.parse(eventResult[0].content as string), + clientServer.logger + ) + }) + .catch((err) => { + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', err.toString()), + clientServer.logger + ) + }) + } else { + // The requester is currently in the room + clientServer.matrixDb + .getJoin( + ['events', 'current_state_events'], + ['events.content'], + { + 'events.room_id': roomId, + 'events.type': eventType, + 'events.state_key': stateKey + }, + { + 'events.event_id': 'current_state_events.event_id', + 'events.room_id': 'current_state_events.room_id', + 'events.type': 'current_state_events.type', + 'events.state_key': 'current_state_events.state_key' + } + ) + .then((eventResult) => { + if (eventResult.length === 0) { + send( + res, + 404, + errMsg( + 'notFound', + 'The room has no state with the given type or key.' + ), + clientServer.logger + ) + return + } + send( + res, + 200, + JSON.parse(eventResult[0].events_content as string), + clientServer.logger + ) + }) + .catch((err) => { + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', err.toString()), + clientServer.logger + ) + }) + } + } + }) + .catch((err) => { + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', err.toString()), clientServer.logger) + }) + }) + } +} + +export default getRoomStateEvent diff --git a/packages/matrix-client-server/src/rooms/roomId/getTimestampToEvent.ts b/packages/matrix-client-server/src/rooms/roomId/getTimestampToEvent.ts new file mode 100644 index 00000000..06f13d0b --- /dev/null +++ b/packages/matrix-client-server/src/rooms/roomId/getTimestampToEvent.ts @@ -0,0 +1,107 @@ +import type MatrixClientServer from '../..' +import { errMsg, type expressAppHandler, send } from '@twake/utils' + +interface query_parameters { + dir: 'b' | 'f' + ts: number +} + +const GetTimestampToEvent = ( + ClientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const roomId: string = (req as Request).params.roomId + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const params: query_parameters = (req as Request).query + if (params.dir !== 'b' && params.dir !== 'f') { + send( + res, + 400, + errMsg('invalidParam', 'Invalid parameters'), + ClientServer.logger + ) + return + } + ClientServer.authenticate(req, res, (data, id) => { + if (params.dir === 'b') { + ClientServer.matrixDb + .getMaxWhereEqualAndLower( + 'events', + 'origin_server_ts', + ['event_id', 'origin_server_ts'], + { + room_id: roomId + }, + { + origin_server_ts: params.ts + } + ) + .then((rows) => { + if (rows.length === 0) { + send( + res, + 404, + errMsg( + 'notFound', + `Unable to find event from ${params.ts} in backward direction` + ), + ClientServer.logger + ) + return + } + send(res, 200, rows[0]) + }) + .catch((err) => { + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', err.toString()), + ClientServer.logger + ) + }) + } + if (params.dir === 'f') { + ClientServer.matrixDb + .getMinWhereEqualAndHigher( + 'events', + 'origin_server_ts', + ['event_id', 'origin_server_ts'], + { + room_id: roomId + }, + { origin_server_ts: params.ts } + ) + .then((rows) => { + if (rows.length === 0) { + send( + res, + 404, + errMsg( + 'notFound', + `Unable to find event from ${params.ts} in forward direction` + ), + ClientServer.logger + ) + return + } + send(res, 200, rows[0]) + }) + .catch((err) => { + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', err.toString()), + ClientServer.logger + ) + }) + } + }) + } +} + +export default GetTimestampToEvent diff --git a/packages/matrix-client-server/src/rooms/room_information/get_joined_rooms.ts b/packages/matrix-client-server/src/rooms/room_information/get_joined_rooms.ts new file mode 100644 index 00000000..b4cdae71 --- /dev/null +++ b/packages/matrix-client-server/src/rooms/room_information/get_joined_rooms.ts @@ -0,0 +1,31 @@ +import type MatrixClientServer from '../../index' +import { errMsg, send, type expressAppHandler } from '@twake/utils' + +export const getJoinedRooms = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + clientServer.authenticate(req, res, (data) => { + const userId = data.sub + clientServer.matrixDb + .get('room_memberships', ['room_id'], { + user_id: userId, + membership: 'join', + forgotten: 0 + }) + .then((roomsResult) => { + const roomIds = roomsResult.map((row) => row.room_id) as string[] + send(res, 200, { joined_rooms: roomIds }, clientServer.logger) + }) + .catch((e) => { + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', 'Error querying joined rooms'), + clientServer.logger + ) + }) + }) + } +} diff --git a/packages/matrix-client-server/src/rooms/room_information/room_aliases.ts b/packages/matrix-client-server/src/rooms/room_information/room_aliases.ts new file mode 100644 index 00000000..fea0644f --- /dev/null +++ b/packages/matrix-client-server/src/rooms/room_information/room_aliases.ts @@ -0,0 +1,89 @@ +import type MatrixClientServer from '../../' +import { type Request } from 'express' +import { + errMsg, + isRoomIdValid, + send, + type expressAppHandler +} from '@twake/utils' + +export const getRoomAliases = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + const roomId: string = (req as Request).params.roomId + if (!isRoomIdValid(roomId)) { + send(res, 400, errMsg('invalidParam', 'Invalid room id')) + return + } + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + clientServer.authenticate(req, res, async (data) => { + const userId = data.sub + + try { + // Check the history visibility of the room + const historyResponse = await clientServer.matrixDb.get( + 'room_stats_state', + ['history_visibility'], + { room_id: roomId } + ) + + if (historyResponse.length === 0) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid room id'), + clientServer.logger + ) + return + } + + let accessible = false + if (historyResponse[0].history_visibility === 'world_readable') { + accessible = true + } else { + // Check if the user is a member of the room + const membershipResponse = await clientServer.matrixDb.get( + 'room_memberships', + ['event_id'], + { + room_id: roomId, + user_id: userId, + membership: 'join', + forgotten: 0 + } + ) + + if (membershipResponse.length > 0) { + accessible = true + } + } + + if (!accessible) { + send( + res, + 403, + errMsg( + 'forbidden', + 'The user is not permitted to retrieve the list of local aliases for the room' + ), + clientServer.logger + ) + } else { + // Fetch the room aliases + const aliasRows = await clientServer.matrixDb.get( + 'room_aliases', + ['room_alias'], + { room_id: roomId } + ) + const roomAliases = aliasRows.map((row) => row.room_alias) + send(res, 200, { aliases: roomAliases }, clientServer.logger) + } + } catch (e) { + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', e as string), clientServer.logger) + } + }) + } +} diff --git a/packages/matrix-client-server/src/rooms/room_information/room_tags.ts b/packages/matrix-client-server/src/rooms/room_information/room_tags.ts new file mode 100644 index 00000000..ab950c75 --- /dev/null +++ b/packages/matrix-client-server/src/rooms/room_information/room_tags.ts @@ -0,0 +1,216 @@ +/* + * This file defines handlers for managing room tags in the Matrix client-server API : + * https://spec.matrix.org/v1.11/client-server-api/#client-behaviour-15 + * It includes three main functions: + * + * 1. `getUserRoomTags`: Retrieves tags associated with a user's room. + * + * 2. `addUserRoomTag`: Adds a new tag to a user's room. + * + * 3. `removeUserRoomTag`: Removes a tag from a user's room. + * + * The only part that is not specified in the Matrix Protocol is the access control logic. + * Following Synapse's implementation, we will allow a user to view, add, and remove their tags only. + * + * For now, it remains possible to add tags to a room you are not part of. + * + * Maximum lengths: + * - `room_tag`: 255 characters + */ + +import type MatrixClientServer from '../../' +import { + errMsg, + send, + type expressAppHandler, + jsonContent, + validateParameters, + isMatrixIdValid, + isRoomIdValid +} from '@twake/utils' +import { type Request } from 'express' + +export const getUserRoomTags = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + const userId = (req as Request).params.userId + const roomId = (req as Request).params.roomId + + if (!isMatrixIdValid(userId)) { + send(res, 400, errMsg('invalidParam', 'Invalid userId')) + return + } + if (!isRoomIdValid(roomId)) { + send(res, 400, errMsg('invalidParam', 'Invalid roomId')) + return + } + + clientServer.authenticate(req, res, (data) => { + const requesterUserId = data.sub + + if (requesterUserId !== userId) { + send( + res, + 403, + errMsg('forbidden', 'You are not allowed to view these tags') + ) + return + } + + clientServer.matrixDb + .get('room_tags', ['tag', 'content'], { + user_id: userId, + room_id: roomId + }) + .then((tagRows) => { + const _tags: Record = {} + tagRows.forEach((row) => { + try { + const content = JSON.parse(row.content as string) + /* istanbul ignore else */ + if (content.order !== undefined) { + _tags[row.tag as string] = { order: content.order } + } else { + _tags[row.tag as string] = {} + } + } catch (error) { + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', 'Error parsing room tag content'), + clientServer.logger + ) + } + }) + send(res, 200, { tags: _tags }, clientServer.logger) + }) + .catch((e) => { + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', 'Error querying room tags'), + clientServer.logger + ) + }) + }) + } +} + +const schema = { + order: true +} + +export const addUserRoomTag = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + const userId = (req as Request).params.userId + const roomId = (req as Request).params.roomId + const _tag = (req as Request).params.tag + + if (!isMatrixIdValid(userId)) { + send(res, 400, errMsg('invalidParam', 'Invalid userId')) + return + } + if (!isRoomIdValid(roomId)) { + send(res, 400, errMsg('invalidParam', 'Invalid roomId')) + return + } + if (_tag.length > 255) { + send( + res, + 400, + errMsg('invalidParam', 'The tag must be less than 255 characters') + ) + return + } + + clientServer.authenticate(req, res, (data) => { + const requesterUserId = data.sub + if (requesterUserId !== userId) { + send(res, 403, errMsg('forbidden', 'You are not allowed to add tags')) + return + } + + jsonContent(req, res, clientServer.logger, (obj) => { + validateParameters(res, schema, obj, clientServer.logger, (obj) => { + const order = obj as { order: number } + if (typeof order.order !== 'number' || order.order <= 0) { + send( + res, + 400, + errMsg('invalidParam', 'The order must be greater than 0') + ) + return + } + clientServer.matrixDb + .insert('room_tags', { + user_id: userId, + room_id: roomId, + tag: _tag, + content: JSON.stringify(order) + }) + .then(() => { + send(res, 200, {}) + }) + .catch((e) => { + /* istanbul ignore next */ + clientServer.logger.error('Error inserting room tag:', e) + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', 'Error inserting room tag')) + }) + }) + }) + }) + } +} + +export const removeUserRoomTag = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + const userId = (req as Request).params.userId + const roomId = (req as Request).params.roomId + const _tag = (req as Request).params.tag + + if (!isMatrixIdValid(userId)) { + send(res, 400, errMsg('invalidParam', 'Invalid userId')) + return + } + if (!isRoomIdValid(roomId)) { + send(res, 400, errMsg('invalidParam', 'Invalid roomId')) + return + } + + clientServer.authenticate(req, res, (data) => { + const requesterUserId = data.sub + if (requesterUserId !== userId) { + send( + res, + 403, + errMsg('forbidden', 'You are not allowed to remove tags') + ) + return + } + + clientServer.matrixDb + .deleteWhere('room_tags', [ + { field: 'user_id', operator: '=', value: userId }, + { field: 'room_id', operator: '=', value: roomId }, + { field: 'tag', operator: '=', value: _tag } + ]) + .then(() => { + send(res, 200, {}) + }) + .catch((e) => { + /* istanbul ignore next */ + clientServer.logger.error('Error deleting room tag:', e) + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', 'Error deleting room tag')) + }) + }) + } +} diff --git a/packages/matrix-client-server/src/rooms/room_information/room_visibilty.ts b/packages/matrix-client-server/src/rooms/room_information/room_visibilty.ts new file mode 100644 index 00000000..15badd5f --- /dev/null +++ b/packages/matrix-client-server/src/rooms/room_information/room_visibilty.ts @@ -0,0 +1,109 @@ +import type MatrixClientServer from '../../' +import { + errMsg, + send, + type expressAppHandler, + jsonContent, + validateParameters +} from '@twake/utils' +import { type Request } from 'express' + +export const getRoomVisibility = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + const roomId = (req as Request).params.roomId + + clientServer.matrixDb + .get('rooms', ['room_id', 'is_public'], { + room_id: roomId + }) + .then((roomsResult) => { + if (roomsResult.length === 0) { + send( + res, + 404, + errMsg('notFound', 'Room not found'), + clientServer.logger + ) + } else { + const roomInfo = roomsResult[0] + + const _visibility = + roomInfo.is_public !== null && roomInfo.is_public === 1 + ? 'public' + : 'private' + send( + res, + 200, + { + visibility: _visibility + }, + clientServer.logger + ) + } + }) + .catch((e) => { + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', 'Error querying room directory info'), + clientServer.logger + ) + }) + } +} + +const schema = { + visibility: true +} + +export const setRoomVisibility = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + const roomId = (req as Request).params.roomId + + // TO DO : eventually implement additional access control checks here + // (not done in the Synapse implementation) + clientServer.authenticate(req, res, (data, id) => { + jsonContent(req, res, clientServer.logger, (obj) => { + validateParameters(res, schema, obj, clientServer.logger, (obj) => { + const order = obj as { visibility: string } + if (order.visibility !== 'public' && order.visibility !== 'private') { + send(res, 400, errMsg('invalidParam', 'Invalid parameters')) + } else { + const isPublic = order.visibility === 'public' ? 1 : 0 + + clientServer.matrixDb + .updateWithConditions('rooms', { is_public: isPublic }, [ + { field: 'room_id', value: roomId } + ]) + .then((rows) => { + if (rows.length === 0) { + send( + res, + 404, + errMsg('notFound', 'Room not found'), + clientServer.logger + ) + } else { + send(res, 200, {}, clientServer.logger) + } + }) + .catch((e) => { + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', 'Error updating room visibility'), + clientServer.logger + ) + }) + } + }) + }) + }) + } +} diff --git a/packages/matrix-client-server/src/rooms/rooms.test.ts b/packages/matrix-client-server/src/rooms/rooms.test.ts new file mode 100644 index 00000000..ad2215b8 --- /dev/null +++ b/packages/matrix-client-server/src/rooms/rooms.test.ts @@ -0,0 +1,1483 @@ +import fs from 'fs' +import request from 'supertest' +import express from 'express' +import ClientServer from '../index' +import { buildMatrixDb, buildUserDB } from '../__testData__/buildUserDB' +import { type Config } from '../types' +import defaultConfig from '../__testData__/registerConf.json' +import { getLogger, type TwakeLogger } from '@twake/logger' +import { randomString } from '@twake/crypto' +import { + setupTokens, + validToken, + validToken2, + validToken3 +} from '../__testData__/setupTokens' + +jest.mock('node-fetch', () => jest.fn()) + +let conf: Config +let clientServer: ClientServer +let app: express.Application + +const logger: TwakeLogger = getLogger() + +beforeAll((done) => { + // @ts-expect-error TS doesn't understand that the config is valid + conf = { + ...defaultConfig, + base_url: 'http://example.com/', + matrix_database_host: './src/__testData__/testMatrixRoom.db', + userdb_host: './src/__testData__/testRoom.db', + database_host: './src/__testData__/testRoom.db' + } + if (process.env.TEST_PG === 'yes') { + conf.database_engine = 'pg' + conf.userdb_engine = 'pg' + conf.database_host = process.env.PG_HOST ?? 'localhost' + conf.database_user = process.env.PG_USER ?? 'twake' + conf.database_password = process.env.PG_PASSWORD ?? 'twake' + conf.database_name = process.env.PG_DATABASE ?? 'test' + } + buildUserDB(conf) + .then(() => { + buildMatrixDb(conf) + .then(() => { + done() + }) + .catch((e) => { + logger.error('Error while building matrix db:', e) + done(e) + }) + }) + .catch((e) => { + logger.error('Error while building user db:', e) + done(e) + }) +}) + +afterAll(() => { + fs.unlinkSync('src/__testData__/testRoom.db') + fs.unlinkSync('src/__testData__/testMatrixRoom.db') +}) + +describe('Use configuration file', () => { + beforeAll((done) => { + clientServer = new ClientServer(conf) + app = express() + clientServer.ready + .then(() => { + Object.keys(clientServer.api.get).forEach((k) => { + app.get(k, clientServer.api.get[k]) + }) + Object.keys(clientServer.api.post).forEach((k) => { + app.post(k, clientServer.api.post[k]) + }) + Object.keys(clientServer.api.put).forEach((k) => { + app.put(k, clientServer.api.put[k]) + }) + Object.keys(clientServer.api.delete).forEach((k) => { + app.delete(k, clientServer.api.delete[k]) + }) + done() + }) + .catch((e) => { + done(e) + }) + }) + + afterAll(() => { + clientServer.cleanJobs() + }) + + describe('Endpoints with authentication', () => { + beforeAll(async () => { + await setupTokens(clientServer, logger) + }) + describe('/_matrix/client/v3/rooms', () => { + describe('/_matrix/client/v3/rooms/:roomId', () => { + describe('/_matrix/client/v3/rooms/:roomId/state', () => { + beforeAll(async () => { + try { + await clientServer.matrixDb.insert('local_current_membership', { + room_id: '!testroom:example.com', + user_id: '@testuser:example.com', + membership: 'join', + event_id: 'joining_user' + }) + await clientServer.matrixDb.insert('local_current_membership', { + room_id: '!testroom:example.com', + user_id: '@testuser2:example.com', + membership: 'leave', + event_id: 'leaving_user' + }) + await clientServer.matrixDb.insert('events', { + event_id: 'joining_user', + room_id: '!testroom:example.com', + sender: '@admin:example.com', + state_key: '1', + type: 'm.room.member', + origin_server_ts: 0, + content: JSON.stringify({ body: 'test message' }), + topological_ordering: 0, + processed: 2, + outlier: 0 + }) + await clientServer.matrixDb.insert('events', { + event_id: 'leaving_user', + room_id: '!testroom:example.com', + sender: '@admin:example.com', + state_key: '2', + type: 'm.room.member', + origin_server_ts: 50, + content: JSON.stringify({ body: 'test message' }), + topological_ordering: 0, + processed: 2, + outlier: 0 + }) + await clientServer.matrixDb.insert('current_state_events', { + event_id: 'joining_user', + room_id: '!testroom:example.com', + type: 'm.room.member', + state_key: '1', + membership: 'join' + }) + await clientServer.matrixDb.insert('current_state_events', { + event_id: 'some_event', + room_id: '!testroom:example.com', + type: 'm.room.message', + state_key: '3', + membership: 'join' + }) + await clientServer.matrixDb.insert('current_state_events', { + event_id: 'leaving_user', + room_id: '!testroom:example.com', + type: 'm.room.member', + state_key: '2', + membership: 'leave' + }) + await clientServer.matrixDb.insert('events', { + event_id: 'some_event', + room_id: '!testroom:example.com', + sender: '@admin:example.com', + state_key: '3', + type: 'm.room.message', + origin_server_ts: 0, + content: JSON.stringify({ body: 'test message' }), + topological_ordering: 0, + processed: 2, + outlier: 0 + }) + } catch (e) { + logger.error('Error setting up test data:', e) + } + }) + afterAll(async () => { + try { + await clientServer.matrixDb.deleteEqual( + 'local_current_membership', + 'room_id', + '!testroom:example.com' + ) + await clientServer.matrixDb.deleteEqual( + 'events', + 'room_id', + '!testroom:example.com' + ) + await clientServer.matrixDb.deleteEqual( + 'current_state_events', + 'room_id', + '!testroom:example.com' + ) + } catch (e) { + logger.error('Error tearing down test data:', e) + } + }) + + it('should require authentication', async () => { + const response = await request(app) + .get('/_matrix/client/v3/rooms/!testroom:example.com/state') + .set('Authorization', 'Bearer invalidToken') + .set('Accept', 'application/json') + expect(response.statusCode).toBe(401) + }) + it('should return 400 if the query parameters are incorrect', async () => { + const response = await request(app) + .get('/_matrix/client/v3/rooms/invalid_room_id/state') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body.errcode).toBe('M_INVALID_PARAM') + }) + it('should return 403 if the user has never been in the room', async () => { + const response = await request(app) + .get('/_matrix/client/v3/rooms/!testroom:example.com/state') + .set('Authorization', `Bearer ${validToken3}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(403) + }) + it('should return 200 and an array of 3 events if the user is in the room', async () => { + const response = await request(app) + .get('/_matrix/client/v3/rooms/!testroom:example.com/state') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + // CHeck if the response.body is an array + expect(response.body).toBeInstanceOf(Array) + expect(response.body).toHaveLength(3) + }) + it('should return 3 instances of a ClientEvent', async () => { + const response = await request(app) + .get('/_matrix/client/v3/rooms/!testroom:example.com/state') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body).toHaveLength(3) + expect(response.body[0]).toHaveProperty('event_id') + expect(response.body[0]).toHaveProperty('room_id') + expect(response.body[0]).toHaveProperty('sender') + expect(response.body[0]).toHaveProperty('type') + expect(response.body[0]).toHaveProperty('origin_server_ts') + expect(response.body[0]).toHaveProperty('content') + expect(response.body[0]).toHaveProperty('state_key') + expect(response.body[0]).toHaveProperty('unsigned') + }) + it('should return 200 and an array of 1 event if the user left the room', async () => { + const response = await request(app) + .get('/_matrix/client/v3/rooms/!testroom:example.com/state') + .set('Authorization', `Bearer ${validToken2}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body).toBeInstanceOf(Array) + expect(response.body).toHaveLength(1) + expect(response.body[0]).toHaveProperty('event_id', 'leaving_user') + }) + }) + + describe('/_matrix/client/v3/rooms/:roomId/state/:eventType/:stateKey', () => { + beforeAll(async () => { + try { + await clientServer.matrixDb.insert('local_current_membership', { + room_id: '!testroom:example.com', + user_id: '@testuser:example.com', + membership: 'join', + event_id: 'joining_user' + }) + await clientServer.matrixDb.insert('local_current_membership', { + room_id: '!testroom:example.com', + user_id: '@testuser2:example.com', + membership: 'leave', + event_id: 'leaving_user' + }) + await clientServer.matrixDb.insert('events', { + event_id: 'joining_user', + room_id: '!testroom:example.com', + sender: '@admin:example.com', + state_key: '1', + type: 'm.room.member', + origin_server_ts: 0, + content: JSON.stringify({ body: 'test message' }), + topological_ordering: 0, + processed: 2, + outlier: 0 + }) + await clientServer.matrixDb.insert('events', { + event_id: 'leaving_user', + room_id: '!testroom:example.com', + sender: '@admin:example.com', + state_key: '2', + type: 'm.room.member', + origin_server_ts: 50, + content: JSON.stringify({ body: 'You left' }), + topological_ordering: 0, + processed: 2, + outlier: 0 + }) + await clientServer.matrixDb.insert('current_state_events', { + event_id: 'joining_user', + room_id: '!testroom:example.com', + type: 'm.room.member', + state_key: '1', + membership: 'join' + }) + await clientServer.matrixDb.insert('current_state_events', { + event_id: 'some_event', + room_id: '!testroom:example.com', + type: 'm.room.message', + state_key: '', + membership: 'join' + }) + await clientServer.matrixDb.insert('current_state_events', { + event_id: 'leaving_user', + room_id: '!testroom:example.com', + type: 'm.room.member', + state_key: '2', + membership: 'leave' + }) + await clientServer.matrixDb.insert('events', { + event_id: 'some_event', + room_id: '!testroom:example.com', + sender: '@admin:example.com', + state_key: '', + type: 'm.room.message', + origin_server_ts: 0, + content: JSON.stringify({ body: 'test message' }), + topological_ordering: 0, + processed: 2, + outlier: 0 + }) + } catch (e) { + logger.error('Error setting up test data:', e) + } + }) + afterAll(async () => { + try { + await clientServer.matrixDb.deleteEqual( + 'local_current_membership', + 'room_id', + '!testroom:example.com' + ) + await clientServer.matrixDb.deleteEqual( + 'events', + 'room_id', + '!testroom:example.com' + ) + await clientServer.matrixDb.deleteEqual( + 'current_state_events', + 'room_id', + '!testroom:example.com' + ) + } catch (e) { + logger.error('Error tearing down test data:', e) + } + }) + + it('should require authentication', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/state/m.room.member/2' + ) + .set('Authorization', 'Bearer invalidToken') + .set('Accept', 'application/json') + expect(response.statusCode).toBe(401) + }) + it('should return 400 if the path parameters are incorrect', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/invalid_room_id/state/m.room.member/2' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body.errcode).toBe('M_INVALID_PARAM') + }) + it('should return 403 if the user has never been in the room', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/state/m.room.member/2' + ) + .set('Authorization', `Bearer ${validToken3}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(403) + }) + it('should return 404 NOT_FOUND if there is no event with such type and key', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/state/m.room.account_data/2' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(404) + expect(response.body.errcode).toBe('M_NOT_FOUND') + }) + it('should return 200 if the user is in the room and stateKey is no empty string (3 ms)', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/state/m.room.member/2' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body).toEqual({ body: 'You left' }) + }) + it('should return 200 and the content of a departure event if the user left the room', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/state/m.room.message/3' + ) + .set('Authorization', `Bearer ${validToken2}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body).toEqual({ body: 'You left' }) + }) + }) + + describe('/_matrix/client/v3/rooms/:roomId/state/:eventType/', () => { + beforeAll(async () => { + try { + await clientServer.matrixDb.insert('local_current_membership', { + room_id: '!testroom:example.com', + user_id: '@testuser:example.com', + membership: 'join', + event_id: 'joining_user' + }) + await clientServer.matrixDb.insert('local_current_membership', { + room_id: '!testroom:example.com', + user_id: '@testuser2:example.com', + membership: 'leave', + event_id: 'leaving_user' + }) + await clientServer.matrixDb.insert('events', { + event_id: 'joining_user', + room_id: '!testroom:example.com', + sender: '@admin:example.com', + state_key: '1', + type: 'm.room.member', + origin_server_ts: 0, + content: JSON.stringify({ body: 'test message' }), + topological_ordering: 0, + processed: 2, + outlier: 0 + }) + await clientServer.matrixDb.insert('events', { + event_id: 'leaving_user', + room_id: '!testroom:example.com', + sender: '@admin:example.com', + state_key: '2', + type: 'm.room.member', + origin_server_ts: 50, + content: JSON.stringify({ body: 'You left' }), + topological_ordering: 0, + processed: 2, + outlier: 0 + }) + await clientServer.matrixDb.insert('current_state_events', { + event_id: 'joining_user', + room_id: '!testroom:example.com', + type: 'm.room.member', + state_key: '1', + membership: 'join' + }) + await clientServer.matrixDb.insert('current_state_events', { + event_id: 'some_event', + room_id: '!testroom:example.com', + type: 'm.room.message', + state_key: '', + membership: 'join' + }) + await clientServer.matrixDb.insert('current_state_events', { + event_id: 'leaving_user', + room_id: '!testroom:example.com', + type: 'm.room.member', + state_key: '2', + membership: 'leave' + }) + await clientServer.matrixDb.insert('events', { + event_id: 'some_event', + room_id: '!testroom:example.com', + sender: '@admin:example.com', + state_key: '', + type: 'm.room.message', + origin_server_ts: 0, + content: JSON.stringify({ body: 'test message' }), + topological_ordering: 0, + processed: 2, + outlier: 0 + }) + } catch (e) { + logger.error('Error setting up test data:', e) + } + }) + afterAll(async () => { + try { + await clientServer.matrixDb.deleteEqual( + 'local_current_membership', + 'room_id', + '!testroom:example.com' + ) + await clientServer.matrixDb.deleteEqual( + 'events', + 'room_id', + '!testroom:example.com' + ) + await clientServer.matrixDb.deleteEqual( + 'current_state_events', + 'room_id', + '!testroom:example.com' + ) + } catch (e) { + logger.error('Error tearing down test data:', e) + } + }) + + it('should require authentication', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/state/m.room.member/' + ) + .set('Authorization', 'Bearer invalidToken') + .set('Accept', 'application/json') + expect(response.statusCode).toBe(401) + }) + it('should return 400 if the path parameters are incorrect', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/invalid_room_id/state/m.room.member' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body.errcode).toBe('M_INVALID_PARAM') + }) + it('should return 403 if the user has never been in the room', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/state/m.room.member/' + ) + .set('Authorization', `Bearer ${validToken3}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(403) + }) + it('should return 404 NOT_FOUND if there is no event with such type and key', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/state/m.room.account_data/' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(404) + expect(response.body.errcode).toBe('M_NOT_FOUND') + }) + it('should return 200 if the user is in the room and stateKey is an empty string', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/state/m.room.member/' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body).toEqual({ body: 'test message' }) + }) + it('should return 200 if the user is in the room and there is no stateKey path', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/state/m.room.member' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body).toEqual({ body: 'test message' }) + }) + it('should return 200 and the content of a departure event if the user left the room', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/state/m.room.message/' + ) + .set('Authorization', `Bearer ${validToken2}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body).toEqual({ body: 'You left' }) + }) + }) + describe('/_matrix/client/v3/rooms/:roomId/event/:eventId', () => { + beforeAll(async () => { + try { + await clientServer.matrixDb.insert('events', { + event_id: 'event_to_retrieve', + room_id: '!testroom:example.com', + sender: '@sender:example.com', + type: 'm.room.message', + state_key: '', + origin_server_ts: 1000, + content: '{ body: test message }', + topological_ordering: 0, + processed: 1, + outlier: 0 + }) + + await clientServer.matrixDb.insert('local_current_membership', { + room_id: '!testroom:example.com', + user_id: '@testuser:example.com', + membership: 'join', + event_id: 'adding_user' + }) + + await clientServer.matrixDb.insert('events', { + event_id: 'adding_user', + room_id: '!testroom:example.com', + sender: '@admin:example.com', + type: 'm.room.message', + origin_server_ts: 0, + content: JSON.stringify({ body: 'test message' }), + topological_ordering: 0, + processed: 2, + outlier: 0 + }) + + logger.info('Test event created') + } catch (e) { + logger.error('Error setting up test data:', e) + } + }) + + afterAll(async () => { + try { + await clientServer.matrixDb.deleteEqual( + 'events', + 'event_id', + 'event_to_retrieve' + ) + await clientServer.matrixDb.deleteEqual( + 'events', + 'event_id', + 'adding_user' + ) + await clientServer.matrixDb.deleteEqual( + 'local_current_membership', + 'event_id', + 'adding_user' + ) + logger.info('Test event deleted') + } catch (e) { + logger.error('Error tearing down test data', e) + } + }) + it('should return 400 if the query parameters are incorrect', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/invalid_room_id/event/invalid_event_id' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body.errcode).toBe('M_INVALID_PARAM') + }) + it('should return 404 if the event does not exist', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/event/invalid_event_id' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(404) + }) + it('should 404 if the user has never been in the room', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/event/event_to_retrieve' + ) + .set('Authorization', `Bearer ${validToken2}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(404) + }) + it('should return 200 if the event can be retrieved by the user', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/event/event_to_retrieve' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty( + 'event_id', + 'event_to_retrieve' + ) + expect(response.body).toHaveProperty( + 'room_id', + '!testroom:example.com' + ) + expect(response.body).toHaveProperty( + 'sender', + '@sender:example.com' + ) + expect(response.body).toHaveProperty('type', 'm.room.message') + expect(response.body).toHaveProperty('origin_server_ts', 1000) + expect(response.body).toHaveProperty( + 'content', + '{ body: test message }' + ) + }) + it('should return 404 if the user is not part of the room currently', async () => { + try { + await clientServer.matrixDb.insert('local_current_membership', { + room_id: '!testroom:example.com', + user_id: '@testuser2:example.com', + membership: 'leave', + event_id: 'deleting_user' + }) + + await clientServer.matrixDb.insert('events', { + event_id: 'deleting_user', + room_id: '!testroom:example.com', + sender: '@admin:example.com', + type: 'm.room.message', + origin_server_ts: 50, + content: JSON.stringify({ body: 'test message' }), + topological_ordering: 0, + processed: 2, + outlier: 0 + }) + logger.info('Test event created') + } catch (e) { + logger.error('Error tearing down test data', e) + } + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/event/event_to_retrieve' + ) + .set('Authorization', `Bearer ${validToken2}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(404) + + try { + await clientServer.matrixDb.deleteEqual( + 'local_current_membership', + 'event_id', + 'deleting_user' + ) + await clientServer.matrixDb.deleteEqual( + 'events', + 'event_id', + 'deleting_user' + ) + logger.info('Test event deleted') + } catch (e) { + logger.error('Error tearing down test data', e) + } + }) + }) + + describe('/_matrix/client/v3/rooms/:roomId/joined_members', () => { + beforeAll(async () => { + try { + await clientServer.matrixDb.insert('local_current_membership', { + room_id: '!testroom:example.com', + user_id: '@testuser:example.com', + membership: 'join', + event_id: 'joining_user' + }) + + await clientServer.matrixDb.insert('profiles', { + user_id: '@testuser:example.com', + displayname: 'Test User', + avatar_url: 'http://example.com/avatar.jpg' + }) + + await clientServer.matrixDb.insert('local_current_membership', { + room_id: '!testroom:example.com', + user_id: '@admin:example.com', + membership: 'join', + event_id: 'joining_admin' + }) + + await clientServer.matrixDb.insert('profiles', { + user_id: '@admin:example.com', + displayname: 'Admin User', + avatar_url: 'http://example.com/avatarAdmin.jpg' + }) + + await clientServer.matrixDb.insert('local_current_membership', { + room_id: '!testroom:example.com', + user_id: '@visit:example.com', + membership: 'leave', + event_id: 'leaving_user' + }) + + await clientServer.matrixDb.insert('profiles', { + user_id: '@visit:example.com', + displayname: 'Visiting User', + avatar_url: 'http://example.com/avatarExample.jpg' + }) + + logger.info('Test event created') + } catch (e) { + logger.error('Error setting up test data:', e) + } + }) + + afterAll(async () => { + try { + await clientServer.matrixDb.deleteEqual( + 'local_current_membership', + 'event_id', + 'joining_user' + ) + await clientServer.matrixDb.deleteEqual( + 'local_current_membership', + 'event_id', + 'joining_admin' + ) + await clientServer.matrixDb.deleteEqual( + 'local_current_membership', + 'event_id', + 'leaving_user' + ) + await clientServer.matrixDb.deleteEqual( + 'profiles', + 'user_id', + '@testuser:example.com' + ) + await clientServer.matrixDb.deleteEqual( + 'profiles', + 'user_id', + '@admin:example.com' + ) + await clientServer.matrixDb.deleteEqual( + 'profiles', + 'user_id', + '@visit:example.com' + ) + logger.info('Test event deleted') + } catch (e) { + logger.error('Error tearing down test data', e) + } + }) + it('should return 403 if the user is not in the room', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/joined_members' + ) + .set('Authorization', `Bearer ${validToken2}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(403) + }) + it('should return 200 if the user is in the room', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/joined_members' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('joined') + expect(response.body.joined['@testuser:example.com']).toBeDefined() + expect( + response.body.joined['@testuser:example.com'] + ).toHaveProperty('display_name', 'Test User') + expect( + response.body.joined['@testuser:example.com'] + ).toHaveProperty('avatar_url', 'http://example.com/avatar.jpg') + expect(response.body.joined['@admin:example.com']).toBeDefined() + expect(response.body.joined['@admin:example.com']).toHaveProperty( + 'display_name', + 'Admin User' + ) + expect(response.body.joined['@admin:example.com']).toHaveProperty( + 'avatar_url', + 'http://example.com/avatarAdmin.jpg' + ) + }) + }) + + describe('/_matrix/client/v3/rooms/:roomId/timestamp_to_event', () => { + beforeAll(async () => { + try { + await clientServer.matrixDb.insert('events', { + event_id: 'event1', + room_id: '!testroom:example.com', + sender: '@sender:example.com', + type: 'm.room.message', + state_key: '', + origin_server_ts: 1000, + content: '{ body: test message }', + topological_ordering: 0, + processed: 1, + outlier: 0 + }) + await clientServer.matrixDb.insert('events', { + event_id: 'event2', + room_id: '!testroom:example.com', + sender: '@sender:example.com', + type: 'm.room.message', + state_key: '', + origin_server_ts: 2000, + content: '{ body: test message }', + topological_ordering: 1, + processed: 1, + outlier: 0 + }) + await clientServer.matrixDb.insert('events', { + event_id: 'event3', + room_id: '!testroom:example.com', + sender: '@sender:example.com', + type: 'm.room.message', + state_key: '', + origin_server_ts: 3000, + content: '{ body: test message }', + topological_ordering: 2, + processed: 1, + outlier: 0 + }) + + logger.info('Test events created') + } catch (e) { + logger.error('Error setting up test data', e) + } + }) + afterAll(async () => { + try { + await clientServer.matrixDb.deleteEqual( + 'events', + 'event_id', + 'event1' + ) + await clientServer.matrixDb.deleteEqual( + 'events', + 'event_id', + 'event2' + ) + await clientServer.matrixDb.deleteEqual( + 'events', + 'event_id', + 'event3' + ) + } catch (e) { + logger.error('Error tearing down test data', e) + } + }) + + it('should return 400 if the query parameters are incorrect', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/timestamp_to_event' + ) + .query({ dir: 'unsupported_string', ts: 500 }) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + }) + + it('should return 404 if the event does not exist (forward)', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/timestamp_to_event' + ) + .query({ dir: 'f', ts: 3500 }) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(404) + expect(response.body.errcode).toBe('M_NOT_FOUND') + }) + + it('should return 404 if the event does not exist (backward)', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/timestamp_to_event' + ) + .query({ dir: 'b', ts: 500 }) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(404) + expect(response.body.errcode).toBe('M_NOT_FOUND') + }) + + it('should return 200 if the event can be retrieved (forward)', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/timestamp_to_event' + ) + .query({ dir: 'f', ts: 1500 }) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('event_id', 'event2') + }) + it('should return 200 if the event can be retrieved (backward)', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/timestamp_to_event' + ) + .query({ dir: 'b', ts: 2500 }) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('event_id', 'event2') + }) + }) + }) + + describe('/_matrix/client/v3/rooms/:roomId/aliases', () => { + const testUserId = '@testuser:example.com' + const testRoomId = '!testroomid:example.com' + const worldReadableRoomId = '!worldreadable:example.com' + + beforeAll(async () => { + try { + // Insert test data for room aliases + await clientServer.matrixDb.insert('room_aliases', { + room_id: testRoomId, + room_alias: '#somewhere:example.com' + }) + await clientServer.matrixDb.insert('room_aliases', { + room_id: testRoomId, + room_alias: '#another:example.com' + }) + await clientServer.matrixDb.insert('room_aliases', { + room_id: worldReadableRoomId, + room_alias: '#worldreadable:example.com' + }) + + // Insert test data for room visibility + await clientServer.matrixDb.insert('room_stats_state', { + room_id: worldReadableRoomId, + history_visibility: 'world_readable' + }) + await clientServer.matrixDb.insert('room_stats_state', { + room_id: testRoomId, + history_visibility: 'joined' + }) + + // Insert test data for room membership + await clientServer.matrixDb.insert('room_memberships', { + user_id: testUserId, + room_id: testRoomId, + membership: 'join', + forgotten: 0, + event_id: randomString(20), + sender: '@admin:example.com' + }) + } catch (e) { + logger.error('Error setting up test data:', e) + } + }) + + afterAll(async () => { + try { + // Clean up test data + await clientServer.matrixDb.deleteEqual( + 'room_aliases', + 'room_id', + testRoomId + ) + await clientServer.matrixDb.deleteEqual( + 'room_aliases', + 'room_id', + worldReadableRoomId + ) + await clientServer.matrixDb.deleteEqual( + 'room_stats_state', + 'room_id', + worldReadableRoomId + ) + await clientServer.matrixDb.deleteEqual( + 'room_stats_state', + 'room_id', + testRoomId + ) + await clientServer.matrixDb.deleteEqual( + 'room_memberships', + 'room_id', + testRoomId + ) + } catch (e) { + logger.error('Error tearing down test data:', e) + } + }) + + it('should require authentication', async () => { + const response = await request(app) + .get(`/_matrix/client/v3/rooms/${testRoomId}/aliases`) + .set('Authorization', 'Bearer invalidToken') + .set('Accept', 'application/json') + expect(response.statusCode).toBe(401) + }) + it('should return 400 if invalid room_id is provided', async () => { + const response = await request(app) + .get(`/_matrix/client/v3/rooms/invalid_room_id/aliases`) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body.errcode).toBe('M_INVALID_PARAM') + }) + it('should return the list of aliases for a world_readable room for any user', async () => { + const response = await request(app) + .get(`/_matrix/client/v3/rooms/${worldReadableRoomId}/aliases`) + .set('Authorization', `Bearer ${validToken2}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body).toEqual({ + aliases: ['#worldreadable:example.com'] + }) + }) + + it('should return the list of aliases for an non-world_readable room if the user is a member', async () => { + const response = await request(app) + .get(`/_matrix/client/v3/rooms/${testRoomId}/aliases`) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body).toEqual({ + aliases: ['#somewhere:example.com', '#another:example.com'] + }) + }) + + it('should return 403 if the user is not a member and the room is not world_readable', async () => { + const response = await request(app) + .get(`/_matrix/client/v3/rooms/${testRoomId}/aliases`) + .set('Authorization', `Bearer ${validToken2}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(403) + expect(response.body).toEqual({ + errcode: 'M_FORBIDDEN', + error: + 'The user is not permitted to retrieve the list of local aliases for the room' + }) + }) + + it('should return 400 if the room ID is invalid', async () => { + const invalidRoomId = '!invalidroomid:example.com' + + const response = await request(app) + .get(`/_matrix/client/v3/rooms/${invalidRoomId}/aliases`) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toEqual({ + errcode: 'M_INVALID_PARAM', + error: 'Invalid room id' + }) + }) + }) + }) + + describe('/_matrix/client/v3/user/:userId/rooms/:roomId/tags', () => { + const testUserId = '@testuser:example.com' + const testRoomId = '!testroomid:example.com' + + beforeAll(async () => { + try { + await clientServer.matrixDb.insert('room_tags', { + user_id: testUserId, + room_id: testRoomId, + tag: 'test_tag', + content: JSON.stringify({ order: 1 }) + }) + await clientServer.matrixDb.insert('room_tags', { + user_id: testUserId, + room_id: testRoomId, + tag: 'test_tag2', + content: JSON.stringify({ order: 0.5 }) + }) + } catch (e) { + logger.error('Error setting up test data:', e) + } + }) + + afterAll(async () => { + try { + await clientServer.matrixDb.deleteEqual( + 'room_tags', + 'tag', + 'test_tag' + ) + await clientServer.matrixDb.deleteEqual( + 'room_tags', + 'tag', + 'test_tag2' + ) + } catch (e) { + logger.error('Error tearing down test data:', e) + } + }) + describe('GET', () => { + it('should require authentication', async () => { + const response = await request(app) + .get( + `/_matrix/client/v3/user/${testUserId}/rooms/${testRoomId}/tags` + ) + .set('Authorization', 'Bearer invalidToken') + .set('Accept', 'application/json') + expect(response.statusCode).toBe(401) + }) + + it('should return 400 if the user_id is invalid', async () => { + const response = await request(app) + .get( + `/_matrix/client/v3/user/invalid_user_id/rooms/${testRoomId}/tags` + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + + expect(response.statusCode).toBe(400) + }) + + it('should return 400 if the room_id is invalid', async () => { + const response = await request(app) + .get( + `/_matrix/client/v3/user/${testUserId}/rooms/invalid_room_id/tags` + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + + expect(response.statusCode).toBe(400) + }) + + it('should return 403 if the requesting user is not the target user', async () => { + const response = await request(app) + .get( + `/_matrix/client/v3/user/${testUserId}/rooms/${testRoomId}/tags` + ) + .set('Authorization', `Bearer ${validToken2}`) + .set('Accept', 'application/json') + + expect(response.statusCode).toBe(403) + }) + + it('should return the tags for the room', async () => { + const response = await request(app) + .get( + `/_matrix/client/v3/user/${testUserId}/rooms/${testRoomId}/tags` + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('tags') + expect(response.body.tags).toEqual({ + test_tag: { order: 1 }, + test_tag2: { order: 0.5 } + }) + }) + }) + + describe('PUT', () => { + const testTag = 'new_tag' + + it('should require authentication', async () => { + const response = await request(app) + .put( + `/_matrix/client/v3/user/${testUserId}/rooms/${testRoomId}/tags/${testTag}` + ) + .set('Authorization', 'Bearer invalidToken') + .set('Accept', 'application/json') + .send({ order: 0.2 }) + + expect(response.statusCode).toBe(401) + }) + + it('should return 400 if the order is not a number', async () => { + const response = await request(app) + .put( + `/_matrix/client/v3/user/${testUserId}/rooms/${testRoomId}/tags/${testTag}` + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ order: 'invalid_order' }) + + expect(response.statusCode).toBe(400) + }) + + it('should return 400 if the order is less than 0', async () => { + const response = await request(app) + .put( + `/_matrix/client/v3/user/${testUserId}/rooms/${testRoomId}/tags/${testTag}` + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ order: -1 }) + + expect(response.statusCode).toBe(400) + }) + + it('should return 400 if the tag is too long', async () => { + const response = await request(app) + .put( + `/_matrix/client/v3/user/${testUserId}/rooms/${testRoomId}/tags/${'a'.repeat( + 256 + )}` + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ order: 0.2 }) + + expect(response.statusCode).toBe(400) + }) + + it('should return 400 if the user_id is invalid', async () => { + const response = await request(app) + .put( + `/_matrix/client/v3/user/invalid_user_id/rooms/${testRoomId}/tags/${testTag}` + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ order: 0.2 }) + + expect(response.statusCode).toBe(400) + }) + + it('should return 400 if the room_id is invalid', async () => { + const response = await request(app) + .put( + `/_matrix/client/v3/user/${testUserId}/rooms/invalid_room_id/tags/${testTag}` + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ order: 0.2 }) + + expect(response.statusCode).toBe(400) + }) + + it('should return 403 if the requesting user is not the target user', async () => { + const response = await request(app) + .put( + `/_matrix/client/v3/user/${testUserId}/rooms/${testRoomId}/tags/${testTag}` + ) + .set('Authorization', `Bearer ${validToken2}`) + .set('Accept', 'application/json') + .send({ order: 0.2 }) + + expect(response.statusCode).toBe(403) + }) + + it('should add a tag to the room', async () => { + const response = await request(app) + .put( + `/_matrix/client/v3/user/${testUserId}/rooms/${testRoomId}/tags/${testTag}` + ) + .set('Authorization', `Bearer ${validToken}`) + .send({ order: 0.2 }) + expect(response.statusCode).toBe(200) + const rows = await clientServer.matrixDb.get( + 'room_tags', + ['tag', 'content'], + { + user_id: testUserId, + room_id: testRoomId + } + ) + expect(rows[0]).toEqual({ + tag: testTag, + content: JSON.stringify({ order: 0.2 }) + }) + }) + }) + + describe('DELETE', () => { + const testTag = 'test_tag' + + it('should require authentication', async () => { + const response = await request(app) + .delete( + `/_matrix/client/v3/user/${testUserId}/rooms/${testRoomId}/tags/${testTag}` + ) + .set('Authorization', 'Bearer invalidToken') + .set('Accept', 'application/json') + + expect(response.statusCode).toBe(401) + }) + + it('should return 400 if the user_id is invalid', async () => { + const response = await request(app) + .delete( + `/_matrix/client/v3/user/invalid_user_id/rooms/${testRoomId}/tags/${testTag}` + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + + expect(response.statusCode).toBe(400) + }) + + it('should return 400 if the room_id is invalid', async () => { + const response = await request(app) + .delete( + `/_matrix/client/v3/user/${testUserId}/rooms/invalid_room_id/tags/${testTag}` + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + + expect(response.statusCode).toBe(400) + }) + + it('should return 403 if the requesting user is not the target user', async () => { + const response = await request(app) + .delete( + `/_matrix/client/v3/user/${testUserId}/rooms/${testRoomId}/tags/${testTag}` + ) + .set('Authorization', `Bearer ${validToken2}`) + .set('Accept', 'application/json') + + expect(response.statusCode).toBe(403) + }) + + it('should delete the tag from the room', async () => { + const response = await request(app) + .delete( + `/_matrix/client/v3/user/${testUserId}/rooms/${testRoomId}/tags/${testTag}` + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + + expect(response.statusCode).toBe(200) + const rows = await clientServer.matrixDb.get('room_tags', ['tag'], { + user_id: testUserId, + room_id: testRoomId + }) + expect(rows).not.toContainEqual({ tag: testTag }) + }) + }) + }) + + describe('/_matrix/client/v3/joined_rooms', () => { + const testUserId = '@testuser:example.com' + const testRoomIds = ['!foo:example.com', '!bar:example.com'] + const testRoomIdBan = '!ban:example.com' + + beforeAll(async () => { + try { + await Promise.all( + // eslint-disable-next-line @typescript-eslint/promise-function-async + testRoomIds.map((roomId) => + clientServer.matrixDb.insert('room_memberships', { + user_id: testUserId, + room_id: roomId, + membership: 'join', + event_id: randomString(20), + sender: '@admin:example.com' + }) + ) + ) + await clientServer.matrixDb.insert('room_memberships', { + user_id: testUserId, + room_id: testRoomIdBan, + membership: 'ban', + event_id: randomString(20), + sender: '@admin:example.com' + }) + } catch (e) { + logger.error('Error setting up test data:', e) + } + }) + + afterAll(async () => { + try { + // Clean up test data + await clientServer.matrixDb.deleteEqual( + 'room_memberships', + 'sender', + '@admin:example.com' + ) + } catch (e) { + logger.error('Error tearing down test data:', e) + } + }) + + it('should require authentication', async () => { + const response = await request(app) + .get('/_matrix/client/v3/joined_rooms') + .set('Authorization', 'Bearer invalidToken') + .set('Accept', 'application/json') + expect(response.statusCode).toBe(401) + }) + + it('should return the list of rooms the user has joined', async () => { + const response = await request(app) + .get('/_matrix/client/v3/joined_rooms') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body).toEqual({ + joined_rooms: testRoomIds + }) + }) + }) + }) +}) diff --git a/packages/matrix-client-server/src/typecheckers.test.ts b/packages/matrix-client-server/src/typecheckers.test.ts new file mode 100644 index 00000000..ddbe747d --- /dev/null +++ b/packages/matrix-client-server/src/typecheckers.test.ts @@ -0,0 +1,312 @@ +import { + verifyString, + verifyArray, + verifyObject, + verifyNumber, + verifyBoolean, + verifyUserIdentifier, + verifyThreepidCreds, + verifyAuthenticationData +} from './typecheckers' +import { type AuthenticationData, type UserIdentifier } from './types' + +describe('Typecheck Functions', () => { + describe('verifyString', () => { + it('should return true for valid strings', () => { + expect(verifyString('hello')).toBe(true) + expect(verifyString('a'.repeat(511))).toBe(true) + }) + + it('should return false for invalid strings', () => { + expect(verifyString('')).toBe(false) + expect(verifyString('a'.repeat(513))).toBe(false) + expect(verifyString(123)).toBe(false) + expect(verifyString(null)).toBe(false) + expect(verifyString(undefined)).toBe(false) + }) + }) + + describe('verifyArray', () => { + it('should return true for valid arrays', () => { + expect(verifyArray(['a', 'b', 'c'], 'string')).toBe(true) + expect(verifyArray([1, 2, 3], 'number')).toBe(true) + }) + + it('should return false for invalid arrays', () => { + expect(verifyArray([], 'string')).toBe(false) + expect(verifyArray([1, 'b', 3], 'string')).toBe(false) + expect(verifyArray('not an array', 'string')).toBe(false) + }) + }) + + describe('verifyObject', () => { + it('should return true for valid objects', () => { + expect(verifyObject({ key: 'value' })).toBe(true) + expect(verifyObject({})).toBe(true) + }) + + it('should return false for invalid objects', () => { + expect(verifyObject(null)).toBe(false) + expect(verifyObject([])).toBe(false) + expect(verifyObject('not an object')).toBe(false) + }) + }) + + describe('verifyNumber', () => { + it('should return true for valid numbers', () => { + expect(verifyNumber(123)).toBe(true) + expect(verifyNumber(-456)).toBe(true) + }) + + it('should return false for invalid numbers', () => { + expect(verifyNumber('not a number')).toBe(false) + expect(verifyNumber(NaN)).toBe(false) + }) + }) + + describe('verifyBoolean', () => { + it('should return true for valid booleans', () => { + expect(verifyBoolean(true)).toBe(true) + expect(verifyBoolean(false)).toBe(true) + }) + + it('should return false for invalid booleans', () => { + expect(verifyBoolean('true')).toBe(false) + expect(verifyBoolean(1)).toBe(false) + }) + }) + + describe('verifyUserIdentifier', () => { + it('should return true for valid MatrixIdentifier', () => { + const identifier = { type: 'm.id.user', user: '@user:matrix.org' } + expect(verifyUserIdentifier(identifier as UserIdentifier)).toBe(true) + }) + + it('should return false for invalid MatrixIdentifier', () => { + const identifier = { type: 'm.id.user', user: 'invalidUser' } + expect(verifyUserIdentifier(identifier as UserIdentifier)).toBe(false) + }) + + it('should return true for valid ThirdPartyIdentifier', () => { + const identifier = { + type: 'm.id.thirdparty', + medium: 'email', + address: 'user@example.com' + } + expect(verifyUserIdentifier(identifier as UserIdentifier)).toBe(true) + }) + + it('should return false for invalid ThirdPartyIdentifier', () => { + const identifier = { + type: 'm.id.thirdparty', + medium: 'email', + address: 'invalidEmail' + } + expect(verifyUserIdentifier(identifier as UserIdentifier)).toBe(false) + }) + + it('should return true for valid PhoneIdentifier', () => { + const identifier = { + type: 'm.id.phone', + country: 'US', + phone: '1234567890' + } + expect(verifyUserIdentifier(identifier as UserIdentifier)).toBe(true) + }) + + it('should return false for invalid PhoneIdentifier', () => { + const identifier = { + type: 'm.id.phone', + country: 'US', + phone: 'invalidPhone' + } + expect(verifyUserIdentifier(identifier as UserIdentifier)).toBe(false) + }) + it('should return false for invalid UserIdentifier', () => { + const identifier = { + type: 'm.id.invalid' + } + expect(verifyUserIdentifier(identifier as UserIdentifier)).toBe(false) + }) + }) + + describe('verifyThreepidCreds', () => { + it('should return true for valid ThreepidCreds', () => { + const creds = { sid: 'sid123', client_secret: 'secret' } + expect(verifyThreepidCreds(creds)).toBe(true) + }) + + it('should return false for invalid ThreepidCreds', () => { + const creds = { sid: 'sid123', client_secret: '' } // Invalid client_secret + expect(verifyThreepidCreds(creds)).toBe(false) + }) + }) + + describe('verifyAuthenticationData', () => { + it('should return true for valid PasswordAuth', () => { + const authData = { + type: 'm.login.password', + identifier: { type: 'm.id.user', user: '@user:matrix.org' }, + password: 'password123', + session: 'session123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + true + ) + }) + + it('should return false for invalid PasswordAuth', () => { + const authData = { + type: 'm.login.password', + identifier: { type: 'm.id.user', user: 'invalidUser' }, // Invalid user ID + password: 'password123', + session: 'session123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + false + ) + }) + + it('should return true for valid EmailAuth', () => { + const authData = { + type: 'm.login.email.identity', + threepid_creds: { sid: 'sid123', client_secret: 'secret' }, + session: 'session123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + true + ) + }) + + it('should return false for invalid EmailAuth', () => { + const authData = { + type: 'm.login.email.identity', + threepid_creds: { sid: '', client_secret: 'secret' }, // Invalid sid + session: 'session123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + false + ) + }) + it('should return true for valid RecaptchaAuth', () => { + const authData = { + type: 'm.login.recaptcha', + response: 'recaptchaResponse', + session: 'session123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + true + ) + }) + + it('should return false for invalid RecaptchaAuth (missing session)', () => { + const authData = { + type: 'm.login.recaptcha', + response: 'recaptchaResponse' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + false + ) + }) + + it('should return false for invalid RecaptchaAuth (empty response)', () => { + const authData = { + type: 'm.login.recaptcha', + response: '', + session: 'session123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + false + ) + }) + + it('should return true for valid SsoAuth', () => { + const authData = { + type: 'm.login.sso', + session: 'session123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + true + ) + }) + + it('should return false for invalid SsoAuth (missing session)', () => { + const authData = { + type: 'm.login.sso' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + false + ) + }) + + it('should return true for valid DummyAuth', () => { + const authData = { + type: 'm.login.dummy', + session: 'session123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + true + ) + }) + + it('should return false for invalid DummyAuth (empty session)', () => { + const authData = { + type: 'm.login.dummy', + session: '' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + false + ) + }) + + it('should return true for valid TokenAuth', () => { + const authData = { + type: 'm.login.registration_token', + token: 'registrationToken', + session: 'session123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + true + ) + }) + + it('should return false for invalid TokenAuth (missing token)', () => { + const authData = { + type: 'm.login.registration_token', + session: 'session123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + false + ) + }) + + it('should return true for valid ApplicationServiceAuth', () => { + const authData = { + type: 'm.login.application_service', + username: 'user123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + true + ) + }) + + it('should return false for invalid ApplicationServiceAuth (missing username)', () => { + const authData = { + type: 'm.login.application_service' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + false + ) + }) + + it('should return false for invalid AuthenticationData (unknown type)', () => { + const authData = { + type: 'm.login.unknown', + session: 'session123' + } + expect(verifyAuthenticationData(authData as AuthenticationData)).toBe( + false + ) + }) + }) +}) diff --git a/packages/matrix-client-server/src/typecheckers.ts b/packages/matrix-client-server/src/typecheckers.ts new file mode 100644 index 00000000..9e1d7f09 --- /dev/null +++ b/packages/matrix-client-server/src/typecheckers.ts @@ -0,0 +1,121 @@ +import { + isClientSecretValid, + isCountryValid, + isEmailValid, + isMatrixIdValid, + isPhoneNumberValid, + isSidValid +} from '@twake/utils' +import { + type AuthenticationData, + type ThreepidCreds, + type UserIdentifier +} from './types' + +const MAX_STRINGS_LENGTH = 512 // Arbitrary value, could be changed + +export const verifyString = (value: any): boolean => { + return ( + typeof value === 'string' && + value.length > 0 && + value.length < MAX_STRINGS_LENGTH + ) +} + +export const verifyArray = (value: any, expectedType: string): boolean => { + if (!Array.isArray(value) || value.length === 0) { + return false + } + // eslint-disable-next-line valid-typeof + return value.every((element) => typeof element === expectedType) +} +export const verifyObject = (value: any): boolean => { + return typeof value === 'object' && value !== null && !Array.isArray(value) // Since typeof returns 'object' for arrays, we need to check that it's not an array +} + +export const verifyNumber = (value: any): boolean => { + return ( + typeof value === 'number' && + !Number.isNaN(value) && + value.toString().length < MAX_STRINGS_LENGTH // Again arbitrary check so that the numbers aren't absurdly large + ) +} + +export const verifyBoolean = (value: any): boolean => { + return typeof value === 'boolean' +} + +// Function to validate UserIdentifier +export const verifyUserIdentifier = (identifier: UserIdentifier): boolean => { + if (!verifyObject(identifier)) return false + + switch (identifier.type) { + case 'm.id.user': + return isMatrixIdValid(identifier.user) + + case 'm.id.thirdparty': + return ( + (identifier.medium === 'msisdn' && + isPhoneNumberValid(identifier.address)) || + (identifier.medium === 'email' && isEmailValid(identifier.address)) + ) + + case 'm.id.phone': + return ( + isCountryValid(identifier.country) && + isPhoneNumberValid(identifier.phone) + ) + + default: + return false + } +} + +// Function to validate ThreepidCreds +export const verifyThreepidCreds = (creds: ThreepidCreds): boolean => { + return ( + isSidValid(creds.sid) && + isClientSecretValid(creds.client_secret) && + (creds.id_server === undefined || verifyString(creds.id_server)) && + (creds.id_access_token === undefined || verifyString(creds.id_access_token)) + ) +} + +// Main function to validate AuthenticationData +export const verifyAuthenticationData = ( + authData: AuthenticationData +): boolean => { + if (!verifyObject(authData)) return false + + switch (authData.type) { + case 'm.login.password': + return ( + verifyUserIdentifier(authData.identifier) && + verifyString(authData.password) && + verifyString(authData.session) + ) + + case 'm.login.email.identity': + case 'm.login.msisdn': + return ( + verifyThreepidCreds(authData.threepid_creds) && + verifyString(authData.session) + ) + + case 'm.login.recaptcha': + return verifyString(authData.response) && verifyString(authData.session) + + case 'm.login.sso': + case 'm.login.dummy': + case 'm.login.terms': + return verifyString(authData.session) + + case 'm.login.registration_token': + return verifyString(authData.token) && verifyString(authData.session) + + case 'm.login.application_service': + return verifyString(authData.username) // Could be userId or localpart according to spec so we only check if it's a string : https://spec.matrix.org/v1.11/client-server-api/#appservice-login + default: + return false + } +} diff --git a/packages/matrix-client-server/src/types.ts b/packages/matrix-client-server/src/types.ts new file mode 100644 index 00000000..1d8cc6dc --- /dev/null +++ b/packages/matrix-client-server/src/types.ts @@ -0,0 +1,433 @@ +/* istanbul ignore file */ + +import { + type IdentityServerDb, + type Config as MIdentityServerConfig +} from '@twake/matrix-identity-server' + +export type Config = MIdentityServerConfig & { + login_flows: LoginFlowContent + application_services: AppServiceRegistration[] + sms_folder: string + is_registration_enabled: boolean + capabilities: Capabilities + is_sso_login_enabled: boolean + is_password_login_enabled: boolean + is_email_login_enabled: boolean + is_msisdn_login_enabled: boolean + is_recaptcha_login_enabled: boolean + is_terms_login_enabled: boolean + is_registration_token_login_enabled: boolean + registration_required_3pid: string[] + user_directory: UserDirectoryConfig + open_id_token_lifetime: number +} + +export type DbGetResult = Array< + Record> +> + +/* +/* FILTERS */ +/* + +/* https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3useruseridfilter */ +/* https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3useruseridfilterfilterid */ + +export interface Filter { + account_data?: EventFilter + event_fields?: string[] + event_format?: string // 'client' | 'federation' + presence?: EventFilter + room?: RoomFilter +} + +export interface EventFilter { + limit?: number + not_senders?: string[] + not_types?: string[] + senders?: string[] + types?: string[] +} + +export interface RoomFilter { + account_data?: RoomEventFilter + ephemeral?: RoomEventFilter + include_leave?: boolean + not_rooms?: string[] + rooms?: string[] + state?: StateFilter + timeline?: RoomEventFilter +} + +export interface RoomEventFilter extends EventFilter { + contains_url?: boolean + include_redundant_members?: boolean + lazy_load_members?: boolean + not_rooms?: string[] + rooms?: string[] + unread_thread_notifications?: boolean +} + +export type StateFilter = RoomEventFilter + +/* +/* EVENTS */ +/* + +/* https://spec.matrix.org/latest/client-server-api/#room-event-format */ + +export interface ClientEvent { + content: Record + event_id: string + origin_server_ts: number + room_id: string + sender: string + state_key?: string + type: string + unsigned?: UnsignedData +} + +export interface StateEvent extends Omit { + state_key: string +} + +/* https://spec.matrix.org/latest/client-server-api/#stripped-state */ +export interface StrippedStateEvent { + content: Record + sender: string + type: string + state_key: string +} + +export const stripEvent = (event: StateEvent): StrippedStateEvent => { + return { + content: event.content, + sender: event.sender, + type: event.type, + state_key: event.state_key + } +} + +/* ROOMS */ + +export interface RoomMember { + avatar_url: string + display_name: string +} +export interface PreviousRoom { + room_id: string + event_id: string +} + +/* +/* ROOM EVENTS */ + +export enum RoomEventTypes { + Member = 'm.room.member', + Create = 'm.room.create', + JoinRules = 'm.room.join_rules', + PowerLevels = 'm.room.power_levels', + CanonicalAlias = 'm.room.canonical_alias', + Aliases = 'm.room.aliases', // https://spec.matrix.org/v1.11/client-server-api/#historical-events + Tombstone = 'm.room.tombstone', // https://spec.matrix.org/v1.11/client-server-api/#mroomtombstone + Redaction = 'm.room.redaction', // https://spec.matrix.org/v1.11/client-server-api/#mroomredaction + ThirdPartyInvite = 'm.room.third_party_invite', // https://spec.matrix.org/v1.11/client-server-api/#mroomredaction + RoomHistoryVisibility = 'm.room.history_visibility', // https://spec.matrix.org/v1.11/client-server-api/#mroomhistory_visibility + Encrypted = 'm.room.encrypted', // https://spec.matrix.org/v1.11/client-server-api/#mroomencrypted + RoomAvatar = 'm.room.avatar', // https://spec.matrix.org/v1.11/client-server-api/#mroomavatar + RoomEncryption = 'm.room.encryption', // https://spec.matrix.org/v1.11/client-server-api/#mroomencryption + GuestAccess = 'm.room.guest_access', // https://spec.matrix.org/v1.11/client-server-api/#mroomguest_access + Message = 'm.room.message', // https://spec.matrix.org/v1.11/client-server-api/#mroommessage + Topic = 'm.room.topic', // https://spec.matrix.org/v1.11/client-server-api/#mroomtopic + Name = 'm.room.name', // https://spec.matrix.org/v1.11/client-server-api/#mroomname + ServerACL = 'm.room.server_acl', // https://spec.matrix.org/v1.11/client-server-api/#mroomserver_acl + Pinned = 'm.room.pinned_events' // https://spec.matrix.org/v1.11/client-server-api/#mroompinned_events +} + +/* m.room.canonical_alias */ +/* https://spec.matrix.org/v1.11/client-server-api/#mroomcanonical_alias */ + +export interface RoomCanonicalAliasEvent extends StateEvent { + content: { + alias?: string + alt_aliases?: string[] + } + state_key: '' +} + +/* m.room.create */ +/* https://spec.matrix.org/v1.11/client-server-api/#mroomcreate */ +export interface RoomCreateEvent extends StateEvent { + content: { + creator?: string + 'm.federate'?: boolean + predecessor?: PreviousRoom + room_version?: string + type?: string + } + state_key: '' +} + +/* m.room.join_rules */ +/* https://spec.matrix.org/v1.11/client-server-api/#mroomjoin_rules */ +export interface RoomJoinRulesEvent extends StateEvent { + content: { + allow?: AllowCondition[] + join_rule: string + } + state_key: '' +} + +export interface AllowCondition { + room_id?: string + type: string // 'm.room_membership' +} + +/* m.room.member */ +/* https://spec.matrix.org/v1.11/client-server-api/#mroommember */ +export interface RoomMemberEvent extends StateEvent { + content: EventContent +} + +export interface EventContent { + avatar_url?: string + displayname?: string | null + is_direct?: boolean + join_authorised_via_users_server?: string + membership: string + reason?: string + third_party_invite?: Invite +} + +export interface Invite { + display_name: string + signed: signed +} + +export interface signed { + mxid: string + signatures: Record> + token: string +} + +export enum Membership { + INVITE = 'invite', + JOIN = 'join', + KNOCK = 'knock', + LEAVE = 'leave', + BAN = 'ban' +} + +/* m.room.power_levels */ +/* https://spec.matrix.org/v1.11/client-server-api/#mroompower_levels */ +export interface RoomPowerLevelsEvent extends StateEvent { + content: { + ban?: number + events?: Record + events_default?: number + invite?: number + kick?: number + notifications?: Notifications + redact?: number + state_default?: number + users?: Record + users_default?: number + } + state_key: '' +} + +export interface Notifications { + room?: number + [key: string]: number | undefined +} + +/* General */ + +export interface LocalMediaRepository { + media_id: string + media_length: string + user_id: string +} +export interface MatrixUser { + name: string +} + +export interface UnsignedData { + age?: number + membership?: string + prev_content?: Record + redacted_because?: ClientEvent + transaction_id?: string +} + +export interface UserQuota { + user_id: string + size: number +} + +export type clientDbCollections = '' + +export type ClientServerDb = IdentityServerDb + +/* https://spec.matrix.org/v1.11/client-server-api/#identifier-types */ +export interface MatrixIdentifier { + type: 'm.id.user' + user: string +} + +export interface ThirdPartyIdentifier { + type: 'm.id.thirdparty' + medium: string + address: string +} + +export interface PhoneIdentifier { + type: 'm.id.phone' + country: string + phone: string +} + +export type UserIdentifier = + | MatrixIdentifier + | ThirdPartyIdentifier + | PhoneIdentifier + +/* https://spec.matrix.org/v1.11/client-server-api/#authentication-types */ +export type AuthenticationTypes = + | 'm.login.password' + | 'm.login.email.identity' + | 'm.login.msisdn' + | 'm.login.recaptcha' + | 'm.login.sso' + | 'm.login.dummy' + | 'm.login.registration_token' + | 'm.login.terms' + | 'm.login.application_service' + +interface PasswordAuth { + type: 'm.login.password' + identifier: UserIdentifier + password: string + session: string +} + +export interface ThreepidCreds { + sid: string + client_secret: string + id_server?: string + id_access_token?: string +} + +interface EmailAuth { + type: 'm.login.email.identity' + threepid_creds: ThreepidCreds + session: string +} + +interface PhoneAuth { + type: 'm.login.msisdn' + threepid_creds: ThreepidCreds + session: string +} + +interface RecaptchaAuth { + type: 'm.login.recaptcha' + response: string + session: string +} + +// TODO : Implement fallback to handle SSO authentication : https://spec.matrix.org/v1.11/client-server-api/#fallback +interface SsoAuth { + type: 'm.login.sso' + session: string +} + +interface DummyAuth { + type: 'm.login.dummy' + session: string +} + +interface TokenAuth { + type: 'm.login.registration_token' + token: string + session: string +} + +interface TermsAuth { + type: 'm.login.terms' + session: string +} + +export interface ApplicationServiceAuth { + type: 'm.login.application_service' + username: string +} + +export type AuthenticationData = + | PasswordAuth + | EmailAuth + | PhoneAuth + | RecaptchaAuth + | DummyAuth + | TokenAuth + | TermsAuth + | ApplicationServiceAuth + | SsoAuth + +export interface AuthenticationFlowContent { + flows: flowContent + params: Record // TODO : Fix any typing when we implement params for authentication types other than m.login.terms +} + +export type flowContent = stagesContent[] + +export interface LoginFlowContent { + flows: LoginFlow[] +} + +interface stagesContent { + stages: AuthenticationTypes[] +} + +interface LoginFlow { + get_login_token?: string + type: AuthenticationTypes +} + +/* https://spec.matrix.org/v1.11/application-service-api/#registration */ +export interface AppServiceRegistration { + as_token: string + hs_token: string + id: string + namespaces: Namespaces + protocols?: string[] + rate_limited?: boolean + sender_localpart: string + url: string +} + +interface Namespaces { + alias?: Namespace[] + rooms?: Namespace[] + users?: Namespace[] +} + +interface Namespace { + exclusive: boolean + regex: string +} + +/* https://spec.matrix.org/latest/client-server-api/#capabilities-negotiation */ + +interface Capabilities { + enable_set_displayname?: boolean + enable_set_avatar_url?: boolean + enable_3pid_changes?: boolean + enable_change_password?: boolean + enable_account_validity?: boolean +} + +interface UserDirectoryConfig { + enable_all_users_search?: boolean +} diff --git a/packages/matrix-client-server/src/user/account_data/getAccountData.ts b/packages/matrix-client-server/src/user/account_data/getAccountData.ts new file mode 100644 index 00000000..b02aafca --- /dev/null +++ b/packages/matrix-client-server/src/user/account_data/getAccountData.ts @@ -0,0 +1,75 @@ +import type MatrixClientServer from '../..' +import { + errMsg, + type expressAppHandler, + send, + isMatrixIdValid, + isEventTypeValid +} from '@twake/utils' + +interface Parameters { + userId: string + type: string +} + +const getAccountData = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const parameters: Parameters = req.params as Parameters + if ( + !isMatrixIdValid(parameters.userId) || + !isEventTypeValid(parameters.type) + ) { + send(res, 400, errMsg('invalidParam'), clientServer.logger) + return + } + clientServer.authenticate(req, res, (data, token) => { + if (parameters.userId !== data.sub) { + // The config is only visible to the user that set the account data + send( + res, + 403, + { + errcode: 'M_FORBIDDEN', + error: + 'The access token provided is not authorized to update this user’s account data.' + }, + clientServer.logger + ) + return + } + clientServer.matrixDb + .get('account_data', ['content'], { + user_id: parameters.userId, + account_data_type: parameters.type + }) + .then((rows) => { + if (rows.length === 0) { + send( + res, + 404, + { + errcode: 'M_NOT_FOUND', + error: + 'No account data has been provided for this user with the given type.' + }, + clientServer.logger + ) + } else { + const body: Record = {} + body[parameters.type] = rows[0].content as string + send(res, 200, body, clientServer.logger) + } + }) + .catch((e) => { + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', e.toString()), clientServer.logger) + }) + }) + } +} + +export default getAccountData diff --git a/packages/matrix-client-server/src/user/account_data/putAccountData.ts b/packages/matrix-client-server/src/user/account_data/putAccountData.ts new file mode 100644 index 00000000..64acfdee --- /dev/null +++ b/packages/matrix-client-server/src/user/account_data/putAccountData.ts @@ -0,0 +1,88 @@ +import type MatrixClientServer from '../..' +import { + jsonContent, + validateParameters, + errMsg, + type expressAppHandler, + send, + isMatrixIdValid, + isEventTypeValid +} from '@twake/utils' + +interface Parameters { + userId: string + type: string +} + +interface PutRequestBody { + content: string +} + +const schema = { + content: true +} +const contentRegex = /^.{0,2048}$/ // Prevent the client from sending too long messages that could crash the DB. This value is arbitrary and could be changed + +const putAccountData = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const parameters: Parameters = req.params as Parameters + if ( + !isMatrixIdValid(parameters.userId) || + !isEventTypeValid(parameters.type) + ) { + send(res, 400, errMsg('invalidParam'), clientServer.logger) + return + } + clientServer.authenticate(req, res, (data, token) => { + jsonContent(req, res, clientServer.logger, (obj) => { + validateParameters(res, schema, obj, clientServer.logger, (obj) => { + if (parameters.userId !== data.sub) { + // The config is only visible to the user that set the account data + send( + res, + 403, + { + errcode: 'M_FORBIDDEN', + error: + 'The access token provided is not authorized to update this user’s account data.' + }, + clientServer.logger + ) + return + } + if (!contentRegex.test((obj as PutRequestBody).content)) { + send(res, 400, errMsg('invalidParam', 'Content is too long')) + return + } + clientServer.matrixDb + .updateWithConditions( + 'account_data', + { content: (obj as PutRequestBody).content }, + [ + { field: 'user_id', value: parameters.userId }, + { field: 'account_data_type', value: parameters.type } + ] + ) + .then(() => { + send(res, 200, {}, clientServer.logger) + }) + .catch((e) => { + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', e.toString()), + clientServer.logger + ) + }) + }) + }) + }) + } +} + +export default putAccountData diff --git a/packages/matrix-client-server/src/user/filter/getFilter.ts b/packages/matrix-client-server/src/user/filter/getFilter.ts new file mode 100644 index 00000000..e776a357 --- /dev/null +++ b/packages/matrix-client-server/src/user/filter/getFilter.ts @@ -0,0 +1,43 @@ +import type MatrixClientServer from '../..' +import { errMsg, send, type expressAppHandler } from '@twake/utils' +import { type Request } from 'express' + +const GetFilter = (clientServer: MatrixClientServer): expressAppHandler => { + return (req, res) => { + clientServer.authenticate(req, res, (data, id) => { + const filterId = (req as Request).params.filterId + const userId = (req as Request).params.userId + if (userId !== data.sub || !clientServer.isMine(userId)) { + clientServer.logger.error( + 'Forbidden user id for getting a filter:', + userId + ) + send(res, 403, errMsg('forbidden')) + return + } + clientServer.matrixDb + .get('user_filters', ['filter_json'], { + user_id: userId, + filter_id: filterId + }) + .then((rows) => { + if (rows.length === 0) { + clientServer.logger.error('Filter not found') + send(res, 404, errMsg('notFound', 'Cannot retrieve filter')) + return + } + const filter = JSON.parse(rows[0].filter_json as string) // TODO : clarify the type of the filter_json (bytea, string ???) + clientServer.logger.info('Fetched filter:', filterId) + send(res, 200, filter) + }) + .catch((e) => { + /* istanbul ignore next */ + clientServer.logger.error('Error while fetching filter', e) + /* istanbul ignore next */ + send(res, 500, errMsg('unknown')) + }) + }) + } +} + +export default GetFilter diff --git a/packages/matrix-client-server/src/user/filter/postFilter.ts b/packages/matrix-client-server/src/user/filter/postFilter.ts new file mode 100644 index 00000000..d35c8952 --- /dev/null +++ b/packages/matrix-client-server/src/user/filter/postFilter.ts @@ -0,0 +1,73 @@ +import { + errMsg, + type expressAppHandler, + jsonContent, + send, + validateParametersStrict, + isMatrixIdValid +} from '@twake/utils' +import type MatrixClientServer from '../..' +import type { Request } from 'express' +import { randomString } from '@twake/crypto' +import { Filter } from '../../utils/filter' + +const schema = { + account_data: false, + event_fields: false, + event_format: false, + presence: false, + room: false +} + +const PostFilter = (clientServer: MatrixClientServer): expressAppHandler => { + return (req, res) => { + clientServer.authenticate(req, res, (data) => { + jsonContent(req, res, clientServer.logger, (obj) => { + validateParametersStrict( + res, + schema, + obj, + clientServer.logger, + (obj) => { + const filter: Filter = new Filter(obj) + // TODO : verify if the user is allowed to make requests for this user id + // we consider for the moment that the user is only allowed to make requests for his own user id + const userId = (req as Request).params.userId + if (!isMatrixIdValid(userId)) { + send(res, 400, errMsg('invalidParam', 'Invalid user ID')) + return + } + if (userId !== data.sub || !clientServer.isMine(userId)) { + clientServer.logger.error( + 'Forbidden user id for posting a filter:', + userId + ) + send(res, 403, errMsg('forbidden')) + return + } + // Assuming this will guarantee the unique constraint + const filterId = randomString(16) + clientServer.matrixDb + .insert('user_filters', { + user_id: userId, + filter_id: filterId, + filter_json: JSON.stringify(filter) // TODO : clarify the type of the filter_json (bytea, string ???) + }) + .then(() => { + clientServer.logger.info('Inserted filter:', filterId) + send(res, 200, { filter_id: filterId }) + }) + .catch((e) => { + /* istanbul ignore next */ + clientServer.logger.error('Error while inserting filter:', e) + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', e.toString())) + }) + } + ) + }) + }) + } +} + +export default PostFilter diff --git a/packages/matrix-client-server/src/user/openid/requestToken.ts b/packages/matrix-client-server/src/user/openid/requestToken.ts new file mode 100644 index 00000000..4928251f --- /dev/null +++ b/packages/matrix-client-server/src/user/openid/requestToken.ts @@ -0,0 +1,74 @@ +import { + epoch, + errMsg, + isMatrixIdValid, + send, + type expressAppHandler +} from '@twake/utils' +import type MatrixClientServer from '../..' +import { randomString } from '@twake/crypto' +import { type DbGetResult } from '@twake/matrix-identity-server' + +interface Parameters { + userId: string +} + +export const insertOpenIdToken = async ( + clientServer: MatrixClientServer, + userId: string, + token: string +): Promise => { + return await clientServer.matrixDb.insert('open_id_tokens', { + token, + user_id: userId, + ts_valid_until_ms: epoch() + clientServer.conf.open_id_token_lifetime + }) +} + +const requestToken = (clientServer: MatrixClientServer): expressAppHandler => { + return (req, res) => { + clientServer.authenticate(req, res, (data) => { + // @ts-expect-error req has params + const userId = (req.params as Parameters).userId + if (!isMatrixIdValid(userId)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid user ID'), + clientServer.logger + ) + return + } + if (userId !== data.sub) { + clientServer.logger.error( + 'The access token provided does not correspond to the userId sent in the request parameters.' + ) + send(res, 403, errMsg('forbidden'), clientServer.logger) + return + } + const accessToken = randomString(64) + insertOpenIdToken(clientServer, userId, accessToken) + .then(() => { + send( + res, + 200, + { + access_token: accessToken, + expires_in: clientServer.conf.open_id_token_lifetime, + matrix_server_name: clientServer.conf.server_name, + token_type: 'Bearer' + }, + clientServer.logger + ) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error('Error while inserting open_id_token', e) + // istanbul ignore next + send(res, 500, e, clientServer.logger) + }) + }) + } +} + +export default requestToken diff --git a/packages/matrix-client-server/src/user/rooms/getRoomAccountData.ts b/packages/matrix-client-server/src/user/rooms/getRoomAccountData.ts new file mode 100644 index 00000000..b183359f --- /dev/null +++ b/packages/matrix-client-server/src/user/rooms/getRoomAccountData.ts @@ -0,0 +1,78 @@ +import type MatrixClientServer from '../..' +import { + errMsg, + type expressAppHandler, + send, + isMatrixIdValid, + isEventTypeValid, + isRoomIdValid +} from '@twake/utils' + +interface Parameters { + userId: string + type: string + roomId: string +} + +const getRoomAccountData = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const parameters: Parameters = req.params as Parameters + if ( + !isMatrixIdValid(parameters.userId) || + !isEventTypeValid(parameters.type) || + !isRoomIdValid(parameters.roomId) + ) { + send(res, 400, errMsg('invalidParam'), clientServer.logger) + return + } + clientServer.authenticate(req, res, (data, token) => { + if (parameters.userId !== data.sub) { + send( + res, + 403, + { + errcode: 'M_FORBIDDEN', + error: + 'The access token provided is not authorized to update this user’s account data.' + }, + clientServer.logger + ) + return + } + clientServer.matrixDb + .get('room_account_data', ['content', 'user_id'], { + user_id: parameters.userId, + account_data_type: parameters.type, + room_id: parameters.roomId + }) + .then((rows) => { + if (rows.length === 0) { + send( + res, + 404, + { + errcode: 'M_NOT_FOUND', + error: + 'No account data has been provided for this user and this room with the given type.' + }, + clientServer.logger + ) + } else { + const body: Record = {} + body[parameters.type] = rows[0].content as string + send(res, 200, body, clientServer.logger) + } + }) + .catch((e) => { + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', e.toString()), clientServer.logger) + }) + }) + } +} + +export default getRoomAccountData diff --git a/packages/matrix-client-server/src/user/rooms/putRoomAccountData.ts b/packages/matrix-client-server/src/user/rooms/putRoomAccountData.ts new file mode 100644 index 00000000..415f4b36 --- /dev/null +++ b/packages/matrix-client-server/src/user/rooms/putRoomAccountData.ts @@ -0,0 +1,94 @@ +import type MatrixClientServer from '../..' +import { + jsonContent, + validateParameters, + errMsg, + type expressAppHandler, + send, + isMatrixIdValid, + isEventTypeValid, + isRoomIdValid +} from '@twake/utils' + +interface Parameters { + userId: string + type: string + roomId: string +} + +interface PutRequestBody { + content: string +} + +const schema = { + content: true +} + +const contentRegex = /^.{0,2048}$/ // Prevent the client from sending too long messages that could crash the DB. This value is arbitrary and could be changed + +// TODO : Handle error 405 where the type of account data is controlled by the server and cannot be modified by the client + +const putRoomAccountData = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const parameters: Parameters = req.params as Parameters + if ( + !isMatrixIdValid(parameters.userId) || + !isEventTypeValid(parameters.type) || + !isRoomIdValid(parameters.roomId) + ) { + send(res, 400, errMsg('invalidParam'), clientServer.logger) + return + } + clientServer.authenticate(req, res, (data, token) => { + jsonContent(req, res, clientServer.logger, (obj) => { + validateParameters(res, schema, obj, clientServer.logger, (obj) => { + if (parameters.userId !== data.sub) { + send( + res, + 403, + { + errcode: 'M_FORBIDDEN', + error: + 'The access token provided is not authorized to update this user’s account data.' + }, + clientServer.logger + ) + return + } + if (!contentRegex.test((obj as PutRequestBody).content)) { + send(res, 400, errMsg('invalidParam', 'Content is too long')) + return + } + clientServer.matrixDb + .updateWithConditions( + 'room_account_data', + { content: (obj as PutRequestBody).content }, + [ + { field: 'user_id', value: parameters.userId }, + { field: 'account_data_type', value: parameters.type }, + { field: 'room_id', value: parameters.roomId } + ] + ) + .then(() => { + send(res, 200, {}, clientServer.logger) + }) + .catch((e) => { + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', e.toString()), + clientServer.logger + ) + }) + }) + }) + }) + } +} + +export default putRoomAccountData diff --git a/packages/matrix-client-server/src/user/user.test.ts b/packages/matrix-client-server/src/user/user.test.ts new file mode 100644 index 00000000..831a6b69 --- /dev/null +++ b/packages/matrix-client-server/src/user/user.test.ts @@ -0,0 +1,661 @@ +import fs from 'fs' +import request from 'supertest' +import express from 'express' +import ClientServer from '../index' +import { buildMatrixDb, buildUserDB } from '../__testData__/buildUserDB' +import { type Config, type Filter } from '../types' +import defaultConfig from '../__testData__/registerConf.json' +import { getLogger, type TwakeLogger } from '@twake/logger' +import { + setupTokens, + validToken, + validToken2 +} from '../__testData__/setupTokens' +jest.mock('node-fetch', () => jest.fn()) +const sendMailMock = jest.fn() +jest.mock('nodemailer', () => ({ + createTransport: jest.fn().mockImplementation(() => ({ + sendMail: sendMailMock + })) +})) + +let conf: Config +let clientServer: ClientServer +let app: express.Application + +const logger: TwakeLogger = getLogger() + +beforeAll((done) => { + // @ts-expect-error TS doesn't understand that the config is valid + conf = { + ...defaultConfig, + base_url: 'http://example.com/', + matrix_database_host: 'src/__testData__/userTestMatrix.db', + userdb_host: 'src/__testData__/userTest.db', + database_host: 'src/__testData__/userTest.db', + registration_required_3pid: ['email', 'msisdn'] + } + if (process.env.TEST_PG === 'yes') { + conf.database_engine = 'pg' + conf.userdb_engine = 'pg' + conf.database_host = process.env.PG_HOST ?? 'localhost' + conf.database_user = process.env.PG_USER ?? 'twake' + conf.database_password = process.env.PG_PASSWORD ?? 'twake' + conf.database_name = process.env.PG_DATABASE ?? 'test' + } + buildUserDB(conf) + .then(() => { + buildMatrixDb(conf) + .then(() => { + done() + }) + .catch((e) => { + logger.error('Error while building matrix db:', e) + done(e) + }) + }) + .catch((e) => { + logger.error('Error while building user db:', e) + done(e) + }) +}) + +afterAll(() => { + fs.unlinkSync('src/__testData__/userTest.db') + fs.unlinkSync('src/__testData__/userTestMatrix.db') +}) + +beforeEach(() => { + jest.clearAllMocks() +}) + +describe('Use configuration file', () => { + beforeAll((done) => { + clientServer = new ClientServer(conf) + app = express() + clientServer.ready + .then(() => { + Object.keys(clientServer.api.get).forEach((k) => { + app.get(k, clientServer.api.get[k]) + }) + Object.keys(clientServer.api.post).forEach((k) => { + app.post(k, clientServer.api.post[k]) + }) + Object.keys(clientServer.api.put).forEach((k) => { + app.put(k, clientServer.api.put[k]) + }) + Object.keys(clientServer.api.delete).forEach((k) => { + app.delete(k, clientServer.api.delete[k]) + }) + done() + }) + .catch((e) => { + done(e) + }) + }) + + afterAll(() => { + clientServer.cleanJobs() + }) + + describe('Endpoints with authentication', () => { + beforeAll(async () => { + await setupTokens(clientServer, logger) + }) + + describe('/_matrix/client/v3/user/:userId', () => { + describe('/_matrix/client/v3/user/:userId/account_data/:type', () => { + it('should reject invalid userId', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/invalidUserId/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject invalid roomId', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/rooms/invalidRoomId/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject an invalid event type', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/account_data/invalidEventType' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject missing account data', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(404) + expect(response.body).toHaveProperty('errcode', 'M_NOT_FOUND') + }) + it('should refuse to return account data for another user', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@anotheruser:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + }) + it('should return account data', async () => { + await clientServer.matrixDb.insert('account_data', { + user_id: '@testuser:example.com', + account_data_type: 'm.room.message', + stream_id: 1, + content: 'test content' + }) + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body['m.room.message']).toBe('test content') + }) + it('should reject invalid userId', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/invalidUserId/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject an invalid event type', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@testuser:example.com/account_data/invalidEventType' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject missing account data', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@testuser:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_UNKNOWN') // Error code from jsonContent function of @twake/utils + }) + it('should refuse to update account data for another user', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@anotheruser:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ content: 'new content' }) + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + }) + it('should update account data', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@testuser:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ content: 'updated content' }) + expect(response.statusCode).toBe(200) + const response2 = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response2.statusCode).toBe(200) + expect(response2.body['m.room.message']).toBe('updated content') + }) + }) + + describe('/_matrix/client/v3/user/:userId/rooms/:roomId/account_data/:type', () => { + describe('GET', () => { + it('should reject invalid userId', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/invalidUserId/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject invalid roomId', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/rooms/invalidRoomId/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject an invalid event type', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/rooms/!roomId:example.com/account_data/invalidEventType' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject missing account data', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(404) + expect(response.body).toHaveProperty('errcode', 'M_NOT_FOUND') + }) + it('should refuse to return account data for another user', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@anotheruser:example.com/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + }) + it('should return account data', async () => { + await clientServer.matrixDb.insert('room_account_data', { + user_id: '@testuser:example.com', + account_data_type: 'm.room.message', + stream_id: 1, + content: 'test content', + room_id: '!roomId:example.com' + }) + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body['m.room.message']).toBe('test content') + }) + }) + describe('PUT', () => { + it('should reject invalid userId', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/invalidUserId/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject invalid roomId', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@testuser:example.com/rooms/invalidRoomId/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject an invalid event type', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@testuser:example.com/rooms/!roomId:example.com/account_data/invalidEventType' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject missing account data', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@testuser:example.com/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_UNKNOWN') // Error code from jsonContent function of @twake/utils + }) + it('should refuse to update account data for another user', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@anotheruser:example.com/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ content: 'new content' }) + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + }) + it('should update account data', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@testuser:example.com/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ content: 'updated content' }) + expect(response.statusCode).toBe(200) + const response2 = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response2.statusCode).toBe(200) + expect(response2.body['m.room.message']).toBe('updated content') + }) + }) + }) + }) + describe('/_matrix/client/v3/user/:userId/openid/request_token', () => { + it('should reject invalid userId', async () => { + const response = await request(app) + .post('/_matrix/client/v3/user/invalidUserId/openid/request_token') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject a userId that does not match the token', async () => { + const response = await request(app) + .post( + '/_matrix/client/v3/user/@testuser:example.com/openid/request_token' + ) + .set('Authorization', `Bearer ${validToken2}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + }) + it('should return a token on a valid attempt', async () => { + const response = await request(app) + .post( + '/_matrix/client/v3/user/@testuser:example.com/openid/request_token' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('access_token') + expect(response.body).toHaveProperty('expires_in') + expect(response.body).toHaveProperty('matrix_server_name') + expect(response.body).toHaveProperty('token_type') + }) + describe('/_matrix/client/v3/user/:userId/filter', () => { + beforeAll(async () => { + try { + await clientServer.matrixDb.insert('user_filters', { + user_id: '@testuser:example.com', + filter_id: '1234', + filter_json: JSON.stringify({ filter: true }) + }) + await clientServer.matrixDb.insert('user_filters', { + user_id: '@testuser2:example.com', + filter_id: '1235', + filter_json: JSON.stringify({ filter: true }) + }) + await clientServer.matrixDb.insert('user_filters', { + user_id: '@testuser:example2.com', + filter_id: '1234', + filter_json: JSON.stringify({ filter: true }) + }) + logger.info('Filters inserted') + } catch (e) { + logger.error('Error inserting filters in db', e) + } + }) + afterAll(async () => { + try { + await clientServer.matrixDb.deleteEqual( + 'user_filters', + 'user_id', + '@testuser:example.com' + ) + await clientServer.matrixDb.deleteEqual( + 'user_filters', + 'user_id', + '@testuser2:example.com' + ) + await clientServer.matrixDb.deleteEqual( + 'user_filters', + 'user_id', + '@testuser:example2.com' + ) + logger.info('Filters deleted') + } catch (e) { + logger.error('Error deleting filters in db', e) + } + }) + const filter: Filter = { + event_fields: ['type', 'content', 'sender'], + event_format: 'client', + presence: { + not_senders: ['@alice:example.com'], + types: ['m.presence'] + }, + room: { + ephemeral: { + not_rooms: ['!726s6s6q:example.com'], + not_senders: ['@spam:example.com'], + types: ['m.receipt', 'm.typing'] + }, + state: { + not_rooms: ['!726s6s6q:example.com'], + types: ['m.room.*'] + }, + timeline: { + limit: 10, + not_rooms: ['!726s6s6q:example.com'], + not_senders: ['@spam:example.com'], + types: ['m.room.message'] + } + } + } + let filterId: string + + describe('POST', () => { + it('should reject invalid parameters', async () => { + // Additional parameters not supported + const response = await request(app) + .post('/_matrix/client/v3/user/@testuser:example.com/filter') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ notAFilterField: 'test' }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'UNKNOWN_PARAM') + }) + it('should reject posting a filter for an other userId', async () => { + const response = await request(app) + .post('/_matrix/client/v3/user/@testuser2:example.com/filter') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send(filter) + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + }) + it('should reject posting a filter for an other server name', async () => { + const response = await request(app) + .post('/_matrix/client/v3/user/@testuser:example2.com/filter') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send(filter) + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + }) + it('should post a filter', async () => { + const response = await request(app) + .post('/_matrix/client/v3/user/@testuser:example.com/filter') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send(filter) + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('filter_id') + filterId = response.body.filter_id + }) + }) + describe('GET', () => { + it('should reject getting a filter for an other userId', async () => { + const response = await request(app) + .get( + `/_matrix/client/v3/user/@testuser2:example.com/filter/${filterId}` + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + }) + it('should reject getting a filter for an other server name', async () => { + const response = await request(app) + .get( + `/_matrix/client/v3/user/@testuser:example2.com/filter/${filterId}` + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + }) + it('should reject getting a filter that does not exist', async () => { + const response = await request(app) + .get( + `/_matrix/client/v3/user/@testuser:example.com/filter/invalidFilterId` + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(404) + expect(response.body).toHaveProperty('errcode', 'M_NOT_FOUND') + }) + it('should get a filter', async () => { + const response = await request(app) + .get( + `/_matrix/client/v3/user/@testuser:example.com/filter/${filterId}` + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + // We can't simply write expect(response.body).toEqual(filter) because many default values were added + expect(response.body.event_fields).toEqual(filter.event_fields) + expect(response.body.event_format).toEqual(filter.event_format) + expect(response.body.presence.not_senders).toEqual( + filter.presence?.not_senders + ) + expect(response.body.presence.types).toEqual(filter.presence?.types) + expect(response.body.room.ephemeral.not_rooms).toEqual( + filter.room?.ephemeral?.not_rooms + ) + expect(response.body.room.ephemeral.not_senders).toEqual( + filter.room?.ephemeral?.not_senders + ) + expect(response.body.room.ephemeral.types).toEqual( + filter.room?.ephemeral?.types + ) + expect(response.body.room.state.not_rooms).toEqual( + filter.room?.state?.not_rooms + ) + expect(response.body.room.state.types).toEqual( + filter.room?.state?.types + ) + expect(response.body.room.timeline.limit).toEqual( + filter.room?.timeline?.limit + ) + expect(response.body.room.timeline.not_rooms).toEqual( + filter.room?.timeline?.not_rooms + ) + expect(response.body.room.timeline.not_senders).toEqual( + filter.room?.timeline?.not_senders + ) + expect(response.body.room.timeline.types).toEqual( + filter.room?.timeline?.types + ) + }) + }) + }) + }) + }) + describe('/_matrix/client/v3/register', () => { + let session: string + it('should validate UIAuth with msisdn and email verification', async () => { + const response1 = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ + username: 'new_user', + device_id: 'device_Id', + inhibit_login: true, + initial_device_display_name: 'testdevice' + }) + expect(response1.statusCode).toBe(401) + expect(response1.body).toHaveProperty('session') + session = response1.body.session + const response2 = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ + username: 'new_user', + device_id: 'device_Id', + inhibit_login: true, + initial_device_display_name: 'testdevice', + auth: { + type: 'm.login.msisdn', + session, + threepid_creds: { + sid: 'validatedSession', + client_secret: 'validatedSecret' + } + } + }) + expect(response2.statusCode).toBe(401) + expect(response2.body).toHaveProperty('completed') + expect(response2.body.completed).toEqual(['m.login.msisdn']) + const response3 = await request(app) + .post('/_matrix/client/v3/register') + .set('User-Agent', 'curl/7.31.0-DEV') + .set('X-Forwarded-For', '203.0.113.195') + .query({ kind: 'user' }) + .send({ + username: 'new_user', + device_id: 'device_Id', + inhibit_login: true, + initial_device_display_name: 'testdevice', + auth: { + type: 'm.login.email.identity', + session, + threepid_creds: { + sid: 'validatedSession2', + client_secret: 'validatedSecret2' + } + } + }) + expect(response3.statusCode).toBe(200) + }) + }) +}) diff --git a/packages/matrix-client-server/src/user_data/index.test.ts b/packages/matrix-client-server/src/user_data/index.test.ts new file mode 100644 index 00000000..9ba5cb0c --- /dev/null +++ b/packages/matrix-client-server/src/user_data/index.test.ts @@ -0,0 +1,814 @@ +import { getLogger, type TwakeLogger } from '@twake/logger' +import { randomString } from '@twake/crypto' +import ClientServer from '../index' +import { type Config } from '../types' +import express from 'express' +import defaultConfig from '../__testData__/registerConf.json' +import { buildMatrixDb, buildUserDB } from '../__testData__/buildUserDB' +import fs from 'fs' +import request from 'supertest' +import { + setupTokens, + validToken, + validToken2 +} from '../__testData__/setupTokens' + +jest.mock('node-fetch', () => jest.fn()) + +let conf: Config +let clientServer: ClientServer +let app: express.Application + +const logger: TwakeLogger = getLogger() + +beforeAll((done) => { + // @ts-expect-error TS doesn't understand that the config is valid + conf = { + ...defaultConfig, + cron_service: false, + database_engine: 'sqlite', + base_url: 'http://example.com/', + userdb_engine: 'sqlite', + matrix_database_engine: 'sqlite', + matrix_database_host: './src/__testData__/testMatrixUserData.db', + database_host: './src/__testData__/testUserData.db', + userdb_host: './src/__testData__/testUserData.db' + } + if (process.env.TEST_PG === 'yes') { + conf.database_engine = 'pg' + conf.userdb_engine = 'pg' + conf.database_host = process.env.PG_HOST ?? 'localhost' + conf.database_user = process.env.PG_USER ?? 'twake' + conf.database_password = process.env.PG_PASSWORD ?? 'twake' + conf.database_name = process.env.PG_DATABASE ?? 'test' + } + buildUserDB(conf) + .then(() => { + buildMatrixDb(conf) + .then(() => { + done() + }) + .catch((e) => { + logger.error('Error while building matrix db:', e) + done(e) + }) + }) + .catch((e) => { + logger.error('Error while building user db:', e) + done(e) + }) +}) + +afterAll(() => { + fs.unlinkSync('./src/__testData__/testMatrixUserData.db') + fs.unlinkSync('./src/__testData__/testUserData.db') +}) + +describe('Use configuration file', () => { + beforeAll((done) => { + clientServer = new ClientServer(conf) + app = express() + clientServer.ready + .then(() => { + Object.keys(clientServer.api.get).forEach((k) => { + app.get(k, clientServer.api.get[k]) + }) + Object.keys(clientServer.api.post).forEach((k) => { + app.post(k, clientServer.api.post[k]) + }) + Object.keys(clientServer.api.put).forEach((k) => { + app.put(k, clientServer.api.put[k]) + }) + Object.keys(clientServer.api.delete).forEach((k) => { + app.delete(k, clientServer.api.delete[k]) + }) + done() + }) + .catch((e) => { + done(e) + }) + }) + + afterAll(() => { + clientServer.cleanJobs() + }) + + describe('/_matrix/client/v3/profile/:userId', () => { + describe('GET', () => { + const testUserId = '@testuser:example.com' + const incompleteUserId = '@incompleteuser:example.com' + + beforeAll(async () => { + try { + await clientServer.matrixDb.insert('profiles', { + user_id: testUserId, + displayname: 'Test User', + avatar_url: 'http://example.com/avatar.jpg' + }) + logger.info('Test user profile created') + + await clientServer.matrixDb.insert('profiles', { + user_id: incompleteUserId + }) + logger.info('Incomplete test user profile created') + } catch (e) { + logger.error('Error creating profiles:', e) + } + }) + + afterAll(async () => { + try { + await clientServer.matrixDb.deleteEqual( + 'profiles', + 'user_id', + testUserId + ) + logger.info('Test user profile deleted') + + await clientServer.matrixDb.deleteEqual( + 'profiles', + 'user_id', + incompleteUserId + ) + logger.info('Incomplete test user profile deleted') + } catch (e) { + logger.error('Error deleting profiles:', e) + } + }) + + describe('/_matrix/client/v3/profile/:userId', () => { + it('should return the profile information for an existing user', async () => { + const response = await request(app).get( + `/_matrix/client/v3/profile/${testUserId}` + ) + + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('avatar_url') + expect(response.body).toHaveProperty('displayname') + }) + + // it('should return error 403 if the server is unwilling to disclose profile information', async () => { + // const response = await request(app).get( + // '/_matrix/client/v3/profile/@forbiddenuser:example.com' + // ) + + // expect(response.statusCode).toBe(403) + // expect(response.body.errcode).toBe('M_FORBIDDEN') + // expect(response.body).toHaveProperty('error') + // }) + + it('should return error 404 if the user does not exist', async () => { + const response = await request(app).get( + '/_matrix/client/v3/profile/@nonexistentuser:example.com' + ) + + expect(response.statusCode).toBe(404) + expect(response.body.errcode).toBe('M_NOT_FOUND') + expect(response.body).toHaveProperty('error') + }) + }) + + describe('/_matrix/client/v3/profile/:userId/avatar_url', () => { + it('should return the avatar_url for an existing user', async () => { + const response = await request(app).get( + `/_matrix/client/v3/profile/${testUserId}/avatar_url` + ) + + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('avatar_url') + }) + + it('should return error 404 if the user does not exist', async () => { + const response = await request(app).get( + '/_matrix/client/v3/profile/@nonexistentuser:example.com/avatar_url' + ) + expect(response.statusCode).toBe(404) + expect(response.body.errcode).toBe('M_NOT_FOUND') + expect(response.body).toHaveProperty('error') + }) + + it('should return error 404 if the user does not have an existing avatar_url', async () => { + const response = await request(app).get( + '/_matrix/client/v3/profile/@incompleteuser:example.com/avatar_url' + ) + expect(response.statusCode).toBe(404) + expect(response.body.errcode).toBe('M_NOT_FOUND') + expect(response.body).toHaveProperty('error') + }) + }) + + describe('/_matrix/client/v3/profile/:userId/displayname', () => { + it('should return the displayname for an existing user', async () => { + const response = await request(app).get( + `/_matrix/client/v3/profile/${testUserId}/displayname` + ) + + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('displayname') + }) + + it('should return error 404 if the user does not exist', async () => { + const response = await request(app).get( + '/_matrix/client/v3/profile/@nonexistentuser:example.com/displayname' + ) + + expect(response.statusCode).toBe(404) + expect(response.body.errcode).toBe('M_NOT_FOUND') + expect(response.body).toHaveProperty('error') + }) + + it('should return error 404 if the user does not have an existing avatar_url', async () => { + const response = await request(app).get( + '/_matrix/client/v3/profile/@incompleteuser:example.com/displayname' + ) + + expect(response.statusCode).toBe(404) + expect(response.body.errcode).toBe('M_NOT_FOUND') + expect(response.body).toHaveProperty('error') + }) + }) + }) + }) + + describe('Endpoints with authentication', () => { + beforeAll(async () => { + await setupTokens(clientServer, logger) + }) + + describe('/_matrix/client/v3/profile/:userId', () => { + describe('PUT', () => { + const testUserId = '@testuser:example.com' + beforeAll(async () => { + try { + await clientServer.matrixDb.insert('users', { + name: '@testuser2:example.com', + admin: 1 + }) + await clientServer.matrixDb.insert('users', { + name: '@testuser3:example.com', + admin: 0 + }) + await clientServer.matrixDb.insert('profiles', { + user_id: testUserId, + displayname: 'Test User', + avatar_url: 'http://example.com/avatar.jpg' + }) + logger.info('Test user profile created') + } catch (e) { + logger.error('Error creating test user profile:', e) + } + }) + + afterAll(async () => { + try { + await clientServer.matrixDb.deleteEqual( + 'users', + 'name', + '@testuser2:example.com' + ) + await clientServer.matrixDb.deleteEqual( + 'users', + 'name', + '@testuser3:example.com' + ) + await clientServer.matrixDb.deleteEqual( + 'profiles', + 'user_id', + testUserId + ) + logger.info('Test user profile deleted') + } catch (e) { + logger.error('Error deleting test user profile:', e) + } + }) + + describe('/_matrix/client/v3/profile/:userId/avatar_url', () => { + it('should require authentication', async () => { + const response = await request(app) + .put(`/_matrix/client/v3/profile/${testUserId}/avatar_url`) + .set('Authorization', 'Bearer invalidToken') + .set('Accept', 'application/json') + expect(response.statusCode).toBe(401) + }) + + it('should return 400 if the target user is on a remote server', async () => { + const response = await request(app) + .put( + `/_matrix/client/v3/profile/@testuser:anotherexample.com/avatar_url` + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ avatar_url: 'http://example.com/new_avatar.jpg' }) + expect(response.statusCode).toBe(400) + }) + + it('should return 403 if the requester is not admin and is not the target user', async () => { + const response = await request(app) + .put( + `/_matrix/client/v3/profile/@testuser2:example.com/avatar_url` + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ avatar_url: 'http://example.com/new_avatar.jpg' }) + expect(response.statusCode).toBe(403) + }) + + it('should return 403 if the requester is not admin and the config does not allow changing avatar_url', async () => { + clientServer.conf.capabilities.enable_set_avatar_url = false + + const response = await request(app) + .put(`/_matrix/client/v3/profile/${testUserId}/avatar_url`) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ avatar_url: 'http://example.com/new_avatar.jpg' }) + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + + clientServer.conf.capabilities.enable_set_avatar_url = true + }) + + it('should return 400 if provided avatar_url is too long', async () => { + clientServer.conf.capabilities.enable_set_avatar_url = true + const response = await request(app) + .put(`/_matrix/client/v3/profile/${testUserId}/avatar_url`) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ avatar_url: randomString(2049) }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + + it('should send correct response when requester is admin and target user is on local server', async () => { + clientServer.conf.capabilities.enable_set_avatar_url = true + const response = await request(app) + .put(`/_matrix/client/v3/profile/${testUserId}/avatar_url`) + .set('Authorization', `Bearer ${validToken2}`) + .send({ avatar_url: 'http://example.com/new_avatar.jpg' }) + + expect(response.statusCode).toBe(200) + expect(response.body).toEqual({}) + }) + + it('should send correct response when requester is target user (on local server)', async () => { + clientServer.conf.capabilities.enable_set_avatar_url = true + const response = await request(app) + .put(`/_matrix/client/v3/profile/${testUserId}/avatar_url`) + .set('Authorization', `Bearer ${validToken}`) + .send({ avatar_url: 'http://example.com/new_avatar.jpg' }) + + expect(response.statusCode).toBe(200) + expect(response.body).toEqual({}) + }) + + it('should correctly update the avatar_url of an existing user', async () => { + clientServer.conf.capabilities.enable_set_avatar_url = undefined + const response = await request(app) + .put(`/_matrix/client/v3/profile/${testUserId}/avatar_url`) + .set('Authorization', `Bearer ${validToken}`) + .send({ avatar_url: 'http://example.com/new_avatar.jpg' }) + expect(response.statusCode).toBe(200) + const rows = await clientServer.matrixDb.get( + 'profiles', + ['avatar_url'], + { user_id: testUserId } + ) + + expect(rows.length).toBe(1) + expect(rows[0].avatar_url).toBe('http://example.com/new_avatar.jpg') + }) + }) + + describe('/_matrix/client/v3/profile/{userId}/displayname', () => { + it('should require authentication', async () => { + await clientServer.cronTasks?.ready + const response = await request(app) + .put(`/_matrix/client/v3/profile/${testUserId}/displayname`) + .set('Authorization', 'Bearer invalidToken') + .set('Accept', 'application/json') + expect(response.statusCode).toBe(401) + }) + + it('should return 400 if the target user is on a remote server', async () => { + const response = await request(app) + .put( + `/_matrix/client/v3/profile/@testuser:anotherexample.com/displayname` + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ displayname: 'New name' }) + expect(response.statusCode).toBe(400) + }) + + it('should return 403 if the requester is not admin and is not the target user', async () => { + const response = await request(app) + .put( + `/_matrix/client/v3/profile/@testuser2:example.com/displayname` + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ displayname: 'New name' }) + expect(response.statusCode).toBe(403) + }) + + it('should return 403 if the requester is not admin and the config does not allow changing display_name', async () => { + clientServer.conf.capabilities.enable_set_displayname = false + + const response = await request(app) + .put(`/_matrix/client/v3/profile/${testUserId}/displayname`) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ displayname: 'New name' }) + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + + clientServer.conf.capabilities.enable_set_displayname = true + }) + + it('should return 400 if provided display_name is too long', async () => { + const response = await request(app) + .put(`/_matrix/client/v3/profile/${testUserId}/displayname`) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ displayname: randomString(257) }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + + it('should send correct response when requester is admin and target user is on local server', async () => { + const response = await request(app) + .put(`/_matrix/client/v3/profile/${testUserId}/displayname`) + .set('Authorization', `Bearer ${validToken2}`) + .send({ displayname: 'New name' }) + + expect(response.statusCode).toBe(200) + expect(response.body).toEqual({}) + }) + + it('should correctly update the display_name of an existing user', async () => { + clientServer.conf.capabilities.enable_set_displayname = undefined + const response = await request(app) + .put(`/_matrix/client/v3/profile/${testUserId}/displayname`) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ displayname: 'New name' }) + expect(response.statusCode).toBe(200) + const rows = await clientServer.matrixDb.get( + 'profiles', + ['displayname'], + { user_id: testUserId } + ) + + expect(rows.length).toBe(1) + expect(rows[0].displayname).toBe('New name') + }) + }) + }) + }) + + describe('/_matrix/client/v3/user_directory/search', () => { + describe('POST', () => { + const testUserId = '@testuser:example.com' + const anotherUserId = '@anotheruser:example.com' + const publicRoomUserId = '@publicroomuser:example.com' + const sharedRoomUserId = '@sharedroomuser:example.com' + + beforeAll(async () => { + // Setup test data + try { + // Populate the user_directory table + await clientServer.matrixDb.insert('user_directory', { + user_id: testUserId, + display_name: 'Test User', + avatar_url: 'http://example.com/avatar.jpg' + }) + await clientServer.matrixDb.insert('user_directory', { + user_id: anotherUserId, + display_name: 'Another User', + avatar_url: 'http://example.com/another_avatar.jpg' + }) + await clientServer.matrixDb.insert('user_directory', { + user_id: publicRoomUserId, + display_name: 'Public Room User', + avatar_url: 'http://example.com/public_avatar.jpg' + }) + await clientServer.matrixDb.insert('user_directory', { + user_id: sharedRoomUserId, + display_name: 'Shared Room User', + avatar_url: 'http://example.com/shared_avatar.jpg' + }) + + // Populate the user_directory_search table + await clientServer.matrixDb.insert('user_directory_search', { + user_id: testUserId, + value: 'Test User http://example.com/avatar.jpg' + }) + await clientServer.matrixDb.insert('user_directory_search', { + user_id: anotherUserId, + value: 'Another user http://example.com/another_avatar.jpg' + }) + await clientServer.matrixDb.insert('user_directory_search', { + user_id: publicRoomUserId, + value: 'Public Room User http://example.com/public_avatar.jpg' + }) + await clientServer.matrixDb.insert('user_directory_search', { + user_id: sharedRoomUserId, + value: 'Shared Room User http://example.com/shared_avatar.jpg' + }) + + // Populate the users table + await clientServer.matrixDb.insert('users', { + name: anotherUserId + }) + await clientServer.matrixDb.insert('users', { + name: publicRoomUserId + }) + await clientServer.matrixDb.insert('users', { + name: sharedRoomUserId + }) + + // Populate the users_in_public_rooms table + await clientServer.matrixDb.insert('users_in_public_rooms', { + user_id: publicRoomUserId, + room_id: '!publicroom:example.com' + }) + + // Populate the users_who_share_private_rooms table + await clientServer.matrixDb.insert( + 'users_who_share_private_rooms', + { + user_id: testUserId, + other_user_id: sharedRoomUserId, + room_id: '!sharedroom:example.com' + } + ) + + // Add more users and data as needed for testing + } catch (e) { + clientServer.logger.error('Error creating user directory data:', e) + } + }) + + afterAll(async () => { + // Cleanup test data + try { + await clientServer.matrixDb.deleteEqual( + 'user_directory', + 'user_id', + testUserId + ) + await clientServer.matrixDb.deleteEqual( + 'user_directory', + 'user_id', + anotherUserId + ) + await clientServer.matrixDb.deleteEqual( + 'user_directory_search', + 'user_id', + testUserId + ) + await clientServer.matrixDb.deleteEqual( + 'user_directory_search', + 'user_id', + anotherUserId + ) + await clientServer.matrixDb.deleteEqual('users', 'name', testUserId) + await clientServer.matrixDb.deleteEqual( + 'users', + 'name', + anotherUserId + ) + await clientServer.matrixDb.deleteEqual( + 'users_in_public_rooms', + 'user_id', + publicRoomUserId + ) + await clientServer.matrixDb.deleteEqual( + 'users_who_share_private_rooms', + 'user_id', + sharedRoomUserId + ) + await clientServer.matrixDb.deleteEqual( + 'users_who_share_private_rooms', + 'other_user_id', + testUserId + ) + // Delete more users and data as needed + } catch (e) { + clientServer.logger.error('Error deleting user directory data:', e) + } + }) + + it('should require authentication', async () => { + const response = await request(app) + .post('/_matrix/client/v3/user_directory/search') + .set('Authorization', `Bearer invalidToken`) + .set('Accept', 'application/json') + .send({ + search_term: 'anotheruser', + limit: 5 + }) + + expect(response.statusCode).toBe(401) + }) + + it('should set the limit to 10 when none is provided', async () => { + const response = await request(app) + .post('/_matrix/client/v3/user_directory/search') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + search_term: 'anotheruser' + }) + + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('results') + expect(response.body.limited).toBe(false) + }) + + it('should set the searchAll parameter to false when the config does not specify it', async () => { + const response = await request(app) + .post('/_matrix/client/v3/user_directory/search') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + search_term: 'anotheruser' + }) + + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('results') + expect(response.body.results.length).toBe(0) + }) + + it('should return error 400 if invalid limit is provided', async () => { + const response = await request(app) + .post('/_matrix/client/v3/user_directory/search') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + search_term: 'anotheruser', + limit: 'invalid' + }) + + expect(response.statusCode).toBe(400) + expect(response.body.errcode).toBe('M_MISSING_PARAMS') + }) + + it('should return error 400 if invalid search term (or no search Term) is provided', async () => { + const response = await request(app) + .post('/_matrix/client/v3/user_directory/search') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + limit: 5 + }) + + expect(response.statusCode).toBe(400) + expect(response.body.errcode).toBe('M_MISSING_PARAMS') + }) + + it('should return search results for users when searchAll is enabled', async () => { + clientServer.conf.user_directory.enable_all_users_search = true + const response = await request(app) + .post('/_matrix/client/v3/user_directory/search') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + search_term: 'another user', + limit: 5 + }) + + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('results') + expect(response.body.results.length).toBeGreaterThan(0) + expect(response.body.results[0]).toHaveProperty('user_id') + expect(response.body.results[0]).toHaveProperty('display_name') + expect(response.body.results[0]).toHaveProperty('avatar_url') + + clientServer.conf.user_directory.enable_all_users_search = false + }) + + it('should return correct search results (searchAll disabled and searching public user)', async () => { + const response = await request(app) + .post('/_matrix/client/v3/user_directory/search') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + search_term: 'public', + limit: 5 + }) + + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('results') + expect(response.body.results.length).toBe(1) + expect(response.body.results[0].user_id).toBe(publicRoomUserId) + expect(response.body.results[0].display_name).toBe('Public Room User') + expect(response.body.results[0].avatar_url).toBe( + 'http://example.com/public_avatar.jpg' + ) + }) + + it('should return correct search results (searchAll disabled and searching sharing room user)', async () => { + const response = await request(app) + .post('/_matrix/client/v3/user_directory/search') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + search_term: 'shared', + limit: 5 + }) + + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('results') + expect(response.body.results.length).toBe(1) + expect(response.body.results[0].user_id).toBe(sharedRoomUserId) + expect(response.body.results[0].display_name).toBe('Shared Room User') + expect(response.body.results[0].avatar_url).toBe( + 'http://example.com/shared_avatar.jpg' + ) + }) + + it('should return no results for non-existent user', async () => { + const response = await request(app) + .post('/_matrix/client/v3/user_directory/search') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + search_term: 'nonExistentSearchTerm', + limit: 5 + }) + + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('results') + expect(response.body.results.length).toBe(0) + }) + + it('should return no results for a non-existent search term when searchAllUsers is enabled', async () => { + clientServer.conf.user_directory.enable_all_users_search = true + const response = await request(app) + .post('/_matrix/client/v3/user_directory/search') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + search_term: 'nonexistentterm', + limit: 5 + }) + + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('results') + expect(response.body.results.length).toBe(0) + clientServer.conf.user_directory.enable_all_users_search = false + }) + + it('should respect the limit parameter', async () => { + clientServer.conf.user_directory.enable_all_users_search = true + const response = await request(app) + .post('/_matrix/client/v3/user_directory/search') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + search_term: 'User', + limit: 2 + }) + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('results') + expect(response.body.results.length).toBe(2) + clientServer.conf.user_directory.enable_all_users_search = false + }) + + it('should handle search term with special characters', async () => { + const response = await request(app) + .post('/_matrix/client/v3/user_directory/search') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + search_term: '@user!#', + limit: 5 + }) + + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('results') + expect(response.body.results.length).toBe(2) + }) + + it('should perform case-insensitive search', async () => { + const response = await request(app) + .post('/_matrix/client/v3/user_directory/search') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + search_term: 'PUBLIC USER', + limit: 5 + }) + + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('results') + expect(response.body.results.length).toBe(1) + expect(response.body.results[0]).toHaveProperty( + 'user_id', + publicRoomUserId + ) + }) + }) + }) + }) +}) diff --git a/packages/matrix-client-server/src/user_data/profiles/changeProfiles.ts b/packages/matrix-client-server/src/user_data/profiles/changeProfiles.ts new file mode 100644 index 00000000..b34e6a80 --- /dev/null +++ b/packages/matrix-client-server/src/user_data/profiles/changeProfiles.ts @@ -0,0 +1,264 @@ +/* +As done in the Synapse implementation of the Matrix Protocol, +one user will have the ability to change the displayname and avatar_url of another user if they are an admin, +if they are changing their own displayname and avatar_url, +or if the servers congiguration allows it. + +Future implementations may include : + - the ability to change the displayname and avatar_url of a user while deactivating them + - the ability to change the displayname and avatar_url of a user and have it apply to the user's membership events + - the ability to change another user's profile if the requester presents one of the target's valid tokens + +After updating the profile in the profile table, the Synapse implementation also updates the user's membership events +by propagating the change to the user's membership events. +This includes the Application service which is not yet implemented in this codebase. +*/ + +import type MatrixClientServer from '../../index' +import { type Request } from 'express' +import { + errMsg, + send, + type expressAppHandler, + jsonContent, + validateParameters +} from '@twake/utils' +import { isAdmin } from '../../utils/utils' + +const MAX_DISPLAYNAME_LEN = 256 +const MAX_AVATAR_URL_LEN = 1000 + +const schema = { + avatar_url: true +} + +// eslint-disable-next-line @typescript-eslint/naming-convention +const schema_name = { + displayname: true +} + +interface changeAvatarUrlArgs { + avatar_url: string +} +interface changeDisplaynameArgs { + displayname: string +} + +export const changeAvatarUrl = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + /* + Sets the avatar_url of a user + + Arguments to take into account : + target_user: the user whose avatar_url is to be changed. + requester: The user attempting to make this change. + newAvatarUrl: The avatar_url to give this user. + byAdmin: Whether this change was made by an administrator. + + TODO : The following arguments are not used in this function, + but are used in the equivalent function in the Synapse codebase: + deactivation: Whether this change was made while deactivating the user. + propagate: Whether this change also applies to the user's membership events. + */ + const userId: string = (req as Request).params.userId + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + clientServer.authenticate(req, res, async (data) => { + const requesterUserId = data.sub + const byAdmin = await isAdmin(clientServer, requesterUserId) + + jsonContent(req, res, clientServer.logger, (obj) => { + validateParameters(res, schema, obj, clientServer.logger, (obj) => { + const newAvatarUrl = (obj as changeAvatarUrlArgs).avatar_url + const targetUserId: string = (req as Request).params.userId + + /* istanbul ignore if */ + if (!clientServer.isMine(targetUserId)) { + send( + res, + 400, + errMsg('unknown', 'Cannot change displayname of a remote user'), + clientServer.logger + ) + return + } + + if (!byAdmin && requesterUserId !== targetUserId) { + send( + res, + 403, + errMsg( + 'forbidden', + 'Cannot change displayname of another user when not admin' + ), + clientServer.logger + ) + return + } + + const allowed = + clientServer.conf.capabilities.enable_set_avatar_url ?? true + if (!byAdmin && !allowed) { + send( + res, + 403, + errMsg( + 'forbidden', + 'Cannot change avatar_url as not allowed by server' + ), + clientServer.logger + ) + return + } + + if (newAvatarUrl.length > MAX_AVATAR_URL_LEN) { + send( + res, + 400, + errMsg( + 'invalidParam', + `Avatar url too long. Max length is + ${MAX_AVATAR_URL_LEN}` + ), + clientServer.logger + ) + return + } + + clientServer.matrixDb + .updateWithConditions('profiles', { avatar_url: newAvatarUrl }, [ + { field: 'user_id', value: userId } + ]) + .then(() => { + clientServer.logger.debug('AvatarUrl updated') + send(res, 200, {}, clientServer.logger) + }) + .catch((e) => { + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', 'Error querying profiles'), + clientServer.logger + ) + }) + }) + }) + }) + } +} + +export const changeDisplayname = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + /* + Set the displayname of a user + + Arguments to take into account : + target_user: the user whose displayname is to be changed. + requester: The user attempting to make this change. + newDisplayname: The displayname to give this user. + byAdmin: Whether this change was made by an administrator. + + TODO : The following arguments are not used in this function, + but are used in the equivalent function in the Synapse codebase: + deactivation: Whether this change was made while deactivating the user. + propagate: Whether this change also applies to the user's membership events. + */ + const userId: string = (req as Request).params.userId + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + clientServer.authenticate(req, res, async (data) => { + const requesterUserId = data.sub + // Check wether requester is admin or not + const byAdmin = await isAdmin(clientServer, requesterUserId) + + jsonContent(req, res, clientServer.logger, (obj) => { + validateParameters( + res, + schema_name, + obj, + clientServer.logger, + (obj) => { + const newDisplayname = (obj as changeDisplaynameArgs).displayname + const targetUserId: string = (req as Request).params.userId + + /* istanbul ignore if */ + if (!clientServer.isMine(targetUserId)) { + send( + res, + 400, + errMsg('unknown', 'Cannot change displayname of a remote user'), + clientServer.logger + ) + return + } + + if (!byAdmin && requesterUserId !== targetUserId) { + send( + res, + 403, + errMsg( + 'forbidden', + 'Cannot change displayname of another user when not admin' + ), + clientServer.logger + ) + return + } + + const allowed = + clientServer.conf.capabilities.enable_set_displayname ?? true + if (!byAdmin && !allowed) { + send( + res, + 403, + errMsg( + 'forbidden', + 'Cannot change displayname as not allowed by server' + ), + clientServer.logger + ) + return + } + + if (newDisplayname.length > MAX_DISPLAYNAME_LEN) { + send( + res, + 400, + errMsg( + 'invalidParam', + `Displayname too long. Max length is + ${MAX_DISPLAYNAME_LEN}` + ), + clientServer.logger + ) + return + } + + clientServer.matrixDb + .updateWithConditions( + 'profiles', + { displayname: newDisplayname }, + [{ field: 'user_id', value: userId }] + ) + .then(() => { + clientServer.logger.debug('Displayname updated') + send(res, 200, {}, clientServer.logger) + }) + .catch((e) => { + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', 'Error querying profiles'), + clientServer.logger + ) + }) + } + ) + }) + }) + } +} diff --git a/packages/matrix-client-server/src/user_data/profiles/getProfiles.ts b/packages/matrix-client-server/src/user_data/profiles/getProfiles.ts new file mode 100644 index 00000000..0f292d15 --- /dev/null +++ b/packages/matrix-client-server/src/user_data/profiles/getProfiles.ts @@ -0,0 +1,207 @@ +/* +As specified in the Matrix Protocol, access to the profile information of another user is allowed on the local server, +and may be allowed on remote servers via federation. + +TODO : implement the ability to access the profile information of another user on a remote server via federation. +TODO : implement the ability to close access to the profile information of another user on the local server. +*/ + +import type MatrixClientServer from '../../index' +import { type Request } from 'express' +import { errMsg, send, type expressAppHandler } from '@twake/utils' + +export const getProfile = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + const userId: string = (req as Request).params.userId + /* istanbul ignore else */ + if ( + userId !== undefined && + typeof userId === 'string' && + userId.length > 0 + ) { + if (clientServer.isMine(userId)) { + clientServer.matrixDb + .get('profiles', ['displayname', 'avatar_url'], { + user_id: userId + }) + .then((rows) => { + if (rows.length === 0) { + send( + res, + 404, + errMsg('notFound', 'Profile not found'), + clientServer.logger + ) + } else { + send( + res, + 200, + { + avatar_url: rows[0].avatar_url, + displayname: rows[0].displayname + }, + clientServer.logger + ) + } + }) + .catch((e) => { + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', e.toString()), clientServer.logger) + }) + } else { + // TODO : Have a look on remote server via federation + send( + res, + 500, + errMsg('unknown', 'Cannot get profile of a remote user'), + clientServer.logger + ) + } + } else { + send( + res, + 400, + errMsg('missingParams', 'No user ID provided'), + clientServer.logger + ) + } + } +} + +export const getAvatarUrl = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + const userId: string = (req as Request).params.userId + /* istanbul ignore else */ + if ( + userId !== undefined && + typeof userId === 'string' && + userId.length > 0 + ) { + if (clientServer.isMine(userId)) { + clientServer.matrixDb + .get('profiles', ['avatar_url'], { + user_id: userId + }) + .then((rows) => { + if (rows.length === 0) { + send( + res, + 404, + errMsg('notFound', 'User not found'), + clientServer.logger + ) + } else { + if (rows[0].avatar_url === null) { + send( + res, + 404, + errMsg('notFound', 'Avatar not found'), + clientServer.logger + ) + } else { + send( + res, + 200, + { + avatar_url: rows[0].avatar_url + }, + clientServer.logger + ) + } + } + }) + .catch((e) => { + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', e.toString()), clientServer.logger) + }) + } else { + // TODO : Have a look on remote server via federation + send( + res, + 500, + errMsg('unknown', 'Cannot get profile of a remote user'), + clientServer.logger + ) + } + } else { + send( + res, + 400, + errMsg('missingParams', 'No user ID provided'), + clientServer.logger + ) + } + } +} + +export const getDisplayname = ( + clientServer: MatrixClientServer +): expressAppHandler => { + return (req, res) => { + const userId: string = (req as Request).params.userId + /* istanbul ignore else */ + if ( + userId !== undefined && + typeof userId === 'string' && + userId.length > 0 + ) { + if (clientServer.isMine(userId)) { + clientServer.matrixDb + .get('profiles', ['displayname'], { + user_id: userId + }) + .then((rows) => { + if (rows.length === 0) { + send( + res, + 404, + errMsg('notFound', 'User not found'), + clientServer.logger + ) + } else { + if (rows[0].displayname === null) { + send( + res, + 404, + errMsg('notFound', 'Displayname not found'), + clientServer.logger + ) + } else { + send( + res, + 200, + { + displayname: rows[0].displayname + }, + clientServer.logger + ) + } + } + }) + .catch((e) => { + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', e.toString()), clientServer.logger) + }) + } else { + // TODO : Have a look on remote server via federation + send( + res, + 500, + errMsg('unknown', 'Cannot get profile of a remote user'), + clientServer.logger + ) + } + } else { + send( + res, + 400, + errMsg('missingParams', 'No user ID provided'), + clientServer.logger + ) + } + } +} diff --git a/packages/matrix-client-server/src/user_data/user_directory/search.ts b/packages/matrix-client-server/src/user_data/user_directory/search.ts new file mode 100644 index 00000000..c301b376 --- /dev/null +++ b/packages/matrix-client-server/src/user_data/user_directory/search.ts @@ -0,0 +1,117 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +/** + * Implements https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3user_directorysearch + * + * This is not fixed in the spec and could be modified if needed : + * Performs a search for users. The homeserver may determine which subset of users are searched. + * We have decided to consider the users the requesting user shares a room with and those who reside in public rooms (known to the homeserver) + * + * The search MUST consider local users to the homeserver, and SHOULD query remote users as part of the search. + * (This problem is currently hidden by the use of specific tables in the MatrixDB database) + * + * WARNING : Following Synapse implementation, we have used many tables (user_directory, user_who_share_private_rooms, users_in_public_rooms) to implement this feature. + * These tables are used almost solely for the user_directory feature and are not used elsewhere. + * Thus for now these tables are not filled and the feature is not yet usable. + * + * TODO : implement the auto-update and track of these tables + */ + +import type MatrixClientServer from '../../index' +import { + send, + type expressAppHandler, + jsonContent, + validateParameters, + errMsg +} from '@twake/utils' + +const schema = { + limit: false, + search_term: true +} + +interface UserSearchArgs { + limit: number + search_term: string +} + +const userSearch = (clientServer: MatrixClientServer): expressAppHandler => { + return (req, res) => { + clientServer.authenticate(req, res, (data) => { + const userId = data.sub + + // ( TODO : could add a check to the capabilities to see if the user has the right to search for users) + + jsonContent(req, res, clientServer.logger, (obj) => { + validateParameters( + res, + schema, + obj, + clientServer.logger, + (validatedObj) => { + const searchArgs = validatedObj as UserSearchArgs + // We retrieve the limit from the request, if not present we set it to 10 + const limit = searchArgs.limit === undefined ? 10 : searchArgs.limit + + // We retrieve the search term from the request if not present we return an error + const searchTerm = searchArgs.search_term + if ( + searchTerm === undefined || + searchTerm === '' || + searchTerm === null || + typeof limit !== 'number' + ) { + send( + res, + 400, + errMsg('missingParams', 'Missing search term'), + clientServer.logger + ) + return + } + + const searchAllUsers = + clientServer.conf.user_directory.enable_all_users_search ?? false + + clientServer.matrixDb + .searchUserDirectory(userId, searchTerm, limit, searchAllUsers) + .then((rows) => { + const _limited = rows.length > limit + if (_limited) { + rows = rows.slice(0, limit) + } + + const _results = rows.map((row) => { + return { + avatar_url: row.avatar_url, + display_name: row.display_name, + user_id: row.user_id + } + }) + + send( + res, + 200, + { results: _results, limited: _limited }, + clientServer.logger + ) + }) + .catch((err) => { + /* istanbul ignore next */ + clientServer.logger.error('Error when searching for users') + /* istanbul ignore next */ + send( + res, + 500, + errMsg('unknown', err.toString()), + clientServer.logger + ) + }) + } + ) + }) + }) + } +} + +export default userSearch diff --git a/packages/matrix-client-server/src/utils/UIAuth.md b/packages/matrix-client-server/src/utils/UIAuth.md new file mode 100644 index 00000000..177e4f9e --- /dev/null +++ b/packages/matrix-client-server/src/utils/UIAuth.md @@ -0,0 +1,34 @@ +# User Interactive Authentication + +User Interactive Authentication is based on the Matrix.org Client-Server specification: [User Interactive Authentication API](https://spec.matrix.org/v1.11/client-server-api/#user-interactive-authentication-api). + +## Usage Instructions + +To use this method in functions that require user interactive authentication, follow these steps: + +1. Use the `uiauthenticate` method similarly to the `authenticate` method for `/register` +2. For other endpoints that use UI-Authentication and that are authenticated (such as `/add` for example), you first need to call the `clientServer.authenticate` method, followed by `validateUserWithUiAuthentication`. The second method checks that the user associated to the given access token is indeed who he claims to be, it serves as additional security. +3. Since we insert the request body in the `clientdict` column of the `ui_auth_sessions` table, we need to verify its content. For that we check type validity and that the strings are not too long (don't exceed 512 characters) before calling the `uiauthenticate` or the `validateUserWithUIAuth` methods. + +## Allowed Flows + +For endpoints other than `/register`, the allowed flows are generated automatically inside the `validateUserWithUiAuthentication` method. If you call `uiauthenticate` (cf account/password/index.ts) then you need to specifiy the allowed flows as argument to the function. + For the `/register` endpoint, they are generated using the config with a function defined in utils/userInteractiveAuthentication. + +## Callback Usage + +For non-`/register` endpoints, the `uiauthenticate` method calls the callback method with the `userId` as the second argument. This allows access to the `userId` in endpoints requiring UIAuth. + +## Auth key + +As things are now, an endpoint that uses UI Authentication requires the client to send the data in the call that will validate authentication. For example, in account/password/index.ts, it is expected that the client sends a password on the call to the endpoint that corresponds to the last stage of the flow it has chosen. Meaning if the client sends a password in a previous request, it should resend it in further requests to ensure the server receives the data, and shouldn't assume sending it only once suffices. This behaviour might be undesirable and could be changed by storing relevant data and then deleting them when we're done with them (since we already store `clientdict` in the `ui_auth_sessions` table we could exploit this for example). + +## Testing + +- If the `userId` is required, ensure the relevant data is in the database to recognize the user. For example, for "m.login.email.identity", populate the "user_threepids" table with the necessary data (client_secret, session_id, and address). + +### Session IDs + +## In order to get a valid session ID for tests, you first need to call your endpoint without an `auth`field, and get the session ID from the response body. This session ID will then be used in all other calls to THE SAME endpoint while authentication has not been completed. Once completed, you need to generate a new session ID for future API calls using the same procedure. If the intended behaviour is that once a session is validated, you can use it for all following calls, then you need to change the insert call in the table `ui_auth_sessions_credentials` to a new function that executes the query `INSERT ... ON CONFLICT ... DO NOTHING`. + +If you have any questions or need further assistance, refer to the [Matrix.org Client-Server API specification](https://spec.matrix.org/v1.11/client-server-api/#user-interactive-authentication-api). diff --git a/packages/matrix-client-server/src/utils/authenticate.ts b/packages/matrix-client-server/src/utils/authenticate.ts new file mode 100644 index 00000000..b4edef4a --- /dev/null +++ b/packages/matrix-client-server/src/utils/authenticate.ts @@ -0,0 +1,126 @@ +/* eslint-disable @typescript-eslint/strict-boolean-expressions */ +import { type TwakeLogger } from '@twake/logger' +import { type Request, type Response } from 'express' +import type http from 'http' +import type MatrixDBmodified from '../matrixDb' +import { epoch, errMsg, getAccessToken, send, toMatrixId } from '@twake/utils' +import { type AppServiceRegistration, type Config } from '../types' + +export interface TokenContent { + sub: string + device_id?: string + epoch: number +} + +export type AuthenticationFunction = ( + req: Request | http.IncomingMessage, + res: Response | http.ServerResponse, + callback: (data: TokenContent, id: string | null) => void +) => void + +// TODO : Check for guest access. As is, there is nothing that prevents a guest from accessing the endpoints he is not supposed to access +// Since register assigns him an access token. Maybe it should assign him a guest token that is differentiated in authenticate +// To only allow him access to the endpoints he is supposed to access +// Check this for more information : https://spec.matrix.org/v1.11/client-server-api/#guest-access +const Authenticate = ( + matrixDb: MatrixDBmodified, + logger: TwakeLogger, + conf: Config +): AuthenticationFunction => { + return (req, res, callback) => { + const token = getAccessToken(req) + if (token != null) { + let data: TokenContent + matrixDb + .get( + 'access_tokens', + ['user_id, device_id', 'refresh_token_id', 'used'], + { + token + } + ) + .then((rows) => { + if (rows.length === 0) { + const applicationServices = conf.application_services + const asTokens: string[] = applicationServices.map( + (as: AppServiceRegistration) => as.as_token + ) + if (asTokens.includes(token)) { + // Check if the request is made by an application-service + const appService = applicationServices.find( + (as: AppServiceRegistration) => as.as_token === token + ) + // @ts-expect-error req.query exists + const userId = req.query.user_id + ? // @ts-expect-error req.query exists + req.query.user_id + : // @ts-expect-error appService exists since we found a matching token + toMatrixId(appService.sender_localpart, conf.server_name) + if ( + appService?.namespaces.users && + !appService?.namespaces.users.some((namespace) => + new RegExp(namespace.regex).test(userId) + ) // check if the userId is registered by the appservice + ) { + send( + res, + 403, + errMsg( + 'forbidden', + 'The appservice cannot masquerade as the user or has not registered them.' + ), + logger + ) + return + } + // Should we check if the userId is already registered in the database? + data = { sub: userId, epoch: epoch() } + callback(data, token) + } else { + throw new Error() + } + } else { + if (rows[0].used === 1) { + logger.error('Access tried with an invalid token', req.headers) + send( + res, + 401, + errMsg('invalidToken', 'Access token has been refreshed') + ) + return + } + data = { sub: rows[0].user_id as string, epoch: epoch() } + data.sub = rows[0].user_id as string + if (rows[0].device_id) { + data.device_id = rows[0].device_id as string + } + matrixDb + .deleteWhere('refresh_tokens', { + // Invalidate the old refresh token and access token (condition ON DELETE CASCADE) once the new access token is used + field: 'next_token_id', + value: rows[0].refresh_token_id as number, + operator: '=' + }) + .then(() => { + callback(data, token) + }) + .catch((e) => { + // istanbul ignore next + logger.error('Error deleting the old refresh token', e) + // istanbul ignore next + send(res, 500, errMsg('unknown', e.toString())) + }) + } + }) + .catch((e) => { + logger.warn('Access tried with an unkown token', req.headers) + send(res, 401, errMsg('unknownToken'), logger) + }) + } else { + logger.warn('Access tried without token', req.headers) + send(res, 401, errMsg('missingToken'), logger) + } + } +} + +export default Authenticate diff --git a/packages/matrix-client-server/src/utils/event.test.ts b/packages/matrix-client-server/src/utils/event.test.ts new file mode 100644 index 00000000..97f451ed --- /dev/null +++ b/packages/matrix-client-server/src/utils/event.test.ts @@ -0,0 +1,236 @@ +import { type ClientEvent } from '../types' +import { SafeClientEvent } from './event' +import { type TwakeLogger } from '@twake/logger' + +describe('Test suites for event.ts', () => { + let mockLogger: TwakeLogger + beforeEach(() => { + mockLogger = { + error: jest.fn(), + warn: jest.fn(), + log: jest.fn(), + info: jest.fn() + } as unknown as TwakeLogger + }) + describe('constructor', () => { + it('should create a redactedEvent if the event is correct', () => { + const clientEvent: ClientEvent = { + content: { + alias: 'alias', + alt_aliases: ['alt_aliases'] + }, + event_id: 'event_id', + origin_server_ts: 123456, + room_id: '!726s6s6q:example.com', + sender: '@alice:example.com', + state_key: '', + type: 'm.room.canonical_alias' + } + const redactedEvent = new SafeClientEvent(clientEvent) + expect(redactedEvent).toBeDefined() + }) + it('should throw an error if the eventID is incorrect', () => { + const clientEvent: ClientEvent = { + content: { + alias: 'alias', + alt_aliases: ['alt_aliases'] + }, + // @ts-expect-error : invalid event_id for test + event_id: 123456, + origin_server_ts: 123456, + room_id: '!726s6s6q:example.com', + sender: '@alice:example.com', + state_key: '', + type: 'm.room.canonical_alias' + } + expect(() => new SafeClientEvent(clientEvent)).toThrow('Invalid event_id') + }) + it('should throw an error if the type is incorrect', () => { + const clientEvent: ClientEvent = { + content: { + alias: 'alias', + alt_aliases: ['alt_aliases'] + }, + event_id: 'event_id', + origin_server_ts: 123456, + room_id: '!726s6s6q:example.com', + sender: '@alice:example.com', + state_key: '', + type: 'invalid_type' + } + expect(() => new SafeClientEvent(clientEvent)).toThrow('Invalid type') + }) + it('should throw an error if the roomID is incorrect', () => { + const clientEvent: ClientEvent = { + content: { + alias: 'alias', + alt_aliases: ['alt_aliases'] + }, + event_id: 'event_id', + origin_server_ts: 123456, + room_id: 'invalid_room_id', + sender: '@alice:example.com', + state_key: '', + type: 'm.room.canonical_alias' + } + expect(() => new SafeClientEvent(clientEvent)).toThrow('Invalid room_id') + }) + it('should throw an error if the sender is incorrect', () => { + const clientEvent: ClientEvent = { + content: { + alias: 'alias', + alt_aliases: ['alt_aliases'] + }, + event_id: 'event_id', + origin_server_ts: 123456, + room_id: '!726s6s6q:example.com', + // @ts-expect-error : invalid sender for test + sender: 123456, + state_key: '', + type: 'm.room.canonical_alias' + } + expect(() => new SafeClientEvent(clientEvent)).toThrow('Invalid sender') + }) + it('should throw an error if the content is incorrect', () => { + const clientEvent: ClientEvent = { + content: ['content'], + event_id: 'event_id', + origin_server_ts: 123456, + room_id: '!726s6s6q:example.com', + sender: '@alice:example.com', + state_key: '', + type: 'm.room.canonical_alias' + } + expect(() => new SafeClientEvent(clientEvent)).toThrow('Invalid content') + }) + it('should throw an error if the originServerTs is incorrect', () => { + const clientEvent: ClientEvent = { + content: { + alias: 'alias', + alt_aliases: ['alt_aliases'] + }, + event_id: 'event_id', + // @ts-expect-error : invalid origin_server_ts for test + origin_server_ts: '123456', + room_id: '!726s6s6q:example.com', + sender: '@alice:example.com', + state_key: '', + type: 'm.room.canonical_alias' + } + expect(() => new SafeClientEvent(clientEvent)).toThrow( + 'Invalid origin_server_ts' + ) + }) + }) + describe('redact', () => { + const clientEvent: ClientEvent = { + content: { + alias: 'alias', + alt_aliases: ['alt_aliases'] + }, + event_id: 'event_id', + origin_server_ts: 123456, + room_id: '!726s6s6q:example.com', + sender: '@alice:example.com', + state_key: '', + type: 'm.room.canonical_alias', + // @ts-expect-error : invalid keys for test + invalid_key: 'invalid_key', + another_invalid_key: 'another_invalid_key' + } + it('should log an info message if the event has already been redacted', () => { + const redactedEvent = new SafeClientEvent(clientEvent) + redactedEvent.redact() + redactedEvent.redact(mockLogger) + expect(mockLogger.info).toHaveBeenCalledWith('Event is already redacted') + }) + it('should return a redacted event', () => { + const redactedEvent = new SafeClientEvent(clientEvent) + redactedEvent.redact() + expect(redactedEvent).toBeDefined() + expect(redactedEvent.hasBeenRedacted()).toEqual(true) + }) + it('should remove all the keys that are not allowed', () => { + const redactedEvent = new SafeClientEvent(clientEvent) + redactedEvent.redact() + const redactedEventKeys = Object.keys(redactedEvent.getEvent()) + const expectedKeys = [ + 'content', + 'event_id', + 'origin_server_ts', + 'room_id', + 'sender', + 'state_key', + 'type' + ] + expectedKeys.forEach((key) => { + expect(redactedEventKeys).toContain(key) + }) + expect(redactedEventKeys.length).toBe(expectedKeys.length) + }) + it('should remove all the content keys that are not allowed', () => { + const redactedEvent = new SafeClientEvent(clientEvent) + redactedEvent.redact() + const redactedEventContentKeys = Object.keys( + redactedEvent.getEvent().content + ) + expect(redactedEventContentKeys).toHaveLength(0) + }) + it('should not remove the allowed content keys', () => { + const clientEvent: ClientEvent = { + content: { + ban: 50, + events: 50, + events_default: 50, + invite: 50 + }, + event_id: 'event_id', + origin_server_ts: 123456, + room_id: '!726s6s6q:example.com', + sender: '@alice:example.com', + state_key: '', + type: 'm.room.power_levels', + // @ts-expect-error : invalid keys for test + invalid_key: 'invalid_key', + another_invalid_key: 'another_invalid_key' + } + const redactedEvent = new SafeClientEvent(clientEvent) + redactedEvent.redact() + const redactedEventContentKeys = Object.keys( + redactedEvent.getEvent().content + ) + const expectedKeys = ['ban', 'events', 'events_default', 'invite'] + expectedKeys.forEach((key) => { + expect(redactedEventContentKeys).toContain(key) + }) + expect(redactedEventContentKeys.length).toBe(expectedKeys.length) + }) + it('should not remove any content key for a m.room.create event', () => { + const clientEvent: ClientEvent = { + content: { + creator: '@alice:example.com', + random_key: 'random' + }, + event_id: 'event_id', + origin_server_ts: 123456, + room_id: '!726s6s6q:example.com', + sender: '@alice:example.com', + state_key: '', + type: 'm.room.create', + // @ts-expect-error : invalid keys for test + invalid_key: 'invalid_key', + another_invalid_key: 'another_invalid_key' + } + const redactedEvent = new SafeClientEvent(clientEvent) + redactedEvent.redact() + const redactedEventContentKeys = Object.keys( + redactedEvent.getEvent().content + ) + const expectedKeys = ['creator', 'random_key'] + expectedKeys.forEach((key) => { + expect(redactedEventContentKeys).toContain(key) + }) + expect(redactedEventContentKeys.length).toBe(expectedKeys.length) + }) + }) +}) diff --git a/packages/matrix-client-server/src/utils/event.ts b/packages/matrix-client-server/src/utils/event.ts new file mode 100644 index 00000000..2dc098e0 --- /dev/null +++ b/packages/matrix-client-server/src/utils/event.ts @@ -0,0 +1,154 @@ +import { type TwakeLogger } from '@twake/logger' +import { type ClientEvent } from '../types' +import { isEventTypeValid, isMatrixIdValid, isRoomIdValid } from '@twake/utils' + +export class SafeClientEvent { + private event: ClientEvent + private isRedacted: boolean + + constructor(event: ClientEvent, logger?: TwakeLogger) { + // Validate and assign properties to ensure data integrity + this.event = this.validateAndCreateEvent(event, logger) + this.isRedacted = false + } + + protected validateAndCreateEvent( + event: ClientEvent, + logger?: TwakeLogger + ): ClientEvent { + if (event.event_id == null || typeof event.event_id !== 'string') { + logger?.error('Invalid event_id') + throw new Error('Invalid event_id') + } + if ( + event.type == null || + typeof event.type !== 'string' || + !isEventTypeValid(event.type) + ) { + logger?.error('Invalid type') + throw new Error('Invalid type') + } + if ( + event.room_id == null || + typeof event.room_id !== 'string' || + !isRoomIdValid(event.room_id) + ) { + logger?.error('Invalid room_id') + throw new Error('Invalid room_id') + } + if ( + event.sender == null || + typeof event.sender !== 'string' || + !isMatrixIdValid(event.sender) + ) { + logger?.error('Invalid sender') + throw new Error('Invalid sender') + } + if ( + event.content == null || + typeof event.content !== 'object' || + Array.isArray(event.content) || + Object.keys(event.content).some((key) => typeof key !== 'string') + ) { + logger?.error('Invalid content') + throw new Error('Invalid content') + } + if ( + event.origin_server_ts == null || + typeof event.origin_server_ts !== 'number' + ) { + logger?.error('Invalid origin_server_ts') + throw new Error('Invalid origin_server_ts') + } + + return { + event_id: event.event_id, + type: event.type, + room_id: event.room_id, + sender: event.sender, + origin_server_ts: event.origin_server_ts, + content: event.content, + state_key: event.state_key, + unsigned: event.unsigned + } + } + + public redact(logger?: TwakeLogger): void { + if (this.isRedacted) { + logger?.info('Event is already redacted') + return + } + this.isRedacted = true + const allowedKeys = new Set([ + 'event_id', + 'type', + 'room_id', + 'sender', + 'state_key', + 'content', + 'hashes', + 'signatures', + 'depth', + 'prev_events', + 'auth_events', + 'origin_server_ts' + ]) + const allowedContentKeys: Record> = { + 'm.room.member': new Set([ + 'membership', + 'join_authorised_via_users_server', + 'third_party_invite' + ]), + 'm.room.join_rules': new Set(['join_rule', 'allow']), + 'm.room.power_levels': new Set([ + 'ban', + 'events', + 'events_default', + 'invite', + 'kick', + 'redact', + 'state_default', + 'users', + 'users_default' + ]), + 'm.room.history_visibility': new Set(['history_visibility']), + 'm.room.redaction': new Set(['redacts']) + } + + const filterObject = (obj: T, allowedKeys: Set): Partial => { + const result: Partial = {} + for (const key in obj) { + if (allowedKeys.has(key as string)) { + result[key as keyof T] = obj[key] + } else { + logger?.warn(`Redacted key: ${key}`) + } + } + return result + } + + // Create a filtered copy of the event + this.event = filterObject(this.event, allowedKeys) as ClientEvent + + // Handle content-specific redactions + const eventType = this.event.type + if (eventType.length > 0 && this.event.content != null) { + const allowedContent = allowedContentKeys[eventType] + if (allowedContent != null) { + this.event.content = filterObject(this.event.content, allowedContent) + } else if (eventType !== 'm.room.create') { + // Redact all content for events other than m.room.create and specified allowed content keys + logger?.warn(`Redacted content for event type: ${eventType}`) + this.event.content = {} + } + } + } + + public hasBeenRedacted(): boolean { + return this.isRedacted + } + + public getEvent(): ClientEvent { + return this.event + } +} diff --git a/packages/matrix-client-server/src/utils/filter.test.ts b/packages/matrix-client-server/src/utils/filter.test.ts new file mode 100644 index 00000000..488faf50 --- /dev/null +++ b/packages/matrix-client-server/src/utils/filter.test.ts @@ -0,0 +1,1165 @@ +import { + EventFilter, + Filter, + _matchesWildcard, + MAX_LIMIT, + RoomEventFilter, + RoomFilter +} from './filter' +import { type TwakeLogger } from '@twake/logger' + +describe('Test suites for filter.ts', () => { + let mockLogger: TwakeLogger + beforeEach(() => { + mockLogger = { + error: jest.fn(), + warn: jest.fn(), + log: jest.fn() + } as unknown as TwakeLogger + }) + + describe('Filter', () => { + it('should create a filter with the correct parameters', () => { + // Using example for the spec + const filter = new Filter({ + event_fields: ['type', 'content', 'sender'], + event_format: 'client', + presence: { + not_senders: ['@alice:example.com'], + types: ['m.presence'] + }, + room: { + ephemeral: { + not_rooms: ['!726s6s6q:example.com'], + not_senders: ['@spam:example.com'], + types: ['m.receipt', 'm.typing'] + }, + state: { + not_rooms: ['!726s6s6q:example.com'], + types: ['m.room.*'] + }, + timeline: { + limit: 10, + not_rooms: ['!726s6s6q:example.com'], + not_senders: ['@spam:example.com'], + types: ['m.room.message'] + } + } + }) + expect(filter.event_fields).toEqual(['type', 'content', 'sender']) + expect(filter.event_format).toEqual('client') + expect(filter.presence).toEqual({ + limit: 10, + senders: null, + not_senders: ['@alice:example.com'], + types: ['m.presence'], + not_types: [] + }) + expect(filter.room?.ephemeral?.not_rooms).toEqual([ + '!726s6s6q:example.com' + ]) + expect(filter.room?.state?.types).toEqual(['m.room.*']) + }) + + it('should not create empty filters when not needed', () => { + const filter = new Filter({ + event_fields: ['type', 'content', 'sender'], + event_format: 'client' + }) + expect(filter.account_data).toBeUndefined() + expect(filter.presence).toBeUndefined() + expect(filter.room).toBeUndefined() + }) + + it('it should log a warning when creating a filter with an invalid event format', () => { + const filter = new Filter( + { + event_format: 'invalid' + }, + mockLogger + ) + expect(mockLogger.warn).toHaveBeenCalled() + expect(filter.event_format).toBe('client') + }) + + it('it should log a warning when creating a filter with an invalid event field', () => { + const filter = new Filter( + { + event_fields: ['invalid', 'm.room.wrongField'] + }, + mockLogger + ) + expect(mockLogger.warn).toHaveBeenCalled() + expect(filter.event_fields).toEqual([]) + }) + + describe('check', () => { + describe('inner account_data filter', () => { + it('should return true when the account_data filter matches the event', () => { + const filter = new Filter({ + account_data: { + types: ['m.push_rules'] + } + }) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + // insert tag content here + }, + origin_server_ts: 0, + type: 'm.push_rules', + sender: '@alice:example.com' + }) + ).toBe(true) + }) + it('should return false when the account_data filter does not match the event', () => { + const filter = new Filter({ + account_data: { + not_types: ['m.push_rules'] + } + }) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + // insert tag content here + }, + origin_server_ts: 0, + type: 'm.push_rules', + sender: '@alice:example.com' + }) + ).toBe(false) + }) + }) + describe('inner presence filter', () => { + it('should return true when the presence filter matches the event', () => { + const filter = new Filter({ + presence: { + types: ['m.presence'], + not_senders: ['@alice:example.com'] + } + }) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + // insert presence content here + }, + origin_server_ts: 0, + type: 'm.presence', + sender: '@bob:example.com' + }) + ).toBe(true) + }) + it('should return false when the presence filter does not match the event', () => { + const filter = new Filter({ + presence: { + types: ['m.presence'], + not_senders: ['@alice:example.com'] + } + }) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + // insert presence content here + }, + origin_server_ts: 0, + type: 'm.presence', + sender: '@alice:example.com' + }) + ).toBe(false) + }) + }) + describe('inner room filter', () => { + it('should return true when the room filter matches the event', () => { + const filter = new Filter({ + room: { + rooms: ['!726s6s6q:example.com'], + timeline: { + types: ['m.room.message'], + not_senders: ['@alice:example.com'] + } + } + }) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + // insert message content here + }, + origin_server_ts: 0, + type: 'm.room.message', + sender: '@bob:example.com' + }) + ).toBe(true) + }) + it('should return false when the room filter does not match the event', () => { + const filter = new Filter({ + room: { + rooms: ['!726s6s6q:example.com'], + timeline: { + types: ['m.room.message'], + not_senders: ['@alice:example.com'] + } + } + }) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + // insert message content here + }, + origin_server_ts: 0, + type: 'm.room.message', + sender: '@alice:example.com' + }) + ).toBe(false) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + // insert message content here + }, + origin_server_ts: 0, + type: 'm.room.redaction', + sender: '@bob:example.com' + }) + ).toBe(false) + }) + }) + }) + }) + + describe('EventFilter', () => { + it('should create a filter with the correct parameters', () => { + const filter = new EventFilter({ + limit: 10, + types: ['m.room.message'], + not_types: ['m.room.member'], + senders: ['@alice:example.com'], + not_senders: ['@bob:example.com'] + }) + expect(filter.limit).toBe(10) + expect(filter.types).toEqual(['m.room.message']) + expect(filter.not_types).toEqual(['m.room.member']) + expect(filter.senders).toEqual(['@alice:example.com']) + expect(filter.not_senders).toEqual(['@bob:example.com']) + }) + + it('should set the default limit at 10', () => { + const filter = new EventFilter({}) + expect(filter.limit).toBe(10) + }) + + it('should log a warning when creating a filter with a limit above the maximum limit', () => { + const filter = new EventFilter({ limit: 100 }, mockLogger) + expect(mockLogger.warn).toHaveBeenCalled() + expect(filter.limit).toBe(MAX_LIMIT) + }) + + it('should log a warning when creating a filter with a limit below 1', () => { + const filter = new EventFilter({ limit: 0 }, mockLogger) + expect(mockLogger.warn).toHaveBeenCalled() + expect(filter.limit).toBe(1) + }) + + it('should log a warning when creating a filter with an invalid type or not_type', () => { + const filter = new EventFilter( + { + types: ['m.room.message', 'invalid'] + }, + mockLogger + ) + expect(mockLogger.warn).toHaveBeenCalled() + expect(filter.types).toEqual(['m.room.message']) + + const filter2 = new EventFilter( + { + not_types: ['m.room.message', 'invalid'] + }, + mockLogger + ) + expect(mockLogger.warn).toHaveBeenCalled() + expect(filter2.not_types).toEqual(['m.room.message']) + }) + + it('should log a warning when creating a filter with an invalid sender or not_sender', () => { + const filter = new EventFilter( + { + senders: ['@alice:example.com', 'invalid'] + }, + mockLogger + ) + expect(mockLogger.warn).toHaveBeenCalled() + expect(filter.senders).toEqual(['@alice:example.com']) + + const filter2 = new EventFilter( + { + not_senders: ['@alice:example.com', 'invalid'] + }, + mockLogger + ) + expect(mockLogger.warn).toHaveBeenCalled() + expect(filter2.not_senders).toEqual(['@alice:example.com']) + }) + + describe('filtersAllTypes', () => { + it('should return true when the filter filters all types', () => { + const filter = new EventFilter({ + types: [] + }) + expect(filter.filtersAllTypes()).toBe(true) + const filter2 = new EventFilter({ + not_types: ['*', 'm.room.member'] + }) + expect(filter2.filtersAllTypes()).toBe(true) + }) + + it('should return false when the filter does not filter all types', () => { + const filter = new EventFilter({ + types: ['m.room.message'] + }) + expect(filter.filtersAllTypes()).toBe(false) + const filter2 = new EventFilter({ + types: ['m.room.*'] + }) + expect(filter2.filtersAllTypes()).toBe(false) + }) + }) + + describe('filtersAllSenders', () => { + it('should return true when the filter filters all senders', () => { + const filter = new EventFilter({ + senders: [] + }) + expect(filter.filtersAllSenders()).toBe(true) + }) + + it('should return false when the filter does not filter all senders', () => { + const filter = new EventFilter({ + senders: ['@alice:example.com', '@bob:example.com'] + }) + expect(filter.filtersAllSenders()).toBe(false) + }) + }) + + describe('get', () => { + const filter = new EventFilter({ + senders: ['@alice:example.com', '@bob:example.com'], + types: ['m.room.message', 'm.room.member'] + }) + it('should return the senders array', () => { + // @ts-expect-error - Testing private method + expect(filter.get('senders')).toEqual([ + '@alice:example.com', + '@bob:example.com' + ]) + }) + it('should return the types array', () => { + // @ts-expect-error - Testing private method + expect(filter.get('types')).toEqual(['m.room.message', 'm.room.member']) + }) + it('should throw an error when the key is not found', () => { + // @ts-expect-error - Testing private method + expect(() => filter.get('not_senders')).toThrow( + 'Wrong element in get function of EventFilter' + ) + }) + it('should return null when the field is not set', () => { + const filter = new EventFilter({}) + // @ts-expect-error - Testing private method + expect(filter.get('senders')).toBeNull() + // @ts-expect-error - Testing private method + expect(filter.get('types')).toBeNull() + }) + }) + + describe('getNot', () => { + const filter = new EventFilter({ + not_senders: ['@alice:example.com', '@bob:example.com'], + not_types: ['m.room.message', 'm.room.member'] + }) + it('should return the not_senders array', () => { + // @ts-expect-error - Testing private method + expect(filter.getNot('senders')).toEqual([ + '@alice:example.com', + '@bob:example.com' + ]) + }) + it('should return the not_types array', () => { + // @ts-expect-error - Testing private method + expect(filter.getNot('types')).toEqual([ + 'm.room.message', + 'm.room.member' + ]) + }) + it('should throw an error when the key is not found', () => { + // @ts-expect-error - Testing private method + expect(() => filter.getNot('not_senders')).toThrow( + 'Wrong element in getNot function of EventFilter' + ) + }) + it('should return an empty array when the field is not set', () => { + const filter = new EventFilter({}) + // @ts-expect-error - Testing private method + expect(filter.getNot('senders')).toEqual([]) + // @ts-expect-error - Testing private method + expect(filter.getNot('types')).toEqual([]) + }) + }) + + describe('check', () => { + const filter = new EventFilter({ + types: ['m.room.message', 'm.call.*'], + senders: ['@alice:example.com'] + }) + it('should return true when the filter matches the event', () => { + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + body: 'Hello, world!' + }, + origin_server_ts: 0, + type: 'm.room.message', + sender: '@alice:example.com' + }) + ).toBe(true) + }) + it('should return true when the filter matches the event with a wildcard', () => { + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + call_id: '1234' + }, + origin_server_ts: 0, + type: 'm.call.invite', + sender: '@alice:example.com' + }) + ).toBe(true) + }) + it('should return false when the filter does not match the event', () => { + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + membership: 'join' + }, + origin_server_ts: 0, + type: 'm.room.member', + sender: '@alice:example.com' + }) + ).toBe(false) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + body: 'Hello, world!' + }, + origin_server_ts: 0, + type: 'm.room.message', + sender: '@bob:example.com' + }) + ).toBe(false) + }) + }) + }) + + describe('RoomEventFilter', () => { + it('should create a filter with the correct parameters', () => { + const filter = new RoomEventFilter({ + limit: 10, + types: ['m.room.message'], + not_types: ['m.room.member'], + senders: ['@alice:example.com'], + not_senders: ['@bob:example.com'], + not_rooms: ['!726s6s6q:example.com'], + include_redundant_members: true, + lazy_load_members: true + }) + expect(filter.limit).toBe(10) + expect(filter.types).toEqual(['m.room.message']) + expect(filter.not_types).toEqual(['m.room.member']) + expect(filter.senders).toEqual(['@alice:example.com']) + expect(filter.not_senders).toEqual(['@bob:example.com']) + expect(filter.not_rooms).toEqual(['!726s6s6q:example.com']) + expect(filter.include_redundant_members).toBe(true) + expect(filter.lazy_load_members).toBe(true) + }) + + it('should set include_redundant_members, lazy_load_members and unread_thread_notifications to false by default', () => { + const filter = new RoomEventFilter({}) + expect(filter.include_redundant_members).toBe(false) + expect(filter.lazy_load_members).toBe(false) + expect(filter.unread_thread_notifications).toBe(false) + }) + + it('should set contains_url to undefined by default', () => { + const filter = new RoomEventFilter({}) + expect(filter.contains_url).toBeUndefined() + }) + + it('should log a warning when creating a filter with an invalid room or not_room', () => { + const filter = new RoomEventFilter( + { + rooms: ['726s6s6q:example.com'] + }, + mockLogger + ) + expect(mockLogger.warn).toHaveBeenCalled() + expect(filter.rooms).toEqual([]) + const filter2 = new RoomEventFilter( + { + not_rooms: ['726s6s6q:example.com'] + }, + mockLogger + ) + expect(mockLogger.warn).toHaveBeenCalled() + expect(filter2.not_rooms).toEqual([]) + }) + + describe('get', () => { + const filter = new RoomEventFilter({ + senders: ['@alice:example.com', '@bob:example.com'], + types: ['m.room.message', 'm.room.member'], + rooms: ['!726s6s6q:example.com'] + }) + it('should return the senders array', () => { + // @ts-expect-error - Testing private method + expect(filter.get('senders')).toEqual([ + '@alice:example.com', + '@bob:example.com' + ]) + }) + it('should return the types array', () => { + // @ts-expect-error - Testing private method + expect(filter.get('types')).toEqual(['m.room.message', 'm.room.member']) + }) + it('should return the rooms array', () => { + // @ts-expect-error - Testing private method + expect(filter.get('rooms')).toEqual(['!726s6s6q:example.com']) + }) + it('should throw an error when the key is not found', () => { + // @ts-expect-error - Testing private method + expect(() => filter.get('not_senders')).toThrow( + 'Wrong element in get function of RoomEventFilter' + ) + }) + it('should return null when the field is not set', () => { + const filter = new RoomEventFilter({}) + // @ts-expect-error - Testing private method + expect(filter.get('senders')).toBeNull() + // @ts-expect-error - Testing private method + expect(filter.get('types')).toBeNull() + // @ts-expect-error - Testing private method + expect(filter.get('rooms')).toBeNull() + }) + }) + + describe('getNot', () => { + const filter = new RoomEventFilter({ + not_senders: ['@alice:example.com', '@bob:example.com'], + not_types: ['m.room.message', 'm.room.member'], + not_rooms: ['!726s6s6q:example.com'] + }) + it('should return the not_senders array', () => { + // @ts-expect-error - Testing private method + expect(filter.getNot('senders')).toEqual([ + '@alice:example.com', + '@bob:example.com' + ]) + }) + it('should return the not_types array', () => { + // @ts-expect-error - Testing private method + expect(filter.getNot('types')).toEqual([ + 'm.room.message', + 'm.room.member' + ]) + }) + it('should return the not_rooms array', () => { + // @ts-expect-error - Testing private method + expect(filter.getNot('rooms')).toEqual(['!726s6s6q:example.com']) + }) + it('should throw an error when the key is not found', () => { + // @ts-expect-error - Testing private method + expect(() => filter.getNot('not_senders')).toThrow( + 'Wrong element in getNot function of RoomEventFilter' + ) + }) + it('should return an empty array when the field is not set', () => { + const filter = new RoomEventFilter({}) + // @ts-expect-error - Testing private method + expect(filter.getNot('senders')).toEqual([]) + // @ts-expect-error - Testing private method + expect(filter.getNot('types')).toEqual([]) + // @ts-expect-error - Testing private method + expect(filter.getNot('rooms')).toEqual([]) + }) + }) + + describe('check', () => { + it('should return true when the filter matches the event', () => { + const filter = new RoomEventFilter({ + types: ['m.room.message', 'm.call.*'], + senders: ['@alice:example.com'], + rooms: ['!726s6s6q:example.com'] + }) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + body: 'Hello, world!' + }, + origin_server_ts: 0, + type: 'm.room.message', + sender: '@alice:example.com' + }) + ).toBe(true) + }) + it('should return true when the filter matches the event with a wildcard', () => { + const filter = new RoomEventFilter({ + types: ['m.room.message', 'm.call.*'], + senders: ['@alice:example.com'], + rooms: ['!726s6s6q:example.com'] + }) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + call_id: '1234' + }, + origin_server_ts: 0, + type: 'm.call.invite', + sender: '@alice:example.com' + }) + ).toBe(true) + }) + it('should return false when the filter does not match the event', () => { + const filter = new RoomEventFilter({ + types: ['m.room.message', 'm.call.*'], + senders: ['@alice:example.com'], + rooms: ['!726s6s6q:example.com'] + }) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + membership: 'join' + }, + origin_server_ts: 0, + type: 'm.room.member', + sender: '@alice:example.com' + }) + ).toBe(false) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + body: 'Hello, world!' + }, + origin_server_ts: 0, + type: 'm.room.message', + sender: '@bob:example.com' + }) + ).toBe(false) + expect( + filter.check({ + room_id: '!wrongroom:example.com', + event_id: '1234', + content: { + body: 'Hello, world!' + }, + origin_server_ts: 0, + type: 'm.room.message', + sender: '@alice:example.com' + }) + ).toBe(false) + }) + it('should return true when the contains_url condition match the event', () => { + const filter = new RoomEventFilter({ + contains_url: true + }) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + body: 'Hello, world!', + url: 'https://example.com' + }, + origin_server_ts: 0, + type: 'm.room.message', + sender: '@alice:example.com' + }) + ).toBe(true) + const filter2 = new RoomEventFilter({ + contains_url: false + }) + expect( + filter2.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + body: 'Hello, world!' + }, + origin_server_ts: 0, + type: 'm.room.message', + sender: '@alice:example.com' + }) + ).toBe(true) + }) + it('should return false when the contains_url condition does not match the event', () => { + const filter = new RoomEventFilter({ + contains_url: true + }) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + body: 'Hello, world!' + }, + origin_server_ts: 0, + type: 'm.room.message', + sender: '@alice:example.com' + }) + ).toBe(false) + const filter2 = new RoomEventFilter({ + contains_url: false + }) + expect( + filter2.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + body: 'Hello, world!', + url: 'https://example.com' + }, + origin_server_ts: 0, + type: 'm.room.message', + sender: '@alice:example.com' + }) + ).toBe(false) + }) + it('should return true when the filter includes_redundant_members, accepts lazy_loading_members and the event is a membership event', () => { + const filter = new RoomEventFilter({ + lazy_load_members: true, + include_redundant_members: true + }) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + membership: 'join' + }, + origin_server_ts: 0, + type: 'm.room.member', + sender: '@alice:example.com' + }) + ).toBe(true) + }) + }) + }) + + describe('RoomFilter', () => { + it('should create a filter with the correct parameters', () => { + const filter = new RoomFilter({ + ephemeral: { + limit: 10, + types: ['m.receipt', 'm.typing'], + not_rooms: ['!726s6s6q:example.com'], + not_senders: ['@spam:example.com'] + }, + state: { + types: ['m.room.*'], + not_rooms: ['!726s6s6q:example.com'] + }, + rooms: ['!726s6s6q:example.com'], + include_leave: true + }) + expect(filter.ephemeral?.limit).toBe(10) + expect(filter.ephemeral?.types).toEqual(['m.receipt', 'm.typing']) + expect(filter.ephemeral?.not_rooms).toEqual(['!726s6s6q:example.com']) + expect(filter.ephemeral?.not_senders).toEqual(['@spam:example.com']) + expect(filter.state?.types).toEqual(['m.room.*']) + expect(filter.state?.not_rooms).toEqual(['!726s6s6q:example.com']) + expect(filter.rooms).toEqual(['!726s6s6q:example.com']) + expect(filter.include_leave).toBe(true) + }) + + it('should set include_leave to false by default', () => { + const filter = new RoomFilter({}) + expect(filter.include_leave).toBe(false) + }) + + it('should not create empty filters when not needed', () => { + const filter = new RoomFilter({}) + expect(filter.ephemeral).toBeUndefined() + expect(filter.state).toBeUndefined() + expect(filter.account_data).toBeUndefined() + expect(filter.timeline).toBeUndefined() + }) + + it('should log a warning when creating a filter with an invalid room or not_room', () => { + const filter = new RoomFilter( + { + rooms: ['726s6s6q:example.com'] + }, + mockLogger + ) + expect(mockLogger.warn).toHaveBeenCalled() + expect(filter.rooms).toEqual([]) + const filter2 = new RoomFilter( + { + not_rooms: ['726s6s6q:example.com'] + }, + mockLogger + ) + expect(mockLogger.warn).toHaveBeenCalled() + expect(filter2.not_rooms).toEqual([]) + }) + + describe('get', () => { + it('should return the rooms array', () => { + const filter = new RoomFilter({ + rooms: ['!726s6s6q:example.com'] + }) + // @ts-expect-error - Testing private method + expect(filter.get('rooms')).toEqual(['!726s6s6q:example.com']) + }) + it('should throw an error when the key is not found', () => { + const filter = new RoomFilter({}) + // @ts-expect-error - Testing private method + expect(() => filter.get('not_rooms')).toThrow( + 'Wrong element in get function of RoomFilter' + ) + }) + it('should return null when the field is not set', () => { + const filter = new RoomFilter({}) + // @ts-expect-error - Testing private method + expect(filter.get('rooms')).toBeNull() + }) + }) + describe('getNot', () => { + it('should return the not_rooms array', () => { + const filter = new RoomFilter({ + not_rooms: ['!726s6s6q:example.com'] + }) + // @ts-expect-error - Testing private method + expect(filter.getNot('rooms')).toEqual(['!726s6s6q:example.com']) + }) + it('should throw an error when the key is not found', () => { + const filter = new RoomFilter({}) + // @ts-expect-error - Testing private method + expect(() => filter.getNot('not_rooms')).toThrow( + 'Wrong element in getNot function of RoomFilter' + ) + }) + it('should return an empty array when the field is not set', () => { + const filter = new RoomFilter({}) + // @ts-expect-error - Testing private method + expect(filter.getNot('rooms')).toEqual([]) + }) + }) + describe('check', () => { + it('should return true when the filter matches the event', () => { + const filter = new RoomFilter({ + rooms: ['!726s6s6q:example.com'] + }) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + body: 'Hello, world!' + }, + origin_server_ts: 0, + type: 'm.room.message', + sender: '@alice:example.com' + }) + ).toBe(true) + }) + it('should return false when the filter does not match the event', () => { + const filter = new RoomFilter({ + rooms: ['!726s6s6q:example.com'] + }) + expect( + filter.check({ + room_id: '!wrongroom:example.com', + event_id: '1234', + content: { + body: 'Hello, world!' + }, + origin_server_ts: 0, + type: 'm.room.message', + sender: '@alice:example.com' + }) + ).toBe(false) + }) + describe('inner account_data filter', () => { + it('should return true when the account_data filter matches the event', () => { + const filter = new RoomFilter({ + account_data: { + limit: 10, + types: ['m.tag'] + } + }) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + // insert tag content here + }, + origin_server_ts: 0, + type: 'm.tag', + sender: '@alice:example.com' + }) + ).toBe(true) + }) + it('should return false when the account_data filter does not match the event', () => { + const filter = new RoomFilter({ + account_data: { + limit: 10, + not_types: ['m.tag'] + } + }) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + // insert tag content here + }, + origin_server_ts: 0, + type: 'm.tag', + sender: '@alice:example.com' + }) + ).toBe(false) + }) + }) + describe('inner presence filter', () => { + it('should return true when the ephemeral filter matches the event', () => { + const filter = new RoomFilter({ + ephemeral: { + limit: 10, + types: ['m.receipt', 'm.typing'], + rooms: ['!726s6s6q:example.com'], + not_senders: ['@spam:example.com'] + } + }) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + // insert receipt content here + }, + origin_server_ts: 0, + type: 'm.receipt', + sender: '@alice:example.com' + }) + ).toBe(true) + }) + it('should return false when the ephemeral filter does not match the event', () => { + const filter = new RoomFilter({ + ephemeral: { + limit: 10, + types: ['m.receipt', 'm.typing'], + not_rooms: ['!726s6s6q:example.com'], + not_senders: ['@spam:example.com'] + } + }) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + // insert receipt content here + }, + origin_server_ts: 0, + type: 'm.receipt', + sender: '@spam:example.com' + }) + ).toBe(false) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + // insert receipt content here + }, + origin_server_ts: 0, + type: 'm.presence', + sender: '@spam:example2.com' + }) + ).toBe(false) + }) + }) + describe('inner state filter', () => { + it('should return true when the state filter matches the event', () => { + const filter = new RoomFilter({ + state: { + types: ['m.room.*'], + rooms: ['!726s6s6q:example.com'] + } + }) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + // insert state content here + }, + origin_server_ts: 0, + type: 'm.room.name', + sender: '@alice:example.com' + }) + ).toBe(true) + }) + it('should return false when the state filter does not match the event', () => { + const filter = new RoomFilter({ + state: { + types: ['m.room.n*'], + rooms: ['!726s6s6q:example.com'], + not_senders: ['@spam:example.com'] + } + }) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + // insert state content here + }, + origin_server_ts: 0, + type: 'm.room.name', + sender: '@spam:example.com' + }) + ).toBe(false) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + // insert state content here + }, + origin_server_ts: 0, + type: 'm.room.member', + sender: '@alice:example.com' + }) + ).toBe(false) + }) + }) + describe('inner timeline filter', () => { + it('should return true when the timeline filter matches the event', () => { + const filter = new RoomFilter({ + timeline: { + limit: 10, + types: ['m.room.message'], + rooms: ['!726s6s6q:example.com'], + not_senders: ['@spam:example.com'] + } + }) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + body: 'Hello, world!' + }, + origin_server_ts: 0, + type: 'm.room.message', + sender: '@alice:example.com' + }) + ).toBe(true) + }) + it('should return false when the timeline filter does not match the event', () => { + const filter = new RoomFilter({ + timeline: { + limit: 10, + types: ['m.room.message'], + not_rooms: ['!726s6s6q:example.com'], + not_senders: ['@spam:example.com'] + } + }) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + body: 'Hello, world!' + }, + origin_server_ts: 0, + type: 'm.room.message', + sender: '@spam:example.com' + }) + ).toBe(false) + expect( + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + body: 'Hello, world!' + }, + origin_server_ts: 0, + type: 'm.room.message', + sender: '@alice:example.com' + }) + ).toBe(false) + }) + }) + + it('should throw an error if the event type is not a room event', () => { + const filter = new RoomFilter({}) + expect(() => + filter.check({ + room_id: '!726s6s6q:example.com', + event_id: '1234', + content: { + body: 'Hello, world!' + }, + origin_server_ts: 0, + type: 'm.unknownType', + sender: '@alice:example.com' + }) + ).toThrow('Wrong event type in getType') + }) + }) + }) + + describe('_matchesWildcard', () => { + it('should return true for a wildcard', () => { + expect(_matchesWildcard('m.room', 'm.room')).toBe(true) + }) + + it('should return true for a wildcard with a suffix', () => { + expect(_matchesWildcard('m.room.message', 'm.room.*')).toBe(true) + expect(_matchesWildcard('m.room.message', 'm.room*')).toBe(true) + }) + + it('should return false for a mismatch', () => { + expect(_matchesWildcard('m.room.message', 'm.room.member')).toBe(false) + expect(_matchesWildcard('m.room.message', 'm.room')).toBe(false) + }) + }) +}) diff --git a/packages/matrix-client-server/src/utils/filter.ts b/packages/matrix-client-server/src/utils/filter.ts new file mode 100644 index 00000000..6ae07c78 --- /dev/null +++ b/packages/matrix-client-server/src/utils/filter.ts @@ -0,0 +1,592 @@ +/* eslint-disable @typescript-eslint/strict-boolean-expressions */ +/* eslint-disable @typescript-eslint/naming-convention */ + +/* +Filters are used in the Matrix Protocol in the context of lazy loading. +They are used to filter out events that are not needed by the client. +cf : https://spec.matrix.org/v1.11/client-server-api/#api-endpoints for more details + +The Filter class is used to create filters for the client. + +All Filter objecs have a check method which allows it to check if an event should be filter out or not. + +To be noted: the limit attributes of the filters are not used in the check method but are intented to be used in the +lazy-loading dependent endpoint after events have been filtered. + +To be noted: for fields like room and not_room, fields and not_fields etc. the default values are null and [] respectively. This +is to allow setting room (or fields, etc.) to [] to filter out all events of that type, while setting it to null will allow all +events of that type. not_rooms (or not_fields, etc.) being equal to [] will allow all events of that type. +*/ + +import { type TwakeLogger } from '@twake/logger' +import { type ClientEvent } from '../types' + +import { validEventTypes, isMatrixIdValid, isRoomIdValid } from '@twake/utils' + +type JsonMapping = Record + +export class Filter { + public event_format: string + public event_fields: string[] + public account_data?: AccountDataFilter + public presence?: PresenceFilter + public room?: RoomFilter + + constructor(filter_json: JsonMapping, logger?: TwakeLogger) { + this.account_data = filter_json.account_data + ? new AccountDataFilter(filter_json.account_data) + : undefined + + this.presence = filter_json.presence + ? new PresenceFilter(filter_json.presence) + : undefined + + this.room = filter_json.room ? new RoomFilter(filter_json.room) : undefined + + this.event_format = convertToValidEventFormat( + filter_json.event_format, + logger + ) + + this.event_fields = removeWrongEventFields( + this.event_format, + filter_json.event_fields, + logger + ) + } + + public check(event: ClientEvent): boolean { + const event_type = getTypeAllEvent(event.type) + + switch (event_type) { + case 'account_data': + return this.account_data ? this.account_data.check(event) : true + + case 'presence': + return this.presence ? this.presence.check(event) : true + + case 'room': + return this.room ? this.room.check(event) : true + + /* istanbul ignore next */ // Unreachable code given the return of getTypeAllEvent + default: + throw new Error('Wrong event type in Filter') + } + } +} + +export const MAX_LIMIT = 50 // The maximum limit of events that can be returned in a single request (avoid resource exhaustion) +export class EventFilter { + public limit: number + readonly types: string[] | null + readonly not_types: string[] + readonly senders: string[] | null + readonly not_senders: string[] + + constructor(filter_json: JsonMapping, logger?: TwakeLogger) { + this.limit = filter_json.limit || 10 + if (filter_json.limit < 1) { + logger?.warn('Limit is below 1') + this.limit = 1 + } + if (filter_json.limit > MAX_LIMIT) { + logger?.warn('Limit is higher than the maximum limit') + this.limit = MAX_LIMIT + } + this.types = filter_json.types + ? removeWrongTypes(filter_json.types, logger) + : null + this.not_types = filter_json.not_types + ? removeWrongTypes(filter_json.not_types, logger) + : [] + this.senders = filter_json.senders + ? removeWrongIds(filter_json.senders, logger) + : null + this.not_senders = filter_json.not_senders + ? removeWrongIds(filter_json.not_senders, logger) + : [] + } + + filtersAllTypes(): boolean { + return this.types?.length === 0 || this.not_types.includes('*') // The Matrix spec allows for a wildcard to match all types + } + + filtersAllSenders(): boolean { + return this.senders?.length === 0 + } + + protected get(element: string): string[] | null { + if (element === 'senders') { + return this.senders + } else if (element === 'types') { + return this.types + } else { + throw new Error('Wrong element in get function of EventFilter') + } + } + + protected getNot(element: string): string[] { + if (element === 'senders') { + return this.not_senders + } else if (element === 'types') { + return this.not_types + } else { + throw new Error('Wrong element in getNot function of EventFilter') + } + } + + protected _checkFields( + field_matchers: Record<'senders' | 'types', (v: string) => boolean> + ): boolean { + for (const [name, match_func] of Object.entries(field_matchers)) { + const disallowed_values = this.getNot(name) + if (disallowed_values.some(match_func)) return false + + const allowed_values = this.get(name) + if (allowed_values !== null && !allowed_values.some(match_func)) + return false + } + return true + } + + public check(event: ClientEvent): boolean { + const content = event.content || {} + const sender = event.sender || content.user_id + const ev_type = event.type || null + + const field_matchers = { + senders: (v: string) => sender === v, + types: (v: string) => _matchesWildcard(ev_type, v) + } + + const result = this._checkFields(field_matchers) + return result + } +} + +// This function is used to check if the actual value matches the filter value +export const _matchesWildcard = ( + actual_value: string | null, + filter_value: string +): boolean => { + if (filter_value.endsWith('*') && typeof actual_value === 'string') { + const type_prefix = filter_value.slice(0, -1) + return actual_value.startsWith(type_prefix) + } else { + return actual_value === filter_value + } +} + +/* Filters: + m.push_rules: Stores the user's push notification rules. + m.ignored_user_list: Stores the list of users that the user has chosen to ignore. + m.direct: Stores information about direct message rooms. + m.tag_order: Stores the order of tags for the user. + m.user_devices: Stores information about the user's devices. +*/ +class AccountDataFilter extends EventFilter {} + +// Filters: m.presence +class PresenceFilter extends EventFilter {} + +/* Filters: + m.room.message: Represents a message sent to a room. + m.room.name: Sets the name of the room. + m.room.topic: Sets the topic of the room. + m.room.avatar: Sets the avatar of the room. + m.room.canonical_alias: Sets the primary alias of the room. + m.room.aliases: Lists the aliases of the room. + m.room.member: Manages membership of users in the room (e.g., join, leave, ban). + m.room.create: Indicates the creation of the room and defines properties like the room creator. + m.room.join_rules: Defines the rules for how users can join the room (e.g., public, invite-only). + m.room.power_levels: Defines the power levels of users in the room, determining their permissions. + m.room.history_visibility: Controls who can see the room history. + m.room.guest_access: Controls guest access to the room. + m.room.encryption: Indicates that the room is encrypted and provides encryption settings. + m.room.server_acl: Defines the servers that are allowed or denied access to the room. + m.room.third_party_invite: Used to invite a third-party user to the room. + m.room.pinned_events: Specifies events that are pinned in the room. + + Ephemeral Events + m.typing: Indicates which users are currently typing. + m.receipt: Acknowledges the receipt of messages, typically used for read receipts. + m.presence: Updates presence information of users (e.g., online, offline). + m.room.message.feedback: Provides feedback on messages (e.g., read receipts). + m.room.redaction: Redacts (removes) another event from the room. + + Call Events + m.call.invite: Invites a user to a VoIP call. + m.call.candidates: Provides ICE candidates for establishing a call. + m.call.answer: Answers a VoIP call. + m.call.hangup: Ends a VoIP call. + m.call.reject: Rejects a VoIP call. + Reaction Events + m.reaction: Represents reactions (like emojis) to other events. + Room Tags + m.tag: Tags events to allow clients to organize rooms by tags (e.g., favorites). + + User-Defined Events + m.custom.event: Allows users to define and use custom events. These are not standardized and can vary between implementations. +*/ + +export class RoomEventFilter extends EventFilter { + public include_redundant_members: boolean + public lazy_load_members: boolean + public unread_thread_notifications: boolean + public not_rooms: string[] + public rooms: string[] | null + public contains_url?: boolean + + constructor(filter_json: JsonMapping, logger?: TwakeLogger) { + super(filter_json) + this.not_rooms = filter_json.not_rooms + ? removeWrongRoomIds(filter_json.not_rooms, logger) + : [] + this.rooms = filter_json.rooms + ? removeWrongRoomIds(filter_json.rooms, logger) + : null + this.include_redundant_members = + filter_json.include_redundant_members || false + this.lazy_load_members = filter_json.lazy_load_members || false + this.unread_thread_notifications = + filter_json.unread_thread_notifications || false + this.contains_url = filter_json.contains_url + } + + // Overriding method to include room field + protected get(element: string): string[] | null { + if (element === 'senders') { + return this.senders + } else if (element === 'types') { + return this.types + } else if (element === 'rooms') { + return this.rooms + } else { + throw new Error('Wrong element in get function of RoomEventFilter') + } + } + + // Overriding method to include room field + protected getNot(element: string): string[] { + if (element === 'senders') { + return this.not_senders + } else if (element === 'types') { + return this.not_types + } else if (element === 'rooms') { + return this.not_rooms + } else { + throw new Error('Wrong element in getNot function of RoomEventFilter') + } + } + + protected _checkFields( + field_matchers: Record boolean> + ): boolean { + for (const [name, match_func] of Object.entries(field_matchers)) { + const disallowed_values = this.getNot(name) + if (disallowed_values.some(match_func)) return false + + const allowed_values = this.get(name) + if (allowed_values !== null && !allowed_values.some(match_func)) + return false + } + return true + } + + public check(event: ClientEvent): boolean { + const content = event.content || {} + const sender = event.sender || content.user_id + const ev_type = event.type || null + const room_id = event.room_id || null + + if (this.include_redundant_members && this.lazy_load_members) { + if (ev_type === 'm.room.member') { + return true + } + } + + const field_matchers = { + senders: (v: string) => sender === v, + types: (v: string) => _matchesWildcard(ev_type, v), + rooms: (v: string) => room_id === v + } + + const result = this._checkFields(field_matchers) + if (!result) return false + + if (this.contains_url !== undefined) { + const contains_url = typeof content.url === 'string' + if (this.contains_url !== contains_url) return false + } + + return true + } +} + +// The include leave key is used to include rooms that the user has left in the sync response. +export class RoomFilter { + public include_leave: boolean + public not_rooms: string[] + public rooms: string[] | null + public account_data?: RoomEventFilter + public ephemeral?: RoomEventFilter + public state?: RoomEventFilter + public timeline?: RoomEventFilter + + constructor(filter_json: JsonMapping, logger?: TwakeLogger) { + this.account_data = filter_json.account_data + ? new RoomEventFilter(filter_json.account_data) + : undefined + this.ephemeral = filter_json.ephemeral + ? new RoomEventFilter(filter_json.ephemeral) + : undefined + this.include_leave = filter_json.include_leave || false + this.not_rooms = filter_json.not_rooms + ? removeWrongRoomIds(filter_json.not_rooms, logger) + : [] + this.rooms = filter_json.rooms + ? removeWrongRoomIds(filter_json.rooms, logger) + : null + this.state = filter_json.state + ? new RoomEventFilter(filter_json.state) + : undefined + this.timeline = filter_json.timeline + ? new RoomEventFilter(filter_json.timeline) + : undefined + } + + protected get(element: string): string[] | null { + if (element === 'rooms') { + return this.rooms + } else { + throw new Error('Wrong element in get function of RoomFilter') + } + } + + protected getNot(element: string): string[] { + if (element === 'rooms') { + return this.not_rooms + } else { + throw new Error('Wrong element in getNot function of RoomFilter') + } + } + + protected _checkFields( + field_matchers: Record boolean> + ): boolean { + for (const [name, match_func] of Object.entries(field_matchers)) { + const disallowed_values = this.getNot(name) + if (disallowed_values.some(match_func)) return false + + const allowed_values = this.get(name) + if (allowed_values !== null && !allowed_values.some(match_func)) + return false + } + return true + } + + public check(event: ClientEvent): boolean { + const room_id = event.room_id || null + + const field_matchers = { + rooms: (v: string) => room_id === v + } + + const result = this._checkFields(field_matchers) + if (!result) return false + + // check if the event is account_data, ephemeral, state, or timeline + const event_type = getTypeRoomEvent(event.type) + + switch (event_type) { + case 'account_data': + return this.account_data ? this.account_data.check(event) : true + + case 'ephemeral': + return this.ephemeral ? this.ephemeral.check(event) : true + + case 'state': + return this.state ? this.state.check(event) : true + + case 'timeline': + return this.timeline ? this.timeline.check(event) : true + + /* istanbul ignore next */ // Unreachable code given the return of getTypeRoomEvent + default: + throw new Error('Wrong event type in RoomFilter') + } + } +} + +// +/* Getting an event type functions */ +// + +// TODO : verify validity of the 2 functions below +function getTypeRoomEvent(event_type: string): string { + const roomEvents = { + account_data: ['m.tag'], + ephemeral: [ + 'm.typing', + 'm.receipt', + 'm.presence', + 'm.room.message.feedback' + ], + state: [ + 'm.room.name', + 'm.room.topic', + 'm.room.avatar', + 'm.room.canonical_alias', + 'm.room.aliases', + 'm.room.member', + 'm.room.create', + 'm.room.join_rules', + 'm.room.power_levels', + 'm.room.history_visibility', + 'm.room.guest_access', + 'm.room.encryption', + 'm.room.server_acl', + 'm.room.third_party_invite', + 'm.room.pinned_events' + ], + timeline: [ + 'm.room.message', + 'm.room.redaction', + 'm.call.invite', + 'm.call.candidates', + 'm.call.answer', + 'm.call.hangup', + 'm.call.reject', + 'm.reaction', + 'm.custom.event' + ] + } + + for (const [type, events] of Object.entries(roomEvents)) { + if (events.includes(event_type)) { + return type + } + } + throw new Error('Wrong event type in getType') +} + +function getTypeAllEvent(event_type: string): string { + const allEvents = { + account_data: [ + 'm.push_rules', + 'm.ignored_user_list', + 'm.direct', + 'm.user_devices', + 'm.tag_order' + ], + presence: ['m.presence'] + } + + // If not in any of the above categories, it is a room event + // In the roomFilter we check again the event type so it is assured that the event is a correct room event + + for (const [type, events] of Object.entries(allEvents)) { + if (events.includes(event_type)) { + return type + } + } + return 'room' +} + +// +/* Data verification methods */ +// + +const convertToValidEventFormat = ( + event_format?: string, + logger?: TwakeLogger +): string => { + if (event_format === 'client' || event_format === 'federation') { + return event_format + } else { + logger?.warn('Wrong event format in Filter - using default value') + return 'client' + } +} + +const validClientEventFields = Object.freeze( + new Set([ + 'content', + 'event_id', + 'origin_server_ts', + 'room_id', + 'sender', + 'state_key', + 'type', + 'unsigned' + ]) +) + +const removeWrongEventFields = ( + event_format: string, + event_fields?: string[], + logger?: TwakeLogger +): string[] => { + if (!event_fields) { + return [] + } + + if (event_format === 'client') { + return event_fields.filter((field) => { + const [fieldName, subField] = field.split('.') + + const isValid = + validClientEventFields.has(fieldName) && + (subField === undefined || subField.length <= 30) // Arbitrary limit to avoid too long subfields + + if (!isValid) { + logger?.warn(`Invalid field given in filter constructor : ${field}`) + } + + return isValid + }) + } + + if (event_format === 'federation') { + // TODO: Implement restrictions for federationEventFields + return event_fields + } + /* istanbul ignore next */ + throw new Error('Missing event format in call to removeWrongEventFields') +} + +const removeWrongTypes = (types: string[], logger?: TwakeLogger): string[] => { + return types.filter((type) => { + // TODO : verify in @twake/utils if validEventTypes is correctly implemented + const isValid = validEventTypes.some((eventType) => + _matchesWildcard(eventType, type) + ) + if (!isValid) { + logger?.warn(`Removed invalid type: ${type}`) + } + return isValid + }) +} + +const removeWrongIds = (senders: string[], logger?: TwakeLogger): string[] => { + return senders.filter((sender) => { + const isValid = isMatrixIdValid(sender) + if (!isValid && logger) { + logger.warn(`Removed invalid sender: ${sender}`) + } + return isValid + }) +} + +const removeWrongRoomIds = ( + rooms: string[], + logger?: TwakeLogger +): string[] => { + return rooms.filter((room) => { + const isValid = isRoomIdValid(room) + if (!isValid) { + logger?.warn(`Removed invalid room ID: ${room}`) + } + return isValid + }) +} diff --git a/packages/matrix-client-server/src/utils/mailer.ts b/packages/matrix-client-server/src/utils/mailer.ts new file mode 100644 index 00000000..06683319 --- /dev/null +++ b/packages/matrix-client-server/src/utils/mailer.ts @@ -0,0 +1,54 @@ +// TODO : Add Mailer to @twake/utils after creating a union-type Config = clientServerConfig | identityServerConfig | federatedIdentityServerConfig ... and changing to Config to the right subtype in the relevant files + +import nodeMailer, { + type Transporter, + type SentMessageInfo, + type SendMailOptions +} from 'nodemailer' +import { type Config } from '../types' + +class Mailer { + transport: Transporter + from: string + constructor(conf: Config) { + const opt: { + host: string + port: number + auth?: Record + secure?: boolean + tls: { + rejectUnauthorized?: boolean + } + } = { + host: conf.smtp_server, + /* istanbul ignore next */ + port: conf.smtp_port != null ? conf.smtp_port : 25, + tls: { rejectUnauthorized: conf.smtp_verify_certificate } + } + if (conf.smtp_tls != null) { + opt.secure = conf.smtp_tls + } + if (conf.smtp_user != null) { + opt.auth = { + type: 'LOGIN', + user: conf.smtp_user, + pass: conf.smtp_password as string + } + } + this.transport = nodeMailer.createTransport(opt) + /* istanbul ignore next */ + this.from = + conf.smtp_sender != null && conf.smtp_sender.length > 0 + ? conf.smtp_sender + : `no-reply@${conf.server_name}` + } + + async sendMail(opt: SendMailOptions): Promise { + if (opt.from == null) { + opt.from = this.from + } + await this.transport.sendMail(opt) + } +} + +export default Mailer diff --git a/packages/matrix-client-server/src/utils/sms.test.ts b/packages/matrix-client-server/src/utils/sms.test.ts new file mode 100644 index 00000000..16e7d507 --- /dev/null +++ b/packages/matrix-client-server/src/utils/sms.test.ts @@ -0,0 +1,76 @@ +import fs from 'fs/promises' +import path from 'path' +import SmsSender from '../utils/smsSender' +import defaultConfig from '../config.json' +import { type Config } from '../types' + +jest.mock('fs/promises', () => ({ + writeFile: jest.fn(), + mkdir: jest.fn() +})) + +describe('SmsSender', () => { + // @ts-expect-error : TS doesn't know that the Config object is valid + const conf: Config = { + ...defaultConfig, + database_engine: 'sqlite', + base_url: 'http://example.com/', + userdb_engine: 'sqlite', + matrix_database_engine: 'sqlite' + } + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should write SMS content to the correct file', async () => { + const sender = new SmsSender(conf) + const smsData = { to: '1234567890', raw: 'Test SMS content' } + const fileName = `sms_${smsData.to}}.txt` + const filePath = path.join(conf.sms_folder, fileName) + + await sender.sendSMS(smsData) + expect(fs.writeFile).toHaveBeenCalledWith(filePath, smsData.raw, 'utf8') + expect(fs.mkdir).toHaveBeenCalledWith(conf.sms_folder, { recursive: true }) + expect(fs.writeFile).toHaveBeenCalledWith( + expect.any(String), + smsData.raw, + 'utf8' + ) + }) + + it('should throw an error if SMS folder is not specified in the config', () => { + // @ts-expect-error : Not a Config object + expect(() => new SmsSender({})).toThrow( + 'SMS folder path not specified in the configuration' + ) + }) + + it('should log a message when SMS content is written successfully', async () => { + console.log = jest.fn() + const sender = new SmsSender(conf) + const smsData = { to: '1234567890', raw: 'Test SMS content' } + + await sender.sendSMS(smsData) + + expect(console.log).toHaveBeenCalledWith( + expect.stringContaining('SMS content written to') + ) + }) + + it('should log an error message if writing SMS content to file fails', async () => { + console.error = jest.fn() + ;(fs.writeFile as jest.Mock).mockRejectedValueOnce( + new Error('Failed to write file') + ) + const sender = new SmsSender(conf) + const smsData = { to: '1234567890', raw: 'Test SMS content' } + + await expect(sender.sendSMS(smsData)).rejects.toThrow( + 'Failed to write file' + ) + expect(console.error).toHaveBeenCalledWith( + 'Failed to write SMS content to file', + expect.any(Error) + ) + }) +}) diff --git a/packages/matrix-client-server/src/utils/smsSender.ts b/packages/matrix-client-server/src/utils/smsSender.ts new file mode 100644 index 00000000..f3bf5404 --- /dev/null +++ b/packages/matrix-client-server/src/utils/smsSender.ts @@ -0,0 +1,33 @@ +import fs from 'fs/promises' +import path from 'path' +import { type Config } from '../types' + +// TODO : Modify this class to effectively send SMS messages and not just write in a file +class SmsSender { + private readonly folderPath: string + + constructor(conf: Config) { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (!conf.sms_folder) { + throw new Error('SMS folder path not specified in the configuration') + } + this.folderPath = conf.sms_folder + } + + async sendSMS(obj: { to: string; raw: string }): Promise { + const { to, raw } = obj + const fileName = `sms_${to}}.txt` + const filePath = path.join(this.folderPath, fileName) + + try { + await fs.mkdir(this.folderPath, { recursive: true }) + await fs.writeFile(filePath, raw, 'utf8') + console.log(`SMS content written to ${filePath}`) + } catch (error) { + console.error('Failed to write SMS content to file', error) + throw error + } + } +} + +export default SmsSender diff --git a/packages/matrix-client-server/src/utils/userInteractiveAuthentication.ts b/packages/matrix-client-server/src/utils/userInteractiveAuthentication.ts new file mode 100644 index 00000000..4c3a06c9 --- /dev/null +++ b/packages/matrix-client-server/src/utils/userInteractiveAuthentication.ts @@ -0,0 +1,662 @@ +import { type TwakeLogger } from '@twake/logger' +import { type Request, type Response } from 'express' +import type http from 'http' +import type e from 'express' +import { + type MatrixIdentifier, + type AuthenticationData, + type Config, + type AppServiceRegistration, + type ThreepidCreds, + type AuthenticationFlowContent, + type AuthenticationTypes, + type ApplicationServiceAuth +} from '../types' +import { Hash, randomString } from '@twake/crypto' +import type MatrixDBmodified from '../matrixDb' +import { epoch, errMsg, send, toMatrixId, isMatrixIdValid } from '@twake/utils' +import type MatrixClientServer from '..' +export type UiAuthFunction = ( + req: Request | http.IncomingMessage, + res: Response | http.ServerResponse, + allowedFlows: AuthenticationFlowContent, + description: string, + obj: any, + callback: (data: any, userId: string | null) => void +) => void + +interface requestBody { + auth?: AuthenticationData + [key: string]: any // others parameters given in request body +} + +export const getParams = (type: AuthenticationTypes): any => { + // for now only terms has params, spec is unclear about the other types. Add params here if needed in other endpoints + // For production,maybe these params should be included in the config. The values here are only illustrative and taken from examples in the spec, they are not relevant and should be adapted before deployment. + // TODO : Modify this before deployment + switch (type) { + case 'm.login.terms': + return { + policies: { + terms_of_service: { + version: '1.2', + en: { + name: 'Terms of Service', + url: 'https://example.org/somewhere/terms-1.2-en.html' + }, + fr: { + name: "Conditions d'utilisation", + url: 'https://example.org/somewhere/terms-1.2-fr.html' + } + } + } + } + default: + return {} + } +} + +// This method is used after the user has been authenticated with clientServer.authenticate to verify that the user is indeed who he claims to be +export const validateUserWithUIAuthentication = ( + clientServer: MatrixClientServer, + req: Request | http.IncomingMessage, + res: Response | http.ServerResponse, + userId: string, + description: string, + obj: any, + callback: (data: any, userId: string | null) => void +): void => { + if (userId != null && !isMatrixIdValid(userId)) { + send( + res, + 400, + errMsg('invalidParam', 'Invalid user ID'), + clientServer.logger + ) + return + } + // Authentication flows to verify that the user who has an access token is indeed who he claims to be, and has not just stolen another user's access token + getAvailableValidateUIAuthFlows(clientServer, userId) + .then((verificationFlows) => { + clientServer.uiauthenticate( + req, + res, + verificationFlows, + description, + obj, + callback + ) + }) + .catch((e) => { + // istanbul ignore next + clientServer.logger.error( + 'Error getting available authentication flows for user', + e + ) + // istanbul ignore next + send(res, 500, e, clientServer.logger) + }) +} + +// Function to get the available authentication flows for a user +const getAvailableValidateUIAuthFlows = async ( + clientServer: MatrixClientServer, + userId: string +): Promise => { + const availableFlows: AuthenticationFlowContent = { + flows: [], + params: {} + } + const passwordRows = await clientServer.matrixDb.get( + 'users', + ['password_hash'], + { + name: userId + } + ) + if ( + clientServer.conf.is_password_login_enabled && + passwordRows.length > 0 && + passwordRows[0].password_hash !== null + ) { + // If the user has a password registered, he can authenticate using it + availableFlows.flows.push({ + stages: ['m.login.password'] + }) + availableFlows.params['m.login.password'] = getParams('m.login.password') + } + if (clientServer.conf.is_sso_login_enabled) { + availableFlows.flows.push({ + stages: ['m.login.sso'] + }) + availableFlows.params['m.login.sso'] = getParams('m.login.sso') + } + return availableFlows +} + +// We do a separate function for the /register endpoint since the authentication flows are different +// For now we use the same config variables to allow the flows for login and register, but this can be changed in the future +// We don't include m.login.sso as done in the ElementHQ implementation but we could add it if needed +export const getRegisterAllowedFlows = ( + conf: Config +): AuthenticationFlowContent => { + const availableFlows: AuthenticationFlowContent = { + flows: [], + params: {} + } + const requireEmail: boolean = + conf.registration_required_3pid.includes('email') + const requireMsisdn: boolean = + conf.registration_required_3pid.includes('msisdn') + if (requireEmail && !conf.is_email_login_enabled) { + // istanbul ignore next + throw new Error('Email registration is required but not enabled') + } + if (requireMsisdn && !conf.is_msisdn_login_enabled) { + // istanbul ignore next + throw new Error('Msisdn registration is required but not enabled') + } + if (conf.is_recaptcha_login_enabled) { + availableFlows.flows.push({ + stages: ['m.login.recaptcha'] + }) + availableFlows.params['m.login.recaptcha'] = getParams('m.login.recaptcha') + } + if (conf.is_registration_token_login_enabled) { + availableFlows.flows.push({ + stages: ['m.login.registration_token'] + }) + availableFlows.params['m.login.registration_token'] = getParams( + 'm.login.registration_token' + ) + } + if (conf.is_terms_login_enabled) { + availableFlows.flows.push({ + stages: ['m.login.terms'] + }) + availableFlows.params['m.login.terms'] = getParams('m.login.terms') + } + if (requireEmail && requireMsisdn) { + availableFlows.flows.push({ + stages: ['m.login.email.identity', 'm.login.msisdn'] + }) + availableFlows.params['m.login.email.identity'] = getParams( + 'm.login.email.identity' + ) + availableFlows.params['m.login.msisdn'] = getParams('m.login.msisdn') + } else { + if (conf.is_msisdn_login_enabled) { + availableFlows.flows.push({ + stages: ['m.login.msisdn'] + }) + availableFlows.params['m.login.msisdn'] = getParams('m.login.msisdn') + } + if (conf.is_email_login_enabled) { + availableFlows.flows.push({ + stages: ['m.login.email.identity'] + }) + availableFlows.params['m.login.email.identity'] = getParams( + 'm.login.email.identity' + ) + } + if (!requireEmail && !requireMsisdn) { + // If no 3pid authentication is required, we add the dummy auth flow as done in elementHQ's implementation. + // This allows anybody to register so it could be removed if it is considered a security risk + availableFlows.flows.push({ + stages: ['m.login.dummy'] + }) + // No parameters for dummy auth since it always succeeds + } + } + return availableFlows +} + +// eslint-disable-next-line @typescript-eslint/promise-function-async +const checkAuthentication = ( + auth: AuthenticationData, + matrixDb: MatrixDBmodified +): Promise => { + // It returns a Promise so that it can return the userId of the authenticated user for endpoints other than /register. For register and dummy auth we return ''. + switch (auth.type) { + case 'm.login.password': + return new Promise((resolve, reject) => { + const hash = new Hash() + hash.ready + .then(() => { + matrixDb + .get('users', ['name'], { + name: (auth.identifier as MatrixIdentifier).user, + password_hash: hash.sha256(auth.password) // TODO : Handle other hash functions + }) + .then((rows) => { + if (rows.length === 0) { + throw new Error('User not found') + } else { + // Maybe should also check that the user account isn't shadowbanned nor deactivated (check that rows[0].shadow_banned/deactivated ===0) + // Normally upon deactivation the password_hash should be set to null so we shouldn't need to check for that but maybe it's better to be safe + // We only consider the case where the identifier is a MatrixIdentifier + // since the only table that has a password field is the users table + // which only contains a "name" field with the userId and no address field + // meaning we can't access it without the userId associated to that password + + resolve(rows[0].name as string) + } + }) + .catch((e) => { + reject( + errMsg( + 'forbidden', + 'The user does not have a password registered or the provided password is wrong.' + ) + ) + }) + }) + .catch((e) => { + // istanbul ignore next + reject(e) + }) + }) + case 'm.login.sso': + return new Promise((resolve, reject) => { + // TODO : Complete this after implementing fallback mechanism : https://spec.matrix.org/v1.11/client-server-api/#fallback + resolve('') // Placeholder return statement + }) + case 'm.login.msisdn': + case 'm.login.email.identity': // Both cases are handled the same through their threepid_creds + return new Promise((resolve, reject) => { + const threepidCreds: ThreepidCreds = auth.threepid_creds + matrixDb + .get('threepid_validation_session', ['address', 'validated_at'], { + client_secret: threepidCreds.client_secret, + session_id: threepidCreds.sid + }) + .then((sessionRows) => { + if (sessionRows.length === 0) { + reject(errMsg('noValidSession')) + return + } + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (!sessionRows[0].validated_at) { + reject(errMsg('sessionNotValidated')) + return + } + matrixDb + .get('user_threepids', ['user_id'], { + address: sessionRows[0].address, + medium: auth.type === 'm.login.msisdn' ? 'msisdn' : 'email' // So that you can't validate with an email if you're in the msisdn flow and vice versa + }) + .then((rows) => { + if (rows.length === 0) { + reject(errMsg('threepidNotFound')) + } else { + resolve(rows[0].user_id as string) + } + }) + .catch((e) => { + // istanbul ignore next + reject(e) + }) + }) + .catch((e) => { + // istanbul ignore next + reject(e) + }) + }) + case 'm.login.recaptcha': + return new Promise((resolve, reject) => { + // TODO : Implement this after understanding the structure of the response field in request body + resolve('') // Placeholder return statement + }) + case 'm.login.dummy': + return new Promise((resolve, reject) => { + resolve('') // Dummy authentication always succeeds + }) + case 'm.login.registration_token': // Only valid on the /register endpoint as per the spec // TODO : add uses_allowed to config ? + return new Promise((resolve, reject) => { + matrixDb + .get( + 'registration_tokens', + ['uses_allowed', 'pending', 'completed'], + { + // We don't check for expiry time as the client should use the /validity API before attempting registration to make sure the token is still valid before using it, as per the spec + token: auth.token + } + ) + .then((rows) => { + const pending: number = rows[0].pending as number + matrixDb + .updateWithConditions( + 'registration_tokens', + { pending: pending + 1 }, + [{ field: 'token', value: auth.token }] + ) + .then(() => { + const completed: number = rows[0].completed as number + const usesAllowed: number = rows[0].uses_allowed as number + if ( + pending + completed + 1 > usesAllowed && + usesAllowed !== null + ) { + reject( + errMsg('invalidToken', 'Token has been used too many times') + ) + } else { + matrixDb + .updateWithConditions( + 'registration_tokens', + { completed: completed + 1, pending }, + [{ field: 'token', value: auth.token }] + ) + .then(() => { + resolve('') + }) + .catch((e) => { + // istanbul ignore next + reject(e) + }) + } + }) + .catch((e) => { + // istanbul ignore next + reject(e) + }) + }) + .catch((e) => { + // istanbul ignore next + reject(e) + }) + }) + case 'm.login.terms': // Only valid on the /register endpoint as per the spec + return new Promise((resolve, reject) => { + resolve('') // The client makes sure the user has accepted all the terms before sending the request indicating the user has accepted the terms + }) + } + // istanbul ignore next + return new Promise((resolve, reject) => { + // istanbul ignore next + resolve('') // Placeholder to prevent error since m.login.application_service isn't handled here + }) +} + +// eslint-disable-next-line @typescript-eslint/promise-function-async +const handleAppServiceAuthentication = ( + req: Request | http.IncomingMessage, + conf: Config, + auth: ApplicationServiceAuth +): Promise => { + return new Promise((resolve, reject) => { + const applicationServices = conf.application_services + const asTokens: string[] = applicationServices.map( + (as: AppServiceRegistration) => as.as_token + ) + if (req.headers.authorization === undefined) { + reject(errMsg('missingToken')) + } + // @ts-expect-error req.headers.authorization is defined + const token = req.headers.authorization.split(' ')[1] + if (asTokens.includes(token)) { + // Check if the request is made by an application-service + const appService = applicationServices.find( + (as: AppServiceRegistration) => as.as_token === token + ) + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + const userId = auth.username + ? toMatrixId(auth.username, conf.server_name) + : // @ts-expect-error : appService is defined since asTokens contains token + toMatrixId(appService?.sender_localpart, conf.server_name) + if ( + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + appService?.namespaces.users && + !appService?.namespaces.users.some((namespace) => + new RegExp(namespace.regex).test(userId) + ) // check if the userId is registered by the appservice + ) { + reject(errMsg('invalidUsername')) + } else { + resolve(userId) + } + } else { + reject(errMsg('unknownToken')) + } + }) +} + +const doAppServiceAuthentication = ( + req: Request | http.IncomingMessage, + res: Response | http.ServerResponse, + allowedFlows: AuthenticationFlowContent, + auth: ApplicationServiceAuth, + conf: Config, + logger: TwakeLogger, + obj: any, + callback: (data: any, userId: string | null) => void +): void => { + handleAppServiceAuthentication(req, conf, auth) + .then((userId) => { + callback(obj, userId) + }) + .catch((e) => { + send( + res, + 401, + { + errcode: e.errcode, + error: e.error, + ...allowedFlows + }, + logger + ) + }) +} + +const UiAuthenticate = ( + // db: ClientServerDb, + matrixDb: MatrixDBmodified, + conf: Config, + logger: TwakeLogger +): UiAuthFunction => { + return (req, res, allowedFlows, description, obj, callback) => { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (!(obj as requestBody).auth) { + // If there is no auth key in the request body, we create a new authentication session + const sessionId = randomString(24) // Chose 24 according to synapse implementation but seems arbitrary + const ip = (req as e.Request).ip + // istanbul ignore if + if (ip === undefined) { + // istanbul ignore next + send(res, 500, errMsg('unknown', 'IP address is missing')) + return + } + const userAgent = req.headers['user-agent'] ?? 'undefined' + const addUserIps = matrixDb.insert('ui_auth_sessions_ips', { + session_id: sessionId, + ip, + user_agent: userAgent + }) + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (obj.password) { + // Since we store the clientdict in the database, we don't want to store the unhashed password in it + delete obj.password + } + const createAuthSession = matrixDb.insert('ui_auth_sessions', { + session_id: sessionId, + creation_time: epoch(), + clientdict: JSON.stringify(obj), + serverdict: JSON.stringify({}), + uri: req.url as string, // TODO : Ensure this is the right way to get the URI + method: req.method as string, + description + }) + Promise.all([addUserIps, createAuthSession]) + .then(() => { + send( + // We send back the session_id to the client so that he can use it in future requests + res, + 401, + { + ...allowedFlows, + session: sessionId + }, + logger + ) + }) + .catch((e) => { + /* istanbul ignore next */ + logger.error( + 'Error while creating a new session during User-Interactive Authentication', + e + ) + /* istanbul ignore next */ + send(res, 500, e, logger) + }) + } else { + const auth = (obj as requestBody).auth as AuthenticationData + if (auth.type === 'm.login.application_service') { + doAppServiceAuthentication( + req, + res, + allowedFlows, + auth, + conf, + logger, + obj, + callback + ) + return + } + matrixDb + .get('ui_auth_sessions', ['*'], { session_id: auth.session }) + .then((rows) => { + if (rows.length === 0) { + logger.error(`Unknown session ID : ${auth.session}`) + send(res, 400, errMsg('noValidSession'), logger) + } else if (rows[0].uri !== req.url || rows[0].method !== req.method) { + send( + res, + 403, + errMsg( + 'forbidden', + 'Requested operation has changed during the UI authentication session.' + ), + logger + ) + } else { + checkAuthentication(auth, matrixDb) + .then((userId) => { + matrixDb + .insert('ui_auth_sessions_credentials', { + session_id: auth.session, + stage_type: auth.type, + result: userId + }) + .then((rows) => { + const getCompletedStages = matrixDb.get( + 'ui_auth_sessions_credentials', + ['stage_type'], + { + session_id: auth.session + } + ) + const updateClientDict = matrixDb.updateWithConditions( + 'ui_auth_sessions', + { clientdict: JSON.stringify(obj) }, + [{ field: 'session_id', value: auth.session }] + ) + Promise.all([getCompletedStages, updateClientDict]) + .then((rows) => { + const completed: string[] = rows[0].map( + (row) => row.stage_type as string + ) + const authOver = allowedFlows.flows.some((flow) => { + return ( + flow.stages.length === completed.length && + flow.stages.every((stage) => + completed.includes(stage) + ) + ) + }) + + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (authOver) { + callback(obj, userId) // Arguments of callback are subject to change + } else { + send( + res, + 401, + { + ...allowedFlows, + session: auth.session, + completed + }, + logger + ) + } + }) + .catch((e) => { + /* istanbul ignore next */ + logger.error( + 'Error while retrieving session credentials from the database during User-Interactive Authentication', + e + ) + /* istanbul ignore next */ + send(res, 500, e, logger) + }) + }) + .catch((e) => { + /* istanbul ignore next */ + logger.error( + 'Error while inserting session credentials into the database during User-Interactive Authentication', + e + ) + /* istanbul ignore next */ + send(res, 500, e, logger) + }) + }) + .catch((e) => { + matrixDb + .get('ui_auth_sessions_credentials', ['stage_type'], { + session_id: auth.session + }) + .then((rows) => { + const completed: string[] = rows.map( + // istanbul ignore next + (row) => row.stage_type as string + ) + send( + res, + 401, + { + errcode: e.errcode, + error: e.error, + completed, + ...allowedFlows, + session: auth.session + }, + logger + ) + }) + .catch((e) => { + /* istanbul ignore next */ + logger.error( + 'Error while retrieving session credentials from the database during User-Interactive Authentication', + e + ) + /* istanbul ignore next */ + send(res, 500, e, logger) + }) + }) + } + }) + .catch((e) => { + // istanbul ignore next + logger.error( + 'Error retrieving UI Authentication session from the database' + ) + // istanbul ignore next + send(res, 500, e, logger) + }) + } + } +} + +export default UiAuthenticate diff --git a/packages/matrix-client-server/src/utils/utils.ts b/packages/matrix-client-server/src/utils/utils.ts new file mode 100644 index 00000000..6b56b910 --- /dev/null +++ b/packages/matrix-client-server/src/utils/utils.ts @@ -0,0 +1,21 @@ +import type MatrixClientServer from '..' +import { type DbGetResult } from '../types' + +export const isAdmin = async ( + clientServer: MatrixClientServer, + userId: string +): Promise => { + try { + const response: DbGetResult = await clientServer.matrixDb.get( + 'users', + ['admin'], + { + name: userId + } + ) + return response.length > 0 && response[0].admin === 1 + } catch (e) { + clientServer.logger.error('Error checking admin', e) + return false + } +} diff --git a/packages/matrix-client-server/src/versions.ts b/packages/matrix-client-server/src/versions.ts new file mode 100644 index 00000000..10a714cf --- /dev/null +++ b/packages/matrix-client-server/src/versions.ts @@ -0,0 +1,43 @@ +import { send, type expressAppHandler } from '@twake/utils' + +/* This part deals with supported versions of the matrix Protocol itself */ + +// TODO: fix supported versions +export const versions = [ + // 'r0.1.0', + // 'r0.2.0', + // 'r0.2.1', + // 'r0.3.0', + 'v1.1', + 'v1.2', + 'v1.3', + 'v1.4', + 'v1.5', + 'v1.6' +] + +const getVersions: expressAppHandler = (req, res) => { + send(res, 200, { versions }) +} + +export default getVersions + +/* This part deals with supported room versions */ + +// TODO : update the room versions to the latest supported versions + +export const DEFAULT_ROOM_VERSION = 10 + +export const ROOM_VERSIONS = { + 1: 'stable', + 2: 'stable', + 3: 'stable', + 4: 'stable', + 5: 'stable', + 6: 'stable', + 7: 'stable', + 8: 'stable', + 9: 'stable', + 10: 'stable', + 11: 'stable' +} diff --git a/packages/matrix-client-server/templates/3pidInvitation.tpl b/packages/matrix-client-server/templates/3pidInvitation.tpl new file mode 100644 index 00000000..e5bbad27 --- /dev/null +++ b/packages/matrix-client-server/templates/3pidInvitation.tpl @@ -0,0 +1,67 @@ +Date: __date__ +From: __from__ +To: __to__ +Message-ID: __messageid__ +Subject: Invitation to join a Matrix room +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="__multipart_boundary__" + +--__multipart_boundary__ +Content-Type: text/plain; charset=UTF-8 +Content-Disposition: inline +Hello, + +You have been invited to join a Matrix room by __inviter_name__. If you possess a Matrix account, please consider binding this email address to your account in order to accept the invitation. + + +About Matrix: +Matrix is an open standard for interoperable, decentralised, real-time communication over IP. It can be used to power Instant Messaging, VoIP/WebRTC signalling, Internet of Things communication - or anywhere you need a standard HTTP API for publishing and subscribing to data whilst tracking the conversation history. + +Matrix defines the standard, and provides open source reference implementations of Matrix-compatible Servers, Clients, Client SDKs and Application Services to help you create new communication solutions or extend the capabilities and reach of existing ones. + +--__multipart_boundary__ +Content-Type: text/html; charset=UTF-8 +Content-Disposition: inline + + + + + +Invitation to join a Matrix room + + + +

Hello,

+ +

You have been invited to join a Matrix room by __inviter_name__. If you possess a Matrix account, please consider binding this email address to your account in order to accept the invitation.

+ +

If your client requires a code, the code is __token__

+ +
+

Invitation Details:

+
    +
  • Inviter: __inviter_name__ (display name: __inviter_display_name__)
  • +
  • Room Name: __room_name__
  • +
  • Room Type: __room_type__
  • +
  • Room Avatar: Room Avatar
  • +
+ +
+

About Matrix:

+ +

Matrix is an open standard for interoperable, decentralised, real-time communication over IP. It can be used to power Instant Messaging, VoIP/WebRTC signalling, Internet of Things communication - or anywhere you need a standard HTTP API for publishing and subscribing to data whilst tracking the conversation history.

+ +

Matrix defines the standard, and provides open source reference implementations of Matrix-compatible Servers, Clients, Client SDKs and Application Services to help you create new communication solutions or extend the capabilities and reach of existing ones.

+ + + + +--__multipart_boundary__-- + diff --git a/packages/matrix-client-server/templates/mailVerification.tpl b/packages/matrix-client-server/templates/mailVerification.tpl new file mode 100644 index 00000000..6a5c4740 --- /dev/null +++ b/packages/matrix-client-server/templates/mailVerification.tpl @@ -0,0 +1,77 @@ +Date: __date__ +From: __from__ +To: __to__ +Message-ID: __messageid__ +Subject: Confirm your email address for Matrix +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="__multipart_boundary__" + +--__multipart_boundary__ +Content-Type: text/plain; charset=UTF-8 +Content-Disposition: inline +Hello, +We have received a request to use this email address with a matrix.org identity +server. If this was you who made this request, you may use the following link +to complete the verification of your email address: +__link__ +If you aren't aware of making such a request, please disregard this email. +About Matrix: +Matrix is an open standard for interoperable, decentralised, real-time communication +over IP. It can be used to power Instant Messaging, VoIP/WebRTC signalling, Internet +of Things communication - or anywhere you need a standard HTTP API for publishing and +subscribing to data whilst tracking the conversation history. +Matrix defines the standard, and provides open source reference implementations of +Matrix-compatible Servers, Clients, Client SDKs and Application Services to help you +create new communication solutions or extend the capabilities and reach of existing ones. +--__multipart_boundary__ +Content-Type: text/html; charset=UTF-8 +Content-Disposition: inline + + + + + + + + + +

Hello,

+ +

We have received a request to use this email address with a matrix.org +identity server. If this was you who made this request, you may use the +following link to complete the verification of your email address:

+ +

Complete email verification

+ +

...or copy this link into your web browser:

+ +

__link__

+ +

If your client requires a code, the code is __token__

+ +

If you aren't aware of making such a request, please disregard this +email.

+ +
+

About Matrix:

+ +

Matrix is an open standard for interoperable, decentralised, real-time communication +over IP. It can be used to power Instant Messaging, VoIP/WebRTC signalling, Internet +of Things communication - or anywhere you need a standard HTTP API for publishing and +subscribing to data whilst tracking the conversation history.

+ +

Matrix defines the standard, and provides open source reference implementations of +Matrix-compatible Servers, Clients, Client SDKs and Application Services to help you +create new communication solutions or extend the capabilities and reach of existing ones.

+ + + + +--__multipart_boundary__-- diff --git a/packages/matrix-client-server/templates/smsVerification.tpl b/packages/matrix-client-server/templates/smsVerification.tpl new file mode 100644 index 00000000..c7b4cf6a --- /dev/null +++ b/packages/matrix-client-server/templates/smsVerification.tpl @@ -0,0 +1,10 @@ +Hello, + +We have received a request to use this phone number with a matrix.org identity server. If this was you, use the following link to complete the verification: + +__link__ + + +If you aren't aware of making such a request, please disregard this message. + +- Matrix \ No newline at end of file diff --git a/packages/matrix-client-server/tsconfig.json b/packages/matrix-client-server/tsconfig.json new file mode 100644 index 00000000..5ddb593d --- /dev/null +++ b/packages/matrix-client-server/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig-build.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src/**/*", "test/**/*"] +} diff --git a/packages/matrix-identity-server/README.md b/packages/matrix-identity-server/README.md index fe329e26..4ffa5564 100644 --- a/packages/matrix-identity-server/README.md +++ b/packages/matrix-identity-server/README.md @@ -23,12 +23,12 @@ const idServer = new IdServer(config) const app = express() -idServer.ready.then( () => { - Object.keys(idServer.api.get).forEach( k => { +idServer.ready.then(() => { + Object.keys(idServer.api.get).forEach((k) => { app.get(k, idServer.api.get[k]) }) - Object.keys(idServer.api.post).forEach( k => { + Object.keys(idServer.api.post).forEach((k) => { app.post(k, idServer.api.get[k]) }) diff --git a/packages/matrix-identity-server/matrix-server/.env b/packages/matrix-identity-server/matrix-server/.env new file mode 100644 index 00000000..46c2948d --- /dev/null +++ b/packages/matrix-identity-server/matrix-server/.env @@ -0,0 +1,2 @@ +export UID=${SUDO_UID:-$(id --user)} +export GID=${SUDO_GID:-$(id --group)} diff --git a/packages/matrix-identity-server/matrix-server/README.md b/packages/matrix-identity-server/matrix-server/README.md new file mode 100644 index 00000000..68315cb7 --- /dev/null +++ b/packages/matrix-identity-server/matrix-server/README.md @@ -0,0 +1,57 @@ +# Matrix-Synapse server for tests + +This repository launches a local Matrix server named "matrix.example.com". + +## Setup + +First, you need to include DNS names. To do this, just add this line into your +`/etc/hosts` file: + +``` +127.0.0.1 auth.example.com matrix.example.com tom.example.com +``` + +To initialize the server, simply launch [`./init`](./init) + +## Run server + +Just launch [`./run`](./run). + +To see logs: + +- SSO logs: `docker compose logs auth` +- Synapse logs are in `synapse-data/homeserver.log` + +The Matrix-Synapse server runs on https://matrix.example.com/ (API only), and the SSO on https://auth.example.com/ _(certificate invalid of course)_ + +### Available accounts + +This repo uses the "Demo" interface of [LemonLDAP::NG](https://lemonldap-ng.org/) +which provides 3 demon accounts: **dwho**, **rsmith** and **rtyler**. +Password is the login. + +## Test the server + +You can use any Matrix client, but to just test is server is up: + +- Download **llng** tool from [Simple OIDC client repo](https://github.com/linagora/simple-oidc-client) +- Launch the following command + +```shell +llng --llng-server auth.example.com --matrix-server matrix.example.com:443 \ + --login dwho --password dwho matrix_token +``` + +It will use the dwho account to authenticate, then propagate authenticate to Matrix via +[OIDC](https://openid.net/specs/openid-connect-core-1_0.html), then get a matrix `access_token` + +To get a federation `access_token`, reuse the result of previous command with `matrix_federation_token` subcommand: + +```shell +llng --llng-server auth.example.com --matrix-server matrix.example.com:443 --login dwho \ + --password dwho matrix_federation_token syt_ZHdobw_JswjzYCRQiPxhPJPAfbj_15jQrD +``` + +## Stop server + +Launch [`./stop`](./stop) or `docker-compose down` diff --git a/packages/matrix-identity-server/matrix-server/docker-compose.yml b/packages/matrix-identity-server/matrix-server/docker-compose.yml new file mode 100644 index 00000000..d9d2500e --- /dev/null +++ b/packages/matrix-identity-server/matrix-server/docker-compose.yml @@ -0,0 +1,40 @@ +version: '3.4' + +services: + synapse: + image: matrixdotorg/synapse:latest + container_name: synapse + hostname: matrix.example.com + volumes: + - ./synapse-data:/data + - ./ssl/ca-cert.pem:/etc/ssl/certs/ca-cert.pem + - ./ssl/9da13359.0:/etc/ssl/certs/9da13359.0 + environment: + - SYNAPSE_SERVER_NAME=matrix.example.com + - SYNAPSE_REPORT_STATS=no + - UID=${MYUID} + - GID=${MYGID} + depends_on: + auth: + condition: service_started + + auth: + image: yadd/lemonldap-ng-portal + container_name: auth + hostname: auth.example.com + volumes: + - ./lemon/lmConf-1.json:/var/lib/lemonldap-ng/conf/lmConf-1.json + - ./lemon/matrix-vhost.conf:/etc/nginx/sites-enabled/matrix.conf + - ./lemon/ssl.conf:/etc/nginx/sites-enabled/0000default.conf + - ./ssl:/etc/nginx/ssl + ports: + - 80:80 + - 443:443 + - 8008:8008 + - 8448:8448 + environment: + - SSODOMAIN=example.com + - PORTAL=https://auth.example.com + - LOGLEVEL=debug + - LOGGER=stderr + - USERLOGGER=stderr diff --git a/packages/matrix-identity-server/matrix-server/init b/packages/matrix-identity-server/matrix-server/init new file mode 100755 index 00000000..218e36b2 --- /dev/null +++ b/packages/matrix-identity-server/matrix-server/init @@ -0,0 +1,20 @@ +#!/bin/sh + +rm -rf synapse-data +cp -a synapse-ref synapse-data + +export MYUID=${SUDO_UID:-$(id -u)} +export MYGID=${SUDO_GID:-$(id -g)} + +docker-compose down +docker run -it --rm \ + -v `pwd`/synapse-data:/data \ + -e SYNAPSE_SERVER_NAME=matrix.example.com \ + -e SYNAPSE_REPORT_STATS=no \ + -e UID=$MYUID \ + -e GID=$MYGID \ + matrixdotorg/synapse:latest generate + +chmod 644 synapse-data/example.com.signing.key +mkdir -p synapse-data/media_store +touch /data/homeserver.log diff --git a/packages/matrix-identity-server/matrix-server/lemon/fluffychat.conf b/packages/matrix-identity-server/matrix-server/lemon/fluffychat.conf new file mode 100644 index 00000000..5b95a969 --- /dev/null +++ b/packages/matrix-identity-server/matrix-server/lemon/fluffychat.conf @@ -0,0 +1,9 @@ +server { + listen 80; + server_name fluffychat.example.com; + root /var/www/fluffychat; + index index.html; + location / { + try_files $uri $uri/ =404; + } +} diff --git a/packages/matrix-identity-server/matrix-server/lemon/lmConf-1.json b/packages/matrix-identity-server/matrix-server/lemon/lmConf-1.json new file mode 100644 index 00000000..64c447de --- /dev/null +++ b/packages/matrix-identity-server/matrix-server/lemon/lmConf-1.json @@ -0,0 +1,515 @@ +{ + "ADPwdExpireWarning": 0, + "ADPwdMaxAge": 0, + "SMTPServer": "", + "SMTPTLS": "", + "SSLAuthnLevel": 5, + "SSLIssuerVar": "SSL_CLIENT_I_DN", + "SSLVar": "SSL_CLIENT_S_DN_Email", + "SSLVarIf": {}, + "activeTimer": 1, + "apacheAuthnLevel": 3, + "applicationList": { + "1matrix": { + "catname": "Matrix clients", + "element": { + "options": { + "description": "Element web client", + "display": "auto", + "logo": "demo.png", + "name": "Element", + "uri": "https://element.example.com/" + }, + "type": "application" + }, + "fluffy": { + "options": { + "description": "FluffyChat web client", + "display": "auto", + "logo": "demo.png", + "name": "FluffyChat", + "uri": "https://fluffychat.example.com/" + }, + "type": "application" + }, + "type": "category" + } + }, + "authChoiceParam": "lmAuth", + "authentication": "Demo", + "available2F": "UTOTP,TOTP,U2F,REST,Mail2F,Ext2F,WebAuthn,Yubikey,Radius,Password", + "available2FSelfRegistration": "Password,TOTP,U2F,WebAuthn,Yubikey", + "bruteForceProtectionLockTimes": "15, 30, 60, 300, 600", + "bruteForceProtectionMaxAge": 300, + "bruteForceProtectionMaxFailed": 3, + "bruteForceProtectionMaxLockTime": 900, + "bruteForceProtectionTempo": 30, + "captcha_mail_enabled": 1, + "captcha_register_enabled": 1, + "captcha_size": 6, + "casAccessControlPolicy": "none", + "casAuthnLevel": 1, + "casTicketExpiration": 0, + "certificateResetByMailCeaAttribute": "description", + "certificateResetByMailCertificateAttribute": "userCertificate;binary", + "certificateResetByMailURL": "https://auth.example.com/certificateReset", + "certificateResetByMailValidityDelay": 0, + "cfgAuthor": "The LemonLDAP::NG team", + "cfgDate": "1627287638", + "cfgNum": "1", + "cfgVersion": "2.0.16", + "checkDevOpsCheckSessionAttributes": 1, + "checkDevOpsDisplayNormalizedHeaders": 1, + "checkDevOpsDownload": 1, + "checkHIBPRequired": 1, + "checkHIBPURL": "https://api.pwnedpasswords.com/range/", + "checkTime": 600, + "checkUserDisplayComputedSession": 1, + "checkUserDisplayEmptyHeaders": 0, + "checkUserDisplayEmptyValues": 0, + "checkUserDisplayHiddenAttributes": 0, + "checkUserDisplayHistory": 0, + "checkUserDisplayNormalizedHeaders": 0, + "checkUserDisplayPersistentInfo": 0, + "checkUserHiddenAttributes": "_loginHistory, _session_id, hGroups", + "checkUserIdRule": 1, + "checkXSS": 1, + "confirmFormMethod": "post", + "contextSwitchingIdRule": 1, + "contextSwitchingPrefix": "switching", + "contextSwitchingRule": 0, + "contextSwitchingStopWithLogout": 1, + "cookieName": "lemonldap", + "corsAllow_Credentials": "true", + "corsAllow_Headers": "*", + "corsAllow_Methods": "POST,GET", + "corsAllow_Origin": "*", + "corsEnabled": 1, + "corsExpose_Headers": "*", + "corsMax_Age": "86400", + "crowdsecAction": "reject", + "cspConnect": "'self'", + "cspDefault": "'self'", + "cspFont": "'self'", + "cspFormAction": "*", + "cspFrameAncestors": "", + "cspImg": "'self' data:", + "cspScript": "'self'", + "cspStyle": "'self'", + "dbiAuthnLevel": 2, + "dbiExportedVars": {}, + "decryptValueRule": 0, + "defaultNewKeySize": 2048, + "demoExportedVars": { + "cn": "cn", + "mail": "mail", + "uid": "uid" + }, + "displaySessionId": 1, + "domain": "example.com", + "exportedHeaders": {}, + "exportedVars": {}, + "ext2fActivation": 0, + "ext2fCodeActivation": "\\d{6}", + "facebookAuthnLevel": 1, + "facebookExportedVars": {}, + "facebookUserField": "id", + "failedLoginNumber": 5, + "findUserControl": "^[*\\w]+$", + "findUserWildcard": "*", + "formTimeout": 120, + "githubAuthnLevel": 1, + "githubScope": "user:email", + "githubUserField": "login", + "globalLogoutRule": 0, + "globalLogoutTimer": 1, + "globalStorage": "Apache::Session::File", + "globalStorageOptions": { + "Directory": "/var/lib/lemonldap-ng/sessions", + "LockDirectory": "/var/lib/lemonldap-ng/sessions/lock", + "generateModule": "Lemonldap::NG::Common::Apache::Session::Generate::SHA256" + }, + "gpgAuthnLevel": 5, + "gpgDb": "", + "grantSessionRules": {}, + "groups": {}, + "handlerInternalCache": 15, + "handlerServiceTokenTTL": 30, + "hiddenAttributes": "_password, _2fDevices", + "httpOnly": 1, + "https": -1, + "impersonationHiddenAttributes": "_2fDevices, _loginHistory", + "impersonationIdRule": 1, + "impersonationMergeSSOgroups": 0, + "impersonationPrefix": "real_", + "impersonationRule": 0, + "impersonationSkipEmptyValues": 1, + "infoFormMethod": "get", + "issuerDBCASPath": "^/cas/", + "issuerDBCASRule": 1, + "issuerDBGetParameters": {}, + "issuerDBGetPath": "^/get/", + "issuerDBGetRule": 1, + "issuerDBJitsiMeetTokensPath": "^/jitsi/", + "issuerDBJitsiMeetTokensRule": 1, + "issuerDBOpenIDConnectActivation": 1, + "issuerDBOpenIDConnectPath": "^/oauth2/", + "issuerDBOpenIDConnectRule": 1, + "issuerDBOpenIDPath": "^/openidserver/", + "issuerDBOpenIDRule": 1, + "issuerDBSAMLPath": "^/saml/", + "issuerDBSAMLRule": 1, + "issuersTimeout": 120, + "jitsiExpiration": "300", + "jitsiSigningAlg": "RS256", + "jsRedirect": 0, + "key": "^vmTGvh{+]5!ToB?", + "krbAuthnLevel": 3, + "krbRemoveDomain": 1, + "ldapAuthnLevel": 2, + "ldapBase": "dc=example,dc=com", + "ldapExportedVars": { + "cn": "cn", + "mail": "mail", + "uid": "uid" + }, + "ldapGroupAttributeName": "member", + "ldapGroupAttributeNameGroup": "dn", + "ldapGroupAttributeNameSearch": "cn", + "ldapGroupAttributeNameUser": "dn", + "ldapGroupObjectClass": "groupOfNames", + "ldapIOTimeout": 10, + "ldapPasswordResetAttribute": "pwdReset", + "ldapPasswordResetAttributeValue": "TRUE", + "ldapPwdEnc": "utf-8", + "ldapSearchDeref": "find", + "ldapServer": "ldap://localhost", + "ldapTimeout": 10, + "ldapUsePasswordResetAttribute": 1, + "ldapVerify": "require", + "ldapVersion": 3, + "linkedInAuthnLevel": 1, + "linkedInFields": "id,first-name,last-name,email-address", + "linkedInScope": "r_liteprofile r_emailaddress", + "linkedInUserField": "emailAddress", + "localSessionStorage": "Cache::FileCache", + "localSessionStorageOptions": { + "cache_depth": 3, + "cache_root": "/var/lib/lemonldap-ng/cache", + "default_expires_in": 600, + "directory_umask": "007", + "namespace": "lemonldap-ng-sessions" + }, + "locationDetectGeoIpLanguages": "en, fr", + "locationRules": { + "auth.example.com": { + "(?#checkUser)^/checkuser": "inGroup(\"timelords\")", + "(?#errors)^/lmerror/": "accept", + "default": "accept" + }, + "element.example.com": { + "default": "accept" + }, + "fluffychat.example.com": { + "default": "accept" + }, + "manager.example.com": { + "(?#Configuration)^/(.*?\\.(fcgi|psgi)/)?(manager\\.html|confs|prx/|$)": "inGroup(\"timelords\")", + "(?#Notifications)/(.*?\\.(fcgi|psgi)/)?notifications": "inGroup(\"timelords\") or $uid eq \"rtyler\"", + "(?#Sessions)/(.*?\\.(fcgi|psgi)/)?sessions": "inGroup(\"timelords\") or $uid eq \"rtyler\"", + "default": "inGroup(\"timelords\") or $uid eq \"rtyler\"" + } + }, + "loginHistoryEnabled": 1, + "logoutServices": {}, + "lwpOpts": { + "timeout": 10 + }, + "macros": { + "UA": "$ENV{HTTP_USER_AGENT}", + "_whatToTrace": "$_auth eq 'SAML' ? lc($_user.'@'.$_idpConfKey) : $_auth eq 'OpenIDConnect' ? lc($_user.'@'.$_oidc_OP) : lc($_user)" + }, + "mail2fActivation": 0, + "mail2fCodeRegex": "\\d{6}", + "mailCharset": "utf-8", + "mailFrom": "noreply@example.com", + "mailSessionKey": "mail", + "mailTimeout": 0, + "mailUrl": "https://auth.example.com/resetpwd", + "managerDn": "", + "managerPassword": "", + "max2FDevices": 10, + "max2FDevicesNameLength": 20, + "multiValuesSeparator": "; ", + "mySessionAuthorizedRWKeys": [ + "_appsListOrder", + "_oidcConnectedRP", + "_oidcConsents" + ], + "newLocationWarningLocationAttribute": "ipAddr", + "newLocationWarningLocationDisplayAttribute": "", + "newLocationWarningMaxValues": "0", + "notification": 0, + "notificationDefaultCond": "", + "notificationServerPOST": 1, + "notificationServerSentAttributes": "uid reference date title subtitle text check", + "notificationStorage": "File", + "notificationStorageOptions": { + "dirName": "/var/lib/lemonldap-ng/notifications" + }, + "notificationWildcard": "allusers", + "notificationsMaxRetrieve": 3, + "notifyDeleted": 1, + "nullAuthnLevel": 0, + "oidcAuthnLevel": 1, + "oidcOPMetaDataExportedVars": {}, + "oidcOPMetaDataJSON": {}, + "oidcOPMetaDataJWKS": {}, + "oidcOPMetaDataOptions": {}, + "oidcRPCallbackGetParam": "openidconnectcallback", + "oidcRPMetaDataExportedVars": { + "matrix": { + "email": "mail", + "family_name": "cn", + "given_name": "cn", + "name": "cn", + "nickname": "uid", + "preferred_username": "uid" + } + }, + "oidcRPMetaDataMacros": null, + "oidcRPMetaDataOptions": { + "matrix": { + "oidcRPMetaDataOptionsAccessTokenClaims": 0, + "oidcRPMetaDataOptionsAccessTokenJWT": 0, + "oidcRPMetaDataOptionsAccessTokenSignAlg": "RS256", + "oidcRPMetaDataOptionsAllowClientCredentialsGrant": 0, + "oidcRPMetaDataOptionsAllowOffline": 0, + "oidcRPMetaDataOptionsAllowPasswordGrant": 0, + "oidcRPMetaDataOptionsBypassConsent": 1, + "oidcRPMetaDataOptionsClientID": "matrix1", + "oidcRPMetaDataOptionsClientSecret": "matrix1", + "oidcRPMetaDataOptionsIDTokenForceClaims": 0, + "oidcRPMetaDataOptionsIDTokenSignAlg": "RS256", + "oidcRPMetaDataOptionsLogoutBypassConfirm": 0, + "oidcRPMetaDataOptionsLogoutSessionRequired": 1, + "oidcRPMetaDataOptionsLogoutType": "back", + "oidcRPMetaDataOptionsLogoutUrl": "https://matrix.example.com/_synapse/client/oidc/backchannel_logout", + "oidcRPMetaDataOptionsPublic": 0, + "oidcRPMetaDataOptionsRedirectUris": "https://matrix.example.com/_synapse/client/oidc/callback", + "oidcRPMetaDataOptionsRefreshToken": 0, + "oidcRPMetaDataOptionsRequirePKCE": 0, + "oidcRPMetaDataOptionsTokenXAuthorizedMatrix": "example.com" + } + }, + "oidcRPMetaDataOptionsExtraClaims": null, + "oidcRPMetaDataScopeRules": null, + "oidcRPStateTimeout": 600, + "oidcServiceAccessTokenExpiration": 3600, + "oidcServiceAllowAuthorizationCodeFlow": 1, + "oidcServiceAllowImplicitFlow": 0, + "oidcServiceAuthorizationCodeExpiration": 60, + "oidcServiceDynamicRegistrationExportedVars": {}, + "oidcServiceDynamicRegistrationExtraClaims": {}, + "oidcServiceEncAlgorithmAlg": "RSA-OAEP", + "oidcServiceEncAlgorithmEnc": "A256GCM", + "oidcServiceIDTokenExpiration": 3600, + "oidcServiceIgnoreScopeForClaims": 1, + "oidcServiceKeyIdSig": "oMGHInscAW3Nsa0FcnCnDA", + "oidcServiceKeyTypeEnc": "RSA", + "oidcServiceKeyTypeSig": "RSA", + "oidcServiceMetaDataAuthnContext": { + "loa-1": 1, + "loa-2": 2, + "loa-3": 3, + "loa-4": 4, + "loa-5": 5 + }, + "oidcServiceMetaDataAuthorizeURI": "authorize", + "oidcServiceMetaDataBackChannelURI": "blogout", + "oidcServiceMetaDataCheckSessionURI": "checksession.html", + "oidcServiceMetaDataEndSessionURI": "logout", + "oidcServiceMetaDataFrontChannelURI": "flogout", + "oidcServiceMetaDataIntrospectionURI": "introspect", + "oidcServiceMetaDataJWKSURI": "jwks", + "oidcServiceMetaDataRegistrationURI": "register", + "oidcServiceMetaDataTokenURI": "token", + "oidcServiceMetaDataUserInfoURI": "userinfo", + "oidcServiceNewKeyTypeSig": "RSA", + "oidcServiceOfflineSessionExpiration": 2592000, + "oidcServiceOldKeyTypeEnc": "RSA", + "oidcServiceOldKeyTypeSig": "RSA", + "oidcServicePrivateKeySig": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDywteBzIOlhKc4\nO+vhMStDYOpPYrWDOodkUZ7OsxlWVNZ/b/lqIFS56+MHPkKNQuT4zZCyO8bEKmmR\nZ6kPFJoGbO1zJCPQ/RKjimX4J/5gDb1BAlo+6agJi55e3Bw0zKNJDU0mRyedcIzW\n7ywTgyj6B35pl/Sfloi4Q1XEizHar+26h66SOEtnppMxGvwsxO8gFWz26CPmalvY\n5GNYR0txbXUZn7I4kDa4mMWgNfeocWc78Qbt4RV5EuQdbRh1sou4tL9Nn4EuGhg0\nmfsSI0xVAj7f82Wn3kW6qEbhuejrY7aqmZjN7yrMKtCBuV7o4hVrjYLuM2j0mInY\nMy5nRNOVAgMBAAECggEAJ145nK8R2lG83H27LvXOUkrxNJaJYRKoyjgCTPr2bO2t\nK1V5WSCNHOmIE7ChEk962m5bvMu83CsUm6P34p4wrEIV78o4lLe1whe7mZbCxcj0\nnApJoFI8EfA2aqO/X0CgakRh8ocvgXSzIlf/CdsHViTI907ROOAso9Unn4wDNbdp\nMrhi3H2SnA+ewzj85WygBVTNQmVBjJSSLXTQRkfHye0ztvQm59gqqaJaM2rkBjvA\nlPWAVsgakOk4pgClKElCsIjWPJwdYtcd8VJrwnro5J9KhMwB//AArGgqOaXUHnLH\nv5aZZp6FjV/M3BxbSp4cG6hXmK1hrDFLecRddYP1gQKBgQD+Y4/ee57Z0E2V8833\nYfrK3F23sfxmZ7zUwEbgFXUfRy3RVW7Hbc7PAJzxzrk+LYk/zaZrrfEJguqG2O6m\nVNYkqxKu69Nn964CMdV15JGxVzpzsN5adKlcvKVVv9gx2rF3SMUOHiRutj2BlUtO\niCq0G3jFsXWIRzePig9PbWP6CQKBgQD0TG2DeDDUgKbeJYIzXfmCvGxlm5MZqCc/\nK7d8P9U0svG//jJRTsa9hcLjk7N24CzhLNHyJmT7dh1Xy1oLyHNPZ4nQRmCe+HUf\nu0SK10WZ2K55ekUmqS+xSuDFWJtWa5SE46cKg0fKu7YkiDKI1s6I3qrF4lew2aDE\n2p8GJRrgLQKBgCh2PZPtpb6PW0fWl5QZiYJqup1VOggvx+EvFBbgUti+wZLiO9SM\nqrBSMKRldSFmrMXxN984s3YH1LXOG2dpZwY+D6Ky79VBl/PRaVpvGJ1Uen+cSkGo\n/Kc7ejDBaunDFycZ8/3i3Xiek/ngfTHohqJPHE6Vg1RBv5ydIQJJK/XBAoGAU1XO\n9c4GOjc4tQbuhz9DYgmMoIyVfWcTHEV5bfUIcdWpCelYmMval8QNWzyDN8X5CUcU\nxxm50N3V3KENsn9KdofHRzj6tL/klFJ5azNMFtMHkYDYHfwQvNXiHu++7Zf9LefK\nj5eA4fNuir+7HVrJUX9DmgVADJ/wa7Z4EMyPgnECgYA/NLUs4920h10ie5lFffpM\nqq6CRcBjsQ7eGK9UI1Z2KZUh94eqIENSJ7whBjXKvJJvhAlH4//lVFMMRs7oJePY\nThg+8In7PB64yMOIJZLc5Fekn9aGG6YtErPzePQkXSYCKZxWl5EpjQZGgPRVkNtD\n2nflyJLjiCbTjeNgWIOZlw==\n-----END PRIVATE KEY-----\n", + "oidcServicePublicKeySig": "-----BEGIN CERTIFICATE-----\nMIICuDCCAaCgAwIBAgIEFU77HjANBgkqhkiG9w0BAQsFADAeMRwwGgYDVQQDDBNt\nYXRyaXgubGluYWdvcmEuY29tMB4XDTIzMDIxNTAzMTk0NloXDTQzMDIxMDAzMTk0\nNlowHjEcMBoGA1UEAwwTbWF0cml4LmxpbmFnb3JhLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBAPLC14HMg6WEpzg76+ExK0Ng6k9itYM6h2RRns6z\nGVZU1n9v+WogVLnr4wc+Qo1C5PjNkLI7xsQqaZFnqQ8UmgZs7XMkI9D9EqOKZfgn\n/mANvUECWj7pqAmLnl7cHDTMo0kNTSZHJ51wjNbvLBODKPoHfmmX9J+WiLhDVcSL\nMdqv7bqHrpI4S2emkzEa/CzE7yAVbPboI+ZqW9jkY1hHS3FtdRmfsjiQNriYxaA1\n96hxZzvxBu3hFXkS5B1tGHWyi7i0v02fgS4aGDSZ+xIjTFUCPt/zZafeRbqoRuG5\n6OtjtqqZmM3vKswq0IG5XujiFWuNgu4zaPSYidgzLmdE05UCAwEAATANBgkqhkiG\n9w0BAQsFAAOCAQEArNmGxZVvmvdOLctv+zQ+npzQtOTaJcf+r/1xYuM4FZVe4yLc\ny9ElDskoDWjvQU7jKeJeaDOYgMJQNrek8Doj8uHPWNe6jYFa62Csg9aPz6e8qbtq\nWI+sXds5GJd6xZ8mi2L4MdT/tf8dBgcgybuoRyhBtJwG1rLNAYkeXMxkBzOFcU7K\nR/SZ0q9ToLAWFDhn42MTjPN3t6GwKDzGNsM/SI/3WvUwpQbtK91hjPnNDwKiAtGG\nfUteuigfXY+0hEcQwJdR0St/FQ8UYYcAB5YT9IkT1wCcU5LfPHCBf3OXNpbnQsHh\netQMKLibM6wWdXNwmsd1szO66ft3QZ4h4EG3Vw==\n-----END CERTIFICATE-----\n", + "oidcStorageOptions": {}, + "okta2fActivation": 0, + "openIdAuthnLevel": 1, + "openIdExportedVars": {}, + "openIdIDPList": "0;", + "openIdSPList": "0;", + "openIdSreg_email": "mail", + "openIdSreg_fullname": "cn", + "openIdSreg_nickname": "uid", + "openIdSreg_timezone": "_timezone", + "pamAuthnLevel": 2, + "pamService": "login", + "password2fActivation": 0, + "password2fSelfRegistration": 0, + "password2fUserCanRemoveKey": 1, + "passwordDB": "Demo", + "passwordPolicyActivation": 1, + "passwordPolicyMinDigit": 0, + "passwordPolicyMinLower": 0, + "passwordPolicyMinSize": 0, + "passwordPolicyMinSpeChar": 0, + "passwordPolicyMinUpper": 0, + "passwordPolicySpecialChar": "__ALL__", + "passwordResetAllowedRetries": 3, + "persistentSessionAttributes": "_loginHistory _2fDevices notification_", + "persistentStorage": "Apache::Session::File", + "persistentStorageOptions": { + "Directory": "/var/lib/lemonldap-ng/psessions", + "LockDirectory": "/var/lib/lemonldap-ng/psessions/lock" + }, + "port": -1, + "portal": "https://auth.example.com/", + "portalAntiFrame": 1, + "portalCheckLogins": 1, + "portalDisplayAppslist": 1, + "portalDisplayChangePassword": "$_auth =~ /^(LDAP|DBI|Demo)$/", + "portalDisplayGeneratePassword": 1, + "portalDisplayLoginHistory": 1, + "portalDisplayLogout": 1, + "portalDisplayOidcConsents": "$_oidcConsents && $_oidcConsents =~ /\\w+/", + "portalDisplayOrder": "Appslist ChangePassword LoginHistory OidcConsents Logout", + "portalDisplayRefreshMyRights": 1, + "portalDisplayRegister": 1, + "portalErrorOnExpiredSession": 1, + "portalFavicon": "common/favicon.ico", + "portalForceAuthnInterval": 5, + "portalMainLogo": "common/logos/logo_llng_400px.png", + "portalPingInterval": 60000, + "portalRequireOldPassword": 1, + "portalSkin": "bootstrap", + "portalSkinBackground": "1280px-Cedar_Breaks_National_Monument_partially.jpg", + "portalUserAttr": "_user", + "proxyAuthServiceChoiceParam": "lmAuth", + "proxyAuthnLevel": 2, + "radius2fActivation": 0, + "radius2fRequestAttributes": {}, + "radius2fTimeout": 20, + "radiusAuthnLevel": 3, + "radiusExportedVars": {}, + "radiusRequestAttributes": {}, + "randomPasswordRegexp": "[A-Z]{3}[a-z]{5}.\\d{2}", + "redirectFormMethod": "get", + "registerDB": "Null", + "registerTimeout": 0, + "registerUrl": "https://auth.example.com/register", + "reloadTimeout": 5, + "reloadUrls": { + "localhost": "https://reload.example.com/reload" + }, + "rememberAuthChoiceRule": 0, + "rememberCookieName": "llngrememberauthchoice", + "rememberCookieTimeout": 31536000, + "rememberTimer": 5, + "remoteGlobalStorage": "Lemonldap::NG::Common::Apache::Session::SOAP", + "remoteGlobalStorageOptions": { + "ns": "https://auth.example.com/Lemonldap/NG/Common/PSGI/SOAPService", + "proxy": "https://auth.example.com/sessions" + }, + "requireToken": 1, + "rest2fActivation": 0, + "restAuthnLevel": 2, + "restClockTolerance": 15, + "sameSite": "", + "samlAttributeAuthorityDescriptorAttributeServiceSOAP": "urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/AA/SOAP;", + "samlAuthnContextMapKerberos": 4, + "samlAuthnContextMapPassword": 2, + "samlAuthnContextMapPasswordProtectedTransport": 3, + "samlAuthnContextMapTLSClient": 5, + "samlEntityID": "#PORTAL#/saml/metadata", + "samlIDPSSODescriptorArtifactResolutionServiceArtifact": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/artifact", + "samlIDPSSODescriptorSingleLogoutServiceHTTPPost": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/singleLogout;#PORTAL#/saml/singleLogoutReturn", + "samlIDPSSODescriptorSingleLogoutServiceHTTPRedirect": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect;#PORTAL#/saml/singleLogout;#PORTAL#/saml/singleLogoutReturn", + "samlIDPSSODescriptorSingleLogoutServiceSOAP": "urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/singleLogoutSOAP;", + "samlIDPSSODescriptorSingleSignOnServiceHTTPArtifact": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/singleSignOnArtifact;", + "samlIDPSSODescriptorSingleSignOnServiceHTTPPost": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/singleSignOn;", + "samlIDPSSODescriptorSingleSignOnServiceHTTPRedirect": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect;#PORTAL#/saml/singleSignOn;", + "samlIDPSSODescriptorWantAuthnRequestsSigned": 1, + "samlMetadataForceUTF8": 1, + "samlNameIDFormatMapEmail": "mail", + "samlNameIDFormatMapKerberos": "uid", + "samlNameIDFormatMapWindows": "uid", + "samlNameIDFormatMapX509": "mail", + "samlOrganizationDisplayName": "Example", + "samlOrganizationName": "Example", + "samlOrganizationURL": "https://www.example.com", + "samlOverrideIDPEntityID": "", + "samlRelayStateTimeout": 600, + "samlSPSSODescriptorArtifactResolutionServiceArtifact": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/artifact", + "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact": "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact", + "samlSPSSODescriptorAssertionConsumerServiceHTTPPost": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost", + "samlSPSSODescriptorAuthnRequestsSigned": 1, + "samlSPSSODescriptorSingleLogoutServiceHTTPPost": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleLogout;#PORTAL#/saml/proxySingleLogoutReturn", + "samlSPSSODescriptorSingleLogoutServiceHTTPRedirect": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect;#PORTAL#/saml/proxySingleLogout;#PORTAL#/saml/proxySingleLogoutReturn", + "samlSPSSODescriptorSingleLogoutServiceSOAP": "urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/proxySingleLogoutSOAP;", + "samlSPSSODescriptorWantAssertionsSigned": 1, + "samlServiceSignatureMethod": "RSA_SHA256", + "scrollTop": 400, + "securedCookie": 0, + "sessionDataToRemember": {}, + "sfEngine": "::2F::Engines::Default", + "sfManagerRule": 1, + "sfRemovedMsgRule": 0, + "sfRemovedNotifMsg": "_removedSF_ expired second factor(s) has/have been removed (_nameSF_)!", + "sfRemovedNotifRef": "RemoveSF", + "sfRemovedNotifTitle": "Second factor notification", + "sfRequired": 0, + "showLanguages": 1, + "singleIP": 0, + "singleSession": 0, + "singleUserByIP": 0, + "slaveAuthnLevel": 2, + "slaveExportedVars": {}, + "soapProxyUrn": "urn:Lemonldap/NG/Common/PSGI/SOAPService", + "stayConnected": 0, + "stayConnectedCookieName": "llngconnection", + "stayConnectedTimeout": 2592000, + "successLoginNumber": 5, + "timeout": 72000, + "timeoutActivity": 0, + "timeoutActivityInterval": 60, + "totp2fActivation": 0, + "totp2fDigits": 6, + "totp2fInterval": 30, + "totp2fRange": 1, + "totp2fSelfRegistration": 0, + "totp2fUserCanRemoveKey": 1, + "trustedBrowserRule": 0, + "twitterAuthnLevel": 1, + "twitterUserField": "screen_name", + "u2fActivation": 0, + "u2fSelfRegistration": 0, + "u2fUserCanRemoveKey": 1, + "upgradeSession": 1, + "useRedirectOnError": 1, + "useSafeJail": 1, + "userControl": "^[\\w\\.\\-@]+$", + "userDB": "Same", + "utotp2fActivation": 0, + "viewerHiddenKeys": "samlIDPMetaDataNodes, samlSPMetaDataNodes", + "webIDAuthnLevel": 1, + "webIDExportedVars": {}, + "webauthn2fActivation": 0, + "webauthn2fAttestation": "none", + "webauthn2fSelfRegistration": 0, + "webauthn2fUserCanRemoveKey": 1, + "webauthn2fUserVerification": "preferred", + "whatToTrace": "_whatToTrace", + "yubikey2fActivation": 0, + "yubikey2fPublicIDSize": 12, + "yubikey2fSelfRegistration": 0, + "yubikey2fUserCanRemoveKey": 1 +} diff --git a/packages/matrix-identity-server/matrix-server/lemon/matrix-vhost.conf b/packages/matrix-identity-server/matrix-server/lemon/matrix-vhost.conf new file mode 100644 index 00000000..7b28564a --- /dev/null +++ b/packages/matrix-identity-server/matrix-server/lemon/matrix-vhost.conf @@ -0,0 +1,12 @@ +server { + listen 80; + listen 8008; + server_name matrix.example.com; + location / { + proxy_pass http://matrix.example.com:8008/; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $host; + proxy_redirect off; + } +} diff --git a/packages/matrix-identity-server/matrix-server/lemon/openid-configuration.json b/packages/matrix-identity-server/matrix-server/lemon/openid-configuration.json new file mode 100644 index 00000000..4db4fcb0 --- /dev/null +++ b/packages/matrix-identity-server/matrix-server/lemon/openid-configuration.json @@ -0,0 +1,50 @@ +{ + "acr_values_supported": ["loa-5", "loa-3", "loa-1", "loa-4", "loa-2"], + "authorization_endpoint": "https://auth.example.com/oauth2/authorize", + "backchannel_logout_session_supported": true, + "backchannel_logout_supported": true, + "claims_supported": ["sub", "iss", "auth_time", "acr", "sid"], + "code_challenge_methods_supported": ["plain", "S256"], + "end_session_endpoint": "https://auth.example.com/oauth2/logout", + "frontchannel_logout_session_supported": true, + "frontchannel_logout_supported": true, + "grant_types_supported": ["authorization_code"], + "id_token_signing_alg_values_supported": [ + "none", + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512" + ], + "introspection_endpoint": "https://auth.example.com/oauth2/introspect", + "introspection_endpoint_auth_methods_supported": [ + "client_secret_post", + "client_secret_basic" + ], + "issuer": "https://auth.example.com", + "jwks_uri": "https://auth.example.com/oauth2/jwks", + "request_parameter_supported": true, + "request_uri_parameter_supported": true, + "require_request_uri_registration": false, + "response_modes_supported": ["query", "fragment", "form_post"], + "response_types_supported": ["code"], + "scopes_supported": ["openid", "profile", "email", "address", "phone"], + "subject_types_supported": ["public"], + "token_endpoint": "https://auth.example.com/oauth2/token", + "token_endpoint_auth_methods_supported": [ + "client_secret_post", + "client_secret_basic" + ], + "userinfo_endpoint": "https://auth.example.com/oauth2/userinfo", + "userinfo_signing_alg_values_supported": [ + "none", + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512" + ] +} diff --git a/packages/matrix-identity-server/matrix-server/lemon/ssl.conf b/packages/matrix-identity-server/matrix-server/lemon/ssl.conf new file mode 100644 index 00000000..db5839bc --- /dev/null +++ b/packages/matrix-identity-server/matrix-server/lemon/ssl.conf @@ -0,0 +1,39 @@ +server { + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + ssl_certificate /etc/nginx/ssl/server.pem; + ssl_certificate_key /etc/nginx/ssl/server.key; + server_name _; + location / { + proxy_pass http://localhost:80/; + proxy_redirect off; + proxy_set_header Host $host; + } +} + +server { + listen 443; + listen 8448; + ssl_certificate /etc/nginx/ssl/server.pem; + ssl_certificate_key /etc/nginx/ssl/server.key; + server_name matrix.example.com; + location / { + proxy_pass http://matrix.example.com:8008/; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $host; + proxy_http_version 1.1; + } +} + +server { + listen 443; + ssl_certificate /etc/nginx/ssl/server.pem; + ssl_certificate_key /etc/nginx/ssl/server.key; + server_name fluffychat.example.com; + root /var/www/fluffychat; + index index.html; + location / { + try_files $uri $uri/ =404; + } +} diff --git a/packages/matrix-identity-server/matrix-server/llng b/packages/matrix-identity-server/matrix-server/llng new file mode 100755 index 00000000..01ed5895 --- /dev/null +++ b/packages/matrix-identity-server/matrix-server/llng @@ -0,0 +1,190 @@ +#!/bin/sh +# + +# +# Authors : P Vilarem +# X Guimard +# +# Licence : GPL V3 https://www.gnu.org/licenses/gpl-3.0.en.html + +VERSION='0.1.0' + +SIMPLEOIDCCLIENTLIBDIR=${SIMPLEOIDCCLIENTLIBDIR:-$(dirname $(test -L "$0" && readlink "$0" || echo "$0"))} + +if [ ! -f $SIMPLEOIDCCLIENTLIBDIR/llng-lib.sh ] ; then + SIMPLEOIDCCLIENTLIBDIR=$(dirname $0)/"$SIMPLEOIDCCLIENTLIBDIR" +fi + +. $SIMPLEOIDCCLIENTLIBDIR/llng-lib.sh + +check_install + +# 1. GET PARAMETERS +# TEMP=$(getopt -o 'c:u:Pp:H:ki:s:r:t:z:vh' "$@") +# echo $TEMP +if [ $? -ne 0 ]; then + echo 'Terminating...' >&2 + exit 1 +fi + +# eval set -- "$TEMP" +# echo $TEMP + +# unset TEMP +# echo $TEMP + +usage () { + echo 'LemonLDAP::NG OpenID-Connect client' + echo + echo "$0 " + echo + echo 'See https://github.com/linagora/simple-oidc-client/tree/master/sh#readme' +} +# echo "parametre 1bis = $@" + +while true; do + case "$1" in + '-v'|'--version') + echo $VERSION + exit + ;; + '-h'|'--help') + usage + exit + ;; + '-c'|'--cookie-jar') + COOKIEJAR="$2" + shift 2 + ;; + '-u'|'--login'|'--user') + LLNG_LOGIN="$2" + shift 2 + ;; + '-P'|'--prompt') + PROMPT=yes + shift + ;; + '-p'|'--password'|'--llng-password') + LLNG_PASSWORD="$2" + shift 2 + ;; + '-l'|'--llng-server') + LLNG_SERVER="$2" + shift 2 + ;; + '-H'|'--llng-url') + LLNG_URL="$2" + shift 2 + ;; + '-k'|'--pkce') + PKCE=1 + shift + ;; + '-i'|'--client-id') + CLIENT_ID="$2" + shift 2 + ;; + '-s'|'--client-secret') + CLIENT_SECRET="$2" + shift 2 + ;; + '-r'|'--redirect-uri') + REDIRECT_URI="$2" + shift 2 + ;; + '-o'|'--scope') + SCOPE="$2" + shift 2 + ;; + '-t' | '--matrix-server') + MATRIX_SERVER="$2" + shift 2 + ;; + '-z' | '--matrix-user') + MATRIX_USER="$2" + shift 2 + ;; + '--') + shift + break + ;; + *) + echo "Unknown option $1" >&2 + usage + exit 1 + break + ;; + ?) + echo Aborting >&2 + exit 1 + ;; + esac +done + +# echo "parametre 1 = $1" + +if test "$LLNG_SERVER" = "" -a "$LLNG_URL" = "" +then + LLNG_SERVER=$(askString Server) +fi + +if test "$LLNG_URL" = "" +then + LLNG_URL=$(build_llng_url) +fi + +COMMAND="$1" +if test "$COMMAND" != ""; then + shift +fi +# echo "tous les param : $@" + +case "$COMMAND" in + whoami) + whoami + ;; + languages) + getLanguages + ;; + llng_cookie) + getLlngId + ;; + oidc_metadata) + getOidcMetadata + ;; + oidc_endpoints) + getOidcEndpoints + env|grep _ENDPOINT + ;; + oidc_tokens) + getOidcTokens + ;; + access_token) + getAccessToken + ;; + id_token) + getIdToken + ;; + refresh_token) + getRefreshToken + ;; + user_info) + getUserInfo + ;; + introspection) + getIntrospection "$@" + ;; + matrix_token) + getMatrixToken "$@" + ;; + matrix_federation_token) + getMatrixFederationToken "$@" + ;; + matrix_token_exchange) + getAccessTokenFromMatrixToken "$@" + ;; + *) + echo "BAD COMMAND $COMMAND" >&2 + echo "Accepted commands: whoami, access_token, id_token, refresh_token" >&2 + exit 1 +esac diff --git a/packages/matrix-identity-server/matrix-server/llng-lib.sh b/packages/matrix-identity-server/matrix-server/llng-lib.sh new file mode 100644 index 00000000..72df4675 --- /dev/null +++ b/packages/matrix-identity-server/matrix-server/llng-lib.sh @@ -0,0 +1,351 @@ +#!/bin/sh + +# LemonLDAP::NG libraries + +askString () { + _READ='' + while [ "$_READ" = "" ] + do + read -p "$1: " _READ + #if test "$_READ" != ""; then + # echo OK + # break + #fi + done + + echo $_READ +} + +# Default values, overriden by options + +COOKIEJAR=~/.cache/llng-cookies +PROMPT=no +LLNG_SERVER="auth.example.com:19876" +PKCE=0 +SCOPE='openid email profile' + +# CURL clients + +# ERREUR ICI ! +client () { + umask 0077 + curl -sk --user-agent 'LLNG-CLient/2.20.0' --cookie "$COOKIEJAR" \ + --cookie-jar "$COOKIEJAR" -H "Accept: application/json" "$@" +} + +clientWeb () { + umask 0077 + curl -sk --user-agent 'LLNG-CLient/2.20.0' --cookie "$COOKIEJAR" \ + --cookie-jar "$COOKIEJAR" -H "Accept: test/html" "$@" +} + +uri_escape () { + perl -MURI::Escape -e '$_=uri_escape($ARGV[0]);s/(?:\s|%20)+/+/g;print' "$1" +} + +_authz () { + if test "$CLIENT_ID" = "" ; then + CLIENT_ID=$(askString 'Client ID') + fi + if test "$CLIENT_SECRET" != ""; then + echo "--basic -u $CLIENT_ID:$CLIENT_SECRET" + fi +} + +check_install () { + for _tool in jq openssl curl base64 grep sed; do + which $_tool >/dev/null 2>&1 + [ $? -ne 0 ] && echo "Missing dependency: $_tool)" >&2 && exit 1 + done + echo -n '' +} + +build_llng_url () { + perl -e '$ARGV[0]=~s#/+$##;$prefix = "https://";$prefix = $1 if $ARGV[0] =~ s#^(https?://)##;print "$prefix$ARGV[0]"' "$LLNG_SERVER" +} + +# 1. LLNG Connection + +llng_connect () { + LLNG_CONNECTED=0 + if client -f $LLNG_URL >/dev/null 2>&1; then + LLNG_CONNECTED=1 + + # else try to authenticate + else + if test "$LLNG_LOGIN" = "" + then + LLNG_LOGIN=$(askString Login) + fi + + if test "$PROMPT" = yes -o "$LLNG_PASSWORD" = "" + then + stty -echo + LLNG_PASSWORD=$(askString Password) + stty echo + echo + fi + + # Test if token is required + echo $(client $LLNG_URL) + TMP=$(client $LLNG_URL 2>/dev/null) + echo "TMP : $TMP" + TOKEN='' + if echo "$TMP" | jq -r ".token" >/dev/null 2>&1; then + TOKEN="--data-urlencode token="$( echo "$TMP" | jq -r ".token" ) + fi + + TMP=$(client -XPOST --data-urlencode "user=$LLNG_LOGIN" --data-urlencode "password=$LLNG_PASSWORD" $TOKEN $LLNG_URL) + ID='' + if echo "$TMP" | jq -r ".id" >/dev/null 2>&1; then + LLNG_CONNECTED=1 + ID=$(echo "$TMP" | jq -r ".id") + fi + if test "$ID" = "null" -o "$ID" = ""; then + echo "Unable to connect:" >&2 + echo "$TMP" >&2 + exit 1 + fi + fi +} + +whoami () { + if test "$LLNG_CONNECTED" != 1; then + llng_connect + fi + client "${LLNG_URL}/mysession/?whoami" | jq -r '.result' +} + +getLanguages () { + client "${LLNG_URL}/languages" | jq -S +} + +getLlngId () { + if test "$LLNG_CONNECTED" != 1; then + llng_connect + fi + client -lv "${LLNG_URL}/session/my/?whoami" 2>&1 | grep -E '> *Cookie' | sed -e 's/.*Cookie: *//' +} + +# 2. OIDC + +_oidcmetadata () { + client -f "${LLNG_URL}/.well-known/openid-configuration" +} + +getOidcMetadata () { + TMP=$(client -Sf "${LLNG_URL}/.well-known/openid-configuration") + if test "$TMP" != ''; then + echo $TMP | jq -S + else + exit 1 + fi +} + +getOidcEndpoints () { + TMP=$(_oidcmetadata || true) + if test "$TMP" = ""; then + export AUTHZ_ENDPOINT="${LLNG_URL}/oauth2/authorize" + export TOKEN_ENDPOINT="${LLNG_URL}/oauth2/token" + export ENDSESSION_ENDPOINT="${LLNG_URL}/oauth2/logout" + export USERINFO_ENDPOINT="${LLNG_URL}/oauth2/userinfo" + export INTROSPECTION_ENDPOINT="${LLNG_URL}/oauth2/introspect" + else + export AUTHZ_ENDPOINT=$(echo $TMP | jq -r .authorization_endpoint) + export TOKEN_ENDPOINT=$(echo $TMP | jq -r .token_endpoint) + export ENDSESSION_ENDPOINT=$(echo $TMP | jq -r .end_session_endpoint) + export USERINFO_ENDPOINT=$(echo $TMP | jq -r .userinfo_endpoint) + export INTROSPECTION_ENDPOINT=$(echo $TMP | jq -r .introspection_endpoint) + fi +} + +# 2.2 PKCE +getCodeVerifier () { + tr -dc A-Za-z0-9 &2 + echo "Tried with: $TMP" >&2 + exit 2 + fi + + # Get access token + RAWTOKENS=$(client -XPOST -SsL -d "client_id=${CLIENT_ID}" \ + -d 'grant_type=authorization_code' \ + -d "$REDIRECT_URI" \ + -d "$_SCOPE" \ + $CODE_VERIFIER \ + $AUTHZ \ + --data-urlencode "code=$_CODE" \ + "$TOKEN_ENDPOINT") + if echo "$RAWTOKENS" | grep access_token >/dev/null 2>&1; then + LLNG_ACCESS_TOKEN=$(echo "$RAWTOKENS" | jq -r .access_token) + else + echo "Bad response:" >&2 + echo $RAWTOKENS >&2 + exit 3 + fi + if echo "$RAWTOKENS" | grep id_token >/dev/null 2>&1; then + LLNG_ID_TOKEN=$(echo "$RAWTOKENS" | jq -r .id_token) + fi + if echo "$RAWTOKENS" | grep refresh_token >/dev/null 2>&1; then + LLNG_REFRESH_TOKEN=$(echo "$RAWTOKENS" | jq -r .refresh_token) + fi +} + +getOidcTokens () { + if test "$RAWTOKENS" = ''; then + _queryToken + fi + echo $RAWTOKENS | jq -S +} + +getAccessToken () { + if test "$LLNG_ACCESS_TOKEN" = ''; then + _queryToken + fi + echo $LLNG_ACCESS_TOKEN +} + +getIdToken () { + if test "$LLNG_ID_TOKEN" = ''; then + _queryToken + fi + echo $LLNG_ID_TOKEN +} + +getRefreshToken () { + if test "$LLNG_REFRESH_TOKEN" = ''; then + _queryToken + fi + echo $LLNG_REFRESH_TOKEN +} + +getUserInfo () { + TOKEN=${1:-$LLNG_ACCESS_TOKEN} + if test "$TOKEN" = ''; then + _queryToken + TOKEN="$LLNG_ACCESS_TOKEN" + fi + client -H "Authorization: Bearer $TOKEN" "$USERINFO_ENDPOINT" | jq -S +} + +getIntrospection () { + TOKEN=${1:-$LLNG_ACCESS_TOKEN} + if test "$TOKEN" = ''; then + _queryToken + TOKEN="$LLNG_ACCESS_TOKEN" + fi + AUTHZ=$(_authz) + client $AUTHZ -d "token=$TOKEN" "$INTROSPECTION_ENDPOINT" | jq -S +} + +_getMatrixToken () { + if test "$MATRIX_SERVER" = ""; then + MATRIX_SERVER=$(askString 'Matrix server') + fi + MATRIX_URL=${MATRIX_URL:-https://$MATRIX_SERVER}/_matrix/client + if test "$MATRIX_TOKEN" = ""; then + PROVIDER=$(client $MATRIX_URL/v3/login | jq -r .flows[0].identity_providers[0].id) + if test "$LLNG_CONNECTED" != 1; then + llng_connect + fi + _CONTENT=$(client -i --location "$MATRIX_URL/r0/login/sso/redirect/$PROVIDER?redirectUrl=http%3A%2F%2Flocalhost%3A9876") + _LOGIN_TOKEN=$(echo $_CONTENT|perl -ne 'print $1 if/loginToken=(.*?)"/') + if test "$_LOGIN_TOKEN" = ""; then + echo "Unable to get matrix login_token" >&2 + echo $_CONTENT >&2 + exit 1 + fi + _CONTENT=$(client -XPOST -d '{"initial_device_display_name":"Shell Test Client","token":"'"$_LOGIN_TOKEN"'","type":"m.login.token"}' "$MATRIX_URL/v3/login") + MATRIX_TOKEN=$(echo $_CONTENT | jq -r .access_token) + if test "$MATRIX_TOKEN" = "" -o "$MATRIX_TOKEN" = "null"; then + echo "Unable to get matrix_token" >&2 + echo $_CONTENT >&2 + exit 1 + fi + fi +} + +getMatrixToken () { + if test "$MATRIX_TOKEN" = ""; then + _getMatrixToken + fi + echo $MATRIX_TOKEN +} + +_getMatrixFederationToken () { + if test "$MATRIX_SERVER" = ""; then + MATRIX_SERVER=$(askString 'Matrix server') + fi + MATRIX_URL=${MATRIX_URL:-https://$MATRIX_SERVER}/_matrix/client + MATRIX_TOKEN=${1:-$MATRIX_TOKEN} + if test "$MATRIX_USER" = ""; then + if test "$LLNG_LOGIN" = ""; then + MATRIX_USER=$(askString 'Matrix username') + else + MATRIX_USER=@$LLNG_LOGIN:$(echo $LLNG_SERVER | perl -pe 's/.*?\.//') + fi + fi + if test "$MATRIX_TOKEN" = ""; then + _getMatrixToken + fi + _CONTENT=$(client -XPOST -H "Authorization: Bearer $MATRIX_TOKEN" -d '{}' "$MATRIX_URL/v3/user/$MATRIX_USER/openid/request_token") + MATRIX_FEDERATION_TOKEN=$(echo $_CONTENT | jq -r .access_token) +} + +getMatrixFederationToken () { + _getMatrixFederationToken "$@" + echo $MATRIX_FEDERATION_TOKEN +} + +getAccessTokenFromMatrixToken () { + MATRIX_TOKEN="$1" + SUBJECT_ISSUER="$2" + AUDIENCE="$3" + _SCOPE=scope=$(uri_escape "${SCOPE}") + if test "$MATRIX_TOKEN" = "" -o "$SUBJECT_ISSUER" = ""; then + echo "Missing parameter" >&2 + exit 1 + fi + if test "$TOKEN_ENDPOINT" = ""; then + getOidcEndpoints + fi + AUTHZ=$(_authz) + client -XPOST -fSsL \ + $AUTHZ \ + -d 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \ + -d "client_id=$CLIENT_ID" \ + --data-urlencode "subject_token=$MATRIX_TOKEN" \ + -d "$_SCOPE" \ + --data-urlencode "subject_issuer=$SUBJECT_ISSUER" \ + --data-urlencode "audience=$AUDIENCE" \ + "$TOKEN_ENDPOINT" | jq -S +} diff --git a/packages/matrix-identity-server/matrix-server/run b/packages/matrix-identity-server/matrix-server/run new file mode 100755 index 00000000..9e49f76f --- /dev/null +++ b/packages/matrix-identity-server/matrix-server/run @@ -0,0 +1,6 @@ +#!/bin/sh + +export MYUID=${SUDO_UID:-$(id -u)} +export MYGID=${SUDO_GID:-$(id -g)} + +docker-compose up -d diff --git a/packages/matrix-identity-server/matrix-server/ssl/9da13359.0 b/packages/matrix-identity-server/matrix-server/ssl/9da13359.0 new file mode 120000 index 00000000..188face1 --- /dev/null +++ b/packages/matrix-identity-server/matrix-server/ssl/9da13359.0 @@ -0,0 +1 @@ +ca-cert.pem \ No newline at end of file diff --git a/packages/matrix-identity-server/matrix-server/ssl/both.pem b/packages/matrix-identity-server/matrix-server/ssl/both.pem new file mode 100644 index 00000000..1fab886b --- /dev/null +++ b/packages/matrix-identity-server/matrix-server/ssl/both.pem @@ -0,0 +1,50 @@ +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIB/zANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJBVTET +MBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ +dHkgTHRkMCAXDTIzMDkwMzE3NDQyNloYDzIxMjMwODEwMTc0NDI2WjBcMQswCQYD +VQQGEwJGUjEPMA0GA1UECAwGQ2VudHJlMQ4wDAYDVQQHDAVQYXJpczERMA8GA1UE +CgwITGluYWdvcmExGTAXBgNVBAMMEGF1dGguZXhhbXBsZS5jb20wggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDcsDoFGlSc0mN2HEinfil6iLPdVIjnF32c +4nbJqddPL7941pD3LMObaZtaMAepsjZCM7iGL5olkhBhdMvpk1Efz3aFUZrQaOHU +bB5jq2Y6ZMecMi4kqszflhS+9M8PHcioAN2SXXujS2ftCNN69+8hX6gkr2ZYWYLe +rlxGkIVPdVRbuiZcIgOas/HAFyEQxAoutcXKofH+dHWPjAnAu2+zaWQEDYqv9qPm +OT44il6RAtEAECa+c4XDN7ya6vrI42ltb0RAuL1LwYhAzGEBp3FKKVseA6jnBS8s +doxQA64PxKdqzY4+7dRHF8MkNtyGvebczmnCMnfomZWIiztHfZPRAgMBAAGjcDBu +MCwGA1UdEQQlMCOCEGF1dGguZXhhbXBsZS5jb22CD3RvbS5leGFtcGxlLmNvbTAd +BgNVHQ4EFgQUrcIkGzn+WSJV49d2KZ/zbZqCiWAwHwYDVR0jBBgwFoAUlntXm3UF +zj9nIGMR00+iKbD3EvUwDQYJKoZIhvcNAQELBQADggEBAE/ESzViMDvPv8SZSZmT +uq2/m/rqUn3AWdRDgdOX01kUVzo0/asg8XYc5qKVxhhdq5ajx2SxWQQvqxTGOHeF +sQgbSBAAjZFA5og2LW+w6ihWhQy/sSqwUUlYLE4A/Q4RKcn1jm145NyWbgCAqc8G +W/EfGnbdgahAwM8W0g0M3nrNN3weLFYz2hMd1OrgDCUu/0nk0jfzyHGWk1h6SgKE +cHe2QZlQ9V8DPBpdgPbzCa626Io4L1UR/jnCOXFF1YIA+i2EchKhNGM+yXrFF/up +2zIWQVw6vC3SYv5hSmSazyNPddZoLWG6MFoQV/vOtnkMeycWyJ2xXqa12CP27X0U +jgs= +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDcsDoFGlSc0mN2 +HEinfil6iLPdVIjnF32c4nbJqddPL7941pD3LMObaZtaMAepsjZCM7iGL5olkhBh +dMvpk1Efz3aFUZrQaOHUbB5jq2Y6ZMecMi4kqszflhS+9M8PHcioAN2SXXujS2ft +CNN69+8hX6gkr2ZYWYLerlxGkIVPdVRbuiZcIgOas/HAFyEQxAoutcXKofH+dHWP +jAnAu2+zaWQEDYqv9qPmOT44il6RAtEAECa+c4XDN7ya6vrI42ltb0RAuL1LwYhA +zGEBp3FKKVseA6jnBS8sdoxQA64PxKdqzY4+7dRHF8MkNtyGvebczmnCMnfomZWI +iztHfZPRAgMBAAECggEAA/eJdb3tSBsKJQmbsFj5yi8tE8Dnrs0ToWP61ctcGtIN +todakMnrKB7q2vmL2z14m7y0FBjXDR+mBq8DsHtnexmFi4w9c+/6IebDQLNOXpka ++XNGzMb7G4JaB+Czt/DwNjF4E0qOOigeOTfV03B5A81fMiSq6EBSg5MQydl0oJOb +BLJnP3G4XiwLrMXHaT8+ZN7Tf82Qm5SSFXceRzfKVbIrBgnIxL8vVZX/0go+Ki/z +wrh05Lmr8ABB9GQ123e2pvJ2zJ0AsZxTNQrSTZH+Hmzf9X6LX6LzPiO4LDukzZ9K +ryw3Ypv8CkMXC2xgqC5xS6FdekTvg6OHFXcsPM5D/QKBgQDeY6m4r/uzuYNJeX6D +PhWDQJgHCBfluBJcmv5YjUmFfJLC9Fv/HAr0eF0bWOQhQDrtx4tditfjP4I37b0L +G9pe7d0MgG8ukU7WajiJziNvn42gk+bE5NzKgmONVZ/NYV+06z4jLmqu0el7TUWg +tQ5bDhaM49G4ozwc8dGREj+h5wKBgQD+CsEU04qNiTQ6yAYcSn5stED+Njyt+QjG +FzzQ4tGjZoGJPz482TCtvY92TSDPETftPIHZI7GtTmJ22FDJPajZx/YhpCm7kUzW ++vHRobMIj39yX/hBm8QdsDqqInIjxZWWvqx9IO/jH/+gi5Q366swgFVLhE/dZLiN +ePshBLbVhwKBgQCHWMScoI5hzY/3kbfLjGdvYEqmTOiuaJ7UOYh+wE50rWJswGeV +Fa4dJ3wS/sCo8/xpZr6NCclmhupru4cIUcVPbRjRWQFRqCIBINiUFh8++i4qApm6 +T0eJAF4yUGBXkOG8rEc/BirrhtnAr0CnFEpOZH6Y9LZY1w/o1cujrSWJFQKBgH0X +sJHfxLcDG7viKNgfendunx5OeLy2BzL72E+HkPEkFZ8OjEgMLqMu14jKW+B6uw5P +oCTbJa+QDH428DjX6uAqTbGtE3uwBaVKdm7ib7VEa95XEXjFCeIQmCKUyZ2BurI6 ++9a1tEojxI06jeanXmmIl/eSlH0RDqtjKk3M83bbAoGADd1f4RVKt7s0B9nCLRxl +hdGHgicY514KUaF3uO5Q9K1MR9vokfT/tQ+G9WWyHZQbPQlk9gwuEKmlrgY4CVnL +9BoY0GuScnXn4UAvmB8WRdxDZy+xPrDV9/Watil63ib+Mmiz27o7U3fTjQ+dUel1 +2oofqczL8xo5Ks3Bm+z5QYU= +-----END PRIVATE KEY----- diff --git a/packages/matrix-identity-server/matrix-server/ssl/ca-cert.pem b/packages/matrix-identity-server/matrix-server/ssl/ca-cert.pem new file mode 100644 index 00000000..9f0cff16 --- /dev/null +++ b/packages/matrix-identity-server/matrix-server/ssl/ca-cert.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDbTCCAlWgAwIBAgIUSmlvJ1Ymie7M0482aVgmuCTsAWUwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAgFw0yMzA5MDMxNzQyMzVaGA8yMTIz +MDgxMDE3NDIzNVowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx +ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAM6R98uoWvM1lJUHbLgfd4ieS3b2SZehwQF5KLFF +NMfiReztyqzpR/sangt67bcftRnzqMQigCzdaWlMW4TobIGz1ervuFqM+Sv+s4wz +CUlkpnR1aon/3XQeLp3i6aES5e9JE8SK291Wp99xsMeXVawdMYVcV3wwqHFilY7J +fgRk3BDMAD/Is6XVAKZnjnxYzVfHKcede61BEDk5ZcsXYuK7ft0gxoQGV6K/Mz45 +eFHqmHPdVWmgfpLMqJFFxeeeUPLI+qt+DLb7K5bx4tk6w8zbl26ljBtmwnlgoL12 +Ce4bymMpMycbkgKpqQjTT5jJ+Ki5C5dIo9+2ESIKANRQnH0CAwEAAaNTMFEwHQYD +VR0OBBYEFJZ7V5t1Bc4/ZyBjEdNPoimw9xL1MB8GA1UdIwQYMBaAFJZ7V5t1Bc4/ +ZyBjEdNPoimw9xL1MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB +AL9gnGOETtOK3/HNs7xRM49PUiCQWmVEUA/yQzmnFGZ+MGmm19jb8O8I69BB0JLK +7Pd/ZeaBNn73j8C5M/kzwXbsuIPgkVVi/jsUrJ0W9p4BR/EKH1GGpZ7LTCI/fwFi +NWA3dQz8WyNtqBzFJZ3zLW+ZN4UqPBIerLcckn/3vRXQCw8DCqn6OFqyTGpywVBy +y8s4VGr4kWZhwel/D0vSZW3FAx6CCZNgaLc0GQw5f9/3iEXaM9v6nN8NX/f4bZbF +6tcu7q7T43jDxNWZSJD4t0mNcd033hSL/BGwa+yA9cxY4OzWwVb3F0mASECB50PT +GFYsEkY4Q1/hovB5wemGMLY= +-----END CERTIFICATE----- diff --git a/packages/matrix-identity-server/matrix-server/ssl/ca-key.pem b/packages/matrix-identity-server/matrix-server/ssl/ca-key.pem new file mode 100644 index 00000000..7feabb3e --- /dev/null +++ b/packages/matrix-identity-server/matrix-server/ssl/ca-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCjQKg4lKjikTia +4g0U5ZC1w43eRe+5OUm2L+0sI0/Ird+59Eb08mnWmTQvOqHxNInqbQmrQp7RsJuK +IymyXoarHRDjmij9+3GPietGbTit+rHXBGKw3afTM97YQMWDeqiAyunll2BJ4Ldp +Zfqv18PKNgypPlCsEOySZHKgljbWP+vXsi93bqkVKDGiI3bLi49zkhJbSczZTwJI +96md+TsVmgmLEHyzBljzpvaE1IylaubbdvNUoN2d3/PbFxdyscespYtk3GcTHxYI +h/lLRU+n3q5I4Lletp+SBcQyY/pV9MUD+dYwXYlkd7srE17ynVIFgqrMd3vVQMUx +KSWbmX3XAgMBAAECggEAC3RxeUrhEMi8zo1W+qTjAwX+Md75Y6dD5080lpjBs+ST +YrJjujM6Pu8QDNJR46FangLZn1U03KTGPSqbdeMREveepEHAZ5XZkYdNZWsrg3Wo +PdyWai+bY4PruoPM+B1GrobOY6g7/uMQK4qWzPjYSZuCRuvS8EfduaXnryHrJhxI +Z751MTt1iFRKm6QeEEJJH7BGNQuQCV0wlbtDK0YEAzu4VuWgak/87TNmAd/rabK4 +2OQsgquBZbdcCujreG1VqJK/a/qecZo99PZR8yIKCLnrmYayf0EDWgH2gp0pRQEU +a1Ab59VxPegMkBiHT/cXL9DuLpvpGcjdgpkigORzQQKBgQDQl0we3ZZGhlBjfNZ3 +e2a5xkK6cXlF2f8UU7UFjZJNk1XigjBnQ1vemwtbvKBO6ee6CNpJ4CCaUNXH38aw +DKBZ6gO4UrCC+ECbJcvWBxhBiJzAD1nodoBjkNADKJ+eWNr5J4g6BDzux6Pf4R9D +80HKey0c7K90OtxF23g3f1/gwQKBgQDIW2DrPE+IV5J4quzCgAddH9RYeud8nm7p +tCJw7crsb/AO82NyxVkTGFF+7oJi4sfCJuxqRyga7XeiEkCZplKO6saRM0sD2kDY +n4aBGew+3+EhKoHGzj23V1eg4l7oGgem7gi3CVeEvhorgHMfVxJQg/QTV3CIN/EF +TrJJ01rslwKBgCjqNHUtc8ebmvMYzpybKPgxqm8VyPrpmr4q+SwAq/zpdIQ8ky/+ +J2wPr3esFSnFeb2k9ORewSZjyrss6rUnlOBuJZKnLZZTCaElFcmClMBuAoktua3+ +aIqfIh4sfrq5pSIQHgl7QVR49mz4pIBYm8QSyzOMPZIn5YMSXI9OPclBAoGBAIGG +kaGB2+jItGhOF8GmAxyw9xY8XmqyAgIT8jAPiqBPvWHs9t27t1og3o6woppLAdkC +UNRkLAk8e5rLMfgjDjxWiwhToKtc7Y8dklbj61a6ZVCLqlpb+ooMbRoVPkXOjiPc +vsWVxH6MZ164K7SXFb/3jlLytE1b3PURazFO8fkBAoGBAMCGvU9SQTULMi9BgMMN +5K1GA2K/WhJLS3Wfjx/aGjB/BWp4PRImEYZX8AKtyQKWCy5Ft/ivV6gsfQGknd4n +6nX/u8tKMeAcaWn+3vSlNc68v/1ebPoyJrViCvrw2LdZTKTbgaFIbsIIoQK94MIL +FOWGXz1kFBvHBb3woKWWqVPm +-----END PRIVATE KEY----- diff --git a/packages/matrix-identity-server/matrix-server/ssl/server.key b/packages/matrix-identity-server/matrix-server/ssl/server.key new file mode 100644 index 00000000..28a4d85e --- /dev/null +++ b/packages/matrix-identity-server/matrix-server/ssl/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDcsDoFGlSc0mN2 +HEinfil6iLPdVIjnF32c4nbJqddPL7941pD3LMObaZtaMAepsjZCM7iGL5olkhBh +dMvpk1Efz3aFUZrQaOHUbB5jq2Y6ZMecMi4kqszflhS+9M8PHcioAN2SXXujS2ft +CNN69+8hX6gkr2ZYWYLerlxGkIVPdVRbuiZcIgOas/HAFyEQxAoutcXKofH+dHWP +jAnAu2+zaWQEDYqv9qPmOT44il6RAtEAECa+c4XDN7ya6vrI42ltb0RAuL1LwYhA +zGEBp3FKKVseA6jnBS8sdoxQA64PxKdqzY4+7dRHF8MkNtyGvebczmnCMnfomZWI +iztHfZPRAgMBAAECggEAA/eJdb3tSBsKJQmbsFj5yi8tE8Dnrs0ToWP61ctcGtIN +todakMnrKB7q2vmL2z14m7y0FBjXDR+mBq8DsHtnexmFi4w9c+/6IebDQLNOXpka ++XNGzMb7G4JaB+Czt/DwNjF4E0qOOigeOTfV03B5A81fMiSq6EBSg5MQydl0oJOb +BLJnP3G4XiwLrMXHaT8+ZN7Tf82Qm5SSFXceRzfKVbIrBgnIxL8vVZX/0go+Ki/z +wrh05Lmr8ABB9GQ123e2pvJ2zJ0AsZxTNQrSTZH+Hmzf9X6LX6LzPiO4LDukzZ9K +ryw3Ypv8CkMXC2xgqC5xS6FdekTvg6OHFXcsPM5D/QKBgQDeY6m4r/uzuYNJeX6D +PhWDQJgHCBfluBJcmv5YjUmFfJLC9Fv/HAr0eF0bWOQhQDrtx4tditfjP4I37b0L +G9pe7d0MgG8ukU7WajiJziNvn42gk+bE5NzKgmONVZ/NYV+06z4jLmqu0el7TUWg +tQ5bDhaM49G4ozwc8dGREj+h5wKBgQD+CsEU04qNiTQ6yAYcSn5stED+Njyt+QjG +FzzQ4tGjZoGJPz482TCtvY92TSDPETftPIHZI7GtTmJ22FDJPajZx/YhpCm7kUzW ++vHRobMIj39yX/hBm8QdsDqqInIjxZWWvqx9IO/jH/+gi5Q366swgFVLhE/dZLiN +ePshBLbVhwKBgQCHWMScoI5hzY/3kbfLjGdvYEqmTOiuaJ7UOYh+wE50rWJswGeV +Fa4dJ3wS/sCo8/xpZr6NCclmhupru4cIUcVPbRjRWQFRqCIBINiUFh8++i4qApm6 +T0eJAF4yUGBXkOG8rEc/BirrhtnAr0CnFEpOZH6Y9LZY1w/o1cujrSWJFQKBgH0X +sJHfxLcDG7viKNgfendunx5OeLy2BzL72E+HkPEkFZ8OjEgMLqMu14jKW+B6uw5P +oCTbJa+QDH428DjX6uAqTbGtE3uwBaVKdm7ib7VEa95XEXjFCeIQmCKUyZ2BurI6 ++9a1tEojxI06jeanXmmIl/eSlH0RDqtjKk3M83bbAoGADd1f4RVKt7s0B9nCLRxl +hdGHgicY514KUaF3uO5Q9K1MR9vokfT/tQ+G9WWyHZQbPQlk9gwuEKmlrgY4CVnL +9BoY0GuScnXn4UAvmB8WRdxDZy+xPrDV9/Watil63ib+Mmiz27o7U3fTjQ+dUel1 +2oofqczL8xo5Ks3Bm+z5QYU= +-----END PRIVATE KEY----- diff --git a/packages/matrix-identity-server/matrix-server/ssl/server.pem b/packages/matrix-identity-server/matrix-server/ssl/server.pem new file mode 100644 index 00000000..eaa6b1c4 --- /dev/null +++ b/packages/matrix-identity-server/matrix-server/ssl/server.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIB/zANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJBVTET +MBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ +dHkgTHRkMCAXDTIzMDkwMzE3NDQyNloYDzIxMjMwODEwMTc0NDI2WjBcMQswCQYD +VQQGEwJGUjEPMA0GA1UECAwGQ2VudHJlMQ4wDAYDVQQHDAVQYXJpczERMA8GA1UE +CgwITGluYWdvcmExGTAXBgNVBAMMEGF1dGguZXhhbXBsZS5jb20wggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDcsDoFGlSc0mN2HEinfil6iLPdVIjnF32c +4nbJqddPL7941pD3LMObaZtaMAepsjZCM7iGL5olkhBhdMvpk1Efz3aFUZrQaOHU +bB5jq2Y6ZMecMi4kqszflhS+9M8PHcioAN2SXXujS2ftCNN69+8hX6gkr2ZYWYLe +rlxGkIVPdVRbuiZcIgOas/HAFyEQxAoutcXKofH+dHWPjAnAu2+zaWQEDYqv9qPm +OT44il6RAtEAECa+c4XDN7ya6vrI42ltb0RAuL1LwYhAzGEBp3FKKVseA6jnBS8s +doxQA64PxKdqzY4+7dRHF8MkNtyGvebczmnCMnfomZWIiztHfZPRAgMBAAGjcDBu +MCwGA1UdEQQlMCOCEGF1dGguZXhhbXBsZS5jb22CD3RvbS5leGFtcGxlLmNvbTAd +BgNVHQ4EFgQUrcIkGzn+WSJV49d2KZ/zbZqCiWAwHwYDVR0jBBgwFoAUlntXm3UF +zj9nIGMR00+iKbD3EvUwDQYJKoZIhvcNAQELBQADggEBAE/ESzViMDvPv8SZSZmT +uq2/m/rqUn3AWdRDgdOX01kUVzo0/asg8XYc5qKVxhhdq5ajx2SxWQQvqxTGOHeF +sQgbSBAAjZFA5og2LW+w6ihWhQy/sSqwUUlYLE4A/Q4RKcn1jm145NyWbgCAqc8G +W/EfGnbdgahAwM8W0g0M3nrNN3weLFYz2hMd1OrgDCUu/0nk0jfzyHGWk1h6SgKE +cHe2QZlQ9V8DPBpdgPbzCa626Io4L1UR/jnCOXFF1YIA+i2EchKhNGM+yXrFF/up +2zIWQVw6vC3SYv5hSmSazyNPddZoLWG6MFoQV/vOtnkMeycWyJ2xXqa12CP27X0U +jgs= +-----END CERTIFICATE----- diff --git a/packages/matrix-identity-server/matrix-server/stop b/packages/matrix-identity-server/matrix-server/stop new file mode 100755 index 00000000..308ac233 --- /dev/null +++ b/packages/matrix-identity-server/matrix-server/stop @@ -0,0 +1,6 @@ +#!/bin/sh + +export MYUID=${SUDO_UID:-$(id -u)} +export MYGID=${SUDO_GID:-$(id -g)} + +docker-compose down diff --git a/packages/matrix-identity-server/matrix-server/synapse-ref/homeserver.yaml b/packages/matrix-identity-server/matrix-server/synapse-ref/homeserver.yaml new file mode 100644 index 00000000..a94a35b7 --- /dev/null +++ b/packages/matrix-identity-server/matrix-server/synapse-ref/homeserver.yaml @@ -0,0 +1,140 @@ +# Configuration file for Synapse. +# +# This is a YAML file: see [1] for a quick introduction. Note in particular +# that *indentation is important*: all the elements of a list or dictionary +# should have the same indentation. +# +# [1] https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html +# +# For more information on how to configure Synapse, including a complete accounting of +# each option, go to docs/usage/configuration/config_documentation.md or +# https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html +server_name: 'example.com' +public_baseurl: 'https://matrix.example.com/' +suppress_key_server_warning: true + +pid_file: /data/homeserver.pid + +#web_client_location: 'https://fluffychat.example.com/' +presence: + enabled: true + +listeners: + - port: 8008 + tls: false + type: http + x_forwarded: true + resources: + - names: [client] + compress: true + - names: [federation] + compress: false + +database: + name: sqlite3 + args: + database: /data/homeserver.db +log_config: '/data/matrix.example.com.log.config' +media_store_path: /data/media_store +uploads_path: /data/uploads +max_upload_size: '100M' +max_image_pixels: '32M' +dynamic_thumbnails: false +thumbnail_sizes: + - width: 32 + height: 32 + method: crop + - width: 96 + height: 96 + method: crop + - width: 320 + height: 240 + method: scale + - width: 640 + height: 480 + method: scale + - width: 800 + height: 600 + method: scale +url_preview_enabled: true +url_preview_ip_range_blacklist: + - '192.168.254.0/24' +max_spider_size: '10M' +url_preview_accept_language: + +enable_registration: false +account_validity: + +bcrypt_rounds: 12 +allow_guest_access: False + +trusted_third_party_id_servers: + - matrix.org + - vector.im + - riot.im + +enable_metrics: true + +report_stats: true +macaroon_secret_key: 'ZZ=.PN_w4&OY~UGamp_Vhq#e^csHeDJ2_6O#iSJDQs@goul+gb' +form_secret: 'E:u*OOR_0GInF_qtO#NiP_s:mZzhoytDEmkJUo+IYGsyQ1Vl3@' +#signing_key_path: "/data/keys/matrix.example.com.signing.key" +trusted_key_servers: + - server_name: 'matrix.org' + accept_keys_insecurely: true + #- server_name: "twake_root_server" + # verify_keys: + # "ed25519:auto": "aabbccddeeff..." + +## SSO + +password_config: + enabled: false + +# Old fashion: prefer separated oidc_providers files +oidc_providers: + - idp_id: lemon + idp_name: Connect with Twake + enabled: true + issuer: 'https://auth.example.com' + client_id: 'matrix1' + client_secret: 'matrix1' + scopes: ['openid', 'profile', 'email'] + + discover: true + #authorization_endpoint: "https://auth.example.com/oauth2/authorize" + #token_endpoint: "https://auth.example.com/oauth2/token" + #userinfo_endpoint: "https://auth.example.com/oauth2/userinfo" + #jwks_uri: "https://auth.example.com/oauth2/jwks" + + backchannel_logout_enabled: true + backchannel_logout_is_soft: true + + user_profile_method: 'userinfo_endpoint' + user_mapping_provider: + config: + subject_claim: 'sub' + localpart_template: '{{ user.preferred_username }}' + display_name_template: '{{ user.name }}' + +# Whether to allow non server admin to create groups on this server +enable_group_creation: false +#group_creation_prefix: "unofficial/" + +user_directory: + search_all_users: true + +e2e_key_export: true +encryption_enabled: true + +# FOR TEST ONLY +accept_keys_insecurely: true +federation_verify_certificates: false + +# TODO: identity_server integration +# * invite_client_location +# * account_threepid_delegates +default_identity_server: https://tom.example.com + +# Used for auto-registrating the admin. NOTE : this string MUST NOT be shared anywhere! +registration_shared_secret: astringthatyoumustnevershare diff --git a/packages/matrix-identity-server/matrix-server/synapse-ref/matrix.example.com.log.config b/packages/matrix-identity-server/matrix-server/synapse-ref/matrix.example.com.log.config new file mode 100644 index 00000000..0b522eb1 --- /dev/null +++ b/packages/matrix-identity-server/matrix-server/synapse-ref/matrix.example.com.log.config @@ -0,0 +1,75 @@ +# Log configuration for Synapse. +# +# This is a YAML file containing a standard Python logging configuration +# dictionary. See [1] for details on the valid settings. +# +# Synapse also supports structured logging for machine readable logs which can +# be ingested by ELK stacks. See [2] for details. +# +# [1]: https://docs.python.org/3/library/logging.config.html#configuration-dictionary-schema +# [2]: https://element-hq.github.io/synapse/latest/structured_logging.html + +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +handlers: + file: + class: logging.handlers.TimedRotatingFileHandler + formatter: precise + filename: /data/homeserver.log + when: midnight + backupCount: 3 # Does not include the current log file. + encoding: utf8 + + # Default to buffering writes to log file for efficiency. + # WARNING/ERROR logs will still be flushed immediately, but there will be a + # delay (of up to `period` seconds, or until the buffer is full with + # `capacity` messages) before INFO/DEBUG logs get written. + buffer: + class: synapse.logging.handlers.PeriodicallyFlushingMemoryHandler + target: file + + # The capacity is the maximum number of log lines that are buffered + # before being written to disk. Increasing this will lead to better + # performance, at the expensive of it taking longer for log lines to + # be written to disk. + # This parameter is required. + capacity: 10 + + # Logs with a level at or above the flush level will cause the buffer to + # be flushed immediately. + # Default value: 40 (ERROR) + # Other values: 50 (CRITICAL), 30 (WARNING), 20 (INFO), 10 (DEBUG) + flushLevel: 30 # Flush immediately for WARNING logs and higher + + # The period of time, in seconds, between forced flushes. + # Messages will not be delayed for longer than this time. + # Default value: 5 seconds + period: 5 + + # A handler that writes logs to stderr. Unused by default, but can be used + # instead of "buffer" and "file" in the logger handlers. + console: + class: logging.StreamHandler + formatter: precise + +loggers: + synapse.storage.SQL: + # beware: increasing this to DEBUG will make synapse log sensitive + # information such as access tokens. + level: INFO + +root: + level: INFO + + # Write logs to the `buffer` handler, which will buffer them together in memory, + # then write them to a file. + # + # Replace "buffer" with "console" to log to stderr instead. + # + handlers: [buffer] + +disable_existing_loggers: false diff --git a/packages/matrix-identity-server/package.json b/packages/matrix-identity-server/package.json index d15c8c9e..a48c5a4e 100644 --- a/packages/matrix-identity-server/package.json +++ b/packages/matrix-identity-server/package.json @@ -39,15 +39,17 @@ "build:example": "rollup -p @rollup/plugin-typescript -e express,@twake/matrix-identity-server -m -o example/identity-server.js example/identity-server.ts", "build:lib": "rollup -c", "start": "node example/identity-server.js", - "test": "jest" + "test": "LOG_TRANSPORTS=File LOG_FILE=/dev/null jest" }, "dependencies": { "@twake/config-parser": "*", "@twake/crypto": "*", "@twake/logger": "*", + "@twake/utils": "*", "express": "^4.19.2", "express-rate-limit": "^7.2.0", "generic-pool": "^3.9.0", + "jest": "^29.7.0", "matrix-resolve": "^1.0.1", "node-cron": "^3.0.2", "node-fetch": "^3.3.0", diff --git a/packages/matrix-identity-server/rollup.config.js b/packages/matrix-identity-server/rollup.config.js index f2e9cfb1..f85b74da 100644 --- a/packages/matrix-identity-server/rollup.config.js +++ b/packages/matrix-identity-server/rollup.config.js @@ -16,5 +16,6 @@ export default config([ 'redis', '@twake/config-parser', '@twake/crypto', - "@twake/logger" + '@twake/logger', + '@twake/utils' ]) diff --git a/packages/matrix-identity-server/server.mjs b/packages/matrix-identity-server/server.mjs new file mode 100644 index 00000000..be3ef308 --- /dev/null +++ b/packages/matrix-identity-server/server.mjs @@ -0,0 +1,112 @@ +import MatrixIdentityServer from '@twake/matrix-identity-server' +import express from 'express' +import path from 'node:path' +import { fileURLToPath } from 'url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const conf = { + base_url: process.env.BASE_URL, + additional_features: process.env.ADDITIONAL_FEATURES || false, + cron_service: process.env.CRON_SERVICE ?? true, + database_engine: process.env.DATABASE_ENGINE || 'sqlite', + database_host: process.env.DATABASE_HOST || './tokens.db', + database_name: process.env.DATABASE_NAME, + database_user: process.env.DATABASE_USER, + database_ssl: process.env.DATABASE_SSL + ? JSON.parse(process.env.DATABASE_SSL) + : false, + database_password: process.env.DATABASE_PASSWORD, + federated_identity_services: process.env.FEDERATED_IDENTITY_SERVICES + ? process.env.FEDERATED_IDENTITY_SERVICES.split(/[,\s]+/) + : [], + hashes_rate_limit: process.env.HASHES_RATE_LIMIT, + is_federated_identity_service: false, + ldap_base: process.env.LDAP_BASE, + ldap_filter: process.env.LDAP_FILTER, + ldap_user: process.env.LDAP_USER, + ldap_password: process.env.LDAP_PASSWORD, + ldap_uri: process.env.LDAP_URI, + matrix_database_engine: process.env.MATRIX_DATABASE_ENGINE, + matrix_database_host: process.env.MATRIX_DATABASE_HOST, + matrix_database_name: process.env.MATRIX_DATABASE_NAME, + matrix_database_password: process.env.MATRIX_DATABASE_PASSWORD, + matrix_database_user: process.env.MATRIX_DATABASE_USER, + matrix_database_ssl: process.env.MATRIX_DATABASE_SSL + ? JSON.parse(process.env.MATRIX_DATABASE_SSL) + : false, + pepperCron: process.env.PEPPER_CRON || '9 1 * * *', + rate_limiting_window: process.env.RATE_LIMITING_WINDOW || 600000, + rate_limiting_nb_requests: process.env.RATE_LIMITING_NB_REQUESTS || 100, + redis_uri: process.env.REDIS_URI, + server_name: process.env.SERVER_NAME, + smtp_password: process.env.SMTP_PASSWORD, + smtp_tls: process.env.SMTP_TLS ?? true, + smtp_user: process.env.SMTP_USER, + smtp_verify_certificate: process.env.SMTP_VERIFY_CERTIFICATE, + smtp_sender: process.env.SMTP_SENDER ?? '', + smtp_server: process.env.SMTP_SERVER || 'localhost', + smtp_port: process.env.SMTP_PORT || 25, + template_dir: process.env.TEMPLATE_DIR || path.join(__dirname, 'templates'), + update_federated_identity_hashes_cron: + process.env.UPDATE_FEDERATED_IDENTITY_HASHES_CRON || '*/10 * * * *', + update_users_cron: process.env.UPDATE_USERS_CRON || '*/10 * * * *', + userdb_engine: process.env.USERDB_ENGINE || 'sqlite', + userdb_host: process.env.USERDB_HOST || './users.db', + userdb_name: process.env.USERDB_NAME, + userdb_password: process.env.USERDB_PASSWORD, + userdb_ssl: process.env.USERDB_SSL + ? JSON.parse(process.env.USERDB_SSL) + : false, + userdb_user: process.env.USERDB_USER +} + +const app = express() +const trustProxy = process.env.TRUSTED_PROXIES + ? process.env.TRUSTED_PROXIES.split(/\s+/) + : [] +if (trustProxy.length > 0) { + conf.trust_x_forwarded_for = true + app.set('trust proxy', ...trustProxy) +} +const matrixIdServer = new MatrixIdentityServer(conf) +const promises = [matrixIdServer.ready] + +if (process.env.CROWDSEC_URI) { + if (!process.env.CROWDSEC_KEY) { + throw new Error('Missing CROWDSEC_KEY') + } + promises.push( + new Promise((resolve, reject) => { + import('@crowdsec/express-bouncer') + .then((m) => + m.default({ + url: process.env.CROWDSEC_URI, + apiKey: process.env.CROWDSEC_KEY + }) + ) + .then((crowdsecMiddleware) => { + app.use(crowdsecMiddleware) + resolve() + }) + .catch(reject) + }) + ) +} + +Promise.all(promises) + .then(() => { + Object.keys(matrixIdServer.api.get).forEach((k) => { + app.get(k, matrixIdServer.api.get[k]) + }) + Object.keys(matrixIdServer.api.post).forEach((k) => { + app.post(k, matrixIdServer.api.post[k]) + }) + const port = process.argv[2] != null ? parseInt(process.argv[2]) : 3000 + console.log(`Listening on port ${port}`) + app.listen(port) + }) + .catch((e) => { + throw new Error(e) + }) diff --git a/packages/matrix-identity-server/src/3pid/bind.ts b/packages/matrix-identity-server/src/3pid/bind.ts new file mode 100644 index 00000000..0ef39658 --- /dev/null +++ b/packages/matrix-identity-server/src/3pid/bind.ts @@ -0,0 +1,155 @@ +import { Hash, signJson } from '@twake/crypto' +import type MatrixIdentityServer from '..' +import { + errMsg, + jsonContent, + send, + validateParameters, + type expressAppHandler +} from '@twake/utils' + +const clientSecretRe = /^[0-9a-zA-Z.=_-]{6,255}$/ +const mxidRe = /^@[0-9a-zA-Z._=-]+:[0-9a-zA-Z.-]+$/ +const sidRe = /^[0-9a-zA-Z.=_-]{1,255}$/ + +interface ResponseArgs { + address: string + medium: string + mxid: string + not_after: number + not_before: number + ts: number +} + +interface RequestTokenArgs { + client_secret: string + mxid: string + sid: string +} + +const schema = { + client_secret: true, + mxid: true, + sid: true +} + +const bind = ( + idServer: MatrixIdentityServer +): expressAppHandler => { + return (req, res) => { + idServer.authenticate(req, res, (data, id) => { + jsonContent(req, res, idServer.logger, (obj) => { + validateParameters(res, schema, obj, idServer.logger, (obj) => { + const clientSecret = (obj as RequestTokenArgs).client_secret + const mxid = (obj as RequestTokenArgs).mxid + const sid = (obj as RequestTokenArgs).sid + if (!clientSecretRe.test(clientSecret)) { + send(res, 400, errMsg('invalidParam', 'invalid client_secret')) + } else if (!mxidRe.test(mxid)) { + send(res, 400, errMsg('invalidParam', 'invalid Matrix user ID')) + } else if (!sidRe.test(sid)) { + send(res, 400, errMsg('invalidParam', 'invalid session ID')) + } else { + idServer.logger.debug( + `bind request to associate 3pid with matrix_ID ${JSON.stringify( + obj + )}` + ) + + idServer.db + .get('mappings', ['address', 'medium', 'submit_time', 'valid'], { + session_id: sid, + client_secret: clientSecret + }) + .then((rows) => { + if (rows.length === 0) { + send(res, 404, { + errcode: 'M_NO_VALID_SESSION', + error: + 'No valid session was found matching that sid and client secret' + }) + return + } + if (rows[0].valid === 0) { + send(res, 400, { + errcode: 'M_SESSION_NOT_VALIDATED', + error: 'This validation session has not yet been completed' + }) + return + } + + // TODO : hook for any pending invite and call the onbind api : https://spec.matrix.org/v1.11/client-server-api/#room-aliases + + idServer.db + .get('keys', ['data'], { name: 'pepper' }) + .then(async (pepperRow) => { + const field = rows[0].medium === 'email' ? 'mail' : 'msisdn' + const hash = new Hash() + await hash.ready + await idServer.db.insert('hashes', { + hash: hash.sha256( + `${rows[0].address as string} ${field} ${ + pepperRow[0].data as string + }` + ), + pepper: pepperRow[0].data as string, + type: field, + value: mxid, + active: 1 + }) + const body: ResponseArgs = { + address: rows[0].address as string, + medium: rows[0].medium as string, + mxid, + not_after: + (rows[0].submit_time as number) + 86400000 * 36500, + not_before: rows[0].submit_time as number, + ts: rows[0].submit_time as number + } + idServer.db + .get('longTermKeypairs', ['keyID', 'private'], {}) + .then((keyrows) => { + if ( + typeof keyrows[0].private === 'string' && + typeof keyrows[0].keyID === 'string' + ) { + send( + res, + 200, + signJson( + body, + keyrows[0].private, + idServer.conf.server_name, + keyrows[0].keyID + ) + ) + } + }) + .catch((err) => { + // istanbul ignore next + idServer.logger.error( + 'Error getting long term key', + err + ) + send(res, 500, errMsg('unknown', err.toString())) + }) + }) + .catch((err) => { + // istanbul ignore next + idServer.logger.error('Error getting pepper', err) + send(res, 500, errMsg('unknown', err.toString())) + }) + }) + .catch((err) => { + // istanbul ignore next + idServer.logger.error('Error getting mapping', err) + send(res, 500, errMsg('unknown', err.toString())) + }) + } + }) + }) + }) + } +} + +export default bind diff --git a/packages/matrix-identity-server/src/3pid/index.ts b/packages/matrix-identity-server/src/3pid/index.ts new file mode 100644 index 00000000..c32a6c9d --- /dev/null +++ b/packages/matrix-identity-server/src/3pid/index.ts @@ -0,0 +1,69 @@ +import type MatrixIdentityServer from '..' +import { epoch, errMsg, send, type expressAppHandler } from '@twake/utils' +interface parameters { + client_secret: string + sid: string +} + +const validationTime: number = 100 * 365 * 24 * 60 * 60 * 1000 + +const GetValidated3pid = ( + idServer: MatrixIdentityServer +): expressAppHandler => { + return (req, res) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const prms: parameters = req.query as parameters + if (prms.client_secret?.length != null && prms.sid?.length != null) { + idServer.authenticate(req, res, (data, id) => { + idServer.db + .get('mappings', ['valid', 'address', 'medium', 'submit_time'], { + client_secret: prms.client_secret, + session_id: prms.sid + }) + .then((rows) => { + if (rows.length === 0) { + send(res, 404, { + errcode: 'M_NO_VALID_SESSION', + error: + 'No valid session was found matching that sid and client secret' + }) + return + } + + const validRow = rows.find((row) => row.valid === 1) + + if (validRow !== undefined) { + const submitTime = Number(validRow.submit_time) + /* istanbul ignore next */ // Set validationTime sufficiently low to enter this case + if (epoch() > validationTime + submitTime) { + send(res, 400, { + errcode: 'M_SESSION_EXPIRED', + error: 'This validation session has expired' + }) + return + } + send(res, 200, { + address: validRow.address, + medium: validRow.medium, + validated_at: submitTime + }) + } else { + send(res, 400, { + errcode: 'M_SESSION_NOT_VALIDATED', + error: 'This validation session has not yet been completed' + }) + } + }) + .catch((err) => { + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', err.toString())) + }) + }) + } else { + send(res, 400, errMsg('missingParams')) + } + } +} + +export default GetValidated3pid diff --git a/packages/matrix-identity-server/src/3pid/unbind.ts b/packages/matrix-identity-server/src/3pid/unbind.ts new file mode 100644 index 00000000..475f4d1c --- /dev/null +++ b/packages/matrix-identity-server/src/3pid/unbind.ts @@ -0,0 +1,109 @@ +import type MatrixIdentityServer from '..' +import { + errMsg, + jsonContent, + send, + validateParameters, + type expressAppHandler +} from '@twake/utils' + +const clientSecretRe = /^[0-9a-zA-Z.=_-]{6,255}$/ +const mxidRe = /^@[0-9a-zA-Z._=-]+:[0-9a-zA-Z.-]+$/ +const sidRe = /^[0-9a-zA-Z.=_-]{1,255}$/ + +interface RequestTokenArgs { + client_secret: string + mxid: string + sid: string + threepid: { + address: string + medium: string + } +} + +const schema = { + client_secret: false, + mxid: true, + sid: false, + threepid: true +} + +const unbind = ( + idServer: MatrixIdentityServer +): expressAppHandler => { + return (req, res) => { + idServer.authenticate(req, res, (data, id) => { + jsonContent(req, res, idServer.logger, (obj) => { + validateParameters(res, schema, obj, idServer.logger, (obj) => { + if ( + (obj as RequestTokenArgs).client_secret != null && + (obj as RequestTokenArgs).sid != null + ) { + const clientSecret = (obj as RequestTokenArgs).client_secret + const mxid = (obj as RequestTokenArgs).mxid + const sid = (obj as RequestTokenArgs).sid + if (!clientSecretRe.test(clientSecret)) { + send(res, 400, errMsg('invalidParam', 'invalid client_secret')) + } else if (!mxidRe.test(mxid)) { + send(res, 400, errMsg('invalidParam', 'invalid Matrix user ID')) + } else if (!sidRe.test(sid)) { + // istanbul ignore next + send(res, 400, errMsg('invalidParam', 'invalid session ID')) + } else { + idServer.db + .get('mappings', ['address'], { + session_id: sid, + client_secret: clientSecret + }) + .then((rows) => { + if (rows.length === 0) { + send( + res, + 403, + errMsg( + 'invalidParam', + 'invalid session ID or client_secret' + ) + ) + } else { + if ( + (obj as RequestTokenArgs).threepid.address !== + rows[0].address + ) { + send(res, 403, errMsg('invalidParam', 'invalid address')) + } else { + idServer.db + .deleteEqual('hashes', 'value', mxid) + .then(() => { + send(res, 200, {}) + }) + .catch((e) => { + // istanbul ignore next + idServer.logger.error( + 'Error deleting 3pid association', + e + ) + send(res, 500, errMsg('unknown', e.toString())) + }) + } + } + }) + .catch((e) => { + // istanbul ignore next + idServer.logger.error( + 'Error finding 3pid associated with this session_id or client_secret', + e + ) + send(res, 500, errMsg('unknown', e.toString())) + }) + } + } else { + // TODO : implement signature verification. If the request doesn't have a client_secret or sid, it should be signed + } + }) + }) + }) + } +} + +export default unbind diff --git a/packages/matrix-identity-server/src/__testData__/buildUserDB.ts b/packages/matrix-identity-server/src/__testData__/buildUserDB.ts index 671bffbe..8b40306e 100644 --- a/packages/matrix-identity-server/src/__testData__/buildUserDB.ts +++ b/packages/matrix-identity-server/src/__testData__/buildUserDB.ts @@ -11,10 +11,13 @@ const logger: TwakeLogger = getLogger() let created = false let matrixDbCreated = false -const createQuery = 'CREATE TABLE users (uid varchar(8), mobile varchar(12), mail varchar(32))' -const insertQuery = "INSERT INTO users VALUES('dwho', '33612345678', 'dwho@company.com')" -const insertQuery2 = "INSERT INTO users VALUES('rtyler', '33687654321', 'rtyler@company.com')" -const mCreateQuery = 'CREATE TABLE users (name text)' +const createQuery = + 'CREATE TABLE IF NOT EXISTS users (uid varchar(8), mobile varchar(12), mail varchar(32))' +const insertQuery = + "INSERT INTO users VALUES('dwho', '33612345678', 'dwho@company.com')" +const insertQuery2 = + "INSERT INTO users VALUES('rtyler', '33687654321', 'rtyler@company.com')" +const mCreateQuery = 'CREATE TABLE IF NOT EXISTS users (name text)' const mInsertQuery = "INSERT INTO users VALUES('@dwho:company.com')" // eslint-disable-next-line @typescript-eslint/promise-function-async @@ -24,25 +27,29 @@ const buildUserDB = (conf: Config): Promise => { return new Promise((resolve, reject) => { /* istanbul ignore else */ if (conf.userdb_engine === 'sqlite') { - userDb.ready.then(() => { - (userDb.db as UserDBSQLite).db?.run(createQuery, () => { - (userDb.db as UserDBSQLite).db?.run(insertQuery, () => { - (userDb.db as UserDBSQLite).db?.run(insertQuery2).close((err) => { - /* istanbul ignore if */ - if(err != null) { - reject(err) - } else { - logger.close() - created = true - resolve() - } + userDb.ready + .then(() => { + ;(userDb.db as UserDBSQLite).db?.run(createQuery, () => { + ;(userDb.db as UserDBSQLite).db?.run(insertQuery, () => { + ;(userDb.db as UserDBSQLite).db + ?.run(insertQuery2) + .close((err) => { + /* istanbul ignore if */ + if (err != null) { + reject(err) + } else { + logger.close() + created = true + resolve() + } + }) }) }) }) - }).catch(reject) + .catch(reject) } else { - (userDb.db as UserDBPg).db?.query(createQuery, () => { - (userDb.db as UserDBPg).db?.query(insertQuery, () => { + ;(userDb.db as UserDBPg).db?.query(createQuery, () => { + ;(userDb.db as UserDBPg).db?.query(insertQuery, () => { logger.close() created = true resolve() @@ -57,12 +64,12 @@ export const buildMatrixDb = (conf: Config): Promise => { if (matrixDbCreated) return Promise.resolve() const matrixDb = new sqlite3.Database(conf.matrix_database_host as string) return new Promise((resolve, reject) => { - /* istanbul ignore else */ - if (conf.matrix_database_engine === 'sqlite') { + /* istanbul ignore else */ + if (conf.matrix_database_engine === 'sqlite') { matrixDb.run(mCreateQuery, () => { matrixDb.run(mInsertQuery).close((err) => { /* istanbul ignore if */ - if(err != null) { + if (err != null) { reject(err) } else { matrixDbCreated = true diff --git a/packages/matrix-identity-server/src/__testData__/registerConf.json b/packages/matrix-identity-server/src/__testData__/registerConf.json index da7c067f..9c24b50d 100644 --- a/packages/matrix-identity-server/src/__testData__/registerConf.json +++ b/packages/matrix-identity-server/src/__testData__/registerConf.json @@ -3,6 +3,7 @@ "database_engine": "sqlite", "database_host": "./src/__testData__/test.db", "database_vacuum_delay": 7200, + "invitation_server_name": "matrix.to", "is_federated_identity_service": false, "key_delay": 3600, "keys_depth": 5, @@ -14,4 +15,4 @@ "template_dir": "./templates", "userdb_engine": "sqlite", "userdb_host": "./src/__testData__/test.db" -} \ No newline at end of file +} diff --git a/packages/matrix-identity-server/src/__testData__/termsConf.json b/packages/matrix-identity-server/src/__testData__/termsConf.json index 67b08b48..f46c77ea 100644 --- a/packages/matrix-identity-server/src/__testData__/termsConf.json +++ b/packages/matrix-identity-server/src/__testData__/termsConf.json @@ -3,6 +3,7 @@ "database_engine": "sqlite", "database_host": "./src/__testData__/terms.db", "database_vacuum_delay": 7200, + "invitation_server_name": "matrix.to", "is_federated_identity_service": false, "key_delay": 3600, "keys_depth": 5, @@ -38,4 +39,4 @@ "template_dir": "./templates", "userdb_engine": "sqlite", "userdb_host": "./src/__testData__/terms.db" -} \ No newline at end of file +} diff --git a/packages/matrix-identity-server/src/account/index.ts b/packages/matrix-identity-server/src/account/index.ts index df74753f..178c74fd 100644 --- a/packages/matrix-identity-server/src/account/index.ts +++ b/packages/matrix-identity-server/src/account/index.ts @@ -1,8 +1,10 @@ import type MatrixIdentityServer from '..' -import { send, type expressAppHandler } from '../utils' +import { send, type expressAppHandler } from '@twake/utils' import { type tokenContent } from './register' -const Account = (idServer: MatrixIdentityServer): expressAppHandler => { +const Account = ( + idServer: MatrixIdentityServer +): expressAppHandler => { return (req, res) => { idServer.authenticate(req, res, (idToken: tokenContent) => { send(res, 200, { user_id: idToken.sub }) diff --git a/packages/matrix-identity-server/src/account/logout.ts b/packages/matrix-identity-server/src/account/logout.ts index 52f01217..26549336 100644 --- a/packages/matrix-identity-server/src/account/logout.ts +++ b/packages/matrix-identity-server/src/account/logout.ts @@ -1,9 +1,10 @@ import type MatrixIdentityServer from '..' -import { send, type expressAppHandler } from '../utils' -import { errMsg } from '../utils/errors' +import { errMsg, send, type expressAppHandler } from '@twake/utils' import { type tokenContent } from './register' -const Logout = (idServer: MatrixIdentityServer): expressAppHandler => { +const Logout = ( + idServer: MatrixIdentityServer +): expressAppHandler => { return (req, res) => { // @ts-expect-error id is defined here idServer.authenticate(req, res, (idToken: tokenContent, id: string) => { diff --git a/packages/matrix-identity-server/src/account/register.ts b/packages/matrix-identity-server/src/account/register.ts index b96a3b92..f82a200d 100644 --- a/packages/matrix-identity-server/src/account/register.ts +++ b/packages/matrix-identity-server/src/account/register.ts @@ -3,12 +3,12 @@ import { type TwakeLogger } from '@twake/logger' import type IdentityServerDb from '../db' import { epoch, + errMsg, jsonContent, send, validateParameters, type expressAppHandler -} from '../utils' -import { errMsg } from '../utils/errors' +} from '@twake/utils' import validateMatrixToken from '../utils/validateMatrixToken' const schema = { @@ -25,8 +25,8 @@ export interface tokenContent { epoch: number } -const Register = ( - db: IdentityServerDb, +const Register = ( + db: IdentityServerDb, logger: TwakeLogger ): expressAppHandler => { const validateToken = validateMatrixToken(logger) diff --git a/packages/matrix-identity-server/src/additionalFeatures.test.ts b/packages/matrix-identity-server/src/additionalFeatures.test.ts index c633a320..795adb3c 100644 --- a/packages/matrix-identity-server/src/additionalFeatures.test.ts +++ b/packages/matrix-identity-server/src/additionalFeatures.test.ts @@ -90,12 +90,12 @@ describe('/_matrix/identity/v2/account/register', () => { const mockResponse = Promise.resolve({ ok: true, status: 200, - json: () => { - return { + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ sub: '@dwho:example.com', 'm.server': 'matrix.example.com:8448' - } - } + }) }) // @ts-expect-error mock is unknown fetch.mockImplementation(async () => await mockResponse) diff --git a/packages/matrix-identity-server/src/config.json b/packages/matrix-identity-server/src/config.json index 015c75d4..278cdcf7 100644 --- a/packages/matrix-identity-server/src/config.json +++ b/packages/matrix-identity-server/src/config.json @@ -12,6 +12,7 @@ "database_vacuum_delay": 3600, "federated_identity_services": null, "hashes_rate_limit": 100, + "invitation_server_name": "matrix.to", "is_federated_identity_service": false, "key_delay": 3600, "keys_depth": 5, diff --git a/packages/matrix-identity-server/src/cron/changePepper.test.ts b/packages/matrix-identity-server/src/cron/changePepper.test.ts index 11ce6173..ca8fa65b 100644 --- a/packages/matrix-identity-server/src/cron/changePepper.test.ts +++ b/packages/matrix-identity-server/src/cron/changePepper.test.ts @@ -28,7 +28,7 @@ describe('updateHashes', () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error // @ts-ignore run is a sqlite3 method only userDB.db.db.run( - 'CREATE TABLE users (uid varchar(8), mobile varchar(12), mail varchar(32))', + 'CREATE TABLE IF NOT EXISTS users (uid varchar(8), mobile varchar(12), mail varchar(32))', () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error // @ts-ignore same diff --git a/packages/matrix-identity-server/src/cron/changePepper.ts b/packages/matrix-identity-server/src/cron/changePepper.ts index 82ed86ef..508d359d 100644 --- a/packages/matrix-identity-server/src/cron/changePepper.ts +++ b/packages/matrix-identity-server/src/cron/changePepper.ts @@ -9,6 +9,7 @@ import updateHash, { type UpdatableFields } from '../lookup/updateHash' import MatrixDB from '../matrixDb' import { type Config, type DbGetResult } from '../types' import type UserDB from '../userdb' +import { toMatrixId } from '@twake/utils' export const dbFieldsToHash = ['mobile', 'mail'] @@ -66,9 +67,9 @@ export const filter = async ( } // eslint-disable-next-line @typescript-eslint/promise-function-async -const updateHashes = ( +const updateHashes = ( conf: Config, - db: IdentityServerDb, + db: IdentityServerDb, userDB: UserDB, logger: TwakeLogger ): Promise => { @@ -137,7 +138,7 @@ const updateHashes = ( db, logger, rows.reduce((res, row) => { - res[`@${row.uid as string}:${conf.server_name}`] = { + res[toMatrixId(row.uid as string, conf.server_name)] = { email: row.mail as string, phone: row.mobile as string, active: isMatrixDbAvailable ? (row.active as number) : 1 diff --git a/packages/matrix-identity-server/src/cron/check-quota.test.ts b/packages/matrix-identity-server/src/cron/check-quota.test.ts index 87ba89c6..90eacfc9 100644 --- a/packages/matrix-identity-server/src/cron/check-quota.test.ts +++ b/packages/matrix-identity-server/src/cron/check-quota.test.ts @@ -86,7 +86,7 @@ beforeAll(async () => { }) await new Promise((resolve, reject) => { testdb.run( - 'CREATE TABLE users (name varchar(64) PRIMARY KEY)', + 'CREATE TABLE IF NOT EXISTS users (name varchar(64) PRIMARY KEY)', (e: unknown) => { if (e !== null) reject( diff --git a/packages/matrix-identity-server/src/cron/check-quota.ts b/packages/matrix-identity-server/src/cron/check-quota.ts index 9ec13adc..e8ef2439 100644 --- a/packages/matrix-identity-server/src/cron/check-quota.ts +++ b/packages/matrix-identity-server/src/cron/check-quota.ts @@ -12,11 +12,11 @@ import type { * check user quota cron job. * * @param {Config} conf - the configuration. - * @param {IdentityServerDb} db - the identity server database. + * @param {IdentityServerDb} db - the identity server database. */ -export default async ( +export default async ( conf: Config, - db: IdentityServerDb, + db: IdentityServerDb, logger: TwakeLogger ): Promise => { try { @@ -114,12 +114,12 @@ const getUserUsage = async ( /** * Saves the media usage for a given user in the database. * - * @param {IdentityServerDb} db -the identity server database instance. + * @param {IdentityServerDb} db -the identity server database instance. * @param {string} userId - the user id of which to save the usage for. * @param {number } size - the total size of the media. */ -const saveUserUsage = async ( - db: IdentityServerDb, +const saveUserUsage = async ( + db: IdentityServerDb, userId: string, size: number ): Promise => { diff --git a/packages/matrix-identity-server/src/cron/index.test.ts b/packages/matrix-identity-server/src/cron/index.test.ts index d6ec99cb..e122c3c6 100644 --- a/packages/matrix-identity-server/src/cron/index.test.ts +++ b/packages/matrix-identity-server/src/cron/index.test.ts @@ -42,7 +42,7 @@ describe('cron tasks', () => { try { await new Promise((resolve, reject) => { testdb.run( - 'CREATE TABLE users (name varchar(64) PRIMARY KEY)', + 'CREATE TABLE IF NOT EXISTS users (name varchar(64) PRIMARY KEY)', (e: unknown) => { if (e !== null) reject( @@ -67,7 +67,7 @@ describe('cron tasks', () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error // @ts-ignore run is a sqlite3 method only userDB.db.db.run( - 'CREATE TABLE users (uid varchar(8), mobile varchar(12), mail varchar(32))', + 'CREATE TABLE IF NOT EXISTS users (uid varchar(8), mobile varchar(12), mail varchar(32))', () => { resolve() } diff --git a/packages/matrix-identity-server/src/cron/index.ts b/packages/matrix-identity-server/src/cron/index.ts index 3390b321..f9f19518 100644 --- a/packages/matrix-identity-server/src/cron/index.ts +++ b/packages/matrix-identity-server/src/cron/index.ts @@ -12,14 +12,14 @@ import checkQuota from './check-quota' import updateFederatedIdentityHashes from './update-federated-identity-hashes' import updateUsers from './updateUsers' -class CronTasks { +class CronTasks { tasks: ScheduledTask[] ready: Promise readonly options: Record = { timezone: 'GMT' } constructor( conf: Config, - db: IdentityServerDb, + db: IdentityServerDb, userDB: UserDB, logger: TwakeLogger ) { @@ -37,13 +37,13 @@ class CronTasks { * Initializes the cron tasks * * @param {Config} conf - the config - * @param {IdentityServerDb} db - the identity server db instance + * @param {IdentityServerDb} db - the identity server db instance * @param {UserDB} userDB - the user db instance * @param {TwakeLogger} logger - the logger */ private readonly init = async ( conf: Config, - db: IdentityServerDb, + db: IdentityServerDb, userDB: UserDB, logger: TwakeLogger ): Promise => { @@ -80,13 +80,13 @@ class CronTasks { * Update the hashes job. * * @param {Config} conf - the config - * @param {IdentityServerDb} db - the identity server db instance + * @param {IdentityServerDb} db - the identity server db instance * @param {UserDB} userDB - the user db instance * @param {TwakeLogger} logger - the logger */ private readonly _addUpdateHashesJob = async ( conf: Config, - db: IdentityServerDb, + db: IdentityServerDb, userDB: UserDB, logger: TwakeLogger ): Promise => { @@ -159,12 +159,12 @@ class CronTasks { * Adds the check user quota job * * @param {Config} conf - the configuration - * @param {IdentityServerDb} db - the identity server db instance + * @param {IdentityServerDb} db - the identity server db instance * @param {TwakeLogger} logger - the logger */ private readonly _addCheckUserQuotaJob = async ( conf: Config, - db: IdentityServerDb, + db: IdentityServerDb, logger: TwakeLogger ): Promise => { const cronString = conf.check_quota_cron ?? '0 0 0 * * *' diff --git a/packages/matrix-identity-server/src/cron/update-federated-identity-hashes.test.ts b/packages/matrix-identity-server/src/cron/update-federated-identity-hashes.test.ts index e52ff537..161231cf 100644 --- a/packages/matrix-identity-server/src/cron/update-federated-identity-hashes.test.ts +++ b/packages/matrix-identity-server/src/cron/update-federated-identity-hashes.test.ts @@ -5,7 +5,7 @@ import fetch from 'node-fetch' import defaultConfig from '../config.json' import { type Config } from '../types' import UserDB from '../userdb' -import { errCodes } from '../utils/errors' +import { errCodes } from '@twake/utils' import updateFederatedIdentityHashes from './update-federated-identity-hashes' jest.mock('node-fetch', () => { @@ -153,7 +153,7 @@ beforeAll((done) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error // @ts-ignore run is a sqlite3 method only userDB.db.db.run( - 'CREATE TABLE users (uid varchar(8), mobile varchar(12), mail varchar(32))', + 'CREATE TABLE IF NOT EXISTS users (uid varchar(8), mobile varchar(12), mail varchar(32))', () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error // @ts-ignore same diff --git a/packages/matrix-identity-server/src/cron/update-federated-identity-hashes.ts b/packages/matrix-identity-server/src/cron/update-federated-identity-hashes.ts index e2533bb1..e5ea24f1 100644 --- a/packages/matrix-identity-server/src/cron/update-federated-identity-hashes.ts +++ b/packages/matrix-identity-server/src/cron/update-federated-identity-hashes.ts @@ -9,6 +9,7 @@ import { import { type Config } from '../types' import type UserDB from '../userdb' import { dbFieldsToHash, filter } from './changePepper' +import { toMatrixId } from '@twake/utils' interface HashDetails { algorithms: string[] @@ -140,7 +141,7 @@ export default async ( logger ) ).reduce((acc, row) => { - acc[`@${row.uid as string}:${conf.server_name}`] = { + acc[toMatrixId(row.uid as string, conf.server_name)] = { email: row.mail as string, phone: row.mobile as string, active: isMatrixDbAvailable ? (row.active as number) : 1 diff --git a/packages/matrix-identity-server/src/cron/updateUsers.ts b/packages/matrix-identity-server/src/cron/updateUsers.ts index e7ed2cac..0c407b05 100644 --- a/packages/matrix-identity-server/src/cron/updateUsers.ts +++ b/packages/matrix-identity-server/src/cron/updateUsers.ts @@ -4,7 +4,7 @@ import updateHash, { type UpdatableFields } from '../lookup/updateHash' import MatrixDB from '../matrixDb' import { type Config, type DbGetResult } from '../types' import type UserDB from '../userdb' -import { epoch } from '../utils' +import { epoch, toMatrixId } from '@twake/utils' /** * updateUsers is a cron task that reads users from UserDB and find which of @@ -12,9 +12,9 @@ import { epoch } from '../utils' * @param idServer Matrix identity server * @returns Promise */ -const updateUsers = async ( +const updateUsers = async ( conf: Config, - db: IdentityServerDb, + db: IdentityServerDb, userDB: UserDB, logger: TwakeLogger ): Promise => { @@ -88,7 +88,7 @@ const updateUsers = async ( const timestamp = epoch() users.forEach((user) => { const uid = user.uid as string - const matrixAddress = `@${uid}:${conf.server_name}` + const matrixAddress = toMatrixId(uid, conf.server_name) const pos = knownUids.indexOf(uid) const isMatrixUser = matrixUsers.includes(uid) const data = { @@ -150,7 +150,8 @@ const updateUsers = async ( const seen: Record = {} knownUids.forEach((uid, i) => { if (!uids.includes(uid)) { - if (!seen[uid]) updates.push(setInactive(`@${uid}:${conf.server_name}`)) + if (!seen[uid]) + updates.push(setInactive(toMatrixId(uid, conf.server_name))) seen[uid] = true } }) diff --git a/packages/matrix-identity-server/src/db/README.md b/packages/matrix-identity-server/src/db/README.md index 4f9e61a8..fe358e7e 100644 --- a/packages/matrix-identity-server/src/db/README.md +++ b/packages/matrix-identity-server/src/db/README.md @@ -2,17 +2,16 @@ What we have to store: -| Object to store | Delay | Content | Additional index | -|:----------------------:|:-------:|-----------------------------------------------|:----------------:| -| Access token[^1] | 1 day | Data given by Matrix server | expires | -| Mail/phone attempts[^2]| 1 day | Mail, attempt, expires | | -| Registered mails/phones| 1 day | mail/phone, user, expires, hash256, hash512 | | -| One-Time-Token | 10 mn | JSON object | expires | -| Terms accepts | always | Version of policy accepted | | -| Pepper used in hash | 1 day | Must recalculate adress hashes at each change | | -| Previous pepper | 1 day | | | -| Last pepper change | 1 day | | | - +| Object to store | Delay | Content | Additional index | +| :---------------------: | :----: | --------------------------------------------- | :--------------: | +| Access token[^1] | 1 day | Data given by Matrix server | expires | +| Mail/phone attempts[^2] | 1 day | Mail, attempt, expires | | +| Registered mails/phones | 1 day | mail/phone, user, expires, hash256, hash512 | | +| One-Time-Token | 10 mn | JSON object | expires | +| Terms accepts | always | Version of policy accepted | | +| Pepper used in hash | 1 day | Must recalculate adress hashes at each change | | +| Previous pepper | 1 day | | | +| Last pepper change | 1 day | | | [^1]: token given after validating Matrix Server Token -[^2]: attempts to validate a phone or an email \ No newline at end of file +[^2]: attempts to validate a phone or an email diff --git a/packages/matrix-identity-server/src/db/index.test.ts b/packages/matrix-identity-server/src/db/index.test.ts index 8677a0ec..e3654deb 100644 --- a/packages/matrix-identity-server/src/db/index.test.ts +++ b/packages/matrix-identity-server/src/db/index.test.ts @@ -1,9 +1,10 @@ +/* eslint-disable @typescript-eslint/strict-boolean-expressions */ import { randomString } from '@twake/crypto' import { getLogger, type TwakeLogger } from '@twake/logger' -import fs from 'fs' import DefaultConfig from '../config.json' import { type Config, type DbGetResult } from '../types' import IdDb from './index' +import fs from 'fs' const baseConf: Config = { ...DefaultConfig, @@ -24,18 +25,17 @@ const logger: TwakeLogger = getLogger() describe('Id Server DB', () => { let idDb: IdDb - afterEach(() => { process.env.TEST_PG === 'yes' || fs.unlinkSync('./testdb.db') }) afterAll(() => { - idDb.close() logger.close() + // if (fs.existsSync('./testdb.db')) { + // fs.unlinkSync('./testdb.db') + // } }) - it('should have SQLite database initialized', (done) => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises idDb = new IdDb(baseConf, logger) idDb.ready .then(() => { @@ -53,11 +53,11 @@ describe('Id Server DB', () => { idDb.close() done() }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) it('should provide one-time-token', (done) => { @@ -77,7 +77,7 @@ describe('Id Server DB', () => { idDb .verifyOneTimeToken(token) .then((data) => { - done("Souldn't have find a value") + done("Shouldn't have found a value") }) .catch((e) => { clearTimeout(idDb.cleanJob) @@ -85,6 +85,31 @@ describe('Id Server DB', () => { done() }) }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + + it('should provide verification-token', (done) => { + idDb = new IdDb(baseConf, logger) + idDb.ready + .then(() => { + idDb + .createInvitationToken('randomMailorPhone', { a: 1 }) + .then((token) => { + expect(token).toMatch(/^[a-zA-Z0-9]+$/) + idDb + .verifyInvitationToken(token) + .then((data) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error + // @ts-ignore + expect(data.a).toEqual(1) + clearTimeout(idDb.cleanJob) + idDb.close() + done() + }) .catch((e) => done(e)) }) .catch((e) => done(e)) @@ -114,15 +139,13 @@ describe('Id Server DB', () => { idDb.close() done() }) - .catch((e) => done(e)) - }) - .catch((e) => { - done(e) + .catch(done) }) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) it('should update', (done) => { @@ -145,13 +168,13 @@ describe('Id Server DB', () => { idDb.close() done() }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) it('should return entry on update', (done) => { @@ -165,19 +188,106 @@ describe('Id Server DB', () => { .update('oneTimeTokens', { data: '{"a": 2}' }, 'id', token) .then((rows) => { expect(rows.length).toBe(1) - expect(rows[0].data).toEqual('{"a": 2}') // [OMH] Same as the test from get but directly from update. + expect(rows[0].data).toEqual('{"a": 2}') clearTimeout(idDb.cleanJob) + idDb.close() done() }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) + }) + + it('should update records matching both conditions', (done) => { + idDb = new IdDb(baseConf, logger) + idDb.ready + .then(() => { + idDb + .insert('roomTags', { id: 1, roomId: 1, authorId: 1, content: '' }) + .then(() => { + idDb + .updateAnd( + 'roomTags', + { id: 2 }, + { field: 'id', value: 1 }, + { field: 'roomId', value: 1 } + ) + .then((rows) => { + expect(rows[0].id).toEqual('2') + clearTimeout(idDb.cleanJob) + idDb.close() + done() + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + + it('should return entry on updateAnd', (done) => { + idDb = new IdDb(baseConf, logger) + idDb.ready + .then(() => { + idDb + .insert('roomTags', { id: 3, roomId: 1, authorId: 1, content: '' }) + .then(() => { + idDb + .updateAnd( + 'roomTags', + { id: 4 }, + { field: 'id', value: 3 }, + { field: 'roomId', value: 1 } + ) + .then((rows) => { + expect(rows.length).toBe(1) + expect(rows[0].id).toEqual('4') + clearTimeout(idDb.cleanJob) + idDb.close() + done() + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + + it('should not update records if conditions do not match', (done) => { + idDb = new IdDb(baseConf, logger) + idDb.ready + .then(() => { + idDb + .insert('roomTags', { id: 4, roomId: 1, authorId: 1, content: '' }) + .then(() => { + idDb + .updateAnd( + 'roomTags', + { authorId: 2 }, + { field: 'id', value: 4 }, + { field: 'roomId', value: 100 } + ) + .then(() => { + idDb + .get('roomTags', ['*'], { id: 4 }) + .then((rows) => { + expect(rows[0].authorId).toEqual('1') + clearTimeout(idDb.cleanJob) + idDb.close() + done() + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) }) it('should return entry on insert', (done) => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises idDb = new IdDb(baseConf, logger) idDb.ready .then(() => { @@ -192,9 +302,9 @@ describe('Id Server DB', () => { idDb.close() done() }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) it('should return count without value', (done) => { @@ -215,13 +325,13 @@ describe('Id Server DB', () => { idDb.close() done() }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) it('should return count with value', (done) => { @@ -247,15 +357,15 @@ describe('Id Server DB', () => { idDb.close() done() }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) it('should delete lower than value', (done) => { @@ -276,13 +386,13 @@ describe('Id Server DB', () => { idDb.close() done() }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) it('should delete lines with specified filters', (done) => { @@ -319,16 +429,63 @@ describe('Id Server DB', () => { idDb.close() done() }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) - it('should delete lines with specified filters', (done) => { + it('should delete records matching both conditions', (done) => { + idDb = new IdDb(baseConf, logger) + idDb.ready + .then(() => { + const idsNumber = 8 + const ids: string[] = [] + const insertsPromises: Array> = [] + for (let index = 0; index < idsNumber; index++) { + ids[index] = randomString(64) + insertsPromises[index] = idDb.insert('attempts', { + email: `email${index}`, + expires: index, + attempt: index + }) + } + + Promise.all(insertsPromises) + .then(() => { + idDb + .deleteEqualAnd( + 'attempts', + { field: 'email', value: 'email0' }, + { field: 'expires', value: '0' } + ) + .then(() => { + idDb + .getAll('attempts', ['email', 'expires', 'attempt']) + .then((rows) => { + expect(rows.length).toBe(idsNumber - 1) + rows.forEach((row) => { + expect(row.email).not.toEqual('email0') + expect(row.attempt).not.toEqual('0') + expect(row.expires).not.toEqual('0') + }) + clearTimeout(idDb.cleanJob) + idDb.close() + done() + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + + it('should not delete records if conditions do not match', (done) => { idDb = new IdDb(baseConf, logger) idDb.ready .then(() => { @@ -338,37 +495,43 @@ describe('Id Server DB', () => { for (let index = 0; index < idsNumber; index++) { ids[index] = randomString(64) - insertsPromises[index] = idDb.insert('accessTokens', { + insertsPromises[index] = idDb.insert('privateNotes', { id: ids[index], - data: `{${index % 2}}` + authorId: `author${index % 2}`, + content: `{${index % 2}}`, + targetId: 'targetC' }) } Promise.all(insertsPromises) .then(() => { idDb - .deleteWhere('accessTokens', { - field: 'data', - operator: '=', - value: '{0}' - }) + .deleteEqualAnd( + 'privateNotes', + { field: 'content', value: '{0}' }, + { field: 'authorId', value: 'authorC' } + ) .then(() => { idDb - .getAll('accessTokens', ['id', 'data']) + .getAll('privateNotes', [ + 'id', + 'authorId', + 'content', + 'targetId' + ]) .then((rows) => { - expect(rows.length).toBe(Math.floor(idsNumber / 2)) - expect(rows[0].data).toEqual('{1}') + expect(rows.length).toBe(idsNumber) clearTimeout(idDb.cleanJob) idDb.close() done() }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) - .catch((e) => done(e)) + .catch(done) }) test('OneTimeToken timeout', (done) => { @@ -399,4 +562,666 @@ describe('Id Server DB', () => { done(e) }) }) + + describe('testing sql requests', () => { + it('should create aliases if the fields contain periods', (done) => { + idDb = new IdDb(baseConf, logger) + idDb.ready + .then(() => { + idDb + .insert('accessTokens', { id: '1', data: '{}' }) + .then(() => { + idDb + .get('accessTokens', ['accessTokens.id'], { id: '1' }) + .then((rows) => { + expect(rows.length).toBe(1) + expect(rows[0].accessTokens_id).toEqual('1') + clearTimeout(idDb.cleanJob) + idDb.close() + done() + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + + it('should get entry with corresponding higher than condition', (done) => { + idDb = new IdDb(baseConf, logger) + idDb.ready + .then(() => { + idDb + .insert('accessTokens', { id: '1', data: '{}' }) + .then(() => { + idDb + .insert('accessTokens', { id: '2', data: '{}' }) + .then(() => { + idDb + .insert('accessTokens', { id: '3', data: '{}' }) + .then(() => { + idDb + .getHigherThan('accessTokens', ['id'], { + id: '1' + }) + .then((rows) => { + expect(rows.length).toBe(2) + expect(rows[0].id).toEqual('2') + expect(rows[1].id).toEqual('3') + clearTimeout(idDb.cleanJob) + idDb.close() + done() + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + + it('should get entry with corresponding multiple conditions', (done) => { + idDb = new IdDb(baseConf, logger) + idDb.ready + .then(() => { + idDb + .insert('accessTokens', { id: '1', data: '{}' }) + .then(() => { + idDb + .insert('accessTokens', { id: '2', data: '{}' }) + .then(() => { + idDb + .insert('accessTokens', { id: '3', data: '{}' }) + .then(() => { + idDb + .get('accessTokens', ['id'], { + id: ['1', '2'] + }) + .then((rows) => { + expect(rows.length).toBe(2) + expect(rows[0].id).toEqual('1') + expect(rows[1].id).toEqual('2') + clearTimeout(idDb.cleanJob) + idDb.close() + done() + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + + it('should sort entry by order', (done) => { + idDb = new IdDb(baseConf, logger) + idDb.ready + .then(() => { + idDb + .insert('accessTokens', { id: '2', data: '{}' }) + .then(() => { + idDb + .insert('accessTokens', { id: '1', data: '{}' }) + .then(() => { + idDb + .get('accessTokens', ['id'], { data: '{}' }, 'id DESC') + .then((rows) => { + expect(rows.length).toBe(2) + expect(rows[0].id).toEqual('2') + expect(rows[1].id).toEqual('1') + clearTimeout(idDb.cleanJob) + idDb.close() + done() + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + + it('should get entry with corresponding join conditions', (done) => { + idDb = new IdDb(baseConf, logger) + idDb.ready + .then(() => { + idDb + .insert('accessTokens', { id: '1', data: '{}' }) + .then(() => { + idDb + .insert('oneTimeTokens', { id: '1', expires: 0 }) + .then(() => { + idDb + .insert('accessTokens', { id: '2', data: '{}' }) + .then(() => { + idDb + .insert('oneTimeTokens', { id: '2', expires: 1 }) + .then(() => { + idDb + .getJoin( + ['accessTokens', 'oneTimeTokens'], + ['accessTokens.id', 'oneTimeTokens.expires'], + { + 'accessTokens.data': '{}', + 'oneTimeTokens.expires': 0 + }, + { 'accessTokens.id': 'oneTimeTokens.id' } + ) + .then((rows) => { + expect(rows.length).toBe(1) + expect(rows[0].accessTokens_id).toEqual('1') + expect(rows[0].oneTimeTokens_expires).toEqual(0) + clearTimeout(idDb.cleanJob) + idDb.close() + done() + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + + it('should get entry with corresponding equal or different conditions', (done) => { + idDb = new IdDb(baseConf, logger) + idDb.ready + .then(() => { + const id = randomString(64) + idDb + .insert('accessTokens', { id, data: '{}' }) + .then(() => { + idDb + .insert('accessTokens', { + id: 'wrong_id_1', + data: '{wrong_data}' + }) + .then(() => { + idDb + .insert('accessTokens', { + id: 'wrong_id_2', + data: '{}' + }) + .then(() => { + idDb + .getWhereEqualOrDifferent( + 'accessTokens', + ['id', 'data'], + { id }, + { data: '{}' } + ) + .then((rows) => { + expect(rows.length).toBe(2) + expect(rows[0].id).toEqual(id) + expect(rows[1].data).toEqual('{wrong_data}') + clearTimeout(idDb.cleanJob) + idDb.close() + done() + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + + it('should get entry with corresponding equal and higher conditions', (done) => { + idDb = new IdDb(baseConf, logger) + idDb.ready + .then(() => { + idDb + .insert('accessTokens', { id: '1', data: '{}' }) + .then(() => { + idDb + .insert('accessTokens', { + id: '2', + data: '{}' + }) + .then(() => { + idDb + .insert('accessTokens', { + id: '3', + data: '{wrong_data}' + }) + .then(() => { + idDb + .getWhereEqualAndHigher( + 'accessTokens', + ['id', 'data'], + { data: '{}' }, + { id: '1' } + ) + .then((rows) => { + expect(rows.length).toBe(1) + expect(rows[0].id).toEqual('2') + expect(rows[0].data).toEqual('{}') + clearTimeout(idDb.cleanJob) + idDb.close() + done() + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + + it('should not return a null row if the conditions are not matched', (done) => { + idDb = new IdDb(baseConf, logger) + idDb.ready + .then(() => { + idDb + .insert('accessTokens', { id: '1', data: '{}' }) + .then(() => { + idDb + .getMaxWhereEqual('accessTokens', 'id', ['id', 'data'], { + id: '2' + }) + .then((rows) => { + expect(rows.length).toBe(0) + clearTimeout(idDb.cleanJob) + idDb.close() + done() + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + + it('should get max entry with corresponding equal condition', (done) => { + idDb = new IdDb(baseConf, logger) + idDb.ready + .then(() => { + idDb + .insert('accessTokens', { id: '1', data: '{}' }) + .then(() => { + idDb + .insert('accessTokens', { id: '2', data: '{}' }) + .then(() => { + idDb + .insert('accessTokens', { id: '3', data: '{wrong_data}' }) + .then(() => { + idDb + .getMaxWhereEqual( + 'accessTokens', + 'id', + ['id', 'data'], + { + data: '{}' + } + ) + .then((rows) => { + expect(rows.length).toBe(1) + expect(rows[0].id).toEqual('2') + clearTimeout(idDb.cleanJob) + idDb.close() + done() + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + + it('should get max entry with corresponding lower condition and select all fields if not specified', (done) => { + idDb = new IdDb(baseConf, logger) + idDb.ready + .then(() => { + idDb + .insert('accessTokens', { id: '1', data: '{}' }) + .then(() => { + idDb + .insert('accessTokens', { id: '2', data: '{}' }) + .then(() => { + idDb + .insert('accessTokens', { id: '3', data: '{wrong_data}' }) + .then(() => { + idDb + .getMaxWhereEqual('accessTokens', 'id', [], { + data: '{}' + }) + .then((rows) => { + expect(rows.length).toBe(1) + expect(rows[0]).toHaveProperty('id') + expect(rows[0]).toHaveProperty('data') + expect(rows[0].id).toEqual('2') + expect(rows[0].data).toEqual('{}') + clearTimeout(idDb.cleanJob) + idDb.close() + done() + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + + it('should get max entry with multiple corresponding equal conditions', (done) => { + idDb = new IdDb(baseConf, logger) + idDb.ready + .then(() => { + idDb + .insert('accessTokens', { id: '1', data: '{}' }) + .then(() => { + idDb + .insert('accessTokens', { id: '2', data: '{...}' }) + .then(() => { + idDb + .insert('accessTokens', { id: '3', data: '{wrong_data}' }) + .then(() => { + idDb + .getMaxWhereEqual( + 'accessTokens', + 'id', + ['id', 'data'], + { + data: ['{}', '{...}'] + } + ) + .then((rows) => { + expect(rows.length).toBe(1) + expect(rows[0].id).toEqual('2') + clearTimeout(idDb.cleanJob) + idDb.close() + done() + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + + it('should get max entry with corresponding equal and lower conditions', (done) => { + idDb = new IdDb(baseConf, logger) + idDb.ready + .then(() => { + idDb + .insert('accessTokens', { id: '1', data: '{}' }) + .then(() => { + idDb + .insert('accessTokens', { id: '2', data: '{}' }) + .then(() => { + idDb + .insert('accessTokens', { id: '3', data: '{wrong_data}' }) + .then(() => { + idDb + .getMaxWhereEqualAndLower( + 'accessTokens', + 'id', + ['id', 'data'], + { + data: '{}' + }, + { id: '4' } + ) + .then((rows) => { + expect(rows.length).toBe(1) + expect(rows[0].id).toEqual('2') + expect(rows[0].data).toEqual('{}') + clearTimeout(idDb.cleanJob) + idDb.close() + done() + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + + it('should get min entry with corresponding equal and higher conditions', (done) => { + idDb = new IdDb(baseConf, logger) + idDb.ready + .then(() => { + idDb + .insert('accessTokens', { id: '1', data: '{wrong_data}' }) + .then(() => { + idDb + .insert('accessTokens', { id: '2', data: '{}' }) + .then(() => { + idDb + .insert('accessTokens', { id: '3', data: '{}' }) + .then(() => { + idDb + .getMinWhereEqualAndHigher( + 'accessTokens', + 'id', + ['id', 'data'], + { + data: '{}' + }, + { id: '0' } + ) + .then((rows) => { + expect(rows.length).toBe(1) + expect(rows[0].id).toEqual('2') + expect(rows[0].data).toEqual('{}') + clearTimeout(idDb.cleanJob) + idDb.close() + done() + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + + it('should get max entry with corresponding equal and lower conditions on multiple joined tables', (done) => { + idDb = new IdDb(baseConf, logger) + idDb.ready + .then(() => { + idDb + .insert('accessTokens', { id: '1', data: '{}' }) + .then(() => { + idDb + .insert('oneTimeTokens', { id: '1', expires: 999 }) + .then(() => { + idDb + .insert('accessTokens', { id: '2', data: '{}' }) + .then(() => { + idDb + .insert('oneTimeTokens', { id: '2', expires: 999 }) + .then(() => { + idDb + .insert('accessTokens', { + id: '3', + data: '{wrong_data}' + }) + .then(() => { + idDb + .insert('oneTimeTokens', { + id: '3', + expires: 999 + }) + .then(() => { + idDb + .insert('accessTokens', { + id: '4', + data: '{wrong_data}' + }) + .then(() => { + idDb + .insert('oneTimeTokens', { + id: '4', + expires: 1001 + }) + .then(() => { + idDb + .getMaxWhereEqualAndLowerJoin( + ['accessTokens', 'oneTimeTokens'], + 'accessTokens.id', + [ + 'accessTokens.id', + 'accessTokens.data', + 'oneTimeTokens.expires' + ], + { + 'accessTokens.data': '{}' + }, + { 'oneTimeTokens.expires': 1000 }, + { + 'accessTokens.id': + 'oneTimeTokens.id' + } + ) + .then((rows) => { + expect(rows.length).toBe(1) + expect( + rows[0].accessTokens_id + ).toEqual('2') + expect( + rows[0].accessTokens_data + ).toEqual('{}') + expect( + rows[0].oneTimeTokens_expires + ).toBeLessThan(1000) + clearTimeout(idDb.cleanJob) + idDb.close() + done() + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + }) + + it('should provide ephemeral Keypair', (done) => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + idDb = new IdDb(baseConf, logger) + idDb.ready + .then(() => { + idDb + .createKeypair('shortTerm', 'curve25519') + .then((_key) => { + expect(_key.keyId).toMatch(/^(ed25519|curve25519):[A-Za-z0-9_-]+$/) + clearTimeout(idDb.cleanJob) + idDb.close() + done() + }) + .catch((e) => { + done(e) + }) + }) + .catch(done) + }) + + it('should return entry when creating new keyPair ', (done) => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + idDb = new IdDb(baseConf, logger) + idDb.ready + .then(() => { + idDb + .createKeypair('shortTerm', 'curve25519') + .then((_key) => { + idDb + .get('shortTermKeypairs', ['keyID'], {}) + .then((rows) => { + expect(rows.length).toBe(1) + expect(rows[0].keyID).toEqual(_key.keyId) + clearTimeout(idDb.cleanJob) + idDb.close() + done() + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + + it('should delete a key from the shortKey pairs table', (done) => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + idDb = new IdDb(baseConf, logger) + idDb.ready + .then(() => { + idDb + .createKeypair('shortTerm', 'ed25519') + .then((key1) => { + idDb + .createKeypair('shortTerm', 'curve25519') + .then((key2) => { + idDb + .deleteKey(key1.keyId) + .then(() => { + idDb + .get('shortTermKeypairs', ['keyID'], {}) + .then((rows) => { + expect(rows.length).toBe(1) + expect(rows[0].keyID).toEqual(key2.keyId) + clearTimeout(idDb.cleanJob) + idDb.close() + done() + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) + .catch(done) + }) }) diff --git a/packages/matrix-identity-server/src/db/index.ts b/packages/matrix-identity-server/src/db/index.ts index 2acd9efe..065cb5c3 100644 --- a/packages/matrix-identity-server/src/db/index.ts +++ b/packages/matrix-identity-server/src/db/index.ts @@ -1,94 +1,202 @@ -import { randomString } from '@twake/crypto' +import { generateKeyPair, randomString } from '@twake/crypto' import { type TwakeLogger } from '@twake/logger' import { type Config, type DbGetResult } from '../types' -import { epoch } from '../utils' +import { epoch } from '@twake/utils' import Pg from './sql/pg' +import { type ISQLCondition } from './sql/sql' import Sqlite from './sql/sqlite' export type SupportedDatabases = 'sqlite' | 'pg' export type Collections = | 'accessTokens' - | 'oneTimeTokens' + | 'activeContacts' | 'attempts' - | 'keys' + | 'oneTimeTokens' | 'hashes' + | 'invitationTokens' + | 'keys' + | 'longTermKeypairs' + | 'mappings' | 'privateNotes' | 'roomTags' + | 'shortTermKeypairs' | 'userHistory' + | 'userPolicies' | 'userQuotas' + | 'activeContacts' const cleanByExpires: Collections[] = ['oneTimeTokens', 'attempts'] -type sqlComparaisonOperator = '=' | '!=' | '>' | '<' | '>=' | '<=' | '<>' +const tables: Record = { + accessTokens: 'id varchar(64) PRIMARY KEY, data text', + activeContacts: 'userId text PRIMARY KEY, contacts text', + attempts: 'email text PRIMARY KEY, expires int, attempt int', + oneTimeTokens: 'id varchar(64) PRIMARY KEY, expires int, data text', + hashes: + 'hash varchar(48) PRIMARY KEY, pepper varchar(32), type varchar(8), value text, active integer', + invitationTokens: 'id varchar(64) PRIMARY KEY, address text, data text', + keys: 'name varchar(32) PRIMARY KEY, data text', + longTermKeypairs: + 'name text PRIMARY KEY, keyID varchar(64), public text, private text', + mappings: + 'client_secret varchar(255) PRIMARY KEY, session_id varchar(12), medium varchar(8), valid integer, address text, submit_time integer, send_attempt integer', + privateNotes: + 'id varchar(64) PRIMARY KEY, authorId varchar(64), content text, targetId varchar(64)', + roomTags: + 'id varchar(64) PRIMARY KEY, authorId varchar(64), content text, roomId varchar(64)', + shortTermKeypairs: + 'keyID varchar(64) PRIMARY KEY, public text, private text, active integer', + userHistory: 'address text PRIMARY KEY, active integer, timestamp integer', + userPolicies: + 'user_id text, policy_name text, accepted integer, PRIMARY KEY (user_id, policy_name)', + userQuotas: 'user_id varchar(64) PRIMARY KEY, size int' +} -export interface ISQLCondition { - field: string - operator: sqlComparaisonOperator - value: string | number +const indexes: Partial> = { + attempts: ['expires'], + invitationTokens: ['address'], + oneTimeTokens: ['expires'], + userHistory: ['timestamp'] } -type Insert = ( - table: Collections, +const initializeValues: Partial< + Record>> +> = { + keys: [ + { name: 'pepper', data: '' }, + { name: 'previousPepper', data: '' } + ] +} + +interface keyPair { + publicKey: string + privateKey: string + keyId: string +} + +type Insert = ( + table: T, values: Record ) => Promise -type Update = ( - table: Collections, +type Update = ( + table: T, values: Record, field: string, value: string | number ) => Promise -type Get = ( - table: Collections, +type UpdateAnd = ( + table: T, + values: Record, + condition1: { field: string; value: string | number }, + condition2: { field: string; value: string | number } +) => Promise +type Get = ( + table: T, fields: string[], filterFields: Record>, order?: string ) => Promise -type GetCount = ( - table: Collections, +type Get2 = ( + table: T, + fields: string[], + filterFields1: Record>, + filterFields2: Record>, + order?: string +) => Promise +type GetJoin = ( + tables: T[], + fields: string[], + filterFields: Record>, + joinFields: Record, + order?: string +) => Promise +type GetMinMax = ( + table: T, + targetField: string, + fields: string[], + filterFields: Record>, + order?: string +) => Promise +type GetMinMax2 = ( + table: T, + targetField: string, + fields: string[], + filterFields1: Record>, + filterFields2: Record>, + order?: string +) => Promise +type GetMinMaxJoin2 = ( + tables: T[], + targetField: string, + fields: string[], + filterFields1: Record>, + filterFields2: Record>, + joinFields: Record, + order?: string +) => Promise +type GetCount = ( + table: T, field: string, value?: string | number | string[] ) => Promise -type GetAll = ( - table: Collections, +type GetAll = ( + table: T, fields: string[], order?: string ) => Promise -type Match = ( - table: Collections, +type Match = ( + table: T, fields: string[], searchFields: string[], value: string | number ) => Promise -type DeleteEqual = ( - table: Collections, +type DeleteEqual = ( + table: T, field: string, value: string | number ) => Promise -type DeleteLowerThan = ( - table: Collections, +type DeleteEqualAnd = ( + table: T, + condition1: { + field: string + value: string | number | Array + }, + condition2: { field: string; value: string | number | Array } +) => Promise +type DeleteLowerThan = ( + table: T, field: string, value: string | number ) => Promise -type DeleteWhere = ( - table: string, +type DeleteWhere = ( + table: T, conditions: ISQLCondition | ISQLCondition[] ) => Promise -export interface IdDbBackend { +export interface IdDbBackend { ready: Promise createDatabases: (conf: Config, ...args: any) => Promise - insert: Insert - get: Get - getCount: GetCount - getAll: GetAll - getHigherThan: Get - match: Match - update: Update - deleteEqual: DeleteEqual - deleteLowerThan: DeleteLowerThan - deleteWhere: DeleteWhere + insert: Insert + get: Get + getJoin: GetJoin + getWhereEqualOrDifferent: Get2 + getWhereEqualAndHigher: Get2 + getMaxWhereEqual: GetMinMax + getMaxWhereEqualAndLower: GetMinMax2 + getMinWhereEqualAndHigher: GetMinMax2 + getMaxWhereEqualAndLowerJoin: GetMinMaxJoin2 + getCount: GetCount + getAll: GetAll + getHigherThan: Get + match: Match + update: Update + updateAnd: UpdateAnd + deleteEqual: DeleteEqual + deleteEqualAnd: DeleteEqualAnd + deleteLowerThan: DeleteLowerThan + deleteWhere: DeleteWhere close: () => void } export type InsertType = ( @@ -96,13 +204,23 @@ export type InsertType = ( values: Array ) => Promise -class IdentityServerDb implements IdDbBackend { +class IdentityServerDb + implements IdDbBackend +{ ready: Promise - db: IdDbBackend + db: IdDbBackend cleanJob?: NodeJS.Timeout - cleanByExpires: Collections[] + cleanByExpires: Array - constructor(conf: Config, private readonly logger: TwakeLogger) { + constructor( + conf: Config, + private readonly logger: TwakeLogger, + additionnalTables?: Record, + additionnalIndexes?: Partial>, + additionnalInitializeValues?: Partial< + Record>> + > + ) { this.cleanByExpires = cleanByExpires let Module /* istanbul ignore next */ @@ -120,7 +238,26 @@ class IdentityServerDb implements IdDbBackend { throw new Error(`Unsupported database type ${conf.database_engine}`) } } - this.db = new Module(conf, this.logger) + + const allTables = + additionnalTables != null ? { ...tables, ...additionnalTables } : tables + const allIndexes = + additionnalIndexes != null + ? { ...indexes, ...additionnalIndexes } + : indexes + const allInitializeValues = + additionnalInitializeValues != null + ? { ...initializeValues, ...additionnalInitializeValues } + : initializeValues + this.db = new Module( + conf, + this.logger, + allTables as Record, + allIndexes as Partial>, + allInitializeValues as Partial< + Record>> + > + ) this.ready = new Promise((resolve, reject) => { this.db.ready .then(() => { @@ -162,13 +299,13 @@ class IdentityServerDb implements IdDbBackend { } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async - insert(table: Collections, values: Record) { + insert(table: Collections | T, values: Record) { return this.db.insert(table, values) } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async update( - table: Collections, + table: Collections | T, values: Record, field: string, value: string | number @@ -176,18 +313,150 @@ class IdentityServerDb implements IdDbBackend { return this.db.update(table, values, field, value) } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + updateAnd( + table: Collections | T, + values: Record, + condition1: { field: string; value: string | number }, + condition2: { field: string; value: string | number } + ) { + return this.db.updateAnd(table, values, condition1, condition2) + } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async get( - table: Collections, + table: Collections | T, + fields: string[], + filterFields: Record>, + order?: string + ) { + return this.db.get(table, fields, filterFields, order) + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + getJoin( + table: Array, fields: string[], - filterFields: Record> + filterFields: Record>, + joinFields: Record, + order?: string ) { - return this.db.get(table, fields, filterFields) + return this.db.getJoin(table, fields, filterFields, joinFields, order) + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + getWhereEqualOrDifferent( + table: Collections | T, + fields: string[], + filterFields1: Record>, + filterFields2: Record>, + order?: string + ) { + return this.db.getWhereEqualOrDifferent( + table, + fields, + filterFields1, + filterFields2, + order + ) + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + getWhereEqualAndHigher( + table: Collections | T, + fields: string[], + filterFields1: Record>, + filterFields2: Record>, + order?: string + ) { + return this.db.getWhereEqualAndHigher( + table, + fields, + filterFields1, + filterFields2, + order + ) + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + getMaxWhereEqual( + table: Collections | T, + targetField: string, + fields: string[], + filterFields: Record>, + order?: string + ) { + return this.db.getMaxWhereEqual( + table, + targetField, + fields, + filterFields, + order + ) + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + getMaxWhereEqualAndLower( + table: Collections | T, + targetField: string, + fields: string[], + filterFields1: Record>, + filterFields2: Record>, + order?: string + ) { + return this.db.getMaxWhereEqualAndLower( + table, + targetField, + fields, + filterFields1, + filterFields2, + order + ) + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + getMinWhereEqualAndHigher( + table: Collections | T, + targetField: string, + fields: string[], + filterFields1: Record>, + filterFields2: Record>, + order?: string + ) { + return this.db.getMinWhereEqualAndHigher( + table, + targetField, + fields, + filterFields1, + filterFields2, + order + ) + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + getMaxWhereEqualAndLowerJoin( + tables: Array, + targetField: string, + fields: string[], + filterFields1: Record>, + filterFields2: Record>, + joinFields: Record, + order?: string + ) { + return this.db.getMaxWhereEqualAndLowerJoin( + tables, + targetField, + fields, + filterFields1, + filterFields2, + joinFields, + order + ) } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async getCount( - table: Collections, + table: Collections | T, field: string, value?: string | number | string[] ) { @@ -195,13 +464,13 @@ class IdentityServerDb implements IdDbBackend { } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async - getAll(table: Collections, fields: string[], order?: string) { + getAll(table: Collections | T, fields: string[], order?: string) { return this.db.getAll(table, fields, order) } // eslint-disable-next-line @typescript-eslint/promise-function-async getHigherThan( - table: Collections, + table: Collections | T, fields: string[], filterFields: Record>, order?: string @@ -211,7 +480,7 @@ class IdentityServerDb implements IdDbBackend { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async match( - table: Collections, + table: Collections | T, fields: string[], searchFields: string[], value: string | number @@ -220,32 +489,62 @@ class IdentityServerDb implements IdDbBackend { } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async - deleteEqual(table: Collections, field: string, value: string | number) { + deleteEqual(table: Collections | T, field: string, value: string | number) { return this.db.deleteEqual(table, field, value) } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async - deleteLowerThan(table: Collections, field: string, value: string | number) { + deleteEqualAnd( + table: Collections | T, + condition1: { + field: string + value: string | number | Array + }, + condition2: { + field: string + value: string | number | Array + } + ) { + return this.db.deleteEqualAnd(table, condition1, condition2) + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + deleteLowerThan( + table: Collections | T, + field: string, + value: string | number + ) { return this.db.deleteLowerThan(table, field, value) } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async - deleteWhere(table: string, conditions: ISQLCondition | ISQLCondition[]) { + deleteWhere( + table: Collections | T, + conditions: ISQLCondition | ISQLCondition[] + ) { // Deletes from table where filters correspond to values // Size of filters and values must be the same return this.db.deleteWhere(table, conditions) } // eslint-disable-next-line @typescript-eslint/promise-function-async - createOneTimeToken(data: object, expires?: number): Promise { + createOneTimeToken( + data: object, + expires?: number, + nextLink?: string + ): Promise { /* istanbul ignore if */ if (this.db == null) { throw new Error('Wait for database to be ready') } + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (nextLink) { + data = { ...data, next_link: nextLink } + } const id = randomString(64) // default: expires in 600 s const expiresForDb = - epoch() + (expires != null && expires > 0 ? expires : 600) + epoch() + 1000 * (expires != null && expires > 0 ? expires : 600) return new Promise((resolve, reject) => { this.db .insert('oneTimeTokens', { @@ -259,6 +558,8 @@ class IdentityServerDb implements IdDbBackend { .catch((err) => { /* istanbul ignore next */ this.logger.error('Failed to insert token', err) + /* istanbul ignore next */ + reject(err) }) }) } @@ -269,6 +570,59 @@ class IdentityServerDb implements IdDbBackend { return this.createOneTimeToken(data, expires) } + // eslint-disable-next-line @typescript-eslint/promise-function-async + createInvitationToken(address: string, data: object): Promise { + /* istanbul ignore if */ + if (this.db == null) { + throw new Error('Wait for database to be ready') + } + const id = randomString(64) + return new Promise((resolve, reject) => { + this.db + .insert('invitationTokens', { + id, + address, + data: JSON.stringify(data) + }) + .then(() => { + this.logger.info(`Invitation token created for ${address}`) + resolve(id) + }) + .catch((err) => { + /* istanbul ignore next */ + this.logger.error('Failed to insert token', err) + /* istanbul ignore next */ + reject(err) + }) + }) + } + + // eslint-disable-next-line @typescript-eslint/promise-function-async + verifyInvitationToken(id: string): Promise { + /* istanbul ignore if */ + if (this.db == null) { + throw new Error('Wait for database to be ready') + } + return new Promise((resolve, reject) => { + this.db + .get('invitationTokens', ['data', 'address'], { id }) + .then((rows) => { + /* istanbul ignore else */ + if (rows.length > 0) { + resolve(JSON.parse(rows[0].data as string)) + } else { + reject(new Error('Unknown token')) + } + }) + .catch((e) => { + /* istanbul ignore next */ + this.logger.error('Failed to get token', e) + /* istanbul ignore next */ + reject(e) + }) + }) + } + // eslint-disable-next-line @typescript-eslint/promise-function-async verifyToken(id: string): Promise { /* istanbul ignore if */ @@ -285,12 +639,13 @@ class IdentityServerDb implements IdDbBackend { } else { reject( new Error( - 'Token expired' + (rows[0].expires as number).toString() + 'Token expired' + (rows[0]?.expires as number)?.toString() ) ) } }) .catch((e) => { + /* istanbul ignore next */ reject(e) }) }) @@ -340,12 +695,96 @@ class IdentityServerDb implements IdDbBackend { }) } + // eslint-disable-next-line @typescript-eslint/promise-function-async + getKeys(type: 'current' | 'previous'): Promise { + /* istanbul ignore if */ + if (this.db == null) { + throw new Error('Wait for database to be ready') + } + return new Promise((resolve, reject) => { + const _type = type === 'current' ? 'currentKey' : 'previousKey' + this.db + .get('longTermKeypairs', ['keyID', 'public', 'private'], { + name: _type + }) + .then((rows) => { + if (rows.length === 0) { + reject(new Error(`No ${_type} found`)) + } + resolve({ + keyId: rows[0].keyID as string, + publicKey: rows[0].public as string, + privateKey: rows[0].private as string + }) + }) + .catch((e) => { + /* istanbul ignore next */ + reject(e) + }) + }) + } + + // eslint-disable-next-line @typescript-eslint/promise-function-async + createKeypair( + type: 'longTerm' | 'shortTerm', + algorithm: 'ed25519' | 'curve25519' + ): Promise { + /* istanbul ignore if */ + if (this.db == null) { + throw new Error('Wait for database to be ready') + } + const keyPair = generateKeyPair(algorithm) + if (type === 'longTerm') { + throw new Error('Long term key pairs are not supported') + } + const _type = 'shortTermKeypairs' + return new Promise((resolve, reject) => { + this.db + .insert(_type, { + keyID: keyPair.keyId, + public: keyPair.publicKey, + private: keyPair.privateKey + }) + .then(() => { + resolve(keyPair) + }) + .catch((err) => { + /* istanbul ignore next */ + this.logger.error('Failed to insert ephemeral Key Pair', err) + /* istanbul ignore next */ + reject(err) + }) + }) + } + + // Deletes a short term key pair from the database + // eslint-disable-next-line @typescript-eslint/promise-function-async + deleteKey(_keyID: string): Promise { + /* istanbul ignore if */ + if (this.db == null) { + throw new Error('Wait for database to be ready') + } + return new Promise((resolve, reject) => { + this.db + .deleteEqual('shortTermKeypairs', 'KeyID', _keyID) + .then(() => { + resolve() + }) + .catch((e) => { + /* istanbul ignore next */ + this.logger.info(`Key ${_keyID} already deleted`, e) + /* istanbul ignore next */ + resolve() + }) + }) + } + dbMaintenance(delay: number): void { const _vacuum = async (): Promise => { /* istanbul ignore next */ await Promise.all( // eslint-disable-next-line @typescript-eslint/promise-function-async - cleanByExpires.map((table) => { + this.cleanByExpires.map((table) => { return this.deleteLowerThan(table, 'expires', epoch()) }) ) diff --git a/packages/matrix-identity-server/src/db/sql/_createTables.ts b/packages/matrix-identity-server/src/db/sql/_createTables.ts index 57298312..d2d85854 100644 --- a/packages/matrix-identity-server/src/db/sql/_createTables.ts +++ b/packages/matrix-identity-server/src/db/sql/_createTables.ts @@ -1,21 +1,18 @@ import { type TwakeLogger } from '@twake/logger' -import { type Collections } from '..' import type Pg from './pg' import type SQLite from './sqlite' -const createTables = ( - db: SQLite | Pg, - tables: Record, - indexes: Partial>, - initializeValues: Partial< - Record>> - >, +function createTables( + db: SQLite | Pg, + tables: Record, + indexes: Partial>, + initializeValues: Partial>>>, logger: TwakeLogger, resolve: () => void, reject: (e: Error) => void -): void => { +): void { const promises: Array> = [] - Object.keys(tables).forEach((table) => { + ;(Object.keys(tables) as T[]).forEach((table: T) => { promises.push( new Promise((_resolve, _reject) => { db.exists(table) @@ -23,13 +20,13 @@ const createTables = ( /* istanbul ignore else */ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (!count) { db.rawQuery( - `CREATE TABLE ${table}(${tables[table as keyof typeof tables]})` + `CREATE TABLE IF NOT EXISTS ${table}(${tables[table]})` ) // eslint-disable-next-line @typescript-eslint/promise-function-async .then(() => Promise.all( - ((indexes[table as Collections] as string[]) != null - ? (indexes[table as Collections] as string[]) + ((indexes[table] as string[]) != null + ? (indexes[table] as string[]) : [] ).map< Promise @@ -37,7 +34,7 @@ const createTables = ( >((index) => db .rawQuery( - `CREATE INDEX i_${table}_${index} ON ${table} (${index})` + `CREATE INDEX IF NOT EXISTS i_${table}_${index} ON ${table} (${index})` ) .catch((e) => { /* istanbul ignore next */ @@ -49,8 +46,8 @@ const createTables = ( // eslint-disable-next-line @typescript-eslint/promise-function-async .then(() => Promise.all( - (initializeValues[table as Collections] != null - ? (initializeValues[table as Collections] as Array< + (initializeValues[table] != null + ? (initializeValues[table] as Array< Record >) : [] @@ -63,8 +60,8 @@ const createTables = ( .then(() => { _resolve() }) - // istanbul ignore next .catch((e) => { + /* istanbul ignore next */ _reject(e) }) } else { @@ -80,9 +77,10 @@ const createTables = ( .then(() => { resolve() }) - // istanbul ignore next .catch((e) => { + /* istanbul ignore next */ logger.error('Unable to create tables', e) + /* istanbul ignore next */ reject(e) }) } diff --git a/packages/matrix-identity-server/src/db/sql/pg.ts b/packages/matrix-identity-server/src/db/sql/pg.ts index f5c30d28..97773e5a 100644 --- a/packages/matrix-identity-server/src/db/sql/pg.ts +++ b/packages/matrix-identity-server/src/db/sql/pg.ts @@ -1,22 +1,23 @@ +/* eslint-disable @typescript-eslint/strict-boolean-expressions */ /* eslint-disable @typescript-eslint/promise-function-async */ /* istanbul ignore file */ import { type TwakeLogger } from '@twake/logger' import { type ClientConfig, type Pool as PgPool } from 'pg' -import { type Collections, type ISQLCondition, type IdDbBackend } from '..' +import { type IdDbBackend } from '..' import { type Config, type DbGetResult } from '../../types' import createTables from './_createTables' -import SQL from './sql' +import SQL, { type ISQLCondition } from './sql' export type PgDatabase = PgPool -class Pg extends SQL implements IdDbBackend { +class Pg extends SQL implements IdDbBackend { declare db?: PgDatabase createDatabases( conf: Config, - tables: Record, - indexes: Partial>, + tables: Record, + indexes: Partial>, initializeValues: Partial< - Record>> + Record>> >, logger: TwakeLogger ): Promise { @@ -88,7 +89,7 @@ class Pg extends SQL implements IdDbBackend { return this.db.query(query) } - exists(table: string): Promise { + exists(table: T): Promise { return new Promise((resolve, reject) => { if (this.db != null) { this.db.query( @@ -108,7 +109,7 @@ class Pg extends SQL implements IdDbBackend { } insert( - table: string, + table: T, values: Record ): Promise { return new Promise((resolve, reject) => { @@ -138,7 +139,7 @@ class Pg extends SQL implements IdDbBackend { } update( - table: Collections, + table: string, values: Record, field: string, value: string | number @@ -171,11 +172,54 @@ class Pg extends SQL implements IdDbBackend { }) } + updateAnd( + table: T, + values: Record, + condition1: { field: string; value: string | number }, + condition2: { field: string; value: string | number } + ): Promise { + return new Promise((resolve, reject) => { + /* istanbul ignore if */ + if (this.db == null) { + reject(new Error('Wait for database to be ready')) + } else { + const names: string[] = [] + const vals: + | (string[] & Array) + | (number[] & Array) = [] + Object.keys(values).forEach((k) => { + names.push(k) + vals.push(values[k]) + }) + vals.push(condition1.value, condition2.value) + this.db.query( + `UPDATE ${table} SET ${names + .map((name, i) => `${name}=$${i + 1}`) + .join(',')} WHERE ${condition1.field}=$${vals.length - 1} AND ${ + condition2.field + }=$${vals.length} RETURNING *;`, + vals, + (err, rows) => { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + err ? reject(err) : resolve(rows.rows) + } + ) + } + }) + } + _get( - op: string, - table: string, + tables: T[], fields?: string[], - filterFields?: Record>, + op1?: string, + filterFields1?: Record>, + op2?: string, + linkop1?: string, + filterFields2?: Record>, + op3?: string, + linkop2?: string, + filterFields3?: Record>, + joinFields?: Record, order?: string ): Promise { return new Promise((resolve, reject) => { @@ -187,9 +231,25 @@ class Pg extends SQL implements IdDbBackend { const values: string[] = [] if (fields == null || fields.length === 0) { fields = ['*'] + } else { + // Generate aliases for fields containing periods + fields = fields.map((field) => { + if (field.includes('.')) { + const alias = field.replace(/\./g, '_') + return `${field} AS ${alias}` + } + return field + }) } - if (filterFields != null) { - let index = 0 + + let index: number = 0 + + const buildCondition = ( + op: string, + filterFields: Record> + ): string => { + let localCondition = '' + Object.keys(filterFields) .filter( (key) => @@ -197,30 +257,81 @@ class Pg extends SQL implements IdDbBackend { filterFields[key].toString() !== [].toString() ) .forEach((key) => { - condition += condition === '' ? 'WHERE ' : ' AND ' + localCondition += localCondition !== '' ? ' AND ' : '' if (Array.isArray(filterFields[key])) { - condition += `${(filterFields[key] as Array) + localCondition += `(${( + filterFields[key] as Array + ) .map((val) => { index++ values.push(val.toString()) return `${key}${op}$${index}` }) - .join(' OR ')}` + .join(' OR ')})` } else { index++ values.push(filterFields[key].toString()) - condition += `${key}${op}$${index}` + localCondition += `${key}${op}$${index}` } }) + return localCondition + } + + const condition1 = + op1 != null && + filterFields1 != null && + Object.keys(filterFields1).length > 0 + ? buildCondition(op1, filterFields1) + : '' + const condition2 = + op2 != null && + linkop1 != null && + filterFields2 != null && + Object.keys(filterFields2).length > 0 + ? buildCondition(op2, filterFields2) + : '' + const condition3 = + op3 != null && + linkop2 != null && + filterFields3 != null && + Object.keys(filterFields3).length > 0 + ? buildCondition(op3, filterFields3) + : '' + + condition += condition1 !== '' ? 'WHERE ' + condition1 : '' + condition += + condition2 !== '' + ? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + (condition !== '' ? ` ${linkop1} ` : 'WHERE ') + condition2 + : '' + condition += + condition3 !== '' + ? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + (condition !== '' ? ` ${linkop2} ` : 'WHERE ') + condition3 + : '' + + if (joinFields != null) { + let joinCondition = '' + Object.keys(joinFields) + .filter( + (key) => + joinFields[key] != null && + joinFields[key].toString() !== [].toString() + ) + .forEach((key) => { + joinCondition += joinCondition !== '' ? ' AND ' : '' + joinCondition += `${key}=${joinFields[key]}` + }) + condition += condition !== '' ? ' AND ' : 'WHERE ' + condition += joinCondition } if (order != null) condition += ` ORDER BY ${order}` this.db.query( - `SELECT ${fields.join(',')} FROM ${table} ${condition}`, + `SELECT ${fields.join(',')} FROM ${tables.join(',')} ${condition}`, values, - (err, rows) => { - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + (err: any, rows: any) => { err ? reject(err) : resolve(rows.rows) } ) @@ -229,25 +340,335 @@ class Pg extends SQL implements IdDbBackend { } get( - table: string, + table: T, fields?: string[], filterFields?: Record>, order?: string ): Promise { - return this._get('=', table, fields, filterFields, order) + return this._get( + [table], + fields, + '=', + filterFields, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + order + ) + } + + getJoin( + tables: T[], + fields?: string[], + filterFields?: Record>, + joinFields?: Record, + order?: string + ): Promise { + return this._get( + tables, + fields, + '=', + filterFields, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + joinFields, + order + ) } getHigherThan( - table: string, + table: T, + fields?: string[], + filterFields?: Record>, + order?: string + ): Promise { + return this._get( + [table], + fields, + '>', + filterFields, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + order + ) + } + + getWhereEqualOrDifferent( + table: T, + fields?: string[], + filterFields1?: Record>, + filterFields2?: Record>, + order?: string + ): Promise { + return this._get( + [table], + fields, + '=', + filterFields1, + '<>', + ' OR ', + filterFields2, + undefined, + undefined, + undefined, + undefined, + order + ) + } + + getWhereEqualAndHigher( + table: T, + fields?: string[], + filterFields1?: Record>, + filterFields2?: Record>, + order?: string + ): Promise { + return this._get( + [table], + fields, + '=', + filterFields1, + '>', + ' AND ', + filterFields2, + undefined, + undefined, + undefined, + undefined, + order + ) + } + + _getMinMax( + minmax: 'MIN' | 'MAX', + tables: T[], + targetField: string, + fields?: string[], + op1?: string, + filterFields1?: Record>, + op2?: string, + linkop?: string, + filterFields2?: Record>, + joinFields?: Record, + order?: string + ): Promise { + return new Promise((resolve, reject) => { + /* istanbul ignore if */ + if (this.db == null) { + reject(new Error('Wait for database to be ready')) + } else { + let condition: string = '' + const values: string[] = [] + if (fields == null || fields.length === 0) { + fields = ['*'] + } else { + // Generate aliases for fields containing periods + fields = fields.map((field) => { + if (field.includes('.')) { + const alias = field.replace(/\./g, '_') + return `${field} AS ${alias}` + } + return field + }) + } + const targetFieldAlias: string = targetField.replace(/\./g, '_') + + let index = 0 + + const buildCondition = ( + op: string, + filterFields: Record> + ): string => { + let localCondition = '' + + Object.keys(filterFields) + .filter( + (key) => + filterFields[key] != null && + filterFields[key].toString() !== [].toString() + ) + .forEach((key) => { + localCondition += localCondition !== '' ? ' AND ' : '' + if (Array.isArray(filterFields[key])) { + localCondition += `(${( + filterFields[key] as Array + ) + .map((val) => { + index++ + values.push(val.toString()) + return `${key}${op}$${index}` + }) + .join(' OR ')})` + } else { + index++ + values.push(filterFields[key].toString()) + localCondition += `${key}${op}$${index}` + } + }) + return localCondition + } + + const condition1 = + op1 != null && + filterFields1 != null && + Object.keys(filterFields1).length > 0 + ? buildCondition(op1, filterFields1) + : '' + const condition2 = + op2 != null && + linkop != null && + filterFields2 != null && + Object.keys(filterFields2).length > 0 + ? buildCondition(op2, filterFields2) + : '' + + condition += condition1 !== '' ? 'WHERE ' + condition1 : '' + condition += + condition2 !== '' + ? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + (condition ? ` ${linkop} ` : 'WHERE ') + condition2 + : '' + + if (joinFields != null) { + let joinCondition = '' + Object.keys(joinFields) + .filter( + (key) => + joinFields[key] != null && + joinFields[key].toString() !== [].toString() + ) + .forEach((key) => { + joinCondition += joinCondition !== '' ? ' AND ' : '' + joinCondition += `${key}=${joinFields[key]}` + }) + condition += condition !== '' ? ' AND ' : 'WHERE ' + condition += joinCondition + } + + if (order != null) condition += ` ORDER BY ${order}` + + this.db.query( + `SELECT ${fields.join( + ',' + )}, ${minmax}(${targetField}) AS max_${targetFieldAlias} FROM ${tables.join( + ',' + )} ${condition} HAVING COUNT(*) > 0`, // HAVING COUNT(*) > 0 is to avoid returning a row with NULL values + values, + (err, rows) => { + err ? reject(err) : resolve(rows.rows) + } + ) + } + }) + } + + getMaxWhereEqual( + table: T, + targetField: string, fields?: string[], filterFields?: Record>, order?: string ): Promise { - return this._get('>', table, fields, filterFields, order) + return this._getMinMax( + 'MAX', + [table], + targetField, + fields, + '=', + filterFields, + undefined, + undefined, + undefined, + undefined, + order + ) + } + + getMaxWhereEqualAndLower( + table: T, + targetField: string, + fields?: string[], + filterFields1?: Record>, + filterFields2?: Record>, + order?: string + ): Promise { + return this._getMinMax( + 'MAX', + [table], + targetField, + fields, + '=', + filterFields1, + '<', + ' AND ', + filterFields2, + undefined, + order + ) + } + + getMinWhereEqualAndHigher( + table: T, + targetField: string, + fields?: string[], + filterFields1?: Record>, + filterFields2?: Record>, + order?: string + ): Promise { + return this._getMinMax( + 'MIN', + [table], + targetField, + fields, + '=', + filterFields1, + '>', + ' AND ', + filterFields2, + undefined, + order + ) + } + + getMaxWhereEqualAndLowerJoin( + tables: T[], + targetField: string, + fields: string[], + filterFields1?: Record>, + filterFields2?: Record>, + joinFields?: Record, + order?: string + ): Promise { + return this._getMinMax( + 'MAX', + tables, + targetField, + fields, + '=', + filterFields1, + '<', + ' AND ', + filterFields2, + joinFields, + order + ) } match( - table: string, + table: T, fields: string[], searchFields: string[], value: string | number, @@ -277,11 +698,7 @@ class Pg extends SQL implements IdDbBackend { }) } - deleteEqual( - table: string, - field: string, - value: string | number - ): Promise { + deleteEqual(table: T, field: string, value: string | number): Promise { return new Promise((resolve, reject) => { if (this.db == null) { reject(new Error('DB not ready')) @@ -311,8 +728,55 @@ class Pg extends SQL implements IdDbBackend { }) } + deleteEqualAnd( + table: T, + condition1: { + field: string + value: string | number | Array + }, + condition2: { + field: string + value: string | number | Array + } + ): Promise { + return new Promise((resolve, reject) => { + if (this.db == null) { + reject(new Error('DB not ready')) + } else { + if ( + !condition1.field || + condition1.field.length === 0 || + !condition1.value || + condition1.value.toString().length === 0 || + !condition2.field || + condition2.field.length === 0 || + !condition2.value || + condition2.value.toString().length === 0 + ) { + reject( + new Error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Bad deleteAnd call, conditions: ${condition1.field}=${condition1.value}, ${condition2.field}=${condition2.value}` + ) + ) + return + } + this.db.query( + `DELETE FROM ${table} WHERE ${condition1.field}=$1 AND ${condition2.field}=$2`, + [condition1.value, condition2.value] as + | (string[] & Array) + | (number[] & Array), + (err) => { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + err ? reject(err) : resolve() + } + ) + } + }) + } + deleteLowerThan( - table: string, + table: T, field: string, value: string | number ): Promise { @@ -348,7 +812,7 @@ class Pg extends SQL implements IdDbBackend { } deleteWhere( - table: string, + table: T, conditions: ISQLCondition | ISQLCondition[] ): Promise { return new Promise((resolve, reject) => { diff --git a/packages/matrix-identity-server/src/db/sql/sql.ts b/packages/matrix-identity-server/src/db/sql/sql.ts index 0546c344..97f6523c 100644 --- a/packages/matrix-identity-server/src/db/sql/sql.ts +++ b/packages/matrix-identity-server/src/db/sql/sql.ts @@ -13,42 +13,28 @@ export type CreateDbMethod = ( > ) => Promise -const tables: Record = { - accessTokens: 'id varchar(64) PRIMARY KEY, data text', - oneTimeTokens: 'id varchar(64) PRIMARY KEY, expires int, data text', - attempts: 'email text PRIMARY KEY, expires int, attempt int', - keys: 'name varchar(32) PRIMARY KEY, data text', - hashes: - 'hash varchar(48) PRIMARY KEY, pepper varchar(32), type varchar(8), value text, active integer', - privateNotes: - 'id varchar(64) PRIMARY KEY, authorId varchar(64), content text, targetId varchar(64)', - roomTags: - 'id varchar(64) PRIMARY KEY, authorId varchar(64), content text, roomId varchar(64)', - userHistory: 'address text PRIMARY KEY, active integer, timestamp integer', - userQuotas: 'user_id varchar(64) PRIMARY KEY, size int' -} - -const indexes: Partial> = { - oneTimeTokens: ['expires'], - attempts: ['expires'], - userHistory: ['timestamp'] -} +type sqlComparaisonOperator = '=' | '!=' | '>' | '<' | '>=' | '<=' | '<>' -const initializeValues: Partial< - Record>> -> = { - keys: [ - { name: 'pepper', data: '' }, - { name: 'previousPepper', data: '' } - ] +export interface ISQLCondition { + field: string + operator: sqlComparaisonOperator + value: string | number } -abstract class SQL { +abstract class SQL { db?: SQLiteDatabase | PgDatabase ready: Promise cleanJob?: NodeJS.Timeout - constructor(conf: Config, private readonly logger: TwakeLogger) { + constructor( + conf: Config, + private readonly logger: TwakeLogger, + tables?: Record, + indexes?: Partial>, + initializeValues?: Partial< + Record>> + > + ) { // @ts-expect-error method is defined in child class this.ready = this.createDatabases( conf, @@ -61,7 +47,7 @@ abstract class SQL { // eslint-disable-next-line @typescript-eslint/promise-function-async getCount( - table: Collections, + table: T, field: string, value?: string | number | string[] ): Promise { @@ -81,11 +67,7 @@ abstract class SQL { } // eslint-disable-next-line @typescript-eslint/promise-function-async - getAll( - table: string, - fields: string[], - order?: string - ): Promise { + getAll(table: T, fields: string[], order?: string): Promise { // @ts-expect-error implemented later return this.get(table, fields, undefined, order) } diff --git a/packages/matrix-identity-server/src/db/sql/sqlite.ts b/packages/matrix-identity-server/src/db/sql/sqlite.ts index 0b45fbb8..eac4412e 100644 --- a/packages/matrix-identity-server/src/db/sql/sqlite.ts +++ b/packages/matrix-identity-server/src/db/sql/sqlite.ts @@ -2,26 +2,22 @@ import { type TwakeLogger } from '@twake/logger' import { type Database, type Statement } from 'sqlite3' import { type Config, type DbGetResult } from '../../types' -import { - type Collections, - type ISQLCondition, - type IdDbBackend -} from '../index' +import { type IdDbBackend } from '../index' import createTables from './_createTables' -import SQL from './sql' +import SQL, { type ISQLCondition } from './sql' export type SQLiteDatabase = Database export type SQLiteStatement = Statement -class SQLite extends SQL implements IdDbBackend { +class SQLite extends SQL implements IdDbBackend { declare db?: SQLiteDatabase createDatabases( conf: Config, - tables: Record, - indexes: Partial>, + tables: Record, + indexes: Partial>, initializeValues: Partial< - Record>> + Record>> >, logger: TwakeLogger ): Promise { @@ -78,13 +74,13 @@ class SQLite extends SQL implements IdDbBackend { }) } - exists(table: string): Promise { + exists(table: T): Promise { // @ts-expect-error sqlite_master not listed in Collections return this.getCount('sqlite_master', 'name', table) } insert( - table: string, + table: T, values: Record ): Promise { return new Promise((resolve, reject) => { @@ -114,14 +110,12 @@ class SQLite extends SQL implements IdDbBackend { } } ) - stmt.finalize((err) => { - reject(err) - }) + stmt.finalize(reject) }) } update( - table: string, + table: T, values: Record, field: string, value: string | number @@ -154,17 +148,60 @@ class SQLite extends SQL implements IdDbBackend { } } ) - stmt.finalize((err) => { - reject(err) - }) + stmt.finalize(reject) + }) + } + + // TODO : Merge update and updateAnd into one function that takes an array of conditions as argument + // Done in Client server - go see updateWithConditions + updateAnd( + table: T, + values: Record, + condition1: { field: string; value: string | number }, + condition2: { field: string; value: string | number } + ): Promise { + return new Promise((resolve, reject) => { + /* istanbul ignore if */ + if (this.db == null) { + throw new Error('Wait for database to be ready') + } + const names = Object.keys(values) + const vals = Object.values(values) + vals.push(condition1.value, condition2.value) + + const setClause = names.map((name) => `${name} = ?`).join(', ') + const stmt = this.db.prepare( + `UPDATE ${table} SET ${setClause} WHERE ${condition1.field} = ? AND ${condition2.field} = ? RETURNING *;` + ) + + stmt.all( + vals, + (err: string, rows: Array>) => { + /* istanbul ignore if */ + if (err != null) { + reject(err) + } else { + resolve(rows) + } + } + ) + + stmt.finalize(reject) }) } _get( - op: string, - table: string, + tables: T[], fields?: string[], - filterFields?: Record>, + op1?: string, + filterFields1?: Record>, + op2?: string, + linkop1?: string, + filterFields2?: Record>, + op3?: string, + linkop2?: string, + filterFields3?: Record>, + joinFields?: Record, order?: string ): Promise { return new Promise((resolve, reject) => { @@ -176,9 +213,25 @@ class SQLite extends SQL implements IdDbBackend { const values: string[] = [] if (fields == null || fields.length === 0) { fields = ['*'] + } else { + // Generate aliases for fields containing periods + fields = fields.map((field) => { + if (field.includes('.')) { + const alias = field.replace(/\./g, '_') + return `${field} AS ${alias}` + } + return field + }) } - if (filterFields != null) { - let index = 0 + + let index: number = 0 + + const buildCondition = ( + op: string, + filterFields: Record> + ): string => { + let localCondition = '' + Object.keys(filterFields) .filter( (key) => @@ -186,28 +239,81 @@ class SQLite extends SQL implements IdDbBackend { filterFields[key].toString() !== [].toString() ) .forEach((key) => { - condition += condition === '' ? 'WHERE ' : ' AND ' + localCondition += localCondition !== '' ? ' AND ' : '' if (Array.isArray(filterFields[key])) { - condition += `${(filterFields[key] as Array) + localCondition += `(${( + filterFields[key] as Array + ) .map((val) => { index++ values.push(val.toString()) return `${key}${op}$${index}` }) - .join(' OR ')}` + .join(' OR ')})` } else { index++ values.push(filterFields[key].toString()) - condition += `${key}${op}$${index}` + localCondition += `${key}${op}$${index}` } }) + return localCondition } + + const condition1 = + op1 != null && + filterFields1 != null && + Object.keys(filterFields1).length > 0 + ? buildCondition(op1, filterFields1) + : '' + const condition2 = + op2 != null && + linkop1 != null && + filterFields2 != null && + Object.keys(filterFields2).length > 0 + ? buildCondition(op2, filterFields2) + : '' + const condition3 = + op3 != null && + linkop2 != null && + filterFields3 != null && + Object.keys(filterFields3).length > 0 + ? buildCondition(op3, filterFields3) + : '' + + condition += condition1 !== '' ? 'WHERE ' + condition1 : '' + condition += + condition2 !== '' + ? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + (condition !== '' ? ` ${linkop1} ` : 'WHERE ') + condition2 + : '' + condition += + condition3 !== '' + ? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + (condition !== '' ? ` ${linkop2} ` : 'WHERE ') + condition3 + : '' + + if (joinFields != null) { + let joinCondition = '' + Object.keys(joinFields) + .filter( + (key) => + joinFields[key] != null && + joinFields[key].toString() !== [].toString() + ) + .forEach((key) => { + joinCondition += joinCondition !== '' ? ' AND ' : '' + joinCondition += `${key}=${joinFields[key]}` + }) + condition += condition !== '' ? ' AND ' : 'WHERE ' + condition += joinCondition + } + if (order != null) condition += ` ORDER BY ${order}` // eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error // @ts-ignore never undefined const stmt = this.db.prepare( - `SELECT ${fields.join(',')} FROM ${table} ${condition}` + `SELECT ${fields.join(',')} FROM ${tables.join(',')} ${condition}` ) stmt.all( values, @@ -228,25 +334,347 @@ class SQLite extends SQL implements IdDbBackend { } get( - table: string, + table: T, fields?: string[], filterFields?: Record>, order?: string ): Promise { - return this._get('=', table, fields, filterFields, order) + return this._get( + [table], + fields, + '=', + filterFields, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + order + ) + } + + getJoin( + tables: T[], + fields?: string[], + filterFields?: Record>, + joinFields?: Record, + order?: string + ): Promise { + return this._get( + tables, + fields, + '=', + filterFields, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + joinFields, + order + ) } getHigherThan( - table: Collections, + table: T, + fields?: string[], + filterFields?: Record>, + order?: string + ): Promise { + return this._get( + [table], + fields, + '>', + filterFields, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + order + ) + } + + getWhereEqualOrDifferent( + table: T, + fields?: string[], + filterFields1?: Record>, + filterFields2?: Record>, + order?: string + ): Promise { + return this._get( + [table], + fields, + '=', + filterFields1, + '<>', + ' OR ', + filterFields2, + undefined, + undefined, + undefined, + undefined, + order + ) + } + + getWhereEqualAndHigher( + table: T, + fields?: string[], + filterFields1?: Record>, + filterFields2?: Record>, + order?: string + ): Promise { + return this._get( + [table], + fields, + '=', + filterFields1, + '>', + ' AND ', + filterFields2, + undefined, + undefined, + undefined, + undefined, + order + ) + } + + _getMinMax( + minmax: 'MIN' | 'MAX', + tables: T[], + targetField: string, + fields?: string[], + op1?: string, + filterFields1?: Record>, + op2?: string, + linkop?: string, + filterFields2?: Record>, + joinFields?: Record, + order?: string + ): Promise { + return new Promise((resolve, reject) => { + /* istanbul ignore if */ + if (this.db == null) { + reject(new Error('Wait for database to be ready')) + } else { + let condition: string = '' + const values: string[] = [] + if (fields == null || fields.length === 0) { + fields = ['*'] + } else { + // Generate aliases for fields containing periods + fields = fields.map((field) => { + if (field.includes('.')) { + const alias = field.replace(/\./g, '_') + return `${field} AS ${alias}` + } + return field + }) + } + const targetFieldAlias: string = targetField.replace(/\./g, '_') + + let index: number = 0 + + const buildCondition = ( + op: string, + filterFields: Record> + ): string => { + let localCondition = '' + + Object.keys(filterFields) + .filter( + (key) => + filterFields[key] != null && + filterFields[key].toString() !== [].toString() + ) + .forEach((key) => { + localCondition += localCondition !== '' ? ' AND ' : '' + if (Array.isArray(filterFields[key])) { + localCondition += `(${( + filterFields[key] as Array + ) + .map((val) => { + index++ + values.push(val.toString()) + return `${key}${op}$${index}` + }) + .join(' OR ')})` + } else { + index++ + values.push(filterFields[key].toString()) + localCondition += `${key}${op}$${index}` + } + }) + return localCondition + } + + const condition1 = + op1 != null && + filterFields1 != null && + Object.keys(filterFields1).length > 0 + ? buildCondition(op1, filterFields1) + : '' + const condition2 = + op2 != null && + linkop != null && + filterFields2 != null && + Object.keys(filterFields2).length > 0 + ? buildCondition(op2, filterFields2) + : '' + + condition += condition1 !== '' ? 'WHERE ' + condition1 : '' + condition += + condition2 !== '' + ? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + (condition !== '' ? ` ${linkop} ` : 'WHERE ') + condition2 + : '' + + if (joinFields != null) { + let joinCondition = '' + Object.keys(joinFields) + .filter( + (key) => + joinFields[key] != null && + joinFields[key].toString() !== [].toString() + ) + .forEach((key) => { + joinCondition += joinCondition !== '' ? ' AND ' : '' + joinCondition += `${key}=${joinFields[key]}` + }) + condition += condition !== '' ? ' AND ' : 'WHERE ' + condition += joinCondition + } + + if (order != null) condition += ` ORDER BY ${order}` + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error + // @ts-ignore never undefined + const stmt = this.db.prepare( + `SELECT ${fields.join( + ',' + )}, ${minmax}(${targetField}) AS max_${targetFieldAlias} FROM ${tables.join( + ',' + )} ${condition} HAVING COUNT(*) > 0` // HAVING COUNT(*) > 0 is to avoid returning a row with NULL values + ) + stmt.all( + values, + (err: string, rows: Array>) => { + /* istanbul ignore if */ + if (err != null) { + reject(err) + } else { + resolve(rows) + } + } + ) + stmt.finalize((err) => { + reject(err) + }) + } + }) + } + + getMaxWhereEqual( + table: T, + targetField: string, + fields?: string[], + filterFields?: Record>, + order?: string + ): Promise { + return this._getMinMax( + 'MAX', + [table], + targetField, + fields, + '=', + filterFields, + undefined, + undefined, + undefined, + undefined, + order + ) + } + + getMaxWhereEqualAndLower( + table: T, + targetField: string, + fields?: string[], + filterFields1?: Record>, + filterFields2?: Record>, + order?: string + ): Promise { + return this._getMinMax( + 'MAX', + [table], + targetField, + fields, + '=', + filterFields1, + '<', + ' AND ', + filterFields2, + undefined, + order + ) + } + + getMinWhereEqualAndHigher( + table: T, + targetField: string, + fields?: string[], + filterFields1?: Record>, + filterFields2?: Record>, + order?: string + ): Promise { + return this._getMinMax( + 'MIN', + [table], + targetField, + fields, + '=', + filterFields1, + '>', + ' AND ', + filterFields2, + undefined, + order + ) + } + + getMaxWhereEqualAndLowerJoin( + tables: T[], + targetField: string, fields: string[], - filterFields: Record>, + filterFields1?: Record>, + filterFields2?: Record>, + joinFields?: Record, order?: string ): Promise { - return this._get('>', table, fields, filterFields, order) + return this._getMinMax( + 'MAX', + tables, + targetField, + fields, + '=', + filterFields1, + '<', + ' AND ', + filterFields2, + joinFields, + order + ) } match( - table: string, + table: T, fields: string[], searchFields: string[], value: string | number, @@ -284,11 +712,7 @@ class SQLite extends SQL implements IdDbBackend { }) } - deleteEqual( - table: string, - field: string, - value: string | number - ): Promise { + deleteEqual(table: T, field: string, value: string | number): Promise { return new Promise((resolve, reject) => { /* istanbul ignore if */ if (this.db == null) { @@ -310,8 +734,42 @@ class SQLite extends SQL implements IdDbBackend { }) } + deleteEqualAnd( + table: T, + condition1: { + field: string + value: string | number | Array + }, + condition2: { + field: string + value: string | number | Array + } + ): Promise { + return new Promise((resolve, reject) => { + /* istanbul ignore if */ + if (this.db == null) { + reject(new Error('Wait for database to be ready')) + } else { + const stmt = this.db.prepare( + `DELETE FROM ${table} WHERE ${condition1.field}=? AND ${condition2.field}=?` + ) + stmt.all([condition1.value, condition2.value], (err, rows) => { + /* istanbul ignore if */ + if (err != null) { + reject(err) + } else { + resolve() + } + }) + stmt.finalize((err) => { + reject(err) + }) + } + }) + } + deleteLowerThan( - table: string, + table: T, field: string, value: string | number ): Promise { @@ -329,9 +787,7 @@ class SQLite extends SQL implements IdDbBackend { resolve() } }) - stmt.finalize((err) => { - reject(err) - }) + stmt.finalize(reject) }) } @@ -342,7 +798,7 @@ class SQLite extends SQL implements IdDbBackend { * @param {ISQLCondition | ISQLCondition[]} conditions - the list of filters, operators and values for sql conditions */ deleteWhere( - table: string, + table: T, conditions: ISQLCondition | ISQLCondition[] ): Promise { // Adaptation of the method get, with the delete keyword, 'AND' instead of 'OR', and with filters instead of fields diff --git a/packages/matrix-identity-server/src/ephemeral_signing/index.ts b/packages/matrix-identity-server/src/ephemeral_signing/index.ts new file mode 100644 index 00000000..f3f13ce1 --- /dev/null +++ b/packages/matrix-identity-server/src/ephemeral_signing/index.ts @@ -0,0 +1,75 @@ +import { randomString, signJson, toBase64Url } from '@twake/crypto' +import nacl from 'tweetnacl' +import * as naclUtil from 'tweetnacl-util' +import type MatrixIdentityServer from '..' +import { + errMsg, + jsonContent, + send, + validateParameters, + type expressAppHandler +} from '@twake/utils' + +const mxidRe = /^@[0-9a-zA-Z._=-]+:[0-9a-zA-Z.-]+$/ +const tokenRe = /^[0-9a-zA-Z.=_-]{1,255}$/ + +interface RequestTokenArgs { + private_key: string + mxid: string + token: string +} + +const schema = { + private_key: true, + mxid: true, + token: true +} + +const SignEd25519 = ( + idServer: MatrixIdentityServer +): expressAppHandler => { + return (req, res) => { + idServer.authenticate(req, res, (data, id) => { + jsonContent(req, res, idServer.logger, (obj) => { + validateParameters(res, schema, obj, idServer.logger, (obj) => { + const mxid = (obj as RequestTokenArgs).mxid + const token = (obj as RequestTokenArgs).token + const privateKey = (obj as RequestTokenArgs).private_key + if (!tokenRe.test(token)) { + send(res, 400, errMsg('invalidParam', 'invalid token')) + } else if (!mxidRe.test(mxid)) { + send(res, 400, errMsg('invalidParam', 'invalid Matrix user ID')) + } else { + idServer.db + .verifyInvitationToken(token) + .then((data) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error + // @ts-ignore + const sender = data.sender as string + const newToken = randomString(64) + const identifier = nacl.randomBytes(8) + let identifierHex = naclUtil.encodeBase64(identifier) + identifierHex = toBase64Url(identifierHex) + send( + res, + 200, + signJson( + { mxid, sender, token: newToken }, + privateKey, + idServer.conf.server_name, + `ed25519:${identifierHex}` + ) + ) + }) + .catch((err) => { + idServer.logger.error('Token denied', err) + send(res, 404, errMsg('notFound', err)) + }) + } + }) + }) + }) + } +} + +export default SignEd25519 diff --git a/packages/matrix-identity-server/src/index.test.ts b/packages/matrix-identity-server/src/index.test.ts index 355a6d18..55412cd0 100644 --- a/packages/matrix-identity-server/src/index.test.ts +++ b/packages/matrix-identity-server/src/index.test.ts @@ -1,4 +1,9 @@ -import { Hash, randomString, supportedHashes } from '@twake/crypto' +import { + Hash, + randomString, + supportedHashes, + generateKeyPair +} from '@twake/crypto' import express from 'express' import fs from 'fs' import fetch from 'node-fetch' @@ -8,6 +13,7 @@ import buildUserDB from './__testData__/buildUserDB' import defaultConfig from './__testData__/registerConf.json' import IdServer from './index' import { type Config } from './types' +import { fillPoliciesDB } from './terms/index.post' jest.mock('node-fetch', () => jest.fn()) const sendMailMock = jest.fn() @@ -101,6 +107,10 @@ describe('Use configuration file', () => { idServer.cleanJobs() }) + test('Should have filtered the invalid federated_identity_services', () => { + expect(idServer.conf.federated_identity_services).toEqual([]) + }) + test('Reject unimplemented endpoint with 404', async () => { const response = await request(app).get('/_matrix/unknown') expect(response.statusCode).toBe(404) @@ -154,12 +164,12 @@ describe('Use configuration file', () => { const mockResponse = Promise.resolve({ ok: true, status: 200, - json: () => { - return { + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ sub: '@dwho:example.com', 'm.server': 'matrix.example.com:8448' - } - } + }) }) // @ts-expect-error mock is unknown fetch.mockImplementation(async () => await mockResponse) @@ -201,12 +211,12 @@ describe('Use configuration file', () => { const mockResponse = Promise.resolve({ ok: true, status: 200, - json: () => { - return { + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ email: 'dwho@example.com', 'm.server': 'matrix.example.com:8448' - } - } + }) }) // @ts-expect-error mock is unknown fetch.mockImplementation(async () => await mockResponse) @@ -226,12 +236,12 @@ describe('Use configuration file', () => { const mockResponse = Promise.resolve({ ok: true, status: 200, - json: () => { - return { + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ sub: 'dwho@example.com', 'm.server': 'matrix.example.com:8448' - } - } + }) }) // @ts-expect-error mock is unknown fetch.mockImplementation(async () => await mockResponse) @@ -249,6 +259,177 @@ describe('Use configuration file', () => { }) }) + describe('/_matrix/identity/v2/pubkey', () => { + describe('/_matrix/identity/v2/pubkey/ephemeral/isvalid', () => { + let shortKeyPair: { publicKey: string; privateKey: string; keyId: string } + beforeAll(async () => { + // Insert a test key into the database + await idServer.db + .createKeypair('shortTerm', 'curve25519') + .then((keypair) => { + shortKeyPair = keypair + }) + }) + + afterAll(async () => { + // Remove the test key from the database + await idServer.db.deleteEqual( + 'shortTermKeypairs', + 'keyID', + shortKeyPair.keyId + ) + }) + + it('should return error 400 if no public_key is given (shortTerm case)', async () => { + const response = await request(app).get( + '/_matrix/identity/v2/pubkey/ephemeral/isvalid' + ) + + expect(response.statusCode).toBe(400) + expect(response.body.errcode).toBe('M_MISSING_PARAMS') + }) + + it('should validate a valid ephemeral pubkey', async () => { + const key = shortKeyPair.publicKey + const response = await request(app).get( + '/_matrix/identity/v2/pubkey/ephemeral/isvalid?public_key=' + key + ) + + expect(response.statusCode).toBe(200) + expect(response.body.valid).toBe(true) + }) + + it('should invalidate an invalid ephemeral pubkey', async () => { + const key = 'invalidPub' + const response = await request(app).get( + '/_matrix/identity/v2/pubkey/ephemeral/isvalid?public_key=' + key + ) + + expect(response.statusCode).toBe(200) + expect(response.body.valid).toBe(false) + }) + }) + + describe('/_matrix/identity/v2/pubkey/isvalid', () => { + let longKeyPair: { publicKey: string; privateKey: string; keyId: string } + beforeAll(async () => { + // Insert a test key into the database + longKeyPair = generateKeyPair('ed25519') + await idServer.db.insert('longTermKeypairs', { + name: 'currentKey', + keyID: longKeyPair.keyId, + public: longKeyPair.publicKey, + private: longKeyPair.privateKey + }) + }) + + afterAll(async () => { + // Remove the test key from the database + await idServer.db.deleteEqual( + 'longTermKeypairs', + 'keyID', + longKeyPair.keyId + ) + }) + it('should return error 400 if no public_key is given (longTerm case)', async () => { + const response = await request(app).get( + '/_matrix/identity/v2/pubkey/isvalid' + ) + + expect(response.statusCode).toBe(400) + expect(response.body.errcode).toBe('M_MISSING_PARAMS') + }) + + it('should validate a valid long-term pubkey', async () => { + const key = longKeyPair.publicKey + const response = await request(app).get( + '/_matrix/identity/v2/pubkey/isvalid?public_key=' + key + ) + + expect(response.statusCode).toBe(200) + expect(response.body.valid).toBe(true) + }) + + it('should invalidate an invalid long-term pubkey', async () => { + const key = 'invalidPub' + const response = await request(app) + .get('/_matrix/identity/v2/pubkey/isvalid') + .query({ public_key: key }) + + expect(response.statusCode).toBe(200) + expect(response.body.valid).toBe(false) + }) + }) + + describe('/_matrix/identity/v2/pubkey/:keyID', () => { + let longKeyPair: { publicKey: string; privateKey: string; keyId: string } + let shortKeyPair: { publicKey: string; privateKey: string; keyId: string } + beforeAll(async () => { + // Insert a test key into the database + longKeyPair = generateKeyPair('ed25519') + await idServer.db.insert('longTermKeypairs', { + name: 'currentKey', + keyID: longKeyPair.keyId, + public: longKeyPair.publicKey, + private: longKeyPair.privateKey + }) + await idServer.db + .createKeypair('shortTerm', 'curve25519') + .then((_keypair) => { + shortKeyPair = _keypair + }) + }) + + afterAll(async () => { + // Remove the test key from the database + await idServer.db.deleteEqual( + 'longTermKeypairs', + 'keyID', + longKeyPair.keyId + ) + await idServer.db.deleteEqual( + 'shortTermKeypairs', + 'keyID', + shortKeyPair.keyId + ) + }) + + it('should return the public key when correct keyID is given (from long term key pairs)', async () => { + const _keyID = longKeyPair.keyId + const response = await request(app).get( + `/_matrix/identity/v2/pubkey/${_keyID}` + ) + + expect(response.statusCode).toBe(200) + expect(response.body.public_key).toBeDefined() + expect(response.body.public_key).toMatch(/^[A-Za-z0-9_-]+$/) + expect(response.body.public_key).toBe(longKeyPair.publicKey) + }) + + it('should return the public key when correct keyID is given (from short term key pairs)', async () => { + const _keyID = shortKeyPair.keyId + const response = await request(app).get( + `/_matrix/identity/v2/pubkey/${_keyID}` + ) + + expect(response.statusCode).toBe(200) + expect(response.body.public_key).toBeDefined() + expect(response.body.public_key).toMatch(/^[A-Za-z0-9_-]+$/) + expect(response.body.public_key).toBe(shortKeyPair.publicKey) + }) + + it('should return 404 when incorrect keyID is given', async () => { + const _keyID = 'incorrectKeyID' + const response = await request(app).get( + `/_matrix/identity/v2/pubkey/${_keyID}` + ) // exactly '/_matrix/identity/v2/pubkey/' + _keyID + + expect(response.statusCode).toBe(404) + expect(response.body.errcode).toBe('M_NOT_FOUND') + }) + }) + }) + describe('Endpoint with authentication', () => { it('should reject if more than 100 requests are done in less than 10 seconds', async () => { let response @@ -319,6 +500,48 @@ describe('Use configuration file', () => { expect(response.statusCode).toBe(400) expect(sendMailMock).not.toHaveBeenCalled() }) + it('should refuse an invalid next_link', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/validate/email/requestToken') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + email: 'yadd@debian.org', + next_link: 'wrong link', + send_attempt: 1 + }) + expect(response.statusCode).toBe(400) + expect(sendMailMock).not.toHaveBeenCalled() + }) + it('should refuse a send attempt that is not a number', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/validate/email/requestToken') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + email: 'yadd@debian.org', + next_link: 'http://localhost:8090', + send_attempt: 'NaN' + }) + expect(response.statusCode).toBe(400) + expect(sendMailMock).not.toHaveBeenCalled() + }) + it('should refuse a send attempt that is too large', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/validate/email/requestToken') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + email: 'yadd@debian.org', + next_link: 'http://localhost:8090', + send_attempt: 99999999999 + }) + expect(response.statusCode).toBe(400) + expect(sendMailMock).not.toHaveBeenCalled() + }) it('should accept valid email registration query', async () => { const response = await request(app) .post('/_matrix/identity/v2/validate/email/requestToken') @@ -338,9 +561,43 @@ describe('Use configuration file', () => { token = RegExp.$1 sid = RegExp.$2 }) - }) - describe('/_matrix/identity/v2/validate/email/submitToken', () => { - /* Works but disabled to avoid invalidate previous token + it('should not resend an email for the same attempt', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/validate/email/requestToken') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + email: 'xg@xnr.fr', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(response.statusCode).toBe(200) + expect(sendMailMock).not.toHaveBeenCalled() + expect(response.body).toEqual({ sid }) + }) + it('should resend an email for a different attempt', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/validate/email/requestToken') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret', + email: 'xg@xnr.fr', + next_link: 'http://localhost:8090', + send_attempt: 2 + }) + expect(response.statusCode).toBe(200) + expect(sendMailMock.mock.calls[0][0].to).toBe('xg@xnr.fr') + expect(sendMailMock.mock.calls[0][0].raw).toMatch( + /token=([a-zA-Z0-9]{64})&client_secret=mysecret&sid=([a-zA-Z0-9]{64})/ + ) + const newSid = RegExp.$2 + expect(response.body).toEqual({ sid: newSid }) + expect(sendMailMock).toHaveBeenCalled() + }) + describe('/_matrix/identity/v2/validate/email/submitToken', () => { + /* Works but disabled to avoid invalidate previous token it('should refuse mismatch registration parameters', async () => { const response = await request(app) .get('/_matrix/identity/v2/validate/email/submitToken') @@ -353,42 +610,475 @@ describe('Use configuration file', () => { expect(response.statusCode).toBe(400) }) */ - it('should reject registration with a missing parameter', async () => { + it('should reject registration with a missing parameter', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/validate/email/submitToken') + .send({ + token, + sid + }) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + }) + it('should reject registration with wrong parameters', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/validate/email/submitToken') + .send({ + token, + client_secret: 'wrongclientsecret', + sid: 'wrongSid' + }) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + }) + it('should accept to register mail after click', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/validate/email/submitToken') + .send({ + token, + client_secret: 'mysecret', + sid + }) + .set('Accept', 'application/json') + expect(response.body).toEqual({ success: true }) + expect(response.statusCode).toBe(200) + }) + it('should refuse a second registration', async () => { + const response = await request(app) + .get('/_matrix/identity/v2/validate/email/submitToken') + .query({ + token, + client_secret: 'mysecret', + sid + }) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + }) + it('should redirect to the next_link if it was provided in requestToken with the GET method', async () => { + const requestTokenResponse = await request(app) + .post('/_matrix/identity/v2/validate/email/requestToken') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + client_secret: 'my_secret2', + email: 'abc@abcd.fr', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(requestTokenResponse.statusCode).toBe(200) + expect(sendMailMock.mock.calls[0][0].raw).toMatch( + /token=([a-zA-Z0-9]{64})&client_secret=my_secret2&sid=([a-zA-Z0-9]{64})/ + ) + sid = RegExp.$2 + token = RegExp.$1 + const response = await request(app) + .get('/_matrix/identity/v2/validate/email/submitToken') + .query({ + client_secret: 'my_secret2', + token, + sid + }) + expect(response.status).toBe(302) + expect(response.headers.location).toBe( + new URL('http://localhost:8090').toString() + ) + }) + }) + }) + }) + + describe('/_matrix/identity/v2/3pid', () => { + describe('/_matrix/identity/v2/3pid/getValidated3pid', () => { + let sid: string, token: string + it('should reject missing parameters', async () => { const response = await request(app) - .post('/_matrix/identity/v2/validate/email/submitToken') + .get('/_matrix/identity/v2/3pid/getValidated3pid') + .query({ + client_secret: 'mysecret' + }) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body.errcode).toBe('M_MISSING_PARAMS') + }) + it('should return 404 if no valid session is found', async () => { + const response = await request(app) + .get('/_matrix/identity/v2/3pid/getValidated3pid') + .query({ + client_secret: 'invalidsecret', + sid: 'invalidsid' + }) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.body.errcode).toBe('M_NO_VALID_SESSION') + expect(response.statusCode).toBe(404) + }) + it('should return 400 if the session is not validated', async () => { + const responseRequestToken = await request(app) + .post('/_matrix/identity/v2/validate/email/requestToken') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') .send({ - token, + client_secret: 'newsecret', + email: 'xg@xnr.fr', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(responseRequestToken.statusCode).toBe(200) + expect(sendMailMock.mock.calls[0][0].to).toBe('xg@xnr.fr') + expect(sendMailMock.mock.calls[0][0].raw).toMatch( + /token=([a-zA-Z0-9]{64})&client_secret=newsecret&sid=([a-zA-Z0-9]{64})/ + ) + token = RegExp.$1 + sid = RegExp.$2 + + const response = await request(app) + .get('/_matrix/identity/v2/3pid/getValidated3pid') + .set('Authorization', `Bearer ${validToken}`) + .query({ + client_secret: 'newsecret', sid }) .set('Accept', 'application/json') + expect(response.body.errcode).toBe('M_SESSION_NOT_VALIDATED') expect(response.statusCode).toBe(400) }) - it('should accept to register mail after click', async () => { - const response = await request(app) + /* Works if the validationTime is set to 0 millisecond in 3pid/getValidated3pid.ts + it('should return 400 if the session is expired', async () => { + const responseSubmitToken = await request(app) .get('/_matrix/identity/v2/validate/email/submitToken') .query({ token, - client_secret: 'mysecret', + client_secret: 'newsecret', sid }) .set('Accept', 'application/json') - expect(response.body).toEqual({ success: true }) - expect(response.statusCode).toBe(200) - }) - it('should refuse a second registration', async () => { + expect(responseSubmitToken.body).toEqual({ success: true }) + expect(responseSubmitToken.statusCode).toBe(200) const response = await request(app) - .get('/_matrix/identity/v2/validate/email/submitToken') + .get('/_matrix/identity/v2/3pid/getValidated3pid') + .set('Authorization', `Bearer ${validToken}`) .query({ + client_secret: 'newsecret', + sid + }) + .set('Accept', 'application/json') + expect(response.body.errcode).toBe('M_SESSION_EXPIRED') + expect(response.statusCode).toBe(400) + }) + */ + it('should return 200 if a valid session is found', async () => { + const responseSubmitToken = await request(app) + .post('/_matrix/identity/v2/validate/email/submitToken') + .send({ token, - client_secret: 'mysecret', + client_secret: 'newsecret', + sid + }) + .set('Accept', 'application/json') + expect(responseSubmitToken.body).toEqual({ success: true }) + expect(responseSubmitToken.statusCode).toBe(200) + const response = await request(app) + .get('/_matrix/identity/v2/3pid/getValidated3pid') + .set('Authorization', `Bearer ${validToken}`) + .query({ + client_secret: 'newsecret', sid }) .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + }) + }) + + describe('/_matrix/identity/v2/3pid/bind', () => { + it('should find the 3pid - matrixID association after binding', async () => { + const responseRequestToken = await request(app) + .post('/_matrix/identity/v2/validate/email/requestToken') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret2', + email: 'ab@abc.fr', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(responseRequestToken.statusCode).toBe(200) + expect(sendMailMock.mock.calls[0][0].to).toBe('ab@abc.fr') + expect(sendMailMock.mock.calls[0][0].raw).toMatch( + /token=([a-zA-Z0-9]{64})&client_secret=mysecret2&sid=([a-zA-Z0-9]{64})/ + ) + const bindToken = RegExp.$1 + const bindSid = RegExp.$2 + const responseSubmitToken = await request(app) + .post('/_matrix/identity/v2/validate/email/submitToken') + .send({ + token: bindToken, + client_secret: 'mysecret2', + sid: bindSid + }) + .set('Accept', 'application/json') + expect(responseSubmitToken.statusCode).toBe(200) + const longKeyPair: { + publicKey: string + privateKey: string + keyId: string + } = generateKeyPair('ed25519') + await idServer.db.insert('longTermKeypairs', { + keyID: longKeyPair.keyId, + public: longKeyPair.publicKey, + private: longKeyPair.privateKey + }) + const responseBind = await request(app) + .post('/_matrix/identity/v2/3pid/bind') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret2', + sid: bindSid, + mxid: '@ab:abc.fr' + }) + expect(responseBind.statusCode).toBe(200) + expect(responseBind.body).toHaveProperty('signatures') + await idServer.cronTasks?.ready + const response = await request(app) + .get('/_matrix/identity/v2/hash_details') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.body).toHaveProperty('lookup_pepper') + expect(response.statusCode).toBe(200) + const pepper: string = response.body.lookup_pepper + const hash = new Hash() + await hash.ready + const computedHash = hash.sha256(`ab@abc.fr mail ${pepper}`) + const responseLookup = await request(app) + .post('/_matrix/identity/v2/lookup') + .send({ + addresses: [computedHash], + algorithm: 'sha256', + pepper + }) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(responseLookup.statusCode).toBe(200) + expect(responseLookup.body.mappings).toEqual({ + [computedHash]: '@ab:abc.fr' + }) + }) + it('should refuse an invalid client secret', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/3pid/bind') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + client_secret: 'a', + sid: 'sid', + mxid: '@ab:abc.fr' + }) + expect(response.statusCode).toBe(400) + }) + it('should refuse a session that has not been validated', async () => { + const response1 = await request(app) + .post('/_matrix/identity/v2/validate/email/requestToken') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret3', + email: 'abc@abc.fr', + next_link: 'http://localhost:8090', + send_attempt: 1 + }) + expect(response1.statusCode).toBe(200) + expect(sendMailMock).toHaveBeenCalled() + const sid3: string = response1.body.sid + const response2 = await request(app) + .post('/_matrix/identity/v2/3pid/bind') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret3', + sid: sid3, + mxid: '@abc:abc.fr' + }) + expect(response2.body.errcode).toBe('M_SESSION_NOT_VALIDATED') + expect(response2.statusCode).toBe(400) + }) + it('should refuse an invalid Matrix ID', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/3pid/bind') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret2', + sid: 'sid', + mxid: 'ab@abc.fr' + }) + expect(response.body.errcode).toBe('M_INVALID_PARAM') expect(response.statusCode).toBe(400) }) + it('should refuse an invalid session_id', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/3pid/bind') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret2', + sid: '$!:', + mxid: '@ab:abc.fr' + }) + expect(response.body.errcode).toBe('M_INVALID_PARAM') + expect(response.statusCode).toBe(400) + }) + it('should refuse a non-existing session ID or client secret', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/3pid/bind') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + client_secret: 'invalid_client_secret', + sid: 'invalid_sid', + mxid: '@ab:abc.fr' + }) + expect(response.body.errcode).toBe('M_NO_VALID_SESSION') + expect(response.statusCode).toBe(404) + }) + }) + describe('/_matrix/identity/v2/3pid/unbind', () => { + let token4: string + let sid4: string + it('should refuse an invalid Matrix ID', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/3pid/unbind') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + sid: 'sid', + client_secret: 'mysecret4', + threepid: { + address: 'unbind@unbind.fr', + medium: 'email' + }, + mxid: 'unbind@unbind.fr' + }) + expect(response.body.errcode).toBe('M_INVALID_PARAM') + expect(response.statusCode).toBe(400) + }) + it('should refuse an invalid client secret', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/3pid/unbind') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + mxid: '@ab:abc.fr', + client_secret: 'a', + sid: 'sid', + threepid: { + address: 'ab@abc.fr', + medium: 'email' + } + }) + expect(response.statusCode).toBe(400) + }) + it('should refuse an invalid session id', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/3pid/unbind') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + mxid: '@ab:abc.fr', + sid: '$!:', + client_secret: 'mysecret4', + threepid: { + address: 'ab@abc.fr', + medium: 'email' + } + }) + expect(response.statusCode).toBe(400) + }) + it('should refuse incompatible session_id and client_secret', async () => { + const responseRequestToken = await request(app) + .post('/_matrix/identity/v2/validate/email/requestToken') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret4', + email: 'unbind@unbind.fr', + send_attempt: 1 + }) + expect(responseRequestToken.statusCode).toBe(200) + expect(sendMailMock).toHaveBeenCalled() + expect(sendMailMock.mock.calls[0][0].to).toBe('unbind@unbind.fr') + expect(sendMailMock.mock.calls[0][0].raw).toMatch( + /token=([a-zA-Z0-9]{64})&client_secret=mysecret4&sid=([a-zA-Z0-9]{64})/ + ) + token4 = RegExp.$1 + sid4 = responseRequestToken.body.sid + const responseSubmitToken = await request(app) + .post('/_matrix/identity/v2/validate/email/submitToken') + .send({ + token: token4, + client_secret: 'mysecret4', + sid: sid4 + }) + .set('Accept', 'application/json') + expect(responseSubmitToken.statusCode).toBe(200) + const responseBind = await request(app) + .post('/_matrix/identity/v2/3pid/bind') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + client_secret: 'mysecret4', + sid: sid4, + mxid: '@unbind:unbind.fr' + }) + expect(responseBind.statusCode).toBe(200) + const response = await request(app) + .post('/_matrix/identity/v2/3pid/unbind') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + mxid: '@unbind:unbind.fr', + client_secret: 'mysecret_', + sid: sid4, + threepid: { + address: 'unbind@unbind.fr', + medium: 'email' + } + }) + expect(response.statusCode).toBe(403) + }) + it('should refuse an invalid threepid', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/3pid/unbind') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + mxid: '@ab:abc.fr', + sid: sid4, + client_secret: 'mysecret4', + threepid: { + address: 'ab@ab.fr', + medium: 'email' + } + }) + expect(response.statusCode).toBe(403) + }) + it('should unbind a 3pid when given the right parameters', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/3pid/unbind') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + mxid: '@unbind:unbind.fr', + client_secret: 'mysecret4', + sid: sid4, + threepid: { + address: 'unbind@unbind.fr' + } + }) + expect(response.statusCode).toBe(200) + }) }) }) - describe('/_matrix/identity/v2/lookup', () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars let pepper = '' @@ -482,6 +1172,268 @@ describe('Use configuration file', () => { }) }) + describe('/_matrix/identity/v2/store-invite', () => { + let longKeyPair: { publicKey: string; privateKey: string; keyId: string } + beforeAll(async () => { + // Insert a test key into the database + longKeyPair = generateKeyPair('ed25519') + await idServer.db.insert('longTermKeypairs', { + name: 'currentKey', + keyID: longKeyPair.keyId, + public: longKeyPair.publicKey, + private: longKeyPair.privateKey + }) + }) + + afterAll(async () => { + // Remove the test key from the database + await idServer.db.deleteEqual( + 'longTermKeypairs', + 'keyID', + longKeyPair.keyId + ) + }) + it('should require authentication', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/store-invite') + .set('Accept', 'application/json') + .send({ + address: 'xg@xnr.fr', + medium: 'email', + room_id: '!room:matrix.org', + sender: '@dwho:matrix.org' + }) + expect(response.statusCode).toBe(401) + expect(response.body.errcode).toEqual('M_UNAUTHORIZED') + }) + it('should require all parameters', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/store-invite') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + address: 'xg@xnr.fr', + medium: 'email', + room_id: '!room:matrix.org' + // sender: '@dwho:matrix.org' + }) + expect(response.statusCode).toBe(400) + expect(response.body.errcode).toEqual('M_MISSING_PARAMS') + }) + it('should reject an invalid medium', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/store-invite') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + address: 'xg@xnr.fr', + medium: 'invalid medium', + room_id: '!room:matrix.org', + sender: '@dwho:matrix.org' + }) + expect(response.statusCode).toBe(400) + expect(response.body.errcode).toEqual('M_UNRECOGNIZED') + }) + it('should reject an invalid email', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/store-invite') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + address: '@xg:xnr.fr', + medium: 'email', + room_id: '!room:matrix.org', + sender: '@dwho:matrix.org' + }) + expect(response.statusCode).toBe(400) + expect(response.body.errcode).toEqual('M_INVALID_PARAM') + }) + it('should reject an invalid phone number', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/store-invite') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + phone: '123', + medium: 'msisdn', + room_id: '!room:matrix.org', + sender: '@dwho:matrix.org' + }) + expect(response.statusCode).toBe(400) + expect(response.body.errcode).toEqual('M_INVALID_PARAM') + }) + it('should not send a mail if the address is already binded to a matrix id', async () => { + const pepper = ( + await idServer.db.get('keys', ['data'], { + name: 'pepper' + }) + )[0].data as string + const hash = new Hash() + await hash.ready + const hashedAddress = hash.sha256(`xg@xnr.fr mail ${pepper}`) + await idServer.db.insert('hashes', { + hash: hashedAddress, + pepper, + type: 'mail', + value: '@xg:xnr.fr', + active: 1 + }) + const response = await request(app) + .post('/_matrix/identity/v2/store-invite') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + address: 'xg@xnr.fr', + medium: 'email', + room_id: '!room:matrix.org', + sender: '@alice:example.org' + }) + expect(response.statusCode).toBe(400) + expect(response.body.errcode).toBe('M_THREEPID_IN_USE') + await idServer.db.deleteEqual('hashes', 'value', '@xg:xnr.fr') + }) + it('should accept a valid email request', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/store-invite') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + address: 'xg@xnr.fr', + medium: 'email', + room_id: '!room:matrix.org', + sender: '@dwho:matrix.org' + }) + expect(response.statusCode).toBe(200) + expect(sendMailMock).toHaveBeenCalled() + expect(sendMailMock.mock.calls[0][0].to).toBe('xg@xnr.fr') + expect(response.body).toHaveProperty('display_name') + expect(response.body.display_name).not.toBe('xg@xnr.fr') + expect(response.body).toHaveProperty('public_keys') + expect(response.body).toHaveProperty('token') + expect(response.body.token).toMatch(/^[a-zA-Z0-9]{64}$/) + }) + it('should accept a valid phone number request', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/store-invite') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + phone: '33612345671', + medium: 'msisdn', + room_id: '!room:matrix.org', + sender: '@dwho:matrix.org' + }) + expect(response.statusCode).toBe(200) + // TODO : add call to smsMock when it will be implemented + expect(response.body).toHaveProperty('display_name') + expect(response.body.display_name).not.toBe('33612345678') + expect(response.body).toHaveProperty('public_keys') + expect(response.body).toHaveProperty('token') + expect(response.body.token).toMatch(/^[a-zA-Z0-9]{64}$/) + }) + }) + + describe('/_matrix/identity/v2/sign-ed25519 ', () => { + let token: string + let longKeyPair: { publicKey: string; privateKey: string; keyId: string } + beforeAll(async () => { + longKeyPair = generateKeyPair('ed25519') + await idServer.db.insert('longTermKeypairs', { + name: 'currentKey', + keyID: longKeyPair.keyId, + public: longKeyPair.publicKey, + private: longKeyPair.privateKey + }) + }) + + afterAll(async () => { + await idServer.db.deleteEqual( + 'longTermKeypairs', + 'keyID', + longKeyPair.keyId + ) + }) + it('should refuse an invalid Matrix ID', async () => { + const mockResponse = Promise.resolve({ + ok: false, + status: 400, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + errcode: 'M_INVALID_PEPPER', + error: 'Unknown or invalid pepper - has it been rotated?' + }) + }) + // @ts-expect-error mock is unknown + fetch.mockImplementation(async () => await mockResponse) + await mockResponse + const responseStoreInvite = await request(app) + .post('/_matrix/identity/v2/store-invite') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + address: 'xg@xnr.fr', + medium: 'email', + room_id: '!room:matrix.org', + sender: '@dwho:matrix.org' + }) + expect(responseStoreInvite.statusCode).toBe(200) + token = responseStoreInvite.body.token + const response = await request(app) + .post('/_matrix/identity/v2/sign-ed25519') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + mxid: 'invalid_mxid', + private_key: longKeyPair.privateKey, + token + }) + expect(response.statusCode).toBe(400) + }) + it('should refuse an empty token', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/sign-ed25519') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + mxid: '@test:matrix.org', + private_key: longKeyPair.privateKey, + token: '' + }) + expect(response.statusCode).toBe(400) + }) + it('should refuse an invalid token', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/sign-ed25519') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + mxid: '@test:matrix.org', + private_key: longKeyPair.privateKey, + token: 'invalidtoken' + }) + expect(response.statusCode).toBe(404) + }) + it('should accept a valid token and sign the invitation details', async () => { + const response = await request(app) + .post('/_matrix/identity/v2/sign-ed25519') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ + mxid: '@test:matrix.org', + private_key: longKeyPair.privateKey, + token + }) + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('signatures') + const serverName = idServer.conf.server_name + expect(response.body.signatures[serverName]).toBeDefined() + expect(response.body.mxid).toBe('@test:matrix.org') + expect(response.body.sender).toBe('@dwho:matrix.org') + expect(response.body).toHaveProperty('token') + }) + }) + describe('/_matrix/identity/v2/account', () => { it('should accept valid token in headers', async () => { const response = await request(app) @@ -534,12 +1486,12 @@ describe('Use environment variables', () => { const mockResponse = Promise.resolve({ ok: true, status: 200, - json: () => { - return { + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ sub: '@dwho:example.com', 'm.server': 'matrix.example.com:8448' - } - } + }) }) // @ts-expect-error mock is unknown fetch.mockImplementation(async () => await mockResponse) @@ -607,3 +1559,138 @@ describe('Use environment variables', () => { }) }) }) + +// This test has to be executed after the others so as not to add policies to the database and make the authentication fail for all the other tests +describe('_matrix/identity/v2/terms', () => { + let idServer2: IdServer + let conf2: Config + let app2: express.Application + let validToken2: string + const userId = '@dwho:example.com' + const policies = { + privacy_policy: { + en: { + name: 'Privacy Policy', + url: 'https://example.org/somewhere/privacy-1.2-en.html' + }, + fr: { + name: 'Politique de confidentialité', + url: 'https://example.org/somewhere/privacy-1.2-fr.html' + }, + version: '1.2' + }, + terms_of_service: { + en: { + name: 'Terms of Service', + url: 'https://example.org/somewhere/terms-2.0-en.html' + }, + fr: { + name: "Conditions d'utilisation", + url: 'https://example.org/somewhere/terms-2.0-fr.html' + }, + version: '2.0' + } + } + beforeAll((done) => { + conf2 = { + ...defaultConfig, + database_engine: 'sqlite', + base_url: 'http://example.com/', + userdb_engine: 'sqlite', + policies + } + idServer2 = new IdServer(conf2) + app2 = express() + idServer2.ready + .then(() => { + Object.keys(idServer2.api.get).forEach((k) => { + app2.get(k, idServer2.api.get[k]) + }) + Object.keys(idServer.api.post).forEach((k) => { + app2.post(k, idServer2.api.post[k]) + }) + done() + }) + .catch((e) => { + done(e) + }) + }) + beforeAll(async () => { + idServer2.logger.info('Calling register to obtain a valid token') + const mockResponse = Promise.resolve({ + ok: true, + status: 200, + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ + sub: '@dwho:example.com', + 'm.server': 'matrix.example.com:8448' + }) + }) + // @ts-expect-error mock is unknown + fetch.mockImplementation(async () => await mockResponse) + await mockResponse + const response = await request(app2) + .post('/_matrix/identity/v2/account/register') + .send({ + access_token: 'bar', + expires_in: 86400, + matrix_server_name: 'matrix.example.com', + token_type: 'Bearer' + }) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + validToken2 = response.body.token + + idServer2.logger.info('Adding the policies for the user in the db') + try { + fillPoliciesDB(userId, idServer2, 0) + idServer2.logger.info('Successfully added policies for the user') + } catch (e) { + idServer2.logger.error('Error while setting up policies for the user', e) + } + }) + + afterAll(async () => { + idServer2.cleanJobs() + }) + + it('should update policies', async () => { + const response = await request(app2) + .post('/_matrix/identity/v2/terms') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${validToken2}`) + .send({ user_accepts: policies.privacy_policy.en.url }) + expect(response.statusCode).toBe(200) + const response2 = await idServer2.db.get('userPolicies', ['accepted'], { + user_id: userId, + policy_name: 'privacy_policy 1.2' + }) + expect(response2[0].accepted).toBe(1) + }) + it('should refuse authentifying a user who did not accept the terms', async () => { + const response = await request(app2) + .get('/_matrix/identity/v2/account') + .set('Authorization', `Bearer ${validToken2}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(403) + }) + describe('After accepting the terms', () => { + beforeAll(async () => { + idServer2.logger.info('Accepting the policies for the user in the db') + try { + fillPoliciesDB(userId, idServer2, 1) + idServer2.logger.info('Successfully accepted policies for the user') + } catch (e) { + idServer2.logger.error('Error while accepting policies for the user', e) + } + }) + it('should accept authentifying a user who accepted the terms', async () => { + const response = await request(app2) + .get('/_matrix/identity/v2/account') + .set('Authorization', `Bearer ${validToken2}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + }) + }) +}) diff --git a/packages/matrix-identity-server/src/index.ts b/packages/matrix-identity-server/src/index.ts index dd95602c..b614b589 100644 --- a/packages/matrix-identity-server/src/index.ts +++ b/packages/matrix-identity-server/src/index.ts @@ -5,13 +5,12 @@ import fs from 'fs' import defaultConfDesc from './config.json' import CronTasks from './cron' import { - Authenticate, - hostnameRe, + errMsg as _errMsg, + isHostnameValid, send, - type AuthenticationFunction, type expressAppHandler -} from './utils' -import { errMsg as _errMsg } from './utils/errors' +} from '@twake/utils' +import { Authenticate, type AuthenticationFunction } from './utils' import versions from './versions' // Endpoints @@ -22,11 +21,19 @@ import { } from '@twake/logger' import { type Request, type Response } from 'express' import rateLimit, { type RateLimitRequestHandler } from 'express-rate-limit' +import GetValidated3pid from './3pid' +import bind from './3pid/bind' +import unbind from './3pid/unbind' import account from './account' import logout from './account/logout' import register from './account/register' import Cache from './cache' import IdentityServerDb from './db' +import SignEd25519 from './ephemeral_signing' +import StoreInvit from './invitation' +import getPubkey from './keyManagement/getPubkey' +import isEphemeralPubkeyValid from './keyManagement/validEphemeralPubkey' +import isPubkeyValid from './keyManagement/validPubkey' import lookup from './lookup' import hashDetails from './lookup/hash_details' import updateHash from './lookup/updateHash' @@ -38,37 +45,39 @@ import UserDB from './userdb' import _validateMatrixToken from './utils/validateMatrixToken' import RequestToken from './validate/email/requestToken' import SubmitToken from './validate/email/submitToken' - export { type tokenContent } from './account/register' export { default as updateUsers } from './cron/updateUsers' -export * as IdentityServerDb from './db' +export { default as IdentityServerDb } from './db' export { default as createTables } from './db/sql/_createTables' export { default as Pg } from './db/sql/pg' -export * as SQLite from './db/sql/sqlite' +export { default as SQLite } from './db/sql/sqlite' export { default as MatrixDB, type MatrixDBBackend } from './matrixDb' +export { default as computePolicy } from './terms/_computePolicies' +export { getUrlsFromPolicies } from './terms/index.post' export * from './types' export { default as UserDB, type Collections as userDbCollections } from './userdb' export * as Utils from './utils' -export * as MatrixErrors from './utils/errors' -export const errMsg = _errMsg +export { default as UserDBPg } from './userdb/sql/pg' +export { default as UserDBSQLite } from './userdb/sql/sqlite' export const validateMatrixToken = _validateMatrixToken export const defaultConfig = defaultConfDesc export type IdServerAPI = Record -export default class MatrixIdentityServer { +export default class MatrixIdentityServer { api: { get: IdServerAPI post: IdServerAPI put?: IdServerAPI + delete?: IdServerAPI } - db: IdentityServerDb + db: IdentityServerDb userDB: UserDB - cronTasks?: CronTasks + cronTasks?: CronTasks conf: Config ready: Promise cache?: Cache @@ -83,9 +92,9 @@ export default class MatrixIdentityServer { } set authenticate(auth: AuthenticationFunction) { - this._authenticate = (req, res, cb) => { + this._authenticate = (req, res, cb, requiresTerms = true) => { this.rateLimiter(req as Request, res as Response, () => { - auth(req, res, cb) + auth(req, res, cb, requiresTerms) }) } } @@ -97,7 +106,8 @@ export default class MatrixIdentityServer { constructor( conf?: Partial, confDesc?: ConfigDescription, - logger?: TwakeLogger + logger?: TwakeLogger, + additionnalTables?: Record ) { this.api = { get: {}, post: {} } if (confDesc == null) confDesc = defaultConfDesc @@ -112,14 +122,17 @@ export default class MatrixIdentityServer { ? '/etc/twake/identity-server.conf' : undefined ) as Config - this.conf.federated_identity_services = - typeof this.conf.federated_identity_services === 'object' - ? this.conf.federated_identity_services - : typeof this.conf.federated_identity_services === 'string' - ? (this.conf.federated_identity_services as string) - .split(/[,\s]+/) - .filter((addr) => addr.match(hostnameRe)) - : [] + this.conf.federated_identity_services = Array.isArray( + this.conf.federated_identity_services + ) + ? this.conf.federated_identity_services.filter((addr) => + isHostnameValid(addr) + ) + : typeof this.conf.federated_identity_services === 'string' + ? (this.conf.federated_identity_services as string) + .split(/[,\s]+/) + .filter((addr) => isHostnameValid(addr)) + : [] this._convertStringtoNumberInConfig() this.rateLimiter = rateLimit({ windowMs: this.conf.rate_limiting_window, @@ -143,22 +156,31 @@ export default class MatrixIdentityServer { } // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions this.cache = this.conf.cache_engine ? new Cache(this.conf) : undefined - const db = (this.db = new IdentityServerDb(this.conf, this.logger)) + const db = (this.db = new IdentityServerDb( + this.conf, + this.logger, + additionnalTables + )) const userDB = (this.userDB = new UserDB( this.conf, this.logger, this.cache )) - this.authenticate = Authenticate(db, this.logger) + this.authenticate = Authenticate(db, this.logger) this.ready = new Promise((resolve, reject) => { Promise.all([db.ready, userDB.ready]) .then(() => { - this.cronTasks = new CronTasks(this.conf, db, userDB, this.logger) + this.cronTasks = new CronTasks( + this.conf, + db, + userDB, + this.logger + ) this.updateHash = updateHash this.cronTasks.ready .then(() => { const badMethod: expressAppHandler = (req, res) => { - send(res, 405, errMsg('unrecognized')) + send(res, 405, _errMsg('unrecognized')) } // TODO // const badEndPoint: expressAppHandler = (req, res) => { @@ -176,7 +198,19 @@ export default class MatrixIdentityServer { '/_matrix/identity/v2/validate/email/requestToken': badMethod, '/_matrix/identity/v2/validate/email/submitToken': - SubmitToken(this) + SubmitToken(this), + '/_matrix/identity/v2/pubkey/isvalid': isPubkeyValid( + this.db + ), + '/_matrix/identity/v2/pubkey/ephemeral/isvalid': + isEphemeralPubkeyValid(this.db), + '/_matrix/identity/v2/pubkey/:keyId': getPubkey(this.db), + '/_matrix/identity/v2/3pid/bind': badMethod, + '/_matrix/identity/v2/3pid/getValidated3pid': + GetValidated3pid(this), + '/_matrix/identity/v2/3pid/unbind': badMethod, + '/_matrix/identity/v2/store-invite': badMethod, + '/_matrix/identity/v2/sign-ed25519': badMethod }, post: { '/_matrix/identity/v2': badMethod, @@ -192,7 +226,15 @@ export default class MatrixIdentityServer { '/_matrix/identity/v2/validate/email/requestToken': RequestToken(this), '/_matrix/identity/v2/validate/email/submitToken': - SubmitToken(this) + SubmitToken(this), + '/_matrix/identity/v2/pubkey/isvalid': badMethod, + '/_matrix/identity/v2/pubkey/ephemeral/isvalid': badMethod, + '/_matrix/identity/v2/pubkey/:keyId': badMethod, + '/_matrix/identity/v2/3pid/getValidated3pid': badMethod, + '/_matrix/identity/v2/3pid/bind': bind(this), + '/_matrix/identity/v2/3pid/unbind': unbind(this), + '/_matrix/identity/v2/store-invite': StoreInvit(this), + '/_matrix/identity/v2/sign-ed25519': SignEd25519(this) } } resolve(true) diff --git a/packages/matrix-identity-server/src/invitation/index.ts b/packages/matrix-identity-server/src/invitation/index.ts new file mode 100644 index 00000000..8d4c2bda --- /dev/null +++ b/packages/matrix-identity-server/src/invitation/index.ts @@ -0,0 +1,310 @@ +import { Hash, randomString } from '@twake/crypto' +import fs from 'fs' +import type MatrixIdentityServer from '../index' +import { type Config } from '../types' +import { + errMsg, + jsonContent, + send, + validateParameters, + type expressAppHandler +} from '@twake/utils' +import Mailer from '../utils/mailer' +import { lookup3pid } from '../lookup' + +interface storeInvitationArgs { + address: string + phone: string + medium: string + room_alias?: string + room_avatar_url?: string + room_id: string + room_join_rules?: string + room_name?: string + room_type?: string + sender: string + sender_avatar_url?: string + sender_display_name?: string +} + +const schema = { + address: false, + phone: false, + medium: true, + room_alias: false, + room_avatar_url: false, + room_id: true, + room_join_rules: false, + room_name: false, + room_type: false, + sender: true, + sender_avatar_url: false, + sender_display_name: false +} + +const preConfigureTemplate = ( + template: string, + conf: Config, + transport: Mailer +): string => { + const mb = randomString(32) + return ( + template + // initialize "From" + .replace(/__from__/g, transport.from) + // fix multipart stuff + .replace(/__multipart_boundary__/g, mb) + ) +} + +// TODO : modify this if necessary +const inviteLink = ( + server: string, + senderId: string, + roomAlias?: string +): string => { + if (roomAlias != null) { + return `https://${server}/#/${roomAlias}` + } else { + return `https://${server}/#/${senderId}` + } +} + +const mailBody = ( + template: string, + // eslint-disable-next-line @typescript-eslint/naming-convention + inviter_name: string, + // eslint-disable-next-line @typescript-eslint/naming-convention + sender_user_id: string, + dst: string, + // eslint-disable-next-line @typescript-eslint/naming-convention + room_name: string, + // eslint-disable-next-line @typescript-eslint/naming-convention + room_avatar: string, + // eslint-disable-next-line @typescript-eslint/naming-convention + room_type: string, + // eslint-disable-next-line @typescript-eslint/naming-convention + server_name_creating_invitation: string, + // eslint-disable-next-line @typescript-eslint/naming-convention + room_alias?: string +): string => { + return ( + template + // set "To" + .replace(/__to__/g, dst) + // Set __inviter_name__ + .replace(/__inviter_name__/g, inviter_name) + // set date + .replace(/__date__/g, new Date().toUTCString()) + // initialize message id + .replace(/__messageid__/g, randomString(32)) + .replace(/__room_name__/g, room_name) + .replace(/__room_avatar__/g, room_avatar) + .replace(/__room_type__/g, room_type) + .replace( + /__link__/g, + inviteLink(server_name_creating_invitation, sender_user_id, room_alias) + ) + ) +} + +// To complete if another 3PID is added for this endpoint +const validMediums: string[] = ['email', 'msisdn'] + +// Regular expressions for different mediums +const validEmailRe = /^\w[+.-\w]*\w@\w[.-\w]*\w\.\w{2,6}$/ +const validPhoneRe = /^\d{4,16}$/ + +const redactAddress = (medium: string, address: string): string => { + switch (medium) { + case 'email': { + const atIndex = address.indexOf('@') + const localPart = address.slice(0, atIndex) + const domainPart = address.slice(atIndex + 1) + + const redactedLocalPart = replaceLastCharacters(localPart) + const redactedDomainPart = replaceLastCharacters(domainPart) + + return `${redactedLocalPart}@${redactedDomainPart}` + } + case 'msisdn': + return replaceLastCharacters(address) + /* istanbul ignore next : call to redactAddress is done after checking if the medium was valid */ + default: + return address + } +} + +const replaceLastCharacters = ( + str: string, + redactionRatio: number = 0.4 +): string => { + const chars = str.split('') + const redactionCount = Math.ceil(chars.length * redactionRatio) + + // Replace the last `redactionCount` characters with '*' + for (let i = chars.length - redactionCount; i < chars.length; i++) { + chars[i] = '*' + } + + return chars.join('') +} + +const StoreInvit = ( + idServer: MatrixIdentityServer +): expressAppHandler => { + const transport = new Mailer(idServer.conf) + const verificationTemplate = preConfigureTemplate( + fs + .readFileSync(`${idServer.conf.template_dir}/3pidInvitation.tpl`) + .toString(), + idServer.conf, + transport + ) + return (req, res) => { + idServer.authenticate(req, res, (_data, _id) => { + jsonContent(req, res, idServer.logger, (obj) => { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + validateParameters(res, schema, obj, idServer.logger, async (obj) => { + const medium = (obj as storeInvitationArgs).medium + if (!validMediums.includes(medium)) { + send( + res, + 400, + errMsg('unrecognized', 'This medium is not supported.') + ) + return + } + const address = (obj as storeInvitationArgs).address + const phone = (obj as storeInvitationArgs).phone + let mediumAddress: string = '' + // Check the validity of the media + switch (medium) { + case 'email': + if (address == null || !validEmailRe.test(address)) { + send(res, 400, errMsg('invalidParam', 'Invalid email address.')) + return + } else mediumAddress = address + break + case 'msisdn': + if (phone == null || !validPhoneRe.test(phone)) { + send(res, 400, errMsg('invalidParam', 'Invalid phone number.')) + return + } else mediumAddress = phone + break + } + // Call to the lookup API to check for any existing third-party identifiers + try { + const pepperRows = await idServer.db.get('keys', ['data'], { + name: 'pepper' + }) + if (pepperRows.length === 0) { + // This should never happen + // istanbul ignore next + send(res, 500, errMsg('unknown', 'Pepper not found')) + // istanbul ignore next + return + } + const pepper = pepperRows[0].data as string + const field = medium === 'email' ? 'mail' : 'msisdn' + const hash = new Hash() + await hash.ready + const hashedAddress = hash.sha256( + `${mediumAddress} ${field} ${pepper}` + ) + const mappings = ( + await lookup3pid(idServer, { + addresses: [hashedAddress] + }) + )[0] + if (Object.keys(mappings).length > 0) { + send(res, 400, { + errcode: 'M_THREEPID_IN_USE', + error: + 'The third party identifier is already in use by another user.', + mxid: (obj as storeInvitationArgs).sender + }) + } else { + // Create invitation token + const ephemeralKey = await idServer.db.createKeypair( + 'shortTerm', + 'curve25519' + ) + const objWithKey = { + ...(obj as storeInvitationArgs), + key: ephemeralKey + } + const token = await idServer.db.createInvitationToken( + mediumAddress, + objWithKey + ) + // Send email/sms + switch (medium) { + case 'email': + void transport.sendMail({ + to: address, + raw: mailBody( + verificationTemplate, + (obj as storeInvitationArgs).sender_display_name ?? + '*****', + (obj as storeInvitationArgs).sender, + address, + (obj as storeInvitationArgs).room_name ?? '*****', + (obj as storeInvitationArgs).room_avatar_url ?? '*****', + (obj as storeInvitationArgs).room_type ?? '*****', + idServer.conf.invitation_server_name ?? 'matrix.to', + (obj as storeInvitationArgs).room_alias + ) + }) + break + case 'msisdn': + // TODO implement smsSender + break + } + // Send 200 response + const redactedAddress = redactAddress(medium, mediumAddress) + idServer.db + .getKeys('current') + .then((keys) => { + const responseBody = { + display_name: redactedAddress, + public_keys: [ + { + key_validity_url: `https://${idServer.conf.server_name}/_matrix/identity/v2/pubkey/isvalid`, + public_key: keys.publicKey + }, + { + key_validity_url: `https://${idServer.conf.server_name}/_matrix/identity/v2/pubkey/ephemeral/isvalid`, + public_key: ephemeralKey.privateKey + } + ], + token + } + send(res, 200, responseBody) + }) + .catch((err) => { + /* istanbul ignore next */ + idServer.logger.debug( + 'Error while getting the current key', + err + ) + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', err.toString())) + }) + } + } catch (err) { + /* istanbul ignore next */ + idServer.logger.error( + 'Error while making a call to the lookup API (/_matrix/identity/v2/lookup)', + err + ) + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', err as string)) + } + }) + }) + }) + } +} + +export default StoreInvit diff --git a/packages/matrix-identity-server/src/invitation/invitation.md b/packages/matrix-identity-server/src/invitation/invitation.md new file mode 100644 index 00000000..df695252 --- /dev/null +++ b/packages/matrix-identity-server/src/invitation/invitation.md @@ -0,0 +1,19 @@ +# Extension of the Matrix specification v1.11 : adding phone ('msisdn') to valid 3pid invitation media + +- the parameter media now accepts two values : 'email' and 'msisdn' +- a parameter phone is added to the request body +- a new scheme for request body parameters is adopted : + +--- + +| | address : REQUIRED | +| medium === 'email' | | +| | phone : OPTIONAL | + +--- + +| | address : OPTIONAL | +| medium === 'msisdn' | | +| | phone : REQUIRED | + +--- diff --git a/packages/matrix-identity-server/src/keyManagement/getPubkey.ts b/packages/matrix-identity-server/src/keyManagement/getPubkey.ts new file mode 100644 index 00000000..baa60327 --- /dev/null +++ b/packages/matrix-identity-server/src/keyManagement/getPubkey.ts @@ -0,0 +1,41 @@ +import { type Request } from 'express' +import type IdentityServerDB from '../db' +import { errMsg, send, type expressAppHandler } from '@twake/utils' + +const getPubkey = ( + idServerDB: IdentityServerDB +): expressAppHandler => { + return (req, res) => { + const _keyID: string = (req as Request).params.keyId + + idServerDB + .get('shortTermKeypairs', ['public'], { keyID: _keyID }) + // eslint-disable-next-line @typescript-eslint/promise-function-async + .then((rows) => { + if (rows.length === 1) { + send(res, 200, { public_key: rows[0].public }) + } else { + return idServerDB + .get('longTermKeypairs', ['public'], { keyID: _keyID }) + .then((rows) => { + if (rows.length === 0) { + send( + res, + 404, + errMsg('notFound', 'The public key was not found') + ) + } else { + send(res, 200, { public_key: rows[0].public }) + } + }) + } + }) + .catch((e) => { + console.error('Error querying keypairs:', e) // Debugging statement + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', e.toString())) + }) + } +} + +export default getPubkey diff --git a/packages/matrix-identity-server/src/keyManagement/updateKey.test.ts b/packages/matrix-identity-server/src/keyManagement/updateKey.test.ts new file mode 100644 index 00000000..e71db80a --- /dev/null +++ b/packages/matrix-identity-server/src/keyManagement/updateKey.test.ts @@ -0,0 +1,101 @@ +import { getLogger, type TwakeLogger } from '@twake/logger' +import { generateKeyPair } from '@twake/crypto' +import fs from 'fs' +import defaultConfig from '../config.json' +import IdentityServerDB from '../db' +import { type Config } from '../types' +import updateKey from './updateKey' + +const conf: Config = { + ...defaultConfig, + database_engine: 'sqlite', + database_host: ':memory:', + userdb_engine: 'sqlite', + userdb_host: './src/__testData__/key.db', + server_name: 'company.com' +} + +const logger: TwakeLogger = getLogger() + +describe('updateHashes', () => { + let db: IdentityServerDB + let currentKey: { publicKey: string; privateKey: string; keyId: string } + let previousKey: { publicKey: string; privateKey: string; keyId: string } + + beforeAll((done) => { + db = new IdentityServerDB(conf, logger) + db.ready + .then(() => { + currentKey = generateKeyPair('ed25519') + previousKey = generateKeyPair('ed25519') + db.insert('longTermKeypairs', { + name: 'currentKey', + keyID: currentKey.keyId, + public: currentKey.publicKey, + private: currentKey.privateKey + }) + .then(() => { + db.insert('longTermKeypairs', { + name: 'previousKey', + keyID: previousKey.keyId, + public: previousKey.publicKey, + private: previousKey.privateKey + }) + .then(() => { + done() + }) + .catch((e) => { + done(e) + }) + }) + .catch((e) => { + done(e) + }) + }) + .catch((e) => { + done(e) + }) + }) + + afterAll(() => { + clearTimeout(db.cleanJob) + if (fs.existsSync('./src/__testData__/key.db')) { + fs.unlinkSync('./src/__testData__/key.db') + } + db.close() + logger.close() + }) + + it('should be able to generate new key and update concerned fields', (done) => { + updateKey(db, logger).catch((e) => { + done(e) + }) + setTimeout(() => { + db.get('longTermKeypairs', ['keyID', 'public', 'private'], { + name: 'currentKey' + }) + .then((currentKeyRows) => { + expect(currentKeyRows.length).toEqual(1) + expect(currentKeyRows[0].keyID).not.toEqual(undefined) + expect(currentKeyRows[0].public).not.toEqual(undefined) + expect(currentKeyRows[0].private).not.toEqual(undefined) + db.get('longTermKeypairs', ['keyID', 'public', 'private'], { + name: 'previousKey' + }) + .then((previousKeyRows) => { + expect(previousKeyRows.length).toEqual(1) + expect(previousKeyRows[0].keyID).toEqual(currentKey.keyId) + expect(previousKeyRows[0].public).toEqual(currentKey.publicKey) + expect(previousKeyRows[0].private).toEqual(currentKey.privateKey) + done() + }) + .catch((e) => { + done(e) + }) + }) + .catch((e) => { + done(e) + }) + }, 1000) + }) +}) diff --git a/packages/matrix-identity-server/src/keyManagement/updateKey.ts b/packages/matrix-identity-server/src/keyManagement/updateKey.ts new file mode 100644 index 00000000..1055c853 --- /dev/null +++ b/packages/matrix-identity-server/src/keyManagement/updateKey.ts @@ -0,0 +1,98 @@ +/* istanbul ignore file */ + +// TO BE MODIFIED LATER ON --- FILE NOT IN USE FOR THE MOMENT + +/** + * Change long-term key + */ + +import { generateKeyPair } from '@twake/crypto' +import { type TwakeLogger } from '@twake/logger' +import type IdentityServerDb from '../db' + +const updateKey = async ( + db: IdentityServerDb, + logger: TwakeLogger +): Promise => { + try { + // Step 1: + // - Drop old-old key + // - Get current key + const previousKeyRows = await db.get('longTermKeypairs', ['keyID'], { + name: 'previousKey' + }) + + if (previousKeyRows.length === 0) { + /* istanbul ignore next */ + throw new Error('No previousKey found') + } + + // Check if keyID is in the correct format /^ed25519:[A-Za-z0-9_-]+$/ + if (!/^ed25519:[A-Za-z0-9_-]+$/.test(previousKeyRows[0].keyID as string)) { + /* istanbul ignore next */ + throw new Error('previousKey value is not valid') + } + + const currentKeyRows = await db.get( + 'longTermKeypairs', + ['keyID', 'public', 'private'], + { name: 'currentKey' } + ) + + if (currentKeyRows.length === 0) { + /* istanbul ignore next */ + throw new Error('currentKey undefined') + } + + // Step 2: + // - Generate new key pair + // - Set previousKey to current value + // - Update database with new key pair + const newKey = generateKeyPair('ed25519') + + try { + await db.update( + 'longTermKeypairs', + { + keyID: currentKeyRows[0].keyID as string, + public: currentKeyRows[0].public as string, + private: currentKeyRows[0].private as string + }, + 'name', + 'previousKey' + ) + logger.info('Previous key updated successfully') + } catch (error) { + /* istanbul ignore next */ + logger.error('Error updating previous key', error) + /* istanbul ignore next */ + throw error + } + + try { + await db.update( + 'longTermKeypairs', + { + keyID: newKey.keyId, + public: newKey.publicKey, + private: newKey.privateKey + }, + 'name', + 'currentKey' + ) + logger.info('Current key updated successfully') + } catch (error) { + /* istanbul ignore next */ + logger.error('Error updating current key', error) + /* istanbul ignore next */ + throw error + } + + logger.info('Long-term key updated successfully') + } catch (error) { + /* istanbul ignore next */ + logger.error('Error updating long-term key', error) + } +} + +export default updateKey diff --git a/packages/matrix-identity-server/src/keyManagement/validEphemeralPubkey.ts b/packages/matrix-identity-server/src/keyManagement/validEphemeralPubkey.ts new file mode 100644 index 00000000..145f87b4 --- /dev/null +++ b/packages/matrix-identity-server/src/keyManagement/validEphemeralPubkey.ts @@ -0,0 +1,37 @@ +import { type Request } from 'express' +import type IdentityServerDB from '../db' +import { errMsg, send, type expressAppHandler } from '@twake/utils' + +const isEphemeralPubkeyValid = ( + idServerDB: IdentityServerDB +): expressAppHandler => { + return (req, res) => { + const publicKey = (req as Request).query.public_key + if ( + publicKey !== undefined && + typeof publicKey === 'string' && + publicKey.length > 0 + ) { + idServerDB + .get('shortTermKeypairs', ['public'], { + public: publicKey + }) + .then((rows) => { + if (rows.length === 0) { + send(res, 200, { valid: false }) + } else { + // TO DO : ensure that the pubkey only appears one time + send(res, 200, { valid: true }) + } + }) + .catch((e) => { + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', e.toString())) + }) + } else { + send(res, 400, errMsg('missingParams')) + } + } +} + +export default isEphemeralPubkeyValid diff --git a/packages/matrix-identity-server/src/keyManagement/validPubkey.ts b/packages/matrix-identity-server/src/keyManagement/validPubkey.ts new file mode 100644 index 00000000..d602e1d7 --- /dev/null +++ b/packages/matrix-identity-server/src/keyManagement/validPubkey.ts @@ -0,0 +1,37 @@ +import { type Request } from 'express' +import type IdentityServerDB from '../db' +import { errMsg, send, type expressAppHandler } from '@twake/utils' + +const isPubkeyValid = ( + idServerDB: IdentityServerDB +): expressAppHandler => { + return (req, res) => { + const publicKey = (req as Request).query.public_key + if ( + publicKey !== undefined && + typeof publicKey === 'string' && + publicKey.length > 0 + ) { + idServerDB + .get('longTermKeypairs', ['public'], { + public: publicKey + }) + .then((rows) => { + if (rows.length === 0) { + send(res, 200, { valid: false }) + } else { + // TODO : ensure that the pubkey only appears one time + send(res, 200, { valid: true }) + } + }) + .catch((e) => { + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', e.toString())) + }) + } else { + send(res, 400, errMsg('missingParams')) + } + } +} + +export default isPubkeyValid diff --git a/packages/matrix-identity-server/src/lookup/README.md b/packages/matrix-identity-server/src/lookup/README.md index a5846994..f805b4fc 100644 --- a/packages/matrix-identity-server/src/lookup/README.md +++ b/packages/matrix-identity-server/src/lookup/README.md @@ -1,4 +1,4 @@ # lookup Lookup endpoints uses "pepper" and hashes provided by -[updateHashes](../cron/updateHashes.ts) task. \ No newline at end of file +[updateHashes](../cron/updateHashes.ts) task. diff --git a/packages/matrix-identity-server/src/lookup/hash_details.ts b/packages/matrix-identity-server/src/lookup/hash_details.ts index 48c050fc..2b29b569 100644 --- a/packages/matrix-identity-server/src/lookup/hash_details.ts +++ b/packages/matrix-identity-server/src/lookup/hash_details.ts @@ -1,9 +1,10 @@ import { supportedHashes } from '@twake/crypto' -import { send, type expressAppHandler } from '../utils' -import { errMsg } from '../utils/errors' import type MatrixIdentityServer from '..' +import { errMsg, send, type expressAppHandler } from '@twake/utils' -const hashDetails = (idServer: MatrixIdentityServer): expressAppHandler => { +const hashDetails = ( + idServer: MatrixIdentityServer +): expressAppHandler => { return (req, res) => { idServer.authenticate(req, res, (tokenContent, id) => { idServer.db @@ -16,7 +17,7 @@ const hashDetails = (idServer: MatrixIdentityServer): expressAppHandler => { }) .catch((e) => { /* istanbul ignore next */ - send(res, 500, errMsg('unknown', e)) + send(res, 500, errMsg('unknown', e.toString())) }) }) } diff --git a/packages/matrix-identity-server/src/lookup/index.ts b/packages/matrix-identity-server/src/lookup/index.ts index f51a005a..c4bb0c06 100644 --- a/packages/matrix-identity-server/src/lookup/index.ts +++ b/packages/matrix-identity-server/src/lookup/index.ts @@ -1,11 +1,11 @@ import type MatrixIdentityServer from '..' import { + errMsg, jsonContent, send, validateParameters, type expressAppHandler -} from '../utils' -import { errMsg } from '../utils/errors' +} from '@twake/utils' const schema = { addresses: true, @@ -13,7 +13,30 @@ const schema = { pepper: false } -const lookup = (idServer: MatrixIdentityServer): expressAppHandler => { +export const lookup3pid = async ( + idServer: MatrixIdentityServer, + obj: { addresses: string[] } +): Promise>> => { + const rows = await idServer.db.get('hashes', ['value', 'hash', 'active'], { + hash: obj.addresses + }) + const mappings: Record = {} + const inactives: Record = {} + rows.forEach((row) => { + if (row.active === 1) { + // @ts-expect-error row.hash is not null + mappings[row.hash] = row.value + } else { + // @ts-expect-error row.hash is not null + inactives[row.hash] = row.value + } + }) + return [mappings, inactives] +} + +const lookup = ( + idServer: MatrixIdentityServer +): expressAppHandler => { return (req, res) => { idServer.authenticate(req, res, (data, id) => { jsonContent(req, res, idServer.logger, (obj) => { @@ -34,32 +57,20 @@ const lookup = (idServer: MatrixIdentityServer): expressAppHandler => { idServer.logger.debug( `lookup request to search ${JSON.stringify(obj)}` ) - idServer.db - .get('hashes', ['value', 'hash', 'active'], { - hash: (obj as { addresses: string[] }).addresses - }) - .then((rows) => { - // send(res, 200, rows) - const mappings: Record = {} - const inactives: Record = {} - rows.forEach((row) => { - if (row.active === 1) { - // @ts-expect-error row.hash is not null - mappings[row.hash] = row.value - } else { - // @ts-expect-error row.hash is not null - inactives[row.hash] = row.value - } - }) + lookup3pid(idServer, obj as { addresses: string[] }) + .then((result) => { if (idServer.conf.additional_features ?? false) { - send(res, 200, { mappings, inactive_mappings: inactives }) + send(res, 200, { + mappings: result[0], + inactive_mappings: result[1] + }) } else { - send(res, 200, { mappings }) + send(res, 200, { mappings: result[0] }) } }) .catch((e) => { /* istanbul ignore next */ - send(res, 500, errMsg('unknown', e)) + send(res, 500, errMsg('unknown', e.toString())) }) } }) diff --git a/packages/matrix-identity-server/src/lookup/updateHash.ts b/packages/matrix-identity-server/src/lookup/updateHash.ts index d080dbfb..7d3f13a6 100644 --- a/packages/matrix-identity-server/src/lookup/updateHash.ts +++ b/packages/matrix-identity-server/src/lookup/updateHash.ts @@ -32,8 +32,8 @@ type _Update = ( ) => Promise // eslint-disable-next-line @typescript-eslint/promise-function-async -const updateHash = ( - db: IdentityServerDb, +const updateHash = ( + db: IdentityServerDb, logger: TwakeLogger, data: UpdatableFields, pepper?: string diff --git a/packages/matrix-identity-server/src/matrixDb/index.ts b/packages/matrix-identity-server/src/matrixDb/index.ts index 5122c2ce..0e9a5a49 100644 --- a/packages/matrix-identity-server/src/matrixDb/index.ts +++ b/packages/matrix-identity-server/src/matrixDb/index.ts @@ -3,7 +3,7 @@ import { type Config, type DbGetResult } from '../types' import MatrixDBPg from './sql/pg' import MatrixDBSQLite from './sql/sqlite' -type Collections = +export type Collections = | 'users' | 'room_memberships' | 'room_stats_state' diff --git a/packages/matrix-identity-server/src/matrixDb/sql/pg.ts b/packages/matrix-identity-server/src/matrixDb/sql/pg.ts index 31061f3d..0c48b89c 100644 --- a/packages/matrix-identity-server/src/matrixDb/sql/pg.ts +++ b/packages/matrix-identity-server/src/matrixDb/sql/pg.ts @@ -1,11 +1,11 @@ import { type TwakeLogger } from '@twake/logger' import { type ClientConfig } from 'pg' +import { type Collections } from '..' import { type MatrixDBBackend } from '../' -import { type Collections } from '../../db' import Pg from '../../db/sql/pg' import { type Config } from '../../types' -class MatrixDBPg extends Pg implements MatrixDBBackend { +class MatrixDBPg extends Pg implements MatrixDBBackend { // eslint-disable-next-line @typescript-eslint/promise-function-async createDatabases( conf: Config, diff --git a/packages/matrix-identity-server/src/matrixDb/sql/sqlite.ts b/packages/matrix-identity-server/src/matrixDb/sql/sqlite.ts index 6e6fbba2..813b4d3a 100644 --- a/packages/matrix-identity-server/src/matrixDb/sql/sqlite.ts +++ b/packages/matrix-identity-server/src/matrixDb/sql/sqlite.ts @@ -1,9 +1,8 @@ -import { type Collections } from '../../db' -import { type MatrixDBBackend } from '../' -import { type Config } from '../../types' +import { type Collections, type MatrixDBBackend } from '../' import SQLite from '../../db/sql/sqlite' +import { type Config } from '../../types' -class MatrixDBSQLite extends SQLite implements MatrixDBBackend { +class MatrixDBSQLite extends SQLite implements MatrixDBBackend { // eslint-disable-next-line @typescript-eslint/promise-function-async createDatabases( conf: Config, diff --git a/packages/matrix-identity-server/src/status.ts b/packages/matrix-identity-server/src/status.ts index 22602118..7b640796 100644 --- a/packages/matrix-identity-server/src/status.ts +++ b/packages/matrix-identity-server/src/status.ts @@ -1,4 +1,4 @@ -import { type expressAppHandler, send } from './utils' +import { type expressAppHandler, send } from '@twake/utils' const status: expressAppHandler = (req, res, next) => { send(res, 200, {}) diff --git a/packages/matrix-identity-server/src/terms.test.ts b/packages/matrix-identity-server/src/terms.test.ts index 68d4d3a5..56a969cc 100644 --- a/packages/matrix-identity-server/src/terms.test.ts +++ b/packages/matrix-identity-server/src/terms.test.ts @@ -72,12 +72,12 @@ test('Get authentication token', async () => { const mockResponse = Promise.resolve({ ok: true, status: 200, - json: () => { - return { + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ sub: '@dwho:example.com', 'm.server': 'matrix.example.com:8448' - } - } + }) }) // @ts-expect-error mock is unknown fetch.mockImplementation(async () => await mockResponse) diff --git a/packages/matrix-identity-server/src/terms/__testData__/policies.json b/packages/matrix-identity-server/src/terms/__testData__/policies.json index ac9fdb18..dbcc0323 100644 --- a/packages/matrix-identity-server/src/terms/__testData__/policies.json +++ b/packages/matrix-identity-server/src/terms/__testData__/policies.json @@ -21,4 +21,4 @@ }, "version": "2.0" } -} \ No newline at end of file +} diff --git a/packages/matrix-identity-server/src/terms/index.post.ts b/packages/matrix-identity-server/src/terms/index.post.ts index 7e7cec16..d13918ae 100644 --- a/packages/matrix-identity-server/src/terms/index.post.ts +++ b/packages/matrix-identity-server/src/terms/index.post.ts @@ -2,17 +2,18 @@ import { type Policies } from '.' import type MatrixIdentityServer from '..' import { + errMsg, jsonContent, send, validateParameters, type expressAppHandler -} from '../utils' -import { errMsg } from '../utils/errors' +} from '@twake/utils' import computePolicy from './_computePolicies' +import { type DbGetResult } from '..' type UrlsFromPolicies = Record -const getUrlsFromPolicies = (policies: Policies): UrlsFromPolicies => { +export const getUrlsFromPolicies = (policies: Policies): UrlsFromPolicies => { const urlsFromPolicies: UrlsFromPolicies = {} Object.keys(policies).forEach((policyName) => { const policy = policies[policyName as 'privacy_policy' | 'terms_of_service'] @@ -31,41 +32,123 @@ const getUrlsFromPolicies = (policies: Policies): UrlsFromPolicies => { return urlsFromPolicies } -const PostTerms = (idServer: MatrixIdentityServer): expressAppHandler => { +/* +Filling every policy for a given user. Useful when setting up test data or initializing every policy to not accepted. +CAUTION: This function completely overwrites the user's previous policy acceptance status. +*/ + +// eslint-disable-next-line @typescript-eslint/promise-function-async +export const fillPoliciesDB = ( + userId: string, + idServer: MatrixIdentityServer, + accepted: number +): Promise => { + const policies = getUrlsFromPolicies( + computePolicy(idServer.conf, idServer.logger) + ) + const policyNames = Object.keys(policies) + idServer.logger.debug( + `Filling policies ${policyNames.join(', ')} for user userId ${userId}` + ) + + // eslint-disable-next-line @typescript-eslint/promise-function-async + const promises: Array> = policyNames.map( + // eslint-disable-next-line @typescript-eslint/promise-function-async + (policyName) => { + return ( + idServer.db + .get('userPolicies', [], { + user_id: userId, + policy_name: policyName + }) + // eslint-disable-next-line @typescript-eslint/promise-function-async + .then((alreadyExists) => { + if (alreadyExists.length > 0) { + return idServer.db.updateAnd( + 'userPolicies', + { accepted }, + { field: 'user_id', value: userId }, + { field: 'policy_name', value: policyName } + ) + } else { + return idServer.db.insert('userPolicies', { + policy_name: policyName, + user_id: userId, + accepted + }) + } + }) + .catch((e) => { + /* istanbul ignore next - Tested separatly by deliberately violating unique constraints in the insert above */ + idServer.logger.error('Error filling policies', e) + /* istanbul ignore next */ + throw e // Re-throw the error to be caught by Promise.all + }) + ) + } + ) + + return Promise.all(promises) +} + +const PostTerms = ( + idServer: MatrixIdentityServer +): expressAppHandler => { const urlsFromPolicies = getUrlsFromPolicies( computePolicy(idServer.conf, idServer.logger) ) return (req, res) => { - idServer.authenticate(req, res, (data, id) => { - jsonContent(req, res, idServer.logger, (data) => { - validateParameters( - res, - { user_accepts: true }, - data, - idServer.logger, - (data) => { - let urls = (data as { user_accepts: string[] | string }) - .user_accepts - const done: string[] = [] - /* istanbul ignore if */ - if (typeof urls === 'string') urls = [urls] - Object.keys(urlsFromPolicies).forEach((policyName) => { - ;(urls as string[]).forEach((url) => { - if (urlsFromPolicies[policyName].includes(url)) { - done.push(policyName) - } + idServer.authenticate( + req, + res, + (data, id) => { + const userId: string = data.sub + + jsonContent(req, res, idServer.logger, (data) => { + validateParameters( + res, + { user_accepts: true }, + data, + idServer.logger, + (data) => { + let urls = (data as { user_accepts: string[] | string }) + .user_accepts + const done: string[] = [] + if (typeof urls === 'string') urls = [urls] + Object.keys(urlsFromPolicies).forEach((policyName) => { + ;(urls as string[]).forEach((url) => { + if (urlsFromPolicies[policyName].includes(url)) { + done.push(policyName) + } + }) }) - }) - if (done.length > 0) { - // TODO register validation - send(res, 200, {}) - } else { - send(res, 400, errMsg('unrecognized', 'Unknown policy')) + if (done.length > 0) { + done.forEach((policyName) => { + idServer.db + .updateAnd( + 'userPolicies', + { accepted: 1 }, + { field: 'user_id', value: userId }, + { field: 'policy_name', value: policyName } + ) + .then(() => {}) + .catch((e) => { + // istanbul ignore next + idServer.logger.error('Error updating user policies', e) + // istanbul ignore next + send(res, 500, errMsg('unknown')) + }) + }) + send(res, 200, {}) + } else { + send(res, 400, errMsg('unrecognized', 'Unknown policy')) + } } - } - ) - }) - }) + ) + }) + }, + false + ) // send(res, 200, {}) } } diff --git a/packages/matrix-identity-server/src/terms/index.ts b/packages/matrix-identity-server/src/terms/index.ts index a05ea2b9..0b2a3e06 100644 --- a/packages/matrix-identity-server/src/terms/index.ts +++ b/packages/matrix-identity-server/src/terms/index.ts @@ -1,6 +1,6 @@ import { type TwakeLogger } from '@twake/logger' import { type Config } from '../types' -import { send, type expressAppHandler } from '../utils' +import { send, type expressAppHandler } from '@twake/utils' import computePolicy from './_computePolicies' export interface Policy { diff --git a/packages/matrix-identity-server/src/types.ts b/packages/matrix-identity-server/src/types.ts index bbd3fde3..491d97d6 100644 --- a/packages/matrix-identity-server/src/types.ts +++ b/packages/matrix-identity-server/src/types.ts @@ -18,6 +18,7 @@ export interface Config { database_vacuum_delay: number federated_identity_services?: string[] | null hashes_rate_limit?: number + invitation_server_name?: string is_federated_identity_service: boolean key_delay: number keys_depth: number diff --git a/packages/matrix-identity-server/src/userdb/index.test.ts b/packages/matrix-identity-server/src/userdb/index.test.ts index d16db880..990f2346 100644 --- a/packages/matrix-identity-server/src/userdb/index.test.ts +++ b/packages/matrix-identity-server/src/userdb/index.test.ts @@ -15,19 +15,23 @@ describe('UserDB', () => { beforeAll((done) => { const db = new sqlite3.Database(dbName) - db.run('CREATE TABLE users(uid varchar(64) primary key)', (err) => { - if (err != null) { - done(err) - } else { - db.run("INSERT INTO users values('dwho')", (err) => { - if (err != null) { - done(err) - } else { - done() - } - }) + db.run( + 'CREATE TABLE IF NOT EXISTS users(uid varchar(64) primary key)', + (err) => { + if (err != null) { + console.log('BIZARRE', err) + done(err) + } else { + db.run("INSERT INTO users values('dwho')", (err) => { + if (err != null) { + done(err) + } else { + done() + } + }) + } } - }) + ) }) afterEach(() => { diff --git a/packages/matrix-identity-server/src/userdb/sql/pg.ts b/packages/matrix-identity-server/src/userdb/sql/pg.ts index b72a928a..2e5d17bd 100644 --- a/packages/matrix-identity-server/src/userdb/sql/pg.ts +++ b/packages/matrix-identity-server/src/userdb/sql/pg.ts @@ -1,11 +1,10 @@ import { type TwakeLogger } from '@twake/logger' import { type ClientConfig } from 'pg' -import { type UserDBBackend } from '../' -import { type Collections } from '../../db' +import { type Collections, type UserDBBackend } from '..' import Pg from '../../db/sql/pg' import { type Config } from '../../types' -class UserDBPg extends Pg implements UserDBBackend { +class UserDBPg extends Pg implements UserDBBackend { // eslint-disable-next-line @typescript-eslint/promise-function-async createDatabases( conf: Config, diff --git a/packages/matrix-identity-server/src/userdb/sql/sqlite.ts b/packages/matrix-identity-server/src/userdb/sql/sqlite.ts index e123da01..10ea02a1 100644 --- a/packages/matrix-identity-server/src/userdb/sql/sqlite.ts +++ b/packages/matrix-identity-server/src/userdb/sql/sqlite.ts @@ -1,9 +1,8 @@ -import { type Collections } from '../../db' -import { type UserDBBackend } from '..' -import { type Config } from '../../types' +import { type Collections, type UserDBBackend } from '..' import SQLite from '../../db/sql/sqlite' +import { type Config } from '../../types' -class UserDBSQLite extends SQLite implements UserDBBackend { +class UserDBSQLite extends SQLite implements UserDBBackend { // eslint-disable-next-line @typescript-eslint/promise-function-async createDatabases( conf: Config, @@ -27,6 +26,9 @@ class UserDBSQLite extends SQLite implements UserDBBackend { if (db == null) { reject(new Error('Database not created')) } + db.run( + 'CREATE TABLE IF NOT EXISTS users (uid varchar(255), mobile text, mail test)' + ) resolve() }) .catch((e) => { diff --git a/packages/matrix-identity-server/src/utils.ts b/packages/matrix-identity-server/src/utils.ts index d66dadf1..96bba99f 100644 --- a/packages/matrix-identity-server/src/utils.ts +++ b/packages/matrix-identity-server/src/utils.ts @@ -1,83 +1,80 @@ import { type TwakeLogger } from '@twake/logger' -import { type NextFunction, type Request, type Response } from 'express' +import { type Request, type Response } from 'express' import type http from 'http' -import querystring from 'querystring' import { type tokenContent } from './account/register' import type IdentityServerDb from './db' -import { errMsg } from './utils/errors' - -export const hostnameRe = - /^((([a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*([a-zA-Z][-a-zA-Z0-9]*[a-zA-Z0-9]|[a-zA-Z])(:(\d+))?$/ - -export type expressAppHandler = ( - req: Request | http.IncomingMessage, - res: Response | http.ServerResponse, - next?: NextFunction -) => void - -export const send = ( - res: Response | http.ServerResponse, - status: number, - body: string | object -): void => { - /* istanbul ignore next */ - const content = typeof body === 'string' ? body : JSON.stringify(body) - res.writeHead(status, { - 'Content-Type': 'application/json; charset=utf-8', - 'Content-Length': Buffer.byteLength(content, 'utf-8'), - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': - 'Origin, X-Requested-With, Content-Type, Accept, Authorization' - }) - res.write(content) - res.end() -} +import { errMsg, getAccessToken, send } from '@twake/utils' export type AuthenticationFunction = ( req: Request | http.IncomingMessage, res: Response | http.ServerResponse, - callback: (data: tokenContent, id: string | null) => void + callback: (data: tokenContent, id: string | null) => void, + requiresTerms?: boolean ) => void -export const Authenticate = ( - db: IdentityServerDb, +export const Authenticate = ( + db: IdentityServerDb, logger: TwakeLogger ): AuthenticationFunction => { - const tokenRe = /^Bearer (\S+)$/ - return (req, res, callback) => { - let token: string | null = null - if (req.headers.authorization != null) { - const re = req.headers.authorization.match(tokenRe) - if (re != null) { - token = re[1] - } - // @ts-expect-error req.query exists - } else if (req.query != null) { - // @ts-expect-error req.query.access_token may be null - token = req.query.access_token - } + return (req, res, callback, requiresTerms = true) => { + const token = getAccessToken(req) if (token != null) { db.get('accessTokens', ['data'], { id: token }) .then((rows) => { // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (!rows || rows.length === 0) { logger.error( - `${req.socket.remoteAddress as string} sent an inexistent token ${ - token as string - }` + `${ + req.socket.remoteAddress as string + } sent an inexistent token ${token}` ) send(res, 401, errMsg('unAuthorized')) } else { - callback(JSON.parse(rows[0].data as string), token) + if (requiresTerms) { + db.get('userPolicies', ['policy_name', 'accepted'], { + user_id: JSON.parse(rows[0].data as string).sub + }) + .then((policies) => { + if (policies.length === 0) { + callback(JSON.parse(rows[0].data as string), token) + // If there are no policies to accept. This assumes that for each policy we add to the config, we update the database and add the corresponding policy with accepted = 0 for all users + return + } + const notAcceptedTerms = policies.find( + (row) => row.accepted === 0 + ) // We assume the terms database contains all policies. If we update the policies we must also update the database and add the corresponding policy with accepted = 0 for all users + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (notAcceptedTerms) { + logger.error( + `Please accept our updated terms of service before continuing.` + ) + send(res, 403, errMsg('termsNotSigned')) + } else { + callback(JSON.parse(rows[0].data as string), token) + } + }) + .catch((e) => { + /* istanbul ignore next */ + logger.error( + 'Error while trying to get the terms from the database', + e + ) + /* istanbul ignore next */ + send(res, 500, errMsg('unknown')) + }) + } else { + callback(JSON.parse(rows[0].data as string), token) + } } }) .catch((e) => { + /* istanbul ignore next */ logger.error( - `${req.socket.remoteAddress as string} sent an invalid token`, + 'Error while trying to get the token from the database', e ) - send(res, 401, errMsg('unAuthorized')) + /* istanbul ignore next */ + send(res, 500, errMsg('unknown')) }) } else { logger.error( @@ -87,93 +84,3 @@ export const Authenticate = ( } } } - -export const jsonContent = ( - req: Request | http.IncomingMessage, - res: Response | http.ServerResponse, - logger: TwakeLogger, - callback: (obj: Record) => void -): void => { - let content = '' - let accept = true - req.on('data', (body: string) => { - content += body - }) - /* istanbul ignore next */ - req.on('error', (err) => { - send(res, 400, errMsg('unknown', err.message)) - accept = false - }) - req.on('end', () => { - let obj - try { - // eslint-disable-next-line @typescript-eslint/prefer-optional-chain - if ( - req.headers['content-type']?.match( - /^application\/x-www-form-urlencoded/ - ) != null - ) { - obj = querystring.parse(content) - } else { - obj = JSON.parse(content) - } - } catch (err) { - logger.error('JSON error', err) - logger.error(`Content was: ${content}`) - send(res, 400, errMsg('unknown', err as string)) - accept = false - } - if (accept) callback(obj) - }) -} - -type validateParametersSchema = Record - -type validateParametersType = ( - res: Response | http.ServerResponse, - desc: validateParametersSchema, - content: Record, - logger: TwakeLogger, - callback: (obj: object) => void -) => void - -export const validateParameters: validateParametersType = ( - res, - desc, - content, - logger, - callback -) => { - const missingParameters: string[] = [] - const additionalParameters: string[] = [] - // Check for required parameters - Object.keys(desc).forEach((key) => { - if (desc[key] && content[key] == null) { - missingParameters.push(key) - } - }) - if (missingParameters.length > 0) { - send( - res, - 400, - errMsg( - 'missingParams', - `Missing parameters ${missingParameters.join(', ')}` - ) - ) - } else { - Object.keys(content).forEach((key) => { - if (desc[key] == null) { - additionalParameters.push(key) - } - }) - if (additionalParameters.length > 0) { - logger.warn('Additional parameters', additionalParameters) - } - callback(content) - } -} - -export const epoch = (): number => { - return Math.floor(Date.now() / 1000) -} diff --git a/packages/matrix-identity-server/src/utils/errors.ts b/packages/matrix-identity-server/src/utils/errors.ts deleted file mode 100644 index 059066ea..00000000 --- a/packages/matrix-identity-server/src/utils/errors.ts +++ /dev/null @@ -1,75 +0,0 @@ -export const errCodes = { - // Not authorizated (not authenticated) - forbidden: 'M_FORBIDDEN', - - // Not authorized - unAuthorized: 'M_UNAUTHORIZED', - - // The resource requested could not be located. - notFound: 'M_NOT_FOUND', - - // The request was missing one or more parameters. - missingParams: 'M_MISSING_PARAMS', - - // The request contained one or more invalid parameters. - invalidParam: 'M_INVALID_PARAM', - - // The session has not been validated. - sessionNotValidated: 'M_SESSION_NOT_VALIDATED', - - // A session could not be located for the given parameters. - noValidSession: 'M_NO_VALID_SESSION', - - // The session has expired and must be renewed. - sessionExpired: 'M_SESSION_EXPIRED', - - // The email address provided was not valid. - invalidEmail: 'M_INVALID_EMAIL', - - // There was an error sending an email. Typically seen when attempting to verify ownership of a given email address. - emailSendError: 'M_EMAIL_SEND_ERROR', - - // The provided third party address was not valid. - invalidAddress: 'M_INVALID_ADDRESS', - - // There was an error sending a notification. Typically seen when attempting to verify ownership of a given third party address. - sendError: 'M_SEND_ERROR', - - // Server requires some policies - termsNotSigned: 'M_TERMS_NOT_SIGNED', - - // The third party identifier is already in use by another user. Typically this error will have an additional mxid property to indicate who owns the third party identifier. - threepidInUse: 'M_THREEPID_IN_USE', - - // An unknown error has occurred. - unknown: 'M_UNKNOWN', - - // Invalid access token - unknownToken: 'Unrecognised access token', - - // The request contained an unrecognised value, such as an unknown token or medium. - // This is also used as the response if a server did not understand the request. This is expected to be returned with a 404 HTTP status code if the endpoint is not implemented or a 405 HTTP status code if the endpoint is implemented, but the incorrect HTTP method is used. - unrecognized: 'M_UNRECOGNIZED' -} as const - -export const defaultMsg = (s: string): string => { - return s - .replace(/^M_/, '') - .split('_') - .map((s) => { - const t = s.toLowerCase() - return t.charAt(0).toUpperCase() + t.slice(1) - }) - .join(' ') -} - -export const errMsg = ( - code: keyof typeof errCodes, - explanation?: string -): object => { - const errCode = errCodes[code] - return { - errcode: errCode, - error: explanation != null ? explanation : defaultMsg(errCode) - } -} diff --git a/packages/matrix-identity-server/src/utils/validateMatrixToken.ts b/packages/matrix-identity-server/src/utils/validateMatrixToken.ts index 85c0e48a..3e7aafa1 100644 --- a/packages/matrix-identity-server/src/utils/validateMatrixToken.ts +++ b/packages/matrix-identity-server/src/utils/validateMatrixToken.ts @@ -1,9 +1,8 @@ /* eslint-disable prefer-promise-reject-errors */ import { type TwakeLogger } from '@twake/logger' +import { isHostnameValid } from '@twake/utils' import { MatrixResolve } from 'matrix-resolve' import fetch from 'node-fetch' -import { hostnameRe } from '../utils' - interface userInfoResponse { sub: string } @@ -16,7 +15,7 @@ const validateMatrixToken = (logger: TwakeLogger) => { // eslint-disable-next-line @typescript-eslint/promise-function-async return (matrixServer: string, accessToken: string): Promise => { /* istanbul ignore if */ - if (!hostnameRe.test(matrixServer)) + if (!isHostnameValid(matrixServer)) return Promise.reject('Bad matrix_server_name') return new Promise((resolve, reject) => { matrixResolve diff --git a/packages/matrix-identity-server/src/validate/email/requestToken.ts b/packages/matrix-identity-server/src/validate/email/requestToken.ts index 294d0459..ee901d8b 100644 --- a/packages/matrix-identity-server/src/validate/email/requestToken.ts +++ b/packages/matrix-identity-server/src/validate/email/requestToken.ts @@ -4,18 +4,19 @@ import { type tokenContent } from '../../account/register' import type MatrixIdentityServer from '../../index' import { type Config } from '../../types' import { + errMsg, + isValidUrl, jsonContent, send, validateParameters, type expressAppHandler -} from '../../utils' -import { errMsg } from '../../utils/errors' +} from '@twake/utils' import Mailer from '../../utils/mailer' interface RequestTokenArgs { client_secret: string email: string - next_link?: string + next_link?: string // Store it in the db if present to redirect to it after validation with submitToken send_attempt: number } @@ -27,8 +28,8 @@ const schema = { } const clientSecretRe = /^[0-9a-zA-Z.=_-]{6,255}$/ - const validEmailRe = /^\w[+.-\w]*\w@\w[.-\w]*\w\.\w{2,6}$/ +const maxAttemps = 1000000000 const preConfigureTemplate = ( template: string, @@ -80,7 +81,63 @@ const mailBody = ( ) } -const RequestToken = (idServer: MatrixIdentityServer): expressAppHandler => { +const fillTableAndSend = ( + idServer: MatrixIdentityServer, + dst: string, + clientSecret: string, + sendAttempt: number, + verificationTemplate: string, + transport: Mailer, + res: any, + nextLink?: string +): void => { + const sid = randomString(64) + idServer.db + .createOneTimeToken( + { + sid, + email: dst, + client_secret: clientSecret + }, + idServer.conf.mail_link_delay, + nextLink + ) + .then((token) => { + void transport.sendMail({ + to: dst, + raw: mailBody(verificationTemplate, dst, token, clientSecret, sid) + }) + idServer.db + .insert('mappings', { + client_secret: clientSecret, + address: dst, + medium: 'email', + valid: 0, + submit_time: 0, + session_id: sid, + send_attempt: sendAttempt + }) + .then(() => { + send(res, 200, { sid }) + }) + .catch((err) => { + // istanbul ignore next + idServer.logger.error('Insertion error', err) + // istanbul ignore next + send(res, 400, errMsg('unknown', err.toString())) + }) + }) + .catch((err) => { + /* istanbul ignore next */ + idServer.logger.error('Token error', err) + /* istanbul ignore next */ + send(res, 400, errMsg('unknown', err.toString())) + }) +} + +const RequestToken = ( + idServer: MatrixIdentityServer +): expressAppHandler => { const transport = new Mailer(idServer.conf) const verificationTemplate = preConfigureTemplate( fs @@ -93,54 +150,80 @@ const RequestToken = (idServer: MatrixIdentityServer): expressAppHandler => { idServer.authenticate(req, res, (idToken: tokenContent) => { jsonContent(req, res, idServer.logger, (obj) => { validateParameters(res, schema, obj, idServer.logger, (obj) => { + const clientSecret = (obj as RequestTokenArgs).client_secret + const sendAttempt = (obj as RequestTokenArgs).send_attempt const dst = (obj as RequestTokenArgs).email - if (!clientSecretRe.test((obj as RequestTokenArgs).client_secret)) { + const nextLink = (obj as RequestTokenArgs).next_link + if (!clientSecretRe.test(clientSecret)) { send(res, 400, errMsg('invalidParam', 'invalid client_secret')) + } else if (!validEmailRe.test(dst)) { + send(res, 400, errMsg('invalidEmail')) + } + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + else if (nextLink && !isValidUrl(nextLink)) { + send(res, 400, errMsg('invalidParam', 'invalid next_link')) + } else if ( + typeof sendAttempt !== 'number' || + sendAttempt > maxAttemps + ) { + send(res, 400, errMsg('invalidParam', 'Invalid send attempt')) } else { - if (!validEmailRe.test(dst)) { - send(res, 400, errMsg('invalidEmail')) - } else { - // IDENTITY SERVICE API V1.6 - 11.2 - // send_attempt: The server will only send an email if the - // send_attempt is a number greater than the most recent one - // which it has seen, scoped to that email + client_secret pair. - // This is to avoid repeatedly sending the same email in the - // case of request retries between the POSTing user and the - // identity server. The client should increment this value if they desire a new email (e.g. a reminder) to be sent. If they do not, the server should respond with success but not resend the email. - - // TODO: check for send_attempt - - const sid = randomString(64) - idServer.db - .createOneTimeToken( - { - sid, - email: dst, - client_secret: (obj as RequestTokenArgs).client_secret - }, - idServer.conf.mail_link_delay - ) - .then((token) => { - void transport.sendMail({ - to: (obj as RequestTokenArgs).email, - raw: mailBody( - verificationTemplate, - dst, - token, - (obj as RequestTokenArgs).client_secret, - sid - ) - }) - // TODO: send mail - send(res, 200, { sid }) - }) - .catch((err) => { - /* istanbul ignore next */ - idServer.logger.error('Token error', err) - /* istanbul ignore next */ - send(res, 400, errMsg('unknown', err)) - }) - } + idServer.db + .get('mappings', ['send_attempt', 'session_id'], { + client_secret: clientSecret, + address: dst + }) + .then((rows) => { + if (rows.length > 0) { + if (sendAttempt === rows[0].send_attempt) { + send(res, 200, { sid: rows[0].session_id }) + } else { + idServer.db + .deleteEqualAnd( + 'mappings', + { field: 'client_secret', value: clientSecret }, + { field: 'session_id', value: rows[0].session_id } + ) + .then(() => { + fillTableAndSend( + // The calls to send are made in this function + idServer, + dst, + clientSecret, + sendAttempt, + verificationTemplate, + transport, + res, + nextLink + ) + }) + .catch((err) => { + // istanbul ignore next + idServer.logger.error('Deletion error', err) + // istanbul ignore next + send(res, 400, errMsg('unknown', err.toString())) + }) + } + } else { + fillTableAndSend( + // The calls to send are made in this function + idServer, + dst, + clientSecret, + sendAttempt, + verificationTemplate, + transport, + res, + nextLink + ) + } + }) + .catch((err) => { + /* istanbul ignore next */ + idServer.logger.error('Send_attempt error', err) + /* istanbul ignore next */ + send(res, 400, errMsg('unknown', err.toString())) + }) } }) }) diff --git a/packages/matrix-identity-server/src/validate/email/submitToken.ts b/packages/matrix-identity-server/src/validate/email/submitToken.ts index a42c3522..4ca0f676 100644 --- a/packages/matrix-identity-server/src/validate/email/submitToken.ts +++ b/packages/matrix-identity-server/src/validate/email/submitToken.ts @@ -1,6 +1,11 @@ import type MatrixIdentityServer from '../..' -import { jsonContent, send, type expressAppHandler } from '../../utils' -import { errMsg } from '../../utils/errors' +import { + epoch, + errMsg, + jsonContent, + send, + type expressAppHandler +} from '@twake/utils' interface parameters { client_secret?: string @@ -8,13 +13,16 @@ interface parameters { sid?: string } -interface mailToken { +interface MailToken { client_secret: string mail: string sid: string + next_link?: string } -const SubmitToken = (idServer: MatrixIdentityServer): expressAppHandler => { +const SubmitToken = ( + idServer: MatrixIdentityServer +): expressAppHandler => { return (req, res) => { const realMethod = (prms: parameters): void => { if ( @@ -26,16 +34,53 @@ const SubmitToken = (idServer: MatrixIdentityServer): expressAppHandler => { .verifyToken(prms.token) .then((data) => { if ( - (data as mailToken).sid === prms.sid && - (data as mailToken).client_secret === prms.client_secret + (data as MailToken).sid === prms.sid && + (data as MailToken).client_secret === prms.client_secret ) { - // TODO REGISTER (data as mailToken).mail idServer.db .deleteToken(prms.token as string) .then(() => { - send(res, 200, { success: true }) + idServer.db + .updateAnd( + 'mappings', + { valid: 1, submit_time: epoch() }, + { field: 'session_id', value: (data as MailToken).sid }, + { + field: 'client_secret', + value: (data as MailToken).client_secret + } + ) + .then(() => { + if ( + req.method === 'GET' && + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + (data as MailToken).next_link + ) { + const redirectUrl = new URL( + // @ts-expect-error : We check that next_link is not null beforehand + (data as Token).next_link + ).toString() + res.writeHead(302, { + Location: redirectUrl + }) + res.end() + return + } + send(res, 200, { success: true }) + }) + .catch((e) => { + // istanbul ignore next + idServer.logger.error('Error while updating token', e) + // istanbul ignore next + send(res, 500, errMsg('unknown', e.toString())) + }) + }) + .catch((e) => { + // istanbul ignore next + idServer.logger.error('Error while deleting token', e) + // istanbul ignore next + send(res, 500, errMsg('unknown', e.toString())) }) - .catch((e) => {}) } else { /* istanbul ignore next */ send(res, 400, errMsg('invalidParam', 'sid or secret mismatch')) diff --git a/packages/matrix-identity-server/src/versions.ts b/packages/matrix-identity-server/src/versions.ts index e515b5af..3d17e680 100644 --- a/packages/matrix-identity-server/src/versions.ts +++ b/packages/matrix-identity-server/src/versions.ts @@ -1,4 +1,4 @@ -import { send, type expressAppHandler } from './utils' +import { send, type expressAppHandler } from '@twake/utils' // TODO: fix supported versions const versions = [ diff --git a/packages/matrix-identity-server/templates/3pidInvitation.tpl b/packages/matrix-identity-server/templates/3pidInvitation.tpl new file mode 100644 index 00000000..e6292d96 --- /dev/null +++ b/packages/matrix-identity-server/templates/3pidInvitation.tpl @@ -0,0 +1,54 @@ +Date: __date__ +From: __from__ +To: __to__ +Message-ID: __messageid__ +Subject: Invitation to join a Matrix room +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="__multipart_boundary__" + +--__multipart_boundary__ +Content-Type: text/plain; charset=UTF-8 +Content-Disposition: inline +Hello, + +You have been invited to join the __room_name__ Matrix room by __inviter_name__. +Please click on the following link to accept the invitation: __link__ + +About Matrix: +Matrix is an open standard for interoperable, decentralised, real-time communication over IP. It can be used to power Instant Messaging, VoIP/WebRTC signalling, Internet of Things communication - or anywhere you need a standard HTTP API for publishing and subscribing to data whilst tracking the conversation history. + +Matrix defines the standard, and provides open source reference implementations of Matrix-compatible Servers, Clients, Client SDKs and Application Services to help you create new communication solutions or extend the capabilities and reach of existing ones. + +--__multipart_boundary__ +Content-Type: text/html; charset=UTF-8 +Content-Disposition: inline + + + + + +Invitation to join a Matrix room + + + +

Hello,

+ +

You have been invited to join the __room_name__ Matrix room by __inviter_name__. Please click on the following link to accept the invitation: __link__

+ +

About Matrix:

+ +

Matrix is an open standard for interoperable, decentralised, real-time communication over IP. It can be used to power Instant Messaging, VoIP/WebRTC signalling, Internet of Things communication - or anywhere you need a standard HTTP API for publishing and subscribing to data whilst tracking the conversation history.

+ +

Matrix defines the standard, and provides open source reference implementations of Matrix-compatible Servers, Clients, Client SDKs and Application Services to help you create new communication solutions or extend the capabilities and reach of existing ones.

+ + + + +--__multipart_boundary__-- diff --git a/packages/matrix-identity-server/templates/mailVerification.tpl b/packages/matrix-identity-server/templates/mailVerification.tpl index 08fbe672..6a5c4740 100644 --- a/packages/matrix-identity-server/templates/mailVerification.tpl +++ b/packages/matrix-identity-server/templates/mailVerification.tpl @@ -15,7 +15,6 @@ We have received a request to use this email address with a matrix.org identity server. If this was you who made this request, you may use the following link to complete the verification of your email address: __link__ -If your client requires a code, the code is __token__ If you aren't aware of making such a request, please disregard this email. About Matrix: Matrix is an open standard for interoperable, decentralised, real-time communication diff --git a/packages/matrix-invite/Dockerfile b/packages/matrix-invite/Dockerfile index 2b02c21c..f5696342 100644 --- a/packages/matrix-invite/Dockerfile +++ b/packages/matrix-invite/Dockerfile @@ -15,6 +15,6 @@ COPY --from=builder /app/package.json . COPY --from=builder /app/build ./build COPY --from=builder /app/node_modules ./node_modules -EXPOSE $PORT +EXPOSE $PORT CMD ["node", "build"] diff --git a/packages/matrix-invite/README.md b/packages/matrix-invite/README.md index c6f411eb..33c66b6b 100644 --- a/packages/matrix-invite/README.md +++ b/packages/matrix-invite/README.md @@ -20,7 +20,7 @@ npm run dev -- --open To create a production version of your app: ```bash -npm run build +npm run build ``` You can preview the production build with `npm run preview`. diff --git a/packages/matrix-invite/src/components/Confirmation.svelte b/packages/matrix-invite/src/components/Confirmation.svelte index 3c499c8b..5a78ca4f 100644 --- a/packages/matrix-invite/src/components/Confirmation.svelte +++ b/packages/matrix-invite/src/components/Confirmation.svelte @@ -3,7 +3,7 @@ export let domain: string; let existingDomains: string[]; - + preferredDomains.subscribe(domains => { existingDomains = domains; }) diff --git a/packages/retry-promise/README.md b/packages/retry-promise/README.md index e16ab820..5ea8b35a 100644 --- a/packages/retry-promise/README.md +++ b/packages/retry-promise/README.md @@ -29,7 +29,7 @@ const allPromises = RetryPromise.all([ fetch(URL_1) .then(val => resolve()) .catch(reject) - }), + }), new RetryPromise((resolve, reject) => { fetch(URL_2) .then(val => resolve()) @@ -42,7 +42,7 @@ const allSettledtPromises = RetryPromise.allSettled([ fetch(URL_1) .then(val => resolve()) .catch(reject) - }), + }), new RetryPromise((resolve, reject) => { fetch(URL_2) .then(val => resolve()) diff --git a/packages/tom-server/jest.config.js b/packages/tom-server/jest.config.js index c0f2dfe7..6f3c9338 100644 --- a/packages/tom-server/jest.config.js +++ b/packages/tom-server/jest.config.js @@ -6,7 +6,7 @@ export default { setupFilesAfterEnv: ['/jest.setup.ts'], moduleNameMapper: { ...jestConfigBase.moduleNameMapper, - "node-fetch": "/../../node_modules/node-fetch-jest", + 'node-fetch': '/../../node_modules/node-fetch-jest' }, clearMocks: true } diff --git a/packages/tom-server/package.json b/packages/tom-server/package.json index 25923d91..2f7f8409 100644 --- a/packages/tom-server/package.json +++ b/packages/tom-server/package.json @@ -37,12 +37,13 @@ "build:example": "rollup -p @rollup/plugin-typescript -e express,@twake/server -m -o example/tom-server.js example/tom-server.ts", "build:lib": "rollup -c", "start": "node example/tom-server.js", - "test": "jest" + "test": "LOG_TRANSPORTS=File LOG_FILE=/dev/null jest" }, "dependencies": { "@opensearch-project/opensearch": "^2.5.0", "@twake/matrix-application-server": "*", "@twake/matrix-identity-server": "*", + "@twake/utils": "*", "lodash": "^4.17.21", "redis": "^4.6.6", "validator": "^13.11.0" diff --git a/packages/tom-server/rollup.config.js b/packages/tom-server/rollup.config.js index a2e64380..9aa2a6ef 100644 --- a/packages/tom-server/rollup.config.js +++ b/packages/tom-server/rollup.config.js @@ -10,5 +10,6 @@ export default config([ '@twake/config-parser', '@twake/matrix-identity-server', '@twake/matrix-application-server', + '@twake/utils', '@twake/logger' ]) diff --git a/packages/tom-server/src/active-contacts-api/controllers/index.ts b/packages/tom-server/src/active-contacts-api/controllers/index.ts new file mode 100644 index 00000000..23661f14 --- /dev/null +++ b/packages/tom-server/src/active-contacts-api/controllers/index.ts @@ -0,0 +1,137 @@ +/* eslint-disable no-useless-return */ +import type { Response, NextFunction } from 'express' +import type { + IActiveContactsService, + IActiveContactsApiController +} from '../types' +import type { TwakeLogger } from '@twake/logger' +import ActiveContactsService from '../services' +import type { AuthRequest, TwakeDB } from '../../types' + +export default class ActiveContactsApiController + implements IActiveContactsApiController +{ + ActiveContactsApiService: IActiveContactsService + + /** + * the active contacts API controller constructor + * + * @param {TwakeDB} db - the twake database instance + * @param {TwakeLogger} logger - the twake logger instance + * @example + * const controller = new ActiveContactsApiController(db, logger); + */ + constructor( + private readonly db: TwakeDB, + private readonly logger: TwakeLogger + ) { + this.ActiveContactsApiService = new ActiveContactsService(db, logger) + } + + /** + * Save active contacts + * + * @param {AuthRequest} req - request object + * @param {Response} res - response object + * @param {NextFunction} next - next function + * @returns {Promise} - promise that resolves when the operation is complete + */ + save = async ( + req: AuthRequest, + res: Response, + next: NextFunction + ): Promise => { + try { + const { contacts } = req.body + const { userId } = req + + if (userId === undefined || contacts === undefined) { + res.status(400).json({ message: 'Bad Request' }) + return + } + + await this.ActiveContactsApiService.save(userId, contacts) + + res.status(201).send() + return + } catch (error) { + this.logger.error('An error occured while saving active contacts', { + error + }) + next(error) + } + } + + /** + * Retrieve active contacts + * + * @param {AuthRequest} req - request object + * @param {Response} res - response object + * @param {NextFunction} next - next function + * @returns {Promise} - promise that resolves when the operation is complete + */ + get = async ( + req: AuthRequest, + res: Response, + next: NextFunction + ): Promise => { + try { + const { userId } = req + + if (userId === undefined) { + throw new Error('Missing data', { + cause: 'userId is missing' + }) + } + + const contacts = await this.ActiveContactsApiService.get(userId) + + if (contacts === null) { + res.status(404).json({ message: 'No active contacts found' }) + return + } + + res.status(200).json({ contacts }) + return + } catch (error) { + this.logger.error('An error occured while retrieving active contacts', { + error + }) + next(error) + } + } + + /** + * Delete saved active contacts + * + * @param {AuthRequest} req - request object + * @param {Response} res - response object + * @param {NextFunction} next - next function + * @returns {Promise} - promise that resolves when the operation is complete + */ + delete = async ( + req: AuthRequest, + res: Response, + next: NextFunction + ): Promise => { + try { + const { userId } = req + + if (userId === undefined) { + throw new Error('Missing data', { + cause: 'userId is missing' + }) + } + + await this.ActiveContactsApiService.delete(userId) + + res.status(200).send() + return + } catch (error) { + this.logger.error('An error occured while deleting active contacts', { + error + }) + next(error) + } + } +} diff --git a/packages/tom-server/src/active-contacts-api/index.ts b/packages/tom-server/src/active-contacts-api/index.ts new file mode 100644 index 00000000..eec21749 --- /dev/null +++ b/packages/tom-server/src/active-contacts-api/index.ts @@ -0,0 +1 @@ +export { default } from './routes' diff --git a/packages/tom-server/src/active-contacts-api/middlewares/index.ts b/packages/tom-server/src/active-contacts-api/middlewares/index.ts new file mode 100644 index 00000000..e05b0c64 --- /dev/null +++ b/packages/tom-server/src/active-contacts-api/middlewares/index.ts @@ -0,0 +1,37 @@ +import type { Response, NextFunction } from 'express' +import type { AuthRequest } from '../../types' +import type { IActiveContactsApiValidationMiddleware } from '../types' + +export default class ActiveContactsApiValidationMiddleWare + implements IActiveContactsApiValidationMiddleware +{ + /** + * Check the creation requirements of the active contacts API + * + * @param {AuthRequest} req - the request object + * @param {Response} res - the response object + * @param {NextFunction} next - the next function + * @returns {void} + * @example + * router.post('/', checkCreationRequirements, create) + */ + checkCreationRequirements = ( + req: AuthRequest, + res: Response, + next: NextFunction + ): void => { + try { + const { contacts } = req.body + + if (contacts === undefined) { + throw new Error('Missing required fields', { + cause: 'userId or contacts is missing' + }) + } + + next() + } catch (error) { + res.status(400).json({ message: 'Bad Request' }) + } + } +} diff --git a/packages/tom-server/src/active-contacts-api/routes/index.ts b/packages/tom-server/src/active-contacts-api/routes/index.ts new file mode 100644 index 00000000..195f8b6e --- /dev/null +++ b/packages/tom-server/src/active-contacts-api/routes/index.ts @@ -0,0 +1,123 @@ +/* eslint-disable @typescript-eslint/no-misused-promises */ +import { + getLogger, + type TwakeLogger, + type Config as LoggerConfig +} from '@twake/logger' +import type { AuthenticationFunction, Config, TwakeDB } from '../../types' +import { Router } from 'express' +import authMiddleware from '../../utils/middlewares/auth.middleware' +import ActiveContactsApiController from '../controllers' +import ActiveContactsApiValidationMiddleWare from '../middlewares' + +export const PATH = '/_twake/v1/activecontacts' + +export default ( + db: TwakeDB, + config: Config, + authenticator: AuthenticationFunction, + defaultLogger?: TwakeLogger +): Router => { + const logger = defaultLogger ?? getLogger(config as unknown as LoggerConfig) + const activeContactsApiController = new ActiveContactsApiController( + db, + logger + ) + const authenticate = authMiddleware(authenticator, logger) + const validationMiddleware = new ActiveContactsApiValidationMiddleWare() + const router = Router() + + /** + * @openapi + * components: + * schemas: + * ActiveContacts: + * type: object + * description: the list of active contacts + * properties: + * contacts: + * type: string + * description: active contacts + * responses: + * NotFound: + * description: no active contacts found + * Unauthorized: + * description: the user is not authorized + * Created: + * description: active contacts saved + * NoContent: + * description: operation successful and no content returned + */ + + /** + * @openapi + * /_twake/v1/activecontacts: + * get: + * tags: + * - Active contacts + * description: Get the list of active contacts + * responses: + * 200: + * description: Active contacts found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ActiveContacts' + * 404: + * description: Active contacts not found + * 401: + * description: user is unauthorized + * 500: + * description: Internal error + */ + router.get(PATH, authenticate, activeContactsApiController.get) + + /** + * @openapi + * /_twake/v1/activecontacts: + * post: + * tags: + * - Active contacts + * description: Create or update the list of active contacts + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ActiveContacts' + * responses: + * 201: + * description: Active contacts saved + * 401: + * description: user is unauthorized + * 400: + * description: Bad request + * 500: + * description: Internal error + */ + router.post( + PATH, + authenticate, + validationMiddleware.checkCreationRequirements, + activeContactsApiController.save + ) + + /** + * @openapi + * /_twake/v1/activecontacts: + * delete: + * tags: + * - Active contacts + * description: Delete the list of active contacts + * responses: + * 200: + * description: Active contacts deleted + * 401: + * description: user is unauthorized + * 500: + * description: Internal error/ + */ + router.delete(PATH, authenticate, activeContactsApiController.delete) + + return router +} diff --git a/packages/tom-server/src/active-contacts-api/services/index.ts b/packages/tom-server/src/active-contacts-api/services/index.ts new file mode 100644 index 00000000..91d09089 --- /dev/null +++ b/packages/tom-server/src/active-contacts-api/services/index.ts @@ -0,0 +1,111 @@ +import type { TwakeLogger } from '@twake/logger' +import type { TwakeDB, twakeDbCollections } from '../../types' +import type { ActiveAcountsData, IActiveContactsService } from '../types' + +class ActiveContactsService implements IActiveContactsService { + /** + * The active contacts service constructor. + * + * @param {TwakeDB} db - The Twake database instance. + * @param {TwakeLogger} logger - The Twake logger instance. + * @example + * const service = new ActiveContactsService(db, logger); + */ + constructor( + private readonly db: TwakeDB, + private readonly logger: TwakeLogger + ) {} + + /** + * Fetches the active contacts for a given user ID. + * + * @param {string} userId - The ID of the user whose active contacts need to be fetched. + * @returns {Promise} - Active contacts or null if no active contacts found. + * @throws {Error} - If there is an error while fetching the active contacts. + */ + public get = async (userId: string): Promise => { + try { + const ActiveContacts = (await this.db.get( + 'activeContacts' as twakeDbCollections, + ['contacts'], + { userId } + )) as unknown as ActiveAcountsData[] + + if (ActiveContacts.length === 0) { + this.logger.warn('No active contacts found') + + return null + } + + return ActiveContacts[0].contacts + } catch (error) { + this.logger.error('Failed to get active contacts', { error }) + throw new Error('Failed to get active contacts', { cause: error }) + } + } + + /** + * Saves the active contacts for a given user ID and target ID. + * + * @param {string} userId - The ID of the user whose active contacts need to be saved. + * @param {string} contacts - The active contacts data to be saved. + * @returns {Promise} + * @throws {Error} - If there is an error while saving the active contacts. + */ + save = async (userId: string, contacts: string): Promise => { + try { + const existing = await this.db.get( + 'activeContacts' as twakeDbCollections, + ['contacts'], + { userId } + ) + + if (existing.length > 0) { + await this.db.update( + 'activeContacts' as twakeDbCollections, + { contacts }, + 'userId', + userId + ) + this.logger.info('active contacts updated successfully') + return + } + + await this.db.insert('activeContacts' as twakeDbCollections, { + userId, + contacts + }) + + this.logger.info('active contacts saved successfully') + } catch (error) { + this.logger.error('Failed to save active contacts', { error }) + throw new Error('Failed to save active contacts', { cause: error }) + } + } + + /** + * Deletes saved active contacts for a given user ID. + * + * @param {string} userId - The ID of the user whose saved active contacts need to be deleted. + * @returns {Promise} + * @throws {Error} - If there is an error while deleting the saved active contacts. + */ + delete = async (userId: string): Promise => { + try { + await this.db.deleteEqual( + 'activeContacts' as twakeDbCollections, + 'userId', + userId + ) + + this.logger.info('active contacts deleted successfully') + } catch (error) { + this.logger.error('Failed to delete saved active contacts', { error }) + throw new Error('Failed to delete saved active contacts', { + cause: error + }) + } + } +} + +export default ActiveContactsService diff --git a/packages/tom-server/src/active-contacts-api/tests/controllers.test.ts b/packages/tom-server/src/active-contacts-api/tests/controllers.test.ts new file mode 100644 index 00000000..4c4e2e94 --- /dev/null +++ b/packages/tom-server/src/active-contacts-api/tests/controllers.test.ts @@ -0,0 +1,139 @@ +/* eslint-disable @typescript-eslint/consistent-type-assertions */ +import bodyParser from 'body-parser' +import express, { type NextFunction, type Response } from 'express' +import supertest from 'supertest' +import type { AuthRequest, Config, TwakeDB } from '../../types' +import router, { PATH } from '../routes' +import type { TwakeLogger } from '@twake/logger' + +const app = express() + +const dbMock = { + get: jest.fn(), + insert: jest.fn(), + update: jest.fn(), + deleteEqual: jest.fn(), + getCount: jest.fn() +} + +const loggerMock = { + info: jest.fn(), + error: jest.fn(), + warn: jest.fn() +} + +const authenticatorMock = jest + .fn() + .mockImplementation((_req, _res, callbackMethod) => { + callbackMethod('test', 'test') + }) + +jest.mock('../middlewares/index.ts', () => { + const passiveMiddlewareMock = ( + _req: AuthRequest, + _res: Response, + next: NextFunction + ): void => { + next() + } + + return function () { + return { + checkCreationRequirements: passiveMiddlewareMock + } + } +}) + +app.use(bodyParser.json()) +app.use(bodyParser.urlencoded({ extended: true })) +app.use( + router( + dbMock as unknown as TwakeDB, + {} as Config, + authenticatorMock, + loggerMock as unknown as TwakeLogger + ) +) + +describe('the active contacts API controller', () => { + describe('active contacts fetch', () => { + it('should try to fetch the saved active contacts', async () => { + dbMock.get.mockResolvedValue([ + { + userId: 'test', + contacts: 'test' + } + ]) + + const response = await supertest(app).get(PATH).send() + + expect(response.status).toBe(200) + expect(response.body).toEqual({ contacts: 'test' }) + }) + + it('should return an error if no active contacts are found', async () => { + dbMock.get.mockResolvedValue([]) + + const response = await supertest(app).get(PATH).send() + + expect(response.status).toBe(404) + expect(response.body).toEqual({ message: 'No active contacts found' }) + }) + + it('should return an error if an error occurs while fetching active contacts', async () => { + dbMock.get.mockRejectedValue(new Error('test')) + + const response = await supertest(app).get(PATH).send() + + expect(response.status).toBe(500) + }) + }) + + describe('active contacts save', () => { + it('should try to save active contacts', async () => { + dbMock.get.mockResolvedValue([]) + dbMock.insert.mockResolvedValue([]) + + const response = await supertest(app) + .post(PATH) + .send({ contacts: 'test' }) + + expect(response.status).toBe(201) + }) + + it('should return an error if an error occurs while saving active contacts', async () => { + dbMock.insert.mockRejectedValue(new Error('test')) + + const response = await supertest(app) + .post(PATH) + .send({ contacts: 'test' }) + + expect(response.status).toBe(500) + }) + + it('should return an error if the parameters are missing', async () => { + const response = await supertest(app).post(PATH).send({}) + + expect(response.status).toBe(400) + expect(response.body).toEqual({ message: 'Bad Request' }) + }) + }) + + describe('active contacts delete', () => { + it('should try to delete active contacts', async () => { + dbMock.deleteEqual.mockResolvedValue([]) + + const response = await supertest(app).delete(PATH).send() + + expect(response.status).toBe(200) + }) + + it('should return an error if an error occurs while deleting active contacts', async () => { + dbMock.deleteEqual.mockRejectedValue(new Error('test')) + + const response = await supertest(app).delete(PATH).send() + + expect(response.status).toBe(500) + }) + }) +}) diff --git a/packages/tom-server/src/active-contacts-api/tests/middlewares.test.ts b/packages/tom-server/src/active-contacts-api/tests/middlewares.test.ts new file mode 100644 index 00000000..5cabfe17 --- /dev/null +++ b/packages/tom-server/src/active-contacts-api/tests/middlewares.test.ts @@ -0,0 +1,52 @@ +import type { AuthRequest } from '../../types' +import type { Response, NextFunction } from 'express' +import ActiveContactsMiddleware from '../middlewares' + +describe('The active contacts API middleware', () => { + let mockRequest: Partial + let mockResponse: Partial + const nextFunction: NextFunction = jest.fn() + + const activeContactsMiddleware = new ActiveContactsMiddleware() + + beforeEach(() => { + mockRequest = { + body: {}, + query: {}, + userId: 'test' + } + mockResponse = { + json: jest.fn(), + status: jest.fn().mockReturnThis() + } + }) + + describe('the checkCreationRequirements middleware', () => { + it('should return a 400 error if data is missing', async () => { + mockRequest.body = {} + + activeContactsMiddleware.checkCreationRequirements( + mockRequest as AuthRequest, + mockResponse as Response, + nextFunction + ) + + expect(mockResponse.status).toHaveBeenCalledWith(400) + expect(mockResponse.json).toHaveBeenCalledWith({ + message: 'Bad Request' + }) + }) + + it('should call the next handler if the requirements are met', async () => { + mockRequest.body = { contacts: 'test' } + + activeContactsMiddleware.checkCreationRequirements( + mockRequest as AuthRequest, + mockResponse as Response, + nextFunction + ) + + expect(nextFunction).toHaveBeenCalledWith() + }) + }) +}) diff --git a/packages/tom-server/src/active-contacts-api/tests/routes.test.ts b/packages/tom-server/src/active-contacts-api/tests/routes.test.ts new file mode 100644 index 00000000..fb4f8b9e --- /dev/null +++ b/packages/tom-server/src/active-contacts-api/tests/routes.test.ts @@ -0,0 +1,147 @@ +import express, { type Response, type NextFunction } from 'express' +import bodyParser from 'body-parser' +import type { AuthRequest, Config } from '../../types' +import IdServer from '../../identity-server' +import type { ConfigDescription } from '@twake/config-parser' +import type { TwakeLogger } from '@twake/logger' +import { IdentityServerDb, type MatrixDB } from '@twake/matrix-identity-server' +import router, { PATH } from '../routes' +import errorMiddleware from '../../utils/middlewares/error.middleware' +import JEST_PROCESS_ROOT_PATH from '../../../jest.globals' +import fs from 'fs' +import path from 'path' +import supertest from 'supertest' + +const mockLogger: Partial = { + debug: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + close: jest.fn() +} + +jest + .spyOn(IdentityServerDb.prototype, 'get') + .mockResolvedValue([{ data: '"test"' }]) + +const idServer = new IdServer( + { + get: jest.fn() + } as unknown as MatrixDB, + {} as unknown as Config, + { + database_engine: 'sqlite', + database_host: 'test.db', + rate_limiting_window: 5000, + rate_limiting_nb_requests: 10, + template_dir: './templates', + userdb_host: './tokens.db' + } as unknown as ConfigDescription, + mockLogger as TwakeLogger +) + +const app = express() +const middlewareSpy = jest.fn().mockImplementation((_req, _res, next) => { + next() +}) + +jest.mock('../middlewares', () => { + return function () { + return { + checkCreationRequirements: middlewareSpy + } + } +}) + +jest.mock('../controllers', () => { + const passiveController = ( + _req: AuthRequest, + res: Response, + _next: NextFunction + ): void => { + res.status(200).json({ message: 'test' }) + } + + return function () { + return { + get: passiveController, + save: passiveController, + delete: passiveController + } + } +}) + +app.use(bodyParser.json()) +app.use(bodyParser.urlencoded({ extended: true })) + +describe('The active contacts API router', () => { + beforeAll((done) => { + idServer.ready + .then(() => { + app.use( + router( + idServer.db, + idServer.conf, + idServer.authenticate, + idServer.logger + ) + ) + + app.use(errorMiddleware(idServer.logger)) + done() + }) + .catch((e) => { + done(e) + }) + }) + + afterAll(() => { + idServer.cleanJobs() + + const pathFilesToDelete = [ + path.join(JEST_PROCESS_ROOT_PATH, 'test.db'), + path.join(JEST_PROCESS_ROOT_PATH, 'tokens.db') + ] + + pathFilesToDelete.forEach((path) => { + if (fs.existsSync(path)) fs.unlinkSync(path) + }) + }) + + it('should reject if rate limit is exceeded', async () => { + let response + + for (let i = 0; i < 11; i++) { + response = await supertest(app) + .get(PATH) + .set('Authorization', 'Bearer test') + } + + expect((response as unknown as Response).status).toEqual(429) + await new Promise((resolve) => setTimeout(resolve, 6000)) + }) + + it('should not call the validation middleware if the Bearer token is not set', async () => { + const response = await supertest(app).post(PATH).send({ contacts: 'test' }) + + expect(response.status).toEqual(401) + expect(middlewareSpy).not.toHaveBeenCalled() + }) + + it('should call the validation middleware if the Bearer token is set', async () => { + await supertest(app) + .post(PATH) + .set('Authorization', 'Bearer test') + .send({ contacts: 'test' }) + + expect(middlewareSpy).toHaveBeenCalled() + }) + + it('should call the validation middleware if the access_token is set in the query', async () => { + await supertest(app) + .post(PATH) + .query({ access_token: 'test' }) + .send({ contact: 'test' }) + + expect(middlewareSpy).toHaveBeenCalled() + }) +}) diff --git a/packages/tom-server/src/active-contacts-api/tests/service.test.ts b/packages/tom-server/src/active-contacts-api/tests/service.test.ts new file mode 100644 index 00000000..a6eaf265 --- /dev/null +++ b/packages/tom-server/src/active-contacts-api/tests/service.test.ts @@ -0,0 +1,114 @@ +import type { TwakeLogger } from '@twake/logger' +import type { TwakeDB } from '../../types' +import ActiveContactsService from '../services' + +describe('The active contacts service', () => { + const dbMock = { + get: jest.fn(), + insert: jest.fn(), + deleteEqual: jest.fn(), + update: jest.fn() + } + + const loggerMock = { + info: jest.fn(), + error: jest.fn(), + warn: jest.fn() + } + + const activeContactsService = new ActiveContactsService( + dbMock as unknown as TwakeDB, + loggerMock as unknown as TwakeLogger + ) + + it('should save active contacts for a user', async () => { + dbMock.insert.mockResolvedValue(undefined) + dbMock.get.mockResolvedValue([]) + + await expect( + activeContactsService.save('test', 'contact') + ).resolves.not.toThrow() + + expect(dbMock.insert).toHaveBeenCalledWith('activeContacts', { + userId: 'test', + contacts: 'contact' + }) + }) + + it('should update active contacts for a user if there are existing ones', async () => { + dbMock.get.mockResolvedValue([{ userId: 'test', contacts: 'test contact' }]) + dbMock.update.mockResolvedValue(undefined) + + await expect( + activeContactsService.save('test', 'contact') + ).resolves.not.toThrow() + + expect(dbMock.update).toHaveBeenCalledWith( + 'activeContacts', + { contacts: 'contact' }, + 'userId', + 'test' + ) + }) + + it('should fetch active contacts for a user', async () => { + dbMock.get.mockResolvedValue([{ userId: 'test', contacts: 'contact' }]) + + await expect(activeContactsService.get('test')).resolves.toEqual('contact') + + expect(dbMock.get).toHaveBeenCalledWith('activeContacts', ['contacts'], { + userId: 'test' + }) + }) + + it('should attempt to delete active contacts for a user', async () => { + dbMock.deleteEqual.mockResolvedValue(undefined) + + await expect(activeContactsService.delete('test')).resolves.not.toThrow() + + expect(dbMock.deleteEqual).toHaveBeenCalledWith( + 'activeContacts', + 'userId', + 'test' + ) + }) + + it('should return null if no active contacts found for user', async () => { + dbMock.get.mockResolvedValue([]) + + await expect(activeContactsService.get('test')).resolves.toBeNull() + expect(loggerMock.warn).toHaveBeenCalledWith('No active contacts found') + }) + + it('should log and throw an error if there is an error fetching active contacts', async () => { + dbMock.get.mockRejectedValue(new Error('test')) + + await expect(activeContactsService.get('test')).rejects.toThrow() + expect(loggerMock.error).toHaveBeenCalledWith( + 'Failed to get active contacts', + expect.anything() + ) + }) + + it('should log and throw an error if something wrong happens while saving', async () => { + dbMock.insert.mockRejectedValue(new Error('test')) + + await expect( + activeContactsService.save('test', 'contact') + ).rejects.toThrow() + expect(loggerMock.error).toHaveBeenCalledWith( + 'Failed to save active contacts', + expect.anything() + ) + }) + + it('should log and throw an error if something wrong happens while deleting', async () => { + dbMock.deleteEqual.mockRejectedValue(new Error('test')) + + await expect(activeContactsService.delete('test')).rejects.toThrow() + expect(loggerMock.error).toHaveBeenCalledWith( + 'Failed to delete saved active contacts', + expect.anything() + ) + }) +}) diff --git a/packages/tom-server/src/active-contacts-api/types.ts b/packages/tom-server/src/active-contacts-api/types.ts new file mode 100644 index 00000000..a7a19775 --- /dev/null +++ b/packages/tom-server/src/active-contacts-api/types.ts @@ -0,0 +1,26 @@ +import type { NextFunction, Response } from 'express' +import type { AuthRequest } from '../types' + +export interface IActiveContactsApiController { + get: (req: AuthRequest, res: Response, next: NextFunction) => Promise + save: (req: AuthRequest, res: Response, next: NextFunction) => Promise + delete: (req: AuthRequest, res: Response, next: NextFunction) => Promise +} + +export interface IActiveContactsApiValidationMiddleware { + checkCreationRequirements: ( + req: AuthRequest, + res: Response, + next: NextFunction + ) => void +} + +export interface IActiveContactsService { + get: (userId: string) => Promise + save: (userId: string, targetId: string) => Promise + delete: (userId: string) => Promise +} + +export interface ActiveAcountsData { + contacts: string +} diff --git a/packages/tom-server/src/application-server/__testData__/build-userdb.ts b/packages/tom-server/src/application-server/__testData__/build-userdb.ts index 9261216f..64858e84 100644 --- a/packages/tom-server/src/application-server/__testData__/build-userdb.ts +++ b/packages/tom-server/src/application-server/__testData__/build-userdb.ts @@ -32,27 +32,29 @@ export const buildUserDB = (conf: Partial): Promise => { export const deleteUserDB = (conf: Partial): Promise => { return new Promise((resolve, reject) => { const matrixDb = new sqlite3.Database(conf.matrix_database_host) - matrixDb.run( - 'DROP TABLE users', - (err) => { - if (err != null) { - reject(err) - } else { - resolve() - } + matrixDb.run('DROP TABLE users', (err) => { + if (err != null) { + reject(err) + } else { + resolve() } - ) + }) }) } // eslint-disable-next-line @typescript-eslint/promise-function-async -export const addUser = (conf: Partial, usersIds: string[]): Promise => { +export const addUser = ( + conf: Partial, + usersIds: string[] +): Promise => { return new Promise((resolve, reject) => { const matrixDb = new sqlite3.Database(conf.matrix_database_host) usersIds.forEach((userId) => { matrixDb.run( - // columns headers: name|password_hash|creation_ts(seconds)|admin|upgrade_ts|is_guest|appservice_id|consent_version|consent_server_notice_sent|user_type|deactivated|shadow_banned|consent_ts|approved - `INSERT INTO users VALUES('${userId}', '', ${Math.floor(Date.now() / 1000)}, 0, '', 0, '', '', '', '', 0, 0, '', 1)`, + // columns headers: name|password_hash|creation_ts(seconds)|admin|upgrade_ts|is_guest|appservice_id|consent_version|consent_server_notice_sent|user_type|deactivated|shadow_banned|consent_ts|approved + `INSERT INTO users VALUES('${userId}', '', ${Math.floor( + Date.now() / 1000 + )}, 0, '', 0, '', '', '', '', 0, 0, '', 1)`, (err) => { if (err != null) { reject(err) diff --git a/packages/tom-server/src/application-server/__testData__/docker-compose.yml b/packages/tom-server/src/application-server/__testData__/docker-compose.yml index 378cd76a..3ae93417 100644 --- a/packages/tom-server/src/application-server/__testData__/docker-compose.yml +++ b/packages/tom-server/src/application-server/__testData__/docker-compose.yml @@ -9,17 +9,17 @@ services: - ./nginx/ssl/auth.example.com.crt:/etc/ssl/certs/ca-certificates.crt depends_on: - auth - environment: + environment: - UID=${MYUID} - VIRTUAL_PORT=8008 - VIRTUAL_HOST=matrix.example.com healthcheck: - test: ["CMD", "curl", "-fSs", "http://localhost:8008/health"] + test: ['CMD', 'curl', '-fSs', 'http://localhost:8008/health'] interval: 10s timeout: 10s retries: 3 extra_hosts: - - "host.docker.internal:host-gateway" + - 'host.docker.internal:host-gateway' auth: image: yadd/lemonldap-ng-portal:2.18.2-9 @@ -33,7 +33,7 @@ services: - PORTAL=https://auth.example.com - VIRTUAL_HOST=auth.example.com extra_hosts: - - "host.docker.internal:host-gateway" + - 'host.docker.internal:host-gateway' nginx-proxy: image: nginxproxy/nginx-proxy @@ -43,4 +43,4 @@ services: - HTTPS_PORT=444 volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - - ./nginx/ssl:/etc/nginx/certs \ No newline at end of file + - ./nginx/ssl:/etc/nginx/certs diff --git a/packages/tom-server/src/application-server/__testData__/ldap/Dockerfile b/packages/tom-server/src/application-server/__testData__/ldap/Dockerfile index 83adb618..86d55944 100644 --- a/packages/tom-server/src/application-server/__testData__/ldap/Dockerfile +++ b/packages/tom-server/src/application-server/__testData__/ldap/Dockerfile @@ -3,7 +3,7 @@ LABEL maintainer Linagora ENV DEBIAN_FRONTEND=noninteractive -# Update system and install dependencies +# Update system and install dependencies RUN apt-get update && \ apt-get install -y --no-install-recommends \ apt-transport-https \ @@ -16,7 +16,7 @@ RUN apt-get update && \ apt-get update && \ apt-get install -y openldap-ltb openldap-ltb-contrib-overlays openldap-ltb-mdb-utils ldap-utils && \ apt-get clean && \ - rm -rf /var/lib/apt/lists/* + rm -rf /var/lib/apt/lists/* # Copy configuration files COPY ./ldif/config-20230322180123.ldif /var/backups/openldap/ diff --git a/packages/tom-server/src/application-server/__testData__/llng/lmConf-1.json b/packages/tom-server/src/application-server/__testData__/llng/lmConf-1.json index 3248f5ac..76805a57 100644 --- a/packages/tom-server/src/application-server/__testData__/llng/lmConf-1.json +++ b/packages/tom-server/src/application-server/__testData__/llng/lmConf-1.json @@ -1,457 +1,456 @@ { - "ADPwdExpireWarning": 0, - "ADPwdMaxAge": 0, - "SMTPServer": "", - "SMTPTLS": "", - "SSLAuthnLevel": 5, - "SSLIssuerVar": "SSL_CLIENT_I_DN", - "SSLVar": "SSL_CLIENT_S_DN_Email", - "SSLVarIf": {}, - "activeTimer": 1, - "apacheAuthnLevel": 3, - "applicationList": {}, - "authChoiceParam": "lmAuth", - "authentication": "LDAP", - "available2F": "UTOTP,TOTP,U2F,REST,Mail2F,Ext2F,WebAuthn,Yubikey,Radius,Password", - "available2FSelfRegistration": "Password,TOTP,U2F,WebAuthn,Yubikey", - "bruteForceProtectionLockTimes": "15, 30, 60, 300, 600", - "bruteForceProtectionMaxAge": 300, - "bruteForceProtectionMaxFailed": 3, - "bruteForceProtectionMaxLockTime": 900, - "bruteForceProtectionTempo": 30, - "captcha_mail_enabled": 1, - "captcha_register_enabled": 1, - "captcha_size": 6, - "casAccessControlPolicy": "none", - "casAuthnLevel": 1, - "casTicketExpiration": 0, - "certificateResetByMailCeaAttribute": "description", - "certificateResetByMailCertificateAttribute": "userCertificate;binary", - "certificateResetByMailURL": "https://auth.example.com/certificateReset", - "certificateResetByMailValidityDelay": 0, - "cfgAuthor": "The LemonLDAP::NG team", - "cfgDate": "1627287638", - "cfgNum": "1", - "cfgVersion": "2.0.16", - "checkDevOpsCheckSessionAttributes": 1, - "checkDevOpsDisplayNormalizedHeaders": 1, - "checkDevOpsDownload": 1, - "checkHIBPRequired": 1, - "checkHIBPURL": "https://api.pwnedpasswords.com/range/", - "checkTime": 600, - "checkUserDisplayComputedSession": 1, - "checkUserDisplayEmptyHeaders": 0, - "checkUserDisplayEmptyValues": 0, - "checkUserDisplayHiddenAttributes": 0, - "checkUserDisplayHistory": 0, - "checkUserDisplayNormalizedHeaders": 0, - "checkUserDisplayPersistentInfo": 0, - "checkUserHiddenAttributes": "_loginHistory, _session_id, hGroups", - "checkUserIdRule": 1, - "checkXSS": 1, - "confirmFormMethod": "post", - "contextSwitchingIdRule": 1, - "contextSwitchingPrefix": "switching", - "contextSwitchingRule": 0, - "contextSwitchingStopWithLogout": 1, - "cookieName": "lemonldap", - "corsAllow_Credentials": "true", - "corsAllow_Headers": "*", - "corsAllow_Methods": "POST,GET", - "corsAllow_Origin": "*", - "corsEnabled": 1, - "corsExpose_Headers": "*", - "corsMax_Age": "86400", - "crowdsecAction": "reject", - "cspConnect": "'self'", - "cspDefault": "'self'", - "cspFont": "'self'", - "cspFormAction": "*", - "cspFrameAncestors": "", - "cspImg": "'self' data:", - "cspScript": "'self'", - "cspStyle": "'self'", - "dbiAuthnLevel": 2, - "dbiExportedVars": {}, - "decryptValueRule": 0, - "demoExportedVars": { - "cn": "cn", - "mail": "mail", - "uid": "uid" - }, - "displaySessionId": 1, - "domain": "example.com", - "exportedHeaders": {}, - "exportedVars": {}, - "ext2fActivation": 0, - "ext2fCodeActivation": "\\d{6}", - "facebookAuthnLevel": 1, - "facebookExportedVars": {}, - "facebookUserField": "id", - "failedLoginNumber": 5, - "findUserControl": "^[*\\w]+$", - "findUserWildcard": "*", - "formTimeout": 120, - "githubAuthnLevel": 1, - "githubScope": "user:email", - "githubUserField": "login", - "globalLogoutRule": 0, - "globalLogoutTimer": 1, - "globalStorage": "Apache::Session::File", - "globalStorageOptions": { - "Directory": "/var/lib/lemonldap-ng/sessions", - "LockDirectory": "/var/lib/lemonldap-ng/sessions/lock", - "generateModule": "Lemonldap::NG::Common::Apache::Session::Generate::SHA256" - }, - "gpgAuthnLevel": 5, - "gpgDb": "", - "grantSessionRules": {}, - "groups": {}, - "handlerInternalCache": 15, - "handlerServiceTokenTTL": 30, - "hiddenAttributes": "_password, _2fDevices", - "httpOnly": 1, - "https": -1, - "impersonationHiddenAttributes": "_2fDevices, _loginHistory", - "impersonationIdRule": 1, - "impersonationMergeSSOgroups": 0, - "impersonationPrefix": "real_", - "impersonationRule": 0, - "impersonationSkipEmptyValues": 1, - "infoFormMethod": "get", - "issuerDBCASPath": "^/cas/", - "issuerDBCASRule": 1, - "issuerDBGetParameters": {}, - "issuerDBGetPath": "^/get/", - "issuerDBGetRule": 1, - "issuerDBOpenIDConnectActivation": 1, - "issuerDBOpenIDConnectPath": "^/oauth2/", - "issuerDBOpenIDConnectRule": 1, - "issuerDBOpenIDPath": "^/openidserver/", - "issuerDBOpenIDRule": 1, - "issuerDBSAMLPath": "^/saml/", - "issuerDBSAMLRule": 1, - "issuersTimeout": 120, - "jsRedirect": 0, - "key": "^vmTGvh{+]5!ToB?", - "krbAuthnLevel": 3, - "krbRemoveDomain": 1, - "ldapServer": "host.docker.internal:21389", - "ldapAuthnLevel": 2, - "ldapBase": "dc=example,dc=com", - "ldapExportedVars": { - "cn": "cn", - "mail": "mail", - "uid": "uid" - }, - "ldapGroupAttributeName": "member", - "ldapGroupAttributeNameGroup": "dn", - "ldapGroupAttributeNameSearch": "cn", - "ldapGroupAttributeNameUser": "dn", - "ldapGroupObjectClass": "groupOfNames", - "ldapIOTimeout": 10, - "ldapPasswordResetAttribute": "pwdReset", - "ldapPasswordResetAttributeValue": "TRUE", - "ldapPwdEnc": "utf-8", - "ldapSearchDeref": "find", - "ldapTimeout": 10, - "ldapUsePasswordResetAttribute": 1, - "ldapVerify": "require", - "ldapVersion": 3, - "linkedInAuthnLevel": 1, - "linkedInFields": "id,first-name,last-name,email-address", - "linkedInScope": "r_liteprofile r_emailaddress", - "linkedInUserField": "emailAddress", - "localSessionStorage": "Cache::FileCache", - "localSessionStorageOptions": { - "cache_depth": 3, - "cache_root": "/var/lib/lemonldap-ng/cache", - "default_expires_in": 600, - "directory_umask": "007", - "namespace": "lemonldap-ng-sessions" - }, - "locationDetectGeoIpLanguages": "en, fr", - "locationRules": { - "auth.example.com": { - "(?#checkUser)^/checkuser": "inGroup(\"timelords\")", - "(?#errors)^/lmerror/": "accept", - "default": "accept" - } - }, - "loginHistoryEnabled": 1, - "logoutServices": {}, - "macros": { - "UA": "$ENV{HTTP_USER_AGENT}", - "_whatToTrace": "$_auth eq 'SAML' ? lc($_user.'@'.$_idpConfKey) : $_auth eq 'OpenIDConnect' ? lc($_user.'@'.$_oidc_OP) : lc($_user)" - }, - "mail2fActivation": 0, - "mail2fCodeRegex": "\\d{6}", - "mailCharset": "utf-8", - "mailFrom": "noreply@example.com", - "mailSessionKey": "mail", - "mailTimeout": 0, - "mailUrl": "https://auth.example.com/resetpwd", - "managerDn": "", - "managerPassword": "", - "max2FDevices": 10, - "max2FDevicesNameLength": 20, - "multiValuesSeparator": "; ", - "mySessionAuthorizedRWKeys": [ - "_appsListOrder", - "_oidcConnectedRP", - "_oidcConsents" - ], - "newLocationWarningLocationAttribute": "ipAddr", - "newLocationWarningLocationDisplayAttribute": "", - "newLocationWarningMaxValues": "0", - "notification": 0, - "notificationDefaultCond": "", - "notificationServerPOST": 1, - "notificationServerSentAttributes": "uid reference date title subtitle text check", - "notificationStorage": "File", - "notificationStorageOptions": { - "dirName": "/var/lib/lemonldap-ng/notifications" - }, - "notificationWildcard": "allusers", - "notificationsMaxRetrieve": 3, - "notifyDeleted": 1, - "nullAuthnLevel": 0, - "oidcAuthnLevel": 1, - "oidcOPMetaDataExportedVars": {}, - "oidcOPMetaDataJSON": {}, - "oidcOPMetaDataJWKS": {}, - "oidcOPMetaDataOptions": {}, - "oidcRPCallbackGetParam": "openidconnectcallback", - "oidcRPMetaDataExportedVars": { - "matrix": { - "email": "mail", - "family_name": "cn", - "given_name": "cn", - "name": "cn", - "nickname": "uid", - "preferred_username": "uid" - } - }, - "oidcRPMetaDataMacros": null, - "oidcRPMetaDataOptions": { - "matrix": { - "oidcRPMetaDataOptionsAccessTokenClaims": 0, - "oidcRPMetaDataOptionsAccessTokenJWT": 0, - "oidcRPMetaDataOptionsAccessTokenSignAlg": "RS256", - "oidcRPMetaDataOptionsAllowClientCredentialsGrant": 0, - "oidcRPMetaDataOptionsAllowOffline": 0, - "oidcRPMetaDataOptionsAllowPasswordGrant": 0, - "oidcRPMetaDataOptionsBypassConsent": 1, - "oidcRPMetaDataOptionsClientID": "matrix1", - "oidcRPMetaDataOptionsClientSecret": "matrix1*", - "oidcRPMetaDataOptionsIDTokenForceClaims": 0, - "oidcRPMetaDataOptionsIDTokenSignAlg": "RS256", - "oidcRPMetaDataOptionsLogoutBypassConfirm": 0, - "oidcRPMetaDataOptionsLogoutSessionRequired": 1, - "oidcRPMetaDataOptionsLogoutType": "back", - "oidcRPMetaDataOptionsPublic": 0, - "oidcRPMetaDataOptionsRedirectUris": "https://matrix.example.com:444/_synapse/client/oidc/callback", - "oidcRPMetaDataOptionsRefreshToken": 0, - "oidcRPMetaDataOptionsRequirePKCE": 0 - } - }, - "oidcRPMetaDataOptionsExtraClaims": null, - "oidcRPMetaDataScopeRules": null, - "oidcRPStateTimeout": 600, - "oidcServiceAccessTokenExpiration": 3600, - "oidcServiceAllowAuthorizationCodeFlow": 1, - "oidcServiceAllowImplicitFlow": 0, - "oidcServiceAuthorizationCodeExpiration": 60, - "oidcServiceDynamicRegistrationExportedVars": {}, - "oidcServiceDynamicRegistrationExtraClaims": {}, - "oidcServiceIDTokenExpiration": 3600, - "oidcServiceIgnoreScopeForClaims": 1, - "oidcServiceKeyIdSig": "oMGHInscAW3Nsa0FcnCnDA", - "oidcServiceMetaDataAuthnContext": { - "loa-1": 1, - "loa-2": 2, - "loa-3": 3, - "loa-4": 4, - "loa-5": 5 - }, - "oidcServiceMetaDataAuthorizeURI": "authorize", - "oidcServiceMetaDataBackChannelURI": "blogout", - "oidcServiceMetaDataCheckSessionURI": "checksession.html", - "oidcServiceMetaDataEndSessionURI": "logout", - "oidcServiceMetaDataFrontChannelURI": "flogout", - "oidcServiceMetaDataIntrospectionURI": "introspect", - "oidcServiceMetaDataJWKSURI": "jwks", - "oidcServiceMetaDataRegistrationURI": "register", - "oidcServiceMetaDataTokenURI": "token", - "oidcServiceMetaDataUserInfoURI": "userinfo", - "oidcServiceOfflineSessionExpiration": 2592000, - "oidcServicePrivateKeySig": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDywteBzIOlhKc4\nO+vhMStDYOpPYrWDOodkUZ7OsxlWVNZ/b/lqIFS56+MHPkKNQuT4zZCyO8bEKmmR\nZ6kPFJoGbO1zJCPQ/RKjimX4J/5gDb1BAlo+6agJi55e3Bw0zKNJDU0mRyedcIzW\n7ywTgyj6B35pl/Sfloi4Q1XEizHar+26h66SOEtnppMxGvwsxO8gFWz26CPmalvY\n5GNYR0txbXUZn7I4kDa4mMWgNfeocWc78Qbt4RV5EuQdbRh1sou4tL9Nn4EuGhg0\nmfsSI0xVAj7f82Wn3kW6qEbhuejrY7aqmZjN7yrMKtCBuV7o4hVrjYLuM2j0mInY\nMy5nRNOVAgMBAAECggEAJ145nK8R2lG83H27LvXOUkrxNJaJYRKoyjgCTPr2bO2t\nK1V5WSCNHOmIE7ChEk962m5bvMu83CsUm6P34p4wrEIV78o4lLe1whe7mZbCxcj0\nnApJoFI8EfA2aqO/X0CgakRh8ocvgXSzIlf/CdsHViTI907ROOAso9Unn4wDNbdp\nMrhi3H2SnA+ewzj85WygBVTNQmVBjJSSLXTQRkfHye0ztvQm59gqqaJaM2rkBjvA\nlPWAVsgakOk4pgClKElCsIjWPJwdYtcd8VJrwnro5J9KhMwB//AArGgqOaXUHnLH\nv5aZZp6FjV/M3BxbSp4cG6hXmK1hrDFLecRddYP1gQKBgQD+Y4/ee57Z0E2V8833\nYfrK3F23sfxmZ7zUwEbgFXUfRy3RVW7Hbc7PAJzxzrk+LYk/zaZrrfEJguqG2O6m\nVNYkqxKu69Nn964CMdV15JGxVzpzsN5adKlcvKVVv9gx2rF3SMUOHiRutj2BlUtO\niCq0G3jFsXWIRzePig9PbWP6CQKBgQD0TG2DeDDUgKbeJYIzXfmCvGxlm5MZqCc/\nK7d8P9U0svG//jJRTsa9hcLjk7N24CzhLNHyJmT7dh1Xy1oLyHNPZ4nQRmCe+HUf\nu0SK10WZ2K55ekUmqS+xSuDFWJtWa5SE46cKg0fKu7YkiDKI1s6I3qrF4lew2aDE\n2p8GJRrgLQKBgCh2PZPtpb6PW0fWl5QZiYJqup1VOggvx+EvFBbgUti+wZLiO9SM\nqrBSMKRldSFmrMXxN984s3YH1LXOG2dpZwY+D6Ky79VBl/PRaVpvGJ1Uen+cSkGo\n/Kc7ejDBaunDFycZ8/3i3Xiek/ngfTHohqJPHE6Vg1RBv5ydIQJJK/XBAoGAU1XO\n9c4GOjc4tQbuhz9DYgmMoIyVfWcTHEV5bfUIcdWpCelYmMval8QNWzyDN8X5CUcU\nxxm50N3V3KENsn9KdofHRzj6tL/klFJ5azNMFtMHkYDYHfwQvNXiHu++7Zf9LefK\nj5eA4fNuir+7HVrJUX9DmgVADJ/wa7Z4EMyPgnECgYA/NLUs4920h10ie5lFffpM\nqq6CRcBjsQ7eGK9UI1Z2KZUh94eqIENSJ7whBjXKvJJvhAlH4//lVFMMRs7oJePY\nThg+8In7PB64yMOIJZLc5Fekn9aGG6YtErPzePQkXSYCKZxWl5EpjQZGgPRVkNtD\n2nflyJLjiCbTjeNgWIOZlw==\n-----END PRIVATE KEY-----\n", - "oidcServicePublicKeySig": "-----BEGIN CERTIFICATE-----\nMIICuDCCAaCgAwIBAgIEFU77HjANBgkqhkiG9w0BAQsFADAeMRwwGgYDVQQDDBNt\nYXRyaXgubGluYWdvcmEuY29tMB4XDTIzMDIxNTAzMTk0NloXDTQzMDIxMDAzMTk0\nNlowHjEcMBoGA1UEAwwTbWF0cml4LmxpbmFnb3JhLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBAPLC14HMg6WEpzg76+ExK0Ng6k9itYM6h2RRns6z\nGVZU1n9v+WogVLnr4wc+Qo1C5PjNkLI7xsQqaZFnqQ8UmgZs7XMkI9D9EqOKZfgn\n/mANvUECWj7pqAmLnl7cHDTMo0kNTSZHJ51wjNbvLBODKPoHfmmX9J+WiLhDVcSL\nMdqv7bqHrpI4S2emkzEa/CzE7yAVbPboI+ZqW9jkY1hHS3FtdRmfsjiQNriYxaA1\n96hxZzvxBu3hFXkS5B1tGHWyi7i0v02fgS4aGDSZ+xIjTFUCPt/zZafeRbqoRuG5\n6OtjtqqZmM3vKswq0IG5XujiFWuNgu4zaPSYidgzLmdE05UCAwEAATANBgkqhkiG\n9w0BAQsFAAOCAQEArNmGxZVvmvdOLctv+zQ+npzQtOTaJcf+r/1xYuM4FZVe4yLc\ny9ElDskoDWjvQU7jKeJeaDOYgMJQNrek8Doj8uHPWNe6jYFa62Csg9aPz6e8qbtq\nWI+sXds5GJd6xZ8mi2L4MdT/tf8dBgcgybuoRyhBtJwG1rLNAYkeXMxkBzOFcU7K\nR/SZ0q9ToLAWFDhn42MTjPN3t6GwKDzGNsM/SI/3WvUwpQbtK91hjPnNDwKiAtGG\nfUteuigfXY+0hEcQwJdR0St/FQ8UYYcAB5YT9IkT1wCcU5LfPHCBf3OXNpbnQsHh\netQMKLibM6wWdXNwmsd1szO66ft3QZ4h4EG3Vw==\n-----END CERTIFICATE-----\n", - "oidcStorageOptions": {}, - "openIdAuthnLevel": 1, - "openIdExportedVars": {}, - "openIdIDPList": "0;", - "openIdSPList": "0;", - "openIdSreg_email": "mail", - "openIdSreg_fullname": "cn", - "openIdSreg_nickname": "uid", - "openIdSreg_timezone": "_timezone", - "pamAuthnLevel": 2, - "pamService": "login", - "password2fActivation": 0, - "password2fSelfRegistration": 0, - "password2fUserCanRemoveKey": 1, - "passwordDB": "Demo", - "passwordPolicyActivation": 1, - "passwordPolicyMinDigit": 0, - "passwordPolicyMinLower": 0, - "passwordPolicyMinSize": 0, - "passwordPolicyMinSpeChar": 0, - "passwordPolicyMinUpper": 0, - "passwordPolicySpecialChar": "__ALL__", - "passwordResetAllowedRetries": 3, - "persistentSessionAttributes": "_loginHistory _2fDevices notification_", - "persistentStorage": "Apache::Session::File", - "persistentStorageOptions": { - "Directory": "/var/lib/lemonldap-ng/psessions", - "LockDirectory": "/var/lib/lemonldap-ng/psessions/lock" - }, - "port": -1, - "portal": "https://auth.example.com", - "portalAntiFrame": 1, - "portalCheckLogins": 1, - "portalDisplayAppslist": 1, - "portalDisplayChangePassword": "$_auth =~ /^(LDAP|DBI|Demo)$/", - "portalDisplayGeneratePassword": 1, - "portalDisplayLoginHistory": 1, - "portalDisplayLogout": 1, - "portalDisplayOidcConsents": "$_oidcConsents && $_oidcConsents =~ /\\w+/", - "portalDisplayOrder": "Appslist ChangePassword LoginHistory OidcConsents Logout", - "portalDisplayRefreshMyRights": 1, - "portalDisplayRegister": 1, - "portalErrorOnExpiredSession": 1, - "portalFavicon": "common/favicon.ico", - "portalForceAuthnInterval": 5, - "portalMainLogo": "common/logos/logo_llng_400px.png", - "portalPingInterval": 60000, - "portalRequireOldPassword": 1, - "portalSkin": "bootstrap", - "portalSkinBackground": "1280px-Cedar_Breaks_National_Monument_partially.jpg", - "portalUserAttr": "_user", - "proxyAuthServiceChoiceParam": "lmAuth", - "proxyAuthnLevel": 2, - "radius2fActivation": 0, - "radius2fTimeout": 20, - "radiusAuthnLevel": 3, - "radiusExportedVars": {}, - "randomPasswordRegexp": "[A-Z]{3}[a-z]{5}.\\d{2}", - "redirectFormMethod": "get", - "registerDB": "Null", - "registerTimeout": 0, - "registerUrl": "https://auth.example.com/register", - "reloadTimeout": 5, - "reloadUrls": { - "localhost": "https://reload.example.com/reload" - }, - "rememberAuthChoiceRule": 0, - "rememberCookieName": "llngrememberauthchoice", - "rememberCookieTimeout": 31536000, - "rememberTimer": 5, - "remoteGlobalStorage": "Lemonldap::NG::Common::Apache::Session::SOAP", - "remoteGlobalStorageOptions": { - "ns": "https://auth.example.com/Lemonldap/NG/Common/PSGI/SOAPService", - "proxy": "https://auth.example.com/sessions" - }, - "requireToken": 1, - "rest2fActivation": 0, - "restAuthnLevel": 2, - "restClockTolerance": 15, - "sameSite": "", - "samlAttributeAuthorityDescriptorAttributeServiceSOAP": "urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/AA/SOAP;", - "samlAuthnContextMapKerberos": 4, - "samlAuthnContextMapPassword": 2, - "samlAuthnContextMapPasswordProtectedTransport": 3, - "samlAuthnContextMapTLSClient": 5, - "samlEntityID": "#PORTAL#/saml/metadata", - "samlIDPSSODescriptorArtifactResolutionServiceArtifact": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/artifact", - "samlIDPSSODescriptorSingleLogoutServiceHTTPPost": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/singleLogout;#PORTAL#/saml/singleLogoutReturn", - "samlIDPSSODescriptorSingleLogoutServiceHTTPRedirect": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect;#PORTAL#/saml/singleLogout;#PORTAL#/saml/singleLogoutReturn", - "samlIDPSSODescriptorSingleLogoutServiceSOAP": "urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/singleLogoutSOAP;", - "samlIDPSSODescriptorSingleSignOnServiceHTTPArtifact": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/singleSignOnArtifact;", - "samlIDPSSODescriptorSingleSignOnServiceHTTPPost": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/singleSignOn;", - "samlIDPSSODescriptorSingleSignOnServiceHTTPRedirect": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect;#PORTAL#/saml/singleSignOn;", - "samlIDPSSODescriptorWantAuthnRequestsSigned": 1, - "samlMetadataForceUTF8": 1, - "samlNameIDFormatMapEmail": "mail", - "samlNameIDFormatMapKerberos": "uid", - "samlNameIDFormatMapWindows": "uid", - "samlNameIDFormatMapX509": "mail", - "samlOrganizationDisplayName": "Example", - "samlOrganizationName": "Example", - "samlOrganizationURL": "https://www.example.com", - "samlOverrideIDPEntityID": "", - "samlRelayStateTimeout": 600, - "samlSPSSODescriptorArtifactResolutionServiceArtifact": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/artifact", - "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact": "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact", - "samlSPSSODescriptorAssertionConsumerServiceHTTPPost": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost", - "samlSPSSODescriptorAuthnRequestsSigned": 1, - "samlSPSSODescriptorSingleLogoutServiceHTTPPost": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleLogout;#PORTAL#/saml/proxySingleLogoutReturn", - "samlSPSSODescriptorSingleLogoutServiceHTTPRedirect": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect;#PORTAL#/saml/proxySingleLogout;#PORTAL#/saml/proxySingleLogoutReturn", - "samlSPSSODescriptorSingleLogoutServiceSOAP": "urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/proxySingleLogoutSOAP;", - "samlSPSSODescriptorWantAssertionsSigned": 1, - "samlServiceSignatureMethod": "RSA_SHA256", - "scrollTop": 400, - "securedCookie": 0, - "sessionDataToRemember": {}, - "sfEngine": "::2F::Engines::Default", - "sfManagerRule": 1, - "sfRemovedMsgRule": 0, - "sfRemovedNotifMsg": "_removedSF_ expired second factor(s) has/have been removed (_nameSF_)!", - "sfRemovedNotifRef": "RemoveSF", - "sfRemovedNotifTitle": "Second factor notification", - "sfRequired": 0, - "showLanguages": 1, - "singleIP": 0, - "singleSession": 0, - "singleUserByIP": 0, - "slaveAuthnLevel": 2, - "slaveExportedVars": {}, - "soapProxyUrn": "urn:Lemonldap/NG/Common/PSGI/SOAPService", - "stayConnected": 0, - "stayConnectedCookieName": "llngconnection", - "stayConnectedTimeout": 2592000, - "successLoginNumber": 5, - "timeout": 72000, - "timeoutActivity": 0, - "timeoutActivityInterval": 60, - "totp2fActivation": 0, - "totp2fDigits": 6, - "totp2fInterval": 30, - "totp2fRange": 1, - "totp2fSelfRegistration": 0, - "totp2fUserCanRemoveKey": 1, - "twitterAuthnLevel": 1, - "twitterUserField": "screen_name", - "u2fActivation": 0, - "u2fSelfRegistration": 0, - "u2fUserCanRemoveKey": 1, - "upgradeSession": 1, - "useRedirectOnError": 1, - "useSafeJail": 1, - "userControl": "^[\\w\\.\\-@]+$", - "userDB": "Same", - "utotp2fActivation": 0, - "viewerHiddenKeys": "samlIDPMetaDataNodes, samlSPMetaDataNodes", - "webIDAuthnLevel": 1, - "webIDExportedVars": {}, - "webauthn2fActivation": 0, - "webauthn2fSelfRegistration": 0, - "webauthn2fUserCanRemoveKey": 1, - "webauthn2fUserVerification": "preferred", - "whatToTrace": "_whatToTrace", - "yubikey2fActivation": 0, - "yubikey2fPublicIDSize": 12, - "yubikey2fSelfRegistration": 0, - "yubikey2fUserCanRemoveKey": 1 - } - \ No newline at end of file + "ADPwdExpireWarning": 0, + "ADPwdMaxAge": 0, + "SMTPServer": "", + "SMTPTLS": "", + "SSLAuthnLevel": 5, + "SSLIssuerVar": "SSL_CLIENT_I_DN", + "SSLVar": "SSL_CLIENT_S_DN_Email", + "SSLVarIf": {}, + "activeTimer": 1, + "apacheAuthnLevel": 3, + "applicationList": {}, + "authChoiceParam": "lmAuth", + "authentication": "LDAP", + "available2F": "UTOTP,TOTP,U2F,REST,Mail2F,Ext2F,WebAuthn,Yubikey,Radius,Password", + "available2FSelfRegistration": "Password,TOTP,U2F,WebAuthn,Yubikey", + "bruteForceProtectionLockTimes": "15, 30, 60, 300, 600", + "bruteForceProtectionMaxAge": 300, + "bruteForceProtectionMaxFailed": 3, + "bruteForceProtectionMaxLockTime": 900, + "bruteForceProtectionTempo": 30, + "captcha_mail_enabled": 1, + "captcha_register_enabled": 1, + "captcha_size": 6, + "casAccessControlPolicy": "none", + "casAuthnLevel": 1, + "casTicketExpiration": 0, + "certificateResetByMailCeaAttribute": "description", + "certificateResetByMailCertificateAttribute": "userCertificate;binary", + "certificateResetByMailURL": "https://auth.example.com/certificateReset", + "certificateResetByMailValidityDelay": 0, + "cfgAuthor": "The LemonLDAP::NG team", + "cfgDate": "1627287638", + "cfgNum": "1", + "cfgVersion": "2.0.16", + "checkDevOpsCheckSessionAttributes": 1, + "checkDevOpsDisplayNormalizedHeaders": 1, + "checkDevOpsDownload": 1, + "checkHIBPRequired": 1, + "checkHIBPURL": "https://api.pwnedpasswords.com/range/", + "checkTime": 600, + "checkUserDisplayComputedSession": 1, + "checkUserDisplayEmptyHeaders": 0, + "checkUserDisplayEmptyValues": 0, + "checkUserDisplayHiddenAttributes": 0, + "checkUserDisplayHistory": 0, + "checkUserDisplayNormalizedHeaders": 0, + "checkUserDisplayPersistentInfo": 0, + "checkUserHiddenAttributes": "_loginHistory, _session_id, hGroups", + "checkUserIdRule": 1, + "checkXSS": 1, + "confirmFormMethod": "post", + "contextSwitchingIdRule": 1, + "contextSwitchingPrefix": "switching", + "contextSwitchingRule": 0, + "contextSwitchingStopWithLogout": 1, + "cookieName": "lemonldap", + "corsAllow_Credentials": "true", + "corsAllow_Headers": "*", + "corsAllow_Methods": "POST,GET", + "corsAllow_Origin": "*", + "corsEnabled": 1, + "corsExpose_Headers": "*", + "corsMax_Age": "86400", + "crowdsecAction": "reject", + "cspConnect": "'self'", + "cspDefault": "'self'", + "cspFont": "'self'", + "cspFormAction": "*", + "cspFrameAncestors": "", + "cspImg": "'self' data:", + "cspScript": "'self'", + "cspStyle": "'self'", + "dbiAuthnLevel": 2, + "dbiExportedVars": {}, + "decryptValueRule": 0, + "demoExportedVars": { + "cn": "cn", + "mail": "mail", + "uid": "uid" + }, + "displaySessionId": 1, + "domain": "example.com", + "exportedHeaders": {}, + "exportedVars": {}, + "ext2fActivation": 0, + "ext2fCodeActivation": "\\d{6}", + "facebookAuthnLevel": 1, + "facebookExportedVars": {}, + "facebookUserField": "id", + "failedLoginNumber": 5, + "findUserControl": "^[*\\w]+$", + "findUserWildcard": "*", + "formTimeout": 120, + "githubAuthnLevel": 1, + "githubScope": "user:email", + "githubUserField": "login", + "globalLogoutRule": 0, + "globalLogoutTimer": 1, + "globalStorage": "Apache::Session::File", + "globalStorageOptions": { + "Directory": "/var/lib/lemonldap-ng/sessions", + "LockDirectory": "/var/lib/lemonldap-ng/sessions/lock", + "generateModule": "Lemonldap::NG::Common::Apache::Session::Generate::SHA256" + }, + "gpgAuthnLevel": 5, + "gpgDb": "", + "grantSessionRules": {}, + "groups": {}, + "handlerInternalCache": 15, + "handlerServiceTokenTTL": 30, + "hiddenAttributes": "_password, _2fDevices", + "httpOnly": 1, + "https": -1, + "impersonationHiddenAttributes": "_2fDevices, _loginHistory", + "impersonationIdRule": 1, + "impersonationMergeSSOgroups": 0, + "impersonationPrefix": "real_", + "impersonationRule": 0, + "impersonationSkipEmptyValues": 1, + "infoFormMethod": "get", + "issuerDBCASPath": "^/cas/", + "issuerDBCASRule": 1, + "issuerDBGetParameters": {}, + "issuerDBGetPath": "^/get/", + "issuerDBGetRule": 1, + "issuerDBOpenIDConnectActivation": 1, + "issuerDBOpenIDConnectPath": "^/oauth2/", + "issuerDBOpenIDConnectRule": 1, + "issuerDBOpenIDPath": "^/openidserver/", + "issuerDBOpenIDRule": 1, + "issuerDBSAMLPath": "^/saml/", + "issuerDBSAMLRule": 1, + "issuersTimeout": 120, + "jsRedirect": 0, + "key": "^vmTGvh{+]5!ToB?", + "krbAuthnLevel": 3, + "krbRemoveDomain": 1, + "ldapServer": "host.docker.internal:21389", + "ldapAuthnLevel": 2, + "ldapBase": "dc=example,dc=com", + "ldapExportedVars": { + "cn": "cn", + "mail": "mail", + "uid": "uid" + }, + "ldapGroupAttributeName": "member", + "ldapGroupAttributeNameGroup": "dn", + "ldapGroupAttributeNameSearch": "cn", + "ldapGroupAttributeNameUser": "dn", + "ldapGroupObjectClass": "groupOfNames", + "ldapIOTimeout": 10, + "ldapPasswordResetAttribute": "pwdReset", + "ldapPasswordResetAttributeValue": "TRUE", + "ldapPwdEnc": "utf-8", + "ldapSearchDeref": "find", + "ldapTimeout": 10, + "ldapUsePasswordResetAttribute": 1, + "ldapVerify": "require", + "ldapVersion": 3, + "linkedInAuthnLevel": 1, + "linkedInFields": "id,first-name,last-name,email-address", + "linkedInScope": "r_liteprofile r_emailaddress", + "linkedInUserField": "emailAddress", + "localSessionStorage": "Cache::FileCache", + "localSessionStorageOptions": { + "cache_depth": 3, + "cache_root": "/var/lib/lemonldap-ng/cache", + "default_expires_in": 600, + "directory_umask": "007", + "namespace": "lemonldap-ng-sessions" + }, + "locationDetectGeoIpLanguages": "en, fr", + "locationRules": { + "auth.example.com": { + "(?#checkUser)^/checkuser": "inGroup(\"timelords\")", + "(?#errors)^/lmerror/": "accept", + "default": "accept" + } + }, + "loginHistoryEnabled": 1, + "logoutServices": {}, + "macros": { + "UA": "$ENV{HTTP_USER_AGENT}", + "_whatToTrace": "$_auth eq 'SAML' ? lc($_user.'@'.$_idpConfKey) : $_auth eq 'OpenIDConnect' ? lc($_user.'@'.$_oidc_OP) : lc($_user)" + }, + "mail2fActivation": 0, + "mail2fCodeRegex": "\\d{6}", + "mailCharset": "utf-8", + "mailFrom": "noreply@example.com", + "mailSessionKey": "mail", + "mailTimeout": 0, + "mailUrl": "https://auth.example.com/resetpwd", + "managerDn": "", + "managerPassword": "", + "max2FDevices": 10, + "max2FDevicesNameLength": 20, + "multiValuesSeparator": "; ", + "mySessionAuthorizedRWKeys": [ + "_appsListOrder", + "_oidcConnectedRP", + "_oidcConsents" + ], + "newLocationWarningLocationAttribute": "ipAddr", + "newLocationWarningLocationDisplayAttribute": "", + "newLocationWarningMaxValues": "0", + "notification": 0, + "notificationDefaultCond": "", + "notificationServerPOST": 1, + "notificationServerSentAttributes": "uid reference date title subtitle text check", + "notificationStorage": "File", + "notificationStorageOptions": { + "dirName": "/var/lib/lemonldap-ng/notifications" + }, + "notificationWildcard": "allusers", + "notificationsMaxRetrieve": 3, + "notifyDeleted": 1, + "nullAuthnLevel": 0, + "oidcAuthnLevel": 1, + "oidcOPMetaDataExportedVars": {}, + "oidcOPMetaDataJSON": {}, + "oidcOPMetaDataJWKS": {}, + "oidcOPMetaDataOptions": {}, + "oidcRPCallbackGetParam": "openidconnectcallback", + "oidcRPMetaDataExportedVars": { + "matrix": { + "email": "mail", + "family_name": "cn", + "given_name": "cn", + "name": "cn", + "nickname": "uid", + "preferred_username": "uid" + } + }, + "oidcRPMetaDataMacros": null, + "oidcRPMetaDataOptions": { + "matrix": { + "oidcRPMetaDataOptionsAccessTokenClaims": 0, + "oidcRPMetaDataOptionsAccessTokenJWT": 0, + "oidcRPMetaDataOptionsAccessTokenSignAlg": "RS256", + "oidcRPMetaDataOptionsAllowClientCredentialsGrant": 0, + "oidcRPMetaDataOptionsAllowOffline": 0, + "oidcRPMetaDataOptionsAllowPasswordGrant": 0, + "oidcRPMetaDataOptionsBypassConsent": 1, + "oidcRPMetaDataOptionsClientID": "matrix1", + "oidcRPMetaDataOptionsClientSecret": "matrix1*", + "oidcRPMetaDataOptionsIDTokenForceClaims": 0, + "oidcRPMetaDataOptionsIDTokenSignAlg": "RS256", + "oidcRPMetaDataOptionsLogoutBypassConfirm": 0, + "oidcRPMetaDataOptionsLogoutSessionRequired": 1, + "oidcRPMetaDataOptionsLogoutType": "back", + "oidcRPMetaDataOptionsPublic": 0, + "oidcRPMetaDataOptionsRedirectUris": "https://matrix.example.com:444/_synapse/client/oidc/callback", + "oidcRPMetaDataOptionsRefreshToken": 0, + "oidcRPMetaDataOptionsRequirePKCE": 0 + } + }, + "oidcRPMetaDataOptionsExtraClaims": null, + "oidcRPMetaDataScopeRules": null, + "oidcRPStateTimeout": 600, + "oidcServiceAccessTokenExpiration": 3600, + "oidcServiceAllowAuthorizationCodeFlow": 1, + "oidcServiceAllowImplicitFlow": 0, + "oidcServiceAuthorizationCodeExpiration": 60, + "oidcServiceDynamicRegistrationExportedVars": {}, + "oidcServiceDynamicRegistrationExtraClaims": {}, + "oidcServiceIDTokenExpiration": 3600, + "oidcServiceIgnoreScopeForClaims": 1, + "oidcServiceKeyIdSig": "oMGHInscAW3Nsa0FcnCnDA", + "oidcServiceMetaDataAuthnContext": { + "loa-1": 1, + "loa-2": 2, + "loa-3": 3, + "loa-4": 4, + "loa-5": 5 + }, + "oidcServiceMetaDataAuthorizeURI": "authorize", + "oidcServiceMetaDataBackChannelURI": "blogout", + "oidcServiceMetaDataCheckSessionURI": "checksession.html", + "oidcServiceMetaDataEndSessionURI": "logout", + "oidcServiceMetaDataFrontChannelURI": "flogout", + "oidcServiceMetaDataIntrospectionURI": "introspect", + "oidcServiceMetaDataJWKSURI": "jwks", + "oidcServiceMetaDataRegistrationURI": "register", + "oidcServiceMetaDataTokenURI": "token", + "oidcServiceMetaDataUserInfoURI": "userinfo", + "oidcServiceOfflineSessionExpiration": 2592000, + "oidcServicePrivateKeySig": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDywteBzIOlhKc4\nO+vhMStDYOpPYrWDOodkUZ7OsxlWVNZ/b/lqIFS56+MHPkKNQuT4zZCyO8bEKmmR\nZ6kPFJoGbO1zJCPQ/RKjimX4J/5gDb1BAlo+6agJi55e3Bw0zKNJDU0mRyedcIzW\n7ywTgyj6B35pl/Sfloi4Q1XEizHar+26h66SOEtnppMxGvwsxO8gFWz26CPmalvY\n5GNYR0txbXUZn7I4kDa4mMWgNfeocWc78Qbt4RV5EuQdbRh1sou4tL9Nn4EuGhg0\nmfsSI0xVAj7f82Wn3kW6qEbhuejrY7aqmZjN7yrMKtCBuV7o4hVrjYLuM2j0mInY\nMy5nRNOVAgMBAAECggEAJ145nK8R2lG83H27LvXOUkrxNJaJYRKoyjgCTPr2bO2t\nK1V5WSCNHOmIE7ChEk962m5bvMu83CsUm6P34p4wrEIV78o4lLe1whe7mZbCxcj0\nnApJoFI8EfA2aqO/X0CgakRh8ocvgXSzIlf/CdsHViTI907ROOAso9Unn4wDNbdp\nMrhi3H2SnA+ewzj85WygBVTNQmVBjJSSLXTQRkfHye0ztvQm59gqqaJaM2rkBjvA\nlPWAVsgakOk4pgClKElCsIjWPJwdYtcd8VJrwnro5J9KhMwB//AArGgqOaXUHnLH\nv5aZZp6FjV/M3BxbSp4cG6hXmK1hrDFLecRddYP1gQKBgQD+Y4/ee57Z0E2V8833\nYfrK3F23sfxmZ7zUwEbgFXUfRy3RVW7Hbc7PAJzxzrk+LYk/zaZrrfEJguqG2O6m\nVNYkqxKu69Nn964CMdV15JGxVzpzsN5adKlcvKVVv9gx2rF3SMUOHiRutj2BlUtO\niCq0G3jFsXWIRzePig9PbWP6CQKBgQD0TG2DeDDUgKbeJYIzXfmCvGxlm5MZqCc/\nK7d8P9U0svG//jJRTsa9hcLjk7N24CzhLNHyJmT7dh1Xy1oLyHNPZ4nQRmCe+HUf\nu0SK10WZ2K55ekUmqS+xSuDFWJtWa5SE46cKg0fKu7YkiDKI1s6I3qrF4lew2aDE\n2p8GJRrgLQKBgCh2PZPtpb6PW0fWl5QZiYJqup1VOggvx+EvFBbgUti+wZLiO9SM\nqrBSMKRldSFmrMXxN984s3YH1LXOG2dpZwY+D6Ky79VBl/PRaVpvGJ1Uen+cSkGo\n/Kc7ejDBaunDFycZ8/3i3Xiek/ngfTHohqJPHE6Vg1RBv5ydIQJJK/XBAoGAU1XO\n9c4GOjc4tQbuhz9DYgmMoIyVfWcTHEV5bfUIcdWpCelYmMval8QNWzyDN8X5CUcU\nxxm50N3V3KENsn9KdofHRzj6tL/klFJ5azNMFtMHkYDYHfwQvNXiHu++7Zf9LefK\nj5eA4fNuir+7HVrJUX9DmgVADJ/wa7Z4EMyPgnECgYA/NLUs4920h10ie5lFffpM\nqq6CRcBjsQ7eGK9UI1Z2KZUh94eqIENSJ7whBjXKvJJvhAlH4//lVFMMRs7oJePY\nThg+8In7PB64yMOIJZLc5Fekn9aGG6YtErPzePQkXSYCKZxWl5EpjQZGgPRVkNtD\n2nflyJLjiCbTjeNgWIOZlw==\n-----END PRIVATE KEY-----\n", + "oidcServicePublicKeySig": "-----BEGIN CERTIFICATE-----\nMIICuDCCAaCgAwIBAgIEFU77HjANBgkqhkiG9w0BAQsFADAeMRwwGgYDVQQDDBNt\nYXRyaXgubGluYWdvcmEuY29tMB4XDTIzMDIxNTAzMTk0NloXDTQzMDIxMDAzMTk0\nNlowHjEcMBoGA1UEAwwTbWF0cml4LmxpbmFnb3JhLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBAPLC14HMg6WEpzg76+ExK0Ng6k9itYM6h2RRns6z\nGVZU1n9v+WogVLnr4wc+Qo1C5PjNkLI7xsQqaZFnqQ8UmgZs7XMkI9D9EqOKZfgn\n/mANvUECWj7pqAmLnl7cHDTMo0kNTSZHJ51wjNbvLBODKPoHfmmX9J+WiLhDVcSL\nMdqv7bqHrpI4S2emkzEa/CzE7yAVbPboI+ZqW9jkY1hHS3FtdRmfsjiQNriYxaA1\n96hxZzvxBu3hFXkS5B1tGHWyi7i0v02fgS4aGDSZ+xIjTFUCPt/zZafeRbqoRuG5\n6OtjtqqZmM3vKswq0IG5XujiFWuNgu4zaPSYidgzLmdE05UCAwEAATANBgkqhkiG\n9w0BAQsFAAOCAQEArNmGxZVvmvdOLctv+zQ+npzQtOTaJcf+r/1xYuM4FZVe4yLc\ny9ElDskoDWjvQU7jKeJeaDOYgMJQNrek8Doj8uHPWNe6jYFa62Csg9aPz6e8qbtq\nWI+sXds5GJd6xZ8mi2L4MdT/tf8dBgcgybuoRyhBtJwG1rLNAYkeXMxkBzOFcU7K\nR/SZ0q9ToLAWFDhn42MTjPN3t6GwKDzGNsM/SI/3WvUwpQbtK91hjPnNDwKiAtGG\nfUteuigfXY+0hEcQwJdR0St/FQ8UYYcAB5YT9IkT1wCcU5LfPHCBf3OXNpbnQsHh\netQMKLibM6wWdXNwmsd1szO66ft3QZ4h4EG3Vw==\n-----END CERTIFICATE-----\n", + "oidcStorageOptions": {}, + "openIdAuthnLevel": 1, + "openIdExportedVars": {}, + "openIdIDPList": "0;", + "openIdSPList": "0;", + "openIdSreg_email": "mail", + "openIdSreg_fullname": "cn", + "openIdSreg_nickname": "uid", + "openIdSreg_timezone": "_timezone", + "pamAuthnLevel": 2, + "pamService": "login", + "password2fActivation": 0, + "password2fSelfRegistration": 0, + "password2fUserCanRemoveKey": 1, + "passwordDB": "Demo", + "passwordPolicyActivation": 1, + "passwordPolicyMinDigit": 0, + "passwordPolicyMinLower": 0, + "passwordPolicyMinSize": 0, + "passwordPolicyMinSpeChar": 0, + "passwordPolicyMinUpper": 0, + "passwordPolicySpecialChar": "__ALL__", + "passwordResetAllowedRetries": 3, + "persistentSessionAttributes": "_loginHistory _2fDevices notification_", + "persistentStorage": "Apache::Session::File", + "persistentStorageOptions": { + "Directory": "/var/lib/lemonldap-ng/psessions", + "LockDirectory": "/var/lib/lemonldap-ng/psessions/lock" + }, + "port": -1, + "portal": "https://auth.example.com", + "portalAntiFrame": 1, + "portalCheckLogins": 1, + "portalDisplayAppslist": 1, + "portalDisplayChangePassword": "$_auth =~ /^(LDAP|DBI|Demo)$/", + "portalDisplayGeneratePassword": 1, + "portalDisplayLoginHistory": 1, + "portalDisplayLogout": 1, + "portalDisplayOidcConsents": "$_oidcConsents && $_oidcConsents =~ /\\w+/", + "portalDisplayOrder": "Appslist ChangePassword LoginHistory OidcConsents Logout", + "portalDisplayRefreshMyRights": 1, + "portalDisplayRegister": 1, + "portalErrorOnExpiredSession": 1, + "portalFavicon": "common/favicon.ico", + "portalForceAuthnInterval": 5, + "portalMainLogo": "common/logos/logo_llng_400px.png", + "portalPingInterval": 60000, + "portalRequireOldPassword": 1, + "portalSkin": "bootstrap", + "portalSkinBackground": "1280px-Cedar_Breaks_National_Monument_partially.jpg", + "portalUserAttr": "_user", + "proxyAuthServiceChoiceParam": "lmAuth", + "proxyAuthnLevel": 2, + "radius2fActivation": 0, + "radius2fTimeout": 20, + "radiusAuthnLevel": 3, + "radiusExportedVars": {}, + "randomPasswordRegexp": "[A-Z]{3}[a-z]{5}.\\d{2}", + "redirectFormMethod": "get", + "registerDB": "Null", + "registerTimeout": 0, + "registerUrl": "https://auth.example.com/register", + "reloadTimeout": 5, + "reloadUrls": { + "localhost": "https://reload.example.com/reload" + }, + "rememberAuthChoiceRule": 0, + "rememberCookieName": "llngrememberauthchoice", + "rememberCookieTimeout": 31536000, + "rememberTimer": 5, + "remoteGlobalStorage": "Lemonldap::NG::Common::Apache::Session::SOAP", + "remoteGlobalStorageOptions": { + "ns": "https://auth.example.com/Lemonldap/NG/Common/PSGI/SOAPService", + "proxy": "https://auth.example.com/sessions" + }, + "requireToken": 1, + "rest2fActivation": 0, + "restAuthnLevel": 2, + "restClockTolerance": 15, + "sameSite": "", + "samlAttributeAuthorityDescriptorAttributeServiceSOAP": "urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/AA/SOAP;", + "samlAuthnContextMapKerberos": 4, + "samlAuthnContextMapPassword": 2, + "samlAuthnContextMapPasswordProtectedTransport": 3, + "samlAuthnContextMapTLSClient": 5, + "samlEntityID": "#PORTAL#/saml/metadata", + "samlIDPSSODescriptorArtifactResolutionServiceArtifact": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/artifact", + "samlIDPSSODescriptorSingleLogoutServiceHTTPPost": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/singleLogout;#PORTAL#/saml/singleLogoutReturn", + "samlIDPSSODescriptorSingleLogoutServiceHTTPRedirect": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect;#PORTAL#/saml/singleLogout;#PORTAL#/saml/singleLogoutReturn", + "samlIDPSSODescriptorSingleLogoutServiceSOAP": "urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/singleLogoutSOAP;", + "samlIDPSSODescriptorSingleSignOnServiceHTTPArtifact": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/singleSignOnArtifact;", + "samlIDPSSODescriptorSingleSignOnServiceHTTPPost": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/singleSignOn;", + "samlIDPSSODescriptorSingleSignOnServiceHTTPRedirect": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect;#PORTAL#/saml/singleSignOn;", + "samlIDPSSODescriptorWantAuthnRequestsSigned": 1, + "samlMetadataForceUTF8": 1, + "samlNameIDFormatMapEmail": "mail", + "samlNameIDFormatMapKerberos": "uid", + "samlNameIDFormatMapWindows": "uid", + "samlNameIDFormatMapX509": "mail", + "samlOrganizationDisplayName": "Example", + "samlOrganizationName": "Example", + "samlOrganizationURL": "https://www.example.com", + "samlOverrideIDPEntityID": "", + "samlRelayStateTimeout": 600, + "samlSPSSODescriptorArtifactResolutionServiceArtifact": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/artifact", + "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact": "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact", + "samlSPSSODescriptorAssertionConsumerServiceHTTPPost": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost", + "samlSPSSODescriptorAuthnRequestsSigned": 1, + "samlSPSSODescriptorSingleLogoutServiceHTTPPost": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleLogout;#PORTAL#/saml/proxySingleLogoutReturn", + "samlSPSSODescriptorSingleLogoutServiceHTTPRedirect": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect;#PORTAL#/saml/proxySingleLogout;#PORTAL#/saml/proxySingleLogoutReturn", + "samlSPSSODescriptorSingleLogoutServiceSOAP": "urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/proxySingleLogoutSOAP;", + "samlSPSSODescriptorWantAssertionsSigned": 1, + "samlServiceSignatureMethod": "RSA_SHA256", + "scrollTop": 400, + "securedCookie": 0, + "sessionDataToRemember": {}, + "sfEngine": "::2F::Engines::Default", + "sfManagerRule": 1, + "sfRemovedMsgRule": 0, + "sfRemovedNotifMsg": "_removedSF_ expired second factor(s) has/have been removed (_nameSF_)!", + "sfRemovedNotifRef": "RemoveSF", + "sfRemovedNotifTitle": "Second factor notification", + "sfRequired": 0, + "showLanguages": 1, + "singleIP": 0, + "singleSession": 0, + "singleUserByIP": 0, + "slaveAuthnLevel": 2, + "slaveExportedVars": {}, + "soapProxyUrn": "urn:Lemonldap/NG/Common/PSGI/SOAPService", + "stayConnected": 0, + "stayConnectedCookieName": "llngconnection", + "stayConnectedTimeout": 2592000, + "successLoginNumber": 5, + "timeout": 72000, + "timeoutActivity": 0, + "timeoutActivityInterval": 60, + "totp2fActivation": 0, + "totp2fDigits": 6, + "totp2fInterval": 30, + "totp2fRange": 1, + "totp2fSelfRegistration": 0, + "totp2fUserCanRemoveKey": 1, + "twitterAuthnLevel": 1, + "twitterUserField": "screen_name", + "u2fActivation": 0, + "u2fSelfRegistration": 0, + "u2fUserCanRemoveKey": 1, + "upgradeSession": 1, + "useRedirectOnError": 1, + "useSafeJail": 1, + "userControl": "^[\\w\\.\\-@]+$", + "userDB": "Same", + "utotp2fActivation": 0, + "viewerHiddenKeys": "samlIDPMetaDataNodes, samlSPMetaDataNodes", + "webIDAuthnLevel": 1, + "webIDExportedVars": {}, + "webauthn2fActivation": 0, + "webauthn2fSelfRegistration": 0, + "webauthn2fUserCanRemoveKey": 1, + "webauthn2fUserVerification": "preferred", + "whatToTrace": "_whatToTrace", + "yubikey2fActivation": 0, + "yubikey2fPublicIDSize": 12, + "yubikey2fSelfRegistration": 0, + "yubikey2fUserCanRemoveKey": 1 +} diff --git a/packages/tom-server/src/application-server/__testData__/synapse-data/homeserver.yaml b/packages/tom-server/src/application-server/__testData__/synapse-data/homeserver.yaml index 7e180d96..6a517582 100644 --- a/packages/tom-server/src/application-server/__testData__/synapse-data/homeserver.yaml +++ b/packages/tom-server/src/application-server/__testData__/synapse-data/homeserver.yaml @@ -9,8 +9,8 @@ # For more information on how to configure Synapse, including a complete accounting of # each option, go to docs/usage/configuration/config_documentation.md or # https://matrix-org.github.io/synapse/latest/usage/configuration/config_documentation.html -server_name: "example.com" -public_baseurl: "https://matrix.example.com:444/" +server_name: 'example.com' +public_baseurl: 'https://matrix.example.com:444/' pid_file: /data/homeserver.pid listeners: - port: 8008 @@ -24,15 +24,15 @@ database: name: sqlite3 args: database: /data/homeserver.db -log_config: "/data/matrix.example.com.log.config" +log_config: '/data/matrix.example.com.log.config' media_store_path: /data/media_store -registration_shared_secret: "u+Q^i6&*Y9azZ*~pID^.a=qrvd+mUIBX9SAreEPGJ=xzP&c+Sk" +registration_shared_secret: 'u+Q^i6&*Y9azZ*~pID^.a=qrvd+mUIBX9SAreEPGJ=xzP&c+Sk' report_stats: false -macaroon_secret_key: "=0ws-1~ztzXm&xh+As;7YL5.-U~r-T,F4zR3mW#E;6Y::Rb7&G" -form_secret: "&YFO.XSc*2^2ZsW#hmoR+t:wf03~u#fin#O.R&erFcl9_mEayv" -signing_key_path: "/data/matrix.example.com.signing.key" +macaroon_secret_key: '=0ws-1~ztzXm&xh+As;7YL5.-U~r-T,F4zR3mW#E;6Y::Rb7&G' +form_secret: '&YFO.XSc*2^2ZsW#hmoR+t:wf03~u#fin#O.R&erFcl9_mEayv' +signing_key_path: '/data/matrix.example.com.signing.key' trusted_key_servers: - - server_name: "matrix.org" + - server_name: 'matrix.org' accept_keys_insecurely: true accept_keys_insecurely: true app_service_config_files: @@ -41,14 +41,14 @@ oidc_config: idp_id: lemonldap idp_name: lemonldap enabled: true - issuer: "https://auth.example.com/" - client_id: "matrix1" - client_secret: "matrix1*" - scopes: ["openid", "profile"] + issuer: 'https://auth.example.com/' + client_id: 'matrix1' + client_secret: 'matrix1*' + scopes: ['openid', 'profile'] discover: true - user_profile_method: "userinfo_endpoint" + user_profile_method: 'userinfo_endpoint' user_mapping_provider: config: - subject_claim: "sub" - localpart_template: "{{ user.preferred_username }}" - display_name_template: "{{ user.name }}" \ No newline at end of file + subject_claim: 'sub' + localpart_template: '{{ user.preferred_username }}' + display_name_template: '{{ user.name }}' diff --git a/packages/tom-server/src/application-server/controllers/room.ts b/packages/tom-server/src/application-server/controllers/room.ts index 337a2de7..f62fb26c 100644 --- a/packages/tom-server/src/application-server/controllers/room.ts +++ b/packages/tom-server/src/application-server/controllers/room.ts @@ -9,9 +9,9 @@ import lodash from 'lodash' import fetch, { type Response as FetchResponse } from 'node-fetch' import type TwakeApplicationServer from '..' import type TwakeServer from '../..' -import { type TwakeDB } from '../../db' -import { allMatrixErrorCodes } from '../../types' +import { type TwakeDB, allMatrixErrorCodes } from '../../types' import { TwakeRoom } from '../models/room' +import { toMatrixId } from '@twake/utils' const { intersection } = lodash const domainRe = @@ -36,7 +36,10 @@ export const createRoom = ( if (!hostnameRe.test(twakeServer.conf.matrix_server)) { throw Error('Bad matrix_server_name') } - const appServiceMatrixId = `@${appServer.appServiceRegistration.senderLocalpart}:${twakeServer.conf.server_name}` + const appServiceMatrixId = toMatrixId( + appServer.appServiceRegistration.senderLocalpart, + twakeServer.conf.server_name + ) // eslint-disable-next-line @typescript-eslint/restrict-template-expressions const roomAliasName = `_twake_${req.body.aliasName}` const rooms = await twakeServer.matrixDb.get('room_aliases', undefined, { @@ -133,11 +136,11 @@ export const createRoom = ( twakeServer.matrixDb.getAll('users', ['name']) ]) - const ldapUsersIds = ldapUsers.map( - (user) => - `@${user[twakeServer.conf.ldap_uid_field as string] as string}:${ - twakeServer.conf.server_name - }` + const ldapUsersIds = ldapUsers.map((user) => + toMatrixId( + user[twakeServer.conf.ldap_uid_field as string] as string, + twakeServer.conf.server_name + ) ) const matrixUsersIds = matrixUsers.map((user) => user.name as string) diff --git a/packages/tom-server/src/application-server/index.test.ts b/packages/tom-server/src/application-server/index.test.ts index 99a78fb8..a71446c0 100644 --- a/packages/tom-server/src/application-server/index.test.ts +++ b/packages/tom-server/src/application-server/index.test.ts @@ -20,7 +20,7 @@ import { import AppServiceAPI from '.' import TwakeServer from '..' import JEST_PROCESS_ROOT_PATH from '../../jest.globals' -import { allMatrixErrorCodes, type Collections, type Config } from '../types' +import { allMatrixErrorCodes, type Config } from '../types' import { addUser, buildUserDB, deleteUserDB } from './__testData__/build-userdb' import defaultConfig from './__testData__/config.json' import { TwakeRoom } from './models/room' @@ -326,10 +326,7 @@ describe('ApplicationServer', () => { }) expect(response.statusCode).toBe(200) expect(response.body).toEqual({}) - const rooms = await twakeServer.db?.getAll( - 'rooms' as unknown as Collections, - ['*'] - ) + const rooms = await twakeServer.db?.getAll('rooms', ['*']) expect(rooms).not.toBeUndefined() expect((rooms as DbGetResult).length).toEqual(1) const newRoom = (rooms as DbGetResult)[0] diff --git a/packages/tom-server/src/application-server/models/room.ts b/packages/tom-server/src/application-server/models/room.ts index b4fb11da..502178a2 100644 --- a/packages/tom-server/src/application-server/models/room.ts +++ b/packages/tom-server/src/application-server/models/room.ts @@ -1,6 +1,5 @@ import ldapjs from 'ldapjs' -import { type TwakeDB } from '../../db' -import { type Collections } from '../../types' +import { type TwakeDB } from '../../types' import { type ITwakeRoomModel } from '../types' const { EqualityFilter, OrFilter, SubstringFilter } = ldapjs @@ -11,7 +10,7 @@ export class TwakeRoom implements ITwakeRoomModel { ) {} public async saveRoom(db: TwakeDB): Promise { - await db.insert('rooms' as Collections, { + await db.insert('rooms', { id: this.id, filter: JSON.stringify(this.filter) }) @@ -21,7 +20,7 @@ export class TwakeRoom implements ITwakeRoomModel { db: TwakeDB, id: string ): Promise { - const roomsfromDb = (await db.get('rooms' as Collections, [], { + const roomsfromDb = (await db.get('rooms', [], { id })) as Array<{ id: string @@ -38,7 +37,7 @@ export class TwakeRoom implements ITwakeRoomModel { } static async getAllRooms(db: TwakeDB): Promise { - const roomsfromDb = (await db.getAll('rooms' as Collections, [])) as Array<{ + const roomsfromDb = (await db.getAll('rooms', [])) as Array<{ id: string filter: string }> @@ -54,12 +53,7 @@ export class TwakeRoom implements ITwakeRoomModel { db: TwakeDB, filter: Record ): Promise { - await db.update( - 'rooms' as Collections, - { filter: JSON.stringify(filter) }, - 'id', - this.id - ) + await db.update('rooms', { filter: JSON.stringify(filter) }, 'id', this.id) } public userDataMatchRoomFilter(user: any): boolean { diff --git a/packages/tom-server/src/application-server/types.ts b/packages/tom-server/src/application-server/types.ts index 3a89e77e..11f576b5 100644 --- a/packages/tom-server/src/application-server/types.ts +++ b/packages/tom-server/src/application-server/types.ts @@ -1,4 +1,4 @@ -import { type TwakeDB } from '../db' +import { type TwakeDB } from '../types' export interface ITwakeRoomModel { readonly id: string diff --git a/packages/tom-server/src/db/index.ts b/packages/tom-server/src/db/index.ts deleted file mode 100644 index 6b29d4ca..00000000 --- a/packages/tom-server/src/db/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type TwakeServer from '..' -import type { IdentityServerDb } from '../types' - -export type TwakeDB = IdentityServerDb - -// eslint-disable-next-line @typescript-eslint/promise-function-async -const initializeDb = (server: TwakeServer): Promise => { - return new Promise((resolve, reject) => { - switch (server.conf.database_engine) { - case 'sqlite': - case 'pg': - server.idServer.db - .createDatabases( - server.conf, - { - recoveryWords: 'userId text PRIMARY KEY, words TEXT', - matrixTokens: 'id varchar(64) PRIMARY KEY, data text', - privateNotes: - 'id varchar(64) PRIMARY KEY, authorId varchar(64), content text, targetId varchar(64)', - roomTags: - 'id varchar(64) PRIMARY KEY, authorId varchar(64), content text, roomId varchar(64)', - userQuotas: 'user_id varchar(64) PRIMARY KEY, size int', - rooms: 'id varchar(64) PRIMARY KEY, filter varchar(64)' - }, - {}, - {}, - server.logger - ) - .then(() => { - server.db = server.idServer.db // as TwakeDB - // @ts-expect-error matrixTokens isn't member of Collections - server.db.cleanByExpires.push('matrixTokens') - resolve() - }) - /* istanbul ignore next */ - .catch(reject) - break - default: - /* istanbul ignore next */ throw new Error( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `Unsupported DB type ${server.conf.database_engine}` - ) - } - }) -} - -export default initializeDb diff --git a/packages/tom-server/src/identity-server/__testData__/buildUserDB.ts b/packages/tom-server/src/identity-server/__testData__/buildUserDB.ts index 10561f2a..39540bb9 100644 --- a/packages/tom-server/src/identity-server/__testData__/buildUserDB.ts +++ b/packages/tom-server/src/identity-server/__testData__/buildUserDB.ts @@ -9,9 +9,10 @@ interface Config { let created = false - -const createQuery = 'CREATE TABLE users (uid varchar(8), mobile varchar(12), mail varchar(32), sn varchar(32))' -const insertQuery = "INSERT INTO users VALUES('dwho', '33612345678', 'dwho@example.com', 'Dwho')" +const createQuery = + 'CREATE TABLE users (uid varchar(8), mobile varchar(12), mail varchar(32), sn varchar(32))' +const insertQuery = + "INSERT INTO users VALUES('dwho', '33612345678', 'dwho@example.com', 'Dwho')" // eslint-disable-next-line @typescript-eslint/promise-function-async const buildUserDB = (conf: Config, recreate?: boolean): Promise => { @@ -19,67 +20,79 @@ const buildUserDB = (conf: Config, recreate?: boolean): Promise => { return new Promise((resolve, reject) => { if (conf.database_engine === 'sqlite') { const matrixDb = new sqlite3.Database(conf.matrix_database_host) - - matrixDb.run('CREATE TABLE users (name text, desactivated text, admin integer)', (err) => { - if (err != null) { - reject(err) - } else { - matrixDb.run("INSERT INTO users VALUES('@dwho:example.com', '', 0)", (err) => { - if (err != null) { - reject(err) - } else { - matrixDb.close((err) => { - /* istanbul ignore if */ - if(err != null) { - console.error(err) + + matrixDb.run( + 'CREATE TABLE users (name text, desactivated text, admin integer)', + (err) => { + if (err != null) { + reject(err) + } else { + matrixDb.run( + "INSERT INTO users VALUES('@dwho:example.com', '', 0)", + (err) => { + if (err != null) { reject(err) } else { - const userDb = new sqlite3.Database(conf.userdb_host) - userDb.run(createQuery, (err) => { + matrixDb.close((err) => { + /* istanbul ignore if */ if (err != null) { + console.error(err) reject(err) } else { - Promise.all( - // eslint-disable-next-line @typescript-eslint/promise-function-async - Array.from(Array(31).keys()).map((v: string | number) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error - // @ts-ignore v is first a number - if (v < 10) v = `0${v}` - return new Promise((_resolve, _reject) => { - userDb.run(`INSERT INTO users VALUES('user${v}', '', 'user${v}@example.com', 'User${v}')`, (err) => { - err != null ? _reject(err) : _resolve(true) - }) - }) - }) - ) - .then(() => { - userDb.run(insertQuery, (err) => { - if (err != null) { - reject(err) - } else { - userDb.close((err) => { - /* istanbul ignore if */ - if(err != null) { - console.error(err) + const userDb = new sqlite3.Database(conf.userdb_host) + userDb.run(createQuery, (err) => { + if (err != null) { + reject(err) + } else { + Promise.all( + // eslint-disable-next-line @typescript-eslint/promise-function-async + Array.from(Array(31).keys()).map( + (v: string | number) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error + // @ts-ignore v is first a number + if (v < 10) v = `0${v}` + return new Promise((_resolve, _reject) => { + userDb.run( + `INSERT INTO users VALUES('user${v}', '', 'user${v}@example.com', 'User${v}')`, + (err) => { + err != null + ? _reject(err) + : _resolve(true) + } + ) + }) + } + ) + ) + .then(() => { + userDb.run(insertQuery, (err) => { + if (err != null) { reject(err) } else { - created = true - resolve() + userDb.close((err) => { + /* istanbul ignore if */ + if (err != null) { + console.error(err) + reject(err) + } else { + created = true + resolve() + } + }) } }) - } - }) - }) - .catch(reject) + }) + .catch(reject) + } + }) } }) - } - }) - } - }) + } + } + ) + } } - }) - + ) } else { const userDb = new Pg.Client({ host: conf.userdb_host, @@ -87,14 +100,23 @@ const buildUserDB = (conf: Config, recreate?: boolean): Promise => { password: conf.userdb_password, database: conf.userdb_name }) - userDb.connect().then(() => { - console.error('CONNECT') - userDb.query(createQuery).then(() => { - userDb.query(insertQuery).then(() => { - resolve() - }).catch(reject) - }).catch(reject) - }).catch(reject) + userDb + .connect() + .then(() => { + console.error('CONNECT') + userDb + .query(createQuery) + .then(() => { + userDb + .query(insertQuery) + .then(() => { + resolve() + }) + .catch(reject) + }) + .catch(reject) + }) + .catch(reject) } }) } diff --git a/packages/tom-server/src/identity-server/__testData__/registerConf.json b/packages/tom-server/src/identity-server/__testData__/registerConf.json index bb56a62a..821a7da1 100644 --- a/packages/tom-server/src/identity-server/__testData__/registerConf.json +++ b/packages/tom-server/src/identity-server/__testData__/registerConf.json @@ -25,4 +25,4 @@ "userdb_host": "./src/identity-server/__testData__/users.test.db", "registration_file_path": "registration.yaml", "sender_localpart": "twake" -} \ No newline at end of file +} diff --git a/packages/tom-server/src/identity-server/__testData__/termsConf.json b/packages/tom-server/src/identity-server/__testData__/termsConf.json index b17cc8ea..5570e7ad 100644 --- a/packages/tom-server/src/identity-server/__testData__/termsConf.json +++ b/packages/tom-server/src/identity-server/__testData__/termsConf.json @@ -48,4 +48,4 @@ "userdb_host": "./src/identity-server/__testData__/terms.db", "registration_file_path": "registration.yaml", "sender_localpart": "twake" -} \ No newline at end of file +} diff --git a/packages/tom-server/src/identity-server/index.test.ts b/packages/tom-server/src/identity-server/index.test.ts index 4a7c876c..d7d9d7a6 100644 --- a/packages/tom-server/src/identity-server/index.test.ts +++ b/packages/tom-server/src/identity-server/index.test.ts @@ -1,5 +1,6 @@ import { Hash, supportedHashes } from '@twake/crypto' -import { Utils, updateUsers } from '@twake/matrix-identity-server' +import { updateUsers } from '@twake/matrix-identity-server' +import { epoch } from '@twake/utils' import express from 'express' import fs from 'fs' import fetch from 'node-fetch' @@ -12,7 +13,7 @@ import { type Config } from '../types' import buildUserDB from './__testData__/buildUserDB' import defaultConfig from './__testData__/registerConf.json' -const timestamp = Utils.epoch() +const timestamp = epoch() jest.mock('node-fetch', () => jest.fn()) const sendMailMock = jest.fn() @@ -130,11 +131,11 @@ describe('Using Matrix Token', () => { const mockResponse = Promise.resolve({ ok: true, status: 200, - json: () => { - return { + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ user_id: 'dwho' - } - } + }) }) // @ts-expect-error mock is unknown fetch.mockImplementation(async () => await mockResponse) @@ -417,11 +418,10 @@ describe('/_matrix/identity/v2/account/register', () => { const mockResponse = Promise.resolve({ ok: true, status: 200, - json: () => { - return { + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => Promise.resolve({ sub: '@dwho:example.com' - } - } +}) }) // @ts-expect-error mock is unknown fetch.mockImplementation(async () => await mockResponse) diff --git a/packages/tom-server/src/identity-server/index.ts b/packages/tom-server/src/identity-server/index.ts index d2f8f82e..67b24a88 100644 --- a/packages/tom-server/src/identity-server/index.ts +++ b/packages/tom-server/src/identity-server/index.ts @@ -7,14 +7,16 @@ import MatrixIdentityServer, { type MatrixDB } from '@twake/matrix-identity-server' import defaultConfig from '../config.json' -import { type Config } from '../types' +import type { Config, TwakeDB, twakeDbCollections } from '../types' +import { tables } from '../utils' import autocompletion from './lookup/autocompletion' import diff from './lookup/diff' import Authenticate from './utils/authenticate' export type { WhoAmIResponse } from './utils/authenticate' -export default class AugmentedIdentityServer extends MatrixIdentityServer { +export default class TwakeIdentityServer extends MatrixIdentityServer { + declare db: TwakeDB declare conf: Config constructor( public matrixDb: MatrixDB, @@ -24,7 +26,7 @@ export default class AugmentedIdentityServer extends MatrixIdentityServer { ) { // istanbul ignore if if (confDesc == null) confDesc = defaultConfig - super(conf, confDesc, logger) + super(conf, confDesc, logger, tables) this.authenticate = Authenticate(this.db, this.conf, this.logger) const superReady = this.ready this.ready = new Promise((resolve, reject) => { diff --git a/packages/tom-server/src/identity-server/lookup/_search.ts b/packages/tom-server/src/identity-server/lookup/_search.ts index 02a95c8f..dbd51788 100644 --- a/packages/tom-server/src/identity-server/lookup/_search.ts +++ b/packages/tom-server/src/identity-server/lookup/_search.ts @@ -1,8 +1,8 @@ import { type TwakeLogger } from '@twake/logger' -import { errMsg, Utils } from '@twake/matrix-identity-server' +import { errMsg, send, toMatrixId } from '@twake/utils' import { type Response } from 'express' import type http from 'http' -import type AugmentedIdentityServer from '..' +import type TwakeIdentityServer from '..' type SearchFunction = (res: Response | http.ServerResponse, data: Query) => void @@ -26,7 +26,7 @@ export const SearchFields = new Set([ ]) const _search = ( - idServer: AugmentedIdentityServer, + idServer: TwakeIdentityServer, logger: TwakeLogger ): SearchFunction => { return (res, data) => { @@ -34,7 +34,7 @@ const _search = ( /* istanbul ignore next */ logger.error('Autocompletion error', e) /* istanbul ignore next */ - Utils.send(res, 500, errMsg('unknown', e)) + send(res, 500, errMsg('unknown', e.toString())) } let fields = data.fields let scope = data.scope @@ -64,13 +64,13 @@ const _search = ( .then((rows) => { if (rows.length === 0) { /* istanbul ignore next */ - Utils.send(res, 200, { matches: [], inactive_matches: [] }) + send(res, 200, { matches: [], inactive_matches: [] }) } else { const start = data.offset ?? 0 const end = start + (data.limit ?? 30) rows = rows.slice(start, end) const mUid = rows.map((v) => { - return `@${v.uid as string}:${idServer.conf.server_name}` + return toMatrixId(v.uid as string, idServer.conf.server_name) }) /** * For the record, this can be replaced by a call to @@ -92,23 +92,24 @@ const _search = ( ] = true }) rows.forEach((row) => { - row.address = `@${row.uid as string}:${ + row.address = toMatrixId( + row.uid as string, idServer.conf.server_name - }` + ) if (mUids[row.uid as string]) { matches.push(row) } else { inactive_matches.push(row) } }) - Utils.send(res, 200, { matches, inactive_matches }) + send(res, 200, { matches, inactive_matches }) }) .catch(sendError) } }) .catch(sendError) } else { - Utils.send(res, 400, errMsg('invalidParam')) + send(res, 400, errMsg('invalidParam')) } } } diff --git a/packages/tom-server/src/identity-server/lookup/autocompletion.ts b/packages/tom-server/src/identity-server/lookup/autocompletion.ts index 04ca9ded..778c6176 100644 --- a/packages/tom-server/src/identity-server/lookup/autocompletion.ts +++ b/packages/tom-server/src/identity-server/lookup/autocompletion.ts @@ -1,8 +1,8 @@ import { type TwakeLogger } from '@twake/logger' -import { Utils } from '@twake/matrix-identity-server' +import { jsonContent, validateParameters } from '@twake/utils' import { type expressAppHandler } from '../../types' import _search, { type Query } from './_search' -import type AugmentedIdentityServer from '..' +import type TwakeIdentityServer from '..' const schema = { scope: true, @@ -13,14 +13,14 @@ const schema = { } const autocompletion = ( - idServer: AugmentedIdentityServer, + idServer: TwakeIdentityServer, logger: TwakeLogger ): expressAppHandler => { const search = _search(idServer, logger) return (req, res) => { idServer.authenticate(req, res, (token, id) => { - Utils.jsonContent(req, res, logger, (obj) => { - Utils.validateParameters(res, schema, obj, logger, (data) => { + jsonContent(req, res, logger, (obj) => { + validateParameters(res, schema, obj, logger, (data) => { search(res, data as Query) }) }) diff --git a/packages/tom-server/src/identity-server/lookup/diff.ts b/packages/tom-server/src/identity-server/lookup/diff.ts index 2a282f37..512dd83c 100644 --- a/packages/tom-server/src/identity-server/lookup/diff.ts +++ b/packages/tom-server/src/identity-server/lookup/diff.ts @@ -1,6 +1,12 @@ import { type TwakeLogger } from '@twake/logger' -import { Utils, errMsg } from '@twake/matrix-identity-server' -import type AugmentedIdentityServer from '..' +import { + epoch, + errMsg, + jsonContent, + validateParameters, + send +} from '@twake/utils' +import type TwakeIdentityServer from '..' import { type expressAppHandler } from '../../types' const schema = { @@ -18,7 +24,7 @@ interface DiffQueryBody { } const diff = ( - idServer: AugmentedIdentityServer, + idServer: TwakeIdentityServer, logger: TwakeLogger ): expressAppHandler => { return (req, res) => { @@ -26,12 +32,12 @@ const diff = ( /* istanbul ignore next */ logger.error('lookup/diff error', e) /* istanbul ignore next */ - Utils.send(res, 500, errMsg('unknown')) + send(res, 500, errMsg('unknown')) } idServer.authenticate(req, res, (token) => { - const timestamp = Utils.epoch() - Utils.jsonContent(req, res, logger, (obj) => { - Utils.validateParameters(res, schema, obj, logger, (data) => { + const timestamp = epoch() + jsonContent(req, res, logger, (obj) => { + validateParameters(res, schema, obj, logger, (data) => { idServer.db .getHigherThan( 'userHistory', @@ -70,7 +76,7 @@ const diff = ( }) const start = (data as DiffQueryBody).offset ?? 0 const end = start + ((data as DiffQueryBody).limit ?? 30) - Utils.send(res, 200, { + send(res, 200, { new: newUsers.slice(start, end), deleted: deleted.slice(start, end), timestamp diff --git a/packages/tom-server/src/identity-server/utils/authenticate.ts b/packages/tom-server/src/identity-server/utils/authenticate.ts index fca440f9..0735e411 100644 --- a/packages/tom-server/src/identity-server/utils/authenticate.ts +++ b/packages/tom-server/src/identity-server/utils/authenticate.ts @@ -1,11 +1,8 @@ import { type TwakeLogger } from '@twake/logger' -import { Utils, errMsg, type tokenContent } from '@twake/matrix-identity-server' +import { type tokenContent } from '@twake/matrix-identity-server' +import { epoch, errMsg, getAccessToken, send } from '@twake/utils' import fetch from 'node-fetch' -import { - type AuthenticationFunction, - type Config, - type IdentityServerDb -} from '../../types' +import type { AuthenticationFunction, Config, TwakeDB } from '../../types' export interface WhoAmIResponse { user_id?: string @@ -14,25 +11,13 @@ export interface WhoAmIResponse { } const Authenticate = ( - db: IdentityServerDb, + db: TwakeDB, conf: Config, logger: TwakeLogger ): AuthenticationFunction => { - const tokenRe = /^Bearer (\S+)$/ return (req, res, callback) => { - let token: string | null = null - if (req.headers?.authorization != null) { - const re = req.headers.authorization.match(tokenRe) - if (re != null) { - token = re[1] - } - // @ts-expect-error req.query exists - } else if (req.query != null) { - // @ts-expect-error req.query.access_token may be null - token = req.query.access_token - } + const token = getAccessToken(req) if (token != null) { - // @ts-expect-error matrixTokens not in Collections db.get('matrixTokens', ['data'], { id: token }) .then((rows) => { if (rows.length === 0) { @@ -59,10 +44,9 @@ const Authenticate = ( if (uid != null) { const data: tokenContent = { sub: uid, - epoch: Utils.epoch() + epoch: epoch() } // STORE - // @ts-expect-error recoveryWords not in Collections db.insert('matrixTokens', { // eslint-disable-next-line n/no-callback-literal // eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error @@ -79,19 +63,19 @@ const Authenticate = ( callback(data, token) } else { logger.warn('Bad token', userInfo) - Utils.send(res, 401, errMsg('unAuthorized')) + send(res, 401, errMsg('unAuthorized')) } }) .catch((e) => { /* istanbul ignore next */ logger.debug('Fetch error', e) /* istanbul ignore next */ - Utils.send(res, 401, errMsg('unAuthorized')) + send(res, 401, errMsg('unAuthorized')) }) }) } else { logger.warn('Access tried without token', req.headers) - Utils.send(res, 401, errMsg('unAuthorized')) + send(res, 401, errMsg('unAuthorized')) } } } diff --git a/packages/tom-server/src/identity-server/with-cache.test.ts b/packages/tom-server/src/identity-server/with-cache.test.ts index 2741f82a..ca8c9d78 100644 --- a/packages/tom-server/src/identity-server/with-cache.test.ts +++ b/packages/tom-server/src/identity-server/with-cache.test.ts @@ -119,11 +119,11 @@ describe('Using Matrix Token', () => { const mockResponse = Promise.resolve({ ok: true, status: 200, - json: () => { - return { + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ user_id: 'dwho' - } - } + }) }) // @ts-expect-error mock is unknown fetch.mockImplementation(async () => await mockResponse) @@ -255,11 +255,12 @@ describe('/_matrix/identity/v2/account/register', () => { const mockResponse = Promise.resolve({ ok: true, status: 200, - json: () => { - return { + // eslint-disable-next-line @typescript-eslint/promise-function-async + json: () => + Promise.resolve({ sub: '@dwho:example.com' - } - } +}) + }) // @ts-expect-error mock is unknown fetch.mockImplementation(async () => await mockResponse) diff --git a/packages/tom-server/src/index.ts b/packages/tom-server/src/index.ts index e608d961..676e1a66 100644 --- a/packages/tom-server/src/index.ts +++ b/packages/tom-server/src/index.ts @@ -9,7 +9,6 @@ import { Router } from 'express' import fs from 'fs' import AppServiceAPI from './application-server' import defaultConfig from './config.json' -import initializeDb, { type TwakeDB } from './db' import IdServer from './identity-server' import mutualRoomsAPIRouter from './mutual-rooms-api' import privateNoteApiRouter from './private-note-api' @@ -17,10 +16,11 @@ import roomTagsAPIRouter from './room-tags-api' import TwakeSearchEngine from './search-engine-api' import { type IOpenSearchRepository } from './search-engine-api/repositories/interfaces/opensearch-repository.interface' import smsApiRouter from './sms-api' -import type { Config, ConfigurationFile, TwakeIdentityServer } from './types' +import type { Config, ConfigurationFile, TwakeDB } from './types' import userInfoAPIRouter from './user-info-api' import VaultServer from './vault-api' import WellKnown from './wellKnown' +import ActiveContacts from './active-contacts-api' export default class TwakeServer { conf: Config @@ -30,7 +30,7 @@ export default class TwakeServer { matrixDb: MatrixDB private _openSearchClient: IOpenSearchRepository | undefined ready!: Promise - idServer!: TwakeIdentityServer + idServer!: IdServer constructor( conf?: Partial, @@ -103,19 +103,17 @@ export default class TwakeServer { private async _initServer(confDesc?: ConfigDescription): Promise { await this.idServer.ready + this.db = this.idServer.db + this.db.cleanByExpires.push('matrixTokens') this.logger.debug('idServer initialized') await this.matrixDb.ready this.logger.debug('Connected to Matrix DB') - await initializeDb(this) this.logger.debug('Main database initialized') - const vaultServer = new VaultServer( - this.idServer.db, - this.idServer.authenticate - ) + const vaultServer = new VaultServer(this.db, this.idServer.authenticate) const wellKnown = new WellKnown(this.conf) const privateNoteApi = privateNoteApiRouter( - this.idServer.db, + this.db, this.conf, this.idServer.authenticate, this.logger @@ -127,7 +125,7 @@ export default class TwakeServer { this.logger ) const roomTagsApi = roomTagsAPIRouter( - this.idServer.db, + this.db, this.matrixDb.db, this.conf, this.idServer.authenticate, @@ -141,19 +139,27 @@ export default class TwakeServer { this.logger ) + const activeContactsApi = ActiveContacts( + this.idServer.db, + this.conf, + this.idServer.authenticate, + this.logger + ) + this.endpoints.use(privateNoteApi) this.endpoints.use(mutualRoolsApi) this.endpoints.use(vaultServer.endpoints) this.endpoints.use(roomTagsApi) this.endpoints.use(userInfoApi) this.endpoints.use(smsApi) + this.endpoints.use(activeContactsApi) if ( this.conf.opensearch_is_activated != null && this.conf.opensearch_is_activated ) { const searchEngineApi = new TwakeSearchEngine( - this.idServer.db, + this.db, this.idServer.userDB, this.idServer.authenticate, this.matrixDb, diff --git a/packages/tom-server/src/mutual-rooms-api/tests/routes.test.ts b/packages/tom-server/src/mutual-rooms-api/tests/routes.test.ts index 8615d0a6..1204c947 100644 --- a/packages/tom-server/src/mutual-rooms-api/tests/routes.test.ts +++ b/packages/tom-server/src/mutual-rooms-api/tests/routes.test.ts @@ -35,7 +35,7 @@ const matrixDbMock = { } jest - .spyOn(IdentityServerDb.default.prototype, 'get') + .spyOn(IdentityServerDb.prototype, 'get') .mockResolvedValue([{ data: '"test"' }]) const idServer = new IdServer( diff --git a/packages/tom-server/src/private-note-api/controllers/index.ts b/packages/tom-server/src/private-note-api/controllers/index.ts index 25e24686..ad68a0a2 100644 --- a/packages/tom-server/src/private-note-api/controllers/index.ts +++ b/packages/tom-server/src/private-note-api/controllers/index.ts @@ -1,6 +1,6 @@ import { type NextFunction, type Request, type Response } from 'express' +import { type TwakeDB } from '../../types' import PrivateNoteService from '../services' -import type { TwakeDB } from '../../db' import type { IPrivateNoteApiController, IPrivateNoteService } from '../types' export default class PrivateNoteApiController diff --git a/packages/tom-server/src/private-note-api/middlewares/validation.middleware.ts b/packages/tom-server/src/private-note-api/middlewares/validation.middleware.ts index f9ebd58d..b396f338 100644 --- a/packages/tom-server/src/private-note-api/middlewares/validation.middleware.ts +++ b/packages/tom-server/src/private-note-api/middlewares/validation.middleware.ts @@ -1,7 +1,6 @@ import type { NextFunction, Response } from 'express' +import type { AuthRequest, TwakeDB } from '../../types' import type { IPrivateNoteApiValidationMiddleware, Note } from '../types' -import type { TwakeDB } from '../../db' -import type { Collections, AuthRequest } from '../../types' export default class PrivateNoteApiValidationMiddleware implements IPrivateNoteApiValidationMiddleware @@ -90,11 +89,9 @@ export default class PrivateNoteApiValidationMiddleware throw new Error('Missing required query parameters') } - const ExistingNotes = (await this.db.get( - 'PrivateNotes' as Collections, - ['authorId'], - { id } - )) as unknown as Note[] + const ExistingNotes = (await this.db.get('privateNotes', ['authorId'], { + id + })) as unknown as Note[] /* istanbul ignore if */ if (ExistingNotes.length === 0) { @@ -139,11 +136,9 @@ export default class PrivateNoteApiValidationMiddleware throw new Error('Bad Request') } - const existingNotes = (await this.db.get( - 'PrivateNotes' as Collections, - ['authorId'], - { id: itemId } - )) as unknown as Note[] + const existingNotes = (await this.db.get('privateNotes', ['authorId'], { + id: itemId + })) as unknown as Note[] /* istanbul ignore if */ if (existingNotes.length === 0) { diff --git a/packages/tom-server/src/private-note-api/routes/index.ts b/packages/tom-server/src/private-note-api/routes/index.ts index 7197594d..dca93a27 100644 --- a/packages/tom-server/src/private-note-api/routes/index.ts +++ b/packages/tom-server/src/private-note-api/routes/index.ts @@ -5,11 +5,7 @@ import { type TwakeLogger } from '@twake/logger' import { Router } from 'express' -import type { - AuthenticationFunction, - Config, - IdentityServerDb -} from '../../types' +import type { AuthenticationFunction, Config, TwakeDB } from '../../types' import authMiddleware from '../../utils/middlewares/auth.middleware' import PrivateNoteApiController from '../controllers' import PrivateNoteApiValidationMiddleware from '../middlewares/validation.middleware' @@ -17,7 +13,7 @@ import PrivateNoteApiValidationMiddleware from '../middlewares/validation.middle export const PATH = '/_twake/private_note' export default ( - db: IdentityServerDb, + db: TwakeDB, config: Config, authenticator: AuthenticationFunction, defaultLogger?: TwakeLogger diff --git a/packages/tom-server/src/private-note-api/services/index.ts b/packages/tom-server/src/private-note-api/services/index.ts index 09e00e7e..eefdddc7 100644 --- a/packages/tom-server/src/private-note-api/services/index.ts +++ b/packages/tom-server/src/private-note-api/services/index.ts @@ -1,6 +1,5 @@ -import type { Note, IPrivateNoteService } from '../types' -import type { TwakeDB } from '../../db' -import type { Collections } from '../../types' +import { type TwakeDB } from '../../types' +import type { IPrivateNoteService, Note } from '../types' class PrivateNoteService implements IPrivateNoteService { constructor(private readonly db: TwakeDB) {} @@ -16,11 +15,9 @@ class PrivateNoteService implements IPrivateNoteService { targetId: string ): Promise => { try { - const notes = (await this.db.get( - 'privateNotes' as Collections, - ['content'], - { authorId } - )) as unknown as Note[] + const notes = (await this.db.get('privateNotes', ['content'], { + authorId + })) as unknown as Note[] const note = notes.find((note) => note.targetId === targetId) @@ -48,11 +45,9 @@ class PrivateNoteService implements IPrivateNoteService { content: string ): Promise => { try { - const notes = (await this.db.get( - 'privateNotes' as Collections, - ['content'], - { authorId } - )) as unknown as Note[] + const notes = (await this.db.get('privateNotes', ['content'], { + authorId + })) as unknown as Note[] const existingNote = notes.find((note) => note.targetId === targetId) @@ -61,7 +56,7 @@ class PrivateNoteService implements IPrivateNoteService { throw new Error('Note already exists') } - await this.db.insert('privateNotes' as Collections, { + await this.db.insert('privateNotes', { authorId, targetId, content @@ -79,18 +74,14 @@ class PrivateNoteService implements IPrivateNoteService { */ public update = async (id: number, content: string): Promise => { try { - const existingNoteCount = await this.db.getCount( - 'privateNotes' as Collections, - 'id', - id - ) + const existingNoteCount = await this.db.getCount('privateNotes', 'id', id) /* istanbul ignore if */ if (existingNoteCount === 0) { throw new Error('Note not found') } - await this.db.update('privateNotes' as Collections, { content }, 'id', id) + await this.db.update('privateNotes', { content }, 'id', id) } catch (error) { throw new Error('Failed to update note', { cause: error }) } @@ -103,18 +94,14 @@ class PrivateNoteService implements IPrivateNoteService { */ public delete = async (id: number): Promise => { try { - const existingNoteCount = await this.db.getCount( - 'privateNotes' as Collections, - 'id', - id - ) + const existingNoteCount = await this.db.getCount('privateNotes', 'id', id) /* istanbul ignore if */ if (existingNoteCount === 0) { throw new Error('Note not found') } - await this.db.deleteEqual('privateNotes' as Collections, 'id', id) + await this.db.deleteEqual('privateNotes', 'id', id) } catch (error) { throw new Error('Failed to delete note', { cause: error }) } diff --git a/packages/tom-server/src/private-note-api/tests/controller.test.ts b/packages/tom-server/src/private-note-api/tests/controller.test.ts index 85a54ab8..deb8c921 100644 --- a/packages/tom-server/src/private-note-api/tests/controller.test.ts +++ b/packages/tom-server/src/private-note-api/tests/controller.test.ts @@ -2,7 +2,7 @@ import bodyParser from 'body-parser' import express, { type NextFunction, type Response } from 'express' import supertest from 'supertest' -import type { AuthRequest, Config, IdentityServerDb } from '../../types' +import type { AuthRequest, Config, TwakeDB } from '../../types' import router, { PATH } from '../routes' const app = express() @@ -42,9 +42,7 @@ jest.mock('../../private-note-api/middlewares/validation.middleware.ts', () => { app.use(bodyParser.json()) app.use(bodyParser.urlencoded({ extended: true })) -app.use( - router(dbMock as unknown as IdentityServerDb, {} as Config, authenticatorMock) -) +app.use(router(dbMock as unknown as TwakeDB, {} as Config, authenticatorMock)) describe('the private note controller', () => { it('should try to fetch a note', async () => { diff --git a/packages/tom-server/src/private-note-api/tests/router.test.ts b/packages/tom-server/src/private-note-api/tests/router.test.ts index 3ec61021..63e9ba11 100644 --- a/packages/tom-server/src/private-note-api/tests/router.test.ts +++ b/packages/tom-server/src/private-note-api/tests/router.test.ts @@ -24,7 +24,7 @@ const mockLogger: Partial = { } jest - .spyOn(IdentityServerDb.default.prototype, 'get') + .spyOn(IdentityServerDb.prototype, 'get') .mockResolvedValue([{ data: '"test"' }]) const idServer = new IdServer( diff --git a/packages/tom-server/src/private-note-api/tests/service.test.ts b/packages/tom-server/src/private-note-api/tests/service.test.ts index a9f42a18..d38b0e4e 100644 --- a/packages/tom-server/src/private-note-api/tests/service.test.ts +++ b/packages/tom-server/src/private-note-api/tests/service.test.ts @@ -1,4 +1,4 @@ -import { type IdentityServerDb } from '../../types' +import { type TwakeDB } from '../../types' import PrivateNoteService from '../services' import { type Note } from '../types' @@ -12,7 +12,7 @@ describe('the Private Note Service', () => { } const privateNoteServiceMock = new PrivateNoteService( - dbMock as unknown as IdentityServerDb + dbMock as unknown as TwakeDB ) it('should create a note', async () => { diff --git a/packages/tom-server/src/private-note-api/tests/validation.middleware.test.ts b/packages/tom-server/src/private-note-api/tests/validation.middleware.test.ts index 22f2d0ff..1af735c2 100644 --- a/packages/tom-server/src/private-note-api/tests/validation.middleware.test.ts +++ b/packages/tom-server/src/private-note-api/tests/validation.middleware.test.ts @@ -1,6 +1,6 @@ +import type { NextFunction, Response } from 'express' +import type { AuthRequest, TwakeDB } from '../../types' import PrivateNoteValidationMiddleware from '../middlewares/validation.middleware' -import type { Response, NextFunction } from 'express' -import type { IdentityServerDb, AuthRequest } from '../../types' describe('Validation middlewares', () => { let mockRequest: Partial @@ -16,7 +16,7 @@ describe('Validation middlewares', () => { } const privateNoteValidationMiddlewareMock = - new PrivateNoteValidationMiddleware(dbMock as unknown as IdentityServerDb) + new PrivateNoteValidationMiddleware(dbMock as unknown as TwakeDB) beforeEach(() => { mockRequest = {} diff --git a/packages/tom-server/src/room-tags-api/controllers/index.ts b/packages/tom-server/src/room-tags-api/controllers/index.ts index 3b27dfa5..5410c8d5 100644 --- a/packages/tom-server/src/room-tags-api/controllers/index.ts +++ b/packages/tom-server/src/room-tags-api/controllers/index.ts @@ -1,12 +1,12 @@ -import type { IRoomTagsController, IRoomTagsService } from '../types' -import type { AuthRequest, IdentityServerDb } from '../../types' +import type { NextFunction, Response } from 'express' +import type { AuthRequest, TwakeDB } from '../../types' import RoomTagsService from '../services' -import type { Response, NextFunction } from 'express' +import type { IRoomTagsController, IRoomTagsService } from '../types' class RoomTagsController implements IRoomTagsController { readonly roomTagsService: IRoomTagsService - constructor(private readonly db: IdentityServerDb) { + constructor(private readonly db: TwakeDB) { this.roomTagsService = new RoomTagsService(this.db) } diff --git a/packages/tom-server/src/room-tags-api/middlewares/index.ts b/packages/tom-server/src/room-tags-api/middlewares/index.ts index b786fe21..ff58f257 100644 --- a/packages/tom-server/src/room-tags-api/middlewares/index.ts +++ b/packages/tom-server/src/room-tags-api/middlewares/index.ts @@ -1,12 +1,12 @@ import type { MatrixDBBackend } from '@twake/matrix-identity-server' +import type { NextFunction, Response } from 'express' +import type { AuthRequest, TwakeDB } from '../../types' import type { IRoomTagsMiddleware } from '../types' -import type { Response, NextFunction } from 'express' -import type { IdentityServerDb, AuthRequest } from '../../types' import { isMemberOfRoom, userRoomTagExists } from '../utils' class RoomTagsMiddleware implements IRoomTagsMiddleware { constructor( - private readonly idDb: IdentityServerDb, + private readonly twakeDb: TwakeDB, private readonly matrixDb: MatrixDBBackend ) {} @@ -78,7 +78,7 @@ class RoomTagsMiddleware implements IRoomTagsMiddleware { throw new Error('invalid tags') } - if (await userRoomTagExists(this.idDb, userId, roomId)) { + if (await userRoomTagExists(this.twakeDb, userId, roomId)) { throw new Error('user already has a tag for this room') } @@ -131,7 +131,7 @@ class RoomTagsMiddleware implements IRoomTagsMiddleware { return } - if (!(await userRoomTagExists(this.idDb, userId, roomId))) { + if (!(await userRoomTagExists(this.twakeDb, userId, roomId))) { throw new Error('user tag for this room does not exist') } @@ -167,7 +167,7 @@ class RoomTagsMiddleware implements IRoomTagsMiddleware { throw new Error('user_id is required') } - if (!(await userRoomTagExists(this.idDb, userId, roomId))) { + if (!(await userRoomTagExists(this.twakeDb, userId, roomId))) { throw new Error('user tag for this room does not exist') } diff --git a/packages/tom-server/src/room-tags-api/routes/index.ts b/packages/tom-server/src/room-tags-api/routes/index.ts index da688706..964cb011 100644 --- a/packages/tom-server/src/room-tags-api/routes/index.ts +++ b/packages/tom-server/src/room-tags-api/routes/index.ts @@ -6,11 +6,7 @@ import { } from '@twake/logger' import type { MatrixDBBackend } from '@twake/matrix-identity-server' import { Router } from 'express' -import type { - AuthenticationFunction, - Config, - IdentityServerDb -} from '../../types' +import type { AuthenticationFunction, Config, TwakeDB } from '../../types' import authMiddleware from '../../utils/middlewares/auth.middleware' import RoomTagsController from '../controllers' import RoomTagsMiddleware from '../middlewares' @@ -18,7 +14,7 @@ import RoomTagsMiddleware from '../middlewares' export const PATH = '/_twake/v1/room_tags' export default ( - db: IdentityServerDb, + db: TwakeDB, maxtrixDb: MatrixDBBackend, config: Config, authenticator: AuthenticationFunction, diff --git a/packages/tom-server/src/room-tags-api/services/index.ts b/packages/tom-server/src/room-tags-api/services/index.ts index 6383247e..552902e9 100644 --- a/packages/tom-server/src/room-tags-api/services/index.ts +++ b/packages/tom-server/src/room-tags-api/services/index.ts @@ -1,9 +1,9 @@ -import type { IdentityServerDb } from '../../types' +import { type TwakeDB } from '../../types' import type { IRoomTagsService, RoomTag } from '../types' import { getRoomTagId, userRoomTagExists } from '../utils' class RoomTagsService implements IRoomTagsService { - constructor(private readonly db: IdentityServerDb) {} + constructor(private readonly db: TwakeDB) {} /** * Fetches the tags of a room. diff --git a/packages/tom-server/src/room-tags-api/tests/controller.test.ts b/packages/tom-server/src/room-tags-api/tests/controller.test.ts index 5a10cd56..27ecfaff 100644 --- a/packages/tom-server/src/room-tags-api/tests/controller.test.ts +++ b/packages/tom-server/src/room-tags-api/tests/controller.test.ts @@ -3,7 +3,7 @@ import type { MatrixDBBackend } from '@twake/matrix-identity-server' import bodyParser from 'body-parser' import express, { type NextFunction, type Response } from 'express' import supertest from 'supertest' -import type { AuthRequest, Config, IdentityServerDb } from '../../types' +import type { AuthRequest, Config, TwakeDB } from '../../types' import router, { PATH } from '../routes' const app = express() @@ -47,7 +47,7 @@ app.use(bodyParser.json()) app.use(bodyParser.urlencoded({ extended: true })) app.use( router( - dbMock as unknown as IdentityServerDb, + dbMock as unknown as TwakeDB, matrixDbMock as unknown as MatrixDBBackend, {} as Config, authenticatorMock diff --git a/packages/tom-server/src/room-tags-api/tests/middleware.test.ts b/packages/tom-server/src/room-tags-api/tests/middleware.test.ts index 65e3b1ca..fb17a8fb 100644 --- a/packages/tom-server/src/room-tags-api/tests/middleware.test.ts +++ b/packages/tom-server/src/room-tags-api/tests/middleware.test.ts @@ -1,7 +1,7 @@ import type { MatrixDBBackend } from '@twake/matrix-identity-server' +import type { NextFunction, Response } from 'express' +import type { AuthRequest, TwakeDB } from '../../types' import RoomTagsMiddleware from '../middlewares' -import type { AuthRequest, IdentityServerDb } from '../../types' -import type { Response, NextFunction } from 'express' describe('the room tags API middleware', () => { let mockRequest: Partial @@ -21,7 +21,7 @@ describe('the room tags API middleware', () => { } const roomTagsMiddlewareMock = new RoomTagsMiddleware( - dbMock as unknown as IdentityServerDb, + dbMock as unknown as TwakeDB, matrixDbMock as unknown as MatrixDBBackend ) diff --git a/packages/tom-server/src/room-tags-api/tests/router.test.ts b/packages/tom-server/src/room-tags-api/tests/router.test.ts index c674e0fc..98189f54 100644 --- a/packages/tom-server/src/room-tags-api/tests/router.test.ts +++ b/packages/tom-server/src/room-tags-api/tests/router.test.ts @@ -29,7 +29,7 @@ const middlewareSpy = jest.fn().mockImplementation((_req, _res, next) => { }) jest - .spyOn(IdentityServerDb.default.prototype, 'get') + .spyOn(IdentityServerDb.prototype, 'get') .mockResolvedValue([{ data: '"test"' }]) const idServer = new IdServer( diff --git a/packages/tom-server/src/room-tags-api/tests/service.test.ts b/packages/tom-server/src/room-tags-api/tests/service.test.ts index c6b891e6..2e4581b9 100644 --- a/packages/tom-server/src/room-tags-api/tests/service.test.ts +++ b/packages/tom-server/src/room-tags-api/tests/service.test.ts @@ -1,4 +1,4 @@ -import type { IdentityServerDb } from '../../types' +import { type TwakeDB } from '../../types' import RoomTagsService from '../services' describe('the room tags API service', () => { @@ -10,9 +10,7 @@ describe('the room tags API service', () => { getCount: jest.fn() } - const roomTagsServiceMock = new RoomTagsService( - dbMock as unknown as IdentityServerDb - ) + const roomTagsServiceMock = new RoomTagsService(dbMock as unknown as TwakeDB) it('should get a room tag', async () => { const roomTag = { diff --git a/packages/tom-server/src/room-tags-api/utils.ts b/packages/tom-server/src/room-tags-api/utils.ts index 57043a54..d3938655 100644 --- a/packages/tom-server/src/room-tags-api/utils.ts +++ b/packages/tom-server/src/room-tags-api/utils.ts @@ -1,6 +1,6 @@ import type { MatrixDBBackend } from '@twake/matrix-identity-server' +import { type TwakeDB } from '../types' import type { RoomMembership, RoomTag } from './types' -import type { IdentityServerDb } from '../types' /** * checks whether a user is a member of a room @@ -25,13 +25,13 @@ export const isMemberOfRoom = async ( /** * Checks whether a user has a tag for a room * - * @param {IdentityServerDb} db - the identity server database + * @param {TwakeDB} db - the tom server database * @param {string} userId - the user id * @param {string} roomId - the room id * @returns {Promise} */ export const userRoomTagExists = async ( - db: IdentityServerDb, + db: TwakeDB, userId: string, roomId: string ): Promise => { @@ -41,13 +41,13 @@ export const userRoomTagExists = async ( /** * Returns the id for a room tag * - * @param {IdentityServerDb} db - the identity server database database + * @param {TwakeDB} db - the identity server database database * @param {string} userId - the user id * @param {string} roomId - the room id * @returns {string | null} */ export const getRoomTagId = async ( - db: IdentityServerDb, + db: TwakeDB, userId: string, roomId: string ): Promise => { diff --git a/packages/tom-server/src/search-engine-api/__testData__/config.json b/packages/tom-server/src/search-engine-api/__testData__/config.json index 1fdee6f8..0d14e06f 100644 --- a/packages/tom-server/src/search-engine-api/__testData__/config.json +++ b/packages/tom-server/src/search-engine-api/__testData__/config.json @@ -23,4 +23,4 @@ "registration_file_path": "./src/search-engine-api/__testData__/synapse-data/registration.yaml", "server_name": "example.com", "userdb_engine": "ldap" -} \ No newline at end of file +} diff --git a/packages/tom-server/src/search-engine-api/__testData__/docker-compose.yml b/packages/tom-server/src/search-engine-api/__testData__/docker-compose.yml index 0aee22da..ae10d0a2 100644 --- a/packages/tom-server/src/search-engine-api/__testData__/docker-compose.yml +++ b/packages/tom-server/src/search-engine-api/__testData__/docker-compose.yml @@ -34,19 +34,19 @@ services: - ./nginx/ssl/9da13359.0:/etc/ssl/certs/9da13359.0 depends_on: - auth - environment: + environment: - UID=${MYUID} - VIRTUAL_PORT=8008 - VIRTUAL_HOST=matrix.example.com healthcheck: - test: ["CMD", "curl", "-fSs", "http://localhost:8008/health"] + test: ['CMD', 'curl', '-fSs', 'http://localhost:8008/health'] interval: 10s timeout: 10s retries: 3 networks: - twake_chat extra_hosts: - - "host.docker.internal:host-gateway" + - 'host.docker.internal:host-gateway' auth: image: yadd/lemonldap-ng-portal:2.16.1-bullseye @@ -72,7 +72,7 @@ services: - 21390:389 networks: - twake_chat - + # opensearchdashboard: # image: opensearchproject/opensearch-dashboards # ports: @@ -84,7 +84,7 @@ services: # - DISABLE_SECURITY_DASHBOARDS_PLUGIN=true # networks: # - twake_chat - + nginx-proxy: image: nginxproxy/nginx-proxy container_name: nginx-proxy-tom diff --git a/packages/tom-server/src/search-engine-api/__testData__/images/anakin-at-the-office.jpg b/packages/tom-server/src/search-engine-api/__testData__/images/anakin-at-the-office.jpg index f3b29041..ceb0d4c8 100644 Binary files a/packages/tom-server/src/search-engine-api/__testData__/images/anakin-at-the-office.jpg and b/packages/tom-server/src/search-engine-api/__testData__/images/anakin-at-the-office.jpg differ diff --git a/packages/tom-server/src/search-engine-api/__testData__/ldap/Dockerfile b/packages/tom-server/src/search-engine-api/__testData__/ldap/Dockerfile index 83adb618..86d55944 100644 --- a/packages/tom-server/src/search-engine-api/__testData__/ldap/Dockerfile +++ b/packages/tom-server/src/search-engine-api/__testData__/ldap/Dockerfile @@ -3,7 +3,7 @@ LABEL maintainer Linagora ENV DEBIAN_FRONTEND=noninteractive -# Update system and install dependencies +# Update system and install dependencies RUN apt-get update && \ apt-get install -y --no-install-recommends \ apt-transport-https \ @@ -16,7 +16,7 @@ RUN apt-get update && \ apt-get update && \ apt-get install -y openldap-ltb openldap-ltb-contrib-overlays openldap-ltb-mdb-utils ldap-utils && \ apt-get clean && \ - rm -rf /var/lib/apt/lists/* + rm -rf /var/lib/apt/lists/* # Copy configuration files COPY ./ldif/config-20230322180123.ldif /var/backups/openldap/ diff --git a/packages/tom-server/src/search-engine-api/__testData__/llng/lmConf-1.json b/packages/tom-server/src/search-engine-api/__testData__/llng/lmConf-1.json index 1b56b4b3..1aab9e37 100644 --- a/packages/tom-server/src/search-engine-api/__testData__/llng/lmConf-1.json +++ b/packages/tom-server/src/search-engine-api/__testData__/llng/lmConf-1.json @@ -1,237 +1,237 @@ { - "ADPwdExpireWarning": 0, - "ADPwdMaxAge": 0, - "SMTPServer": "", - "SMTPTLS": "", - "SSLAuthnLevel": 5, - "SSLIssuerVar": "SSL_CLIENT_I_DN", - "SSLVar": "SSL_CLIENT_S_DN_Email", - "SSLVarIf": {}, - "activeTimer": 1, - "apacheAuthnLevel": 3, - "applicationList": {}, - "authChoiceParam": "lmAuth", - "authentication": "LDAP", - "available2F": "UTOTP,TOTP,U2F,REST,Mail2F,Ext2F,WebAuthn,Yubikey,Radius,Password", - "available2FSelfRegistration": "Password,TOTP,U2F,WebAuthn,Yubikey", - "bruteForceProtectionLockTimes": "15, 30, 60, 300, 600", - "bruteForceProtectionMaxAge": 300, - "bruteForceProtectionMaxFailed": 3, - "bruteForceProtectionMaxLockTime": 900, - "bruteForceProtectionTempo": 30, - "captcha_mail_enabled": 1, - "captcha_register_enabled": 1, - "captcha_size": 6, - "casAccessControlPolicy": "none", - "casAuthnLevel": 1, - "casTicketExpiration": 0, - "certificateResetByMailCeaAttribute": "description", - "certificateResetByMailCertificateAttribute": "userCertificate;binary", - "certificateResetByMailURL": "https://auth.example.com/certificateReset", - "certificateResetByMailValidityDelay": 0, - "cfgAuthor": "The LemonLDAP::NG team", - "cfgDate": "1627287638", - "cfgNum": "1", - "cfgVersion": "2.0.16", - "checkDevOpsCheckSessionAttributes": 1, - "checkDevOpsDisplayNormalizedHeaders": 1, - "checkDevOpsDownload": 1, - "checkHIBPRequired": 1, - "checkHIBPURL": "https://api.pwnedpasswords.com/range/", - "checkTime": 600, - "checkUserDisplayComputedSession": 1, - "checkUserDisplayEmptyHeaders": 0, - "checkUserDisplayEmptyValues": 0, - "checkUserDisplayHiddenAttributes": 0, - "checkUserDisplayHistory": 0, - "checkUserDisplayNormalizedHeaders": 0, - "checkUserDisplayPersistentInfo": 0, - "checkUserHiddenAttributes": "_loginHistory, _session_id, hGroups", - "checkUserIdRule": 1, - "checkXSS": 1, - "confirmFormMethod": "post", - "contextSwitchingIdRule": 1, - "contextSwitchingPrefix": "switching", - "contextSwitchingRule": 0, - "contextSwitchingStopWithLogout": 1, - "cookieName": "lemonldap", - "corsAllow_Credentials": "true", - "corsAllow_Headers": "*", - "corsAllow_Methods": "POST,GET", - "corsAllow_Origin": "*", - "corsEnabled": 1, - "corsExpose_Headers": "*", - "corsMax_Age": "86400", - "crowdsecAction": "reject", - "cspConnect": "'self'", - "cspDefault": "'self'", - "cspFont": "'self'", - "cspFormAction": "*", - "cspFrameAncestors": "", - "cspImg": "'self' data:", - "cspScript": "'self'", - "cspStyle": "'self'", - "dbiAuthnLevel": 2, - "dbiExportedVars": {}, - "decryptValueRule": 0, - "demoExportedVars": { - "cn": "cn", - "mail": "mail", - "uid": "uid" - }, - "displaySessionId": 1, - "domain": "example.com", - "exportedHeaders": {}, - "exportedVars": {}, - "ext2fActivation": 0, - "ext2fCodeActivation": "\\d{6}", - "facebookAuthnLevel": 1, - "facebookExportedVars": {}, - "facebookUserField": "id", - "failedLoginNumber": 5, - "findUserControl": "^[*\\w]+$", - "findUserWildcard": "*", - "formTimeout": 120, - "githubAuthnLevel": 1, - "githubScope": "user:email", - "githubUserField": "login", - "globalLogoutRule": 0, - "globalLogoutTimer": 1, - "globalStorage": "Apache::Session::File", - "globalStorageOptions": { - "Directory": "/var/lib/lemonldap-ng/sessions", - "LockDirectory": "/var/lib/lemonldap-ng/sessions/lock", - "generateModule": "Lemonldap::NG::Common::Apache::Session::Generate::SHA256" - }, - "gpgAuthnLevel": 5, - "gpgDb": "", - "grantSessionRules": {}, - "groups": {}, - "handlerInternalCache": 15, - "handlerServiceTokenTTL": 30, - "hiddenAttributes": "_password, _2fDevices", - "httpOnly": 1, - "https": -1, - "impersonationHiddenAttributes": "_2fDevices, _loginHistory", - "impersonationIdRule": 1, - "impersonationMergeSSOgroups": 0, - "impersonationPrefix": "real_", - "impersonationRule": 0, - "impersonationSkipEmptyValues": 1, - "infoFormMethod": "get", - "issuerDBCASPath": "^/cas/", - "issuerDBCASRule": 1, - "issuerDBGetParameters": {}, - "issuerDBGetPath": "^/get/", - "issuerDBGetRule": 1, - "issuerDBOpenIDConnectActivation": 1, - "issuerDBOpenIDConnectPath": "^/oauth2/", - "issuerDBOpenIDConnectRule": 1, - "issuerDBOpenIDPath": "^/openidserver/", - "issuerDBOpenIDRule": 1, - "issuerDBSAMLPath": "^/saml/", - "issuerDBSAMLRule": 1, - "issuersTimeout": 120, - "jsRedirect": 0, - "key": "^vmTGvh{+]5!ToB?", - "krbAuthnLevel": 3, - "krbRemoveDomain": 1, - "ldapServer": "annuaire", - "ldapAuthnLevel": 2, - "ldapBase": "dc=example,dc=com", - "ldapExportedVars": { - "cn": "cn", - "mail": "mail", - "uid": "uid" - }, - "ldapGroupAttributeName": "member", - "ldapGroupAttributeNameGroup": "dn", - "ldapGroupAttributeNameSearch": "cn", - "ldapGroupAttributeNameUser": "dn", - "ldapGroupObjectClass": "groupOfNames", - "ldapIOTimeout": 10, - "ldapPasswordResetAttribute": "pwdReset", - "ldapPasswordResetAttributeValue": "TRUE", - "ldapPwdEnc": "utf-8", - "ldapSearchDeref": "find", - "ldapTimeout": 10, - "ldapUsePasswordResetAttribute": 1, - "ldapVerify": "require", - "ldapVersion": 3, - "linkedInAuthnLevel": 1, - "linkedInFields": "id,first-name,last-name,email-address", - "linkedInScope": "r_liteprofile r_emailaddress", - "linkedInUserField": "emailAddress", - "localSessionStorage": "Cache::FileCache", - "localSessionStorageOptions": { - "cache_depth": 3, - "cache_root": "/var/lib/lemonldap-ng/cache", - "default_expires_in": 600, - "directory_umask": "007", - "namespace": "lemonldap-ng-sessions" - }, - "locationDetectGeoIpLanguages": "en, fr", - "locationRules": { - "auth.example.com": { - "(?#checkUser)^/checkuser": "inGroup(\"timelords\")", - "(?#errors)^/lmerror/": "accept", - "default": "accept" - } - }, - "loginHistoryEnabled": 1, - "logoutServices": {}, - "macros": { - "UA": "$ENV{HTTP_USER_AGENT}", - "_whatToTrace": "$_auth eq 'SAML' ? lc($_user.'@'.$_idpConfKey) : $_auth eq 'OpenIDConnect' ? lc($_user.'@'.$_oidc_OP) : lc($_user)" - }, - "mail2fActivation": 0, - "mail2fCodeRegex": "\\d{6}", - "mailCharset": "utf-8", - "mailFrom": "noreply@example.com", - "mailSessionKey": "mail", - "mailTimeout": 0, - "mailUrl": "https://auth.example.com/resetpwd", - "managerDn": "", - "managerPassword": "", - "max2FDevices": 10, - "max2FDevicesNameLength": 20, - "multiValuesSeparator": "; ", - "mySessionAuthorizedRWKeys": [ - "_appsListOrder", - "_oidcConnectedRP", - "_oidcConsents" - ], - "newLocationWarningLocationAttribute": "ipAddr", - "newLocationWarningLocationDisplayAttribute": "", - "newLocationWarningMaxValues": "0", - "notification": 0, - "notificationDefaultCond": "", - "notificationServerPOST": 1, - "notificationServerSentAttributes": "uid reference date title subtitle text check", - "notificationStorage": "File", - "notificationStorageOptions": { - "dirName": "/var/lib/lemonldap-ng/notifications" - }, - "notificationWildcard": "allusers", - "notificationsMaxRetrieve": 3, - "notifyDeleted": 1, - "nullAuthnLevel": 0, - "oidcAuthnLevel": 1, - "oidcOPMetaDataExportedVars": {}, - "oidcOPMetaDataJSON": {}, - "oidcOPMetaDataJWKS": {}, - "oidcOPMetaDataOptions": {}, - "oidcRPCallbackGetParam": "openidconnectcallback", - "oidcRPMetaDataExportedVars": { - "matrix0": { - "email": "mail", - "family_name": "cn", - "given_name": "cn", - "name": "cn", - "nickname": "uid", - "preferred_username": "uid" - }, - "matrix1": { + "ADPwdExpireWarning": 0, + "ADPwdMaxAge": 0, + "SMTPServer": "", + "SMTPTLS": "", + "SSLAuthnLevel": 5, + "SSLIssuerVar": "SSL_CLIENT_I_DN", + "SSLVar": "SSL_CLIENT_S_DN_Email", + "SSLVarIf": {}, + "activeTimer": 1, + "apacheAuthnLevel": 3, + "applicationList": {}, + "authChoiceParam": "lmAuth", + "authentication": "LDAP", + "available2F": "UTOTP,TOTP,U2F,REST,Mail2F,Ext2F,WebAuthn,Yubikey,Radius,Password", + "available2FSelfRegistration": "Password,TOTP,U2F,WebAuthn,Yubikey", + "bruteForceProtectionLockTimes": "15, 30, 60, 300, 600", + "bruteForceProtectionMaxAge": 300, + "bruteForceProtectionMaxFailed": 3, + "bruteForceProtectionMaxLockTime": 900, + "bruteForceProtectionTempo": 30, + "captcha_mail_enabled": 1, + "captcha_register_enabled": 1, + "captcha_size": 6, + "casAccessControlPolicy": "none", + "casAuthnLevel": 1, + "casTicketExpiration": 0, + "certificateResetByMailCeaAttribute": "description", + "certificateResetByMailCertificateAttribute": "userCertificate;binary", + "certificateResetByMailURL": "https://auth.example.com/certificateReset", + "certificateResetByMailValidityDelay": 0, + "cfgAuthor": "The LemonLDAP::NG team", + "cfgDate": "1627287638", + "cfgNum": "1", + "cfgVersion": "2.0.16", + "checkDevOpsCheckSessionAttributes": 1, + "checkDevOpsDisplayNormalizedHeaders": 1, + "checkDevOpsDownload": 1, + "checkHIBPRequired": 1, + "checkHIBPURL": "https://api.pwnedpasswords.com/range/", + "checkTime": 600, + "checkUserDisplayComputedSession": 1, + "checkUserDisplayEmptyHeaders": 0, + "checkUserDisplayEmptyValues": 0, + "checkUserDisplayHiddenAttributes": 0, + "checkUserDisplayHistory": 0, + "checkUserDisplayNormalizedHeaders": 0, + "checkUserDisplayPersistentInfo": 0, + "checkUserHiddenAttributes": "_loginHistory, _session_id, hGroups", + "checkUserIdRule": 1, + "checkXSS": 1, + "confirmFormMethod": "post", + "contextSwitchingIdRule": 1, + "contextSwitchingPrefix": "switching", + "contextSwitchingRule": 0, + "contextSwitchingStopWithLogout": 1, + "cookieName": "lemonldap", + "corsAllow_Credentials": "true", + "corsAllow_Headers": "*", + "corsAllow_Methods": "POST,GET", + "corsAllow_Origin": "*", + "corsEnabled": 1, + "corsExpose_Headers": "*", + "corsMax_Age": "86400", + "crowdsecAction": "reject", + "cspConnect": "'self'", + "cspDefault": "'self'", + "cspFont": "'self'", + "cspFormAction": "*", + "cspFrameAncestors": "", + "cspImg": "'self' data:", + "cspScript": "'self'", + "cspStyle": "'self'", + "dbiAuthnLevel": 2, + "dbiExportedVars": {}, + "decryptValueRule": 0, + "demoExportedVars": { + "cn": "cn", + "mail": "mail", + "uid": "uid" + }, + "displaySessionId": 1, + "domain": "example.com", + "exportedHeaders": {}, + "exportedVars": {}, + "ext2fActivation": 0, + "ext2fCodeActivation": "\\d{6}", + "facebookAuthnLevel": 1, + "facebookExportedVars": {}, + "facebookUserField": "id", + "failedLoginNumber": 5, + "findUserControl": "^[*\\w]+$", + "findUserWildcard": "*", + "formTimeout": 120, + "githubAuthnLevel": 1, + "githubScope": "user:email", + "githubUserField": "login", + "globalLogoutRule": 0, + "globalLogoutTimer": 1, + "globalStorage": "Apache::Session::File", + "globalStorageOptions": { + "Directory": "/var/lib/lemonldap-ng/sessions", + "LockDirectory": "/var/lib/lemonldap-ng/sessions/lock", + "generateModule": "Lemonldap::NG::Common::Apache::Session::Generate::SHA256" + }, + "gpgAuthnLevel": 5, + "gpgDb": "", + "grantSessionRules": {}, + "groups": {}, + "handlerInternalCache": 15, + "handlerServiceTokenTTL": 30, + "hiddenAttributes": "_password, _2fDevices", + "httpOnly": 1, + "https": -1, + "impersonationHiddenAttributes": "_2fDevices, _loginHistory", + "impersonationIdRule": 1, + "impersonationMergeSSOgroups": 0, + "impersonationPrefix": "real_", + "impersonationRule": 0, + "impersonationSkipEmptyValues": 1, + "infoFormMethod": "get", + "issuerDBCASPath": "^/cas/", + "issuerDBCASRule": 1, + "issuerDBGetParameters": {}, + "issuerDBGetPath": "^/get/", + "issuerDBGetRule": 1, + "issuerDBOpenIDConnectActivation": 1, + "issuerDBOpenIDConnectPath": "^/oauth2/", + "issuerDBOpenIDConnectRule": 1, + "issuerDBOpenIDPath": "^/openidserver/", + "issuerDBOpenIDRule": 1, + "issuerDBSAMLPath": "^/saml/", + "issuerDBSAMLRule": 1, + "issuersTimeout": 120, + "jsRedirect": 0, + "key": "^vmTGvh{+]5!ToB?", + "krbAuthnLevel": 3, + "krbRemoveDomain": 1, + "ldapServer": "annuaire", + "ldapAuthnLevel": 2, + "ldapBase": "dc=example,dc=com", + "ldapExportedVars": { + "cn": "cn", + "mail": "mail", + "uid": "uid" + }, + "ldapGroupAttributeName": "member", + "ldapGroupAttributeNameGroup": "dn", + "ldapGroupAttributeNameSearch": "cn", + "ldapGroupAttributeNameUser": "dn", + "ldapGroupObjectClass": "groupOfNames", + "ldapIOTimeout": 10, + "ldapPasswordResetAttribute": "pwdReset", + "ldapPasswordResetAttributeValue": "TRUE", + "ldapPwdEnc": "utf-8", + "ldapSearchDeref": "find", + "ldapTimeout": 10, + "ldapUsePasswordResetAttribute": 1, + "ldapVerify": "require", + "ldapVersion": 3, + "linkedInAuthnLevel": 1, + "linkedInFields": "id,first-name,last-name,email-address", + "linkedInScope": "r_liteprofile r_emailaddress", + "linkedInUserField": "emailAddress", + "localSessionStorage": "Cache::FileCache", + "localSessionStorageOptions": { + "cache_depth": 3, + "cache_root": "/var/lib/lemonldap-ng/cache", + "default_expires_in": 600, + "directory_umask": "007", + "namespace": "lemonldap-ng-sessions" + }, + "locationDetectGeoIpLanguages": "en, fr", + "locationRules": { + "auth.example.com": { + "(?#checkUser)^/checkuser": "inGroup(\"timelords\")", + "(?#errors)^/lmerror/": "accept", + "default": "accept" + } + }, + "loginHistoryEnabled": 1, + "logoutServices": {}, + "macros": { + "UA": "$ENV{HTTP_USER_AGENT}", + "_whatToTrace": "$_auth eq 'SAML' ? lc($_user.'@'.$_idpConfKey) : $_auth eq 'OpenIDConnect' ? lc($_user.'@'.$_oidc_OP) : lc($_user)" + }, + "mail2fActivation": 0, + "mail2fCodeRegex": "\\d{6}", + "mailCharset": "utf-8", + "mailFrom": "noreply@example.com", + "mailSessionKey": "mail", + "mailTimeout": 0, + "mailUrl": "https://auth.example.com/resetpwd", + "managerDn": "", + "managerPassword": "", + "max2FDevices": 10, + "max2FDevicesNameLength": 20, + "multiValuesSeparator": "; ", + "mySessionAuthorizedRWKeys": [ + "_appsListOrder", + "_oidcConnectedRP", + "_oidcConsents" + ], + "newLocationWarningLocationAttribute": "ipAddr", + "newLocationWarningLocationDisplayAttribute": "", + "newLocationWarningMaxValues": "0", + "notification": 0, + "notificationDefaultCond": "", + "notificationServerPOST": 1, + "notificationServerSentAttributes": "uid reference date title subtitle text check", + "notificationStorage": "File", + "notificationStorageOptions": { + "dirName": "/var/lib/lemonldap-ng/notifications" + }, + "notificationWildcard": "allusers", + "notificationsMaxRetrieve": 3, + "notifyDeleted": 1, + "nullAuthnLevel": 0, + "oidcAuthnLevel": 1, + "oidcOPMetaDataExportedVars": {}, + "oidcOPMetaDataJSON": {}, + "oidcOPMetaDataJWKS": {}, + "oidcOPMetaDataOptions": {}, + "oidcRPCallbackGetParam": "openidconnectcallback", + "oidcRPMetaDataExportedVars": { + "matrix0": { + "email": "mail", + "family_name": "cn", + "given_name": "cn", + "name": "cn", + "nickname": "uid", + "preferred_username": "uid" + }, + "matrix1": { "email": "mail", "family_name": "cn", "given_name": "cn", @@ -255,227 +255,226 @@ "nickname": "uid", "preferred_username": "uid" } - }, - "oidcRPMetaDataMacros": null, - "oidcRPMetaDataOptions": { - "matrix1": { - "oidcRPMetaDataOptionsAccessTokenClaims": 0, - "oidcRPMetaDataOptionsAccessTokenJWT": 0, - "oidcRPMetaDataOptionsAccessTokenSignAlg": "RS256", - "oidcRPMetaDataOptionsAllowClientCredentialsGrant": 0, - "oidcRPMetaDataOptionsAllowOffline": 0, - "oidcRPMetaDataOptionsAllowPasswordGrant": 0, - "oidcRPMetaDataOptionsBypassConsent": 1, - "oidcRPMetaDataOptionsClientID": "matrix1", - "oidcRPMetaDataOptionsClientSecret": "matrix1*", - "oidcRPMetaDataOptionsIDTokenForceClaims": 0, - "oidcRPMetaDataOptionsIDTokenSignAlg": "RS256", - "oidcRPMetaDataOptionsLogoutBypassConfirm": 0, - "oidcRPMetaDataOptionsLogoutSessionRequired": 1, - "oidcRPMetaDataOptionsLogoutType": "back", - "oidcRPMetaDataOptionsPublic": 0, - "oidcRPMetaDataOptionsRedirectUris": "https://matrix.example.com/_synapse/client/oidc/callback", - "oidcRPMetaDataOptionsRefreshToken": 0, - "oidcRPMetaDataOptionsRequirePKCE": 0 - } - }, - "oidcRPMetaDataOptionsExtraClaims": null, - "oidcRPMetaDataScopeRules": null, - "oidcRPStateTimeout": 600, - "oidcServiceAccessTokenExpiration": 3600, - "oidcServiceAllowAuthorizationCodeFlow": 1, - "oidcServiceAllowImplicitFlow": 0, - "oidcServiceAuthorizationCodeExpiration": 60, - "oidcServiceDynamicRegistrationExportedVars": {}, - "oidcServiceDynamicRegistrationExtraClaims": {}, - "oidcServiceIDTokenExpiration": 3600, - "oidcServiceIgnoreScopeForClaims": 1, - "oidcServiceKeyIdSig": "oMGHInscAW3Nsa0FcnCnDA", - "oidcServiceMetaDataAuthnContext": { - "loa-1": 1, - "loa-2": 2, - "loa-3": 3, - "loa-4": 4, - "loa-5": 5 - }, - "oidcServiceMetaDataAuthorizeURI": "authorize", - "oidcServiceMetaDataBackChannelURI": "blogout", - "oidcServiceMetaDataCheckSessionURI": "checksession.html", - "oidcServiceMetaDataEndSessionURI": "logout", - "oidcServiceMetaDataFrontChannelURI": "flogout", - "oidcServiceMetaDataIntrospectionURI": "introspect", - "oidcServiceMetaDataJWKSURI": "jwks", - "oidcServiceMetaDataRegistrationURI": "register", - "oidcServiceMetaDataTokenURI": "token", - "oidcServiceMetaDataUserInfoURI": "userinfo", - "oidcServiceOfflineSessionExpiration": 2592000, - "oidcServicePrivateKeySig": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDywteBzIOlhKc4\nO+vhMStDYOpPYrWDOodkUZ7OsxlWVNZ/b/lqIFS56+MHPkKNQuT4zZCyO8bEKmmR\nZ6kPFJoGbO1zJCPQ/RKjimX4J/5gDb1BAlo+6agJi55e3Bw0zKNJDU0mRyedcIzW\n7ywTgyj6B35pl/Sfloi4Q1XEizHar+26h66SOEtnppMxGvwsxO8gFWz26CPmalvY\n5GNYR0txbXUZn7I4kDa4mMWgNfeocWc78Qbt4RV5EuQdbRh1sou4tL9Nn4EuGhg0\nmfsSI0xVAj7f82Wn3kW6qEbhuejrY7aqmZjN7yrMKtCBuV7o4hVrjYLuM2j0mInY\nMy5nRNOVAgMBAAECggEAJ145nK8R2lG83H27LvXOUkrxNJaJYRKoyjgCTPr2bO2t\nK1V5WSCNHOmIE7ChEk962m5bvMu83CsUm6P34p4wrEIV78o4lLe1whe7mZbCxcj0\nnApJoFI8EfA2aqO/X0CgakRh8ocvgXSzIlf/CdsHViTI907ROOAso9Unn4wDNbdp\nMrhi3H2SnA+ewzj85WygBVTNQmVBjJSSLXTQRkfHye0ztvQm59gqqaJaM2rkBjvA\nlPWAVsgakOk4pgClKElCsIjWPJwdYtcd8VJrwnro5J9KhMwB//AArGgqOaXUHnLH\nv5aZZp6FjV/M3BxbSp4cG6hXmK1hrDFLecRddYP1gQKBgQD+Y4/ee57Z0E2V8833\nYfrK3F23sfxmZ7zUwEbgFXUfRy3RVW7Hbc7PAJzxzrk+LYk/zaZrrfEJguqG2O6m\nVNYkqxKu69Nn964CMdV15JGxVzpzsN5adKlcvKVVv9gx2rF3SMUOHiRutj2BlUtO\niCq0G3jFsXWIRzePig9PbWP6CQKBgQD0TG2DeDDUgKbeJYIzXfmCvGxlm5MZqCc/\nK7d8P9U0svG//jJRTsa9hcLjk7N24CzhLNHyJmT7dh1Xy1oLyHNPZ4nQRmCe+HUf\nu0SK10WZ2K55ekUmqS+xSuDFWJtWa5SE46cKg0fKu7YkiDKI1s6I3qrF4lew2aDE\n2p8GJRrgLQKBgCh2PZPtpb6PW0fWl5QZiYJqup1VOggvx+EvFBbgUti+wZLiO9SM\nqrBSMKRldSFmrMXxN984s3YH1LXOG2dpZwY+D6Ky79VBl/PRaVpvGJ1Uen+cSkGo\n/Kc7ejDBaunDFycZ8/3i3Xiek/ngfTHohqJPHE6Vg1RBv5ydIQJJK/XBAoGAU1XO\n9c4GOjc4tQbuhz9DYgmMoIyVfWcTHEV5bfUIcdWpCelYmMval8QNWzyDN8X5CUcU\nxxm50N3V3KENsn9KdofHRzj6tL/klFJ5azNMFtMHkYDYHfwQvNXiHu++7Zf9LefK\nj5eA4fNuir+7HVrJUX9DmgVADJ/wa7Z4EMyPgnECgYA/NLUs4920h10ie5lFffpM\nqq6CRcBjsQ7eGK9UI1Z2KZUh94eqIENSJ7whBjXKvJJvhAlH4//lVFMMRs7oJePY\nThg+8In7PB64yMOIJZLc5Fekn9aGG6YtErPzePQkXSYCKZxWl5EpjQZGgPRVkNtD\n2nflyJLjiCbTjeNgWIOZlw==\n-----END PRIVATE KEY-----\n", - "oidcServicePublicKeySig": "-----BEGIN CERTIFICATE-----\nMIICuDCCAaCgAwIBAgIEFU77HjANBgkqhkiG9w0BAQsFADAeMRwwGgYDVQQDDBNt\nYXRyaXgubGluYWdvcmEuY29tMB4XDTIzMDIxNTAzMTk0NloXDTQzMDIxMDAzMTk0\nNlowHjEcMBoGA1UEAwwTbWF0cml4LmxpbmFnb3JhLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBAPLC14HMg6WEpzg76+ExK0Ng6k9itYM6h2RRns6z\nGVZU1n9v+WogVLnr4wc+Qo1C5PjNkLI7xsQqaZFnqQ8UmgZs7XMkI9D9EqOKZfgn\n/mANvUECWj7pqAmLnl7cHDTMo0kNTSZHJ51wjNbvLBODKPoHfmmX9J+WiLhDVcSL\nMdqv7bqHrpI4S2emkzEa/CzE7yAVbPboI+ZqW9jkY1hHS3FtdRmfsjiQNriYxaA1\n96hxZzvxBu3hFXkS5B1tGHWyi7i0v02fgS4aGDSZ+xIjTFUCPt/zZafeRbqoRuG5\n6OtjtqqZmM3vKswq0IG5XujiFWuNgu4zaPSYidgzLmdE05UCAwEAATANBgkqhkiG\n9w0BAQsFAAOCAQEArNmGxZVvmvdOLctv+zQ+npzQtOTaJcf+r/1xYuM4FZVe4yLc\ny9ElDskoDWjvQU7jKeJeaDOYgMJQNrek8Doj8uHPWNe6jYFa62Csg9aPz6e8qbtq\nWI+sXds5GJd6xZ8mi2L4MdT/tf8dBgcgybuoRyhBtJwG1rLNAYkeXMxkBzOFcU7K\nR/SZ0q9ToLAWFDhn42MTjPN3t6GwKDzGNsM/SI/3WvUwpQbtK91hjPnNDwKiAtGG\nfUteuigfXY+0hEcQwJdR0St/FQ8UYYcAB5YT9IkT1wCcU5LfPHCBf3OXNpbnQsHh\netQMKLibM6wWdXNwmsd1szO66ft3QZ4h4EG3Vw==\n-----END CERTIFICATE-----\n", - "oidcStorageOptions": {}, - "openIdAuthnLevel": 1, - "openIdExportedVars": {}, - "openIdIDPList": "0;", - "openIdSPList": "0;", - "openIdSreg_email": "mail", - "openIdSreg_fullname": "cn", - "openIdSreg_nickname": "uid", - "openIdSreg_timezone": "_timezone", - "pamAuthnLevel": 2, - "pamService": "login", - "password2fActivation": 0, - "password2fSelfRegistration": 0, - "password2fUserCanRemoveKey": 1, - "passwordDB": "Demo", - "passwordPolicyActivation": 1, - "passwordPolicyMinDigit": 0, - "passwordPolicyMinLower": 0, - "passwordPolicyMinSize": 0, - "passwordPolicyMinSpeChar": 0, - "passwordPolicyMinUpper": 0, - "passwordPolicySpecialChar": "__ALL__", - "passwordResetAllowedRetries": 3, - "persistentSessionAttributes": "_loginHistory _2fDevices notification_", - "persistentStorage": "Apache::Session::File", - "persistentStorageOptions": { - "Directory": "/var/lib/lemonldap-ng/psessions", - "LockDirectory": "/var/lib/lemonldap-ng/psessions/lock" - }, - "port": -1, - "portal": "https://auth.example.com", - "portalAntiFrame": 1, - "portalCheckLogins": 1, - "portalDisplayAppslist": 1, - "portalDisplayChangePassword": "$_auth =~ /^(LDAP|DBI|Demo)$/", - "portalDisplayGeneratePassword": 1, - "portalDisplayLoginHistory": 1, - "portalDisplayLogout": 1, - "portalDisplayOidcConsents": "$_oidcConsents && $_oidcConsents =~ /\\w+/", - "portalDisplayOrder": "Appslist ChangePassword LoginHistory OidcConsents Logout", - "portalDisplayRefreshMyRights": 1, - "portalDisplayRegister": 1, - "portalErrorOnExpiredSession": 1, - "portalFavicon": "common/favicon.ico", - "portalForceAuthnInterval": 5, - "portalMainLogo": "common/logos/logo_llng_400px.png", - "portalPingInterval": 60000, - "portalRequireOldPassword": 1, - "portalSkin": "bootstrap", - "portalSkinBackground": "1280px-Cedar_Breaks_National_Monument_partially.jpg", - "portalUserAttr": "_user", - "proxyAuthServiceChoiceParam": "lmAuth", - "proxyAuthnLevel": 2, - "radius2fActivation": 0, - "radius2fTimeout": 20, - "radiusAuthnLevel": 3, - "radiusExportedVars": {}, - "randomPasswordRegexp": "[A-Z]{3}[a-z]{5}.\\d{2}", - "redirectFormMethod": "get", - "registerDB": "Null", - "registerTimeout": 0, - "registerUrl": "https://auth.example.com/register", - "reloadTimeout": 5, - "reloadUrls": { - "localhost": "https://reload.example.com/reload" - }, - "rememberAuthChoiceRule": 0, - "rememberCookieName": "llngrememberauthchoice", - "rememberCookieTimeout": 31536000, - "rememberTimer": 5, - "remoteGlobalStorage": "Lemonldap::NG::Common::Apache::Session::SOAP", - "remoteGlobalStorageOptions": { - "ns": "https://auth.example.com/Lemonldap/NG/Common/PSGI/SOAPService", - "proxy": "https://auth.example.com/sessions" - }, - "requireToken": 1, - "rest2fActivation": 0, - "restAuthnLevel": 2, - "restClockTolerance": 15, - "sameSite": "", - "samlAttributeAuthorityDescriptorAttributeServiceSOAP": "urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/AA/SOAP;", - "samlAuthnContextMapKerberos": 4, - "samlAuthnContextMapPassword": 2, - "samlAuthnContextMapPasswordProtectedTransport": 3, - "samlAuthnContextMapTLSClient": 5, - "samlEntityID": "#PORTAL#/saml/metadata", - "samlIDPSSODescriptorArtifactResolutionServiceArtifact": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/artifact", - "samlIDPSSODescriptorSingleLogoutServiceHTTPPost": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/singleLogout;#PORTAL#/saml/singleLogoutReturn", - "samlIDPSSODescriptorSingleLogoutServiceHTTPRedirect": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect;#PORTAL#/saml/singleLogout;#PORTAL#/saml/singleLogoutReturn", - "samlIDPSSODescriptorSingleLogoutServiceSOAP": "urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/singleLogoutSOAP;", - "samlIDPSSODescriptorSingleSignOnServiceHTTPArtifact": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/singleSignOnArtifact;", - "samlIDPSSODescriptorSingleSignOnServiceHTTPPost": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/singleSignOn;", - "samlIDPSSODescriptorSingleSignOnServiceHTTPRedirect": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect;#PORTAL#/saml/singleSignOn;", - "samlIDPSSODescriptorWantAuthnRequestsSigned": 1, - "samlMetadataForceUTF8": 1, - "samlNameIDFormatMapEmail": "mail", - "samlNameIDFormatMapKerberos": "uid", - "samlNameIDFormatMapWindows": "uid", - "samlNameIDFormatMapX509": "mail", - "samlOrganizationDisplayName": "Example", - "samlOrganizationName": "Example", - "samlOrganizationURL": "https://www.example.com", - "samlOverrideIDPEntityID": "", - "samlRelayStateTimeout": 600, - "samlSPSSODescriptorArtifactResolutionServiceArtifact": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/artifact", - "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact": "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact", - "samlSPSSODescriptorAssertionConsumerServiceHTTPPost": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost", - "samlSPSSODescriptorAuthnRequestsSigned": 1, - "samlSPSSODescriptorSingleLogoutServiceHTTPPost": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleLogout;#PORTAL#/saml/proxySingleLogoutReturn", - "samlSPSSODescriptorSingleLogoutServiceHTTPRedirect": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect;#PORTAL#/saml/proxySingleLogout;#PORTAL#/saml/proxySingleLogoutReturn", - "samlSPSSODescriptorSingleLogoutServiceSOAP": "urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/proxySingleLogoutSOAP;", - "samlSPSSODescriptorWantAssertionsSigned": 1, - "samlServiceSignatureMethod": "RSA_SHA256", - "scrollTop": 400, - "securedCookie": 0, - "sessionDataToRemember": {}, - "sfEngine": "::2F::Engines::Default", - "sfManagerRule": 1, - "sfRemovedMsgRule": 0, - "sfRemovedNotifMsg": "_removedSF_ expired second factor(s) has/have been removed (_nameSF_)!", - "sfRemovedNotifRef": "RemoveSF", - "sfRemovedNotifTitle": "Second factor notification", - "sfRequired": 0, - "showLanguages": 1, - "singleIP": 0, - "singleSession": 0, - "singleUserByIP": 0, - "slaveAuthnLevel": 2, - "slaveExportedVars": {}, - "soapProxyUrn": "urn:Lemonldap/NG/Common/PSGI/SOAPService", - "stayConnected": 0, - "stayConnectedCookieName": "llngconnection", - "stayConnectedTimeout": 2592000, - "successLoginNumber": 5, - "timeout": 72000, - "timeoutActivity": 0, - "timeoutActivityInterval": 60, - "totp2fActivation": 0, - "totp2fDigits": 6, - "totp2fInterval": 30, - "totp2fRange": 1, - "totp2fSelfRegistration": 0, - "totp2fUserCanRemoveKey": 1, - "twitterAuthnLevel": 1, - "twitterUserField": "screen_name", - "u2fActivation": 0, - "u2fSelfRegistration": 0, - "u2fUserCanRemoveKey": 1, - "upgradeSession": 1, - "useRedirectOnError": 1, - "useSafeJail": 1, - "userControl": "^[\\w\\.\\-@]+$", - "userDB": "Same", - "utotp2fActivation": 0, - "viewerHiddenKeys": "samlIDPMetaDataNodes, samlSPMetaDataNodes", - "webIDAuthnLevel": 1, - "webIDExportedVars": {}, - "webauthn2fActivation": 0, - "webauthn2fSelfRegistration": 0, - "webauthn2fUserCanRemoveKey": 1, - "webauthn2fUserVerification": "preferred", - "whatToTrace": "_whatToTrace", - "yubikey2fActivation": 0, - "yubikey2fPublicIDSize": 12, - "yubikey2fSelfRegistration": 0, - "yubikey2fUserCanRemoveKey": 1 - } - \ No newline at end of file + }, + "oidcRPMetaDataMacros": null, + "oidcRPMetaDataOptions": { + "matrix1": { + "oidcRPMetaDataOptionsAccessTokenClaims": 0, + "oidcRPMetaDataOptionsAccessTokenJWT": 0, + "oidcRPMetaDataOptionsAccessTokenSignAlg": "RS256", + "oidcRPMetaDataOptionsAllowClientCredentialsGrant": 0, + "oidcRPMetaDataOptionsAllowOffline": 0, + "oidcRPMetaDataOptionsAllowPasswordGrant": 0, + "oidcRPMetaDataOptionsBypassConsent": 1, + "oidcRPMetaDataOptionsClientID": "matrix1", + "oidcRPMetaDataOptionsClientSecret": "matrix1*", + "oidcRPMetaDataOptionsIDTokenForceClaims": 0, + "oidcRPMetaDataOptionsIDTokenSignAlg": "RS256", + "oidcRPMetaDataOptionsLogoutBypassConfirm": 0, + "oidcRPMetaDataOptionsLogoutSessionRequired": 1, + "oidcRPMetaDataOptionsLogoutType": "back", + "oidcRPMetaDataOptionsPublic": 0, + "oidcRPMetaDataOptionsRedirectUris": "https://matrix.example.com/_synapse/client/oidc/callback", + "oidcRPMetaDataOptionsRefreshToken": 0, + "oidcRPMetaDataOptionsRequirePKCE": 0 + } + }, + "oidcRPMetaDataOptionsExtraClaims": null, + "oidcRPMetaDataScopeRules": null, + "oidcRPStateTimeout": 600, + "oidcServiceAccessTokenExpiration": 3600, + "oidcServiceAllowAuthorizationCodeFlow": 1, + "oidcServiceAllowImplicitFlow": 0, + "oidcServiceAuthorizationCodeExpiration": 60, + "oidcServiceDynamicRegistrationExportedVars": {}, + "oidcServiceDynamicRegistrationExtraClaims": {}, + "oidcServiceIDTokenExpiration": 3600, + "oidcServiceIgnoreScopeForClaims": 1, + "oidcServiceKeyIdSig": "oMGHInscAW3Nsa0FcnCnDA", + "oidcServiceMetaDataAuthnContext": { + "loa-1": 1, + "loa-2": 2, + "loa-3": 3, + "loa-4": 4, + "loa-5": 5 + }, + "oidcServiceMetaDataAuthorizeURI": "authorize", + "oidcServiceMetaDataBackChannelURI": "blogout", + "oidcServiceMetaDataCheckSessionURI": "checksession.html", + "oidcServiceMetaDataEndSessionURI": "logout", + "oidcServiceMetaDataFrontChannelURI": "flogout", + "oidcServiceMetaDataIntrospectionURI": "introspect", + "oidcServiceMetaDataJWKSURI": "jwks", + "oidcServiceMetaDataRegistrationURI": "register", + "oidcServiceMetaDataTokenURI": "token", + "oidcServiceMetaDataUserInfoURI": "userinfo", + "oidcServiceOfflineSessionExpiration": 2592000, + "oidcServicePrivateKeySig": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDywteBzIOlhKc4\nO+vhMStDYOpPYrWDOodkUZ7OsxlWVNZ/b/lqIFS56+MHPkKNQuT4zZCyO8bEKmmR\nZ6kPFJoGbO1zJCPQ/RKjimX4J/5gDb1BAlo+6agJi55e3Bw0zKNJDU0mRyedcIzW\n7ywTgyj6B35pl/Sfloi4Q1XEizHar+26h66SOEtnppMxGvwsxO8gFWz26CPmalvY\n5GNYR0txbXUZn7I4kDa4mMWgNfeocWc78Qbt4RV5EuQdbRh1sou4tL9Nn4EuGhg0\nmfsSI0xVAj7f82Wn3kW6qEbhuejrY7aqmZjN7yrMKtCBuV7o4hVrjYLuM2j0mInY\nMy5nRNOVAgMBAAECggEAJ145nK8R2lG83H27LvXOUkrxNJaJYRKoyjgCTPr2bO2t\nK1V5WSCNHOmIE7ChEk962m5bvMu83CsUm6P34p4wrEIV78o4lLe1whe7mZbCxcj0\nnApJoFI8EfA2aqO/X0CgakRh8ocvgXSzIlf/CdsHViTI907ROOAso9Unn4wDNbdp\nMrhi3H2SnA+ewzj85WygBVTNQmVBjJSSLXTQRkfHye0ztvQm59gqqaJaM2rkBjvA\nlPWAVsgakOk4pgClKElCsIjWPJwdYtcd8VJrwnro5J9KhMwB//AArGgqOaXUHnLH\nv5aZZp6FjV/M3BxbSp4cG6hXmK1hrDFLecRddYP1gQKBgQD+Y4/ee57Z0E2V8833\nYfrK3F23sfxmZ7zUwEbgFXUfRy3RVW7Hbc7PAJzxzrk+LYk/zaZrrfEJguqG2O6m\nVNYkqxKu69Nn964CMdV15JGxVzpzsN5adKlcvKVVv9gx2rF3SMUOHiRutj2BlUtO\niCq0G3jFsXWIRzePig9PbWP6CQKBgQD0TG2DeDDUgKbeJYIzXfmCvGxlm5MZqCc/\nK7d8P9U0svG//jJRTsa9hcLjk7N24CzhLNHyJmT7dh1Xy1oLyHNPZ4nQRmCe+HUf\nu0SK10WZ2K55ekUmqS+xSuDFWJtWa5SE46cKg0fKu7YkiDKI1s6I3qrF4lew2aDE\n2p8GJRrgLQKBgCh2PZPtpb6PW0fWl5QZiYJqup1VOggvx+EvFBbgUti+wZLiO9SM\nqrBSMKRldSFmrMXxN984s3YH1LXOG2dpZwY+D6Ky79VBl/PRaVpvGJ1Uen+cSkGo\n/Kc7ejDBaunDFycZ8/3i3Xiek/ngfTHohqJPHE6Vg1RBv5ydIQJJK/XBAoGAU1XO\n9c4GOjc4tQbuhz9DYgmMoIyVfWcTHEV5bfUIcdWpCelYmMval8QNWzyDN8X5CUcU\nxxm50N3V3KENsn9KdofHRzj6tL/klFJ5azNMFtMHkYDYHfwQvNXiHu++7Zf9LefK\nj5eA4fNuir+7HVrJUX9DmgVADJ/wa7Z4EMyPgnECgYA/NLUs4920h10ie5lFffpM\nqq6CRcBjsQ7eGK9UI1Z2KZUh94eqIENSJ7whBjXKvJJvhAlH4//lVFMMRs7oJePY\nThg+8In7PB64yMOIJZLc5Fekn9aGG6YtErPzePQkXSYCKZxWl5EpjQZGgPRVkNtD\n2nflyJLjiCbTjeNgWIOZlw==\n-----END PRIVATE KEY-----\n", + "oidcServicePublicKeySig": "-----BEGIN CERTIFICATE-----\nMIICuDCCAaCgAwIBAgIEFU77HjANBgkqhkiG9w0BAQsFADAeMRwwGgYDVQQDDBNt\nYXRyaXgubGluYWdvcmEuY29tMB4XDTIzMDIxNTAzMTk0NloXDTQzMDIxMDAzMTk0\nNlowHjEcMBoGA1UEAwwTbWF0cml4LmxpbmFnb3JhLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBAPLC14HMg6WEpzg76+ExK0Ng6k9itYM6h2RRns6z\nGVZU1n9v+WogVLnr4wc+Qo1C5PjNkLI7xsQqaZFnqQ8UmgZs7XMkI9D9EqOKZfgn\n/mANvUECWj7pqAmLnl7cHDTMo0kNTSZHJ51wjNbvLBODKPoHfmmX9J+WiLhDVcSL\nMdqv7bqHrpI4S2emkzEa/CzE7yAVbPboI+ZqW9jkY1hHS3FtdRmfsjiQNriYxaA1\n96hxZzvxBu3hFXkS5B1tGHWyi7i0v02fgS4aGDSZ+xIjTFUCPt/zZafeRbqoRuG5\n6OtjtqqZmM3vKswq0IG5XujiFWuNgu4zaPSYidgzLmdE05UCAwEAATANBgkqhkiG\n9w0BAQsFAAOCAQEArNmGxZVvmvdOLctv+zQ+npzQtOTaJcf+r/1xYuM4FZVe4yLc\ny9ElDskoDWjvQU7jKeJeaDOYgMJQNrek8Doj8uHPWNe6jYFa62Csg9aPz6e8qbtq\nWI+sXds5GJd6xZ8mi2L4MdT/tf8dBgcgybuoRyhBtJwG1rLNAYkeXMxkBzOFcU7K\nR/SZ0q9ToLAWFDhn42MTjPN3t6GwKDzGNsM/SI/3WvUwpQbtK91hjPnNDwKiAtGG\nfUteuigfXY+0hEcQwJdR0St/FQ8UYYcAB5YT9IkT1wCcU5LfPHCBf3OXNpbnQsHh\netQMKLibM6wWdXNwmsd1szO66ft3QZ4h4EG3Vw==\n-----END CERTIFICATE-----\n", + "oidcStorageOptions": {}, + "openIdAuthnLevel": 1, + "openIdExportedVars": {}, + "openIdIDPList": "0;", + "openIdSPList": "0;", + "openIdSreg_email": "mail", + "openIdSreg_fullname": "cn", + "openIdSreg_nickname": "uid", + "openIdSreg_timezone": "_timezone", + "pamAuthnLevel": 2, + "pamService": "login", + "password2fActivation": 0, + "password2fSelfRegistration": 0, + "password2fUserCanRemoveKey": 1, + "passwordDB": "Demo", + "passwordPolicyActivation": 1, + "passwordPolicyMinDigit": 0, + "passwordPolicyMinLower": 0, + "passwordPolicyMinSize": 0, + "passwordPolicyMinSpeChar": 0, + "passwordPolicyMinUpper": 0, + "passwordPolicySpecialChar": "__ALL__", + "passwordResetAllowedRetries": 3, + "persistentSessionAttributes": "_loginHistory _2fDevices notification_", + "persistentStorage": "Apache::Session::File", + "persistentStorageOptions": { + "Directory": "/var/lib/lemonldap-ng/psessions", + "LockDirectory": "/var/lib/lemonldap-ng/psessions/lock" + }, + "port": -1, + "portal": "https://auth.example.com", + "portalAntiFrame": 1, + "portalCheckLogins": 1, + "portalDisplayAppslist": 1, + "portalDisplayChangePassword": "$_auth =~ /^(LDAP|DBI|Demo)$/", + "portalDisplayGeneratePassword": 1, + "portalDisplayLoginHistory": 1, + "portalDisplayLogout": 1, + "portalDisplayOidcConsents": "$_oidcConsents && $_oidcConsents =~ /\\w+/", + "portalDisplayOrder": "Appslist ChangePassword LoginHistory OidcConsents Logout", + "portalDisplayRefreshMyRights": 1, + "portalDisplayRegister": 1, + "portalErrorOnExpiredSession": 1, + "portalFavicon": "common/favicon.ico", + "portalForceAuthnInterval": 5, + "portalMainLogo": "common/logos/logo_llng_400px.png", + "portalPingInterval": 60000, + "portalRequireOldPassword": 1, + "portalSkin": "bootstrap", + "portalSkinBackground": "1280px-Cedar_Breaks_National_Monument_partially.jpg", + "portalUserAttr": "_user", + "proxyAuthServiceChoiceParam": "lmAuth", + "proxyAuthnLevel": 2, + "radius2fActivation": 0, + "radius2fTimeout": 20, + "radiusAuthnLevel": 3, + "radiusExportedVars": {}, + "randomPasswordRegexp": "[A-Z]{3}[a-z]{5}.\\d{2}", + "redirectFormMethod": "get", + "registerDB": "Null", + "registerTimeout": 0, + "registerUrl": "https://auth.example.com/register", + "reloadTimeout": 5, + "reloadUrls": { + "localhost": "https://reload.example.com/reload" + }, + "rememberAuthChoiceRule": 0, + "rememberCookieName": "llngrememberauthchoice", + "rememberCookieTimeout": 31536000, + "rememberTimer": 5, + "remoteGlobalStorage": "Lemonldap::NG::Common::Apache::Session::SOAP", + "remoteGlobalStorageOptions": { + "ns": "https://auth.example.com/Lemonldap/NG/Common/PSGI/SOAPService", + "proxy": "https://auth.example.com/sessions" + }, + "requireToken": 1, + "rest2fActivation": 0, + "restAuthnLevel": 2, + "restClockTolerance": 15, + "sameSite": "", + "samlAttributeAuthorityDescriptorAttributeServiceSOAP": "urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/AA/SOAP;", + "samlAuthnContextMapKerberos": 4, + "samlAuthnContextMapPassword": 2, + "samlAuthnContextMapPasswordProtectedTransport": 3, + "samlAuthnContextMapTLSClient": 5, + "samlEntityID": "#PORTAL#/saml/metadata", + "samlIDPSSODescriptorArtifactResolutionServiceArtifact": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/artifact", + "samlIDPSSODescriptorSingleLogoutServiceHTTPPost": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/singleLogout;#PORTAL#/saml/singleLogoutReturn", + "samlIDPSSODescriptorSingleLogoutServiceHTTPRedirect": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect;#PORTAL#/saml/singleLogout;#PORTAL#/saml/singleLogoutReturn", + "samlIDPSSODescriptorSingleLogoutServiceSOAP": "urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/singleLogoutSOAP;", + "samlIDPSSODescriptorSingleSignOnServiceHTTPArtifact": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/singleSignOnArtifact;", + "samlIDPSSODescriptorSingleSignOnServiceHTTPPost": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/singleSignOn;", + "samlIDPSSODescriptorSingleSignOnServiceHTTPRedirect": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect;#PORTAL#/saml/singleSignOn;", + "samlIDPSSODescriptorWantAuthnRequestsSigned": 1, + "samlMetadataForceUTF8": 1, + "samlNameIDFormatMapEmail": "mail", + "samlNameIDFormatMapKerberos": "uid", + "samlNameIDFormatMapWindows": "uid", + "samlNameIDFormatMapX509": "mail", + "samlOrganizationDisplayName": "Example", + "samlOrganizationName": "Example", + "samlOrganizationURL": "https://www.example.com", + "samlOverrideIDPEntityID": "", + "samlRelayStateTimeout": 600, + "samlSPSSODescriptorArtifactResolutionServiceArtifact": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/artifact", + "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact": "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact", + "samlSPSSODescriptorAssertionConsumerServiceHTTPPost": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost", + "samlSPSSODescriptorAuthnRequestsSigned": 1, + "samlSPSSODescriptorSingleLogoutServiceHTTPPost": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleLogout;#PORTAL#/saml/proxySingleLogoutReturn", + "samlSPSSODescriptorSingleLogoutServiceHTTPRedirect": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect;#PORTAL#/saml/proxySingleLogout;#PORTAL#/saml/proxySingleLogoutReturn", + "samlSPSSODescriptorSingleLogoutServiceSOAP": "urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/proxySingleLogoutSOAP;", + "samlSPSSODescriptorWantAssertionsSigned": 1, + "samlServiceSignatureMethod": "RSA_SHA256", + "scrollTop": 400, + "securedCookie": 0, + "sessionDataToRemember": {}, + "sfEngine": "::2F::Engines::Default", + "sfManagerRule": 1, + "sfRemovedMsgRule": 0, + "sfRemovedNotifMsg": "_removedSF_ expired second factor(s) has/have been removed (_nameSF_)!", + "sfRemovedNotifRef": "RemoveSF", + "sfRemovedNotifTitle": "Second factor notification", + "sfRequired": 0, + "showLanguages": 1, + "singleIP": 0, + "singleSession": 0, + "singleUserByIP": 0, + "slaveAuthnLevel": 2, + "slaveExportedVars": {}, + "soapProxyUrn": "urn:Lemonldap/NG/Common/PSGI/SOAPService", + "stayConnected": 0, + "stayConnectedCookieName": "llngconnection", + "stayConnectedTimeout": 2592000, + "successLoginNumber": 5, + "timeout": 72000, + "timeoutActivity": 0, + "timeoutActivityInterval": 60, + "totp2fActivation": 0, + "totp2fDigits": 6, + "totp2fInterval": 30, + "totp2fRange": 1, + "totp2fSelfRegistration": 0, + "totp2fUserCanRemoveKey": 1, + "twitterAuthnLevel": 1, + "twitterUserField": "screen_name", + "u2fActivation": 0, + "u2fSelfRegistration": 0, + "u2fUserCanRemoveKey": 1, + "upgradeSession": 1, + "useRedirectOnError": 1, + "useSafeJail": 1, + "userControl": "^[\\w\\.\\-@]+$", + "userDB": "Same", + "utotp2fActivation": 0, + "viewerHiddenKeys": "samlIDPMetaDataNodes, samlSPMetaDataNodes", + "webIDAuthnLevel": 1, + "webIDExportedVars": {}, + "webauthn2fActivation": 0, + "webauthn2fSelfRegistration": 0, + "webauthn2fUserCanRemoveKey": 1, + "webauthn2fUserVerification": "preferred", + "whatToTrace": "_whatToTrace", + "yubikey2fActivation": 0, + "yubikey2fPublicIDSize": 12, + "yubikey2fSelfRegistration": 0, + "yubikey2fUserCanRemoveKey": 1 +} diff --git a/packages/tom-server/src/search-engine-api/__testData__/opensearch/tmail-data.json b/packages/tom-server/src/search-engine-api/__testData__/opensearch/tmail-data.json index a8563511..a9253085 100644 --- a/packages/tom-server/src/search-engine-api/__testData__/opensearch/tmail-data.json +++ b/packages/tom-server/src/search-engine-api/__testData__/opensearch/tmail-data.json @@ -2,29 +2,37 @@ { "_index": "mailbox_v2", "_source": { - "attachments": [{ - "contentDisposition": "attachment", - "fileExtension": "txt", - "fileName": "message1.txt", - "mediaType": "text/plain", - "textContent": "May the Force be with you!" - }], - "bcc": [{ - "address": "hsolo@example.com", - "domain": "example.com", - "name": "Han Solo" - }], - "cc": [{ - "address": "c3po@example.com", - "domain": "example.com", - "name": "C-3PO" - }], + "attachments": [ + { + "contentDisposition": "attachment", + "fileExtension": "txt", + "fileName": "message1.txt", + "mediaType": "text/plain", + "textContent": "May the Force be with you!" + } + ], + "bcc": [ + { + "address": "hsolo@example.com", + "domain": "example.com", + "name": "Han Solo" + } + ], + "cc": [ + { + "address": "c3po@example.com", + "domain": "example.com", + "name": "C-3PO" + } + ], "date": "2024-02-22T12:30:00Z", - "from": [{ - "address": "lskywalker@example.com", - "domain": "example.com", - "name": "Luke Skywalker" - }], + "from": [ + { + "address": "lskywalker@example.com", + "domain": "example.com", + "name": "Luke Skywalker" + } + ], "hasAttachment": true, "headers": [ { "name": "Header1", "value": "Value1" }, @@ -49,11 +57,13 @@ "subtype": "subtype1", "textBody": "May the Force be with you!", "threadId": "thread1", - "to": [{ - "address": "jbinks@example.com", - "domain": "example.com", - "name": "Jar Jar Binks" - }], + "to": [ + { + "address": "jbinks@example.com", + "domain": "example.com", + "name": "Jar Jar Binks" + } + ], "uid": 123456, "userFlags": ["Flag1", "Flag2"] } @@ -61,29 +71,37 @@ { "_index": "mailbox_v2", "_source": { - "attachments": [{ - "contentDisposition": "attachment", - "fileExtension": "pdf", - "fileName": "attachment2.pdf", - "mediaType": "application/pdf", - "textContent": "The plans are in the droid." - }], - "bcc": [{ - "address": "myoda@example.com", - "domain": "example.com", - "name": "Master Yoda" - }], - "cc": [{ - "address": "lorgana@example.com", - "domain": "example.com", - "name": "Leia Organa" - }], + "attachments": [ + { + "contentDisposition": "attachment", + "fileExtension": "pdf", + "fileName": "attachment2.pdf", + "mediaType": "application/pdf", + "textContent": "The plans are in the droid." + } + ], + "bcc": [ + { + "address": "myoda@example.com", + "domain": "example.com", + "name": "Master Yoda" + } + ], + "cc": [ + { + "address": "lorgana@example.com", + "domain": "example.com", + "name": "Leia Organa" + } + ], "date": "2024-02-23T14:45:00Z", - "from": [{ - "address": "okenobi@example.com", - "domain": "example.com", - "name": "Obi-Wan Kenobi" - }], + "from": [ + { + "address": "okenobi@example.com", + "domain": "example.com", + "name": "Obi-Wan Kenobi" + } + ], "hasAttachment": true, "headers": [ { "name": "Header3", "value": "Value3" }, @@ -108,11 +126,13 @@ "subtype": "subtype2", "textBody": "The plans are in the droid.", "threadId": "thread2", - "to": [{ - "address": "lskywalker@example.com", - "domain": "example.com", - "name": "Luke Skywalker" - }], + "to": [ + { + "address": "lskywalker@example.com", + "domain": "example.com", + "name": "Luke Skywalker" + } + ], "uid": 654321, "userFlags": ["Flag3"] } @@ -120,29 +140,37 @@ { "_index": "mailbox_v2", "_source": { - "attachments": [{ - "contentDisposition": "attachment", - "fileExtension": "jpg", - "fileName": "image1.jpg", - "mediaType": "image/jpeg", - "textContent": "A beautiful galaxy far, far away." - }], - "bcc": [{ - "address": "okenobi@example.com", - "domain": "example.com", - "name": "Obi-Wan Kenobi" - }], - "cc": [{ - "address": "pamidala@example.com", - "domain": "example.com", - "name": "Padme Amidala" - }], + "attachments": [ + { + "contentDisposition": "attachment", + "fileExtension": "jpg", + "fileName": "image1.jpg", + "mediaType": "image/jpeg", + "textContent": "A beautiful galaxy far, far away." + } + ], + "bcc": [ + { + "address": "okenobi@example.com", + "domain": "example.com", + "name": "Obi-Wan Kenobi" + } + ], + "cc": [ + { + "address": "pamidala@example.com", + "domain": "example.com", + "name": "Padme Amidala" + } + ], "date": "2024-02-24T10:15:00Z", - "from": [{ - "address": "dmaul@example.com", - "domain": "example.com", - "name": "Dark Maul" - }], + "from": [ + { + "address": "dmaul@example.com", + "domain": "example.com", + "name": "Dark Maul" + } + ], "hasAttachment": true, "headers": [ { "name": "Header5", "value": "Value5" }, @@ -167,11 +195,13 @@ "subtype": "subtype3", "textBody": "A beautiful galaxy far, far away.", "threadId": "thread3", - "to": [{ - "address": "kren@example.com", - "domain": "example.com", - "name": "Kylo Ren" - }], + "to": [ + { + "address": "kren@example.com", + "domain": "example.com", + "name": "Kylo Ren" + } + ], "uid": 987654, "userFlags": ["Flag4", "Flag5"] } @@ -179,33 +209,42 @@ { "_index": "mailbox_v2", "_source": { - "attachments": [{ - "contentDisposition": "inline", - "fileExtension": "png", - "fileName": "image2.png", - "mediaType": "image/png", - "textContent": "May the pixels be with you." - }], - "bcc": [{ - "address": "chewbacca@example.com", - "domain": "example.com", - "name": "Chewbacca" - }, { - "address": "jbinks@example.com", - "domain": "example.com", - "name": "Jar Jar Binks" - }], - "cc": [{ - "address": "qjinn@example.com", - "domain": "example.com", - "name": "Qui-Gon Jinn" - }], + "attachments": [ + { + "contentDisposition": "inline", + "fileExtension": "png", + "fileName": "image2.png", + "mediaType": "image/png", + "textContent": "May the pixels be with you." + } + ], + "bcc": [ + { + "address": "chewbacca@example.com", + "domain": "example.com", + "name": "Chewbacca" + }, + { + "address": "jbinks@example.com", + "domain": "example.com", + "name": "Jar Jar Binks" + } + ], + "cc": [ + { + "address": "qjinn@example.com", + "domain": "example.com", + "name": "Qui-Gon Jinn" + } + ], "date": "2024-02-25T08:45:00Z", - "from": [{ - "address": "lskywalker@example.com", - "domain": "example.com", - "name": "Luke Skywalker" - }], + "from": [ + { + "address": "lskywalker@example.com", + "domain": "example.com", + "name": "Luke Skywalker" + } + ], "hasAttachment": true, "headers": [ { "name": "Header7", "value": "Value7" }, @@ -230,11 +269,13 @@ "subtype": "subtype4", "textBody": "May the pixels be with you.", "threadId": "thread4", - "to": [{ - "address": "hsolo@example.com", - "domain": "example.com", - "name": "Han Solo" - }], + "to": [ + { + "address": "hsolo@example.com", + "domain": "example.com", + "name": "Han Solo" + } + ], "uid": 1234567, "userFlags": ["Flag6"] } @@ -242,29 +283,37 @@ { "_index": "mailbox_v2", "_source": { - "attachments": [{ - "contentDisposition": "attachment", - "fileExtension": "docx", - "fileName": "document1.docx", - "mediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "textContent": "A long time ago in a galaxy far, far away." - }], - "bcc": [{ - "address": "lskywalker@example.com", - "domain": "example.com", - "name": "Luke Skywalker" - }], - "cc": [{ - "address": "myoda@example.com", - "domain": "example.com", - "name": "Master Yoda" - }], + "attachments": [ + { + "contentDisposition": "attachment", + "fileExtension": "docx", + "fileName": "document1.docx", + "mediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "textContent": "A long time ago in a galaxy far, far away." + } + ], + "bcc": [ + { + "address": "lskywalker@example.com", + "domain": "example.com", + "name": "Luke Skywalker" + } + ], + "cc": [ + { + "address": "myoda@example.com", + "domain": "example.com", + "name": "Master Yoda" + } + ], "date": "2024-02-26T16:30:00Z", - "from": [{ - "address": "hsolo@example.com", - "domain": "example.com", - "name": "Han Solo" - }], + "from": [ + { + "address": "hsolo@example.com", + "domain": "example.com", + "name": "Han Solo" + } + ], "hasAttachment": true, "headers": [ { "name": "Header9", "value": "Value9" }, @@ -289,11 +338,13 @@ "subtype": "subtype5", "textBody": "A long time ago in a galaxy far, far away.", "threadId": "thread5", - "to": [{ - "address": "chewbacca@example.com", - "domain": "example.com", - "name": "Chewbacca" - }], + "to": [ + { + "address": "chewbacca@example.com", + "domain": "example.com", + "name": "Chewbacca" + } + ], "uid": 2345678, "userFlags": ["Flag7", "Flag8"] } diff --git a/packages/tom-server/src/search-engine-api/__testData__/synapse-data/homeserver.yaml b/packages/tom-server/src/search-engine-api/__testData__/synapse-data/homeserver.yaml index d9cc5250..0f9807af 100644 --- a/packages/tom-server/src/search-engine-api/__testData__/synapse-data/homeserver.yaml +++ b/packages/tom-server/src/search-engine-api/__testData__/synapse-data/homeserver.yaml @@ -9,8 +9,8 @@ # For more information on how to configure Synapse, including a complete accounting of # each option, go to docs/usage/configuration/config_documentation.md or # https://matrix-org.github.io/synapse/latest/usage/configuration/config_documentation.html -server_name: "example.com" -public_baseurl: "https://matrix.example.com/" +server_name: 'example.com' +public_baseurl: 'https://matrix.example.com/' pid_file: /data/homeserve.pid listeners: - port: 8008 @@ -32,15 +32,15 @@ database: keepalives_idle: 10 keepalives_interval: 10 keepalives_count: 3 -log_config: "/data/matrix.example.com.log.config" +log_config: '/data/matrix.example.com.log.config' media_store_path: /data/media_store -registration_shared_secret: "u+Q^i6&*Y9azZ*~pID^.a=qrvd+mUIBX9SAreEPGJ=xzP&c+Sk" +registration_shared_secret: 'u+Q^i6&*Y9azZ*~pID^.a=qrvd+mUIBX9SAreEPGJ=xzP&c+Sk' report_stats: false -macaroon_secret_key: "=0ws-1~ztzXm&xh+As;7YL5.-U~r-T,F4zR3mW#E;6Y::Rb7&G" -form_secret: "&YFO.XSc*2^2ZsW#hmoR+t:wf03~u#fin#O.R&erFcl9_mEayv" -signing_key_path: "/data/matrix.example.com.signing.key" +macaroon_secret_key: '=0ws-1~ztzXm&xh+As;7YL5.-U~r-T,F4zR3mW#E;6Y::Rb7&G' +form_secret: '&YFO.XSc*2^2ZsW#hmoR+t:wf03~u#fin#O.R&erFcl9_mEayv' +signing_key_path: '/data/matrix.example.com.signing.key' trusted_key_servers: - - server_name: "matrix.org" + - server_name: 'matrix.org' accept_keys_insecurely: true accept_keys_insecurely: true app_service_config_files: @@ -49,17 +49,17 @@ oidc_config: idp_id: lemonldap idp_name: lemonldap enabled: true - issuer: "https://auth.example.com/" - client_id: "matrix1" - client_secret: "matrix1*" - scopes: ["openid", "profile"] + issuer: 'https://auth.example.com/' + client_id: 'matrix1' + client_secret: 'matrix1*' + scopes: ['openid', 'profile'] discover: true - user_profile_method: "userinfo_endpoint" + user_profile_method: 'userinfo_endpoint' user_mapping_provider: config: - subject_claim: "sub" - localpart_template: "{{ user.preferred_username }}" - display_name_template: "{{ user.name }}" + subject_claim: 'sub' + localpart_template: '{{ user.preferred_username }}' + display_name_template: '{{ user.name }}' rc_message: per_second: 0.5 - burst_count: 20 \ No newline at end of file + burst_count: 20 diff --git a/packages/tom-server/src/search-engine-api/__testData__/synapse-data/registration.yaml b/packages/tom-server/src/search-engine-api/__testData__/synapse-data/registration.yaml index 7335edd5..05495cae 100644 --- a/packages/tom-server/src/search-engine-api/__testData__/synapse-data/registration.yaml +++ b/packages/tom-server/src/search-engine-api/__testData__/synapse-data/registration.yaml @@ -7,4 +7,4 @@ namespaces: rooms: - exclusive: false regex: '!.*' -de.sorunome.msc2409.push_ephemeral: true \ No newline at end of file +de.sorunome.msc2409.push_ephemeral: true diff --git a/packages/tom-server/src/search-engine-api/conf/opensearch-configuration.ts b/packages/tom-server/src/search-engine-api/conf/opensearch-configuration.ts index df617978..889253d6 100644 --- a/packages/tom-server/src/search-engine-api/conf/opensearch-configuration.ts +++ b/packages/tom-server/src/search-engine-api/conf/opensearch-configuration.ts @@ -1,5 +1,5 @@ import { type ClientOptions } from '@opensearch-project/opensearch' -import { Utils } from '@twake/matrix-identity-server' +import { isHostnameValid } from '@twake/utils' import fs from 'fs' import { type Config } from '../../types' @@ -33,7 +33,7 @@ export class OpenSearchConfiguration { if (typeof host !== 'string') { throw new Error('opensearch_host must be a string') } - if (host.match(Utils.hostnameRe) == null) { + if (!isHostnameValid(host)) { throw new Error('opensearch_host is invalid') } this._host = host diff --git a/packages/tom-server/src/search-engine-api/index.ts b/packages/tom-server/src/search-engine-api/index.ts index d96e3821..07b792fc 100644 --- a/packages/tom-server/src/search-engine-api/index.ts +++ b/packages/tom-server/src/search-engine-api/index.ts @@ -6,11 +6,7 @@ import MatrixApplicationServer, { } from '@twake/matrix-application-server' import { type MatrixDB, type UserDB } from '@twake/matrix-identity-server' import { Router } from 'express' -import { - type AuthenticationFunction, - type Config, - type IdentityServerDb -} from '../types' +import type { AuthenticationFunction, Config, TwakeDB } from '../types' import { type IMatrixDBRoomsRepository } from './repositories/interfaces/matrix-db-rooms-repository.interface' import { type IOpenSearchRepository } from './repositories/interfaces/opensearch-repository.interface' import { MatrixDBRoomsRepository } from './repositories/matrix-db-rooms.repository' @@ -32,7 +28,7 @@ export default class TwakeSearchEngine public readonly matrixDBRoomsRepository: IMatrixDBRoomsRepository constructor( - public readonly idDb: IdentityServerDb, + public readonly twakeDb: TwakeDB, public readonly userDB: UserDB, public readonly authenticate: AuthenticationFunction, matrixDb: MatrixDB, diff --git a/packages/tom-server/src/search-engine-api/tests/events-listener.test.ts b/packages/tom-server/src/search-engine-api/tests/events-listener.test.ts index fd5f7ec8..c565e51c 100644 --- a/packages/tom-server/src/search-engine-api/tests/events-listener.test.ts +++ b/packages/tom-server/src/search-engine-api/tests/events-listener.test.ts @@ -58,7 +58,7 @@ jest.mock('../../identity-server/index.ts', () => { return function () { return { ready: Promise.resolve(true), - db: {}, + db: { cleanByExpires: [] }, userDB: {}, api: { get: {}, post: {} }, cleanJobs: jest.fn().mockImplementation(() => testServer.logger.close()) @@ -76,8 +76,6 @@ jest.mock('../../application-server/index.ts', () => { } }) -jest.mock('../../db/index.ts', () => jest.fn()) - describe('Search engine API - Opensearch service', () => { let app: express.Application let loggerErrorSpyOn: jest.SpyInstance diff --git a/packages/tom-server/src/search-engine-api/tests/opensearch-controller.test.ts b/packages/tom-server/src/search-engine-api/tests/opensearch-controller.test.ts index 5f52ff60..4fada0f3 100644 --- a/packages/tom-server/src/search-engine-api/tests/opensearch-controller.test.ts +++ b/packages/tom-server/src/search-engine-api/tests/opensearch-controller.test.ts @@ -76,7 +76,7 @@ jest.mock('../../identity-server/index.ts', () => { return function () { return { ready: Promise.resolve(true), - db: {}, + db: { cleanByExpires: [] }, userDB: {}, api: { get: {}, post: {} }, cleanJobs: jest.fn().mockImplementation(() => testServer.logger.close()) @@ -94,8 +94,6 @@ jest.mock('../../application-server/index.ts', () => { } }) -jest.mock('../../db/index.ts', () => jest.fn()) - describe('Search engine API - Opensearch controller', () => { let app: express.Application let loggerErrorSpyOn: jest.SpyInstance diff --git a/packages/tom-server/src/search-engine-api/tests/search-engine-controller.test.ts b/packages/tom-server/src/search-engine-api/tests/search-engine-controller.test.ts index 44ed5c42..ce78cc35 100644 --- a/packages/tom-server/src/search-engine-api/tests/search-engine-controller.test.ts +++ b/packages/tom-server/src/search-engine-api/tests/search-engine-controller.test.ts @@ -141,7 +141,7 @@ jest.mock('../../identity-server/index.ts', () => { return function () { return { ready: Promise.resolve(true), - db: {}, + db: { cleanByExpires: [] }, userDB: { get: mockUserDBGet }, api: { get: {}, post: {} }, cleanJobs: jest.fn().mockImplementation(() => testServer.logger.close()) @@ -159,8 +159,6 @@ jest.mock('../../application-server/index.ts', () => { } }) -jest.mock('../../db/index.ts', () => jest.fn()) - jest.mock('../../utils/middlewares/auth.middleware.ts', () => jest .fn() diff --git a/packages/tom-server/src/search-engine-api/tests/server-instanciation.test.ts b/packages/tom-server/src/search-engine-api/tests/server-instanciation.test.ts index 2448926b..a92add72 100644 --- a/packages/tom-server/src/search-engine-api/tests/server-instanciation.test.ts +++ b/packages/tom-server/src/search-engine-api/tests/server-instanciation.test.ts @@ -40,7 +40,7 @@ jest.mock('../../identity-server/index.ts', () => { return function () { return { ready: Promise.resolve(true), - db: {}, + db: { cleanByExpires: [] }, userDB: {}, api: { get: {}, post: {} }, cleanJobs: jest.fn().mockImplementation(() => testServer.logger.close()) @@ -58,8 +58,6 @@ jest.mock('../../application-server/index.ts', () => { } }) -jest.mock('../../db/index.ts', () => jest.fn()) - describe('Search engine API - Opensearch configuration', () => { afterEach(() => { if (testServer != null) testServer.cleanJobs() diff --git a/packages/tom-server/src/search-engine-api/tests/tsconfig.json b/packages/tom-server/src/search-engine-api/tests/tsconfig.json index 1364af34..96116705 100644 --- a/packages/tom-server/src/search-engine-api/tests/tsconfig.json +++ b/packages/tom-server/src/search-engine-api/tests/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "../../../../../tsconfig-test.json", "include": ["**/*.test.ts"] -} \ No newline at end of file +} diff --git a/packages/tom-server/src/sms-api/tests/router.test.ts b/packages/tom-server/src/sms-api/tests/router.test.ts index ba3d5766..c7d09408 100644 --- a/packages/tom-server/src/sms-api/tests/router.test.ts +++ b/packages/tom-server/src/sms-api/tests/router.test.ts @@ -30,7 +30,7 @@ const middlewareSpy = jest.fn().mockImplementation((_req, _res, next) => { }) jest - .spyOn(IdentityServerDb.default.prototype, 'get') + .spyOn(IdentityServerDb.prototype, 'get') .mockResolvedValue([{ data: '"test"' }]) const idServer = new IdServer( diff --git a/packages/tom-server/src/types.ts b/packages/tom-server/src/types.ts index 4942f807..5f99a084 100644 --- a/packages/tom-server/src/types.ts +++ b/packages/tom-server/src/types.ts @@ -1,16 +1,17 @@ import { type Config as MASConfig } from '@twake/matrix-application-server' -import type MatrixIdentityServer from '@twake/matrix-identity-server' import { - MatrixErrors, + type IdentityServerDb, type Config as MConfig, - type IdentityServerDb as MIdentityServerDb, type Utils as MUtils } from '@twake/matrix-identity-server' +import { + type expressAppHandler as _expressAppHandler, + errCodes +} from '@twake/utils' import { type Request } from 'express' import type { PathOrFileDescriptor } from 'fs' -import type AugmentedIdentityServer from './identity-server' -export type expressAppHandler = MUtils.expressAppHandler +export type expressAppHandler = _expressAppHandler export type AuthenticationFunction = MUtils.AuthenticationFunction export type Config = MConfig & @@ -39,9 +40,6 @@ export type Config = MConfig & sms_api_url?: string } -export type IdentityServerDb = MIdentityServerDb.default -export type Collections = MIdentityServerDb.Collections - export interface AuthRequest extends Request { userId?: string accessToken?: string @@ -49,86 +47,16 @@ export interface AuthRequest extends Request { export type ConfigurationFile = object | PathOrFileDescriptor | undefined -export type TwakeIdentityServer = AugmentedIdentityServer | MatrixIdentityServer - export const allMatrixErrorCodes = { - ...MatrixErrors.errCodes, - // The access or refresh token specified was not recognised - // An additional response parameter, soft_logout, might be present on the response for 401 HTTP status codes - unknownToken: 'M_UNKNOWN_TOKEN', - - // No access token was specified for the request - missingToken: 'M_MISSING_TOKEN', - - // Request contained valid JSON, but it was malformed in some way, e.g. missing required keys, invalid values for keys - badJson: 'M_BAD_JSON', - - // Request did not contain valid JSON - notJson: 'M_NOT_JSON', - - // No resource was found for this request - notFound: 'M_NOT_FOUND', - - // Too many requests have been sent in a short period of time. Wait a while then try again. - limitExceeded: 'M_LIMIT_EXCEEDED', - - // The user ID associated with the request has been deactivated. Typically for endpoints that prove authentication, such as /login. - userDeactivated: 'M_USER_DEACTIVATED', - - // Encountered when trying to register a user ID which has been taken. - userInUse: 'M_USER_IN_USE', - - // Encountered when trying to register a user ID which is not valid. - invalidUsername: 'M_INVALID_USERNAME', - - // Sent when the room alias given to the createRoom API is already in use. - roomInUse: 'M_ROOM_IN_USE', - - // Sent when the initial state given to the createRoom API is invalid. - invalidRoomState: 'M_INVALID_ROOM_STATE', - - // Sent when a threepid given to an API cannot be used because no record matching the threepid was found. - threepidNotFound: 'M_THREEPID_NOT_FOUND', - - // Authentication could not be performed on the third-party identifier. - threepidAuthFailed: 'M_THREEPID_AUTH_FAILED', - - // The server does not permit this third-party identifier. This may happen if the server only permits, for example, email addresses from a particular domain. - threepidDenied: 'M_THREEPID_DENIED', - - // The client’s request used a third-party server, e.g. identity server, that this server does not trust. - serverNotTrusted: 'M_SERVER_NOT_TRUSTED', - - // The client’s request to create a room used a room version that the server does not support. - unsupportedRoomVersion: 'M_UNSUPPORTED_ROOM_VERSION', - - // The client attempted to join a room that has a version the server does not support. Inspect the room_version property of the error response for the room’s version. - incompatibleRoomVersion: 'M_INCOMPATIBLE_ROOM_VERSION', - - // The state change requested cannot be performed, such as attempting to unban a user who is not banned. - badState: 'M_BAD_STATE', - - // The room or resource does not permit guests to access it. - guestAccessForbidden: 'M_GUEST_ACCESS_FORBIDDEN', - - // A Captcha is required to complete the request. - captchaNeeded: 'M_CAPTCHA_NEEDED', - - // The Captcha provided did not match what was expected. - captchaInvalid: 'M_CAPTCHA_INVALID', - - // A required parameter was missing from the request. - missingParam: 'M_MISSING_PARAM', - - // The request or entity was too large. - tooLarge: 'M_TOO_LARGE', - - // The resource being requested is reserved by an application service, or the application service making the request has not created the resource. - exclusive: 'M_EXCLUSIVE', + ...errCodes +} as const - // The request cannot be completed because the homeserver has reached a resource limit imposed on it. For example, a homeserver held in a shared hosting environment may reach a resource limit if it starts using too much memory or disk space. The error MUST have an admin_contact field to provide the user receiving the error a place to reach out to. Typically, this error will appear on routes which attempt to modify state (e.g.: sending messages, account data, etc) and not routes which only read state (e.g.: /sync, get account data, etc). - resourceLimitExceeded: 'M_RESOURCE_LIMIT_EXCEEDED', +export type TwakeDB = IdentityServerDb - // The user is unable to reject an invite to join the server notices room. See the Server Notices module for more information. - cannotLeaveServerNoticeRoom: 'M_CANNOT_LEAVE_SERVER_NOTICE_ROOM' -} as const +export type twakeDbCollections = + | 'recoveryWords' + | 'matrixTokens' + | 'privateNotes' + | 'roomTags' + | 'userQuotas' + | 'rooms' diff --git a/packages/tom-server/src/user-info-api/routes/index.ts b/packages/tom-server/src/user-info-api/routes/index.ts index 48128f4b..98945889 100644 --- a/packages/tom-server/src/user-info-api/routes/index.ts +++ b/packages/tom-server/src/user-info-api/routes/index.ts @@ -4,8 +4,8 @@ import { type Config as LoggerConfig, type TwakeLogger } from '@twake/logger' -import type MatrixIdentityServer from '@twake/matrix-identity-server' import { Router } from 'express' +import type IdServer from '../../identity-server' import type { Config } from '../../types' import authMiddleware from '../../utils/middlewares/auth.middleware' import UserInfoController from '../controllers' @@ -13,7 +13,7 @@ import checkLdapMiddleware from '../middlewares/require-ldap' export const PATH = '/_twake/v1/user_info' export default ( - idServer: MatrixIdentityServer, + idServer: IdServer, config: Config, defaultLogger?: TwakeLogger ): Router => { diff --git a/packages/tom-server/src/user-info-api/tests/router.test.ts b/packages/tom-server/src/user-info-api/tests/router.test.ts index c9dc8740..8b3ee673 100644 --- a/packages/tom-server/src/user-info-api/tests/router.test.ts +++ b/packages/tom-server/src/user-info-api/tests/router.test.ts @@ -12,7 +12,7 @@ import router, { PATH } from '../routes' const app = express() jest - .spyOn(IdentityServerDb.default.prototype, 'get') + .spyOn(IdentityServerDb.prototype, 'get') .mockResolvedValue([{ data: '"test"' }]) const idServer = new IdServer( diff --git a/packages/tom-server/src/utils.ts b/packages/tom-server/src/utils.ts new file mode 100644 index 00000000..54628a9b --- /dev/null +++ b/packages/tom-server/src/utils.ts @@ -0,0 +1,10 @@ +export const tables = { + recoveryWords: 'userId text PRIMARY KEY, words TEXT', + matrixTokens: 'id varchar(64) PRIMARY KEY, data text', + privateNotes: + 'id varchar(64) PRIMARY KEY, authorId varchar(64), content text, targetId varchar(64)', + roomTags: + 'id varchar(64) PRIMARY KEY, authorId varchar(64), content text, roomId varchar(64)', + userQuotas: 'user_id varchar(64) PRIMARY KEY, size int', + rooms: 'id varchar(64) PRIMARY KEY, filter varchar(64)' +} diff --git a/packages/tom-server/src/vault-api/__testData__/buildTokenTable.ts b/packages/tom-server/src/vault-api/__testData__/buildTokenTable.ts index ae1f0b07..e678adc1 100644 --- a/packages/tom-server/src/vault-api/__testData__/buildTokenTable.ts +++ b/packages/tom-server/src/vault-api/__testData__/buildTokenTable.ts @@ -21,22 +21,27 @@ const buildTokenTable = (conf: Config): Promise => { token.content )}')`, () => { - dbManager.run('CREATE TABLE users (uid varchar(8), mobile varchar(12), mail varchar(32))', () => { - dbManager.close((err) => { - /* istanbul ignore if */ - if(err != null) { - console.error(err) - reject(err) - } else { - resolve() - } - }) - }) + dbManager.run( + 'CREATE TABLE users (uid varchar(8), mobile varchar(12), mail varchar(32))', + () => { + dbManager.close((err) => { + /* istanbul ignore if */ + if (err != null) { + console.error(err) + reject(err) + } else { + resolve() + } + }) + } + ) } ) ) - matrixDbManager.run('CREATE TABLE users (uid varchar(8), name varchar(32), mobile varchar(12), mail varchar(32))') + matrixDbManager.run( + 'CREATE TABLE users (uid varchar(8), name varchar(32), mobile varchar(12), mail varchar(32))' + ) }) } diff --git a/packages/tom-server/src/vault-api/__testData__/config.json b/packages/tom-server/src/vault-api/__testData__/config.json index 8aeedfa2..42126ec0 100644 --- a/packages/tom-server/src/vault-api/__testData__/config.json +++ b/packages/tom-server/src/vault-api/__testData__/config.json @@ -6,4 +6,4 @@ "registration_file_path": "registration.yaml", "sender_localpart": "twake", "server_name": "matrix.org" -} \ No newline at end of file +} diff --git a/packages/tom-server/src/vault-api/controllers/vault.test.ts b/packages/tom-server/src/vault-api/controllers/vault.test.ts index 88ae7dac..02d8e435 100644 --- a/packages/tom-server/src/vault-api/controllers/vault.test.ts +++ b/packages/tom-server/src/vault-api/controllers/vault.test.ts @@ -1,8 +1,13 @@ import { type NextFunction, type Request, type Response } from 'express' -import { type TwakeDB } from '../../db' +import { type TwakeDB } from '../../types' import { type tokenDetail } from '../middlewares/auth' import { VaultAPIError, type expressAppHandler } from '../utils' -import { getRecoveryWords, methodNotAllowed, saveRecoveryWords } from './vault' +import { + getRecoveryWords, + methodNotAllowed, + saveRecoveryWords, + updateRecoveryWords +} from './vault' const words = 'This is a test sentence' @@ -14,7 +19,8 @@ describe('Vault controllers', () => { const dbManager: Partial = { get: jest.fn(), insert: jest.fn(), - deleteWhere: jest.fn() + deleteWhere: jest.fn(), + update: jest.fn() } let mockRequest: ITestRequest let mockResponse: Partial @@ -69,6 +75,7 @@ describe('Vault controllers', () => { // Testing saveRecoveryWords it('should return response with status code 201 on save success', async () => { jest.spyOn(dbManager, 'insert').mockResolvedValue([{ words }]) + jest.spyOn(dbManager, 'get').mockResolvedValue([]) const handler: expressAppHandler = saveRecoveryWords(dbManager as TwakeDB) handler(mockRequest as Request, mockResponse as Response, nextFunction) await new Promise(process.nextTick) @@ -78,12 +85,34 @@ describe('Vault controllers', () => { it('should call next function to throw error on saving failed', async () => { const errorMsg = 'Insert failed' jest.spyOn(dbManager, 'insert').mockRejectedValue(new Error(errorMsg)) + jest.spyOn(dbManager, 'get').mockResolvedValue([]) const handler: expressAppHandler = saveRecoveryWords(dbManager as TwakeDB) handler(mockRequest as Request, mockResponse as Response, nextFunction) await new Promise(process.nextTick) expect(nextFunction).toHaveBeenCalledWith(new Error(errorMsg)) }) + it('should return a 409 response when recovery words already exists', async () => { + jest + .spyOn(dbManager, 'get') + .mockResolvedValue([{ words: 'Another sentence for the same user' }]) + const handler: expressAppHandler = saveRecoveryWords(dbManager as TwakeDB) + handler(mockRequest as Request, mockResponse as Response, nextFunction) + await new Promise(process.nextTick) + expect(mockResponse.statusCode).toEqual(409) + expect(dbManager.insert).not.toHaveBeenCalled() + }) + + it('should return a 400 error if the body does not contain recovery words', async () => { + jest.spyOn(dbManager, 'get').mockResolvedValue([]) + const handler: expressAppHandler = saveRecoveryWords(dbManager as TwakeDB) + const emptyRequest = { ...mockRequest, body: {} } + handler(emptyRequest as Request, mockResponse as Response, nextFunction) + await new Promise(process.nextTick) + expect(mockResponse.statusCode).toEqual(400) + expect(dbManager.insert).not.toHaveBeenCalled() + }) + // Testing getRecoveryWords it('should return response with status code 200 on get success', async () => { @@ -127,4 +156,31 @@ describe('Vault controllers', () => { await new Promise(process.nextTick) expect(nextFunction).toHaveBeenCalledWith(new Error(errorMsg)) }) + + it('should return a 200 response on update success', async () => { + jest + .spyOn(dbManager, 'get') + .mockResolvedValue([{ userId: 'test', words: 'some recovery words' }]) + const handler: expressAppHandler = updateRecoveryWords(dbManager as TwakeDB) + handler(mockRequest as Request, mockResponse as Response, nextFunction) + await new Promise(process.nextTick) + expect(mockResponse.statusCode).toEqual(200) + }) + + it('should throw a 404 error when no recovery words were found', async () => { + jest.spyOn(dbManager, 'get').mockResolvedValue([]) + const handler: expressAppHandler = updateRecoveryWords(dbManager as TwakeDB) + handler(mockRequest as Request, mockResponse as Response, nextFunction) + await new Promise(process.nextTick) + expect(mockResponse.statusCode).toEqual(404) + }) + + it('should throw a 400 error when the body does not contain recovery words', async () => { + jest.spyOn(dbManager, 'get').mockResolvedValue([{ userId: 'test' }]) + const handler: expressAppHandler = updateRecoveryWords(dbManager as TwakeDB) + const emptyRequest = { ...mockRequest, body: {} } + handler(emptyRequest as Request, mockResponse as Response, nextFunction) + await new Promise(process.nextTick) + expect(mockResponse.statusCode).toEqual(400) + }) }) diff --git a/packages/tom-server/src/vault-api/controllers/vault.ts b/packages/tom-server/src/vault-api/controllers/vault.ts index 049f0fc2..820aff18 100644 --- a/packages/tom-server/src/vault-api/controllers/vault.ts +++ b/packages/tom-server/src/vault-api/controllers/vault.ts @@ -1,4 +1,6 @@ -import { type TwakeDB } from '../../db' +/* eslint-disable @typescript-eslint/no-misused-promises */ +/* eslint-disable no-useless-return */ +import { type TwakeDB, type twakeDbCollections } from '../../types' import { VaultAPIError, type expressAppHandler } from '../utils' export type VaultController = (db: TwakeDB) => expressAppHandler @@ -7,27 +9,51 @@ export const methodNotAllowed: expressAppHandler = (req, res, next) => { throw new VaultAPIError('Method not allowed', 405) } +/** + * Save use recovery words + * + * @param {TwakeDB} db - the database instance + * @retuns {expressAppHandler} - the express handler + */ export const saveRecoveryWords = (db: TwakeDB): expressAppHandler => { - return (req, res, next) => { - const data: Record = { - userId: req.token.content.sub, - words: req.body.words + return async (req, res, next) => { + const { words } = req.body + const userId = req.token.content.sub + + try { + if (words === undefined || words.length === 0) { + res.status(400).json({ error: 'Missing recovery words' }) + return + } + + const data = await db.get( + 'recoveryWords' as twakeDbCollections, + ['words'], + { + userId + } + ) + + if (data.length > 0) { + res.status(409).json({ error: 'User already has recovery words' }) + return + } else { + await db.insert('recoveryWords' as twakeDbCollections, { + userId, + words + }) + res.status(201).json({ message: 'Saved recovery words successfully' }) + return + } + } catch (err) { + next(err) } - // @ts-expect-error 'recoveryWords' isn't declared in Collection - db.insert('recoveryWords', data) - .then((_) => { - res.status(201).json({ message: 'Saved recovery words sucessfully' }) - }) - .catch((err) => { - next(err) - }) } } export const getRecoveryWords = (db: TwakeDB): expressAppHandler => { return (req, res, next) => { const userId: string = req.token.content.sub - // @ts-expect-error recoveryWords isn't declared in Collections db.get('recoveryWords', ['words'], { userId }) .then((data) => { if (data.length === 0) { @@ -53,7 +79,6 @@ export const deleteRecoveryWords = (db: TwakeDB): expressAppHandler => { return (req, res, next) => { const userId: string = req.token.content.sub - // @ts-expect-error recoveryWords isn't declared in Collections db.get('recoveryWords', ['words'], { userId }) .then((data) => { if (data.length === 0) { @@ -79,3 +104,48 @@ export const deleteRecoveryWords = (db: TwakeDB): expressAppHandler => { }) } } + +/** + * Update recovery words in database + * + * @param {TwakeDB} db - the database instance + * @returns {expressAppHandler} - the express controller handler + */ +export const updateRecoveryWords = (db: TwakeDB): expressAppHandler => { + return async (req, res, next) => { + const userId: string = req.token.content.sub + const { words } = req.body + + try { + if (words === undefined || words.length === 0) { + res.status(400).json({ message: 'Missing recovery sentence' }) + return + } + + const data = await db.get( + 'recoveryWords' as twakeDbCollections, + ['words'], + { + userId + } + ) + + if (data.length === 0) { + res.status(404).json({ message: 'User has no recovery sentence' }) + return + } + + await db.update( + 'recoveryWords' as twakeDbCollections, + { words }, + 'userId', + userId + ) + + res.status(200).json({ message: 'Updated recovery words successfully' }) + return + } catch (err) { + next(err) + } + } +} diff --git a/packages/tom-server/src/vault-api/index.test.ts b/packages/tom-server/src/vault-api/index.test.ts index de57e287..dec9e453 100644 --- a/packages/tom-server/src/vault-api/index.test.ts +++ b/packages/tom-server/src/vault-api/index.test.ts @@ -121,7 +121,7 @@ describe('Vault API server', () => { }) it('reject not allowed method with 405', async () => { - const response = await request(app).put(endpoint) + const response = await request(app).patch(endpoint) expect(response.statusCode).toBe(405) expect(response.body).toStrictEqual({ error: 'Method not allowed' @@ -145,7 +145,7 @@ describe('Vault API server', () => { .set('Authorization', `Bearer ${accessToken}`) expect(response.statusCode).toBe(201) expect(response.body).toStrictEqual({ - message: 'Saved recovery words sucessfully' + message: 'Saved recovery words successfully' }) }) @@ -174,7 +174,6 @@ describe('Vault API server', () => { const recoverySentence = 'This is another recovery sentence' await new Promise((resolve, reject) => { vaultApiServer.db - // @ts-expect-error recoveryWords not in Collections ?.insert('recoveryWords', { userId: matrixServerResponseBody.user_id, words: recoverySentence @@ -206,7 +205,7 @@ describe('Vault API server', () => { .set('Authorization', `Bearer ${unsavedToken}`) expect(response.statusCode).toBe(201) expect(response.body).toStrictEqual({ - message: 'Saved recovery words sucessfully' + message: 'Saved recovery words successfully' }) await removeUserInAccessTokenTable(unsavedToken) await removeUserInRecoveryWordsTable(matrixServerResponseBody.user_id) @@ -236,6 +235,18 @@ describe('Vault API server', () => { }) }) + it('should update words in the dabase if the connected user have some', async () => { + const response = await request(app) + .put(endpoint) + .send({ words }) + .set('Authorization', `Bearer ${accessToken}`) + + expect(response.statusCode).toBe(200) + expect(response.body).toStrictEqual({ + message: 'Updated recovery words successfully' + }) + }) + it('should reject if more than 100 requests are done in less than 10 seconds on get words', async () => { let response let token @@ -269,7 +280,6 @@ describe('Vault API server', () => { // eslint-disable-next-line @typescript-eslint/return-await return new Promise((resolve, reject) => { vaultApiServer.db - // @ts-expect-error matrixTokens isn't member of Collections ?.deleteEqual('matrixTokens', 'id', accessToken) .then(() => { resolve() @@ -284,7 +294,6 @@ describe('Vault API server', () => { // eslint-disable-next-line @typescript-eslint/return-await return new Promise((resolve, reject) => { vaultApiServer.db - // @ts-expect-error recoveryWords not in Collections ?.deleteEqual('recoveryWords', 'userId', userId) .then(() => { resolve() diff --git a/packages/tom-server/src/vault-api/index.ts b/packages/tom-server/src/vault-api/index.ts index be462b84..652ff34a 100644 --- a/packages/tom-server/src/vault-api/index.ts +++ b/packages/tom-server/src/vault-api/index.ts @@ -1,12 +1,12 @@ import { Router } from 'express' import defaultConfDesc from '../config.json' -import { type TwakeDB } from '../db' -import { type AuthenticationFunction } from '../types' +import type { AuthenticationFunction, TwakeDB } from '../types' import { deleteRecoveryWords, getRecoveryWords, methodNotAllowed, saveRecoveryWords, + updateRecoveryWords, type VaultController } from './controllers/vault' import isAuth, { type tokenDetail } from './middlewares/auth' @@ -155,6 +155,49 @@ export default class TwakeVaultAPI { * $ref: '#/components/responses/InternalServerError' */ .delete(...this._middlewares(deleteRecoveryWords)) + /** + * @openapi + * '/_twake/recoveryWords': + * put: + * tags: + * - Vault API + * description: Update stored connected user recovery words in database + * requestBody: + * description: Object containing the recovery words of the connected user + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * words: + * type: string + * description: The new recovery words of the connected user + * required: + * - words + * example: + * words: This is the updated recovery sentence of rtyler + * responses: + * 200: + * description: Success + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * description: Message indicating that words have been successfully updated + * example: + * message: Updated recovery words sucessfully + * 401: + * $ref: '#/components/responses/Unauthorized' + * 500: + * $ref: '#/components/responses/InternalServerError' + * 400: + * description: Bad request + */ + .put(...this._middlewares(updateRecoveryWords)) .all(allowCors, methodNotAllowed, errorMiddleware) } diff --git a/packages/tom-server/src/vault-api/middlewares/auth.test.ts b/packages/tom-server/src/vault-api/middlewares/auth.test.ts index 904c6575..89870832 100644 --- a/packages/tom-server/src/vault-api/middlewares/auth.test.ts +++ b/packages/tom-server/src/vault-api/middlewares/auth.test.ts @@ -49,11 +49,11 @@ const mockRequestDefaultProperties: Partial = { } jest - .spyOn(IdentityServerDb.default.prototype, 'get') + .spyOn(IdentityServerDb.prototype, 'get') .mockResolvedValue([{ id: token.value, data: JSON.stringify(token.content) }]) jest - .spyOn(IdentityServerDb.default.prototype, 'insert') + .spyOn(IdentityServerDb.prototype, 'insert') .mockResolvedValue([{ id: token.value, data: JSON.stringify(token.content) }]) const idServer = new IdServer( diff --git a/packages/tom-server/src/wellKnown/index.ts b/packages/tom-server/src/wellKnown/index.ts index f5563e44..302ebcb8 100644 --- a/packages/tom-server/src/wellKnown/index.ts +++ b/packages/tom-server/src/wellKnown/index.ts @@ -95,7 +95,7 @@ * server_name: example.com */ -import { Utils } from '@twake/matrix-identity-server' +import { send } from '@twake/utils' import { type Config, type expressAppHandler } from '../types' interface WellKnownType { @@ -193,7 +193,7 @@ class WellKnown { issuer: conf.oidc_issuer } } - Utils.send(res, 200, wellKnown) + send(res, 200, wellKnown) } this.api = { get: { diff --git a/packages/tom-server/templates/3pidInvitation.tpl b/packages/tom-server/templates/3pidInvitation.tpl new file mode 100644 index 00000000..e5bbad27 --- /dev/null +++ b/packages/tom-server/templates/3pidInvitation.tpl @@ -0,0 +1,67 @@ +Date: __date__ +From: __from__ +To: __to__ +Message-ID: __messageid__ +Subject: Invitation to join a Matrix room +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="__multipart_boundary__" + +--__multipart_boundary__ +Content-Type: text/plain; charset=UTF-8 +Content-Disposition: inline +Hello, + +You have been invited to join a Matrix room by __inviter_name__. If you possess a Matrix account, please consider binding this email address to your account in order to accept the invitation. + + +About Matrix: +Matrix is an open standard for interoperable, decentralised, real-time communication over IP. It can be used to power Instant Messaging, VoIP/WebRTC signalling, Internet of Things communication - or anywhere you need a standard HTTP API for publishing and subscribing to data whilst tracking the conversation history. + +Matrix defines the standard, and provides open source reference implementations of Matrix-compatible Servers, Clients, Client SDKs and Application Services to help you create new communication solutions or extend the capabilities and reach of existing ones. + +--__multipart_boundary__ +Content-Type: text/html; charset=UTF-8 +Content-Disposition: inline + + + + + +Invitation to join a Matrix room + + + +

Hello,

+ +

You have been invited to join a Matrix room by __inviter_name__. If you possess a Matrix account, please consider binding this email address to your account in order to accept the invitation.

+ +

If your client requires a code, the code is __token__

+ +
+

Invitation Details:

+
    +
  • Inviter: __inviter_name__ (display name: __inviter_display_name__)
  • +
  • Room Name: __room_name__
  • +
  • Room Type: __room_type__
  • +
  • Room Avatar: Room Avatar
  • +
+ +
+

About Matrix:

+ +

Matrix is an open standard for interoperable, decentralised, real-time communication over IP. It can be used to power Instant Messaging, VoIP/WebRTC signalling, Internet of Things communication - or anywhere you need a standard HTTP API for publishing and subscribing to data whilst tracking the conversation history.

+ +

Matrix defines the standard, and provides open source reference implementations of Matrix-compatible Servers, Clients, Client SDKs and Application Services to help you create new communication solutions or extend the capabilities and reach of existing ones.

+ + + + +--__multipart_boundary__-- + diff --git a/packages/tom-server/templates/mailVerification.tpl b/packages/tom-server/templates/mailVerification.tpl index 08fbe672..6a5c4740 100644 --- a/packages/tom-server/templates/mailVerification.tpl +++ b/packages/tom-server/templates/mailVerification.tpl @@ -15,7 +15,6 @@ We have received a request to use this email address with a matrix.org identity server. If this was you who made this request, you may use the following link to complete the verification of your email address: __link__ -If your client requires a code, the code is __token__ If you aren't aware of making such a request, please disregard this email. About Matrix: Matrix is an open standard for interoperable, decentralised, real-time communication diff --git a/packages/utils/README.md b/packages/utils/README.md new file mode 100644 index 00000000..e9bf3322 --- /dev/null +++ b/packages/utils/README.md @@ -0,0 +1,9 @@ +# @twake/utils + +Utilitaries methods for Twake + +## Copyright and license + +Copyright (c) 2023-present Linagora + +License: [GNU AFFERO GENERAL PUBLIC LICENSE](https://ci.linagora.com/publicgroup/oss/twake/tom-server/-/blob/master/LICENSE) diff --git a/packages/utils/jest.config.js b/packages/utils/jest.config.js new file mode 100644 index 00000000..e9476ecf --- /dev/null +++ b/packages/utils/jest.config.js @@ -0,0 +1,5 @@ +import jestConfigBase from '../../jest-base.config.js' + +export default { + ...jestConfigBase +} diff --git a/packages/utils/package.json b/packages/utils/package.json new file mode 100644 index 00000000..8a80b889 --- /dev/null +++ b/packages/utils/package.json @@ -0,0 +1,54 @@ +{ + "name": "@twake/utils", + "version": "0.0.1", + "description": "Utilitary methods for Twake server", + "keywords": [ + "matrix", + "twake" + ], + "homepage": "https://ci.linagora.com/publicgroup/oss/twake/tom-server", + "bugs": { + "url": "https://ci.linagora.com/publicgroup/oss/twake/tom-server/-/issues" + }, + "repository": { + "type": "git", + "url": "https://ci.linagora.com/publicgroup/oss/twake/tom-server.git" + }, + "license": "AGPL-3.0-or-later", + "authors": [ + { + "name": "Xavier Guimard", + "email": "yadd@debian.org" + }, + { + "name": "Mathias Perez", + "email": "mathias.perez.2022@polytechnique.org" + }, + { + "name": "Hippolyte Wallaert", + "email": "hippolyte.wallaert.2022@polytechnique.org" + }, + { + "name": "Amine Chraibi", + "email": "amine.chraibi.2022@polytechnique.org" + } + ], + "type": "module", + "exports": { + "import": "./dist/index.js" + }, + "main": "dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "package.json", + "dist", + "*.md" + ], + "scripts": { + "build": "rollup -c", + "test": "jest" + }, + "dependencies": { + "@twake/logger": "*" + } +} diff --git a/packages/utils/rollup.config.js b/packages/utils/rollup.config.js new file mode 100644 index 00000000..846f28cc --- /dev/null +++ b/packages/utils/rollup.config.js @@ -0,0 +1,3 @@ +import config from '../../rollup-template.js' + +export default config(['express', '@twake/logger']) diff --git a/packages/matrix-identity-server/src/utils/errors.test.ts b/packages/utils/src/errors.test.ts similarity index 100% rename from packages/matrix-identity-server/src/utils/errors.test.ts rename to packages/utils/src/errors.test.ts diff --git a/packages/utils/src/errors.ts b/packages/utils/src/errors.ts new file mode 100644 index 00000000..ed0f2add --- /dev/null +++ b/packages/utils/src/errors.ts @@ -0,0 +1,153 @@ +export const errCodes = { + // Not authorizated (not authenticated) + forbidden: 'M_FORBIDDEN', + + // Not authorized + unAuthorized: 'M_UNAUTHORIZED', + + // The resource requested could not be located. + notFound: 'M_NOT_FOUND', + + // The request was missing one or more parameters. + missingParams: 'M_MISSING_PARAMS', + + // The request contained one or more invalid parameters. + invalidParam: 'M_INVALID_PARAM', + + // The request contained unsupported additional parameters. + unknownParam: 'UNKNOWN_PARAM', + + // The session has not been validated. + sessionNotValidated: 'M_SESSION_NOT_VALIDATED', + + // A session could not be located for the given parameters. + noValidSession: 'M_NO_VALID_SESSION', + + // The session has expired and must be renewed. + sessionExpired: 'M_SESSION_EXPIRED', + + // The email address provided was not valid. + invalidEmail: 'M_INVALID_EMAIL', + + // There was an error sending an email. Typically seen when attempting to verify ownership of a given email address. + emailSendError: 'M_EMAIL_SEND_ERROR', + + // The provided third party address was not valid. + invalidAddress: 'M_INVALID_ADDRESS', + + // There was an error sending a notification. Typically seen when attempting to verify ownership of a given third party address. + sendError: 'M_SEND_ERROR', + + // Server requires some policies + termsNotSigned: 'M_TERMS_NOT_SIGNED', + + // The third party identifier is already in use by another user. Typically this error will have an additional mxid property to indicate who owns the third party identifier. + threepidInUse: 'M_THREEPID_IN_USE', + + // An unknown error has occurred. + unknown: 'M_UNKNOWN', + + // The request contained an unrecognised value, such as an unknown token or medium. + // This is also used as the response if a server did not understand the request. This is expected to be returned with a 404 HTTP status code if the endpoint is not implemented or a 405 HTTP status code if the endpoint is implemented, but the incorrect HTTP method is used. + unrecognized: 'M_UNRECOGNIZED', + + // An additional response parameter, soft_logout, might be present on the response for 401 HTTP status codes + unknownToken: 'M_UNKNOWN_TOKEN', + + // No access token was specified for the request + missingToken: 'M_MISSING_TOKEN', + + // The registration token has been used too many times or has expired and is now invalid. + invalidToken: 'INVALID_TOKEN', + + // Request contained valid JSON, but it was malformed in some way, e.g. missing required keys, invalid values for keys + badJson: 'M_BAD_JSON', + + // Request did not contain valid JSON + notJson: 'M_NOT_JSON', + + // Too many requests have been sent in a short period of time. Wait a while then try again. + limitExceeded: 'M_LIMIT_EXCEEDED', + + // The user ID associated with the request has been deactivated. Typically for endpoints that prove authentication, such as /login. + userDeactivated: 'M_USER_DEACTIVATED', + + // Encountered when trying to register a user ID which has been taken. + userInUse: 'M_USER_IN_USE', + + // Encountered when trying to register a user ID which is not valid. + invalidUsername: 'M_INVALID_USERNAME', + + // Sent when the room alias given to the createRoom API is already in use. + roomInUse: 'M_ROOM_IN_USE', + + // Sent when the initial state given to the createRoom API is invalid. + invalidRoomState: 'M_INVALID_ROOM_STATE', + + // Sent when a threepid given to an API cannot be used because no record matching the threepid was found. + threepidNotFound: 'M_THREEPID_NOT_FOUND', + + // Authentication could not be performed on the third-party identifier. + threepidAuthFailed: 'M_THREEPID_AUTH_FAILED', + + // The server does not permit this third-party identifier. This may happen if the server only permits, for example, email addresses from a particular domain. + threepidDenied: 'M_THREEPID_DENIED', + + // The client’s request used a third-party server, e.g. identity server, that this server does not trust. + serverNotTrusted: 'M_SERVER_NOT_TRUSTED', + + // The client’s request to create a room used a room version that the server does not support. + unsupportedRoomVersion: 'M_UNSUPPORTED_ROOM_VERSION', + + // The client attempted to join a room that has a version the server does not support. Inspect the room_version property of the error response for the room’s version. + incompatibleRoomVersion: 'M_INCOMPATIBLE_ROOM_VERSION', + + // The state change requested cannot be performed, such as attempting to unban a user who is not banned. + badState: 'M_BAD_STATE', + + // The room or resource does not permit guests to access it. + guestAccessForbidden: 'M_GUEST_ACCESS_FORBIDDEN', + + // A Captcha is required to complete the request. + captchaNeeded: 'M_CAPTCHA_NEEDED', + + // The Captcha provided did not match what was expected. + captchaInvalid: 'M_CAPTCHA_INVALID', + + // A required parameter was missing from the request. + missingParam: 'M_MISSING_PARAM', + + // The request or entity was too large. + tooLarge: 'M_TOO_LARGE', + + // The resource being requested is reserved by an application service, or the application service making the request has not created the resource. + exclusive: 'M_EXCLUSIVE', + + // The request cannot be completed because the homeserver has reached a resource limit imposed on it. For example, a homeserver held in a shared hosting environment may reach a resource limit if it starts using too much memory or disk space. The error MUST have an admin_contact field to provide the user receiving the error a place to reach out to. Typically, this error will appear on routes which attempt to modify state (e.g.: sending messages, account data, etc) and not routes which only read state (e.g.: /sync, get account data, etc). + resourceLimitExceeded: 'M_RESOURCE_LIMIT_EXCEEDED', + + // The user is unable to reject an invite to join the server notices room. See the Server Notices module for more information. + cannotLeaveServerNoticeRoom: 'M_CANNOT_LEAVE_SERVER_NOTICE_ROOM' +} as const + +export const defaultMsg = (s: string): string => { + return s + .replace(/^M_/, '') + .split('_') + .map((s) => { + const t = s.toLowerCase() + return t.charAt(0).toUpperCase() + t.slice(1) + }) + .join(' ') +} + +export const errMsg = ( + code: keyof typeof errCodes, + explanation?: string +): object => { + const errCode = errCodes[code] + return { + errcode: errCode, + error: explanation != null ? explanation : defaultMsg(errCode) + } +} diff --git a/packages/utils/src/eventTypes.ts b/packages/utils/src/eventTypes.ts new file mode 100644 index 00000000..c3fb9a20 --- /dev/null +++ b/packages/utils/src/eventTypes.ts @@ -0,0 +1,50 @@ +/* istanbul ignore file */ + +// TODO : Verify the content of this file while we implement the spec. There is absolutely no guarantee that this is correct. + +export const eventTypes = { + // State Events + roomCreate: 'm.room.create', + roomMember: 'm.room.member', + roomPowerLevels: 'm.room.power_levels', + roomJoinRules: 'm.room.join_rules', + roomHistoryVisibility: 'm.room.history_visibility', + roomThirdPartyInvite: 'm.room.third_party_invite', + roomName: 'm.room.name', + roomTopic: 'm.room.topic', + roomAvatar: 'm.room.avatar', + roomCanonicalAlias: 'm.room.canonical_alias', + roomAliases: 'm.room.aliases', // Note: deprecated + roomEncryption: 'm.room.encryption', + roomGuestAccess: 'm.room.guest_access', + roomServerAcl: 'm.room.server_acl', + roomPinnedEvents: 'm.room.pinned_events', + roomTombstone: 'm.room.tombstone', + roomRelatedGroups: 'm.room.related_groups', + spaceChild: 'm.space.child', + spaceParent: 'm.space.parent', + + // Non-State Events + roomMessage: 'm.room.message', + roomEncrypted: 'm.room.encrypted', + roomRedaction: 'm.room.redaction', + reaction: 'm.reaction', + callInvite: 'm.call.invite', + callCandidates: 'm.call.candidates', + callAnswer: 'm.call.answer', + callHangup: 'm.call.hangup', + presence: 'm.presence', + typing: 'm.typing', + receipt: 'm.receipt', + direct: 'm.direct', + pushRules: 'm.push_rules', + sticker: 'm.sticker', + tag: 'm.tag', + roomMessageFeedback: 'm.room.message.feedback', + notification: 'm.notification', + customEvent: 'm.custom.event' +} + +export const validEventTypes = Array.from( + new Set(Object.values(eventTypes)) +) diff --git a/packages/utils/src/index.test.ts b/packages/utils/src/index.test.ts new file mode 100644 index 00000000..2bc19044 --- /dev/null +++ b/packages/utils/src/index.test.ts @@ -0,0 +1,333 @@ +import { type Request, type Response } from 'express' +import type http from 'http' +import querystring from 'querystring' +import { + send, + jsonContent, + validateParameters, + epoch, + toMatrixId, + isValidUrl, + validateParametersStrict, + getAccessToken +} from './index' +import { type TwakeLogger } from '@twake/logger' + +describe('Utility Functions', () => { + let mockResponse: Partial + let mockLogger: TwakeLogger + + beforeEach(() => { + mockResponse = { + writeHead: jest.fn(), + write: jest.fn(), + end: jest.fn() + } + + mockLogger = { + error: jest.fn(), + warn: jest.fn(), + log: jest.fn(), + info: jest.fn(), + debug: jest.fn() + } as unknown as TwakeLogger + }) + + describe('send', () => { + it('should send a response with JSON content', () => { + send(mockResponse as Response, 200, { message: 'ok' }) + + expect(mockResponse.writeHead).toHaveBeenCalledWith(200, { + 'Content-Type': 'application/json; charset=utf-8', + 'Content-Length': 16, + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': + 'Origin, X-Requested-With, Content-Type, Accept, Authorization' + }) + expect(mockResponse.write).toHaveBeenCalledWith( + JSON.stringify({ message: 'ok' }) + ) + expect(mockResponse.end).toHaveBeenCalled() + }) + + it('should log the response status with info if status code in 200-299', () => { + send(mockResponse as Response, 200, { message: 'ok' }, mockLogger) + + expect(mockLogger.debug).toHaveBeenCalledWith( + 'Sending status 200 with content {"message":"ok"}' + ) + }) + + it('should log the response status with error if status code not in 200-299', () => { + send(mockResponse as Response, 400, { message: 'error' }, mockLogger) + + expect(mockLogger.error).toHaveBeenCalledWith( + 'Sending status 400 with content {"message":"error"}' + ) + }) + }) + + describe('jsonContent', () => { + it('should parse JSON content and call the callback', (done) => { + const req = { + headers: { 'content-type': 'application/json' }, + on: (event: string, callback: any) => { + if (event === 'data') { + callback(JSON.stringify({ key: 'value' })) + } + if (event === 'end') { + callback() + } + } + } as unknown as Request + + jsonContent( + req, + mockResponse as Response, + mockLogger, + (obj: Record) => { + expect(obj).toEqual({ key: 'value' }) + done() + } + ) + }) + + it('should handle form-urlencoded content', (done) => { + const req = { + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + on: (event: string, callback: any) => { + if (event === 'data') { + callback(querystring.stringify({ key: 'value' })) + } + if (event === 'end') { + callback() + } + } + } as unknown as Request + + jsonContent( + req, + mockResponse as Response, + mockLogger, + (obj: Record) => { + expect(obj).toEqual({ key: 'value' }) + done() + } + ) + }) + + it('should handle JSON parsing errors', (done) => { + const req = { + headers: { 'content-type': 'application/json' }, + on: (event: string, callback: any) => { + if (event === 'data') { + // eslint-disable-next-line n/no-callback-literal + callback('invalid json') + } + if (event === 'end') { + callback() + } + } + } as unknown as Request + + jsonContent(req, mockResponse as Response, mockLogger, () => { + // No-op + }) + + setImmediate(() => { + expect(mockLogger.error).toHaveBeenCalled() + expect(mockResponse.writeHead).toHaveBeenCalledWith( + 400, + expect.any(Object) + ) + expect(mockResponse.write).toHaveBeenCalled() + expect(mockResponse.end).toHaveBeenCalled() + done() + }) + }) + }) + + describe('validateParameters', () => { + it('should validate required parameters', () => { + const desc = { key: true, optional: false } + const content = { key: 'value' } + + validateParameters( + mockResponse as Response, + desc, + content, + mockLogger, + (obj) => { + expect(obj).toEqual(content) + } + ) + + expect(mockResponse.writeHead).not.toHaveBeenCalled() + }) + + it('should return an error for missing parameters', () => { + const desc = { key: true, missing: true } + const content = { key: 'value' } + + validateParameters( + mockResponse as Response, + desc, + content, + mockLogger, + () => { + // No-op + } + ) + + expect(mockResponse.writeHead).toHaveBeenCalledWith( + 400, + expect.any(Object) + ) + expect(mockResponse.write).toHaveBeenCalled() + expect(mockResponse.end).toHaveBeenCalled() + }) + + it('should log a warning for additional parameters', () => { + const desc = { key: true } + const content = { key: 'value', extra: 'extra' } + + validateParameters( + mockResponse as Response, + desc, + content, + mockLogger, + () => { + // No-op + } + ) + + expect(mockLogger.warn).toHaveBeenCalled() + expect(mockResponse.writeHead).not.toHaveBeenCalled() + }) + + it('should return an error for additional parameters in strict mode', () => { + const desc = { key: true } + const content = { key: 'value', extra: 'extra' } + + validateParametersStrict( + mockResponse as Response, + desc, + content, + mockLogger, + () => { + // No-op + } + ) + + expect(mockResponse.writeHead).toHaveBeenCalledWith( + 400, + expect.any(Object) + ) + expect(mockResponse.write).toHaveBeenCalled() + expect(mockResponse.end).toHaveBeenCalled() + }) + }) + + describe('epoch', () => { + it('should return the current timestamp', () => { + const now = Date.now() + jest.spyOn(Date, 'now').mockReturnValue(now) + + expect(epoch()).toBe(now) + }) + }) + + describe('toMatrixId', () => { + it('should return a Matrix ID for a valid localpart and server', () => { + expect(toMatrixId('localpart', 'server')).toBe('@localpart:server') + }) + + it('should throw an error for an invalid localpart', () => { + expect(() => + toMatrixId('@testuser:example.com', 'example.com') + ).toThrowError() + }) + + it('should throw an error for a localpart longer than 512 characters', () => { + const longLocalpart = 'a'.repeat(513) + expect(() => toMatrixId(longLocalpart, 'example.com')).toThrowError() + }) + }) + describe('isValidUrl', () => { + it('should return false for a non-string input', () => { + // @ts-expect-error Testing non-string input + expect(isValidUrl(12345)).toBe(false) + // @ts-expect-error Testing non-string input + expect(isValidUrl(null)).toBe(false) + // @ts-expect-error Testing non-string input + expect(isValidUrl(undefined)).toBe(false) + }) + it('should return false for an empty string', () => { + expect(isValidUrl('')).toBe(false) + }) + it('should return false for an invalid URL with invalid characters', () => { + expect(isValidUrl('https://exam ple.com')).toBe(false) + }) + it('should return true for a valid URL with query parameters', () => { + expect(isValidUrl('https://example.com/path?name=value')).toBe(true) + }) + + it('should return true for a valid URL with a port number', () => { + expect(isValidUrl('https://example.com:8080')).toBe(true) + }) + + it('should return false for an invalid URL missing scheme', () => { + expect(isValidUrl('example.com')).toBe(false) + }) + + it('should return false for an invalid URL missing domain', () => { + expect(isValidUrl('http://')).toBe(false) + }) + }) + describe('getAccessToken', () => { + it('should return the access token from the Authorization header', () => { + const req = { + headers: { + authorization: 'Bearer some-token' + }, + query: {} + } as unknown as Request + + const token = getAccessToken(req) + expect(token).toBe('some-token') + }) + + it('should return null if there is no authorization header', () => { + const req = { + headers: {}, + query: {} + } as unknown as Request + + const token = getAccessToken(req) + expect(token).toBeNull() + }) + + it('should return the access token from the query parameters', () => { + const req = { + headers: {}, + query: { + access_token: 'some-token' + } + } as unknown as Request + + const token = getAccessToken(req) + expect(token).toBe('some-token') + }) + + it('should return null if there is no token in headers or query', () => { + const req = { + headers: {}, + query: {} + } as unknown as Request + + const token = getAccessToken(req) + expect(token).toBeNull() + }) + }) +}) diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts new file mode 100644 index 00000000..05e5ace8 --- /dev/null +++ b/packages/utils/src/index.ts @@ -0,0 +1,5 @@ +/* istanbul ignore file */ +export * from './errors' +export * from './utils' +export * from './eventTypes' +export * from './regex' diff --git a/packages/utils/src/regex.test.ts b/packages/utils/src/regex.test.ts new file mode 100644 index 00000000..9823acf5 --- /dev/null +++ b/packages/utils/src/regex.test.ts @@ -0,0 +1,124 @@ +import { + isClientSecretValid, + isEventTypeValid, + isMatrixIdValid, + isRoomIdValid, + isSidValid, + isStateKeyValid, + isCountryValid, + isPhoneNumberValid, + isEmailValid, + isRoomAliasValid +} from './regex' + +describe('isClientSecretValid', () => { + it('should return true if the client secret is valid', () => { + expect(isClientSecretValid('abc123._=-')).toBe(true) + }) + + it('should return false if the client secret is invalid', () => { + expect(isClientSecretValid('abc')).toBe(false) + }) +}) + +describe('isEventTypeValid', () => { + it('should return true if the event type is valid', () => { + expect(isEventTypeValid('m.room.message')).toBe(true) + }) + + it('should return false if the event type is invalid', () => { + expect(isEventTypeValid('m.room..message')).toBe(false) + }) + + it('should return false if the event type is too long', () => { + expect(isEventTypeValid('m.' + 'a'.repeat(255))).toBe(false) + }) +}) + +describe('isMatrixIdValid', () => { + it('should return true if the matrix ID is valid', () => { + expect(isMatrixIdValid('@user:matrix.org')).toBe(true) + }) + + it('should return false if the matrix ID is invalid', () => { + expect(isMatrixIdValid('user:matrix.org')).toBe(false) + }) + + it('should return false if the matrix ID is too long', () => { + expect(isMatrixIdValid('@' + 'a'.repeat(256))).toBe(false) + }) +}) + +describe('isRoomIdValid', () => { + it('should return true if the room ID is valid', () => { + expect(isRoomIdValid('!abc123:matrix.org')).toBe(true) + }) + + it('should return false if the room ID is invalid', () => { + expect(isRoomIdValid('abc123:matrix.org')).toBe(false) + }) + + it('should return false if the room ID is too long', () => { + expect(isRoomIdValid('!' + 'a'.repeat(256))).toBe(false) + }) +}) + +describe('isSidValid', () => { + it('should return true if the sid is valid', () => { + expect(isSidValid('abc123._=-')).toBe(true) + }) + + it('should return false if the sid is invalid', () => { + expect(isSidValid('')).toBe(false) + }) +}) + +describe('isStateKeyValid', () => { + it('should return true if the state key is valid', () => { + expect(isStateKeyValid('stateKey')).toBe(true) + }) + + it('should return false if the state key is too long', () => { + expect(isStateKeyValid('a'.repeat(256))).toBe(false) + }) +}) + +describe('isCountryValid', () => { + it('should return true if the country code is valid', () => { + expect(isCountryValid('US')).toBe(true) + }) + + it('should return false if the country code is invalid', () => { + expect(isCountryValid('USA')).toBe(false) + }) +}) + +describe('isPhoneNumberValid', () => { + it('should return true if the phone number is valid', () => { + expect(isPhoneNumberValid('1234567890')).toBe(true) + }) + + it('should return false if the phone number is invalid', () => { + expect(isPhoneNumberValid('01234567890')).toBe(false) + }) +}) + +describe('isEmailValid', () => { + it('should return true if the email is valid', () => { + expect(isEmailValid('test@example.com')).toBe(true) + }) + + it('should return false if the email is invalid', () => { + expect(isEmailValid('test@com')).toBe(false) + }) +}) + +describe('isRoomAliasValid', () => { + it('should return true if the room alias is valid', () => { + expect(isRoomAliasValid('#room:matrix.org')).toBe(true) + }) + + it('should return false if the room alias is invalid', () => { + expect(isRoomAliasValid('room:matrix.org')).toBe(false) + }) +}) diff --git a/packages/utils/src/regex.ts b/packages/utils/src/regex.ts new file mode 100644 index 00000000..9875a023 --- /dev/null +++ b/packages/utils/src/regex.ts @@ -0,0 +1,49 @@ +/* Lists all the regex patterns used */ + +const clientSecretRegex: RegExp = /^[0-9a-zA-Z.=_-]{6,255}$/ +const eventTypeRegex: RegExp = /^(?:[a-z]+(?:\.[a-z][a-z0-9_]*)*)$/ // Following Java's package naming convention as per : https://spec.matrix.org/v1.11/#events +const matrixIdRegex: RegExp = /^@[0-9a-zA-Z._=-]+:[0-9a-zA-Z.-]+$/ +const senderLocalpartRegex: RegExp = /^[a-z0-9_\-./=+]+$/ +const roomIdRegex: RegExp = /^![0-9a-zA-Z._=/+-]+:[0-9a-zA-Z.-]+$/ // From : https://spec.matrix.org/v1.11/#room-structure +const sidRegex: RegExp = /^[0-9a-zA-Z.=_-]{1,255}$/ +const countryRegex: RegExp = /^[A-Z]{2}$/ // ISO 3166-1 alpha-2 as per the spec : https://spec.matrix.org/v1.11/client-server-api/#post_matrixclientv3registermsisdnrequesttoken +const phoneNumberRegex: RegExp = /^[1-9]\d{1,14}$/ +const emailRegex: RegExp = /^\w[+.-\w]*\w@\w[.-\w]*\w\.\w{2,6}$/ +const roomAliasRegex: RegExp = /^#[a-zA-Z0-9_\-=.+]+:[a-zA-Z0-9\-.]+$/ // From : https://spec.matrix.org/v1.11/#room-structure +const hostnameRe = + /^((([a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*([a-zA-Z][-a-zA-Z0-9]*[a-zA-Z0-9]|[a-zA-Z])(:(\d+))?$/ + +export const isClientSecretValid = (clientSecret: string): boolean => + clientSecretRegex.test(clientSecret) + +export const isEventTypeValid = (eventType: string): boolean => + eventTypeRegex.test(eventType) && Buffer.byteLength(eventType) < 256 + +export const isMatrixIdValid = (matrixId: string): boolean => + matrixIdRegex.test(matrixId) && Buffer.byteLength(matrixId) < 256 + +export const isSenderLocalpartValid = (senderLocalpart: string): boolean => + senderLocalpartRegex.test(senderLocalpart) && + Buffer.byteLength(senderLocalpart) < 256 + +export const isRoomIdValid = (roomId: string): boolean => + roomIdRegex.test(roomId) && Buffer.byteLength(roomId) < 256 + +export const isSidValid = (sid: string): boolean => sidRegex.test(sid) + +export const isStateKeyValid = (stateKey: string): boolean => + Buffer.byteLength(stateKey) < 256 + +export const isCountryValid = (country: string): boolean => + countryRegex.test(country) + +export const isPhoneNumberValid = (phoneNumber: string): boolean => + phoneNumberRegex.test(phoneNumber) + +export const isEmailValid = (email: string): boolean => emailRegex.test(email) + +export const isRoomAliasValid = (roomAlias: string): boolean => + roomAliasRegex.test(roomAlias) + +export const isHostnameValid = (hostname: string): boolean => + hostnameRe.test(hostname) diff --git a/packages/utils/src/size_limits.md b/packages/utils/src/size_limits.md new file mode 100644 index 00000000..29667c06 --- /dev/null +++ b/packages/utils/src/size_limits.md @@ -0,0 +1,15 @@ +## General Constraints + +- The complete event MUST NOT be larger than 65536 bytes when formatted with the federation event format, including any signatures, and encoded as Canonical JSON. + +## Size Restrictions Per Key + +| Key | Maximum Size | +|------------|------------------------------------------------------------------| +| sender | 255 bytes (including the `@` sigil and the domain) | +| room_id | 255 bytes (including the `!` sigil and the domain) | +| state_key | 255 bytes | +| type | 255 bytes | +| event_id | 255 bytes (including the `$` sigil and the domain where present) | + +Some event types have additional size restrictions which are specified in the description of the event. Additional restrictions exist also for specific room versions. Additional keys have no limit other than that implied by the total 64 KiB limit on events. diff --git a/packages/utils/src/utils.ts b/packages/utils/src/utils.ts new file mode 100644 index 00000000..468dd7fc --- /dev/null +++ b/packages/utils/src/utils.ts @@ -0,0 +1,209 @@ +/* istanbul ignore file */ +import { type TwakeLogger } from '@twake/logger' +import { type NextFunction, type Request, type Response } from 'express' +import type http from 'http' +import querystring from 'querystring' +import { errMsg } from './errors' + +export type expressAppHandler = ( + req: Request | http.IncomingMessage, + res: Response | http.ServerResponse, + next?: NextFunction +) => void + +export const send = ( + res: Response | http.ServerResponse, + status: number, + body: string | object, + logger?: TwakeLogger +): void => { + /* istanbul ignore next */ + const content = typeof body === 'string' ? body : JSON.stringify(body) + if (logger != null) { + const logMessage = `Sending status ${status} with content ${content}` + if (status >= 200 && status < 300) { + logger.debug(logMessage) + } else { + logger.error(logMessage) + } + } + res.writeHead(status, { + 'Content-Type': 'application/json; charset=utf-8', + 'Content-Length': Buffer.byteLength(content, 'utf-8'), + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': + 'Origin, X-Requested-With, Content-Type, Accept, Authorization' + }) + res.write(content) + res.end() +} + +export const jsonContent = ( + req: Request | http.IncomingMessage, + res: Response | http.ServerResponse, + logger: TwakeLogger, + callback: (obj: Record) => void +): void => { + let content = '' + let accept = true + req.on('data', (body: string) => { + content += body + }) + /* istanbul ignore next */ + req.on('error', (err) => { + send(res, 400, errMsg('unknown', err.toString())) + accept = false + }) + req.on('end', () => { + let obj + try { + // eslint-disable-next-line @typescript-eslint/prefer-optional-chain + if ( + req.headers['content-type']?.match( + /^application\/x-www-form-urlencoded/ + ) != null + ) { + obj = querystring.parse(content) + } else { + obj = JSON.parse(content) + } + } catch (err) { + logger.error('JSON error', err) + logger.error(`Content was: ${content}`) + send(res, 400, errMsg('unknown', err as string)) + accept = false + } + if (accept) callback(obj) + }) +} + +type validateParametersSchema = Record + +type validateParametersType = ( + res: Response | http.ServerResponse, + desc: validateParametersSchema, + content: Record, + logger: TwakeLogger, + callback: (obj: object) => void, + acceptAdditionalParameters?: boolean +) => void + +const _validateParameters: validateParametersType = ( + res, + desc, + content, + logger, + callback, + acceptAdditionalParameters +) => { + const missingParameters: string[] = [] + const additionalParameters: string[] = [] + // Check for required parameters + Object.keys(desc).forEach((key) => { + if (desc[key] && content[key] == null) { + missingParameters.push(key) + } + }) + if (missingParameters.length > 0) { + send( + res, + 400, + errMsg( + 'missingParams', + `Missing parameters ${missingParameters.join(', ')}` + ) + ) + } else { + Object.keys(content).forEach((key) => { + if (desc[key] == null) { + additionalParameters.push(key) + } + }) + if (additionalParameters.length > 0) { + if (acceptAdditionalParameters === false) { + logger.error('Additional parameters', additionalParameters) + send( + res, + 400, + errMsg( + 'unknownParam', + `Unknown additional parameters ${additionalParameters.join(', ')}` + ) + ) + } else { + logger.warn('Additional parameters', additionalParameters) + callback(content) + } + } else { + callback(content) + } + } +} + +export const validateParameters: validateParametersType = ( + res, + desc, + content, + logger, + callback +) => { + _validateParameters(res, desc, content, logger, callback, true) +} + +export const validateParametersStrict: validateParametersType = ( + res, + desc, + content, + logger, + callback +) => { + _validateParameters(res, desc, content, logger, callback, false) +} + +export const epoch = (): number => { + return Date.now() +} + +export const toMatrixId = (localpart: string, serverName: string): string => { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (!localpart.match(/^[a-z0-9_\-./=+]+$/)) { + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw errMsg('invalidUsername') + } + const userId = `@${localpart}:${serverName}` + if (userId.length > 255) { + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw errMsg('invalidUsername') + } + return userId +} + +export const isValidUrl = (link: string): boolean => { + try { + // eslint-disable-next-line no-new + new URL(link) + return true + } catch { + return false + } +} + +export const getAccessToken = ( + req: Request | http.IncomingMessage +): string | null => { + const tokenRe = /^Bearer (\S+)$/ + let token: string | null = null + if (req.headers.authorization != null) { + const re = req.headers.authorization.match(tokenRe) + if (re != null) { + token = re[1] + } + // @ts-expect-error req.query exists + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + } else if (req.query && Object.keys(req.query).length > 0) { + // @ts-expect-error req.query.access_token may be null + token = req.query.access_token + } + return token +} diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json new file mode 100644 index 00000000..5ddb593d --- /dev/null +++ b/packages/utils/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig-build.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src/**/*", "test/**/*"] +} diff --git a/tsconfig-test.json b/tsconfig-test.json index b548b0bd..92286378 100644 --- a/tsconfig-test.json +++ b/tsconfig-test.json @@ -14,8 +14,10 @@ "@twake/config-parser": ["./packages/config-parser/src"], "@twake/crypto": ["./packages/crypto/src"], "@twake/logger": ["./packages/logger/src"], + "@twake/utils": ["./packages/utils/src"], "@twake/matrix-application-server": ["./packages/matrix-application-server/src"], "@twake/matrix-identity-server": ["./packages/matrix-identity-server/src"], + "@twake/matrix-client-server": ["./packages/matrix-client-server/src"], "matrix-resolve": ["./packages/matrix-resolve/src"] } }