From 157de4dea573d79f7c32bf26c2ffb55f75320143 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Tue, 26 Nov 2024 21:25:05 +0100 Subject: [PATCH] make repository public Signed-off-by: Timo Glastra --- .env.example | 10 + .github/workflows/cd.yaml | 80 + .github/workflows/ci.yaml | 29 + .gitignore | 5 + biome.json | 43 + certificates/digital_ocean.cert | 25 + docker-compose.yaml | 28 + dockerfile | 19 + package.json | 35 + pnpm-lock.yaml | 1913 +++++++++++++++++++++ scripts/test.ts | 79 + src/constants.ts | 12 + src/database/Database.ts | 54 + src/database/index.ts | 1 + src/error/WalletIdNotFound.ts | 7 + src/error/index.ts | 0 src/hsm/GoogleHsm.ts | 90 + src/hsm/Hsm.ts | 25 + src/hsm/NodeHsm.ts | 71 + src/hsm/index.ts | 2 + src/hsm/keyType.ts | 4 + src/index.ts | 36 + src/server/index.ts | 50 + src/server/middlewares/addKeyRingId.ts | 34 + src/server/middlewares/authorization.ts | 60 + src/server/middlewares/db.ts | 7 + src/server/middlewares/hsm.ts | 7 + src/server/middlewares/index.ts | 5 + src/server/middlewares/validation.ts | 27 + src/server/oauth2/callbacks.ts | 26 + src/server/oauth2/clientAttestations.ts | 34 + src/server/routes/batchCreateKey.ts | 46 + src/server/routes/createKey.ts | 32 + src/server/routes/getPublicKey.ts | 18 + src/server/routes/getWalletAttestation.ts | 34 + src/server/routes/index.ts | 4 + src/server/routes/registerWallet.ts | 13 + src/server/routes/sign.ts | 24 + src/utils/base58.ts | 13 + src/utils/index.ts | 2 + src/utils/schemas.ts | 5 + tsconfig.json | 12 + 42 files changed, 3021 insertions(+) create mode 100644 .env.example create mode 100644 .github/workflows/cd.yaml create mode 100644 .github/workflows/ci.yaml create mode 100644 .gitignore create mode 100644 biome.json create mode 100644 certificates/digital_ocean.cert create mode 100644 docker-compose.yaml create mode 100644 dockerfile create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100755 scripts/test.ts create mode 100644 src/constants.ts create mode 100644 src/database/Database.ts create mode 100644 src/database/index.ts create mode 100644 src/error/WalletIdNotFound.ts create mode 100644 src/error/index.ts create mode 100644 src/hsm/GoogleHsm.ts create mode 100644 src/hsm/Hsm.ts create mode 100644 src/hsm/NodeHsm.ts create mode 100644 src/hsm/index.ts create mode 100644 src/hsm/keyType.ts create mode 100644 src/index.ts create mode 100644 src/server/index.ts create mode 100644 src/server/middlewares/addKeyRingId.ts create mode 100644 src/server/middlewares/authorization.ts create mode 100644 src/server/middlewares/db.ts create mode 100644 src/server/middlewares/hsm.ts create mode 100644 src/server/middlewares/index.ts create mode 100644 src/server/middlewares/validation.ts create mode 100644 src/server/oauth2/callbacks.ts create mode 100644 src/server/oauth2/clientAttestations.ts create mode 100644 src/server/routes/batchCreateKey.ts create mode 100644 src/server/routes/createKey.ts create mode 100644 src/server/routes/getPublicKey.ts create mode 100644 src/server/routes/getWalletAttestation.ts create mode 100644 src/server/routes/index.ts create mode 100644 src/server/routes/registerWallet.ts create mode 100644 src/server/routes/sign.ts create mode 100644 src/utils/base58.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/schemas.ts create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8694807 --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +PORT=3000 +HSM=node +POSTGRES_PASSWORD= +POSTGRES_USER= +POSTGRES_HOST= +POSTGRES_PORT= +POSTGRES_DATABASE= +POSTGRES_CA_PATH= +GOOGLE_APPLICATION_CREDENTIALS_JSON= +WALLET_PROVIDER_P256_PRIVATE_JWK='{"kty":"EC","x":"X8zXZKsb4i9PsjyZbfu7BR5KkqCurSJi34FytGFWo5w","y":"AKU94AcCkNnKfRRbDB4hygU7_UcpuKFJdRmAIc3ws_I","crv":"P-256","d":"eUWJiIyLqqDjPktYzaCtjsLbUwa8D4nThK_C38k4uLE"}' \ No newline at end of file diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml new file mode 100644 index 0000000..5bdd858 --- /dev/null +++ b/.github/workflows/cd.yaml @@ -0,0 +1,80 @@ +name: Continuous Deployment + +on: + workflow_dispatch: + inputs: + build: + default: true + type: boolean + required: false + description: Build the app before deploying +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + SERVER: server + +jobs: + build-and-push-image-server: + runs-on: ubuntu-latest + if: inputs.build == true && github.ref == 'refs/heads/main' + permissions: + contents: read + packages: write + + defaults: + run: + working-directory: ${{ env.SERVER }} + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Log in to the Container registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ env.SERVER }} + + - name: Build and push Docker image + uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + with: + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + deploy: + # Only run on main branch + runs-on: ubuntu-latest + needs: build-and-push-image-server + if: | + always() && + (needs.build-and-push-image-server.result == 'success' || needs.build-and-push-image-server.result == 'skipped') && + github.ref == 'refs/heads/main' + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Copy stack file to remote + uses: garygrossgarten/github-action-scp@v0.7.3 + with: + local: docker-compose.yml + remote: funke-wallet-service-provider/docker-compose.yml + host: dashboard.dev.animo.id + username: root + privateKey: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }} + + - name: Deploy to Docker Swarm via SSH action + uses: appleboy/ssh-action@v0.1.4 + with: + host: dashboard.dev.animo.id + username: root + key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }} + script: | + docker stack deploy --compose-file funke-wallet-service-provider/docker-compose.yml funke-wallet-service-provider --with-registry-auth diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..263e4dd --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,29 @@ +name: 'Continous Integration' + +on: + push: + branches: [main] + + pull_request: + types: [opened, synchronize] + +jobs: + continous-integration: + name: 'Continuous Integration' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Install dependencies + run: pnpm install + + - name: Check formatting + run: pnpm style:check + + - name: Build + run: pnpm build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..543fdcd --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +node_modules +build +.env +google_credentials.json \ No newline at end of file diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..459487a --- /dev/null +++ b/biome.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "files": { + "maxSize": 10000000 + }, + "formatter": { + "lineWidth": 120, + "indentStyle": "space" + }, + "javascript": { + "parser": { + "unsafeParameterDecoratorsEnabled": true + }, + "formatter": { + "semicolons": "asNeeded", + "quoteStyle": "single", + "trailingCommas": "es5", + "lineWidth": 120, + "indentStyle": "space" + } + }, + "json": { + "parser": { + "allowComments": true + } + }, + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "performance": { + "noAccumulatingSpread": "off" + } + } + } +} diff --git a/certificates/digital_ocean.cert b/certificates/digital_ocean.cert new file mode 100644 index 0000000..21871c3 --- /dev/null +++ b/certificates/digital_ocean.cert @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEQTCCAqmgAwIBAgIUcLIb2qSAI+gdIhnWN/GifNrRRZ8wDQYJKoZIhvcNAQEM +BQAwOjE4MDYGA1UEAwwvMmY1MTMyNTctYjE5Ny00ZjYwLTk4YjQtMDkzNDI5Y2Uw +NGU4IFByb2plY3QgQ0EwHhcNMjExMTA2MTgwNDE4WhcNMzExMTA0MTgwNDE4WjA6 +MTgwNgYDVQQDDC8yZjUxMzI1Ny1iMTk3LTRmNjAtOThiNC0wOTM0MjljZTA0ZTgg +UHJvamVjdCBDQTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBANTH0oia +lwvf1IkbYlhv9qYG6CjldRw1oa6CZR0/kuqYlJe5AWicCduA4NmTAM/+u+lS1dCG +isySSjiDMFrt1DburVdu4u5XqqnItcPfq/YAnvEdlh2jqOVqHJHvhT7sMf8X7LrM +ARYiaewTsxrYWNYYB93DIsNQj4SSjVCFLl6EbQ5PTAfwTm5RJSEMWqet+ieVCZwX +9ZICTq5gwmnb0+DDnaa0LvPExeqyhWRX369Wt5kXGxOz0kWYj1+mbOBNdjzOa4Xr +sWZm9E8VREgiThWOQDKlgV+pU88IIavZ1FUbH2gqfsJWewxnWoqzTIdPQweL9sfr +ZY9uDTuPekHwdvXT3GUMuu/mITGF07kvx3bR3XUp1NQ1GPJ7NTzXlekvBi6vsztf +JyX2Ok2WObVVr382cB4Qfart+VCd5YCVP/zH4/ZZoyNFNKZUZhjGbWhZ2uMOjJ7S +TBe87dyxUAvHP1jQkmMksVPCTBjZG1v7ANB8chNITzIiiVSoZLNljaXIRwIDAQAB +oz8wPTAdBgNVHQ4EFgQUxoRStbb/QheA7Ht97NGbQn1G++swDwYDVR0TBAgwBgEB +/wIBADALBgNVHQ8EBAMCAQYwDQYJKoZIhvcNAQEMBQADggGBAAUzkkH42MSi9+Wf +TGe8IVbu0/BqJLteNxzCGh8BmKAAtbMC241QEWx7EgMSXYLQix311VW00dWrfs6Q +xb1cv3yg5rwElUJS1EwEn8MPADb+x3a/C0kAVV1NlUwNb5qrKKY7Sv3ltTs2QzcF +Jo0/nSqyVIChvqRUZM4TkBnM6kaG5ZAMWFfa1XNgXmi5FFPxwRlHS5RdoBzRqk26 +EUm6A1ZLRJn0Xz1/99duTXToQmwgWxazQ+mGA5BleMYlpF8CL6mw2rugX+hrvrj8 +Jy9MoqD5E6Q9EEDc0C6DA9gYjiO43BIqrV76FxHQjGHZI4e4VdEJnP/d15AoMaD8 +HRAdRk6TBYmXLclhw1mN56n+0TIVvodx8POZBgcPn1ieknO89SUyDtDxKd2RaBsX +JJdPn3AObkefzOHpK+YN4tn/Cy5qs6CWXOGvvp/2tRCrr84pshla+Y+T1Wxq9yWu +HY4yxDHVkasHB1cs/weRIQgnWq12c+FxMs3JTGGWzhwYUXwA8Q== +-----END CERTIFICATE----- diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..7bbbe7a --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,28 @@ +version: "3.5" + +services: + server: + image: ghcr.io/animo/funke-wallet-service-provider/server:main + deploy: + placement: + constraints: + - node.role == worker + labels: + traefik.enable: "true" + + traefik.http.routers.funke-wallet-service-provider.rule: Host(`wsp.funke.animo.id`) + traefik.http.routers.funke-wallet-service-provider.entrypoints: web-secure + traefik.http.routers.funke-wallet-service-provider.tls.certresolver: zerossl + traefik.http.routers.funke-wallet-service-provider.service: funke-wallet-service-provider-service + traefik.http.services.funke-wallet-service-provider-service.loadbalancer.server.port: 3000 + + networks: + - traefik + + ports: + - "3000" + +networks: + traefik: + external: true + name: traefik diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..5482a2d --- /dev/null +++ b/dockerfile @@ -0,0 +1,19 @@ +FROM node:20-slim AS base +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable +COPY . /app +WORKDIR /app + +FROM base AS prod-deps +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile + +FROM base AS build +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile +RUN pnpm run build + +FROM base +COPY --from=prod-deps /app/node_modules /app/node_modules +COPY --from=build /app/build /app/build +EXPOSE 3000 +CMD [ "pnpm", "start" ] diff --git a/package.json b/package.json new file mode 100644 index 0000000..c2ee814 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "funke-wallet-service-provider", + "scripts": { + "dev": "ts-node src/index.ts", + "build": "tsc", + "start": "node build/index.js", + "style:check": "biome check .", + "style:fix": "pnpm style:check --write --unsafe", + "test": "ts-node scripts/test.ts" + }, + "dependencies": { + "@animo-id/oauth2": "0.2.0-alpha-20241126193353", + "@google-cloud/kms": "^4.5.0", + "@multiformats/base-x": "^4.0.1", + "@peculiar/asn1-schema": "^2.3.13", + "@peculiar/asn1-x509": "^2.3.13", + "base58": "^2.0.1", + "dotenv": "^16.4.5", + "express": "^4.21.1", + "jose": "^5.9.6", + "pg": "^8.13.1", + "uuid": "^11.0.3", + "zod": "^3.23.8" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@types/body-parser": "^1.19.5", + "@types/express": "^5.0.0", + "@types/node": "^22.9.0", + "@types/pg": "^8.11.10", + "axios": "^1.7.7", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..adea0ac --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1913 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@animo-id/oauth2': + specifier: 0.2.0-alpha-20241126193353 + version: 0.2.0-alpha-20241126193353(typescript@5.6.3) + '@google-cloud/kms': + specifier: ^4.5.0 + version: 4.5.0 + '@multiformats/base-x': + specifier: ^4.0.1 + version: 4.0.1 + '@peculiar/asn1-schema': + specifier: ^2.3.13 + version: 2.3.13 + '@peculiar/asn1-x509': + specifier: ^2.3.13 + version: 2.3.13 + base58: + specifier: ^2.0.1 + version: 2.0.1 + dotenv: + specifier: ^16.4.5 + version: 16.4.5 + express: + specifier: ^4.21.1 + version: 4.21.1 + jose: + specifier: ^5.9.6 + version: 5.9.6 + pg: + specifier: ^8.13.1 + version: 8.13.1 + uuid: + specifier: ^11.0.3 + version: 11.0.3 + zod: + specifier: ^3.23.8 + version: 3.23.8 + devDependencies: + '@biomejs/biome': + specifier: 1.9.4 + version: 1.9.4 + '@types/body-parser': + specifier: ^1.19.5 + version: 1.19.5 + '@types/express': + specifier: ^5.0.0 + version: 5.0.0 + '@types/node': + specifier: ^22.9.0 + version: 22.9.0 + '@types/pg': + specifier: ^8.11.10 + version: 8.11.10 + axios: + specifier: ^1.7.7 + version: 1.7.7 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@22.9.0)(typescript@5.6.3) + typescript: + specifier: ^5.6.3 + version: 5.6.3 + +packages: + + '@animo-id/oauth2-utils@0.2.0-alpha-20241126193353': + resolution: {integrity: sha512-RCCXdIoG5dbIunVIKntpEBwc7UiUUTXz1qx2u67xAxFUvGJonQFrkd7ERpIHjcnZD4EQCz231ojPBNNRB+i4cg==} + + '@animo-id/oauth2@0.2.0-alpha-20241126193353': + resolution: {integrity: sha512-sg1ezv+QL5PN3Fc4pUBcqwWOOPU4o19hro+uMlFVmrqEQr+vpFm+4SVp1J+g0z8iLaZRFFL7p5Z6u2tfemb/gg==} + + '@biomejs/biome@1.9.4': + resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@1.9.4': + resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@1.9.4': + resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@1.9.4': + resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@1.9.4': + resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@1.9.4': + resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@1.9.4': + resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@1.9.4': + resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@1.9.4': + resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@google-cloud/kms@4.5.0': + resolution: {integrity: sha512-i2vC0DI7bdfEhQszqASTw0KVvbB7HsO2CwTBod423NawAu7FWi+gVVa7NLfXVNGJaZZayFfci2Hu+om/HmyEjQ==} + engines: {node: '>=14.0.0'} + + '@grpc/grpc-js@1.12.2': + resolution: {integrity: sha512-bgxdZmgTrJZX50OjyVwz3+mNEnCTNkh3cIqGPWVNeW9jX6bn1ZkU80uPd+67/ZpIJIjRQ9qaHCjhavyoWYxumg==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.7.13': + resolution: {integrity: sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==} + engines: {node: '>=6'} + hasBin: true + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + + '@multiformats/base-x@4.0.1': + resolution: {integrity: sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw==} + + '@peculiar/asn1-schema@2.3.13': + resolution: {integrity: sha512-3Xq3a01WkHRZL8X04Zsfg//mGaA21xlL4tlVn4v2xGT0JStiztATRkMwa5b+f/HXmY2smsiLXYK46Gwgzvfg3g==} + + '@peculiar/asn1-x509@2.3.13': + resolution: {integrity: sha512-PfeLQl2skXmxX2/AFFCVaWU8U6FKW1Db43mgBhShCOFS1bVxqtvusq1hVjfuEcuSQGedrLdCSvTgabluwN/M9A==} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + + '@tootallnate/once@2.0.0': + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/body-parser@1.19.5': + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + + '@types/caseless@0.12.5': + resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/express-serve-static-core@5.0.1': + resolution: {integrity: sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==} + + '@types/express@5.0.0': + resolution: {integrity: sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==} + + '@types/http-errors@2.0.4': + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + + '@types/long@4.0.2': + resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/node@22.9.0': + resolution: {integrity: sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==} + + '@types/pg@8.11.10': + resolution: {integrity: sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==} + + '@types/qs@6.9.17': + resolution: {integrity: sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/request@2.48.12': + resolution: {integrity: sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==} + + '@types/send@0.17.4': + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + + '@types/serve-static@1.15.7': + resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.1: + resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} + engines: {node: '>= 14'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + asn1js@3.0.5: + resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==} + engines: {node: '>=12.0.0'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.7.7: + resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} + + base58@2.0.1: + resolution: {integrity: sha512-qK6gt2fMSxN2xGOi+btI5oAnXL+vEq0AsHWHhf5jfm2hE6MwmW+2414qF96utV3Xfg3En8hEA9Q4lif4lbXcgw==} + engines: {node: '>= 6'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bignumber.js@9.1.2: + resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} + + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + + duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + + es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + express@4.21.1: + resolution: {integrity: sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==} + engines: {node: '>= 0.10.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@2.5.2: + resolution: {integrity: sha512-GgwY0PS7DbXqajuGf4OYlsrIu3zgxD6Vvql43IBhm6MahqA5SK/7mwhtNj2AdH2z35YR34ujJ7BN+3fFC3jP5Q==} + engines: {node: '>= 0.12'} + + form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + + gcp-metadata@6.1.0: + resolution: {integrity: sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==} + engines: {node: '>=14'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + + google-auth-library@9.14.2: + resolution: {integrity: sha512-R+FRIfk1GBo3RdlRYWPdwk8nmtVUOn6+BkDomAC46KoU8kzXzE1HLmOasSCbWUByMMAGkknVF0G5kQ69Vj7dlA==} + engines: {node: '>=14'} + + google-gax@4.4.1: + resolution: {integrity: sha512-Phyp9fMfA00J3sZbJxbbB4jC55b7DBjE3F6poyL3wKMEBVKA79q6BGuHcTiM28yOzVql0NDbRL8MLLh8Iwk9Dg==} + engines: {node: '>=14'} + + gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + + gtoken@7.1.0: + resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} + engines: {node: '>=14.0.0'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + + has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.5: + resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} + engines: {node: '>= 14'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + ipaddr.js@2.2.0: + resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} + engines: {node: '>= 10'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + jose@5.9.6: + resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==} + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + jwa@2.0.0: + resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==} + + jws@4.0.0: + resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + object-inspect@1.13.3: + resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==} + engines: {node: '>= 0.4'} + + obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-to-regexp@0.1.10: + resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==} + + pg-cloudflare@1.1.1: + resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} + + pg-connection-string@2.7.0: + resolution: {integrity: sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-numeric@1.0.2: + resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} + engines: {node: '>=4'} + + pg-pool@3.7.0: + resolution: {integrity: sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.7.0: + resolution: {integrity: sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg-types@4.0.2: + resolution: {integrity: sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==} + engines: {node: '>=10'} + + pg@8.13.1: + resolution: {integrity: sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==} + engines: {node: '>= 8.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-array@3.0.2: + resolution: {integrity: sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==} + engines: {node: '>=12'} + + postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + postgres-bytea@3.0.0: + resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==} + engines: {node: '>= 6'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-date@2.1.0: + resolution: {integrity: sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==} + engines: {node: '>=12'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + postgres-interval@3.0.0: + resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==} + engines: {node: '>=12'} + + postgres-range@1.1.4: + resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} + + proto3-json-serializer@2.0.2: + resolution: {integrity: sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==} + engines: {node: '>=14.0.0'} + + protobufjs@7.4.0: + resolution: {integrity: sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==} + engines: {node: '>=12.0.0'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + pvtsutils@1.3.5: + resolution: {integrity: sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==} + + pvutils@1.1.3: + resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} + engines: {node: '>=6.0.0'} + + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + retry-request@7.0.2: + resolution: {integrity: sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==} + engines: {node: '>=14'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + stream-events@1.0.5: + resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} + + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + stubs@3.0.0: + resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} + + teeny-request@9.0.0: + resolution: {integrity: sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==} + engines: {node: '>=14'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@11.0.3: + resolution: {integrity: sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + valibot@0.42.1: + resolution: {integrity: sha512-3keXV29Ar5b//Hqi4MbSdV7lfVp6zuYLZuA9V1PvQUsXqogr+u5lvLPLk3A4f74VUXDnf/JfWMN6sB+koJ/FFw==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + +snapshots: + + '@animo-id/oauth2-utils@0.2.0-alpha-20241126193353(typescript@5.6.3)': + dependencies: + buffer: 6.0.3 + valibot: 0.42.1(typescript@5.6.3) + transitivePeerDependencies: + - typescript + + '@animo-id/oauth2@0.2.0-alpha-20241126193353(typescript@5.6.3)': + dependencies: + '@animo-id/oauth2-utils': 0.2.0-alpha-20241126193353(typescript@5.6.3) + valibot: 0.42.1(typescript@5.6.3) + transitivePeerDependencies: + - typescript + + '@biomejs/biome@1.9.4': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 1.9.4 + '@biomejs/cli-darwin-x64': 1.9.4 + '@biomejs/cli-linux-arm64': 1.9.4 + '@biomejs/cli-linux-arm64-musl': 1.9.4 + '@biomejs/cli-linux-x64': 1.9.4 + '@biomejs/cli-linux-x64-musl': 1.9.4 + '@biomejs/cli-win32-arm64': 1.9.4 + '@biomejs/cli-win32-x64': 1.9.4 + + '@biomejs/cli-darwin-arm64@1.9.4': + optional: true + + '@biomejs/cli-darwin-x64@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64@1.9.4': + optional: true + + '@biomejs/cli-linux-x64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-x64@1.9.4': + optional: true + + '@biomejs/cli-win32-arm64@1.9.4': + optional: true + + '@biomejs/cli-win32-x64@1.9.4': + optional: true + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@google-cloud/kms@4.5.0': + dependencies: + google-gax: 4.4.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@grpc/grpc-js@1.12.2': + dependencies: + '@grpc/proto-loader': 0.7.13 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.7.13': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.2.3 + protobufjs: 7.4.0 + yargs: 17.7.2 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@js-sdsl/ordered-map@4.4.2': {} + + '@multiformats/base-x@4.0.1': {} + + '@peculiar/asn1-schema@2.3.13': + dependencies: + asn1js: 3.0.5 + pvtsutils: 1.3.5 + tslib: 2.8.1 + + '@peculiar/asn1-x509@2.3.13': + dependencies: + '@peculiar/asn1-schema': 2.3.13 + asn1js: 3.0.5 + ipaddr.js: 2.2.0 + pvtsutils: 1.3.5 + tslib: 2.8.1 + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + + '@tootallnate/once@2.0.0': {} + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/body-parser@1.19.5': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.9.0 + + '@types/caseless@0.12.5': {} + + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.9.0 + + '@types/express-serve-static-core@5.0.1': + dependencies: + '@types/node': 22.9.0 + '@types/qs': 6.9.17 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + + '@types/express@5.0.0': + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 5.0.1 + '@types/qs': 6.9.17 + '@types/serve-static': 1.15.7 + + '@types/http-errors@2.0.4': {} + + '@types/long@4.0.2': {} + + '@types/mime@1.3.5': {} + + '@types/node@22.9.0': + dependencies: + undici-types: 6.19.8 + + '@types/pg@8.11.10': + dependencies: + '@types/node': 22.9.0 + pg-protocol: 1.7.0 + pg-types: 4.0.2 + + '@types/qs@6.9.17': {} + + '@types/range-parser@1.2.7': {} + + '@types/request@2.48.12': + dependencies: + '@types/caseless': 0.12.5 + '@types/node': 22.9.0 + '@types/tough-cookie': 4.0.5 + form-data: 2.5.2 + + '@types/send@0.17.4': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 22.9.0 + + '@types/serve-static@1.15.7': + dependencies: + '@types/http-errors': 2.0.4 + '@types/node': 22.9.0 + '@types/send': 0.17.4 + + '@types/tough-cookie@4.0.5': {} + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.14.0 + + acorn@8.14.0: {} + + agent-base@6.0.2: + dependencies: + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + agent-base@7.1.1: + dependencies: + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + arg@4.1.3: {} + + array-flatten@1.1.1: {} + + asn1js@3.0.5: + dependencies: + pvtsutils: 1.3.5 + pvutils: 1.1.3 + tslib: 2.8.1 + + asynckit@0.4.0: {} + + axios@1.7.7: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.1 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + base58@2.0.1: {} + + base64-js@1.5.1: {} + + bignumber.js@9.1.2: {} + + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + buffer-equal-constant-time@1.0.1: {} + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bytes@3.1.2: {} + + call-bind@1.0.7: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-signature@1.0.6: {} + + cookie@0.7.1: {} + + create-require@1.1.1: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + + destroy@1.2.0: {} + + diff@4.0.2: {} + + dotenv@16.4.5: {} + + duplexify@4.1.3: + dependencies: + end-of-stream: 1.4.4 + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-shift: 1.0.3 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + emoji-regex@8.0.0: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + + end-of-stream@1.4.4: + dependencies: + once: 1.4.0 + + es-define-property@1.0.0: + dependencies: + get-intrinsic: 1.2.4 + + es-errors@1.3.0: {} + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + event-target-shim@5.0.1: {} + + express@4.21.1: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.10 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + extend@3.0.2: {} + + finalhandler@1.3.1: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + follow-redirects@1.15.9: {} + + form-data@2.5.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + safe-buffer: 5.2.1 + + form-data@4.0.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + forwarded@0.2.0: {} + + fresh@0.5.2: {} + + function-bind@1.1.2: {} + + gaxios@6.7.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.5 + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + gcp-metadata@6.1.0: + dependencies: + gaxios: 6.7.1 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + get-caller-file@2.0.5: {} + + get-intrinsic@1.2.4: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + + google-auth-library@9.14.2: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 6.7.1 + gcp-metadata: 6.1.0 + gtoken: 7.1.0 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + google-gax@4.4.1: + dependencies: + '@grpc/grpc-js': 1.12.2 + '@grpc/proto-loader': 0.7.13 + '@types/long': 4.0.2 + abort-controller: 3.0.0 + duplexify: 4.1.3 + google-auth-library: 9.14.2 + node-fetch: 2.7.0 + object-hash: 3.0.0 + proto3-json-serializer: 2.0.2 + protobufjs: 7.4.0 + retry-request: 7.0.2 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + gopd@1.0.1: + dependencies: + get-intrinsic: 1.2.4 + + gtoken@7.1.0: + dependencies: + gaxios: 6.7.1 + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.0 + + has-proto@1.0.3: {} + + has-symbols@1.0.3: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.5: + dependencies: + agent-base: 7.1.1 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + ipaddr.js@2.2.0: {} + + is-fullwidth-code-point@3.0.0: {} + + is-stream@2.0.1: {} + + jose@5.9.6: {} + + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.1.2 + + jwa@2.0.0: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.0: + dependencies: + jwa: 2.0.0 + safe-buffer: 5.2.1 + + lodash.camelcase@4.3.0: {} + + long@5.2.3: {} + + make-error@1.3.6: {} + + media-typer@0.3.0: {} + + merge-descriptors@1.0.3: {} + + methods@1.1.2: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + negotiator@0.6.3: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + object-hash@3.0.0: {} + + object-inspect@1.13.3: {} + + obuf@1.1.2: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + parseurl@1.3.3: {} + + path-to-regexp@0.1.10: {} + + pg-cloudflare@1.1.1: + optional: true + + pg-connection-string@2.7.0: {} + + pg-int8@1.0.1: {} + + pg-numeric@1.0.2: {} + + pg-pool@3.7.0(pg@8.13.1): + dependencies: + pg: 8.13.1 + + pg-protocol@1.7.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg-types@4.0.2: + dependencies: + pg-int8: 1.0.1 + pg-numeric: 1.0.2 + postgres-array: 3.0.2 + postgres-bytea: 3.0.0 + postgres-date: 2.1.0 + postgres-interval: 3.0.0 + postgres-range: 1.1.4 + + pg@8.13.1: + dependencies: + pg-connection-string: 2.7.0 + pg-pool: 3.7.0(pg@8.13.1) + pg-protocol: 1.7.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.1.1 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + postgres-array@2.0.0: {} + + postgres-array@3.0.2: {} + + postgres-bytea@1.0.0: {} + + postgres-bytea@3.0.0: + dependencies: + obuf: 1.1.2 + + postgres-date@1.0.7: {} + + postgres-date@2.1.0: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + postgres-interval@3.0.0: {} + + postgres-range@1.1.4: {} + + proto3-json-serializer@2.0.2: + dependencies: + protobufjs: 7.4.0 + + protobufjs@7.4.0: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 22.9.0 + long: 5.2.3 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-from-env@1.1.0: {} + + pvtsutils@1.3.5: + dependencies: + tslib: 2.8.1 + + pvutils@1.1.3: {} + + qs@6.13.0: + dependencies: + side-channel: 1.0.6 + + range-parser@1.2.1: {} + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + require-directory@2.1.1: {} + + retry-request@7.0.2: + dependencies: + '@types/request': 2.48.12 + extend: 3.0.2 + teeny-request: 9.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + + setprototypeof@1.2.0: {} + + side-channel@1.0.6: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.3 + + split2@4.2.0: {} + + statuses@2.0.1: {} + + stream-events@1.0.5: + dependencies: + stubs: 3.0.0 + + stream-shift@1.0.3: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + stubs@3.0.0: {} + + teeny-request@9.0.0: + dependencies: + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + node-fetch: 2.7.0 + stream-events: 1.0.5 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + toidentifier@1.0.1: {} + + tr46@0.0.3: {} + + ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.9.0 + acorn: 8.14.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.6.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + tslib@2.8.1: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + typescript@5.6.3: {} + + undici-types@6.19.8: {} + + unpipe@1.0.0: {} + + util-deprecate@1.0.2: {} + + utils-merge@1.0.1: {} + + uuid@11.0.3: {} + + uuid@9.0.1: {} + + v8-compile-cache-lib@3.0.1: {} + + valibot@0.42.1(typescript@5.6.3): + optionalDependencies: + typescript: 5.6.3 + + vary@1.1.2: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yn@3.1.1: {} + + zod@3.23.8: {} diff --git a/scripts/test.ts b/scripts/test.ts new file mode 100755 index 0000000..01bd86e --- /dev/null +++ b/scripts/test.ts @@ -0,0 +1,79 @@ +#!/usr/bin/env ts-node + +import 'dotenv/config' + +import { Axios, AxiosHeaders } from 'axios' +import { v4 as uuid } from 'uuid' + +import { CompactSign, type GenerateKeyPairResult, type KeyLike, exportJWK, generateKeyPair } from 'jose' + +const port = process.env.PORT || 30000 +const KEY_ID = uuid() +const KEY_IDS = [uuid(), uuid(), uuid()] +const headers = new AxiosHeaders({ 'Content-Type': 'application/json' }) + +const request = async ( + c: Axios, + keys: GenerateKeyPairResult, + route: string, + body: Record +): Promise => { + const jwt = await new CompactSign(new TextEncoder().encode(JSON.stringify(body))) + .setProtectedHeader({ alg: 'ES256', jwk: await exportJWK(keys.publicKey) }) + .sign(keys.privateKey) + + const newBody = { + jwt, + } + + const result = await c.post(`http://localhost:${port}/${route}`, JSON.stringify(newBody)) + + const data = result.data + if (result.status > 299) { + throw new Error(data) + } + + return data +} + +void (async () => { + try { + const keys = await generateKeyPair('ES256') + const clientAttestationKey = await generateKeyPair('ES256') + const client = new Axios({ headers }) + const data = new Uint8Array([1, 2, 3]) + await request(client, keys, 'register-wallet', {}) + + const createKeyResult = await request<{ publicKey: Array }>(client, keys, 'create-key', { + keyId: KEY_ID, + keyType: 'P256', + }) + + console.log(`createKeyResult: ${createKeyResult}`) + + const batchCreateKeysResult = await request<{ publicKeys: Array }>(client, keys, 'batch-create-key', { + keyIds: KEY_IDS, + keyType: 'P256', + }) + + console.log(`batchCreateKeysResult: ${batchCreateKeysResult}`) + + const getPublicKeyResult = await request<{ publicKey: Array }>(client, keys, 'get-publickey', { + keyId: KEY_ID, + }) + console.log(`getPublicKeyResult: ${getPublicKeyResult}`) + + const signResult = await request<{ signature: Uint8Array }>(client, keys, 'sign', { + keyId: KEY_ID, + data: new Array(...data), + }) + console.log(`signResult: ${signResult}`) + + const walletAttestationResult = await request<{ walletAttestation: string }>(client, keys, 'attestation', { + jwk: await exportJWK(clientAttestationKey.publicKey), + }) + console.log(`walletAttestationResult: ${walletAttestationResult}`) + } catch (e) { + console.error(e) + } +})() diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..9d1c8b1 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,12 @@ +import { writeFileSync } from 'node:fs' + +if (!process.env.WALLET_PROVIDER_P256_PRIVATE_JWK) { + throw new Error('Missing required WALLET_PROVIDER_P256_PRIVATE_JWK') +} + +export const WALLET_PROVIDER_P256_PRIVATE_JWK = process.env.WALLET_PROVIDER_P256_PRIVATE_JWK +export const GOOGLE_APPLICATION_CREDENTIALS_JSON = process.env.GOOGLE_APPLICATION_CREDENTIALS_JSON +if (GOOGLE_APPLICATION_CREDENTIALS_JSON) { + // Write google credentials + writeFileSync('google_credentials.json', GOOGLE_APPLICATION_CREDENTIALS_JSON) +} diff --git a/src/database/Database.ts b/src/database/Database.ts new file mode 100644 index 0000000..5cef4eb --- /dev/null +++ b/src/database/Database.ts @@ -0,0 +1,54 @@ +import { readFileSync } from 'node:fs' +import { Pool } from 'pg' +import { WalletIdNotFoundError } from '../error/WalletIdNotFound' +import type { PostgresConfig } from '../index' + +export type CreateWalletOptions = { + walletId: string + keyRingId: string +} + +export type GetKeyRingIdOptions = { + walletId: string +} + +export class Database { + public constructor(private pool: Pool) {} + + public static async initialize(postgresConfig: PostgresConfig) { + const ca = postgresConfig.caPath ? readFileSync(postgresConfig.caPath) : undefined + const pool = new Pool({ + idleTimeoutMillis: 30000, + host: postgresConfig.host, + password: postgresConfig.password, + database: postgresConfig.database, + port: postgresConfig.port, + user: postgresConfig.user, + ssl: ca ? { ca } : undefined, + }) + const db = new Database(pool) + await db.createTable() + return db + } + + public async createTable() { + const query = 'CREATE TABLE IF NOT EXISTS wallets (walletId VARCHAR(255) PRIMARY KEY, keyRingId VARCHAR(255));' + await this.pool.query(query) + } + + public async createWallet(options: CreateWalletOptions) { + console.log('createWallet walletId: ', options.walletId) + const query = 'INSERT INTO wallets (walletId, keyRingId) VALUES ($1, $2) RETURNING *' + await this.pool.query(query, [options.walletId, options.keyRingId]) + } + + public async getKeyRingId(options: GetKeyRingIdOptions) { + console.log('getKeyRingId walletId: ', options.walletId) + const query = 'SELECT keyRingId FROM wallets WHERE walletId=$1' + const result = await this.pool.query(query, [options.walletId]) + if (result.rows.length === 0) { + throw new WalletIdNotFoundError(options.walletId) + } + return (result.rows[0] as { keyringid: string }).keyringid + } +} diff --git a/src/database/index.ts b/src/database/index.ts new file mode 100644 index 0000000..79a45db --- /dev/null +++ b/src/database/index.ts @@ -0,0 +1 @@ +export * from './Database' diff --git a/src/error/WalletIdNotFound.ts b/src/error/WalletIdNotFound.ts new file mode 100644 index 0000000..e21bf6d --- /dev/null +++ b/src/error/WalletIdNotFound.ts @@ -0,0 +1,7 @@ +export class WalletIdNotFoundError extends Error { + public name = 'WALLET_ID_NOT_FOUND_ERROR' + + constructor(walletId: string) { + super(`Wallet id '${walletId}' not found`) + } +} diff --git a/src/error/index.ts b/src/error/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/hsm/GoogleHsm.ts b/src/hsm/GoogleHsm.ts new file mode 100644 index 0000000..64e74a3 --- /dev/null +++ b/src/hsm/GoogleHsm.ts @@ -0,0 +1,90 @@ +import { createHash } from 'node:crypto' +import { KeyManagementServiceClient } from '@google-cloud/kms' +import { AsnParser } from '@peculiar/asn1-schema' +import { SubjectPublicKeyInfo } from '@peculiar/asn1-x509' +import { v4 as uuidv4 } from 'uuid' +import type { CreateKeyOptions, GetPublicKeyOptions, Hsm, SignOptions } from './Hsm' +import { KeyType } from './keyType' + +export class GoogleHsm implements Hsm { + private projectId = 'funke-441515' + private locationId = 'europe-west4' + private client = new KeyManagementServiceClient() + + private keyVersionPath(keyRingId: string, keyId: string) { + return this.client.cryptoKeyVersionPath(this.projectId, this.locationId, keyRingId, keyId, '1') + } + + private keyRingName(keyRingId: string) { + return this.client.keyRingPath(this.projectId, this.locationId, keyRingId) + } + + private get locationPath() { + return this.client.locationPath(this.projectId, this.locationId) + } + + public async registerWallet(): Promise { + const keyRingId = uuidv4() + + await this.client.createKeyRing({ + parent: this.locationPath, + keyRingId, + }) + + return keyRingId + } + + public async createKey(options: CreateKeyOptions): Promise { + if (options.keyType !== KeyType.P256) { + throw new Error(`Only keytype of '${KeyType.P256}' is allowed`) + } + + await this.client.createCryptoKey({ + parent: this.keyRingName(options.keyRingId), + cryptoKeyId: options.keyId, + cryptoKey: { + purpose: 'ASYMMETRIC_SIGN', + versionTemplate: { + algorithm: 'EC_SIGN_P256_SHA256', + }, + }, + }) + } + + // Returns the uncompressed key + public async getPublicKey(options: GetPublicKeyOptions): Promise { + const keyName = this.keyVersionPath(options.keyRingId, options.keyId) + const [publicKey] = await this.client.getPublicKey({ name: keyName }) + + const pem = publicKey.pem + + if (!pem) { + throw new Error(`No public key found for key id '${options.keyId}'`) + } + + const keyBytes = pem.slice(26, pem.length - 24) + const pemBytes = Buffer.from(keyBytes, 'base64') + const spki = AsnParser.parse(pemBytes, SubjectPublicKeyInfo) + const uncompressedKey = new Uint8Array(spki.subjectPublicKey) + return new Uint8Array(uncompressedKey) + } + + public async sign(options: SignOptions): Promise { + const keyName = this.keyVersionPath(options.keyRingId, options.keyId) + const hasher = createHash('sha256') + hasher.update(options.data) + const digest = hasher.digest() + const [signResponse] = await this.client.asymmetricSign({ + name: keyName, + digest: { + sha256: digest, + }, + }) + + if (!signResponse.signature) { + throw new Error('No signature found on the response') + } + + return signResponse.signature as Uint8Array + } +} diff --git a/src/hsm/Hsm.ts b/src/hsm/Hsm.ts new file mode 100644 index 0000000..fc89806 --- /dev/null +++ b/src/hsm/Hsm.ts @@ -0,0 +1,25 @@ +import type { KeyType } from './keyType' + +export type SignOptions = { + keyId: string + keyRingId: string + data: Uint8Array +} + +export type CreateKeyOptions = { + keyId: string + keyRingId: string + keyType: KeyType +} + +export type GetPublicKeyOptions = { + keyId: string + keyRingId: string +} + +export interface Hsm { + registerWallet(): Promise + createKey(options: CreateKeyOptions): Promise + getPublicKey(options: GetPublicKeyOptions): Promise + sign(options: SignOptions): Promise +} diff --git a/src/hsm/NodeHsm.ts b/src/hsm/NodeHsm.ts new file mode 100644 index 0000000..4c8ed58 --- /dev/null +++ b/src/hsm/NodeHsm.ts @@ -0,0 +1,71 @@ +import { v4 as uuidv4 } from 'uuid' +import type { CreateKeyOptions, GetPublicKeyOptions, Hsm, SignOptions } from './Hsm' + +import { type KeyObject, createSign, generateKeyPairSync } from 'node:crypto' +import { AsnParser } from '@peculiar/asn1-schema' +import { SubjectKeyIdentifier, SubjectPublicKeyInfo } from '@peculiar/asn1-x509' +import { KeyType } from './keyType' + +type KeyPair = { + publicKey: KeyObject + privateKey: KeyObject +} + +export class NodeHsm implements Hsm { + private keyStore: Record> = {} + + private getKey(keyRingId: string, keyId: string) { + const keyRing = this.getKeyRing(keyRingId) + if (!(keyId in keyRing)) { + throw new Error(`Could not find key with key id: '${keyId}' in keyring with id: '${keyRingId}'`) + } + return keyRing[keyId] + } + + private getKeyRing(keyRingId: string) { + if (!(keyRingId in this.keyStore)) { + throw new Error(`Could not find keyring with keyring id: '${keyRingId}'`) + } + return this.keyStore[keyRingId] + } + + public async registerWallet(): Promise { + const keyRingId = uuidv4() + + this.keyStore[keyRingId] = {} + + return keyRingId + } + + public async createKey(options: CreateKeyOptions): Promise { + let key: KeyPair + switch (options.keyType) { + case KeyType.P256: + key = generateKeyPairSync('ec', { namedCurve: 'prime256v1' }) + this.keyStore[options.keyRingId][options.keyId] = key + return + default: + throw new Error(`Unsupported key type: '${options.keyType}'`) + } + } + + // Returns the uncompressed key + public async getPublicKey(options: GetPublicKeyOptions): Promise { + const key = this.getKey(options.keyRingId, options.keyId) + const pem = key.publicKey.export({ type: 'spki', format: 'pem' }) as string + const keyBytes = pem.slice(26, pem.length - 24) + const pemBytes = Buffer.from(keyBytes, 'base64') + const { subjectPublicKey } = AsnParser.parse(pemBytes, SubjectPublicKeyInfo) + return new Uint8Array(subjectPublicKey) + } + + public async sign(options: SignOptions): Promise { + const key = this.getKey(options.keyRingId, options.keyId) + + const signer = createSign('SHA256') + signer.update(options.data) + const signature = signer.sign(key.privateKey) + + return new Uint8Array(signature) + } +} diff --git a/src/hsm/index.ts b/src/hsm/index.ts new file mode 100644 index 0000000..b865e20 --- /dev/null +++ b/src/hsm/index.ts @@ -0,0 +1,2 @@ +export * from './NodeHsm' +export * from './GoogleHsm' diff --git a/src/hsm/keyType.ts b/src/hsm/keyType.ts new file mode 100644 index 0000000..ef24570 --- /dev/null +++ b/src/hsm/keyType.ts @@ -0,0 +1,4 @@ +export enum KeyType { + ed25519 = 'ed25519', + P256 = 'P256', +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..851a1fc --- /dev/null +++ b/src/index.ts @@ -0,0 +1,36 @@ +import 'dotenv/config' +// Stores google credentials +import './constants' +import type { Database } from './database' +import type { Hsm } from './hsm/Hsm' +import { startServer } from './server' + +declare global { + namespace Express { + interface Request { + hsm: Hsm + db: Database + keyRingId?: string + jwtPayload: Record + walletId: string + } + } +} + +const port = Number(process.env.PORT) || 3000 +const hsmType = process.env.HSM || 'node' + +const postgresConfig = { + host: process.env.POSTGRES_HOST, + user: process.env.POSTGRES_USER, + password: process.env.POSTGRES_PASSWORD, + database: process.env.POSTGRES_DATABASE, + port: Number(process.env.POSTGRES_PORT), + caPath: process.env.POSTGRES_CA_PATH, +} as const + +export type PostgresConfig = typeof postgresConfig + +void startServer({ port, hsmType, postgresConfig }).then(() => + console.log(`Started server on : 'http://localhost:${port}'`) +) diff --git a/src/server/index.ts b/src/server/index.ts new file mode 100644 index 0000000..f14a04d --- /dev/null +++ b/src/server/index.ts @@ -0,0 +1,50 @@ +import express, { type NextFunction, type Request, type Response } from 'express' +import type { PostgresConfig } from '..' +import { Database } from '../database' +import { GoogleHsm, NodeHsm } from '../hsm' +import { addKeyRingId, injectDatabase, injectHsm, validateJwtBody, validateSchema } from './middlewares' +import { + createKey, + createKeySchema, + getPublicKey, + getPublicKeySchema, + registerWallet, + sign, + signSchema, +} from './routes' +import { batchCreateKey, batchCreateKeySchema } from './routes/batchCreateKey' +import { getWalletAttestation, getWalletAttestationSchema } from './routes/getWalletAttestation' + +type StartServerOptions = { + port?: number + hsmType: string + postgresConfig: PostgresConfig +} + +export const startServer = async ({ port, hsmType, postgresConfig }: StartServerOptions) => { + const app = express() + const db = await Database.initialize(postgresConfig) + + const hsm = hsmType === 'node' ? new NodeHsm() : hsmType === 'google' ? new GoogleHsm() : undefined + if (!hsm) { + throw new Error(`Invalid HSM type: '${hsmType}'`) + } + + app.use(express.json()) + app.use(injectHsm(hsm)) + app.use(injectDatabase(db)) + app.use(validateJwtBody) + + app.post('/register-wallet', registerWallet) + app.post('/create-key', addKeyRingId, validateSchema(createKeySchema), createKey) + app.post('/batch-create-key', addKeyRingId, validateSchema(batchCreateKeySchema), batchCreateKey) + app.post('/get-publickey', addKeyRingId, validateSchema(getPublicKeySchema), getPublicKey) + app.post('/sign', addKeyRingId, validateSchema(signSchema), sign) + app.post('/attestation', addKeyRingId, validateSchema(getWalletAttestationSchema), getWalletAttestation) + + app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { + res.status(500).json({ ...err }) + }) + + new Promise((r) => app.listen(port ?? 3000, r)) +} diff --git a/src/server/middlewares/addKeyRingId.ts b/src/server/middlewares/addKeyRingId.ts new file mode 100644 index 0000000..98f8816 --- /dev/null +++ b/src/server/middlewares/addKeyRingId.ts @@ -0,0 +1,34 @@ +import type { NextFunction, Request, Response } from 'express' +import { z } from 'zod' +import { WalletIdNotFoundError } from '../../error/WalletIdNotFound' + +export const addKeyRingId = async (req: Request, res: Response, next: NextFunction) => { + try { + console.log(`[middleware] :: [addKeyRingId] on url: '${req.url}'`) + + const keyRingId = await req.db.getKeyRingId({ walletId: req.walletId }) + + req.keyRingId = keyRingId + + next() + } catch (error) { + console.error('[error] :: [middleware] :: [addKeyRingId] :: ', error) + if (error instanceof z.ZodError) { + console.error(error.errors) + res.status(400).json({ + message: 'Validation failed', + errors: error.errors, + }) + } else if (error instanceof WalletIdNotFoundError) { + res.status(403).json({ + message: error.message, + name: error.name, + }) + } else { + res.status(500).json({ + message: 'Internal server error', + error, + }) + } + } +} diff --git a/src/server/middlewares/authorization.ts b/src/server/middlewares/authorization.ts new file mode 100644 index 0000000..f86fae0 --- /dev/null +++ b/src/server/middlewares/authorization.ts @@ -0,0 +1,60 @@ +import { AsnParser } from '@peculiar/asn1-schema' +import { SubjectPublicKeyInfo } from '@peculiar/asn1-x509' +import type { NextFunction, Request, Response } from 'express' +import { type KeyLike, compactVerify, exportSPKI, importJWK } from 'jose' +import { z } from 'zod' +import { encodeToBase58 } from '../../utils' + +const schema = z.object({ + jwt: z.string(), +}) + +export const validateJwtBody = async (req: Request, res: Response, next: NextFunction) => { + try { + console.log(`[middleware] :: [validateJwtBody] on url: '${req.url}'`) + const { jwt } = schema.parse(req.body) + + const [header] = jwt.split('.') + const headerBytes = Buffer.from(header, 'base64url') + const { jwk } = JSON.parse(headerBytes.toString()) + if (!jwk) { + throw new Error('jwk entry not found in header') + } + + const key = (await importJWK(jwk)) as KeyLike + const publicKeySpki = await exportSPKI(key) + + const { payload } = await compactVerify(jwt, key) + + const keyString = publicKeySpki.slice(27, publicKeySpki.length - 25) + const pemBytes = Buffer.from(keyString, 'base64') + const parsed = AsnParser.parse(pemBytes, SubjectPublicKeyInfo) + const uncompressedKey = new Uint8Array(parsed.subjectPublicKey) + + const decodedPayload = new TextDecoder().decode(payload) + + req.walletId = encodeToBase58(uncompressedKey) + req.jwtPayload = JSON.parse(decodedPayload) + + next() + } catch (error) { + console.error('[error] :: [middleware] :: [validateJwtBody] :: ', error) + if (error instanceof z.ZodError) { + console.error(error.errors) + res.status(400).json({ + message: 'Validation failed', + errors: error.errors, + }) + } else if (error instanceof Error && error.message === 'signature verification failed') { + res.status(403).json({ + message: error.message, + }) + } else { + console.error(error) + res.status(500).json({ + message: 'Internal server error', + error, + }) + } + } +} diff --git a/src/server/middlewares/db.ts b/src/server/middlewares/db.ts new file mode 100644 index 0000000..b07e238 --- /dev/null +++ b/src/server/middlewares/db.ts @@ -0,0 +1,7 @@ +import type { NextFunction, Request, Response } from 'express' +import type { Database } from '../../database' + +export const injectDatabase = (db: Database) => (req: Request, _res: Response, next: NextFunction) => { + req.db = db + next() +} diff --git a/src/server/middlewares/hsm.ts b/src/server/middlewares/hsm.ts new file mode 100644 index 0000000..6ee4550 --- /dev/null +++ b/src/server/middlewares/hsm.ts @@ -0,0 +1,7 @@ +import type { NextFunction, Request, Response } from 'express' +import type { Hsm } from '../../hsm/Hsm' + +export const injectHsm = (hsm: Hsm) => (req: Request, _res: Response, next: NextFunction) => { + req.hsm = hsm + next() +} diff --git a/src/server/middlewares/index.ts b/src/server/middlewares/index.ts new file mode 100644 index 0000000..17564b0 --- /dev/null +++ b/src/server/middlewares/index.ts @@ -0,0 +1,5 @@ +export * from './validation' +export * from './db' +export * from './authorization' +export * from './hsm' +export * from './addKeyRingId' diff --git a/src/server/middlewares/validation.ts b/src/server/middlewares/validation.ts new file mode 100644 index 0000000..05a3485 --- /dev/null +++ b/src/server/middlewares/validation.ts @@ -0,0 +1,27 @@ +import type { NextFunction, Request, Response } from 'express' +import { z } from 'zod' + +export const validateSchema = + (schema: T) => + (req: Request, res: Response, next: NextFunction) => { + try { + console.log(`[middleware] :: [validateBody] on url: '${req.url}'`) + const validatedData = schema.parse(req.jwtPayload) + req.jwtPayload = validatedData + next() + } catch (error) { + console.error('[error] :: [middleware] :: [validateBody] :: ', error) + if (error instanceof z.ZodError) { + console.error('validation error: ', error.errors) + res.status(400).json({ + message: 'Validation failed', + errors: error.errors, + }) + } else { + console.error('generic error: ', error) + res.status(500).json({ + message: 'Internal server error', + }) + } + } + } diff --git a/src/server/oauth2/callbacks.ts b/src/server/oauth2/callbacks.ts new file mode 100644 index 0000000..39f0dc7 --- /dev/null +++ b/src/server/oauth2/callbacks.ts @@ -0,0 +1,26 @@ +import { clientAuthenticationNone, type CallbackContext, type Jwk } from '@animo-id/oauth2' +import { getRandomValues, createHash } from 'node:crypto' +import { importJWK, SignJWT, exportJWK } from 'jose' +import { WALLET_PROVIDER_P256_PRIVATE_JWK } from '../../constants' + +export const getOauth2Callbacks = () => { + return { + clientAuthentication: clientAuthenticationNone(), + generateRandom: (length: number) => getRandomValues(new Uint8Array(length)), + hash: (data, alg) => createHash(alg).update(data).digest(), + verifyJwt: () => { + throw new Error('Not implemented') + }, + signJwt: async (signer, { header, payload }) => { + const privateKey = await importJWK(JSON.parse(WALLET_PROVIDER_P256_PRIVATE_JWK)) + const { d, ...publicKeyJwk } = await exportJWK(privateKey) + + const signed = await new SignJWT(payload).setProtectedHeader(header).sign(privateKey) + + return { + jwt: signed, + signerJwk: publicKeyJwk as Jwk, + } + }, + } satisfies Partial +} diff --git a/src/server/oauth2/clientAttestations.ts b/src/server/oauth2/clientAttestations.ts new file mode 100644 index 0000000..30898ee --- /dev/null +++ b/src/server/oauth2/clientAttestations.ts @@ -0,0 +1,34 @@ +import { type Jwk, Oauth2Client } from '@animo-id/oauth2' +import { getOauth2Callbacks } from './callbacks' + +function addSecondsToDate(date: Date, seconds: number) { + return new Date(date.getTime() + seconds * 1000) +} + +export async function createClientAttestation(options: { + clientJwk: Jwk + clientId: string +}) { + const client = new Oauth2Client({ + callbacks: getOauth2Callbacks(), + }) + + return await client.createClientAttestationJwt({ + clientId: options.clientId, + confirmation: { + jwk: options.clientJwk, + // key is stored in encrypted database of which the key is derived from a pin + user_authentication: 'internal_pin', + // Client attestation key is software at the moment and also used for dpop + key_type: 'software', + }, + issuer: 'https://wallet.paradym.id', + signer: { + method: 'custom', + alg: 'ES256', + }, + + // valid for 5 minutes currently + expiresAt: addSecondsToDate(new Date(), 300), + }) +} diff --git a/src/server/routes/batchCreateKey.ts b/src/server/routes/batchCreateKey.ts new file mode 100644 index 0000000..3504d26 --- /dev/null +++ b/src/server/routes/batchCreateKey.ts @@ -0,0 +1,46 @@ +import type { NextFunction, Request, Response } from 'express' +import { z } from 'zod' +import { KeyType } from '../../hsm/keyType' + +export const batchCreateKeySchema = z.object({ + keyIds: z.array(z.string()), + keyType: z.nativeEnum(KeyType), +}) + +export const batchCreateKey = async (req: Request, res: Response, next: NextFunction) => { + try { + console.log( + `[createKey] with keyIds: '${req.jwtPayload.keyIds}', keyRingId: '${req.keyRingId}', keyType: '${req.jwtPayload.keyType}'` + ) + if (!req.keyRingId) throw new Error('keyRingId not set on the request') + + const requests = (req.jwtPayload.keyIds as Array).map(async (keyId: string) => { + await req.hsm.createKey({ + keyType: req.jwtPayload.keyType as KeyType, + keyId: keyId as string, + keyRingId: req.keyRingId as string, + }) + + const publicKey = await req.hsm.getPublicKey({ + keyId: keyId as string, + keyRingId: req.keyRingId as string, + }) + return { + publicKey, + keyId, + } + }) + + const publicKeys = await Promise.all(requests) + + const keys: Record> = publicKeys.reduce( + (prev, curr) => ({ ...prev, [curr.keyId]: new Array(...curr.publicKey) }), + {} + ) + + res.json({ publicKeys: keys }) + } catch (e) { + console.error('[error] :: [createKey] :: ', e) + next(e) + } +} diff --git a/src/server/routes/createKey.ts b/src/server/routes/createKey.ts new file mode 100644 index 0000000..4f508f0 --- /dev/null +++ b/src/server/routes/createKey.ts @@ -0,0 +1,32 @@ +import type { NextFunction, Request, Response } from 'express' +import { z } from 'zod' +import { KeyType } from '../../hsm/keyType' + +export const createKeySchema = z.object({ + keyId: z.string(), + keyType: z.nativeEnum(KeyType), +}) + +export const createKey = async (req: Request, res: Response, next: NextFunction) => { + try { + console.log( + `[createKey] with keyId: '${req.jwtPayload.keyId}', keyRingId: '${req.keyRingId}', keyType: '${req.jwtPayload.keyType}'` + ) + if (!req.keyRingId) throw new Error('keyRingId not set on the request') + await req.hsm.createKey({ + keyType: req.jwtPayload.keyType as KeyType, + keyId: req.jwtPayload.keyId as string, + keyRingId: req.keyRingId, + }) + + const publicKey = await req.hsm.getPublicKey({ + keyId: req.jwtPayload.keyId as string, + keyRingId: req.keyRingId, + }) + + res.json({ publicKey: new Array(...publicKey) }) + } catch (e) { + console.error('[error] :: [createKey] :: ', e) + next(e) + } +} diff --git a/src/server/routes/getPublicKey.ts b/src/server/routes/getPublicKey.ts new file mode 100644 index 0000000..5e7ba52 --- /dev/null +++ b/src/server/routes/getPublicKey.ts @@ -0,0 +1,18 @@ +import type { NextFunction, Request, Response } from 'express' +import { z } from 'zod' + +export const getPublicKeySchema = z.object({ + keyId: z.string(), +}) + +export const getPublicKey = async (req: Request, res: Response, next: NextFunction) => { + try { + console.log(`[getPublicKey] with keyId: '${req.jwtPayload.keyId}', keyRingId: '${req.keyRingId}'`) + if (!req.keyRingId) throw new Error('keyRingId not set on the request') + const publicKey = await req.hsm.getPublicKey({ keyId: req.jwtPayload.keyId as string, keyRingId: req.keyRingId }) + res.json({ publicKey: new Array(...publicKey) }) + } catch (e) { + console.error('[error] :: [getPublicKey] :: ', e) + next(e) + } +} diff --git a/src/server/routes/getWalletAttestation.ts b/src/server/routes/getWalletAttestation.ts new file mode 100644 index 0000000..bc5db7b --- /dev/null +++ b/src/server/routes/getWalletAttestation.ts @@ -0,0 +1,34 @@ +import type { NextFunction, Request, Response } from 'express' +import { z } from 'zod' +import type { Jwk } from '@animo-id/oauth2' +import { createClientAttestation } from '../oauth2/clientAttestations' +import { randomUUID } from 'node:crypto' + +export const getWalletAttestationSchema = z.object({ + jwk: z.object({ + kty: z.literal('EC'), + crv: z.literal('P-256'), + x: z.string(), + y: z.string(), + }), +}) + +export const getWalletAttestation = async (req: Request, res: Response, next: NextFunction) => { + try { + console.log('[getWalletAttestation]') + + if (!req.keyRingId) throw new Error('keyRingId not set on the request') + + // Client id doesn't really matter we use our domain as prefix to prevent collision + const walletAttestation = await createClientAttestation({ + clientId: `https://wallet.paradym.id/${randomUUID()}`, + // TODO: we now just take the key passed by the client until we can do proper app integrity + clientJwk: req.jwtPayload.jwk as Jwk, + }) + + res.json({ walletAttestation }) + } catch (e) { + console.error('[error] :: [getWalletAttestation] :: ', e) + next(e) + } +} diff --git a/src/server/routes/index.ts b/src/server/routes/index.ts new file mode 100644 index 0000000..adcb7f4 --- /dev/null +++ b/src/server/routes/index.ts @@ -0,0 +1,4 @@ +export * from './sign' +export * from './createKey' +export * from './getPublicKey' +export * from './registerWallet' diff --git a/src/server/routes/registerWallet.ts b/src/server/routes/registerWallet.ts new file mode 100644 index 0000000..2b39023 --- /dev/null +++ b/src/server/routes/registerWallet.ts @@ -0,0 +1,13 @@ +import type { NextFunction, Request, Response } from 'express' + +export const registerWallet = async (req: Request, res: Response, next: NextFunction) => { + try { + console.log(`[registerWallet] with walletId: '${req.walletId}'`) + const keyRingId = await req.hsm.registerWallet() + await req.db.createWallet({ walletId: req.walletId, keyRingId }) + res.json({}) + } catch (e) { + console.error('[error] :: [registerWallet] :: ', e) + next(e) + } +} diff --git a/src/server/routes/sign.ts b/src/server/routes/sign.ts new file mode 100644 index 0000000..4deb6af --- /dev/null +++ b/src/server/routes/sign.ts @@ -0,0 +1,24 @@ +import type { NextFunction, Request, Response } from 'express' +import { z } from 'zod' +import { uint8arraySchema } from '../../utils' + +export const signSchema = z.object({ + keyId: z.string(), + data: uint8arraySchema, +}) + +export const sign = async (req: Request, res: Response, next: NextFunction) => { + try { + console.log(`[sign]: with keyId: '${req.jwtPayload.keyId}'`) + if (!req.keyRingId) throw new Error('keyRingId not set on the request') + const signature = await req.hsm.sign({ + data: req.jwtPayload.data as Uint8Array, + keyId: req.jwtPayload.keyId as string, + keyRingId: req.keyRingId, + }) + res.json({ signature: new Array(...signature) }) + } catch (e) { + console.error('[error] :: [sign] :: ', e) + next(e) + } +} diff --git a/src/utils/base58.ts b/src/utils/base58.ts new file mode 100644 index 0000000..4b3b0d4 --- /dev/null +++ b/src/utils/base58.ts @@ -0,0 +1,13 @@ +import base from '@multiformats/base-x' + +const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' + +const base58Converter = base(BASE58_ALPHABET) + +export function decodeFromBase58(base58: string) { + return base58Converter.decode(base58) +} + +export function encodeToBase58(buffer: Uint8Array) { + return base58Converter.encode(buffer) +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..5ab08a1 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './schemas' +export * from './base58' diff --git a/src/utils/schemas.ts b/src/utils/schemas.ts new file mode 100644 index 0000000..3ce11fa --- /dev/null +++ b/src/utils/schemas.ts @@ -0,0 +1,5 @@ +import { z } from 'zod' + +const uint8schema = z.number().min(0).max(255) + +export const uint8arraySchema = z.array(uint8schema).transform((data) => new Uint8Array(data)) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..692a5f9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2020", + "outDir": "build", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["src"] +}