From d0d183478b5ae23d2bd455b803c920ca417ca7f7 Mon Sep 17 00:00:00 2001 From: Philipp Zagar Date: Thu, 28 Mar 2024 12:01:30 -0700 Subject: [PATCH] Add SpeziLLMFog that performs dynamic LLM job dispatch to fog nodes (#52) # Add SpeziLLMFog that performs dynamic LLM job dispatch to fog nodes ## :recycle: Current situation & Problem Currently, SpeziLLM doesn't support a privacy friendly execution of LLM inference jobs within the local network. ## :gear: Release Notes - Add SpeziLLMFog that performs dynamic LLM job dispatch to fog nodes. - Include a FogNode component that easily starts up a complete fog node ready to perform LLM inference requests via docker compose. - Introduce custom `LLMContext` type that holds the internal state of the `LLMSession`, instead of relying on the `SpeziChat` models. ## :books: Documentation Added proper readmes, docs, and in-line comments ## :white_check_mark: Testing Manual testing (for now) ## :pencil: Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md): - [x] I agree to follow the [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md). --- .github/workflows/build-and-test.yml | 106 +- .gitignore | 6 + FogNode/README.md | 55 + FogNode/auth/.gitignore | 54 + FogNode/auth/Dockerfile | 41 + FogNode/auth/firebaseEmulator/.firebaserc | 5 + .../auth/firebaseEmulator/.firebaserc.license | 5 + FogNode/auth/firebaseEmulator/Dockerfile | 31 + FogNode/auth/firebaseEmulator/firebase.json | 14 + .../firebaseEmulator/firebase.json.license | 5 + FogNode/auth/package-lock.json | 5023 +++++++++++++++++ FogNode/auth/package-lock.json.license | 5 + FogNode/auth/package.json | 26 + FogNode/auth/package.json.license | 5 + FogNode/auth/src/authToken.ts | 34 + FogNode/auth/src/firebase.ts | 36 + FogNode/auth/src/index.ts | 50 + FogNode/auth/tsconfig.json | 16 + FogNode/auth/tsconfig.json.license | 5 + FogNode/avahi/Dockerfile | 29 + FogNode/avahi/Dockerfile-Sidecar | 33 + FogNode/avahi/docker-entrypoint-sidecar.sh | 21 + FogNode/avahi/services/spezillmfog.service | 20 + FogNode/certs/ca/.gitkeep | 0 FogNode/certs/openssl.cnf | 27 + FogNode/certs/openssl.cnf.license | 5 + FogNode/certs/webservice/.gitkeep | 0 FogNode/docker-compose.avahi.yml | 31 + FogNode/docker-compose.dev.yml | 93 + FogNode/docker-compose.yml | 83 + FogNode/setup.sh | 31 + FogNode/traefik/dynamic_conf.yml | 17 + Package.swift | 15 +- README.md | 86 +- Sources/SpeziLLM/Helpers/Chat+Append.swift | 95 - .../SpeziLLM/Helpers/LLMContext+Append.swift | 105 + .../SpeziLLM/Helpers/LLMContext+Chat.swift | 83 + ...{Chat+Init.swift => LLMContext+Init.swift} | 6 +- Sources/SpeziLLM/LLMRunner.swift | 12 +- Sources/SpeziLLM/LLMSession.swift | 4 +- Sources/SpeziLLM/Mock/LLMMockSession.swift | 3 +- Sources/SpeziLLM/Models/LLMContext.swift | 13 + .../SpeziLLM/Models/LLMContextEntity.swift | 92 + Sources/SpeziLLM/{ => Models}/LLMError.swift | 0 .../LLMState+OperationState.swift | 0 Sources/SpeziLLM/{ => Models}/LLMState.swift | 0 Sources/SpeziLLM/Views/LLMChatView.swift | 7 +- .../SpeziLLM/Views/LLMChatViewSchema.swift | 2 +- .../Configuration/LLMFogModelParameters.swift | 66 + .../Configuration/LLMFogParameters.swift | 75 + .../LLMFogPlatformConfiguration.swift | 52 + Sources/SpeziLLMFog/Helpers/Chat+OpenAI.swift | 26 + Sources/SpeziLLMFog/LLMFogError.swift | 111 + Sources/SpeziLLMFog/LLMFogPlatform.swift | 100 + Sources/SpeziLLMFog/LLMFogSchema.swift | 63 + .../LLMFogSession+Configuration.swift | 47 + .../LLMFogSession+Generation.swift | 91 + Sources/SpeziLLMFog/LLMFogSession+Setup.swift | 154 + Sources/SpeziLLMFog/LLMFogSession.swift | 172 + .../Resources/Localizable.xcstrings | 246 + .../Resources/Localizable.xcstrings.license | 5 + .../SpeziLLMFog.docc/SpeziLLMFog.md | 130 + .../LLMLocalContextParameters.swift | 2 +- .../Configuration/LLMLocalParameters.swift | 2 +- .../LLMLocalSamplingParameters.swift | 60 +- .../LLMLocalSchema+PromptFormatting.swift | 58 +- Sources/SpeziLLMLocal/LLMLocalSchema.swift | 4 +- .../LLMLocalSession+Generation.swift | 7 +- .../LLMLocalSession+PromptFormatting.swift | 148 - Sources/SpeziLLMLocal/LLMLocalSession.swift | 2 +- .../LLMOpenAIModelParameters.swift | 11 +- .../Configuration/LLMOpenAIParameters.swift | 10 +- .../LLMOpenAIPlatformConfiguration.swift | 3 + .../LLMFunctionParameterSchemaCollector.swift | 2 +- ...MFunctionParameterWrapper+ArrayTypes.swift | 6 +- ...FunctionParameterWrapper+CustomTypes.swift | 4 +- .../LLMFunctionParameterWrapper+Enum.swift | 8 +- ...nctionParameterWrapper+OptionalTypes.swift | 12 +- ...ctionParameterWrapper+PrimitiveTypes.swift | 6 +- .../LLMFunctionParameterWrapper.swift | 11 +- .../SpeziLLMOpenAI/Helpers/Chat+OpenAI.swift | 15 +- .../Helpers/LLMOpenAIFinishReason.swift | 20 - .../Helpers/LLMOpenAIStreamResult.swift | 65 +- Sources/SpeziLLMOpenAI/LLMOpenAIError.swift | 9 - .../LLMOpenAISession+Configuration.swift | 74 +- .../LLMOpenAISession+Generation.swift | 46 +- Sources/SpeziLLMOpenAI/LLMOpenAISession.swift | 9 +- .../Resources/Localizable.xcstrings | 30 - .../SpeziLLMOpenAI.docc/FunctionCalling.md | 2 +- .../SpeziLLMOpenAI.docc/SpeziLLMOpenAI.md | 4 +- .../LLMOpenAIParameterTests+Array.swift | 4 +- .../LLMOpenAIParameterTests+Enum.swift | 8 +- ...LMOpenAIParameterTests+OptionalTypes.swift | 8 +- ...MOpenAIParameterTests+PrimitiveTypes.swift | 4 +- Tests/UITests/TestApp/FeatureFlags.swift | 2 + .../LLMFog/Account/AccountSetupHeader.swift | 37 + .../TestApp/LLMFog/Account/AccountSheet.swift | 64 + .../TestApp/LLMFog/LLMFogChatTestView.swift | 62 + .../Onboarding/LLMLocalOnboardingFlow.swift | 2 +- .../LLMOpenAIFunctionHealthData.swift | 2 +- .../LLMOpenAI/LLMOpenAIChatTestView.swift | 2 +- .../Onboarding/LLMOpenAITokenOnboarding.swift | 2 +- .../Resources/GoogleService-Info.plist | 34 + .../GoogleService-Info.plist.license | 5 + .../TestApp/Resources/Localizable.xcstrings | 60 + Tests/UITests/TestApp/TestApp.swift | 3 + Tests/UITests/TestApp/TestAppDelegate.swift | 31 +- .../UITests/TestApp/TestAppTestingSetup.swift | 12 +- .../TestAppLLMLocalUITests.swift | 10 +- .../TestAppLLMOpenAIUITests.swift | 70 +- .../UITests/UITests.xcodeproj/project.pbxproj | 75 +- .../xcshareddata/xcschemes/TestApp.xcscheme | 4 + 112 files changed, 8394 insertions(+), 510 deletions(-) create mode 100644 FogNode/README.md create mode 100644 FogNode/auth/.gitignore create mode 100644 FogNode/auth/Dockerfile create mode 100644 FogNode/auth/firebaseEmulator/.firebaserc create mode 100644 FogNode/auth/firebaseEmulator/.firebaserc.license create mode 100644 FogNode/auth/firebaseEmulator/Dockerfile create mode 100644 FogNode/auth/firebaseEmulator/firebase.json create mode 100644 FogNode/auth/firebaseEmulator/firebase.json.license create mode 100644 FogNode/auth/package-lock.json create mode 100644 FogNode/auth/package-lock.json.license create mode 100644 FogNode/auth/package.json create mode 100644 FogNode/auth/package.json.license create mode 100644 FogNode/auth/src/authToken.ts create mode 100644 FogNode/auth/src/firebase.ts create mode 100644 FogNode/auth/src/index.ts create mode 100644 FogNode/auth/tsconfig.json create mode 100644 FogNode/auth/tsconfig.json.license create mode 100644 FogNode/avahi/Dockerfile create mode 100644 FogNode/avahi/Dockerfile-Sidecar create mode 100644 FogNode/avahi/docker-entrypoint-sidecar.sh create mode 100644 FogNode/avahi/services/spezillmfog.service create mode 100644 FogNode/certs/ca/.gitkeep create mode 100644 FogNode/certs/openssl.cnf create mode 100644 FogNode/certs/openssl.cnf.license create mode 100644 FogNode/certs/webservice/.gitkeep create mode 100644 FogNode/docker-compose.avahi.yml create mode 100644 FogNode/docker-compose.dev.yml create mode 100644 FogNode/docker-compose.yml create mode 100755 FogNode/setup.sh create mode 100644 FogNode/traefik/dynamic_conf.yml delete mode 100644 Sources/SpeziLLM/Helpers/Chat+Append.swift create mode 100644 Sources/SpeziLLM/Helpers/LLMContext+Append.swift create mode 100644 Sources/SpeziLLM/Helpers/LLMContext+Chat.swift rename Sources/SpeziLLM/Helpers/{Chat+Init.swift => LLMContext+Init.swift} (94%) create mode 100644 Sources/SpeziLLM/Models/LLMContext.swift create mode 100644 Sources/SpeziLLM/Models/LLMContextEntity.swift rename Sources/SpeziLLM/{ => Models}/LLMError.swift (100%) rename Sources/SpeziLLM/{ => Models}/LLMState+OperationState.swift (100%) rename Sources/SpeziLLM/{ => Models}/LLMState.swift (100%) create mode 100644 Sources/SpeziLLMFog/Configuration/LLMFogModelParameters.swift create mode 100644 Sources/SpeziLLMFog/Configuration/LLMFogParameters.swift create mode 100644 Sources/SpeziLLMFog/Configuration/LLMFogPlatformConfiguration.swift create mode 100644 Sources/SpeziLLMFog/Helpers/Chat+OpenAI.swift create mode 100644 Sources/SpeziLLMFog/LLMFogError.swift create mode 100644 Sources/SpeziLLMFog/LLMFogPlatform.swift create mode 100644 Sources/SpeziLLMFog/LLMFogSchema.swift create mode 100644 Sources/SpeziLLMFog/LLMFogSession+Configuration.swift create mode 100644 Sources/SpeziLLMFog/LLMFogSession+Generation.swift create mode 100644 Sources/SpeziLLMFog/LLMFogSession+Setup.swift create mode 100644 Sources/SpeziLLMFog/LLMFogSession.swift create mode 100644 Sources/SpeziLLMFog/Resources/Localizable.xcstrings create mode 100644 Sources/SpeziLLMFog/Resources/Localizable.xcstrings.license create mode 100644 Sources/SpeziLLMFog/SpeziLLMFog.docc/SpeziLLMFog.md delete mode 100644 Sources/SpeziLLMLocal/LLMLocalSession+PromptFormatting.swift delete mode 100644 Sources/SpeziLLMOpenAI/Helpers/LLMOpenAIFinishReason.swift create mode 100644 Tests/UITests/TestApp/LLMFog/Account/AccountSetupHeader.swift create mode 100644 Tests/UITests/TestApp/LLMFog/Account/AccountSheet.swift create mode 100644 Tests/UITests/TestApp/LLMFog/LLMFogChatTestView.swift create mode 100644 Tests/UITests/TestApp/Resources/GoogleService-Info.plist create mode 100644 Tests/UITests/TestApp/Resources/GoogleService-Info.plist.license diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index f0df892..43b2f6f 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -1,7 +1,7 @@ # # This source file is part of the Stanford Spezi open source project # -# SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +# SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) # # SPDX-License-Identifier: MIT # @@ -16,40 +16,124 @@ on: workflow_dispatch: jobs: - buildandtest: - name: Build and Test Swift Package + buildandtest_ios: + name: Build and Test Swift Package iOS uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 strategy: matrix: include: - buildConfig: Debug - artifactname: SpeziLLM-Package.xcresult + artifactname: SpeziLLM-iOS.xcresult + resultBundle: SpeziLLM-iOS.xcresult - buildConfig: Release - artifactname: SpeziLLM-Package-Release.xcresult + artifactname: SpeziLLM-iOS-Release.xcresult + resultBundle: SpeziLLM-iOS-Release.xcresult with: + runsonlabels: '["macOS", "self-hosted"]' + scheme: SpeziLLM-Package + buildConfig: ${{ matrix.buildConfig }} + resultBundle: ${{ matrix.resultBundle }} artifactname: ${{ matrix.artifactname }} + buildandtest_visionos: + name: Build and Test Swift Package visionOS + uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 + strategy: + matrix: + include: + - buildConfig: Debug + artifactname: SpeziLLM-visionOS.xcresult + resultBundle: SpeziLLM-visionOS.xcresult + - buildConfig: Release + artifactname: SpeziLLM-visionOS-Release.xcresult + resultBundle: SpeziLLM-visionOS-Release.xcresult + with: runsonlabels: '["macOS", "self-hosted"]' scheme: SpeziLLM-Package + destination: 'platform=visionOS Simulator,name=Apple Vision Pro' + buildConfig: ${{ matrix.buildConfig }} + resultBundle: ${{ matrix.resultBundle }} + artifactname: ${{ matrix.artifactname }} + buildandtest_macos: + name: Build and Test Swift Package macOS + uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 + strategy: + matrix: + include: + - buildConfig: Debug + artifactname: SpeziLLM-macOS.xcresult + resultBundle: SpeziLLM-macOS.xcresult + - buildConfig: Release + artifactname: SpeziLLM-macOS-Release.xcresult + resultBundle: SpeziLLM-macOS-Release.xcresult + with: + runsonlabels: '["macOS", "self-hosted"]' + scheme: SpeziLLM-Package + destination: 'platform=macOS,arch=arm64' + buildConfig: ${{ matrix.buildConfig }} + resultBundle: ${{ matrix.resultBundle }} + artifactname: ${{ matrix.artifactname }} + buildandtestuitests_ios: + name: Build and Test UI Tests iOS + uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 + strategy: + matrix: + include: + - buildConfig: Debug + resultBundle: TestApp-iOS.xcresult + artifactname: TestApp-iOS.xcresult + - buildConfig: Release + resultBundle: TestApp-iOS-Release.xcresult + artifactname: TestApp-iOS-Release.xcresult + with: + runsonlabels: '["macOS", "self-hosted"]' + path: 'Tests/UITests' + scheme: TestApp buildConfig: ${{ matrix.buildConfig }} - buildandtestuitests: - name: Build and Test UI Tests + resultBundle: ${{ matrix.resultBundle }} + artifactname: ${{ matrix.artifactname }} + buildandtestuitests_ipad: + name: Build and Test UI Tests iPadOS uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 strategy: matrix: include: - buildConfig: Debug - artifactname: TestApp.xcresult + resultBundle: TestApp-iPad.xcresult + artifactname: TestApp-iPad.xcresult - buildConfig: Release - artifactname: TestApp-Release.xcresult + resultBundle: TestApp-iPad-Release.xcresult + artifactname: TestApp-iPad-Release.xcresult with: + runsonlabels: '["macOS", "self-hosted"]' + path: 'Tests/UITests' + scheme: TestApp + destination: 'platform=iOS Simulator,name=iPad Air (5th generation)' + buildConfig: ${{ matrix.buildConfig }} + resultBundle: ${{ matrix.resultBundle }} artifactname: ${{ matrix.artifactname }} + buildandtestuitests_visionos: + name: Build and Test UI Tests visionOS + uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 + strategy: + matrix: + include: + - buildConfig: Debug + resultBundle: TestApp-visionOS.xcresult + artifactname: TestApp-visionOS.xcresult + - buildConfig: Release + resultBundle: TestApp-visionOS-Release.xcresult + artifactname: TestApp-visionOS-Release.xcresult + with: runsonlabels: '["macOS", "self-hosted"]' path: 'Tests/UITests' scheme: TestApp + destination: 'platform=visionOS Simulator,name=Apple Vision Pro' buildConfig: ${{ matrix.buildConfig }} + resultBundle: ${{ matrix.resultBundle }} + artifactname: ${{ matrix.artifactname }} uploadcoveragereport: name: Upload Coverage Report - needs: [buildandtest, buildandtestuitests] + needs: [buildandtest_ios, buildandtest_visionos, buildandtest_macos, buildandtestuitests_ios, buildandtestuitests_ipad, buildandtestuitests_visionos] uses: StanfordSpezi/.github/.github/workflows/create-and-upload-coverage-report.yml@v2 with: - coveragereports: SpeziLLM-Package.xcresult TestApp.xcresult + coveragereports: 'SpeziLLM-iOS.xcresult SpeziLLM-visionOS.xcresult SpeziLLM-macOS.xcresult TestApp-iOS.xcresult TestApp-iPad.xcresult TestApp-visionOS.xcresult' \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0d282ab..01c3f2c 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,9 @@ docs/ # UITests Project !UITests.xcodeproj + +# Generated CA and TLS keys of the Fog webservice +*.crt +*.key +*.srl +*.csr diff --git a/FogNode/README.md b/FogNode/README.md new file mode 100644 index 0000000..1ccafb5 --- /dev/null +++ b/FogNode/README.md @@ -0,0 +1,55 @@ + + +# SpeziLLMFog FogNode + +Offers the functionality to dynamically dispatch LLM inference jobs from mobile devices to fog nodes within the local network that implement the OpenAI API. + +## Overview + +The client-side implementation of the fog execution environment is part of the Swift-based `SpeziLLM` package, specifically the [`SpeziLLMFog` target](https://swiftpackageindex.com/StanfordSpezi/SpeziLLM/documentation/spezillmfog). +On the other hand, the server-side implementation is a web service that offers LLM inference capabilities within the local network. This setup tutorial demonstrates how to set up the server-side fog node. + +## Architecture + +The SpeziLLM fog node offers LLM inference capabilities within the local network. +It consists of the following components: + +- **LLM Inference capabilities**: The LLM inference is performed by the [Ollama open-source framework](https://github.com/ollama/ollama) that executes openly available LLMs such as [Llama2](https://ollama.com/library/llama2) or [Gemma](https://ollama.com/library/gemma). A full list of the available models can be found [here](https://ollama.com/library). The API surface of [Ollama mirrors the OpenAI API](https://github.com/ollama/ollama/blob/main/docs/openai.md), at least for basic inference requests. +- **Service advertisement**: As SpeziLLM intends to operate within a [fog computing environment](https://en.wikipedia.org/wiki/Fog_computing), the LLM execution resource (the LLM webservice) is advertised within the local network via [mDNS](https://en.wikipedia.org/wiki/Multicast_DNS). On macOS, this is achieved via [Apple's Bonjour framework](https://developer.apple.com/bonjour), on Linux the [Avahi component](https://avahi.org/) is used (both of these services are interoperable with another). +- **Secure local connections**: To ensure secure data transfer from and to the fog node within the local network, the [`traefik` reverse proxy](https://traefik.io/traefik/) only serves the LLM inference API via secure SSL connections. The TLS verification is achieved via a custom-issued [root CA certificate](https://en.wikipedia.org/wiki/Root_certificate) that signs the TLS certificate of the web service offering the LLM inference jobs. As these certificates need to be unique and secret, they are not part of the FogNode package but are rather generated by a script on the respective computing resource by the administrator (see setup instructions below) +- **User authentication**: The fog node requires user authentication by verifying the passed [Firebase User ID Bearer token](https://firebase.google.com/docs/auth/admin/verify-id-tokens) in the HTTP Authentication header. By default, the fog node only verifies the authenticity of the User ID token, not if the user is actually allowed to access the resource (this could be achieved by, e.g., custom claims in the token). +- **Packaging**: Lastly, as the fog node should be able to run on diverse platforms and the setup process should be as easy as possible, the entire fog component is packaged via [Docker](https://www.docker.com/). + +## Setup + +In order to correctly set up the Fog node, a couple of setup steps have to been taken. These steps are performed via the `setup.sh` shell script. + +### Requirements + +- Operating system: Either Linux or macOS +- [Docker Engine v25.0](https://docs.docker.com/engine/install/) as well as [Docker Compose v2](https://docs.docker.com/compose/install/) +- On macOS, one needs to use [Bonjour](https://developer.apple.com/bonjour) for mDNS advertisements (as Avahi only works on Linux distributions) + +### Executing the setup script + +The `setup.sh` script generates the custom CA root certificate as well as the web service certificate. They are persisted in the `ca` as well as `webservice` directories. Keep in mind that the application using the Fog Node (most likely via `SpeziLLMFog`) must trust this custom root CA certificate. +Once the script ran through, the last output of the script should be a warning about issuing the Firebase service account key via the Firebase console. Put the file within the `auth` directory under the name `serviceAccountKey.json`, it is then automatically picked up by the authorization service. + +Lastly, start the container services via Docker Compose: +- On Linux, execute `docker compose --platform=linux up` to start the service, use the `-d` flag to run it in the background like: `docker compose --platform=linux up -d`. The service is automatically advertised by Avahi via mDNS from the Docker service. +- On macOS, run `docker compose up` to start the service. In addition, because of technical limitations of Avahi within a Docker container on macOS, one has to manually run the mDNS advertisement via Bonjour: `dns-sd -R "SpeziLLMFog Service" _https._tcp spezillmfog.local 443`. It advertises the service under the `spezillmfog.local` domain name with the `"SpeziLLMFog Service"` user-friendly name. + +### Development + +For development purposes, the `docker-compose.dev.yml` file starts up the fog node without TLS certificates and with the usage of the Firebase Emulator. In that case, one doesn't have to execute the setup script mentioned above (as no certificates are required without a TLS connection) and doesn't have to get the Firebase service account key from the Firebase Console. +In addition, this development compose file doesn't include an mDNS advertisement service. The developer is responsible for advertising the service. On macOS, which is the primary development environment for SpeziLLMFog, this can be done via Bonjour and the `dns-sd -R "SpeziLLMFog Service" _http._tcp spezillmfog.local 80` command. Note that the service advertises an `http` service with port 80, in contrast to the production setup with HTTPS and port 443 (secure traffic). + +Another file for development purposes is the `docker-compose.avahi.yml` file. One container advertises an mDNS service via Avahi, another container discovers this service via an Avahi Sidecar. This setup is incredibly useful to test mDNS announcements on the Linux platform. \ No newline at end of file diff --git a/FogNode/auth/.gitignore b/FogNode/auth/.gitignore new file mode 100644 index 0000000..a33b696 --- /dev/null +++ b/FogNode/auth/.gitignore @@ -0,0 +1,54 @@ +# +# This source file is part of the Stanford Spezi open source project +# +# SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT +# + +# Firebase Admin SDK Service Account Key +serviceAccountKey.json + +# Compiled TS project +/dist +# NPM dependencies +/node_modules + +# IDE +.vscode/ +.idea/ + +# TypeScript cache +*.tsbuildinfo + +# Log files +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage () +.grunt + +# Bower dependency directory () +bower_components + +# Dependency directory +# Commenting this out is preferred by some developers, npm can +# handle it properly when it's symlinked (npm v3+) +# node_modules/ + +# TSD Debug info +tsd-debug.log \ No newline at end of file diff --git a/FogNode/auth/Dockerfile b/FogNode/auth/Dockerfile new file mode 100644 index 0000000..448d987 --- /dev/null +++ b/FogNode/auth/Dockerfile @@ -0,0 +1,41 @@ +# +# This source file is part of the Stanford Spezi open source project +# +# SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT +# + +# Stage 1: Build stage +FROM node:21-alpine3.19 AS builder + +LABEL org.opencontainers.image.authors="Philipp Zagar " \ + org.opencontainers.image.version="0.1" \ + org.opencontainers.image.title="stanfordspezi/firebase-auth-service" \ + org.opencontainers.image.description="SpeziLLMFog Firebase Authentication Service" \ + org.opencontainers.image.url="https://ghcr.io/stanfordspezi/firebase-auth-service" \ + org.opencontainers.image.source="https://github.com/StanfordSpezi/SpeziLLM" + +WORKDIR /usr/src/app + +# Install npm dependencies +COPY package*.json ./ +RUN npm install + +# Copy source code and compile TypeScript project +COPY tsconfig.json ./ +COPY src/ ./src +RUN npm run build + +# Stage 2: Runtime stage +FROM node:21-alpine3.19 + +WORKDIR /usr/src/app + +# Copy compiled files and necessary npm packages from the builder stage +COPY --from=builder /usr/src/app/dist ./dist +COPY --from=builder /usr/src/app/node_modules ./node_modules +COPY --from=builder /usr/src/app/package.json ./package.json + +# Start the nodeJS application +CMD [ "node", "dist/index.js" ] diff --git a/FogNode/auth/firebaseEmulator/.firebaserc b/FogNode/auth/firebaseEmulator/.firebaserc new file mode 100644 index 0000000..a9a6a19 --- /dev/null +++ b/FogNode/auth/firebaseEmulator/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "spezillmfog" + } +} \ No newline at end of file diff --git a/FogNode/auth/firebaseEmulator/.firebaserc.license b/FogNode/auth/firebaseEmulator/.firebaserc.license new file mode 100644 index 0000000..6917c4b --- /dev/null +++ b/FogNode/auth/firebaseEmulator/.firebaserc.license @@ -0,0 +1,5 @@ +This source file is part of the Stanford Spezi open-source project + +SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) + +SPDX-License-Identifier: MIT diff --git a/FogNode/auth/firebaseEmulator/Dockerfile b/FogNode/auth/firebaseEmulator/Dockerfile new file mode 100644 index 0000000..17ed536 --- /dev/null +++ b/FogNode/auth/firebaseEmulator/Dockerfile @@ -0,0 +1,31 @@ +# +# This source file is part of the Stanford Spezi open source project +# +# SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT +# + +FROM alpine:3.19 + +LABEL org.opencontainers.image.authors="Philipp Zagar " \ + org.opencontainers.image.version="0.1" \ + org.opencontainers.image.title="stanfordspezi/firebase-emulator-auth" \ + org.opencontainers.image.description="SpeziLLMFog Firebase Emulator Auth" \ + org.opencontainers.image.url="https://ghcr.io/stanfordspezi/firebase-emulator-auth" \ + org.opencontainers.image.source="https://github.com/StanfordSpezi/SpeziLLM" + +# Install Firebase CLI +RUN npm install -g firebase-tools + +WORKDIR /app + +# Copy firebase emulator config files +COPY .firebaserc .firebaserc +COPY firebase.json firebase.json + +# Expose web ui and auth service +EXPOSE 4000 9099 + +# Run the Firebase Emulators +CMD ["firebase", "emulators:start"] diff --git a/FogNode/auth/firebaseEmulator/firebase.json b/FogNode/auth/firebaseEmulator/firebase.json new file mode 100644 index 0000000..04b8e28 --- /dev/null +++ b/FogNode/auth/firebaseEmulator/firebase.json @@ -0,0 +1,14 @@ +{ + "emulators": { + "auth": { + "port": 9099, + "host": "0.0.0.0" + }, + "ui": { + "enabled": true, + "port": 4000, + "host": "0.0.0.0" + }, + "singleProjectMode": true + } +} \ No newline at end of file diff --git a/FogNode/auth/firebaseEmulator/firebase.json.license b/FogNode/auth/firebaseEmulator/firebase.json.license new file mode 100644 index 0000000..6917c4b --- /dev/null +++ b/FogNode/auth/firebaseEmulator/firebase.json.license @@ -0,0 +1,5 @@ +This source file is part of the Stanford Spezi open-source project + +SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) + +SPDX-License-Identifier: MIT diff --git a/FogNode/auth/package-lock.json b/FogNode/auth/package-lock.json new file mode 100644 index 0000000..da568c0 --- /dev/null +++ b/FogNode/auth/package-lock.json @@ -0,0 +1,5023 @@ +{ + "name": "auth-service", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "auth-service", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@types/express": "^4.17.21", + "@types/node": "^20.11.26", + "dotenv": "^16.4.5", + "express": "^4.18.3", + "firebase-admin": "^12.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.4.2" + }, + "devDependencies": { + "nodemon": "^3.1.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@fastify/busboy": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.2.1.tgz", + "integrity": "sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==", + "dependencies": { + "text-decoding": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.0.tgz", + "integrity": "sha512-xAxHPZPIgFXnI+vb4sbBjZcde7ZluzPPaSK7Lx3/nmuVk4TjZvnL8ONnkd4ERQKL8WePQySU+pRcWkh8rDf5Sg==" + }, + "node_modules/@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==" + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz", + "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==" + }, + "node_modules/@firebase/component": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.5.tgz", + "integrity": "sha512-2tVDk1ixi12sbDmmfITK8lxSjmcb73BMF6Qwc3U44hN/J1Fi1QY/Hnnb6klFlbB9/G16a3J3d4nXykye2EADTw==", + "dependencies": { + "@firebase/util": "1.9.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.3.tgz", + "integrity": "sha512-9fjqLt9JzL46gw9+NRqsgQEMjgRwfd8XtzcKqG+UYyhVeFCdVRQ0Wp6Dw/dvYHnbH5vNEKzNv36dcB4p+PIAAA==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.0", + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.5", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.4", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.3.tgz", + "integrity": "sha512-7tHEOcMbK5jJzHWyphPux4osogH/adWwncxdMxdBpB9g1DNIyY4dcz1oJdlkXGM/i/AjUBesZsd5CuwTRTBNTw==", + "dependencies": { + "@firebase/component": "0.6.5", + "@firebase/database": "1.0.3", + "@firebase/database-types": "1.0.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.1.tgz", + "integrity": "sha512-Tmcmx5XgiI7UVF/4oGg2P3AOTfq3WKEPsm2yf+uXtN7uG/a4WTWhVMrXGYRY2ZUL1xPxv9V33wQRJ+CcrUhVXw==", + "dependencies": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.4" + } + }, + "node_modules/@firebase/logger": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", + "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/util": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.4.tgz", + "integrity": "sha512-WLonYmS1FGHT97TsUmRN3qnTh5TeeoJp1Gg5fithzuAgdZOUtsYECfy7/noQ3llaguios8r5BuXSEiK82+UrxQ==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@google-cloud/firestore": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.3.1.tgz", + "integrity": "sha512-YluLZbJK3dHXq6Ns5URCtr6hjBiG+6EM17QSivjaozPYDsv1R9a9mkWPz+jCQrb6Ewz6mxp3zavu6DXxvmSWLA==", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^4.0.4", + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.0.tgz", + "integrity": "sha512-87aeg6QQcEPxGCOthnpUjvw4xAZ57G7pL8FS0C4e/81fr3FjkpUpibf1s2v5XGyGhUVGF4Jfg7yEcxqn2iUw1w==", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.8.0.tgz", + "integrity": "sha512-4q8rKdLp35z8msAtrhr0pbos7BeD8T0tr6rMbBINewp9cfrwj7ROIElVwBluU8fZ596OvwQcjb6QCyBzTmkMRQ==", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "^4.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "compressible": "^2.0.12", + "duplexify": "^4.1.3", + "ent": "^2.2.0", + "fast-xml-parser": "^4.3.0", + "gaxios": "^6.0.2", + "google-auth-library": "^9.6.3", + "mime": "^3.0.0", + "mime-types": "^2.0.8", + "p-limit": "^3.0.1", + "retry-request": "^7.0.0", + "teeny-request": "^9.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.2.tgz", + "integrity": "sha512-lSbgu8iayAod8O0YcoXK3+bMFGThY2svtN35Zlm9VepsB3jfyIcoupKknEht7Kh9Q8ITjsp0J4KpYo9l4+FhNg==", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.10", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz", + "integrity": "sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.4", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "optional": true + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "optional": true + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "optional": true + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "optional": true + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "optional": true + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "optional": true + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "optional": true + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "optional": true + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "optional": true + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "optional": true + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.43", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz", + "integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz", + "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "optional": true + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/node": { + "version": "20.11.26", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.26.tgz", + "integrity": "sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/qs": { + "version": "6.9.12", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.12.tgz", + "integrity": "sha512-bZcOkJ6uWrL0Qb2NAWKa7TBU+mJHPzhx9jjLL1KHF+XpzEcR7EXHvjbHlGtR/IsP1vyPrehuS6XqkmaePy//mg==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + }, + "node_modules/@types/request": { + "version": "2.48.12", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", + "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "optional": true, + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "optional": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "optional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "optional": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "optional": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "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.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "optional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "optional": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "optional": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "optional": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "optional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "optional": true + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "optional": true + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.18.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.3.tgz", + "integrity": "sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "optional": true + }, + "node_modules/fast-xml-parser": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.5.tgz", + "integrity": "sha512-sWvP1Pl8H03B8oFJpFR3HE31HUfwtX7Rlf9BNsvdpujD4n7WMhfmu8h9wOV2u+c1k0ZilTADhPqypzx2J690ZQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "optional": true, + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-admin": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-12.0.0.tgz", + "integrity": "sha512-wBrrSSsKV++/+O8E7O/C7/wL0nbG/x4Xv4yatz/+sohaZ+LsnWtYUcrd3gZutO86hLpDex7xgyrkKbgulmtVyQ==", + "dependencies": { + "@fastify/busboy": "^1.2.1", + "@firebase/database-compat": "^1.0.2", + "@firebase/database-types": "^1.0.0", + "@types/node": "^20.10.3", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.0.1", + "node-forge": "^1.3.1", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^7.1.0", + "@google-cloud/storage": "^7.7.0" + } + }, + "node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "optional": true + }, + "node_modules/gaxios": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.3.0.tgz", + "integrity": "sha512-p+ggrQw3fBwH2F5N/PAI4k/G/y1art5OxKpb2J2chwNNHM4hHuAOtivjPuirMF4KNKwTTUal/lPfL2+7h2mEcg==", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "optional": true, + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "optional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/google-auth-library": { + "version": "9.6.3", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.6.3.tgz", + "integrity": "sha512-4CacM29MLC2eT9Cey5GDVK4Q8t+MMp8+OEdOaqD9MG6b0dOyLORaaeJMPQ7EESVgm/+z5EKYyFLxgzBJlJgyHQ==", + "optional": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.3.1.tgz", + "integrity": "sha512-qpSfslpwqToIgQ+Tf3MjWIDjYK4UFIZ0uz6nLtttlW9N1NQA4PhGf9tlGo6KDYJ4rgL2w4CjXVd0z5yeNpN/Iw==", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "~1.10.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.3.0", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.0", + "protobufjs": "7.2.6", + "retry-request": "^7.0.0", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "optional": true, + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "optional": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jose": { + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", + "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "optional": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "optional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz", + "integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==", + "dependencies": { + "@types/express": "^4.17.17", + "@types/jsonwebtoken": "^9.0.2", + "debug": "^4.3.4", + "jose": "^4.14.6", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jwks-rsa/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jwks-rsa/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "optional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "optional": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "optional": true + }, + "node_modules/lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==", + "dependencies": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "node_modules/lru-memoizer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.2.0.tgz", + "integrity": "sha512-QfOZ6jNkxCcM/BkIPnFsqDhtrazLRsghi9mBwFAzol5GCvj4EkFT899Za3+QwikCg5sRX8JstioBDwOxEyzaNw==", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz", + "integrity": "sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "optional": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proto3-json-serializer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.1.tgz", + "integrity": "sha512-8awBvjO+FwkMd6gNoGFZyqkHZXCFd54CIYTb6De7dPaufGJ2XNW+QUNqbMr8MaAocMdb+KpsD4rxEOaTBDCffA==", + "optional": true, + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", + "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", + "hasInstallScript": true, + "optional": true, + "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": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "optional": true, + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "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" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.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" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "optional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "optional": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "optional": true + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "optional": true + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "optional": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/text-decoding": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz", + "integrity": "sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "optional": true + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "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 + } + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "optional": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "optional": true + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "optional": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "optional": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + } + }, + "@fastify/busboy": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.2.1.tgz", + "integrity": "sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==", + "requires": { + "text-decoding": "^1.0.0" + } + }, + "@firebase/app-check-interop-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.0.tgz", + "integrity": "sha512-xAxHPZPIgFXnI+vb4sbBjZcde7ZluzPPaSK7Lx3/nmuVk4TjZvnL8ONnkd4ERQKL8WePQySU+pRcWkh8rDf5Sg==" + }, + "@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==" + }, + "@firebase/auth-interop-types": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz", + "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==" + }, + "@firebase/component": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.5.tgz", + "integrity": "sha512-2tVDk1ixi12sbDmmfITK8lxSjmcb73BMF6Qwc3U44hN/J1Fi1QY/Hnnb6klFlbB9/G16a3J3d4nXykye2EADTw==", + "requires": { + "@firebase/util": "1.9.4", + "tslib": "^2.1.0" + } + }, + "@firebase/database": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.3.tgz", + "integrity": "sha512-9fjqLt9JzL46gw9+NRqsgQEMjgRwfd8XtzcKqG+UYyhVeFCdVRQ0Wp6Dw/dvYHnbH5vNEKzNv36dcB4p+PIAAA==", + "requires": { + "@firebase/app-check-interop-types": "0.3.0", + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.5", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.4", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "@firebase/database-compat": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.3.tgz", + "integrity": "sha512-7tHEOcMbK5jJzHWyphPux4osogH/adWwncxdMxdBpB9g1DNIyY4dcz1oJdlkXGM/i/AjUBesZsd5CuwTRTBNTw==", + "requires": { + "@firebase/component": "0.6.5", + "@firebase/database": "1.0.3", + "@firebase/database-types": "1.0.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.4", + "tslib": "^2.1.0" + } + }, + "@firebase/database-types": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.1.tgz", + "integrity": "sha512-Tmcmx5XgiI7UVF/4oGg2P3AOTfq3WKEPsm2yf+uXtN7uG/a4WTWhVMrXGYRY2ZUL1xPxv9V33wQRJ+CcrUhVXw==", + "requires": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.4" + } + }, + "@firebase/logger": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", + "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.4.tgz", + "integrity": "sha512-WLonYmS1FGHT97TsUmRN3qnTh5TeeoJp1Gg5fithzuAgdZOUtsYECfy7/noQ3llaguios8r5BuXSEiK82+UrxQ==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@google-cloud/firestore": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.3.1.tgz", + "integrity": "sha512-YluLZbJK3dHXq6Ns5URCtr6hjBiG+6EM17QSivjaozPYDsv1R9a9mkWPz+jCQrb6Ewz6mxp3zavu6DXxvmSWLA==", + "optional": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^4.0.4", + "protobufjs": "^7.2.5" + } + }, + "@google-cloud/paginator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.0.tgz", + "integrity": "sha512-87aeg6QQcEPxGCOthnpUjvw4xAZ57G7pL8FS0C4e/81fr3FjkpUpibf1s2v5XGyGhUVGF4Jfg7yEcxqn2iUw1w==", + "optional": true, + "requires": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + } + }, + "@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "optional": true + }, + "@google-cloud/promisify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "optional": true + }, + "@google-cloud/storage": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.8.0.tgz", + "integrity": "sha512-4q8rKdLp35z8msAtrhr0pbos7BeD8T0tr6rMbBINewp9cfrwj7ROIElVwBluU8fZ596OvwQcjb6QCyBzTmkMRQ==", + "optional": true, + "requires": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "^4.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "compressible": "^2.0.12", + "duplexify": "^4.1.3", + "ent": "^2.2.0", + "fast-xml-parser": "^4.3.0", + "gaxios": "^6.0.2", + "google-auth-library": "^9.6.3", + "mime": "^3.0.0", + "mime-types": "^2.0.8", + "p-limit": "^3.0.1", + "retry-request": "^7.0.0", + "teeny-request": "^9.0.0", + "uuid": "^8.0.0" + }, + "dependencies": { + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "optional": true + } + } + }, + "@grpc/grpc-js": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.2.tgz", + "integrity": "sha512-lSbgu8iayAod8O0YcoXK3+bMFGThY2svtN35Zlm9VepsB3jfyIcoupKknEht7Kh9Q8ITjsp0J4KpYo9l4+FhNg==", + "optional": true, + "requires": { + "@grpc/proto-loader": "^0.7.10", + "@js-sdsl/ordered-map": "^4.4.2" + } + }, + "@grpc/proto-loader": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz", + "integrity": "sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==", + "optional": true, + "requires": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.4", + "yargs": "^17.7.2" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "optional": true + }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "optional": true + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "optional": true + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "optional": true + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "optional": true + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "optional": true, + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "optional": true + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "optional": true + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "optional": true + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "optional": true + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "optional": true + }, + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "optional": true + }, + "@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==" + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + }, + "@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + }, + "@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "optional": true + }, + "@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "requires": { + "@types/node": "*" + } + }, + "@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.43", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz", + "integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, + "@types/jsonwebtoken": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz", + "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==", + "requires": { + "@types/node": "*" + } + }, + "@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "optional": true + }, + "@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "@types/node": { + "version": "20.11.26", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.26.tgz", + "integrity": "sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==", + "requires": { + "undici-types": "~5.26.4" + } + }, + "@types/qs": { + "version": "6.9.12", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.12.tgz", + "integrity": "sha512-bZcOkJ6uWrL0Qb2NAWKa7TBU+mJHPzhx9jjLL1KHF+XpzEcR7EXHvjbHlGtR/IsP1vyPrehuS6XqkmaePy//mg==" + }, + "@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + }, + "@types/request": { + "version": "2.48.12", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", + "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "optional": true, + "requires": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "@types/serve-static": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "requires": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, + "@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "optional": true + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "optional": true, + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==" + }, + "acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==" + }, + "agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "optional": true, + "requires": { + "debug": "^4.3.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + } + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "optional": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "optional": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "optional": true + }, + "async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "optional": true, + "requires": { + "retry": "0.13.1" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "optional": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "optional": true + }, + "bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "optional": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "requires": { + "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.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "requires": { + "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.1" + } + }, + "chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "optional": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "optional": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "optional": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "optional": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "optional": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "optional": true + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" + }, + "dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==" + }, + "duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "optional": true, + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "optional": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "optional": true, + "requires": { + "once": "^1.4.0" + } + }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "optional": true + }, + "es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "requires": { + "get-intrinsic": "^1.2.4" + } + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "optional": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "optional": true + }, + "express": { + "version": "4.18.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.3.tgz", + "integrity": "sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "optional": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "optional": true + }, + "fast-xml-parser": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.5.tgz", + "integrity": "sha512-sWvP1Pl8H03B8oFJpFR3HE31HUfwtX7Rlf9BNsvdpujD4n7WMhfmu8h9wOV2u+c1k0ZilTADhPqypzx2J690ZQ==", + "optional": true, + "requires": { + "strnum": "^1.0.5" + } + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + } + }, + "firebase-admin": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-12.0.0.tgz", + "integrity": "sha512-wBrrSSsKV++/+O8E7O/C7/wL0nbG/x4Xv4yatz/+sohaZ+LsnWtYUcrd3gZutO86hLpDex7xgyrkKbgulmtVyQ==", + "requires": { + "@fastify/busboy": "^1.2.1", + "@firebase/database-compat": "^1.0.2", + "@firebase/database-types": "^1.0.0", + "@google-cloud/firestore": "^7.1.0", + "@google-cloud/storage": "^7.7.0", + "@types/node": "^20.10.3", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.0.1", + "node-forge": "^1.3.1", + "uuid": "^9.0.0" + } + }, + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "optional": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "optional": true + }, + "gaxios": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.3.0.tgz", + "integrity": "sha512-p+ggrQw3fBwH2F5N/PAI4k/G/y1art5OxKpb2J2chwNNHM4hHuAOtivjPuirMF4KNKwTTUal/lPfL2+7h2mEcg==", + "optional": true, + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + } + }, + "gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "optional": true, + "requires": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "optional": true + }, + "get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "google-auth-library": { + "version": "9.6.3", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.6.3.tgz", + "integrity": "sha512-4CacM29MLC2eT9Cey5GDVK4Q8t+MMp8+OEdOaqD9MG6b0dOyLORaaeJMPQ7EESVgm/+z5EKYyFLxgzBJlJgyHQ==", + "optional": true, + "requires": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + } + }, + "google-gax": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.3.1.tgz", + "integrity": "sha512-qpSfslpwqToIgQ+Tf3MjWIDjYK4UFIZ0uz6nLtttlW9N1NQA4PhGf9tlGo6KDYJ4rgL2w4CjXVd0z5yeNpN/Iw==", + "optional": true, + "requires": { + "@grpc/grpc-js": "~1.10.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.3.0", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.0", + "protobufjs": "7.2.6", + "retry-request": "^7.0.0", + "uuid": "^9.0.1" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "optional": true, + "requires": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "optional": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "requires": { + "debug": "4" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + } + } + }, + "https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "optional": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + } + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "optional": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "optional": true + }, + "jose": { + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", + "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==" + }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "optional": true, + "requires": { + "bignumber.js": "^9.0.0" + } + }, + "jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "dependencies": { + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "optional": true, + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jwks-rsa": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz", + "integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==", + "requires": { + "@types/express": "^4.17.17", + "@types/jsonwebtoken": "^9.0.2", + "debug": "^4.3.4", + "jose": "^4.14.6", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "optional": true, + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "optional": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "optional": true + }, + "lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==", + "requires": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "lru-memoizer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.2.0.tgz", + "integrity": "sha512-QfOZ6jNkxCcM/BkIPnFsqDhtrazLRsghi9mBwFAzol5GCvj4EkFT899Za3+QwikCg5sRX8JstioBDwOxEyzaNw==", + "requires": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "optional": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" + }, + "nodemon": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz", + "integrity": "sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==", + "dev": true, + "requires": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "optional": true + }, + "object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "optional": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "proto3-json-serializer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.1.tgz", + "integrity": "sha512-8awBvjO+FwkMd6gNoGFZyqkHZXCFd54CIYTb6De7dPaufGJ2XNW+QUNqbMr8MaAocMdb+KpsD4rxEOaTBDCffA==", + "optional": true, + "requires": { + "protobufjs": "^7.2.5" + } + }, + "protobufjs": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", + "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", + "optional": true, + "requires": { + "@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": ">=13.7.0", + "long": "^5.0.0" + } + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "optional": true + }, + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "optional": true + }, + "retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "optional": true, + "requires": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "requires": { + "lru-cache": "^6.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "requires": { + "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" + }, + "dependencies": { + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "requires": { + "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": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "requires": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + } + }, + "simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "optional": true, + "requires": { + "stubs": "^3.0.0" + } + }, + "stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "optional": true + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "optional": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "optional": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "optional": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "optional": true + }, + "stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "optional": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "optional": true, + "requires": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "dependencies": { + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "requires": { + "debug": "4" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "optional": true, + "requires": { + "ms": "2.1.2" + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + } + } + }, + "text-decoding": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz", + "integrity": "sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "~1.0.10" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "optional": true + }, + "ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + } + }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typescript": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==" + }, + "undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "optional": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + }, + "uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" + }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "optional": true + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "optional": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "optional": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "optional": true + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "optional": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "optional": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "optional": true + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "optional": true + } + } +} diff --git a/FogNode/auth/package-lock.json.license b/FogNode/auth/package-lock.json.license new file mode 100644 index 0000000..6917c4b --- /dev/null +++ b/FogNode/auth/package-lock.json.license @@ -0,0 +1,5 @@ +This source file is part of the Stanford Spezi open-source project + +SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) + +SPDX-License-Identifier: MIT diff --git a/FogNode/auth/package.json b/FogNode/auth/package.json new file mode 100644 index 0000000..e5e413a --- /dev/null +++ b/FogNode/auth/package.json @@ -0,0 +1,26 @@ +{ + "name": "auth-service", + "version": "1.0.0", + "description": "Provides authentication services based on the SpeziLLMFog Firebase Admin SDK", + "main": "index.js", + "scripts": { + "start": "node dist/index.js", + "build": "tsc", + "dev": "nodemon src/index.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "dotenv": "^16.4.5", + "express": "^4.18.3", + "firebase-admin": "^12.0.0", + "@types/express": "^4.17.21", + "@types/node": "^20.11.26", + "ts-node": "^10.9.2", + "typescript": "^5.4.2" + }, + "devDependencies": { + "nodemon": "^3.1.0" + } +} \ No newline at end of file diff --git a/FogNode/auth/package.json.license b/FogNode/auth/package.json.license new file mode 100644 index 0000000..6917c4b --- /dev/null +++ b/FogNode/auth/package.json.license @@ -0,0 +1,5 @@ +This source file is part of the Stanford Spezi open-source project + +SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) + +SPDX-License-Identifier: MIT diff --git a/FogNode/auth/src/authToken.ts b/FogNode/auth/src/authToken.ts new file mode 100644 index 0000000..b26870a --- /dev/null +++ b/FogNode/auth/src/authToken.ts @@ -0,0 +1,34 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import { Request, Response } from 'express'; + +// Extract bearer token from the request +export function getTokenFromRequest(req: Request, res: Response): string | null { + const authHeader = req.headers.authorization; + if (!authHeader) { + console.log('SpeziLLMFog: Unauthorized - Authorization header is missing'); + res.status(401).send('SpeziLLMFog: Unauthorized - Authorization header is missing'); + return null; + } + + if (!authHeader.startsWith('Bearer ')) { + console.log('SpeziLLMFog: Unauthorized - Authorization header is not a Bearer token'); + res.status(401).send('SpeziLLMFog: Unauthorized - Authorization header is not a Bearer token'); + return null; + } + + const token = authHeader.substring(7); // "Bearer " is 7 characters long + if (!token) { + console.log('SpeziLLMFog: Unauthorized - Token is missing in Authorization header'); + res.status(401).send('SpeziLLMFog: Unauthorized - Token is missing in Authorization header'); + return null; + } + + return token; +} \ No newline at end of file diff --git a/FogNode/auth/src/firebase.ts b/FogNode/auth/src/firebase.ts new file mode 100644 index 0000000..7965917 --- /dev/null +++ b/FogNode/auth/src/firebase.ts @@ -0,0 +1,36 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import admin from 'firebase-admin'; + +type EnvVar = string | undefined; + +// Initialize Firebase Admin based on environment +export const initializeFirebase = (): void => { + const useFirebaseEmulator: EnvVar = process.env.USE_FIREBASE_EMULATOR; + const firebaseAuthEmulatorHost: EnvVar = process.env.FIREBASE_AUTH_EMULATOR_HOST; + const firebaseProjectId: EnvVar = process.env.FIREBASE_PROJECT_ID; + + if (useFirebaseEmulator) { + if (!firebaseAuthEmulatorHost || !firebaseProjectId) { + throw new Error(`Environment variables FIREBASE_AUTH_EMULATOR_HOST and FIREBASE_PROJECT_ID are not properly set.`); + } + + process.env["FIREBASE_AUTH_EMULATOR_HOST"] = firebaseAuthEmulatorHost; + + admin.initializeApp({ + projectId: firebaseProjectId, + }); + } else { + const serviceAccount = require("../serviceAccountKey.json"); + + admin.initializeApp({ + credential: admin.credential.cert(serviceAccount), + }); + } +}; \ No newline at end of file diff --git a/FogNode/auth/src/index.ts b/FogNode/auth/src/index.ts new file mode 100644 index 0000000..9a5eff9 --- /dev/null +++ b/FogNode/auth/src/index.ts @@ -0,0 +1,50 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import express, { Request, Response } from 'express'; +import admin from 'firebase-admin'; +import * as dotenv from 'dotenv'; +import { initializeFirebase } from './firebase'; +import { getTokenFromRequest } from './authToken'; + +dotenv.config(); + +// Initialize Firebase Admin SDK +initializeFirebase(); + +// Create a new express.js application +const app = express(); +const port: number = parseInt(process.env.PORT || '3000', 10); + +// Serve authorization on all routes +app.all('*', async (req: Request, res: Response) => { + const token = getTokenFromRequest(req, res); + + if (!token) { + return; + } + + try { + // Verify the received bearer token via firebase admin SDK + const decodedToken = await admin.auth().verifyIdToken(token); + + // Possibly add additional checks, e.g. verify if user is allowed to access the fog LLM via token claims + // ... + + console.log('SpeziLLMFog: Authorized - Valid token'); + return res.status(200).send('SpeziLLMFog: Authorized - Valid token'); + } catch (error) { + console.log('SpeziLLMFog: Unauthorized - Invalid user ID token ', error); + return res.status(403).send(`SpeziLLMFog: Unauthorized - Invalid Firebase user ID token: ${error}`); + } +}); + +// Start the server on port 3000 or the configured port +app.listen(port, () => { + console.log(`SpeziLLMFog: Auth service listening at port ${port}`); +}); \ No newline at end of file diff --git a/FogNode/auth/tsconfig.json b/FogNode/auth/tsconfig.json new file mode 100644 index 0000000..8e1d150 --- /dev/null +++ b/FogNode/auth/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/FogNode/auth/tsconfig.json.license b/FogNode/auth/tsconfig.json.license new file mode 100644 index 0000000..6917c4b --- /dev/null +++ b/FogNode/auth/tsconfig.json.license @@ -0,0 +1,5 @@ +This source file is part of the Stanford Spezi open-source project + +SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) + +SPDX-License-Identifier: MIT diff --git a/FogNode/avahi/Dockerfile b/FogNode/avahi/Dockerfile new file mode 100644 index 0000000..d4010f8 --- /dev/null +++ b/FogNode/avahi/Dockerfile @@ -0,0 +1,29 @@ +# +# This source file is part of the Stanford Spezi open source project +# +# SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT +# + +FROM alpine:3.19 + +LABEL org.opencontainers.image.authors="Philipp Zagar " \ + org.opencontainers.image.version="0.1" \ + org.opencontainers.image.title="stanfordspezi/avahi" \ + org.opencontainers.image.description="Avahi advertising services via mDNS" \ + org.opencontainers.image.url="https://ghcr.io/stanfordspezi/avahi" \ + org.opencontainers.image.source="https://github.com/StanfordSpezi/SpeziLLM" + +# Install Avahi daemon without dbus dependency +RUN apk --no-cache --no-progress add avahi + +# Setup services +RUN rm -rf /etc/avahi/services +COPY services/ /etc/avahi/services + +# Disable D-Bus in Avahi's configuration +RUN sed -i 's/.*enable-dbus=.*/enable-dbus=no/' /etc/avahi/avahi-daemon.conf + +# Run Avahi daemon with non-root user, avoid daemonizing to keep container running +CMD ["avahi-daemon", "--no-chroot"] \ No newline at end of file diff --git a/FogNode/avahi/Dockerfile-Sidecar b/FogNode/avahi/Dockerfile-Sidecar new file mode 100644 index 0000000..4893c9a --- /dev/null +++ b/FogNode/avahi/Dockerfile-Sidecar @@ -0,0 +1,33 @@ +# +# This source file is part of the Stanford Spezi open source project +# +# SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT +# + +FROM alpine:3.19 + +LABEL org.opencontainers.image.authors="Philipp Zagar " \ + org.opencontainers.image.version="0.1" \ + org.opencontainers.image.title="stanfordspezi/avahi-sidecar" \ + org.opencontainers.image.description="Avahi sidecar discovering mDNS services" \ + org.opencontainers.image.url="https://ghcr.io/stanfordspezi/avahi-sidecar" \ + org.opencontainers.image.source="https://github.com/StanfordSpezi/SpeziLLM" + +# Install Avahi daemon +RUN apk --no-cache --no-progress add avahi avahi-tools dbus + +# Setup services +RUN rm -rf /etc/avahi/services +COPY services/ /etc/avahi/services + +# Run Avahi daemon with non-root user, avoid daemonizing to keep container running +COPY docker-entrypoint-sidecar.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +# Create the D-Bus system bus socket directory +RUN mkdir -p /var/run/dbus && \ + chown messagebus:messagebus /var/run/dbus + +ENTRYPOINT ["docker-entrypoint.sh"] \ No newline at end of file diff --git a/FogNode/avahi/docker-entrypoint-sidecar.sh b/FogNode/avahi/docker-entrypoint-sidecar.sh new file mode 100644 index 0000000..5033c57 --- /dev/null +++ b/FogNode/avahi/docker-entrypoint-sidecar.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# +# This source file is part of the Stanford Spezi open source project +# +# SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT +# + +# Remove the D-Bus PID file if it exists to avoid startup error +rm -f /run/dbus/dbus.pid + +# Start dbus-daemon +dbus-daemon --system --nofork & + +# Wait a moment to ensure D-Bus is fully up +sleep 1 + +# Start avahi-daemon +avahi-daemon --no-chroot --debug \ No newline at end of file diff --git a/FogNode/avahi/services/spezillmfog.service b/FogNode/avahi/services/spezillmfog.service new file mode 100644 index 0000000..28b95cd --- /dev/null +++ b/FogNode/avahi/services/spezillmfog.service @@ -0,0 +1,20 @@ + + + + + + SpeziLLMFog-Service + + _https._tcp + 443 + + + diff --git a/FogNode/certs/ca/.gitkeep b/FogNode/certs/ca/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/FogNode/certs/openssl.cnf b/FogNode/certs/openssl.cnf new file mode 100644 index 0000000..8eaebe8 --- /dev/null +++ b/FogNode/certs/openssl.cnf @@ -0,0 +1,27 @@ +[req] +default_bits = 2048 +prompt = no +default_md = sha256 +distinguished_name = dn +req_extensions = req_ext +x509_extensions = v3_ca + +[dn] +C = US +ST = California +L = San Francisco +O = Stanford +OU = StanfordSpezi +CN = spezillmfog.local + +[req_ext] +subjectAltName = @alt_names +extendedKeyUsage = serverAuth, clientAuth + +[ v3_ca ] +subjectAltName = @alt_names +extendedKeyUsage = serverAuth, clientAuth + +[alt_names] +DNS.1 = spezillmfog.local + diff --git a/FogNode/certs/openssl.cnf.license b/FogNode/certs/openssl.cnf.license new file mode 100644 index 0000000..6917c4b --- /dev/null +++ b/FogNode/certs/openssl.cnf.license @@ -0,0 +1,5 @@ +This source file is part of the Stanford Spezi open-source project + +SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) + +SPDX-License-Identifier: MIT diff --git a/FogNode/certs/webservice/.gitkeep b/FogNode/certs/webservice/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/FogNode/docker-compose.avahi.yml b/FogNode/docker-compose.avahi.yml new file mode 100644 index 0000000..1100ad3 --- /dev/null +++ b/FogNode/docker-compose.avahi.yml @@ -0,0 +1,31 @@ +# +# This source file is part of the Stanford Spezi open source project +# +# SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT +# + +version: '3.8' +services: + # Advertises dummy mDNS service to sidecar container + avahi: + build: + context: avahi + hostname: spezillmfog.local + networks: + - avahi + + # Receives advertised dummy mDNS service from avahi container + avahi-sidecar: + build: + context: avahi + dockerfile: Dockerfile-Sidecar + hostname: spezillmfog-sidecar.local + networks: + - avahi + +# Enables to bridge mDNS advertise packages between the two avahi containers +networks: + avahi: + driver: bridge \ No newline at end of file diff --git a/FogNode/docker-compose.dev.yml b/FogNode/docker-compose.dev.yml new file mode 100644 index 0000000..c36011d --- /dev/null +++ b/FogNode/docker-compose.dev.yml @@ -0,0 +1,93 @@ +# +# This source file is part of the Stanford Spezi open source project +# +# SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT +# + +version: '3.8' +services: + # Reverse proxy routing requests to the Ollama service + traefik: + image: traefik:v2.5 + restart: unless-stopped + command: + - "--api.insecure=true" # Enables the dashboard and API insecurely + - "--log.level=DEBUG" # Adjust the log level as needed + - "--accesslog=true" # Enables access logs + - "--providers.docker=true" + - "--providers.docker.exposedByDefault=false" + - "--entrypoints.web.address=:80" + ports: + - "80:80" + - "8080:8080" # Expose port 8080 for the dashboard + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" + networks: + - web + depends_on: + - ollama + - auth-service + + # LLM inference service Ollama + ollama: + image: ollama/ollama + labels: + - "traefik.enable=true" + - "traefik.http.routers.ollama.rule=Host(`spezillmfog.local`)" + - "traefik.http.routers.ollama.entrypoints=web" + - "traefik.http.routers.ollama.service=ollama-service" + - "traefik.http.services.ollama-service.loadbalancer.server.port=11434" + - "traefik.http.routers.ollama.middlewares=auth@docker" + - "traefik.http.middlewares.auth.forwardauth.address=http://auth-service:3000/" # Authorizes incoming LLM inference jobs via Firebase Emulator + - "traefik.http.middlewares.auth.forwardauth.trustForwardHeader=true" # Forwards all headers to authorization service + ports: + - "11434:11434" + volumes: + - ollama_storage:/root/.ollama + networks: + - web + + # Authorizes incoming LLM inference requests + auth-service: + build: + context: auth + hostname: auth-service + restart: unless-stopped + environment: + - PORT=3000 + # Use the Firebase emulator + - USE_FIREBASE_EMULATOR=true + - FIREBASE_AUTH_EMULATOR_HOST=firebase-emulator:9099 + - FIREBASE_PROJECT_ID=spezillmfog + labels: + - "traefik.enable=false" + ports: + - "3000:3000" + networks: + - web + depends_on: + - firebase-emulator + + # Firebase emulator that authenticates the incoming LLM requests + firebase-emulator: + build: + context: auth/firebaseEmulator + hostname: firebase-emulator + restart: unless-stopped + labels: + - "traefik.enable=false" + ports: + - "4000:4000" # Expose web UI + - "9099:9099" # Expose auth emulator service + networks: + - web + +# Enables persistence of downloaded LLMs by Ollama +volumes: + ollama_storage: + +networks: + web: + driver: bridge diff --git a/FogNode/docker-compose.yml b/FogNode/docker-compose.yml new file mode 100644 index 0000000..fa0f750 --- /dev/null +++ b/FogNode/docker-compose.yml @@ -0,0 +1,83 @@ +# +# This source file is part of the Stanford Spezi open source project +# +# SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT +# + +version: '3.8' +services: + # Reverse proxy authenticating requests and routing them to the Ollama service + traefik: + image: traefik:v2.5 + restart: unless-stopped + command: + - "--providers.docker=true" + - "--providers.docker.exposedByDefault=false" + - "--providers.file.filename=/etc/traefik/certs/dynamic_conf.yml" # Configures TLS certs + - "--entrypoints.websecure.address=:443" + ports: + - "443:443" + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" + - "./certs/webservice:/etc/traefik/certs" # Mount TLS certs into container + - "./traefik/dynamic_conf.yml:/etc/traefik/certs/dynamic_conf.yml" # Mount TLS config into container + networks: + - web + depends_on: + - ollama + - auth-service + - avahi + + # LLM inference service Ollama + ollama: + image: ollama/ollama + restart: unless-stopped + labels: + - "traefik.enable=true" + - "traefik.http.routers.ollama.rule=Host(`spezillmfog.local`)" + - "traefik.http.routers.ollama.entrypoints=websecure" + - "traefik.http.routers.ollama.tls=true" + - "traefik.http.routers.ollama.service=ollama-service" + - "traefik.http.services.ollama-service.loadbalancer.server.port=11434" + - "traefik.http.routers.ollama.middlewares=auth@docker" + - "traefik.http.middlewares.auth.forwardauth.address=http://auth-service:3000/" # Authorizes incoming LLM inference jobs via Firebase + - "traefik.http.middlewares.auth.forwardauth.trustForwardHeader=true" # Forwards all headers to authorization service + volumes: + - ollama_storage:/root/.ollama + networks: + - web + + # Authorizes incoming LLM inference requests + auth-service: + build: + context: auth + hostname: auth-service + restart: unless-stopped + environment: + - PORT=3000 + labels: + - "traefik.enable=false" + volumes: + - ./auth/serviceAccountKey.json:/usr/src/app/serviceAccountKey.json # Adjust the host mount location as needed + networks: + - web + + # On the Linux platform, advertise LLM inference service via mDNS from Avahi + avahi: + build: + context: avahi + hostname: spezillmfog.local + network_mode: host # Need to run in host network mode for mDNS + profiles: + - linux + restart: unless-stopped + +# Enables persistence of downloaded LLMs by Ollama +volumes: + ollama_storage: + +networks: + web: + driver: bridge diff --git a/FogNode/setup.sh b/FogNode/setup.sh new file mode 100755 index 0000000..c566ec0 --- /dev/null +++ b/FogNode/setup.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# +# This source file is part of the Stanford Spezi open source project +# +# SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT +# + +cd certs + +# Issue the custom root CA certificate with a passphrase for the private key +openssl req -new -x509 -days 3650 -keyout ca/ca.key -out ca/ca.crt -subj "/CN=SpeziLLMFog CA" -passout pass:SpeziLLMFogPassword + +# Create the web service key +openssl genrsa -out webservice/spezillmfog.local.key 2048 + +# Generate a signing request for the web service key +openssl req -new -key webservice/spezillmfog.local.key -out webservice/spezillmfog.local.csr -config openssl.cnf + +# Sign the web service key with the CA certificate, using the CA's passphrase to access the private +openssl x509 -req -in webservice/spezillmfog.local.csr -CA ca/ca.crt -CAkey ca/ca.key -CAcreateserial -out webservice/spezillmfog.local.crt -days 365 -sha256 -extfile openssl.cnf -extensions v3_ca -passin pass:SpeziLLMFogPassword + +RED=$(tput setaf 1) +GREEN=$(tput setaf 2) +RESET=$(tput sgr0) + +echo "" +echo "${GREEN}Success: The root CA certificate as well as the webservice certificate were sucessfully issued.${RESET}" +echo "${RED}Warning: Issue the Firebase Admin Service Account key via the Firebase Console and place it within the 'auth' directory under the name 'serviceAccountKey.json', if not using the Firebase Emulator.${RESET}" diff --git a/FogNode/traefik/dynamic_conf.yml b/FogNode/traefik/dynamic_conf.yml new file mode 100644 index 0000000..3cfb913 --- /dev/null +++ b/FogNode/traefik/dynamic_conf.yml @@ -0,0 +1,17 @@ +# +# This source file is part of the Stanford Spezi open source project +# +# SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +# +# SPDX-License-Identifier: MIT +# + +tls: + certificates: + - certFile: /etc/traefik/certs/spezillmfog.local.crt + keyFile: /etc/traefik/certs/spezillmfog.local.key + stores: + default: + defaultCertificate: + certFile: /etc/traefik/certs/spezillmfog.local.crt + keyFile: /etc/traefik/certs/spezillmfog.local.key diff --git a/Package.swift b/Package.swift index 23b167d..30272c2 100644 --- a/Package.swift +++ b/Package.swift @@ -23,16 +23,17 @@ let package = Package( .library(name: "SpeziLLM", targets: ["SpeziLLM"]), .library(name: "SpeziLLMLocal", targets: ["SpeziLLMLocal"]), .library(name: "SpeziLLMLocalDownload", targets: ["SpeziLLMLocalDownload"]), - .library(name: "SpeziLLMOpenAI", targets: ["SpeziLLMOpenAI"]) + .library(name: "SpeziLLMOpenAI", targets: ["SpeziLLMOpenAI"]), + .library(name: "SpeziLLMFog", targets: ["SpeziLLMFog"]) ], dependencies: [ - .package(url: "https://github.com/MacPaw/OpenAI", .upToNextMinor(from: "0.2.6")), + .package(url: "https://github.com/StanfordBDHG/OpenAI", .upToNextMinor(from: "0.2.8")), .package(url: "https://github.com/StanfordBDHG/llama.cpp", .upToNextMinor(from: "0.2.1")), .package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.2.1"), .package(url: "https://github.com/StanfordSpezi/SpeziFoundation", from: "1.0.4"), .package(url: "https://github.com/StanfordSpezi/SpeziStorage", from: "1.0.2"), .package(url: "https://github.com/StanfordSpezi/SpeziOnboarding", from: "1.1.1"), - .package(url: "https://github.com/StanfordSpezi/SpeziChat", .upToNextMinor(from: "0.1.9")), + .package(url: "https://github.com/StanfordSpezi/SpeziChat", .upToNextMinor(from: "0.2.0")), .package(url: "https://github.com/StanfordSpezi/SpeziViews", from: "1.3.1") ], targets: [ @@ -75,6 +76,14 @@ let package = Package( .product(name: "SpeziOnboarding", package: "SpeziOnboarding") ] ), + .target( + name: "SpeziLLMFog", + dependencies: [ + .target(name: "SpeziLLM"), + .product(name: "Spezi", package: "Spezi"), + .product(name: "OpenAI", package: "OpenAI") + ] + ), .testTarget( name: "SpeziLLMTests", dependencies: [ diff --git a/README.md b/README.md index f902f59..50f620f 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ SPDX-License-Identifier: MIT ## Overview The Spezi LLM Swift Package includes modules that are helpful to integrate LLM-related functionality in your application. -The package provides all necessary tools for local LLM execution as well as the usage of remote OpenAI-based LLMs. +The package provides all necessary tools for local LLM execution, the usage of remote OpenAI-based LLMs, as well as LLMs running on Fog node resources within the local network. |Screenshot displaying the Chat View utilizing the OpenAI API from SpeziLLMOpenAI.|Screenshot displaying the Local LLM Download View from SpeziLLMLocalDownload.|Screenshot displaying the Chat View utilizing a locally executed LLM via SpeziLLMLocal.| |:--:|:--:|:--:| @@ -48,8 +48,9 @@ Spezi LLM provides a number of targets to help developers integrate LLMs in thei - [SpeziLLMLocal](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillmlocal): Local LLM execution capabilities directly on-device. Enables running open-source LLMs like [Meta's Llama2 models](https://ai.meta.com/llama/). - [SpeziLLMLocalDownload](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillmlocaldownload): Download and storage manager of local Language Models, including onboarding views. - [SpeziLLMOpenAI](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillmopenai): Integration with [OpenAIs GPT models](https://openai.com/gpt-4) via using OpenAIs API service. +- [SpeziLLMFog](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillmfog): Discover and dispatch LLM inference jobs to Fog node resources within the local network. -The section below highlights the setup and basic use of the [SpeziLLMLocal](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillmlocal) and [SpeziLLMOpenAI](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillmopenai) targets in order to integrate Language Models in a Spezi-based application. +The section below highlights the setup and basic use of the [SpeziLLMLocal](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillmlocal), [SpeziLLMOpenAI](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillmopenai), and [SpeziLLMFog](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillmfog) targets in order to integrate Language Models in a Spezi-based application. > [!NOTE] > To learn more about the usage of the individual targets, please refer to the [DocC documentation of the package] (https://swiftpackageindex.com/stanfordspezi/spezillm/documentation). @@ -151,8 +152,8 @@ See the [SpeziLLM documentation](https://swiftpackageindex.com/stanfordspezi/spe ```swift class LLMOpenAIAppDelegate: SpeziAppDelegate { override var configuration: Configuration { - Configuration { - LLMRunner { + Configuration { + LLMRunner { LLMOpenAIPlatform() } } @@ -160,6 +161,9 @@ class LLMOpenAIAppDelegate: SpeziAppDelegate { } ``` +> [!IMPORTANT] +> If using `SpeziLLMOpenAI` on macOS, ensure to add the [`Keychain Access Groups` entitlement](https://developer.apple.com/documentation/bundleresources/entitlements/keychain-access-groups) to the enclosing Xcode project via *PROJECT_NAME > Signing&Capabilities > + Capability*. The array of keychain groups can be left empty, only the base entitlement is required. + #### Usage The code example below showcases the interaction with an OpenAI LLM through the the [SpeziLLM](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillm) [`LLMRunner`](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillm/llmrunner), which is injected into the SwiftUI `Environment` via the `Configuration` shown above. @@ -196,6 +200,80 @@ struct LLMOpenAIDemoView: View { > [!NOTE] > To learn more about the usage of SpeziLLMOpenAI, please refer to the [DocC documentation] (https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillmopenai). +### Spezi LLM Fog + +The `SpeziLLMFog` target enables you to use LLMs running on [Fog node](https://en.wikipedia.org/wiki/Fog_computing) computing resources within the local network. The fog nodes advertise their services via [mDNS](https://en.wikipedia.org/wiki/Multicast_DNS), enabling clients to discover all fog nodes serving a specific host within the local network. +`SpeziLLMFog` then dispatches LLM inference jobs dynamically to a random fog node within the local network and streams the response to surface it to the user. + +> [!IMPORTANT] +> `SpeziLLMFog` requires a `SpeziLLMFogNode` within the local network hosted on some computing resource that actually performs the inference requests. `SpeziLLMFog` provides the `SpeziLLMFogNode` Docker-based package that enables an easy setup of these fog nodes. See the `FogNode` directory on the root level of the SPM package as well as the respective `README.md` for more details. + +#### Setup + +In order to use Fog LLMs within the Spezi ecosystem, the [SpeziLLM](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillm) [`LLMRunner`](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillm/llmrunner) needs to be initialized in the Spezi `Configuration` with the `LLMFogPlatform`. Only after, the `LLMRunner` can be used for inference with Fog LLMs. See the [SpeziLLM documentation](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillm) for more details. +The `LLMFogPlatform` needs to be initialized with the custom root CA certificate that was used to sign the fog node web service certificate (see the `FogNode/README.md` documentation for more information). Copy the root CA certificate from the fog node as resource to the application using `SpeziLLMFog` and use it to initialize the `LLMFogPlatform` within the Spezi `Configuration`. + +```swift +class LLMFogAppDelegate: SpeziAppDelegate { + private nonisolated static var caCertificateUrl: URL { + // Return local file URL of root CA certificate in the `.crt` format + } + + override var configuration: Configuration { + Configuration { + LLMRunner { + // Set up the Fog platform with the custom CA certificate + LLMRunner { + LLMFogPlatform(configuration: .init(caCertificate: Self.caCertificateUrl)) + } + } + } + } +} +``` + +#### Usage + +The code example below showcases the interaction with a Fog LLM through the the [SpeziLLM](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillm) [`LLMRunner`](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillm/llmrunner), which is injected into the SwiftUI `Environment` via the `Configuration` shown above. + +The `LLMFogSchema` defines the type and configurations of the to-be-executed `LLMFogSession`. This transformation is done via the [`LLMRunner`](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillm/llmrunner) that uses the `LLMFogPlatform`. The inference via `LLMFogSession/generate()` returns an `AsyncThrowingStream` that yields all generated `String` pieces. +The `LLMFogSession` automatically discovers all available LLM fog nodes within the local network upon setup and the dispatches the LLM inference jobs to the fog computing resource, streaming back the response and surfaces it to the user. + +> [!IMPORTANT] +> The `LLMFogSchema` accepts a closure that returns an authorization token that is passed with every request to the Fog node in the `Bearer` HTTP field via the `LLMFogParameters/init(modelType:systemPrompt:authToken:)`. The token is created via the closure upon every LLM inference request, as the `LLMFogSession` may be long lasting and the token could therefore expire. Ensure that the closure appropriately caches the token in order to prevent unnecessary token refresh roundtrips to external systems. + +```swift +struct LLMFogDemoView: View { + @Environment(LLMRunner.self) var runner + @State var responseText = "" + + var body: some View { + Text(responseText) + .task { + // Instantiate the `LLMFogSchema` to an `LLMFogSession` via the `LLMRunner`. + let llmSession: LLMFogSession = runner( + with: LLMFogSchema( + parameters: .init( + modelType: .llama7B, + systemPrompt: "You're a helpful assistant that answers questions from users.", + authToken: { + // Return authorization token as `String` or `nil` if no token is required by the Fog node. + } + ) + ) + ) + + for try await token in try await llmSession.generate() { + responseText.append(token) + } + } + } +} +``` + +> [!NOTE] +> To learn more about the usage of SpeziLLMFog, please refer to the [DocC documentation] (https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillmfog). + ## Contributing Contributions to this project are welcome. Please make sure to read the [contribution guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md) and the [contributor covenant code of conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) first. diff --git a/Sources/SpeziLLM/Helpers/Chat+Append.swift b/Sources/SpeziLLM/Helpers/Chat+Append.swift deleted file mode 100644 index 62d5d01..0000000 --- a/Sources/SpeziLLM/Helpers/Chat+Append.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// This source file is part of the Stanford Spezi open-source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import SpeziChat - - -extension Chat { - /// Append an `ChatEntity/Role/assistant` output to the `Chat`. - /// - /// Automatically appends to the last `ChatEntity/Role/assistant` message if there is one, otherwise create a new one. - /// If the `overwrite` parameter is `true`, the existing message is overwritten. - /// - /// - Parameters: - /// - output: The `ChatEntity/Role/assistant` output `String` (part) that should be appended. Can contain Markdown-formatted text. - /// - complete: Indicates if the `ChatEntity` is complete after appending to it one last time via the ``append(assistantOutput:complete:overwrite:)`` function. - /// - overwrite: Indicates if the already present content of the assistant message should be overwritten. - @MainActor - public mutating func append(assistantOutput output: String, complete: Bool = false, overwrite: Bool = false) { - guard let lastChatEntity = self.last, - lastChatEntity.role == .assistant else { - self.append(.init(role: .assistant, content: output, complete: complete)) - return - } - - self[self.count - 1] = .init( - role: .assistant, - content: overwrite ? output : (lastChatEntity.content + output), - complete: complete, - id: lastChatEntity.id, - date: lastChatEntity.date - ) - } - - /// Append an `ChatEntity/Role/user` input to the `Chat`. - /// - /// - Parameters: - /// - input: The `ChatEntity/Role/user` input that should be appended. Can contain Markdown-formatted text. - @MainActor - public mutating func append(userInput input: String) { - self.append(.init(role: .user, content: input)) - } - - /// Append an `ChatEntity/Role/system` prompt to the `Chat`. - /// - /// - Parameters: - /// - systemPrompt: The `ChatEntity/Role/system` prompt of the `Chat`, inserted at the very beginning. Can contain Markdown-formatted text. - /// - insertAtStart: Defines if the system prompt should be inserted at the start of the conversational context, defaults to `true`. - @MainActor - public mutating func append(systemMessage systemPrompt: String, insertAtStart: Bool = true) { - if insertAtStart { - if let index = self.lastIndex(where: { $0.role == .system }) { - // Insert new system prompt after the existing ones - self.insert(.init(role: .system, content: systemPrompt), at: index + 1) - } else { - // If no system prompt exists yet, insert at the very beginning - self.insert(.init(role: .system, content: systemPrompt), at: 0) - } - } else { - self.append(.init(role: .system, content: systemPrompt)) - } - } - - /// Append a `ChatEntity/Role/function` response from a function call to the `Chat. - /// - /// - Parameters: - /// - functionName: The name of the `ChatEntity/Role/function` that is called by the LLM. - /// - functionResponse: The response `String` of the `ChatEntity/Role/function` that is called by the LLM. - @MainActor - public mutating func append(forFunction functionName: String, response functionResponse: String) { - self.append(.init(role: .function(name: functionName), content: functionResponse)) - } - - - /// Marks the latest chat entry as `ChatEntity/completed`, if the role of the chat is `ChatEntity/Role/assistant`. - @MainActor - public mutating func completeAssistantStreaming() { - guard let lastChatEntity = self.last, - lastChatEntity.role == .assistant else { - return - } - - self[self.count - 1] = .init( - role: .assistant, - content: lastChatEntity.content, - complete: true, - id: lastChatEntity.id, - date: lastChatEntity.date - ) - } -} diff --git a/Sources/SpeziLLM/Helpers/LLMContext+Append.swift b/Sources/SpeziLLM/Helpers/LLMContext+Append.swift new file mode 100644 index 0000000..69562d8 --- /dev/null +++ b/Sources/SpeziLLM/Helpers/LLMContext+Append.swift @@ -0,0 +1,105 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + + +extension LLMContext { + /// Append an ``LLMContextEntity/Role-swift.enum/assistant(toolCalls:)`` output (without `toolCalls`) to the ``LLMContext``. + /// + /// Automatically appends to the last ``LLMContextEntity/Role-swift.enum/assistant(toolCalls:)`` message if there is one, otherwise create a new one. + /// If the `overwrite` parameter is `true`, the existing message is overwritten. + /// + /// - Parameters: + /// - output: The ``LLMContextEntity/Role-swift.enum/assistant(toolCalls:)`` output `String` (part) that should be appended. Can contain Markdown-formatted text. + /// - complete: Indicates if the ``LLMContextEntity`` is complete after appending to it one last time via the ``append(assistantOutput:complete:overwrite:)`` function. + /// - overwrite: Indicates if the already present content of the assistant message should be overwritten. + @MainActor + public mutating func append(assistantOutput output: String, complete: Bool = false, overwrite: Bool = false) { + guard let lastContextEntity = self.last, + case .assistant(let functionCalls) = lastContextEntity.role, + functionCalls.isEmpty else { + self.append(.init(role: .assistant(), content: output, complete: complete)) + return + } + + self[self.count - 1] = .init( + role: .assistant(), + content: overwrite ? output : (lastContextEntity.content + output), + complete: complete, + id: lastContextEntity.id, + date: lastContextEntity.date + ) + } + + /// Append an ``LLMContextEntity/Role-swift.enum/user`` input to the ``LLMContext``. + /// + /// - Parameters: + /// - input: The ``LLMContextEntity/Role-swift.enum/user`` input that should be appended. Can contain Markdown-formatted text. + @MainActor + public mutating func append(userInput input: String, id: UUID = .init(), date: Date = .now) { + self.append(.init(role: .user, content: input, id: id, date: date)) + } + + /// Append a ``LLMContextEntity/Role-swift.enum/system`` prompt to the ``LLMContext``. + /// + /// - Parameters: + /// - systemPrompt: The ``LLMContextEntity/Role-swift.enum/system`` prompt of the ``LLMContext``, inserted at the very beginning. Can contain Markdown-formatted text. + /// - insertAtStart: Defines if the system prompt should be inserted at the start of the conversational context, defaults to `true`. + @MainActor + public mutating func append(systemMessage systemPrompt: String, insertAtStart: Bool = true) { + if insertAtStart { + if let index = self.lastIndex(where: { $0.role == .system }) { + // Insert new system prompt after the existing ones + self.insert(.init(role: .system, content: systemPrompt), at: index + 1) + } else { + // If no system prompt exists yet, insert at the very beginning + self.insert(.init(role: .system, content: systemPrompt), at: 0) + } + } else { + self.append(.init(role: .system, content: systemPrompt)) + } + } + + /// Append a ``LLMContextEntity/Role-swift.enum/tool(id:name:)`` response from a function call to the ``LLMContext``. + /// + /// - Parameters: + /// - functionName: The name of the ``LLMContextEntity/Role-swift.enum/tool(id:name:)`` that is called by the LLM. + /// - functionResponse: The response `String` of the ``LLMContextEntity/Role-swift.enum/tool(id:name:)`` that is called by the LLM. + @MainActor + public mutating func append(forFunction functionName: String, withID functionID: String, response functionResponse: String) { + self.append(.init(role: .tool(id: functionID, name: functionName), content: functionResponse)) + } + + /// Append an ``LLMContextEntity/Role-swift.enum/assistant(toolCalls:)`` response including `toolCalls` to the ``LLMContext``. + /// + /// - Parameters: + /// - functionCalls: The function calls (tool calls) that the LLM requested. + @MainActor + public mutating func append(functionCalls: [LLMContextEntity.ToolCall]) { + self.append(.init(role: .assistant(toolCalls: functionCalls), content: "")) + } + + /// Marks the latest chat entry as ``LLMContextEntity/complete``, if the role of the chat is ``LLMContextEntity/Role-swift.enum/assistant(toolCalls:)`` without any `toolCalls`. + @MainActor + public mutating func completeAssistantStreaming() { + guard let lastContextEntity = self.last, + case .assistant(let functionCalls) = lastContextEntity.role, + functionCalls.isEmpty else { + return + } + + self[self.count - 1] = .init( + role: .assistant(), + content: lastContextEntity.content, + complete: true, + id: lastContextEntity.id, + date: lastContextEntity.date + ) + } +} diff --git a/Sources/SpeziLLM/Helpers/LLMContext+Chat.swift b/Sources/SpeziLLM/Helpers/LLMContext+Chat.swift new file mode 100644 index 0000000..29e2ceb --- /dev/null +++ b/Sources/SpeziLLM/Helpers/LLMContext+Chat.swift @@ -0,0 +1,83 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SpeziChat + + +extension LLMContext { + /// Maps the ``LLMContext`` to a `SpeziChat/Chat`. + @MainActor public var chat: Chat { + get { + self.map { contextEntity in // swiftlint:disable:this closure_body_length + switch contextEntity.role { + case .user: + ChatEntity( + role: .user, + content: contextEntity.content, + complete: contextEntity.complete, + id: contextEntity.id, + date: contextEntity.date + ) + case .assistant(let toolCalls): + if !toolCalls.isEmpty { + ChatEntity( + role: .hidden(type: .assistantToolCall), + content: toolCalls.map { "\($0.id) \($0.name) \($0.arguments)" }.joined(separator: "\n"), + complete: contextEntity.complete, + id: contextEntity.id, + date: contextEntity.date + ) + } else { + ChatEntity( + role: .assistant, + content: contextEntity.content, + complete: contextEntity.complete, + id: contextEntity.id, + date: contextEntity.date + ) + } + case .system: + ChatEntity( + role: .hidden(type: .system), + content: contextEntity.content, + complete: contextEntity.complete, + id: contextEntity.id, + date: contextEntity.date + ) + case .tool: + ChatEntity( + role: .hidden(type: .function), + content: contextEntity.content, + complete: contextEntity.complete, + id: contextEntity.id, + date: contextEntity.date + ) + } + } + } + set { + /// Write back newly added ``LLMContextEntity/Role-swift.enum/user`` message from `Chat` to the ``LLMSession/context`. + guard let newEntity = newValue.last, + case .user = newEntity.role else { + return + } + + self.append(userInput: newEntity.content, id: newEntity.id, date: newEntity.date) + } + } +} + + +extension ChatEntity.HiddenMessageType { + /// Assistant tool call hidden message type of the `ChatEntity`. + static let assistantToolCall = ChatEntity.HiddenMessageType(name: "assistantToolCall") + /// System hidden message type of the `ChatEntity`. + static let system = ChatEntity.HiddenMessageType(name: "system") + /// Function hidden message type of the `ChatEntity`. + static let function = ChatEntity.HiddenMessageType(name: "function") +} diff --git a/Sources/SpeziLLM/Helpers/Chat+Init.swift b/Sources/SpeziLLM/Helpers/LLMContext+Init.swift similarity index 94% rename from Sources/SpeziLLM/Helpers/Chat+Init.swift rename to Sources/SpeziLLM/Helpers/LLMContext+Init.swift index ab6e60e..08245f2 100644 --- a/Sources/SpeziLLM/Helpers/Chat+Init.swift +++ b/Sources/SpeziLLM/Helpers/LLMContext+Init.swift @@ -6,10 +6,7 @@ // SPDX-License-Identifier: MIT // -import SpeziChat - - -extension Chat { +extension LLMContext { /// Creates a new `Chat` array with an arbitrary number of system messages. /// /// - Parameters: @@ -20,7 +17,6 @@ extension Chat { } } - /// Resets the `Chat` array, deleting all persisted content. @MainActor public mutating func reset() { diff --git a/Sources/SpeziLLM/LLMRunner.swift b/Sources/SpeziLLM/LLMRunner.swift index 1f3f3c1..fc5cd7d 100644 --- a/Sources/SpeziLLM/LLMRunner.swift +++ b/Sources/SpeziLLM/LLMRunner.swift @@ -20,7 +20,7 @@ import SpeziChat /// /// The main functionality of the ``LLMRunner`` is``LLMRunner/callAsFunction(with:)``, turning a ``LLMSchema`` to an executable ``LLMSession`` via the respective ``LLMPlatform``. /// The created ``LLMSession`` then holds the LLM context and is able to perform the actual LLM inference. -/// For one-shot LLM inference tasks, the ``LLMRunner`` provides ``LLMRunner/oneShot(with:chat:)-2a1du`` and ``LLMRunner/oneShot(with:chat:)-24coq``, enabling the ``LLMRunner`` to deal with the LLM state management and reducing the burden on developers by just returning an `AsyncThrowingStream` or `String` directly. +/// For one-shot LLM inference tasks, the ``LLMRunner`` provides ``LLMRunner/oneShot(with:context:)-1nwqi`` and ``LLMRunner/oneShot(with:context:)-5gae7``, enabling the ``LLMRunner`` to deal with the LLM state management and reducing the burden on developers by just returning an `AsyncThrowingStream` or `String` directly. /// /// ### Usage /// @@ -159,13 +159,13 @@ public class LLMRunner: Module, EnvironmentAccessible, DefaultInitializable { /// /// - Parameters: /// - with: The ``LLMSchema`` that should be turned into an ``LLMSession``. - /// - chat: The context of the LLM used for the inference. + /// - context: The context of the LLM used for the inference. /// /// - Returns: The ready to use `AsyncThrowingStream`. - public func oneShot(with llmSchema: L, chat: Chat) async throws -> AsyncThrowingStream { + public func oneShot(with llmSchema: L, context: LLMContext) async throws -> AsyncThrowingStream { let llmSession = callAsFunction(with: llmSchema) await MainActor.run { - llmSession.context = chat + llmSession.context = context } return try await llmSession.generate() @@ -180,10 +180,10 @@ public class LLMRunner: Module, EnvironmentAccessible, DefaultInitializable { /// - chat: The context of the LLM used for the inference. /// /// - Returns: The completed output `String`. - public func oneShot(with llmSchema: L, chat: Chat) async throws -> String { + public func oneShot(with llmSchema: L, context: LLMContext) async throws -> String { var output = "" - for try await stringPiece in try await oneShot(with: llmSchema, chat: chat) { + for try await stringPiece in try await oneShot(with: llmSchema, context: context) { output.append(stringPiece) } diff --git a/Sources/SpeziLLM/LLMSession.swift b/Sources/SpeziLLM/LLMSession.swift index a60fa19..e029be5 100644 --- a/Sources/SpeziLLM/LLMSession.swift +++ b/Sources/SpeziLLM/LLMSession.swift @@ -41,7 +41,7 @@ import SpeziChat /// private var task: Task<(), Never>? /// /// @MainActor public var state: LLMState = .uninitialized -/// @MainActor public var context: Chat = [] +/// @MainActor public var context: LLMContext = [] /// /// init(_ platform: LLMMockPlatform, schema: LLMMockSchema) { /// self.platform = platform @@ -68,7 +68,7 @@ public protocol LLMSession: AnyObject, Sendable { /// The state of the ``LLMSession`` indicated by the ``LLMState``. @MainActor var state: LLMState { get set } /// The current context state of the ``LLMSession``, includes the entire prompt history including system prompts, user input, and model responses. - @MainActor var context: Chat { get set } + @MainActor var context: LLMContext { get set } /// Starts the inference of the ``LLMSession`` based on the ``LLMSession/context``. diff --git a/Sources/SpeziLLM/Mock/LLMMockSession.swift b/Sources/SpeziLLM/Mock/LLMMockSession.swift index 8806663..751acc0 100644 --- a/Sources/SpeziLLM/Mock/LLMMockSession.swift +++ b/Sources/SpeziLLM/Mock/LLMMockSession.swift @@ -25,7 +25,7 @@ public final class LLMMockSession: LLMSession, @unchecked Sendable { @ObservationIgnored private var task: Task<(), Never>? @MainActor public var state: LLMState = .uninitialized - @MainActor public var context: Chat = [] + @MainActor public var context: LLMContext = [] /// Initializer for the ``LLMMockSession``. @@ -38,6 +38,7 @@ public final class LLMMockSession: LLMSession, @unchecked Sendable { self.schema = schema } + @discardableResult public func generate() async throws -> AsyncThrowingStream { let (stream, continuation) = AsyncThrowingStream.makeStream(of: String.self) diff --git a/Sources/SpeziLLM/Models/LLMContext.swift b/Sources/SpeziLLM/Models/LLMContext.swift new file mode 100644 index 0000000..645195e --- /dev/null +++ b/Sources/SpeziLLM/Models/LLMContext.swift @@ -0,0 +1,13 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + + +/// Represents the context of an ``LLMSession``. +/// +/// A ``LLMContext`` is nothing more than an ordered array of ``LLMContextEntity``s which contain the content of the individual messages. +public typealias LLMContext = [LLMContextEntity] diff --git a/Sources/SpeziLLM/Models/LLMContextEntity.swift b/Sources/SpeziLLM/Models/LLMContextEntity.swift new file mode 100644 index 0000000..4f88e0d --- /dev/null +++ b/Sources/SpeziLLM/Models/LLMContextEntity.swift @@ -0,0 +1,92 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + + +/// Represents the basic building block of a Spezi ``LLMContext``. +/// +/// A ``LLMContextEntity`` can be thought of as a single message entity within a ``LLMContext`` +/// It consists of a ``LLMContextEntity/Role``, a unique identifier, a timestamp in the form of a `Date` as well as an `String`-based ``LLMContextEntity/content`` property which can contain Markdown-formatted text. +/// Furthermore, the ``LLMContextEntity/complete`` flag indicates if the current state of the ``LLMContextEntity`` is final and the content will not be updated anymore. +public struct LLMContextEntity: Codable, Equatable, Hashable, Identifiable { + /// Represents a tool call by the LLM, including its parameters + public struct ToolCall: Codable, Equatable, Hashable { + /// The ID of the function call, uniquely identifying the specific function call and matching the response to it. + public let id: String + /// The name of the function call. + public let name: String + /// The arguments as JSON of the function call. + public let arguments: String + + + /// Create a new ``LLMContextEntity/ToolCall``. + /// + /// - Parameters: + /// - id: The ID of the function call. + /// - name: The name of the function call. + /// - arguments: The arguments of the function call. + public init(id: String, name: String, arguments: String) { + self.id = id + self.name = name + self.arguments = arguments + } + } + + /// Indicates which ``LLMContextEntity/Role`` is associated with a ``LLMContextEntity``. + public enum Role: Codable, Equatable, Hashable { + case user + case assistant(toolCalls: [ToolCall] = []) + case system + case tool(id: String, name: String) + + + var rawValue: String { + switch self { + case .user: "user" + case .assistant: "assistant" + case .system: "system" + case .tool: "tool" + } + } + } + + /// ``LLMContextEntity/Role`` associated with the ``LLMContextEntity``. + public let role: Role + /// `String`-based content of the ``LLMContextEntity``. + public let content: String + /// Indicates if the ``LLMContextEntity`` is complete and will not receive any additional content. + public let complete: Bool + /// Unique identifier of the ``LLMContextEntity``. + public let id: UUID + /// The creation date of the ``LLMContextEntity``. + public let date: Date + + + /// Creates a ``LLMContextEntity`` which is the building block of a Spezi ``LLMContext``. + /// + /// - Parameters: + /// - role: ``LLMContextEntity/Role`` associated with the ``LLMContextEntity``. + /// - content: `String`-based content of the ``LLMContextEntity``. Can contain Markdown-formatted text. + /// - complete: Indicates if the content of the ``LLMContextEntity`` is complete and will not receive any additional content. Defaults to `true`. + /// - id: Unique identifier of the ``LLMContextEntity``, defaults to a randomly assigned id. + /// - date: Timestamp on when the ``LLMContextEntity`` was originally created, defaults to the current time. + public init( + role: Role, + content: Content, + complete: Bool = true, + id: UUID = .init(), + date: Date = .now + ) { + self.role = role + self.content = String(content) + self.complete = complete + self.id = id + self.date = date + } +} diff --git a/Sources/SpeziLLM/LLMError.swift b/Sources/SpeziLLM/Models/LLMError.swift similarity index 100% rename from Sources/SpeziLLM/LLMError.swift rename to Sources/SpeziLLM/Models/LLMError.swift diff --git a/Sources/SpeziLLM/LLMState+OperationState.swift b/Sources/SpeziLLM/Models/LLMState+OperationState.swift similarity index 100% rename from Sources/SpeziLLM/LLMState+OperationState.swift rename to Sources/SpeziLLM/Models/LLMState+OperationState.swift diff --git a/Sources/SpeziLLM/LLMState.swift b/Sources/SpeziLLM/Models/LLMState.swift similarity index 100% rename from Sources/SpeziLLM/LLMState.swift rename to Sources/SpeziLLM/Models/LLMState.swift diff --git a/Sources/SpeziLLM/Views/LLMChatView.swift b/Sources/SpeziLLM/Views/LLMChatView.swift index 385138e..efa242b 100644 --- a/Sources/SpeziLLM/Views/LLMChatView.swift +++ b/Sources/SpeziLLM/Views/LLMChatView.swift @@ -10,7 +10,6 @@ import SpeziChat import SpeziViews import SwiftUI - /// Chat view that enables users to interact with an LLM based on an ``LLMSession``. /// /// The ``LLMChatView`` takes an ``LLMSession`` instance and an optional `ChatView/ChatExportFormat` as parameters within the ``LLMChatView/init(session:exportFormat:)``. The ``LLMSession`` is the executable version of the LLM containing context and state as defined by the ``LLMSchema``. @@ -62,7 +61,7 @@ public struct LLMChatView: View { public var body: some View { ChatView( - $llm.context, + $llm.context.chat, disableInput: inputDisabled, exportFormat: exportFormat, messagePendingAnimation: .automatic @@ -80,6 +79,8 @@ public struct LLMChatView: View { for try await token in stream { llm.context.append(assistantOutput: token) } + + llm.context.completeAssistantStreaming() } catch let error as LLMError { llm.state = .error(error: error) } catch { @@ -113,7 +114,7 @@ public struct LLMChatView: View { return NavigationStack { LLMChatView(session: $llm) - .speak(llm.context, muted: true) + .speak(llm.context.chat, muted: true) .speechToolbarButton(muted: .constant(true)) .previewWith { LLMRunner { diff --git a/Sources/SpeziLLM/Views/LLMChatViewSchema.swift b/Sources/SpeziLLM/Views/LLMChatViewSchema.swift index 64d60d9..2857b0c 100644 --- a/Sources/SpeziLLM/Views/LLMChatViewSchema.swift +++ b/Sources/SpeziLLM/Views/LLMChatViewSchema.swift @@ -12,7 +12,7 @@ import SwiftUI /// Chat view that enables users to interact with an LLM based on an ``LLMSchema``. /// -/// The ``LLMChatViewSchema`` takes an ``LLMSchema`` instance as parameter within the ``LLMChatViewSchema/init(with:)``. +/// The ``LLMChatViewSchema`` takes an ``LLMSchema`` instance as parameter within the ``LLMChatViewSchema/init(with:exportFormat:)``. /// The ``LLMSchema`` defines the type and properties of the LLM that will be used by the ``LLMChatViewSchema`` to generate responses to user prompts. /// /// - Tip: The ``LLMChatViewSchema`` is a convenience abstraction of the ``LLMChatView``. Refer to ``LLMChatView`` for more details. diff --git a/Sources/SpeziLLMFog/Configuration/LLMFogModelParameters.swift b/Sources/SpeziLLMFog/Configuration/LLMFogModelParameters.swift new file mode 100644 index 0000000..ee7d6b8 --- /dev/null +++ b/Sources/SpeziLLMFog/Configuration/LLMFogModelParameters.swift @@ -0,0 +1,66 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import OpenAI + + +/// Represents the model-specific parameters of Fog LLMs. +public struct LLMFogModelParameters: Sendable { + /// The format for model responses. + let responseFormat: ChatQuery.ResponseFormat? + /// The sampling temperature (0 to 2). Higher values increase randomness, lower values enhance focus. + let temperature: Double? + /// Nucleus sampling threshold. Considers tokens with top_p probability mass. Alternative to temperature sampling. + let topP: Double? + /// Sequences (up to 4) where generation stops. Output doesn't include these sequences. + let stopSequence: [String] + /// Maximum token count for each completion. + let maxOutputLength: Int? + /// OpenAI will make a best effort to sample deterministically, such that repeated requests with the same seed and parameters should return the same result. Determinism is not guaranteed. + let seed: Int? + /// Adjusts new topic exploration (-2.0 to 2.0). Higher values encourage novelty. + let presencePenalty: Double? + /// Controls repetition (-2.0 to 2.0). Higher values reduce the likelihood of repeating content. + let frequencyPenalty: Double? + + + /// Initializes ``LLMFogModelParameters`` for Fog LLM model configuration. + /// + /// - Parameters: + /// - responseFormat: Format for model responses. + /// - temperature: Sampling temperature (0 to 2); higher values (e.g., 0.8) increase randomness, lower values (e.g., 0.2) enhance focus. Adjust this or topP, not both. + /// - topP: Nucleus sampling threshold; considers tokens with top_p probability mass. Alternative to temperature sampling. + /// - stopSequence: Sequences (up to 4) where generation stops; output doesn't include these sequences. + /// - maxOutputLength: Maximum token count for each completion. + /// - seed: OpenAI will make a best effort to sample deterministically, such that repeated requests with the same seed and parameters should return the same result. Determinism is not guaranteed. + /// - presencePenalty: Adjusts new topic exploration (-2.0 to 2.0); higher values encourage novelty. + /// - frequencyPenalty: Controls repetition (-2.0 to 2.0); higher values reduce likelihood of repeating content. + public init( + responseFormat: ChatQuery.ResponseFormat? = nil, + temperature: Double? = nil, + topP: Double? = nil, + stopSequence: [String] = [], + maxOutputLength: Int? = nil, + seed: Int? = nil, + presencePenalty: Double? = nil, + frequencyPenalty: Double? = nil + ) { + self.responseFormat = responseFormat + self.temperature = temperature + self.topP = topP + self.stopSequence = stopSequence + self.maxOutputLength = maxOutputLength + self.seed = seed + self.presencePenalty = presencePenalty + self.frequencyPenalty = frequencyPenalty + } +} + + +extension ChatQuery.ResponseFormat: @unchecked Sendable {} diff --git a/Sources/SpeziLLMFog/Configuration/LLMFogParameters.swift b/Sources/SpeziLLMFog/Configuration/LLMFogParameters.swift new file mode 100644 index 0000000..7873b4e --- /dev/null +++ b/Sources/SpeziLLMFog/Configuration/LLMFogParameters.swift @@ -0,0 +1,75 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import SpeziLLM + + +/// Represents the parameters of Fog LLMs. +public struct LLMFogParameters: Sendable { + public enum FogModel: String, Sendable { + /// The Gemma model from Google DeepMind in its 7B variation. + case gemma7B = "gemma" + /// The Gemma model from Google DeepMind in its 2B variation. + case gemma2B = "gemma:2b" + /// The Llama 2 model from Meta in its 7B variation. + case llama7B = "llama2" + /// The Llama 2 model from Meta in its 13B variation. + case llama13B = "llama2:13b" + /// The Llama 2 model from Meta in its 70B variation. + case llama70B = "llama2:70b" + /// The 7B model released by Mistral AI, updated to version 0.2. + case mistral + /// A high-quality Mixture of Experts (MoE) model with open weights by Mistral AI. + case mixtral + /// 2.7B language model by Microsoft Research that demonstrates outstanding reasoning and language understanding capabilities. + case phi + /// The TinyLlama project is an open endeavour to train a compact 1.1B Llama model on 3 trillion tokens. + case tinyllama + } + + + /// The to-be-used Fog LLM model. + let modelType: FogModel + /// The to-be-used system prompt(s) of the LLM. + let systemPrompts: [String] + /// Closure that returns an up-to-date auth token for requests to Fog LLMs (e.g., a Firebase ID token). + let authToken: @Sendable () async -> String? + + + /// Creates the ``LLMFogParameters``. + /// + /// - Parameters: + /// - modelType: The to-be-used Fog LLM model such as Google's Gemma models or Meta Llama models. + /// - systemPrompt: The to-be-used system prompt of the LLM enabling fine-tuning of the LLMs behaviour. Defaults to the regular Llama2 system prompt. + /// - authToken: Closure that returns an up-to-date auth token for requests to Fog LLMs (e.g., a Firebase ID token). + public init( + modelType: FogModel, + systemPrompt: String? = nil, + authToken: @Sendable @escaping () async -> String? + ) { + self.init(modelType: modelType, systemPrompts: systemPrompt.map { [$0] } ?? [], authToken: authToken) + } + + /// Creates the ``LLMFogParameters``. + /// + /// - Parameters: + /// - modelType: The to-be-used Fog LLM model such as Google's Gemma models or Meta Llama models. + /// - systemPrompts: The to-be-used system prompt of the LLM enabling fine-tuning of the LLMs behaviour. Defaults to the regular Llama2 system prompt. + /// - authToken: Closure that returns an up-to-date auth token for requests to Fog LLMs (e.g., a Firebase ID token). + @_disfavoredOverload + public init( + modelType: FogModel, + systemPrompts: [String] = [], + authToken: @Sendable @escaping () async -> String? + ) { + self.modelType = modelType + self.systemPrompts = systemPrompts + self.authToken = authToken + } +} diff --git a/Sources/SpeziLLMFog/Configuration/LLMFogPlatformConfiguration.swift b/Sources/SpeziLLMFog/Configuration/LLMFogPlatformConfiguration.swift new file mode 100644 index 0000000..92bae19 --- /dev/null +++ b/Sources/SpeziLLMFog/Configuration/LLMFogPlatformConfiguration.swift @@ -0,0 +1,52 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + + +/// Represents the configuration of the Spezi ``LLMFogPlatform``. +public struct LLMFogPlatformConfiguration: Sendable { + /// Name of the to-be-discovered service within the local network. + let host: String + /// Root CA certificate which should be trusted for the TLS network connection. + let caCertificate: URL? + /// Task priority of the initiated LLM inference tasks. + let taskPriority: TaskPriority + /// Number of concurrent streams to the Fog LLM. + let concurrentStreams: Int + /// Maximum network timeout of Fog LLM requests in seconds. + let timeout: TimeInterval + /// Duration of mDNS browsing for Fog LLM services. + let mDnsBrowsingTimeout: Duration + + + /// Creates the ``LLMFogPlatformConfiguration`` which configures the Spezi ``LLMFogPlatform``. + /// + /// - Parameters: + /// - host: The name of the to-be-discovered service within the local network via mDNS. The hostname must match the issued TLS certificate of the fog node. Defaults to `spezillmfog.local` which is used for the mDNS advertisements as well as the TLS certificate. + /// - caCertificate: The root CA certificate which should be trusted for the TLS network connection. The host certificate must be signed via the CA certificate. + /// - taskPriority: The task priority of the initiated LLM inference tasks, defaults to `.userInitiated`. + /// - concurrentStreams: Indicates the number of concurrent streams to the Fog LLM, defaults to `5`. + /// - timeout: Indicates the maximum network timeout of Fog LLM requests in seconds. defaults to `60`. + /// - mDnsBrowsingTimeout: Duration of mDNS browsing for Fog LLM services, default to `100ms`. + public init( + host: String = "spezillmfog.local", + caCertificate: URL? = nil, + taskPriority: TaskPriority = .userInitiated, + concurrentStreams: Int = 5, + timeout: TimeInterval = 60, + mDnsBrowsingTimeout: Duration = .milliseconds(100) + ) { + self.host = host + self.caCertificate = caCertificate + self.taskPriority = taskPriority + self.concurrentStreams = concurrentStreams + self.timeout = timeout + self.mDnsBrowsingTimeout = mDnsBrowsingTimeout + } +} diff --git a/Sources/SpeziLLMFog/Helpers/Chat+OpenAI.swift b/Sources/SpeziLLMFog/Helpers/Chat+OpenAI.swift new file mode 100644 index 0000000..f60772b --- /dev/null +++ b/Sources/SpeziLLMFog/Helpers/Chat+OpenAI.swift @@ -0,0 +1,26 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import struct OpenAI.ChatQuery +import SpeziLLM + + +extension LLMContextEntity.Role { + typealias Role = ChatQuery.ChatCompletionMessageParam.Role + + + /// Maps the `LLMContextEntity/Role`s to the `OpenAI/Chat/Role`s. + var openAIRepresentation: Role { + switch self { + case .assistant: .assistant + case .user: .user + case .system: .system + case .tool: .tool + } + } +} diff --git a/Sources/SpeziLLMFog/LLMFogError.swift b/Sources/SpeziLLMFog/LLMFogError.swift new file mode 100644 index 0000000..33dad2c --- /dev/null +++ b/Sources/SpeziLLMFog/LLMFogError.swift @@ -0,0 +1,111 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import Network +import SpeziLLM + + +/// Errors that can occur by interacting with Fog LLMs. +public enum LLMFogError: LLMError { + /// Fog LLM user token is invalid. + case invalidAPIToken + /// Connectivity error + case connectivityIssues(URLError) + /// Error during generation + case generationError + /// Error during accessing the Fog LLM Model + case modelAccessError(Error) + /// Fog CA certificate is missing / not readable. + case missingCaCertificate + /// No mDNS services were found + case mDnsServicesNotFound + /// Network error during mDNS service discovery. + case mDnsServiceDiscoveryNetworkError + /// Unknown error + case unknownError(Error) + + + public var errorDescription: String? { + switch self { + case .invalidAPIToken: + String(localized: LocalizedStringResource("LLM_INVALID_TOKEN_ERROR_DESCRIPTION", bundle: .atURL(from: .module))) + case .connectivityIssues: + String(localized: LocalizedStringResource("LLM_CONNECTIVITY_ERROR_DESCRIPTION", bundle: .atURL(from: .module))) + case .generationError: + String(localized: LocalizedStringResource("LLM_GENERATION_ERROR_DESCRIPTION", bundle: .atURL(from: .module))) + case .modelAccessError: + String(localized: LocalizedStringResource("LLM_MODEL_ACCESS_ERROR_DESCRIPTION", bundle: .atURL(from: .module))) + case .missingCaCertificate: + String(localized: LocalizedStringResource("LLM_MISSING_CA_CERT_ERROR_DESCRIPTION", bundle: .atURL(from: .module))) + case .mDnsServicesNotFound: + String(localized: LocalizedStringResource("LLM_NO_MDNS_SERVICE_FOUND_ERROR_DESCRIPTION", bundle: .atURL(from: .module))) + case .mDnsServiceDiscoveryNetworkError: + String(localized: LocalizedStringResource("LLM_SERIVE_DISCOVERY_ERROR_DESCRIPTION", bundle: .atURL(from: .module))) + case .unknownError: + String(localized: LocalizedStringResource("LLM_UNKNOWN_ERROR_DESCRIPTION", bundle: .atURL(from: .module))) + } + } + + public var recoverySuggestion: String? { + switch self { + case .invalidAPIToken: + String(localized: LocalizedStringResource("LLM_INVALID_TOKEN_RECOVERY_SUGGESTION", bundle: .atURL(from: .module))) + case .connectivityIssues: + String(localized: LocalizedStringResource("LLM_CONNECTIVITY_ERROR_RECOVERY_SUGGESTION", bundle: .atURL(from: .module))) + case .generationError: + String(localized: LocalizedStringResource("LLM_GENERATION_ERROR_RECOVERY_SUGGESTION", bundle: .atURL(from: .module))) + case .modelAccessError: + String(localized: LocalizedStringResource("LLM_MODEL_ACCESS_ERROR_RECOVERY_SUGGESTION", bundle: .atURL(from: .module))) + case .missingCaCertificate: + String(localized: LocalizedStringResource("LLM_MISSING_CA_CERT_ERROR_RECOVERY_SUGGESTION", bundle: .atURL(from: .module))) + case .mDnsServicesNotFound: + String(localized: LocalizedStringResource("LLM_NO_MDNS_SERVICE_FOUND_ERROR_RECOVERY_SUGGESTION", bundle: .atURL(from: .module))) + case .mDnsServiceDiscoveryNetworkError: + String(localized: LocalizedStringResource("LLM_SERIVE_DISCOVERY_ERROR_RECOVERY_SUGGESTION", bundle: .atURL(from: .module))) + case .unknownError: + String(localized: LocalizedStringResource("LLM_UNKNOWN_ERROR_RECOVERY_SUGGESTION", bundle: .atURL(from: .module))) + } + } + + public var failureReason: String? { + switch self { + case .invalidAPIToken: + String(localized: LocalizedStringResource("LLM_INVALID_TOKEN_FAILURE_REASON", bundle: .atURL(from: .module))) + case .connectivityIssues: + String(localized: LocalizedStringResource("LLM_CONNECTIVITY_ERROR_FAILURE_REASON", bundle: .atURL(from: .module))) + case .generationError: + String(localized: LocalizedStringResource("LLM_GENERATION_ERROR_FAILURE_REASON", bundle: .atURL(from: .module))) + case .modelAccessError: + String(localized: LocalizedStringResource("LLM_MODEL_ACCESS_ERROR_FAILURE_REASON", bundle: .atURL(from: .module))) + case .missingCaCertificate: + String(localized: LocalizedStringResource("LLM_MISSING_CA_CERT_ERROR_FAILURE_REASON", bundle: .atURL(from: .module))) + case .mDnsServicesNotFound: + String(localized: LocalizedStringResource("LLM_NO_MDNS_SERVICE_FOUND_ERROR_FAILURE_REASON", bundle: .atURL(from: .module))) + case .mDnsServiceDiscoveryNetworkError: + String(localized: LocalizedStringResource("LLM_SERIVE_DISCOVERY_ERROR_FAILURE_REASON", bundle: .atURL(from: .module))) + case .unknownError: + String(localized: LocalizedStringResource("LLM_UNKNOWN_ERROR_FAILURE_REASON", bundle: .atURL(from: .module))) + } + } + + + public static func == (lhs: LLMFogError, rhs: LLMFogError) -> Bool { + switch (lhs, rhs) { + case (.invalidAPIToken, .invalidAPIToken): true + case (.connectivityIssues, .connectivityIssues): true + case (.generationError, .generationError): true + case (.modelAccessError, .modelAccessError): true + case (.missingCaCertificate, .missingCaCertificate): true + case (.mDnsServicesNotFound, .mDnsServicesNotFound): true + case (.mDnsServiceDiscoveryNetworkError, .mDnsServiceDiscoveryNetworkError): true + case (.unknownError, .unknownError): true + default: false + } + } +} diff --git a/Sources/SpeziLLMFog/LLMFogPlatform.swift b/Sources/SpeziLLMFog/LLMFogPlatform.swift new file mode 100644 index 0000000..aae2ba6 --- /dev/null +++ b/Sources/SpeziLLMFog/LLMFogPlatform.swift @@ -0,0 +1,100 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import Spezi +import SpeziFoundation +import SpeziLLM + + +/// LLM execution platform of an ``LLMFogSchema``. +/// +/// The ``LLMFogPlatform`` turns a received ``LLMFogSchema`` to an executable ``LLMFogSession`` which runs on an LLM Fog node within the local network. +/// Use ``LLMFogPlatform/callAsFunction(with:)`` with an ``LLMFogSchema`` parameter to get an executable ``LLMFogSession`` that does the actual inference. +/// +/// It is important to note that the ``LLMFogPlatform`` discovers fog computing resources within the local network and then dispatches LLM inference jobs to these fog nodes. +/// In turn, that means that such a fog node must exist within the local network, see the `FogNode` distributed with the package. +/// +/// In order to establish a secure connection to the fog node, the TLS encryption mechanism is used. +/// That results in the need for the ``LLMFogPlatform`` to be configured via ``LLMFogPlatform/init(configuration:)`` and +/// ``LLMFogPlatformConfiguration/init(host:caCertificate:taskPriority:concurrentStreams:timeout:mDnsBrowsingTimeout:)`` with the custom +/// root CA certificate in the `.crt` format that signed the web service certificate of the fog node. See the `FogNode/README.md` and specifically the `setup.sh` script for more details. +/// +/// - Important: ``LLMFogPlatform`` shouldn't be used directly but used via the `SpeziLLM` `LLMRunner` that delegates the requests towards the ``LLMFogPlatform``. +/// The `SpeziLLM` `LLMRunner` must be configured with the ``LLMFogPlatform`` within the Spezi `Configuration`. +/// +/// - Tip: For more information, refer to the documentation of the `LLMPlatform` from SpeziLLM. +/// +/// ### Usage +/// +/// The example below demonstrates the setup of the ``LLMFogPlatform`` within the Spezi `Configuration`. The initializer requires the passing of a local `URL` to the root CA certificate in the `.crt` format that signed the web service certificate on the fog node. See the `FogNode/README.md` and specifically the `setup.sh` script for more details. +/// +/// ```swift +/// class TestAppDelegate: SpeziAppDelegate { +/// private nonisolated static var caCertificateUrl: URL { +/// // Return local file URL of root CA certificate in the `.crt` format +/// } +/// +/// override var configuration: Configuration { +/// Configuration { +/// LLMRunner { +/// LLMFogPlatform(configuration: .init(caCertificate: Self.caCertificateUrl)) +/// } +/// } +/// } +/// } +/// ``` +/// +/// - Important: For development purposes, one is able to configure the fog node in the development mode, meaning no TLS connection (resulting in no need for custom certificates). See the `FogNode/README.md` for more details regarding server-side (so fog node) instructions. +/// On the client-side within Spezi, one has to pass `nil` for the `caCertificate` parameter of the ``LLMFogPlatform`` as shown above. If used in development mode, no custom CA certificate is required, ensuring a smooth and straightforward development process. +public actor LLMFogPlatform: LLMPlatform { + /// Enforce an arbitrary number of concurrent execution jobs of Fog LLMs. + private let semaphore: AsyncSemaphore + let configuration: LLMFogPlatformConfiguration + + @MainActor public var state: LLMPlatformState = .idle + + + /// Creates an instance of the ``LLMFogPlatform``. + /// + /// - Parameters: + /// - configuration: The configuration of the platform. + public init(configuration: LLMFogPlatformConfiguration) { + self.configuration = configuration + self.semaphore = AsyncSemaphore(value: configuration.concurrentStreams) + } + + + public nonisolated func callAsFunction(with llmSchema: LLMFogSchema) -> LLMFogSession { + LLMFogSession(self, schema: llmSchema) + } + + func exclusiveAccess() async throws { + try await semaphore.waitCheckingCancellation() + + if await state != .processing { + await MainActor.run { + state = .processing + } + } + } + + func signal() async { + let otherTasksWaiting = semaphore.signal() + + if !otherTasksWaiting { + await MainActor.run { + state = .idle + } + } + } + + + deinit { + } +} diff --git a/Sources/SpeziLLMFog/LLMFogSchema.swift b/Sources/SpeziLLMFog/LLMFogSchema.swift new file mode 100644 index 0000000..fcaaf0b --- /dev/null +++ b/Sources/SpeziLLMFog/LLMFogSchema.swift @@ -0,0 +1,63 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import SpeziChat +import SpeziLLM + + +/// Defines the type and configuration of the ``LLMFogSession``. +/// +/// The ``LLMFogSchema`` is used as a configuration for the to-be-used Fog LLM. It contains all information necessary for the creation of an executable ``LLMFogSession``. +/// It is bound to the ``LLMFogPlatform`` that is responsible for turning the ``LLMFogSchema`` to an ``LLMFogSession``. +/// +/// - Important: The ``LLMFogSchema`` accepts a closure that returns an authorization token that is passed with every request to the Fog node in the `Bearer` HTTP field via the ``LLMFogParameters/init(modelType:systemPrompt:authToken:)``. The token is created via the closure upon every LLM inference request, as the ``LLMFogSession`` may be long lasting and the token could therefore expire. Ensure that the closure appropriately caches the token in order to prevent unnecessary token refresh roundtrips to external systems. +/// +/// - Tip: For more information, refer to the documentation of the `LLMSchema` from SpeziLLM. +/// +/// ### Usage +/// +/// The code example below showcases a minimal instantiation of an ``LLMFogSchema``. +/// Note the `authToken` closure that is specified in the ``LLMFogSchema/init(parameters:modelParameters:injectIntoContext:)``, as this closure should return a token that is then passed as a `Bearer` HTTP token to the fog node with every LLM inference request. +/// +/// ```swift +/// var schema = LLMFogSchema( +/// parameters: .init( +/// modelType: .llama7B, +/// systemPrompt: "You're a helpful assistant that answers questions from users.", +/// authToken: { +/// // Return authorization token as `String` or `nil` if no token is required by the Fog node. +/// } +/// ) +/// ) +/// ``` +public struct LLMFogSchema: LLMSchema, @unchecked Sendable { + public typealias Platform = LLMFogPlatform + + + let parameters: LLMFogParameters + let modelParameters: LLMFogModelParameters + public let injectIntoContext: Bool + + + /// Creates an instance of the ``LLMFogSchema`` containing all necessary configuration for Fog LLM inference. + /// + /// - Parameters: + /// - parameters: Parameters of the Fog LLM client. + /// - modelParameters: Parameters of the used Fog LLM. + /// - injectIntoContext: Indicates if the inference output by the ``LLMFogSession`` should automatically be inserted into the ``LLMFogSession/context``, defaults to false. + public init( + parameters: LLMFogParameters, + modelParameters: LLMFogModelParameters = .init(), + injectIntoContext: Bool = false + ) { + self.parameters = parameters + self.modelParameters = modelParameters + self.injectIntoContext = injectIntoContext + } +} diff --git a/Sources/SpeziLLMFog/LLMFogSession+Configuration.swift b/Sources/SpeziLLMFog/LLMFogSession+Configuration.swift new file mode 100644 index 0000000..b5373ad --- /dev/null +++ b/Sources/SpeziLLMFog/LLMFogSession+Configuration.swift @@ -0,0 +1,47 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import OpenAI + + +extension LLMFogSession { + typealias Chat = ChatQuery.ChatCompletionMessageParam + typealias FunctionDeclaration = ChatQuery.ChatCompletionToolParam + + + /// Map the ``LLMFogSession/context`` to the OpenAI `[Chat]` representation. + private var openAIContext: [Chat] { + get async { + await self.context.compactMap { contextEntity in + Chat( + role: contextEntity.role.openAIRepresentation, + content: contextEntity.content + ) + } + } + } + + /// Provides the ``LLMFogSession/context``, the `` LLMFogParameters`` and ``LLMFogModelParameters`` + /// in an OpenAI `ChatQuery` representation used for querying the Fog LLM API. + var openAIChatQuery: ChatQuery { + get async { + await .init( + messages: self.openAIContext, + model: schema.parameters.modelType.rawValue, + frequencyPenalty: schema.modelParameters.frequencyPenalty, + maxTokens: schema.modelParameters.maxOutputLength, + presencePenalty: schema.modelParameters.presencePenalty, + responseFormat: schema.modelParameters.responseFormat, + seed: schema.modelParameters.seed, + stop: .stringList(schema.modelParameters.stopSequence), + temperature: schema.modelParameters.temperature, + topP: schema.modelParameters.topP + ) + } + } +} diff --git a/Sources/SpeziLLMFog/LLMFogSession+Generation.swift b/Sources/SpeziLLMFog/LLMFogSession+Generation.swift new file mode 100644 index 0000000..c37a4a5 --- /dev/null +++ b/Sources/SpeziLLMFog/LLMFogSession+Generation.swift @@ -0,0 +1,91 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import OpenAI +import SpeziChat + + +extension LLMFogSession { + private static let modelNotFoundRegex: Regex = { + guard let regex = try? Regex("model '([\\w:]+)' not found, try pulling it first") else { + preconditionFailure("SpeziLLMFog: Error Regex could not be parsed") + } + + return regex + }() + + + /// Based on the input prompt, generate the output via some OpenAI API, e.g., Ollama. + /// + /// - Parameters: + /// - continuation: A Swift `AsyncThrowingStream` that streams the generated output. + func _generate( // swiftlint:disable:this identifier_name + continuation: AsyncThrowingStream.Continuation + ) async { + Self.logger.debug("SpeziLLMFog: Fog LLM started a new inference") + await MainActor.run { + self.state = .generating + } + + let chatStream: AsyncThrowingStream = await self.model.chatsStream(query: self.openAIChatQuery) + + do { + for try await streamResult in chatStream { + guard await !checkCancellation(on: continuation) else { + Self.logger.debug("SpeziLLMFog: LLM inference cancelled because of Task cancellation.") + return + } + + let outputPiece = streamResult.choices.first?.delta.content ?? "" + + if schema.injectIntoContext { + await MainActor.run { + context.append(assistantOutput: outputPiece) + } + } + + continuation.yield(outputPiece) + } + + continuation.finish() + if schema.injectIntoContext { + await MainActor.run { + context.completeAssistantStreaming() + } + } + } catch let error as APIErrorResponse { + // Sadly, there's no better way to check the error messages as there aren't any Ollama error codes as with the OpenAI API + if error.error.message.contains(Self.modelNotFoundRegex) { + Self.logger.error("SpeziLLMFog: LLM model type could not be accessed on fog node - \(error.error.message)") + await finishGenerationWithError(LLMFogError.modelAccessError(error), on: continuation) + } else if error.error.code == "401" || error.error.code == "403" { + Self.logger.error("SpeziLLMFog: LLM model could not be accessed as the passed token is invalid.") + await finishGenerationWithError(LLMFogError.invalidAPIToken, on: continuation) + } else { + Self.logger.error("SpeziLLMFog: Generation error occurred - \(error)") + await finishGenerationWithError(LLMFogError.generationError, on: continuation) + } + return + } catch let error as URLError { + Self.logger.error("SpeziLLMFog: Connectivity Issues with the Fog Node: \(error)") + await finishGenerationWithError(LLMFogError.connectivityIssues(error), on: continuation) + return + } catch { + Self.logger.error("SpeziLLMFog: Generation error occurred - \(error)") + await finishGenerationWithError(LLMFogError.generationError, on: continuation) + return + } + + Self.logger.debug("SpeziLLMFog: Fog LLM completed an inference") + + await MainActor.run { + self.state = .ready + } + } +} diff --git a/Sources/SpeziLLMFog/LLMFogSession+Setup.swift b/Sources/SpeziLLMFog/LLMFogSession+Setup.swift new file mode 100644 index 0000000..16ae347 --- /dev/null +++ b/Sources/SpeziLLMFog/LLMFogSession+Setup.swift @@ -0,0 +1,154 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import Network +import OpenAI + + +extension LLMFogSession { + /// Set up the Fog LLM execution client. + /// + /// - Parameters: + /// - continuation: A Swift `AsyncThrowingStream` that streams the generated output. + /// - Returns: `true` if the setup was successful, `false` otherwise. + func _setup(continuation: AsyncThrowingStream.Continuation) async -> Bool { + // swiftlint:disable:previous function_body_length identifier_name + Self.logger.debug("SpeziLLMFog: Fog LLM is being initialized") + await MainActor.run { + self.state = .loading + } + + var caCertificate: SecCertificate? + + if let caCertificateUrl = platform.configuration.caCertificate { + // Load the specified CA certificate and strip out irrelevant data + guard let caCertificateContents = try? String(contentsOf: caCertificateUrl, encoding: .utf8) + .replacingOccurrences(of: "-----BEGIN CERTIFICATE-----", with: "") + .replacingOccurrences(of: "-----END CERTIFICATE-----", with: "") + .replacingOccurrences(of: "\n", with: ""), + let caCertificateData = Data(base64Encoded: caCertificateContents), + let caCreatedCertificate = SecCertificateCreateWithData(nil, caCertificateData as CFData) else { + Self.logger.error(""" + SpeziLLMFog: The to-be-trusted CA certificate ensuring encrypted traffic to the fog LLM couldn't be read. + Please ensure that the certificate is in the `.crt` format and available under the specified URL. + """) + await finishGenerationWithError(LLMFogError.missingCaCertificate, on: continuation) + return false + } + + caCertificate = caCreatedCertificate + } + + let fogServiceAddress: String + + do { + // Discover and resolve fog service + fogServiceAddress = try await resolveFogService(secureTraffic: caCertificate != nil) + self.discoveredServiceAddress = fogServiceAddress + } catch is CancellationError { + Self.logger.debug("SpeziLLMFog: mDNS task discovery has been aborted because of Task cancellation.") + continuation.finish() + return false + } catch let error as LLMFogError { + await finishGenerationWithError(error, on: continuation) + return false + } catch { + await finishGenerationWithError(LLMFogError.unknownError(error), on: continuation) + return false + } + + self.wrappedModel = OpenAI( + configuration: .init( + token: await schema.parameters.authToken(), + host: fogServiceAddress, + port: (caCertificate != nil) ? 443 : 80, + scheme: (caCertificate != nil) ? "https" : "http", + timeoutInterval: platform.configuration.timeout, + caCertificate: caCertificate, + expectedHost: platform.configuration.host + ) + ) + + await MainActor.run { + self.state = .ready + } + Self.logger.debug("SpeziLLMFog: Fog LLM finished initializing, now ready to use") + return true + } + + /// Resolves a Spezi Fog LLM computing resource to an IP address. + private func resolveFogService(secureTraffic: Bool = true) async throws -> String { + // Browse for configured mDNS services + let browser = NWBrowser( + for: .bonjour( + type: secureTraffic ? "_https._tcp" : "_http._tcp", + domain: platform.configuration.host + "." + ), + using: .init() + ) + + browser.start(queue: .global(qos: .userInitiated)) + + // Possible `Cancellation` error handled in the caller + try await Task.sleep(for: platform.configuration.mDnsBrowsingTimeout) + + guard let discoveredEndpoint = browser.browseResults.randomElement()?.endpoint else { + browser.cancel() + Self.logger.error("SpeziLLMFog: A \(self.platform.configuration.host + ".") mDNS service of type '_https._tcp' could not be found.") + throw LLMFogError.mDnsServicesNotFound + } + + browser.cancel() + + // Resolve the discovered endpoint to a hostname + let connection = NWConnection(to: discoveredEndpoint, using: .tcp) + + let resolvedService = try await withCheckedThrowingContinuation { continuation in + connection.stateUpdateHandler = { state in + switch state { + case .ready: + if let remoteEndpoint = connection.currentPath?.remoteEndpoint, + case let .hostPort(host, _) = remoteEndpoint { + let ipAddress: String? = switch host { + // No other way to get the current IP address from NWEndpoint + case .ipv4(let ipv4Address): ipv4Address.debugDescription.components(separatedBy: "%").first + case .ipv6(let ipv6Address): ipv6Address.debugDescription.components(separatedBy: "%").first + default: nil + } + + continuation.resume(returning: ipAddress) + } else { + continuation.resume(returning: nil) + } + + connection.stateUpdateHandler = nil // Prevent further updates + connection.cancel() + case .cancelled, .failed: + connection.cancel() + Self.logger.error("SpeziLLMFog: \(discoveredEndpoint.debugDescription) mDNS service could not be resolved because of a network error.") + continuation.resume(throwing: LLMFogError.mDnsServiceDiscoveryNetworkError) + return + default: + break + } + } + + connection.start(queue: .global(qos: .userInitiated)) + } + + guard let resolvedService else { + Self.logger.error("SpeziLLMFog: \(discoveredEndpoint.debugDescription) mDNS service could not be resolved to an IP.") + throw LLMFogError.mDnsServicesNotFound + } + + Self.logger.debug("SpeziLLMFog: \(discoveredEndpoint.debugDescription) mDNS service resolved to: \(resolvedService).") + + return resolvedService + } +} diff --git a/Sources/SpeziLLMFog/LLMFogSession.swift b/Sources/SpeziLLMFog/LLMFogSession.swift new file mode 100644 index 0000000..6a17194 --- /dev/null +++ b/Sources/SpeziLLMFog/LLMFogSession.swift @@ -0,0 +1,172 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import class OpenAI.OpenAI +import os +import SpeziChat +import SpeziLLM + + +/// Represents an ``LLMFogSchema`` in execution. +/// +/// The ``LLMFogSession`` is the executable version of a Fog LLM containing context and state as defined by the ``LLMFogSchema``. +/// It provides access to text-based models from the Fog LLM resource, such as Llama2 or Gemma. +/// +/// As the to-be-used models are running on a Fog node within the local network, the respective LLM computing resource (so the fog node) is discovered upon setup of the ``LLMFogSession``, meaning a ``LLMFogSession`` is bound to a specific fog node after initialization. +/// +/// The inference is started by ``LLMFogSession/generate()``, returning an `AsyncThrowingStream` and can be cancelled via ``LLMFogSession/cancel()``. +/// Additionally, one is able to force the setup of the ``LLMFogSession`` (so discovering the respective fog LLM service) via ``LLMFogSession/setup(continuation:)``. +/// The ``LLMFogSession`` exposes its current state via the ``LLMFogSession/context`` property, containing all the conversational history with the LLM. +/// +/// - Warning: The ``LLMFogSession`` shouldn't be created manually but always through the ``LLMFogPlatform`` via the `LLMRunner`. +/// +/// - Tip: For more information, refer to the documentation of the `LLMSession` from SpeziLLM. +/// +/// ### Usage +/// +/// The example below demonstrates a minimal usage of the ``LLMFogSession`` via the `LLMRunner`. +/// +/// ```swift +/// struct LLMFogDemoView: View { +/// @Environment(LLMRunner.self) var runner +/// @State var responseText = "" +/// +/// var body: some View { +/// Text(responseText) +/// .task { +/// // Instantiate the `LLMFogSchema` to an `LLMFogSession` via the `LLMRunner`. +/// let llmSession: LLMFogSession = runner( +/// with: LLMFogSchema( +/// parameters: .init( +/// modelType: .llama7B, +/// systemPrompt: "You're a helpful assistant that answers questions from users." +/// ) +/// ) +/// ) +/// +/// for try await token in try await llmSession.generate() { +/// responseText.append(token) +/// } +/// } +/// } +/// } +/// ``` +@Observable +public final class LLMFogSession: LLMSession, @unchecked Sendable { + /// A Swift Logger that logs important information from the ``LLMFogSession``. + static let logger = Logger(subsystem: "edu.stanford.spezi", category: "SpeziLLMFog") + + + let platform: LLMFogPlatform + let schema: LLMFogSchema + + /// A set of `Task`s managing the ``LLMFogSession`` output generation. + @ObservationIgnored private var tasks: Set> = [] + /// Ensuring thread-safe access to the `LLMFogSession/task`. + @ObservationIgnored private var lock = NSLock() + /// The wrapped client instance communicating with the Fog LLM + @ObservationIgnored var wrappedModel: OpenAI? + /// Discovered fog node advertising the LLM inference service + @ObservationIgnored var discoveredServiceAddress: String? + + @MainActor public var state: LLMState = .uninitialized + @MainActor public var context: LLMContext = [] + + + var model: OpenAI { + guard let model = wrappedModel else { + preconditionFailure(""" + SpeziLLMFog: Illegal Access - Tried to access the wrapped Fog LLM model of `LLMFogSession` before being initialized. + Ensure that the `LLMFogPlatform` is passed to the `LLMRunner` within the Spezi `Configuration`. + """) + } + return model + } + + + /// Creates an instance of a ``LLMFogSession`` responsible for LLM inference. + /// Only the ``LLMFogPlatform`` should create an instance of ``LLMFogSession``. + /// + /// - Parameters: + /// - platform: Reference to the ``LLMFogPlatform`` where the ``LLMFogSession`` is running on. + /// - schema: The configuration of the Fog LLM expressed by the ``LLMFogSchema``. + init(_ platform: LLMFogPlatform, schema: LLMFogSchema) { + self.platform = platform + self.schema = schema + + // Inject system prompts into context + Task { @MainActor in + schema.parameters.systemPrompts.forEach { systemPrompt in + context.append(systemMessage: systemPrompt) + } + } + } + + + @discardableResult + public func generate() async throws -> AsyncThrowingStream { + try await platform.exclusiveAccess() + + let (stream, continuation) = AsyncThrowingStream.makeStream(of: String.self) + + // Execute the output generation of the LLM + let task = Task(priority: platform.configuration.taskPriority) { + // Unregister as soon as `Task` finishes + defer { + Task { + await platform.signal() + } + } + + // Setup the fog LLM, if not already done + guard await setup(continuation: continuation), + await !checkCancellation(on: continuation) else { + return + } + + // Get fresh auth token + wrappedModel?.configuration.token = await schema.parameters.authToken() + + // Execute the inference + await _generate(continuation: continuation) + } + + _ = lock.withLock { + tasks.insert(task) + } + + return stream + } + + public func setup( + continuation: AsyncThrowingStream.Continuation = AsyncThrowingStream.makeStream(of: String.self).continuation + ) async -> Bool { + // Setup the model, if not already done + if wrappedModel == nil { + guard await _setup(continuation: continuation) else { + return false + } + } + + return true + } + + public func cancel() { + lock.withLock { + for task in tasks { + task.cancel() + } + } + } + + + deinit { + cancel() + } +} diff --git a/Sources/SpeziLLMFog/Resources/Localizable.xcstrings b/Sources/SpeziLLMFog/Resources/Localizable.xcstrings new file mode 100644 index 0000000..9ca4ba3 --- /dev/null +++ b/Sources/SpeziLLMFog/Resources/Localizable.xcstrings @@ -0,0 +1,246 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "LLM_CONNECTIVITY_ERROR_DESCRIPTION" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Connectivity Error with the Fog Node." + } + } + } + }, + "LLM_CONNECTIVITY_ERROR_FAILURE_REASON" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The network connection to the Fog Node servers couldn't be established, most probably the device doesn't have a proper connection to the Fog Node." + } + } + } + }, + "LLM_CONNECTIVITY_ERROR_RECOVERY_SUGGESTION" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Please ensure that the device has a stable internet connection and the Fog Node is running correctly." + } + } + } + }, + "LLM_GENERATION_ERROR_DESCRIPTION" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Generation Error occurred during Fog LLM inference." + } + } + } + }, + "LLM_GENERATION_ERROR_FAILURE_REASON" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The Fog LLM API responded with an error during the output generation." + } + } + } + }, + "LLM_GENERATION_ERROR_RECOVERY_SUGGESTION" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Please retry the input query and ensure that the Fog Node is running correctly." + } + } + } + }, + "LLM_INVALID_TOKEN_ERROR_DESCRIPTION" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unauthorized access to the Fog LLM resource." + } + } + } + }, + "LLM_INVALID_TOKEN_FAILURE_REASON" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The specified token is not valid / not authorized to access the Fog LLM resource." + } + } + } + }, + "LLM_INVALID_TOKEN_RECOVERY_SUGGESTION" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ensure that the specified token is valid and has the potentially required claims to the LLM resource." + } + } + } + }, + "LLM_MISSING_CA_CERT_ERROR_DESCRIPTION" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Couldn't establish a secure connection to the fog node." + } + } + } + }, + "LLM_MISSING_CA_CERT_ERROR_FAILURE_REASON" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The root CA certificate that signed the Fog LLM certificates is missing." + } + } + } + }, + "LLM_MISSING_CA_CERT_ERROR_RECOVERY_SUGGESTION" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ensure that SpeziLLMFog is configured with the valid root CA certificate used to sign the Fog node certificates." + } + } + } + }, + "LLM_MODEL_ACCESS_ERROR_DESCRIPTION" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "LLM model type could not be accessed on fog node." + } + } + } + }, + "LLM_MODEL_ACCESS_ERROR_FAILURE_REASON" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The specified LLM model type is not present on the LLM Fog node." + } + } + } + }, + "LLM_MODEL_ACCESS_ERROR_RECOVERY_SUGGESTION" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Download the specified LLM model on the LLM Fog node." + } + } + } + }, + "LLM_NO_MDNS_SERVICE_FOUND_ERROR_DESCRIPTION" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No LLM Fog node was found in the local network." + } + } + } + }, + "LLM_NO_MDNS_SERVICE_FOUND_ERROR_FAILURE_REASON" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Either, the LLM Fog node is not properly running or the device has network connectivity issues." + } + } + } + }, + "LLM_NO_MDNS_SERVICE_FOUND_ERROR_RECOVERY_SUGGESTION" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ensure proper connectivity within the local network and the correct running of the LLM Fog node." + } + } + } + }, + "LLM_SERIVE_DISCOVERY_ERROR_DESCRIPTION" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error during fog service discovery." + } + } + } + }, + "LLM_SERIVE_DISCOVERY_ERROR_FAILURE_REASON" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "An error occurred during browsing for fog services within the local network." + } + } + } + }, + "LLM_SERIVE_DISCOVERY_ERROR_RECOVERY_SUGGESTION" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ensure proper network connectivity so that the fog service can be discovered." + } + } + } + }, + "LLM_UNKNOWN_ERROR_DESCRIPTION" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "An unknown Fog LLM error has occured." + } + } + } + }, + "LLM_UNKNOWN_ERROR_FAILURE_REASON" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "During service discovery of the LLM fog node, an unknown error occured." + } + } + } + }, + "LLM_UNKNOWN_ERROR_RECOVERY_SUGGESTION" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ensure proper network connectivity and device configuration so that the fog service can be discovered." + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/Sources/SpeziLLMFog/Resources/Localizable.xcstrings.license b/Sources/SpeziLLMFog/Resources/Localizable.xcstrings.license new file mode 100644 index 0000000..6917c4b --- /dev/null +++ b/Sources/SpeziLLMFog/Resources/Localizable.xcstrings.license @@ -0,0 +1,5 @@ +This source file is part of the Stanford Spezi open-source project + +SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) + +SPDX-License-Identifier: MIT diff --git a/Sources/SpeziLLMFog/SpeziLLMFog.docc/SpeziLLMFog.md b/Sources/SpeziLLMFog/SpeziLLMFog.docc/SpeziLLMFog.md new file mode 100644 index 0000000..5ee6364 --- /dev/null +++ b/Sources/SpeziLLMFog/SpeziLLMFog.docc/SpeziLLMFog.md @@ -0,0 +1,130 @@ +# ``SpeziLLMFog`` + + + +Discover and dispatch Large Language Models (LLMs) inference jobs to Fog node resources within the local network. + +## Overview + +A module that allows you to interact with Fog node-based Large Language Models (LLMs) in the local network within your Spezi application. +``SpeziLLMFog`` automatically discovers LLM computing resources within the local network, establishes a connection to these [Fog nodes](https://en.wikipedia.org/wiki/Fog_computing), and then dispatches LLM inference jobs to these nodes. The response is then streamed back to ``SpeziLLMFog`` and surfaced to the user. The fog nodes advertise their services via [mDNS](https://en.wikipedia.org/wiki/Multicast_DNS), enabling clients to discover all fog nodes serving a specific host within the local network. +``SpeziLLMFog`` provides a pure Swift-based API for interacting with the Fog LLMs, building on top of the infrastructure of the [SpeziLLM target](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillm). + +## Setup + +### Add Spezi LLM as a Dependency + +You need to add the SpeziLLM Swift package to +[your app in Xcode](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app#) or +[Swift package](https://developer.apple.com/documentation/xcode/creating-a-standalone-swift-package-with-xcode#Add-a-dependency-on-another-Swift-package). + +> Important: If your application is not yet configured to use Spezi, follow the [Spezi setup article](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/initial-setup) to set up the core Spezi infrastructure. + +## Spezi LLM Fog Components + +The core components of the ``SpeziLLMFog`` target are the ``LLMFogSchema``, ``LLMFogSession`` as well as ``LLMFogPlatform``. These components enable users to automatically discover Fog LLM resources via [mDNS](https://en.wikipedia.org/wiki/Multicast_DNS) and dispatch jobs to these nodes which are then performing the LLM inference on open-source LLMs like Llama2 or Gemma. + +> Important: ``SpeziLLMFog`` requires a `SpeziLLMFogNode` within the local network hosted on some computing resource that actually performs the inference requests. ``SpeziLLMFog`` provides the `SpeziLLMFogNode` Docker-based package that enables an out-of-the-box setup of these fog nodes. See the `FogNode` directory on the root level of the SPM package as well as the respective `README.md` for more details. + +### LLM Fog + +``LLMFogSchema`` offers a variety of configuration possibilities that are supported by the Fog LLM APIs (mirroring the OpenAI API implementation), such as the model type, the system prompt, the temperature of the model, and many more. These options can be set via the ``LLMFogSchema/init(parameters:modelParameters:injectIntoContext:)`` initializer and the ``LLMFogParameters`` and ``LLMFogModelParameters``. + +This ``LLMFogSchema`` is then turned into an in-execution ``LLMFogSession`` by the `LLMRunner` via the ``LLMFogPlatform``. The ``LLMFogSession`` is the executable version of a Fog LLM containing context and state as defined by the ``LLMFogSchema``. +As the to-be-used models are running on a Fog node within the local network, the respective LLM computing resource (so the fog node) is discovered upon setup of the ``LLMFogSession``, meaning a ``LLMFogSession`` is bound to a specific fog node after initialization. + +- Important: The Fog LLM abstractions shouldn't be used on it's own but always used together with the Spezi `LLMRunner`. + +#### Setup + +In order to use Fog LLMs within the Spezi ecosystem, the [SpeziLLM](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillm) [`LLMRunner`](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillm/llmrunner) needs to be initialized in the Spezi `Configuration` with the `LLMFogPlatform`. Only after, the `LLMRunner` can be used for inference with Fog LLMs. See the [SpeziLLM documentation](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillm) for more details. +The `LLMFogPlatform` needs to be initialized with the custom root CA certificate that was used to sign the fog node web service certificate (see the `FogNode/README.md` documentation for more information). Copy the root CA certificate from the fog node as resource to the application using `SpeziLLMFog` and use it to initialize the `LLMFogPlatform` within the Spezi `Configuration`. + +```swift +class LLMFogAppDelegate: SpeziAppDelegate { + private nonisolated static var caCertificateUrl: URL { + // Return local file URL of root CA certificate in the `.crt` format + } + + override var configuration: Configuration { + Configuration { + LLMRunner { + LLMFogPlatform(configuration: .init(caCertificate: Self.caCertificateUrl)) + } + } + } +} +``` + +- Important: For development purposes, one is able to configure the fog node in the development mode, meaning no TLS connection (resulting in no need for custom certificates). See the `FogNode/README.md` for more details regarding server-side (so fog node) instructions. +On the client-side within Spezi, one has to pass `nil` for the `caCertificate` parameter of the ``LLMFogPlatform`` as shown above. If used in development mode, no custom CA certificate is required, ensuring a smooth and straightforward development process. + +#### Usage + +The code example below showcases the interaction with a Fog LLM through the the [SpeziLLM](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillm) [`LLMRunner`](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillm/llmrunner), which is injected into the SwiftUI `Environment` via the `Configuration` shown above. + +The ``LLMFogSchema`` defines the type and configurations of the to-be-executed ``LLMFogSession``. This transformation is done via the [`LLMRunner`](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillm/llmrunner) that uses the ``LLMFogPlatform``. The inference via ``LLMFogSession/generate()`` returns an `AsyncThrowingStream` that yields all generated `String` pieces. +The ``LLMFogSession`` automatically discovers all available LLM fog nodes within the local network upon setup and the dispatches the LLM inference jobs to the fog computing resource, streaming back the response and surfaces it to the user. + +The ``LLMFogSession`` contains the ``LLMFogSession/context`` property which holds the entire history of the model interactions. This includes the system prompt, user input, but also assistant responses. +Ensure the property always contains all necessary information, as the ``LLMFogSession/generate()`` function executes the inference based on the ``LLMFogSession/context``. + +- Important: The ``LLMFogSchema`` accepts a closure that returns an authorization token that is passed with every request to the Fog node in the `Bearer` HTTP field via the ``LLMFogParameters/init(modelType:systemPrompt:authToken:)``. The token is created via the closure upon every LLM inference request, as the ``LLMFogSession`` may be long lasting and the token could therefore expire. Ensure that the closure appropriately caches the token in order to prevent unnecessary token refresh roundtrips to external systems. + +```swift +struct LLMFogDemoView: View { + @Environment(LLMRunner.self) var runner + @State var responseText = "" + + var body: some View { + Text(responseText) + .task { + // Instantiate the `LLMFogSchema` to an `LLMFogSession` via the `LLMRunner`. + let llmSession: LLMFogSession = runner( + with: LLMFogSchema( + parameters: .init( + modelType: .llama7B, + systemPrompt: "You're a helpful assistant that answers questions from users.", + authToken: { + // Return authorization token as `String` or `nil` if no token is required by the Fog node. + } + ) + ) + ) + + for try await token in try await llmSession.generate() { + responseText.append(token) + } + } + } +} +``` + +## Topics + +### LLM Fog abstraction + +- ``LLMFogSchema`` +- ``LLMFogSession`` + +### LLM Execution + +- ``LLMFogPlatform`` +- ``LLMFogPlatformConfiguration`` + +### LLM Configuration + +- ``LLMFogParameters`` +- ``LLMFogModelParameters`` + +### Misc + +- ``LLMFogError`` diff --git a/Sources/SpeziLLMLocal/Configuration/LLMLocalContextParameters.swift b/Sources/SpeziLLMLocal/Configuration/LLMLocalContextParameters.swift index 5d342e9..6b9bc35 100644 --- a/Sources/SpeziLLMLocal/Configuration/LLMLocalContextParameters.swift +++ b/Sources/SpeziLLMLocal/Configuration/LLMLocalContextParameters.swift @@ -7,7 +7,7 @@ // import Foundation -import llama +@preconcurrency import llama /// Represents the context parameters of the LLM. diff --git a/Sources/SpeziLLMLocal/Configuration/LLMLocalParameters.swift b/Sources/SpeziLLMLocal/Configuration/LLMLocalParameters.swift index 9c5233b..2d5e8e5 100644 --- a/Sources/SpeziLLMLocal/Configuration/LLMLocalParameters.swift +++ b/Sources/SpeziLLMLocal/Configuration/LLMLocalParameters.swift @@ -7,7 +7,7 @@ // import Foundation -import llama +@preconcurrency import llama /// Represents the parameters of the LLM. diff --git a/Sources/SpeziLLMLocal/Configuration/LLMLocalSamplingParameters.swift b/Sources/SpeziLLMLocal/Configuration/LLMLocalSamplingParameters.swift index af4a192..bd1f941 100644 --- a/Sources/SpeziLLMLocal/Configuration/LLMLocalSamplingParameters.swift +++ b/Sources/SpeziLLMLocal/Configuration/LLMLocalSamplingParameters.swift @@ -13,7 +13,7 @@ import llama /// Represents the sampling parameters of the LLM. /// /// Internally, these data points are passed as a llama.cpp `llama_sampling_params` C struct to the LLM. -public struct LLMLocalSamplingParameters: Sendable { +public struct LLMLocalSamplingParameters: Sendable { // swiftlint:disable:this type_body_length /// Helper enum for the Mirostat sampling method public enum Mirostat { init(rawValue: Int, targetEntropy: Float = 5.0, learningRate: Float = 0.1) { @@ -227,6 +227,9 @@ public struct LLMLocalSamplingParameters: Sendable { } } + // C++ vector doesn't conform to Swift sequence on VisionOS SDK (Swift C++ Interop bug), + // therefore requiring workaround for VisionSDK + #if !os(visionOS) /// Classifier-Free Guidance. var cfg: ClassifierFreeGuidance { get { @@ -242,7 +245,7 @@ public struct LLMLocalSamplingParameters: Sendable { wrapped.cfg_scale = newValue.scale } } - + /// Creates the ``LLMLocalContextParameters`` which wrap the underlying llama.cpp `llama_context_params` C struct. /// Is passed to the underlying llama.cpp model in order to configure the context of the LLM. @@ -298,4 +301,57 @@ public struct LLMLocalSamplingParameters: Sendable { self.mirostat = mirostat self.cfg = cfg } + #else + /// Creates the ``LLMLocalContextParameters`` which wrap the underlying llama.cpp `llama_context_params` C struct. + /// Is passed to the underlying llama.cpp model in order to configure the context of the LLM. + /// + /// - Parameters: + /// - rememberTokens: Number of previous tokens to remember. + /// - outputProbabilities: If greater than 0, output the probabilities of top n\_probs tokens. + /// - topK: Top-K Sampling: K most likely next words (<= 0 to use vocab size). + /// - topP: Top-p Sampling: Smallest possible set of words whose cumulative probability exceeds the probability p (1.0 = disabled). + /// - minP: Min-p Sampling (0.0 = disabled). + /// - tfs: Tail Free Sampling (1.0 = disabled). + /// - typicalP: Locally Typical Sampling. + /// - temperature: Temperature Sampling: A higher value indicates more creativity of the model but also more hallucinations. + /// - penaltyLastTokens: Last n tokens to penalize (0 = disable penalty, -1 = context size). + /// - penaltyRepeat: Penalize repeated tokens (1.0 = disabled). + /// - penaltyFrequency: Penalize frequency (0.0 = disabled). + /// - penaltyPresence: Presence penalty (0.0 = disabled). + /// - penalizeNewLines: Penalize new lines. + /// - mirostat: Mirostat sampling. + public init( + rememberTokens: Int32 = 256, + outputProbabilities: Int32 = 0, + topK: Int32 = 40, + topP: Float = 0.95, + minP: Float = 0.05, + tfs: Float = 1.0, + typicalP: Float = 1.0, + temperature: Float = 0.8, + penaltyLastTokens: Int32 = 64, + penaltyRepeat: Float = 1.1, + penaltyFrequency: Float = 0.0, + penaltyPresence: Float = 0.0, + penalizeNewLines: Bool = true, + mirostat: Mirostat = .disabled + ) { + self.wrapped = llama_sampling_params() + + self.rememberTokens = rememberTokens + self.outputProbabilities = outputProbabilities + self.topK = topK + self.topP = topP + self.minP = minP + self.tfs = tfs + self.typicalP = typicalP + self.temperature = temperature + self.penaltyLastTokens = penaltyLastTokens + self.penaltyRepeat = penaltyRepeat + self.penaltyFrequency = penaltyFrequency + self.penaltyPresence = penaltyPresence + self.penalizeNewLines = penalizeNewLines + self.mirostat = mirostat + } + #endif } diff --git a/Sources/SpeziLLMLocal/LLMLocalSchema+PromptFormatting.swift b/Sources/SpeziLLMLocal/LLMLocalSchema+PromptFormatting.swift index ec62b2e..d4861f9 100644 --- a/Sources/SpeziLLMLocal/LLMLocalSchema+PromptFormatting.swift +++ b/Sources/SpeziLLMLocal/LLMLocalSchema+PromptFormatting.swift @@ -6,14 +6,14 @@ // SPDX-License-Identifier: MIT // -import SpeziChat +import SpeziLLM extension LLMLocalSchema { /// Holds default prompt formatting strategies for [Llama2](https://ai.meta.com/llama/) as well as [Phi-2](https://www.microsoft.com/en-us/research/blog/phi-2-the-surprising-power-of-small-language-models/) models. public enum PromptFormattingDefaults { /// Prompt formatting closure for the [Llama2](https://ai.meta.com/llama/) model - public static let llama2: (@Sendable (Chat) throws -> String) = { chat in // swiftlint:disable:this closure_body_length + public static let llama2: (@Sendable (LLMContext) throws -> String) = { chat in // swiftlint:disable:this closure_body_length /// BOS token of the LLM, used at the start of each prompt passage. let BOS = "" /// EOS token of the LLM, used at the end of each prompt passage. @@ -34,17 +34,17 @@ extension LLMLocalSchema { var systemPrompts: [String] = [] var initialUserPrompt: String = "" - for chatEntity in chat { - if chatEntity.role != .system { - if chatEntity.role == .user { - initialUserPrompt = chatEntity.content + for contextEntity in chat { + if contextEntity.role != .system { + if contextEntity.role == .user { + initialUserPrompt = contextEntity.content break } else { throw LLMLocalError.illegalContext } } - systemPrompts.append(chatEntity.content) + systemPrompts.append(contextEntity.content) } /// Build the initial Llama2 prompt structure @@ -65,22 +65,22 @@ extension LLMLocalSchema { \(initialUserPrompt) \(EOINST) """ + " " // Add a spacer to the generated output from the model - for chatEntry in chat.dropFirst(2) { - if chatEntry.role == .assistant { + for contextEntity in chat.dropFirst(2) { + if contextEntity.role == .assistant() { /// Append response from assistant to the Llama2 prompt structure /// /// A template for appending an assistant response to the overall prompt looks like: /// {user_message_1} [/INST]){model_reply_1} prompt += """ - \(chatEntry.content)\(EOS) + \(contextEntity.content)\(EOS) """ - } else if chatEntry.role == .user { + } else if contextEntity.role == .user { /// Append response from user to the Llama2 prompt structure /// /// A template for appending an assistant response to the overall prompt looks like: /// [INST] {user_message_2} [/INST] prompt += """ - \(BOS)\(BOINST) \(chatEntry.content) \(EOINST) + \(BOS)\(BOINST) \(contextEntity.content) \(EOINST) """ + " " // Add a spacer to the generated output from the model } } @@ -89,7 +89,7 @@ extension LLMLocalSchema { } /// Prompt formatting closure for the [Phi-2](https://www.microsoft.com/en-us/research/blog/phi-2-the-surprising-power-of-small-language-models/) model - public static let phi2: (@Sendable (Chat) throws -> String) = { chat in + public static let phi2: (@Sendable (LLMContext) throws -> String) = { chat in guard chat.first?.role == .system else { throw LLMLocalError.illegalContext } @@ -97,17 +97,17 @@ extension LLMLocalSchema { var systemPrompts: [String] = [] var initialUserPrompt: String = "" - for chatEntity in chat { - if chatEntity.role != .system { - if chatEntity.role == .user { - initialUserPrompt = chatEntity.content + for contextEntity in chat { + if contextEntity.role != .system { + if contextEntity.role == .user { + initialUserPrompt = contextEntity.content break } else { throw LLMLocalError.illegalContext } } - systemPrompts.append(chatEntity.content) + systemPrompts.append(contextEntity.content) } /// Build the initial Phi-2 prompt structure @@ -123,16 +123,16 @@ extension LLMLocalSchema { Instruct: \(initialUserPrompt)\n """ - for chatEntry in chat.dropFirst(2) { - if chatEntry.role == .assistant { + for contextEntity in chat.dropFirst(2) { + if contextEntity.role == .assistant() { /// Append response from assistant to the Phi-2 prompt structure prompt += """ - Output: \(chatEntry.content)\n + Output: \(contextEntity.content)\n """ - } else if chatEntry.role == .user { + } else if contextEntity.role == .user { /// Append response from assistant to the Phi-2 prompt structure prompt += """ - Instruct: \(chatEntry.content)\n + Instruct: \(contextEntity.content)\n """ } } @@ -147,7 +147,7 @@ extension LLMLocalSchema { /// Prompt formatting closure for the [Gemma](https://ai.google.dev/gemma/docs/formatting) models /// - Important: System prompts are ignored as Gemma doesn't support them - public static let gemma: (@Sendable (Chat) throws -> String) = { chat in + public static let gemma: (@Sendable (LLMContext) throws -> String) = { chat in /// Start token of Gemma let startToken = "" /// End token of Gemma @@ -168,18 +168,18 @@ extension LLMLocalSchema { /// """ var prompt = "" - for chatEntry in chat { - if chatEntry.role == .assistant { + for contextEntity in chat { + if contextEntity.role == .assistant() { /// Append response from assistant to the Gemma prompt structure prompt += """ \(startToken)model - \(chatEntry.content)\(endToken)\n + \(contextEntity.content)\(endToken)\n """ - } else if chatEntry.role == .user { + } else if contextEntity.role == .user { /// Append response from assistant to the Gemma prompt structure prompt += """ \(startToken)user - \(chatEntry.content)\(endToken)\n + \(contextEntity.content)\(endToken)\n """ } } diff --git a/Sources/SpeziLLMLocal/LLMLocalSchema.swift b/Sources/SpeziLLMLocal/LLMLocalSchema.swift index 1909180..40204d2 100644 --- a/Sources/SpeziLLMLocal/LLMLocalSchema.swift +++ b/Sources/SpeziLLMLocal/LLMLocalSchema.swift @@ -30,7 +30,7 @@ public struct LLMLocalSchema: LLMSchema { /// Sampling parameters of the llama.cpp LLM. let samplingParameters: LLMLocalSamplingParameters /// Closure to properly format the ``LLMLocal/context`` to a `String` which is tokenized and passed to the LLM. - let formatChat: (@Sendable (Chat) throws -> String) + let formatChat: (@Sendable (LLMContext) throws -> String) public let injectIntoContext: Bool @@ -49,7 +49,7 @@ public struct LLMLocalSchema: LLMSchema { contextParameters: LLMLocalContextParameters = .init(), samplingParameters: LLMLocalSamplingParameters = .init(), injectIntoContext: Bool = false, - formatChat: @escaping (@Sendable (Chat) throws -> String) = PromptFormattingDefaults.llama2 + formatChat: @escaping (@Sendable (LLMContext) throws -> String) = PromptFormattingDefaults.llama2 ) { self.modelPath = modelPath self.parameters = parameters diff --git a/Sources/SpeziLLMLocal/LLMLocalSession+Generation.swift b/Sources/SpeziLLMLocal/LLMLocalSession+Generation.swift index d8c5a38..96492fd 100644 --- a/Sources/SpeziLLMLocal/LLMLocalSession+Generation.swift +++ b/Sources/SpeziLLMLocal/LLMLocalSession+Generation.swift @@ -179,8 +179,13 @@ extension LLMLocalSession { llama_print_timings(self.modelContext) continuation.finish() + if schema.injectIntoContext { + await MainActor.run { + context.completeAssistantStreaming() + } + } + await MainActor.run { - context.completeAssistantStreaming() self.state = .ready } diff --git a/Sources/SpeziLLMLocal/LLMLocalSession+PromptFormatting.swift b/Sources/SpeziLLMLocal/LLMLocalSession+PromptFormatting.swift deleted file mode 100644 index f77fa16..0000000 --- a/Sources/SpeziLLMLocal/LLMLocalSession+PromptFormatting.swift +++ /dev/null @@ -1,148 +0,0 @@ -// -// This source file is part of the Stanford Spezi open source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import SpeziChat - - -extension LLMLocalSession { - /// Holds default prompt formatting strategies for [Llama2](https://ai.meta.com/llama/) as well as [Phi-2](https://www.microsoft.com/en-us/research/blog/phi-2-the-surprising-power-of-small-language-models/) models. - public enum PromptFormattingDefaults { - /// Prompt formatting closure for the [Llama2](https://ai.meta.com/llama/) model - public static let llama2: ((Chat) throws -> String) = { chat in // swiftlint:disable:this closure_body_length - /// BOS token of the LLM, used at the start of each prompt passage. - let BOS = "" - /// EOS token of the LLM, used at the end of each prompt passage. - let EOS = "" - /// BOSYS token of the LLM, used at the start of the system prompt. - let BOSYS = "<>" - /// EOSYS token of the LLM, used at the end of the system prompt. - let EOSYS = "<>" - /// BOINST token of the LLM, used at the start of the instruction part of the prompt. - let BOINST = "[INST]" - /// EOINST token of the LLM, used at the end of the instruction part of the prompt. - let EOINST = "[/INST]" - - guard chat.first?.role == .system else { - throw LLMLocalError.illegalContext - } - - var systemPrompts: [String] = [] - var initialUserPrompt: String = "" - - for chatEntity in chat { - if chatEntity.role != .system { - if chatEntity.role == .user { - initialUserPrompt = chatEntity.content - break - } else { - throw LLMLocalError.illegalContext - } - } - - systemPrompts.append(chatEntity.content) - } - - /// Build the initial Llama2 prompt structure - /// - /// A template of the prompt structure looks like: - /// """ - /// [INST] <> - /// {your_system_prompt} - /// <> - /// - /// {user_message_1} [/INST] - /// """ - var prompt = """ - \(BOS)\(BOINST) \(BOSYS) - \(systemPrompts.joined(separator: " ")) - \(EOSYS) - - \(initialUserPrompt) \(EOINST) - """ + " " // Add a spacer to the generated output from the model - - for chatEntry in chat.dropFirst(2) { - if chatEntry.role == .assistant { - /// Append response from assistant to the Llama2 prompt structure - /// - /// A template for appending an assistant response to the overall prompt looks like: - /// {user_message_1} [/INST]){model_reply_1} - prompt += """ - \(chatEntry.content)\(EOS) - """ - } else if chatEntry.role == .user { - /// Append response from user to the Llama2 prompt structure - /// - /// A template for appending an assistant response to the overall prompt looks like: - /// [INST] {user_message_2} [/INST] - prompt += """ - \(BOS)\(BOINST) \(chatEntry.content) \(EOINST) - """ + " " // Add a spacer to the generated output from the model - } - } - - return prompt - } - - /// Prompt formatting closure for the [Phi-2](https://www.microsoft.com/en-us/research/blog/phi-2-the-surprising-power-of-small-language-models/) model - public static let phi2: ((Chat) throws -> String) = { chat in - guard chat.first?.role == .system else { - throw LLMLocalError.illegalContext - } - - var systemPrompts: [String] = [] - var initialUserPrompt: String = "" - - for chatEntity in chat { - if chatEntity.role != .system { - if chatEntity.role == .user { - initialUserPrompt = chatEntity.content - break - } else { - throw LLMLocalError.illegalContext - } - } - - systemPrompts.append(chatEntity.content) - } - - /// Build the initial Phi-2 prompt structure - /// - /// A template of the prompt structure looks like: - /// """ - /// System: {your_system_prompt} - /// Instruct: {model_reply_1} - /// Output: {model_reply_1} - /// """ - var prompt = """ - System: \(systemPrompts.joined(separator: " ")) - Instruct: \(initialUserPrompt)\n - """ - - for chatEntry in chat.dropFirst(2) { - if chatEntry.role == .assistant { - /// Append response from assistant to the Phi-2 prompt structure - prompt += """ - Output: \(chatEntry.content)\n - """ - } else if chatEntry.role == .user { - /// Append response from assistant to the Phi-2 prompt structure - prompt += """ - Instruct: \(chatEntry.content)\n - """ - } - } - - /// Model starts responding after - if chat.last?.role == .user { - prompt += "Output: " - } - - return prompt - } - } -} diff --git a/Sources/SpeziLLMLocal/LLMLocalSession.swift b/Sources/SpeziLLMLocal/LLMLocalSession.swift index 2c410fc..61a2fa8 100644 --- a/Sources/SpeziLLMLocal/LLMLocalSession.swift +++ b/Sources/SpeziLLMLocal/LLMLocalSession.swift @@ -65,7 +65,7 @@ public final class LLMLocalSession: LLMSession, @unchecked Sendable { @ObservationIgnored private var task: Task<(), Never>? @MainActor public var state: LLMState = .uninitialized - @MainActor public var context: Chat = [] + @MainActor public var context: LLMContext = [] /// A pointer to the allocated model via llama.cpp. @ObservationIgnored var model: OpaquePointer? diff --git a/Sources/SpeziLLMOpenAI/Configuration/LLMOpenAIModelParameters.swift b/Sources/SpeziLLMOpenAI/Configuration/LLMOpenAIModelParameters.swift index 3e99a31..8428f95 100644 --- a/Sources/SpeziLLMOpenAI/Configuration/LLMOpenAIModelParameters.swift +++ b/Sources/SpeziLLMOpenAI/Configuration/LLMOpenAIModelParameters.swift @@ -13,7 +13,7 @@ import OpenAI /// Represents the model-specific parameters of OpenAIs LLMs. public struct LLMOpenAIModelParameters: Sendable { /// The format for model responses. - let responseFormat: ResponseFormat? + let responseFormat: ChatQuery.ResponseFormat? /// The sampling temperature (0 to 2). Higher values increase randomness, lower values enhance focus. let temperature: Double? /// Nucleus sampling threshold. Considers tokens with top_p probability mass. Alternative to temperature sampling. @@ -24,6 +24,8 @@ public struct LLMOpenAIModelParameters: Sendable { let stopSequence: [String] /// Maximum token count for each completion. let maxOutputLength: Int? + /// OpenAI will make a best effort to sample deterministically, such that repeated requests with the same seed and parameters should return the same result. Determinism is not guaranteed. + let seed: Int? /// Adjusts new topic exploration (-2.0 to 2.0). Higher values encourage novelty. let presencePenalty: Double? /// Controls repetition (-2.0 to 2.0). Higher values reduce the likelihood of repeating content. @@ -43,17 +45,19 @@ public struct LLMOpenAIModelParameters: Sendable { /// - completionsPerOutput: Number of generated chat completions (choices) per input, defaults to 1 choice. /// - stopSequence: Sequences (up to 4) where generation stops; output doesn't include these sequences. /// - maxOutputLength: Maximum token count for each completion. + /// - seed: OpenAI will make a best effort to sample deterministically, such that repeated requests with the same seed and parameters should return the same result. Determinism is not guaranteed. /// - presencePenalty: Adjusts new topic exploration (-2.0 to 2.0); higher values encourage novelty. /// - frequencyPenalty: Controls repetition (-2.0 to 2.0); higher values reduce likelihood of repeating content. /// - logitBias: Alters specific token's likelihood in completion. /// - user: Unique identifier for the end-user, aiding in abuse monitoring. public init( - responseFormat: ResponseFormat? = nil, + responseFormat: ChatQuery.ResponseFormat? = nil, temperature: Double? = nil, topP: Double? = nil, completionsPerOutput: Int? = nil, stopSequence: [String] = [], maxOutputLength: Int? = nil, + seed: Int? = nil, presencePenalty: Double? = nil, frequencyPenalty: Double? = nil, logitBias: [String: Int] = [:], @@ -65,6 +69,7 @@ public struct LLMOpenAIModelParameters: Sendable { self.completionsPerOutput = completionsPerOutput self.stopSequence = stopSequence self.maxOutputLength = maxOutputLength + self.seed = seed self.presencePenalty = presencePenalty self.frequencyPenalty = frequencyPenalty self.logitBias = logitBias @@ -73,4 +78,4 @@ public struct LLMOpenAIModelParameters: Sendable { } -extension ResponseFormat: @unchecked Sendable {} +extension ChatQuery.ResponseFormat: @unchecked Sendable {} diff --git a/Sources/SpeziLLMOpenAI/Configuration/LLMOpenAIParameters.swift b/Sources/SpeziLLMOpenAI/Configuration/LLMOpenAIParameters.swift index 72475e0..f6e564d 100644 --- a/Sources/SpeziLLMOpenAI/Configuration/LLMOpenAIParameters.swift +++ b/Sources/SpeziLLMOpenAI/Configuration/LLMOpenAIParameters.swift @@ -43,10 +43,12 @@ public struct LLMOpenAIParameters: Sendable { modelAccessTest: Bool = false, overwritingToken: String? = nil ) { - self.modelType = modelType - self.systemPrompts = systemPrompt.map { [$0] } ?? [] - self.modelAccessTest = modelAccessTest - self.overwritingToken = overwritingToken + self.init( + modelType: modelType, + systemPrompts: systemPrompt.map { [$0] } ?? [], + modelAccessTest: modelAccessTest, + overwritingToken: overwritingToken + ) } /// Creates the ``LLMOpenAIParameters``. diff --git a/Sources/SpeziLLMOpenAI/Configuration/LLMOpenAIPlatformConfiguration.swift b/Sources/SpeziLLMOpenAI/Configuration/LLMOpenAIPlatformConfiguration.swift index a528efb..90f377d 100644 --- a/Sources/SpeziLLMOpenAI/Configuration/LLMOpenAIPlatformConfiguration.swift +++ b/Sources/SpeziLLMOpenAI/Configuration/LLMOpenAIPlatformConfiguration.swift @@ -13,8 +13,11 @@ import Foundation public struct LLMOpenAIPlatformConfiguration: Sendable { /// The task priority of the initiated LLM inference tasks. let taskPriority: TaskPriority + /// Indicates the number of concurrent streams to the OpenAI API. let concurrentStreams: Int + /// The OpenAI API token on a global basis. let apiToken: String? + /// Maximum network timeout of OpenAI requests in seconds. let timeout: TimeInterval diff --git a/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterSchemaCollector.swift b/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterSchemaCollector.swift index 350594d..b27f805 100644 --- a/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterSchemaCollector.swift +++ b/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterSchemaCollector.swift @@ -19,7 +19,7 @@ protocol LLMFunctionParameterSchemaCollector { extension LLMFunction { - typealias LLMFunctionParameterSchema = JSONSchema + typealias LLMFunctionParameterSchema = ChatQuery.ChatCompletionToolParam.FunctionDefinition.FunctionParameters var schemaValueCollectors: [String: LLMFunctionParameterSchemaCollector] { diff --git a/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper+ArrayTypes.swift b/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper+ArrayTypes.swift index c9d5649..2fe643a 100644 --- a/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper+ArrayTypes.swift +++ b/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper+ArrayTypes.swift @@ -122,7 +122,7 @@ extension _LLMFunctionParameterWrapper where T: AnyArray, T.Element: StringProto /// - description: Describes the purpose of the parameter, used by the LLM to grasp the purpose of the parameter. /// - pattern: A Regular Expression that the parameter needs to conform to. /// - const: Specifies the constant `String`-based value of a certain parameter. - /// - enumValues: Defines all cases of a single `String` `array` element. + /// - enum: Defines all cases of a single `String` `array` element. /// - minItems: Defines the minimum amount of values in the `array`. /// - maxItems: Defines the maximum amount of values in the `array`. /// - uniqueItems: Specifies if all `array` elements need to be unique. @@ -130,7 +130,7 @@ extension _LLMFunctionParameterWrapper where T: AnyArray, T.Element: StringProto description: D, pattern: (any StringProtocol)? = nil, const: (any StringProtocol)? = nil, - enumValues: [any StringProtocol]? = nil, + enum: [any StringProtocol]? = nil, minItems: Int? = nil, maxItems: Int? = nil, uniqueItems: Bool? = nil @@ -142,7 +142,7 @@ extension _LLMFunctionParameterWrapper where T: AnyArray, T.Element: StringProto type: .string, pattern: pattern.map { String($0) }, const: const.map { String($0) }, - enumValues: enumValues.map { $0.map { String($0) } } + enum: `enum`.map { $0.map { String($0) } } ), minItems: minItems, maxItems: maxItems, diff --git a/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper+CustomTypes.swift b/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper+CustomTypes.swift index 03c7a50..c7ab7cd 100644 --- a/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper+CustomTypes.swift +++ b/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper+CustomTypes.swift @@ -32,7 +32,7 @@ extension _LLMFunctionParameterWrapper where T: AnyArray, T.Element: LLMFunction properties: T.Element.itemSchema.properties, pattern: T.Element.itemSchema.pattern, const: T.Element.itemSchema.const, - enumValues: T.Element.itemSchema.enumValues, + enum: T.Element.itemSchema.enum, multipleOf: T.Element.itemSchema.multipleOf, minimum: T.Element.itemSchema.minimum, maximum: T.Element.itemSchema.maximum @@ -66,7 +66,7 @@ extension _LLMFunctionParameterWrapper where T: AnyOptional, T.Wrapped: AnyArray properties: T.Wrapped.Element.itemSchema.properties, pattern: T.Wrapped.Element.itemSchema.pattern, const: T.Wrapped.Element.itemSchema.const, - enumValues: T.Wrapped.Element.itemSchema.enumValues, + enum: T.Wrapped.Element.itemSchema.enum, multipleOf: T.Wrapped.Element.itemSchema.multipleOf, minimum: T.Wrapped.Element.itemSchema.minimum, maximum: T.Wrapped.Element.itemSchema.maximum diff --git a/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper+Enum.swift b/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper+Enum.swift index 85a377a..de61066 100644 --- a/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper+Enum.swift +++ b/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper+Enum.swift @@ -24,7 +24,7 @@ extension _LLMFunctionParameterWrapper where T: LLMFunctionParameterEnum, T.RawV type: .string, description: String(description), const: const.map { String($0) }, - enumValues: T.allCases.map { String($0.rawValue) } + enum: T.allCases.map { String($0.rawValue) } )) } } @@ -43,7 +43,7 @@ extension _LLMFunctionParameterWrapper where T: AnyOptional, T.Wrapped: LLMFunct type: .string, description: String(description), const: const.map { String($0) }, - enumValues: T.Wrapped.allCases.map { String($0.rawValue) } + enum: T.Wrapped.allCases.map { String($0.rawValue) } )) } } @@ -70,7 +70,7 @@ extension _LLMFunctionParameterWrapper where T: AnyArray, T.Element: LLMFunction items: .init( type: .string, const: const.map { String($0) }, - enumValues: T.Element.allCases.map { String($0.rawValue) } + enum: T.Element.allCases.map { String($0.rawValue) } ), minItems: minItems, maxItems: maxItems, @@ -104,7 +104,7 @@ extension _LLMFunctionParameterWrapper where T: AnyOptional, items: .init( type: .string, const: const.map { String($0) }, - enumValues: T.Wrapped.Element.allCases.map { String($0.rawValue) } + enum: T.Wrapped.Element.allCases.map { String($0.rawValue) } ), minItems: minItems, maxItems: maxItems, diff --git a/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper+OptionalTypes.swift b/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper+OptionalTypes.swift index db497fd..a3ff348 100644 --- a/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper+OptionalTypes.swift +++ b/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper+OptionalTypes.swift @@ -87,13 +87,13 @@ extension _LLMFunctionParameterWrapper where T: AnyOptional, T.Wrapped: StringPr /// - format: Defines a required format of the parameter, allowing interoperable semantic validation of the value. /// - pattern: A Regular Expression that the parameter needs to conform to. /// - const: Specifies the constant `String`-based value of a certain parameter. - /// - enumValues: Defines all cases of the `String` parameter. + /// - enum: Defines all cases of the `String` parameter. public convenience init( description: D, format: _LLMFunctionParameterWrapper.Format? = nil, pattern: (any StringProtocol)? = nil, const: (any StringProtocol)? = nil, - enumValues: [any StringProtocol]? = nil + enum: [any StringProtocol]? = nil ) { self.init(schema: .init( type: .string, @@ -101,7 +101,7 @@ extension _LLMFunctionParameterWrapper where T: AnyOptional, T.Wrapped: StringPr format: format?.rawValue, pattern: pattern.map { String($0) }, const: const.map { String($0) }, - enumValues: enumValues.map { $0.map { String($0) } } + enum: `enum`.map { $0.map { String($0) } } )) } } @@ -218,7 +218,7 @@ extension _LLMFunctionParameterWrapper where T: AnyOptional, T.Wrapped: AnyArray /// - description: Describes the purpose of the parameter, used by the LLM to grasp the purpose of the parameter. /// - pattern: A Regular Expression that the parameter needs to conform to. /// - const: Specifies the constant `String`-based value of a certain parameter. - /// - enumValues: Defines all cases of a single `String` `array` element. + /// - enum: Defines all cases of a single `String` `array` element. /// - minItems: Defines the minimum amount of values in the `array`. /// - maxItems: Defines the maximum amount of values in the `array`. /// - uniqueItems: Specifies if all `array` elements need to be unique. @@ -226,7 +226,7 @@ extension _LLMFunctionParameterWrapper where T: AnyOptional, T.Wrapped: AnyArray description: D, pattern: (any StringProtocol)? = nil, const: (any StringProtocol)? = nil, - enumValues: [any StringProtocol]? = nil, + enum: [any StringProtocol]? = nil, minItems: Int? = nil, maxItems: Int? = nil, uniqueItems: Bool? = nil @@ -238,7 +238,7 @@ extension _LLMFunctionParameterWrapper where T: AnyOptional, T.Wrapped: AnyArray type: .string, pattern: pattern.map { String($0) }, const: const.map { String($0) }, - enumValues: enumValues.map { $0.map { String($0) } } + enum: `enum`.map { $0.map { String($0) } } ), minItems: minItems, maxItems: maxItems, diff --git a/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper+PrimitiveTypes.swift b/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper+PrimitiveTypes.swift index 232447f..ab45df6 100644 --- a/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper+PrimitiveTypes.swift +++ b/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper+PrimitiveTypes.swift @@ -86,13 +86,13 @@ extension _LLMFunctionParameterWrapper where T: StringProtocol { /// - format: Defines a required format of the parameter, allowing interoperable semantic validation of the value. /// - pattern: A Regular Expression that the parameter needs to conform to. /// - const: Specifies the constant `String`-based value of a certain parameter. - /// - enumValues: Defines all cases of the `String` parameter. + /// - enum: Defines all cases of the `String` parameter. public convenience init( description: D, format: _LLMFunctionParameterWrapper.Format? = nil, pattern: (any StringProtocol)? = nil, const: (any StringProtocol)? = nil, - enumValues: [any StringProtocol]? = nil + enum: [any StringProtocol]? = nil ) { self.init(schema: .init( type: .string, @@ -100,7 +100,7 @@ extension _LLMFunctionParameterWrapper where T: StringProtocol { format: format?.rawValue, pattern: pattern.map { String($0) }, const: const.map { String($0) }, - enumValues: enumValues.map { $0.map { String($0) } } + enum: `enum`.map { $0.map { String($0) } } )) } } diff --git a/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper.swift b/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper.swift index cb63d34..7a17f20 100644 --- a/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper.swift +++ b/Sources/SpeziLLMOpenAI/FunctionCalling/LLMFunctionParameterWrapper.swift @@ -10,9 +10,9 @@ import OpenAI /// Alias of the OpenAI `JSONSchema/Property` type, describing properties within an object schema. -public typealias LLMFunctionParameterPropertySchema = JSONSchema.Property +public typealias LLMFunctionParameterPropertySchema = ChatQuery.ChatCompletionToolParam.FunctionDefinition.FunctionParameters.Property /// Alias of the OpenAI `JSONSchema/Item` type, describing array items within an array schema. -public typealias LLMFunctionParameterItemSchema = JSONSchema.Items +public typealias LLMFunctionParameterItemSchema = ChatQuery.ChatCompletionToolParam.FunctionDefinition.FunctionParameters.Property.Items /// Refer to the documentation of ``LLMFunction/Parameter`` for information on how to use the `@Parameter` property wrapper. @@ -59,7 +59,7 @@ public class _LLMFunctionParameterWrapper: LLMFunctionParameterSch required: T.schema.required, pattern: T.schema.pattern, const: T.schema.const, - enumValues: T.schema.enumValues, + enum: T.schema.enum, multipleOf: T.schema.multipleOf, minimum: T.schema.minimum, maximum: T.schema.maximum, @@ -108,3 +108,8 @@ extension LLMFunction { public typealias Parameter = _LLMFunctionParameterWrapper where WrappedValue: Decodable } + + +/// Ensuring `Sendable` conformances of ``LLMFunctionParameterPropertySchema`` and ``LLMFunctionParameterItemSchema`` +extension LLMFunctionParameterPropertySchema: @unchecked Sendable {} +extension LLMFunctionParameterItemSchema: @unchecked Sendable {} diff --git a/Sources/SpeziLLMOpenAI/Helpers/Chat+OpenAI.swift b/Sources/SpeziLLMOpenAI/Helpers/Chat+OpenAI.swift index c7396f4..f60772b 100644 --- a/Sources/SpeziLLMOpenAI/Helpers/Chat+OpenAI.swift +++ b/Sources/SpeziLLMOpenAI/Helpers/Chat+OpenAI.swift @@ -6,18 +6,21 @@ // SPDX-License-Identifier: MIT // -import SpeziChat -import struct OpenAI.Chat +import struct OpenAI.ChatQuery +import SpeziLLM -extension SpeziChat.ChatEntity.Role { - /// Maps the `SpeziChat/ChatEntity/Role`s to the `OpenAI/Chat/Role`s. - var openAIRepresentation: OpenAI.Chat.Role { +extension LLMContextEntity.Role { + typealias Role = ChatQuery.ChatCompletionMessageParam.Role + + + /// Maps the `LLMContextEntity/Role`s to the `OpenAI/Chat/Role`s. + var openAIRepresentation: Role { switch self { case .assistant: .assistant case .user: .user case .system: .system - case .function: .function + case .tool: .tool } } } diff --git a/Sources/SpeziLLMOpenAI/Helpers/LLMOpenAIFinishReason.swift b/Sources/SpeziLLMOpenAI/Helpers/LLMOpenAIFinishReason.swift deleted file mode 100644 index 8ddd732..0000000 --- a/Sources/SpeziLLMOpenAI/Helpers/LLMOpenAIFinishReason.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// This source file is part of the Stanford Spezi open source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - - -/// Represents possible OpenAI finish reasons in the inference response -/// More documentation can be found in the [OpenAI docs](https://platform.openai.com/docs/guides/text-generation/chat-completions-api) -enum LLMOpenAIFinishReason: String, Decodable { - case stop - case length - case functionCall = "function_call" - case contentFilter = "content_filter" - case null -} diff --git a/Sources/SpeziLLMOpenAI/Helpers/LLMOpenAIStreamResult.swift b/Sources/SpeziLLMOpenAI/Helpers/LLMOpenAIStreamResult.swift index 558d460..f63f20b 100644 --- a/Sources/SpeziLLMOpenAI/Helpers/LLMOpenAIStreamResult.swift +++ b/Sources/SpeziLLMOpenAI/Helpers/LLMOpenAIStreamResult.swift @@ -11,36 +11,36 @@ import OpenAI /// Helper to process the returned stream by the LLM output generation call, especially in regards to the function call and a possible stop reason struct LLMOpenAIStreamResult { + typealias Role = ChatQuery.ChatCompletionMessageParam.Role + typealias FinishReason = ChatStreamResult.Choice.FinishReason + + struct FunctionCall { + var id: String? var name: String? var arguments: String? - init(name: String? = nil, arguments: String? = nil) { + init(name: String? = nil, id: String? = nil, arguments: String? = nil) { self.name = name + self.id = id self.arguments = arguments } } var deltaContent: String? - var role: Chat.Role? - var functionCall: FunctionCall? - private var finishReasonBase: String? - var finishReason: LLMOpenAIFinishReason { - guard let finishReasonBase else { - return .null - } - - return .init(rawValue: finishReasonBase) ?? .null - } + var role: Role? + var finishReason: FinishReason? + var functionCall: [FunctionCall] + var currentFunctionCallIndex = -1 - init(deltaContent: String? = nil, role: Chat.Role? = nil, functionCall: FunctionCall? = nil, finishReason: String? = nil) { + init(deltaContent: String? = nil, role: Role? = nil, finishReason: FinishReason? = nil, functionCall: [FunctionCall] = []) { self.deltaContent = deltaContent self.role = role + self.finishReason = finishReason self.functionCall = functionCall - self.finishReasonBase = finishReason } @@ -50,24 +50,39 @@ struct LLMOpenAIStreamResult { if let role = choice.delta.role { self.role = role } + + if let finishReason = choice.finishReason { + self.finishReason = finishReason + } + + guard let functionCallId = choice.delta.toolCalls?.last?.index else { + return self + } + + if functionCallId != currentFunctionCallIndex { + functionCall.append(FunctionCall()) + currentFunctionCallIndex += 1 + } + + var newFunctionCall = functionCall[currentFunctionCallIndex] + + if let functionCallId = choice.delta.toolCalls?.first?.id { + newFunctionCall.id = (newFunctionCall.id ?? "") + functionCallId + } - var newFunctionCall = self.functionCall ?? FunctionCall() - - if let deltaName = choice.delta.functionCall?.name { - newFunctionCall.name = (self.functionCall?.name ?? "") + deltaName + if let deltaName = choice.delta.toolCalls?.first?.function?.name { + newFunctionCall.name = (newFunctionCall.name ?? "") + deltaName } - if let deltaArguments = choice.delta.functionCall?.arguments { - newFunctionCall.arguments = (self.functionCall?.arguments ?? "") + deltaArguments + if let deltaArguments = choice.delta.toolCalls?.first?.function?.arguments { + newFunctionCall.arguments = (newFunctionCall.arguments ?? "") + deltaArguments } // Only assign back if there were changes - if choice.delta.functionCall?.name != nil || choice.delta.functionCall?.arguments != nil { - self.functionCall = newFunctionCall - } - - if let finishReasonBase = choice.finishReason { - self.finishReasonBase = (self.finishReasonBase ?? "") + finishReasonBase + if choice.delta.toolCalls?.first?.id != nil || + choice.delta.toolCalls?.first?.function?.name != nil || + choice.delta.toolCalls?.first?.function?.arguments != nil { + functionCall[currentFunctionCallIndex] = newFunctionCall } return self diff --git a/Sources/SpeziLLMOpenAI/LLMOpenAIError.swift b/Sources/SpeziLLMOpenAI/LLMOpenAIError.swift index 2510b3a..8eb17ad 100644 --- a/Sources/SpeziLLMOpenAI/LLMOpenAIError.swift +++ b/Sources/SpeziLLMOpenAI/LLMOpenAIError.swift @@ -32,8 +32,6 @@ public enum LLMOpenAIError: LLMError { case invalidFunctionCallArguments(Error) /// Exception during function call execution case functionCallError(Error) - /// Unknown error - case unknownError(Error) /// Maps the enum cases to error message from the OpenAI API @@ -67,8 +65,6 @@ public enum LLMOpenAIError: LLMError { String(localized: LocalizedStringResource("LLM_INVALID_FUNCTION_ARGUMENTS_ERROR_DESCRIPTION", bundle: .atURL(from: .module))) case .functionCallError: String(localized: LocalizedStringResource("LLM_FUNCTION_CALL_ERROR_DESCRIPTION", bundle: .atURL(from: .module))) - case .unknownError: - String(localized: LocalizedStringResource("LLM_UNKNOWN_ERROR_DESCRIPTION", bundle: .atURL(from: .module))) } } @@ -94,8 +90,6 @@ public enum LLMOpenAIError: LLMError { String(localized: LocalizedStringResource("LLM_INVALID_FUNCTION_ARGUMENTS_RECOVERY_SUGGESTION", bundle: .atURL(from: .module))) case .functionCallError: String(localized: LocalizedStringResource("LLM_FUNCTION_CALL_ERROR_RECOVERY_SUGGESTION", bundle: .atURL(from: .module))) - case .unknownError: - String(localized: LocalizedStringResource("LLM_UNKNOWN_ERROR_RECOVERY_SUGGESTION", bundle: .atURL(from: .module))) } } @@ -121,8 +115,6 @@ public enum LLMOpenAIError: LLMError { String(localized: LocalizedStringResource("LLM_INVALID_FUNCTION_ARGUMENTS_FAILURE_REASON", bundle: .atURL(from: .module))) case .functionCallError: String(localized: LocalizedStringResource("LLM_FUNCTION_CALL_ERROR_FAILURE_REASON", bundle: .atURL(from: .module))) - case .unknownError: - String(localized: LocalizedStringResource("LLM_UNKNOWN_ERROR_FAILURE_REASON", bundle: .atURL(from: .module))) } } @@ -139,7 +131,6 @@ public enum LLMOpenAIError: LLMError { case (.invalidFunctionCallName, .invalidFunctionCallName): true case (.invalidFunctionCallArguments, .invalidFunctionCallArguments): true case (.functionCallError, .functionCallError): true - case (.unknownError, .unknownError): true default: false } } diff --git a/Sources/SpeziLLMOpenAI/LLMOpenAISession+Configuration.swift b/Sources/SpeziLLMOpenAI/LLMOpenAISession+Configuration.swift index 834a96c..1a97757 100644 --- a/Sources/SpeziLLMOpenAI/LLMOpenAISession+Configuration.swift +++ b/Sources/SpeziLLMOpenAI/LLMOpenAISession+Configuration.swift @@ -10,54 +10,80 @@ import OpenAI extension LLMOpenAISession { - /// Map the ``LLMOpenAI/context`` to the OpenAI `[Chat]` representation. + typealias Chat = ChatQuery.ChatCompletionMessageParam + typealias FunctionDeclaration = ChatQuery.ChatCompletionToolParam + + + /// Map the ``LLMOpenAISession/context`` to the OpenAI `[ChatQuery.ChatCompletionMessageParam]` representation. private var openAIContext: [Chat] { get async { - await self.context.map { chatEntity in - if case let .function(name: functionName) = chatEntity.role { - return Chat( - role: chatEntity.role.openAIRepresentation, - content: chatEntity.content, - name: functionName + await self.context.compactMap { contextEntity in + if case let .tool(id: functionId, name: functionName) = contextEntity.role { + Chat( + role: contextEntity.role.openAIRepresentation, + content: contextEntity.content, + name: functionName, + toolCallId: functionId ) + } else if case let .assistant(toolCalls: toolCalls) = contextEntity.role { + // No function calls present -> regular assistant message + if toolCalls.isEmpty { + Chat( + role: contextEntity.role.openAIRepresentation, + content: contextEntity.content + ) + // Function calls present + } else { + Chat( + role: contextEntity.role.openAIRepresentation, + toolCalls: toolCalls.map { toolCall in + .init( + id: toolCall.id, + function: .init(arguments: toolCall.arguments, name: toolCall.name) + ) + } + ) + } } else { - return Chat( - role: chatEntity.role.openAIRepresentation, - content: chatEntity.content + Chat( + role: contextEntity.role.openAIRepresentation, + content: contextEntity.content ) } } } } - /// Provides the ``LLMOpenAI/context``, the `` LLMOpenAIParameters`` and ``LLMOpenAIModelParameters``, as well as the declared ``LLMFunction``s + /// Provides the ``LLMOpenAISession/context``, the `` LLMOpenAIParameters`` and ``LLMOpenAIModelParameters``, as well as the declared ``LLMFunction``s /// in an OpenAI `ChatQuery` representation used for querying the OpenAI API. var openAIChatQuery: ChatQuery { get async { - let functions: [ChatFunctionDeclaration] = schema.functions.values.compactMap { function in + let functions: [FunctionDeclaration] = schema.functions.values.compactMap { function in let functionType = Swift.type(of: function) - return .init( + return .init(function: .init( name: functionType.name, description: functionType.description, parameters: function.schema - ) + )) } - return await .init( - model: schema.parameters.modelType, + return await ChatQuery( messages: self.openAIContext, + model: schema.parameters.modelType, + frequencyPenalty: schema.modelParameters.frequencyPenalty, + logitBias: schema.modelParameters.logitBias.isEmpty ? nil : schema.modelParameters.logitBias, + maxTokens: schema.modelParameters.maxOutputLength, + n: schema.modelParameters.completionsPerOutput, + presencePenalty: schema.modelParameters.presencePenalty, responseFormat: schema.modelParameters.responseFormat, - functions: functions.isEmpty ? nil : functions, + seed: schema.modelParameters.seed, + stop: .stringList(schema.modelParameters.stopSequence), temperature: schema.modelParameters.temperature, + tools: functions.isEmpty ? nil : functions, topP: schema.modelParameters.topP, - n: schema.modelParameters.completionsPerOutput, - stop: schema.modelParameters.stopSequence.isEmpty ? nil : schema.modelParameters.stopSequence, - maxTokens: schema.modelParameters.maxOutputLength, - presencePenalty: schema.modelParameters.presencePenalty, - frequencyPenalty: schema.modelParameters.frequencyPenalty, - logitBias: schema.modelParameters.logitBias.isEmpty ? nil : schema.modelParameters.logitBias, - user: schema.modelParameters.user + user: schema.modelParameters.user, + stream: true ) } } diff --git a/Sources/SpeziLLMOpenAI/LLMOpenAISession+Generation.swift b/Sources/SpeziLLMOpenAI/LLMOpenAISession+Generation.swift index a5eb0d8..93e75ad 100644 --- a/Sources/SpeziLLMOpenAI/LLMOpenAISession+Generation.swift +++ b/Sources/SpeziLLMOpenAI/LLMOpenAISession+Generation.swift @@ -9,6 +9,7 @@ import Foundation import OpenAI import SpeziChat +import SpeziLLM extension LLMOpenAISession { @@ -49,6 +50,11 @@ extension LLMOpenAISession { continue } + guard await !checkCancellation(on: continuation) else { + Self.logger.debug("SpeziLLMOpenAI: LLM inference cancelled because of Task cancellation.") + return + } + // Automatically inject the yielded string piece into the `LLMLocal/context` if schema.injectIntoContext { await MainActor.run { @@ -59,8 +65,10 @@ extension LLMOpenAISession { continuation.yield(content) } - await MainActor.run { - context.completeAssistantStreaming() + if schema.injectIntoContext { + await MainActor.run { + context.completeAssistantStreaming() + } } } catch let error as APIErrorResponse { switch error.error.code { @@ -75,29 +83,48 @@ extension LLMOpenAISession { await finishGenerationWithError(LLMOpenAIError.generationError, on: continuation) } return + } catch let error as URLError { + Self.logger.error("SpeziLLMOpenAI: Connectivity Issues with the OpenAI API: \(error)") + await finishGenerationWithError(LLMOpenAIError.connectivityIssues(error), on: continuation) + return } catch { Self.logger.error("SpeziLLMOpenAI: Generation error occurred - \(error)") await finishGenerationWithError(LLMOpenAIError.generationError, on: continuation) return } - let functionCalls = llmStreamResults.values.compactMap { $0.functionCall } + let functionCalls = llmStreamResults.values.compactMap { $0.functionCall }.flatMap { $0 } // Exit the while loop if we don't have any function calls guard !functionCalls.isEmpty else { break } + // Inject the requested function calls into the LLM context + let functionCallContext: [LLMContextEntity.ToolCall] = functionCalls.compactMap { functionCall in + guard let functionCallId = functionCall.id, + let functionCallName = functionCall.name else { + return nil + } + + return .init(id: functionCallId, name: functionCallName, arguments: functionCall.arguments ?? "") + } + await MainActor.run { + context.append(functionCalls: functionCallContext) + } + // Parallelize function call execution do { try await withThrowingTaskGroup(of: Void.self) { group in // swiftlint:disable:this closure_body_length for functionCall in functionCalls { group.addTask { // swiftlint:disable:this closure_body_length Self.logger.debug(""" - SpeziLLMOpenAI: Function call \(functionCall.name ?? ""), Arguments: \(functionCall.arguments ?? "") + SpeziLLMOpenAI: Function call \(functionCall.name ?? "") + Arguments: \(functionCall.arguments ?? "") """) guard let functionName = functionCall.name, + let functionID = functionCall.id, let functionArgument = functionCall.arguments?.data(using: .utf8), let function = self.schema.functions[functionName] else { Self.logger.debug("SpeziLLMOpenAI: Couldn't find the requested function to call") @@ -120,6 +147,12 @@ extension LLMOpenAISession { // Execute function // Errors thrown by the functions are surfaced to the user as an LLM generation error functionCallResponse = try await function.execute() + } catch is CancellationError { + guard await !self.checkCancellation(on: continuation) else { + Self.logger.debug("SpeziLLMOpenAI: Function call execution cancelled because of Task cancellation.") + throw CancellationError() + } + return } catch { Self.logger.error("SpeziLLMOpenAI: Function call execution error - \(error)") await self.finishGenerationWithError(LLMOpenAIError.functionCallError(error), on: continuation) @@ -127,8 +160,8 @@ extension LLMOpenAISession { } Self.logger.debug(""" - SpeziLLMOpenAI: Function call \(functionCall.name ?? "") \ - Arguments: \(functionCall.arguments ?? "") \ + SpeziLLMOpenAI: Function call \(functionCall.name ?? "") + Arguments: \(functionCall.arguments ?? "") Response: \(functionCallResponse ?? "") """) @@ -138,6 +171,7 @@ extension LLMOpenAISession { // Return `defaultResponse` in case of `nil` or empty return of the function call self.context.append( forFunction: functionName, + withID: functionID, response: functionCallResponse?.isEmpty != false ? defaultResponse : (functionCallResponse ?? defaultResponse) ) } diff --git a/Sources/SpeziLLMOpenAI/LLMOpenAISession.swift b/Sources/SpeziLLMOpenAI/LLMOpenAISession.swift index 561d3df..d9e0e6a 100644 --- a/Sources/SpeziLLMOpenAI/LLMOpenAISession.swift +++ b/Sources/SpeziLLMOpenAI/LLMOpenAISession.swift @@ -7,13 +7,7 @@ // import Foundation -import struct OpenAI.Chat -import struct OpenAI.ChatFunctionDeclaration -import struct OpenAI.ChatQuery import class OpenAI.OpenAI -import struct OpenAI.Model -import struct OpenAI.ChatStreamResult -import struct OpenAI.APIErrorResponse import os import SpeziChat import SpeziLLM @@ -80,9 +74,10 @@ public final class LLMOpenAISession: LLMSession, @unchecked Sendable { @ObservationIgnored private var lock = NSLock() @MainActor public var state: LLMState = .uninitialized - @MainActor public var context: SpeziChat.Chat = [] + @MainActor public var context: LLMContext = [] @ObservationIgnored var wrappedModel: OpenAI? + var model: OpenAI { guard let model = wrappedModel else { preconditionFailure(""" diff --git a/Sources/SpeziLLMOpenAI/Resources/Localizable.xcstrings b/Sources/SpeziLLMOpenAI/Resources/Localizable.xcstrings index 7160c54..90b2c5b 100644 --- a/Sources/SpeziLLMOpenAI/Resources/Localizable.xcstrings +++ b/Sources/SpeziLLMOpenAI/Resources/Localizable.xcstrings @@ -301,36 +301,6 @@ } } }, - "LLM_UNKNOWN_ERROR_DESCRIPTION" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "An unknown OpenAI error has occured." - } - } - } - }, - "LLM_UNKNOWN_ERROR_FAILURE_REASON" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The OpenAI API responded with an unknown error." - } - } - } - }, - "LLM_UNKNOWN_ERROR_RECOVERY_SUGGESTION" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Please retry the query." - } - } - } - }, "OPENAI_API_KEY_PROMPT" : { "localizations" : { "de" : { diff --git a/Sources/SpeziLLMOpenAI/SpeziLLMOpenAI.docc/FunctionCalling.md b/Sources/SpeziLLMOpenAI/SpeziLLMOpenAI.docc/FunctionCalling.md index 64edf79..e6127b0 100644 --- a/Sources/SpeziLLMOpenAI/SpeziLLMOpenAI.docc/FunctionCalling.md +++ b/Sources/SpeziLLMOpenAI/SpeziLLMOpenAI.docc/FunctionCalling.md @@ -109,7 +109,7 @@ struct LLMOpenAIFunctionHealthData: LLMFunction { static let name: String = "get_health_data" static let description: String = "Get the health data of a patient based on health data types." - @Parameter(description: "The types of health data that are requested", enumValues: ["allergies", "medications"]) + @Parameter(description: "The types of health data that are requested", enum: ["allergies", "medications"]) var healthDataTypes: [String] // Use an `array` of `String`s as parameter func execute() async throws -> String? { diff --git a/Sources/SpeziLLMOpenAI/SpeziLLMOpenAI.docc/SpeziLLMOpenAI.md b/Sources/SpeziLLMOpenAI/SpeziLLMOpenAI.docc/SpeziLLMOpenAI.md index e04e254..cecc620 100644 --- a/Sources/SpeziLLMOpenAI/SpeziLLMOpenAI.docc/SpeziLLMOpenAI.md +++ b/Sources/SpeziLLMOpenAI/SpeziLLMOpenAI.docc/SpeziLLMOpenAI.md @@ -85,8 +85,6 @@ The ``LLMOpenAISchema`` defines the type and configurations of the to-be-execute The ``LLMOpenAISession`` contains the ``LLMOpenAISession/context`` property which holds the entire history of the model interactions. This includes the system prompt, user input, but also assistant responses. Ensure the property always contains all necessary information, as the ``LLMOpenAISession/generate()`` function executes the inference based on the ``LLMOpenAISession/context`` -> Important: The OpenAI LLM abstractions should only be used together with the [SpeziLLM](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillm) [`LLMRunner`](https://swiftpackageindex.com/stanfordspezi/spezillm/documentation/spezillm/llmrunner)! - ```swift struct LLMOpenAIDemoView: View { @Environment(LLMRunner.self) var runner @@ -162,7 +160,7 @@ Now the OpenAI API Key entry view will appear within your application's onboardi ## Topics -### LLM Local abstraction +### LLM OpenAI abstraction - ``LLMOpenAISchema`` - ``LLMOpenAISession`` diff --git a/Tests/SpeziLLMTests/LLMOpenAIParameterTests+Array.swift b/Tests/SpeziLLMTests/LLMOpenAIParameterTests+Array.swift index 2e7fde6..59c3265 100644 --- a/Tests/SpeziLLMTests/LLMOpenAIParameterTests+Array.swift +++ b/Tests/SpeziLLMTests/LLMOpenAIParameterTests+Array.swift @@ -34,7 +34,7 @@ final class LLMOpenAIParameterArrayTests: XCTestCase { var doubleArrayParameter: [Double] @Parameter(description: "Bool Array Parameter", const: "true") var boolArrayParameter: [Bool] - @Parameter(description: "String Array Parameter", pattern: "/d/d/d/d", enumValues: ["1234", "5678"]) + @Parameter(description: "String Array Parameter", pattern: "/d/d/d/d", enum: ["1234", "5678"]) var stringArrayParameter: [String] // swiftlint:enable attributes @@ -100,7 +100,7 @@ final class LLMOpenAIParameterArrayTests: XCTestCase { XCTAssertEqual(schemaArrayString.schema.description, "String Array Parameter") XCTAssertEqual(schemaArrayString.schema.items?.type, .string) XCTAssertEqual(schemaArrayString.schema.items?.pattern, "/d/d/d/d") - XCTAssertEqual(schemaArrayString.schema.items?.enumValues, ["1234", "5678"]) + XCTAssertEqual(schemaArrayString.schema.items?.enum, ["1234", "5678"]) // Validate parameter injection let parameterData = try XCTUnwrap( diff --git a/Tests/SpeziLLMTests/LLMOpenAIParameterTests+Enum.swift b/Tests/SpeziLLMTests/LLMOpenAIParameterTests+Enum.swift index 0b6106f..4c96b08 100644 --- a/Tests/SpeziLLMTests/LLMOpenAIParameterTests+Enum.swift +++ b/Tests/SpeziLLMTests/LLMOpenAIParameterTests+Enum.swift @@ -83,12 +83,12 @@ final class LLMOpenAIParameterEnumTests: XCTestCase { XCTAssertEqual(schemaEnum.schema.type, .string) XCTAssertEqual(schemaEnum.schema.description, "Enum Parameter") XCTAssertEqual(schemaEnum.schema.const, "optionA") - XCTAssertEqual(schemaEnum.schema.enumValues, CustomEnumType.allCases.map { $0.rawValue }) + XCTAssertEqual(schemaEnum.schema.enum, CustomEnumType.allCases.map { $0.rawValue }) let schemaOptionalEnum = try XCTUnwrap(llmFunction.schemaValueCollectors["optionalEnumParameter"]) XCTAssertEqual(schemaOptionalEnum.schema.type, .string) XCTAssertEqual(schemaOptionalEnum.schema.description, "Optional Enum Parameter") - XCTAssertEqual(schemaOptionalEnum.schema.enumValues, CustomEnumType.allCases.map { $0.rawValue }) + XCTAssertEqual(schemaOptionalEnum.schema.enum, CustomEnumType.allCases.map { $0.rawValue }) let schemaArrayEnum = try XCTUnwrap(llmFunction.schemaValueCollectors["arrayEnumParameter"]) XCTAssertEqual(schemaArrayEnum.schema.type, .array) @@ -97,13 +97,13 @@ final class LLMOpenAIParameterEnumTests: XCTestCase { XCTAssertEqual(schemaArrayEnum.schema.maxItems, 5) XCTAssertFalse(schemaArrayEnum.schema.uniqueItems ?? true) XCTAssertEqual(schemaArrayEnum.schema.items?.type, .string) - XCTAssertEqual(schemaArrayEnum.schema.items?.enumValues, CustomEnumType.allCases.map { $0.rawValue }) + XCTAssertEqual(schemaArrayEnum.schema.items?.enum, CustomEnumType.allCases.map { $0.rawValue }) let schemaOptionalArrayEnum = try XCTUnwrap(llmFunction.schemaValueCollectors["optionalArrayEnumParameter"]) XCTAssertEqual(schemaOptionalArrayEnum.schema.type, .array) XCTAssertEqual(schemaOptionalArrayEnum.schema.description, "Optional Array Enum Parameter") XCTAssertEqual(schemaOptionalArrayEnum.schema.items?.type, .string) - XCTAssertEqual(schemaOptionalArrayEnum.schema.items?.enumValues, CustomEnumType.allCases.map { $0.rawValue }) + XCTAssertEqual(schemaOptionalArrayEnum.schema.items?.enum, CustomEnumType.allCases.map { $0.rawValue }) // Validate parameter injection let parameterData = try XCTUnwrap( diff --git a/Tests/SpeziLLMTests/LLMOpenAIParameterTests+OptionalTypes.swift b/Tests/SpeziLLMTests/LLMOpenAIParameterTests+OptionalTypes.swift index 6be2db7..4604ecf 100644 --- a/Tests/SpeziLLMTests/LLMOpenAIParameterTests+OptionalTypes.swift +++ b/Tests/SpeziLLMTests/LLMOpenAIParameterTests+OptionalTypes.swift @@ -38,7 +38,7 @@ final class LLMOpenAIParameterOptionalTypesTests: XCTestCase { var doubleParameter: Double? @Parameter(description: "Optional Bool Parameter", const: "false") var boolParameter: Bool? - @Parameter(description: "Optional String Parameter", format: .datetime, pattern: "/d/d/d/d", enumValues: ["1234", "5678"]) + @Parameter(description: "Optional String Parameter", format: .datetime, pattern: "/d/d/d/d", enum: ["1234", "5678"]) var stringParameter: String? @Parameter(description: "Optional Int Array Parameter", minItems: 1, maxItems: 9, uniqueItems: true) var intArrayParameter: [Int]? @@ -46,7 +46,7 @@ final class LLMOpenAIParameterOptionalTypesTests: XCTestCase { var doubleArrayParameter: [Double]? @Parameter(description: "Optional Bool Array Parameter", const: "true") var boolArrayParameter: [Bool]? - @Parameter(description: "Optional String Array Parameter", pattern: "/d/d/d/d", enumValues: ["1234", "5678"]) + @Parameter(description: "Optional String Array Parameter", pattern: "/d/d/d/d", enum: ["1234", "5678"]) var stringArrayParameter: [String]? @Parameter(description: "Optional String Array Nil Parameter") var arrayNilParameter: [String]? @@ -119,7 +119,7 @@ final class LLMOpenAIParameterOptionalTypesTests: XCTestCase { XCTAssertEqual(schemaOptionalString.schema.description, "Optional String Parameter") XCTAssertEqual(schemaOptionalString.schema.format, "date-time") XCTAssertEqual(schemaOptionalString.schema.pattern, "/d/d/d/d") - XCTAssertEqual(schemaOptionalString.schema.enumValues, ["1234", "5678"]) + XCTAssertEqual(schemaOptionalString.schema.enum, ["1234", "5678"]) let schemaArrayInt = try XCTUnwrap(llmFunction.schemaValueCollectors["intArrayParameter"]) XCTAssertEqual(schemaArrayInt.schema.type, .array) @@ -147,7 +147,7 @@ final class LLMOpenAIParameterOptionalTypesTests: XCTestCase { XCTAssertEqual(schemaArrayString.schema.description, "Optional String Array Parameter") XCTAssertEqual(schemaArrayString.schema.items?.type, .string) XCTAssertEqual(schemaArrayString.schema.items?.pattern, "/d/d/d/d") - XCTAssertEqual(schemaArrayString.schema.items?.enumValues, ["1234", "5678"]) + XCTAssertEqual(schemaArrayString.schema.items?.enum, ["1234", "5678"]) // Validate parameter injection let parameterData = try XCTUnwrap( diff --git a/Tests/SpeziLLMTests/LLMOpenAIParameterTests+PrimitiveTypes.swift b/Tests/SpeziLLMTests/LLMOpenAIParameterTests+PrimitiveTypes.swift index 74a0e50..7f285ea 100644 --- a/Tests/SpeziLLMTests/LLMOpenAIParameterTests+PrimitiveTypes.swift +++ b/Tests/SpeziLLMTests/LLMOpenAIParameterTests+PrimitiveTypes.swift @@ -34,7 +34,7 @@ final class LLMOpenAIParameterPrimitiveTypesTests: XCTestCase { var doubleParameter: Double @Parameter(description: "Primitive Bool Parameter", const: "false") var boolParameter: Bool - @Parameter(description: "Primitive String Parameter", format: .datetime, pattern: "/d/d/d/d", enumValues: ["1234", "5678"]) + @Parameter(description: "Primitive String Parameter", format: .datetime, pattern: "/d/d/d/d", enum: ["1234", "5678"]) var stringParameter: String // swiftlint:enable attributes @@ -95,7 +95,7 @@ final class LLMOpenAIParameterPrimitiveTypesTests: XCTestCase { XCTAssertEqual(schemaPrimitiveString.schema.description, "Primitive String Parameter") XCTAssertEqual(schemaPrimitiveString.schema.format, "date-time") XCTAssertEqual(schemaPrimitiveString.schema.pattern, "/d/d/d/d") - XCTAssertEqual(schemaPrimitiveString.schema.enumValues, ["1234", "5678"]) + XCTAssertEqual(schemaPrimitiveString.schema.enum, ["1234", "5678"]) // Validate parameter injection let parameterData = try XCTUnwrap( diff --git a/Tests/UITests/TestApp/FeatureFlags.swift b/Tests/UITests/TestApp/FeatureFlags.swift index 4d674fe..383d0b7 100644 --- a/Tests/UITests/TestApp/FeatureFlags.swift +++ b/Tests/UITests/TestApp/FeatureFlags.swift @@ -14,4 +14,6 @@ enum FeatureFlags: Sendable { static let mockMode = ProcessInfo.processInfo.arguments.contains("--mockMode") /// Resets all credentials in Secure Storage when the application is launched in order to facilitate testing of OpenAI API keys. static let resetSecureStorage = ProcessInfo.processInfo.arguments.contains("--resetSecureStorage") + /// Always show the onboarding when the application is launched. Makes it easy to modify and test the onboarding flow without the need to manually remove the application or reset the simulator. + static let showOnboarding = ProcessInfo.processInfo.arguments.contains("--showOnboarding") } diff --git a/Tests/UITests/TestApp/LLMFog/Account/AccountSetupHeader.swift b/Tests/UITests/TestApp/LLMFog/Account/AccountSetupHeader.swift new file mode 100644 index 0000000..451b193 --- /dev/null +++ b/Tests/UITests/TestApp/LLMFog/Account/AccountSetupHeader.swift @@ -0,0 +1,37 @@ +// +// This source file is part of the Stanford Spezi Template Application open-source project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +#if os(iOS) +import SpeziAccount +import SwiftUI + + +struct AccountSetupHeader: View { + @Environment(Account.self) private var account + @Environment(\._accountSetupState) private var setupState + + + var body: some View { + VStack { + Text("ACCOUNT_TITLE") + .font(.largeTitle) + .bold() + .padding(.bottom) + .padding(.top, 30) + Text("ACCOUNT_SUBTITLE") + .padding(.bottom, 8) + if account.signedIn, case .generic = setupState { + Text("ACCOUNT_SIGNED_IN_DESCRIPTION") + } else { + Text("ACCOUNT_SETUP_DESCRIPTION") + } + } + .multilineTextAlignment(.center) + } +} +#endif diff --git a/Tests/UITests/TestApp/LLMFog/Account/AccountSheet.swift b/Tests/UITests/TestApp/LLMFog/Account/AccountSheet.swift new file mode 100644 index 0000000..16740a2 --- /dev/null +++ b/Tests/UITests/TestApp/LLMFog/Account/AccountSheet.swift @@ -0,0 +1,64 @@ +// +// This source file is part of the Stanford Spezi Template Application open-source project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +#if os(iOS) +import SpeziAccount +import SwiftUI + + +struct AccountSheet: View { + @Environment(\.dismiss) var dismiss + + @Environment(Account.self) private var account + @Environment(\.accountRequired) var accountRequired + + @State var isInSetup = false + @State var overviewIsEditing = false + + + var body: some View { + NavigationStack { + ZStack { + if account.signedIn && !isInSetup { + AccountOverview(isEditing: $overviewIsEditing) + .onDisappear { + overviewIsEditing = false + } + .toolbar { + if !overviewIsEditing { + closeButton + } + } + } else { + AccountSetup { _ in + dismiss() // we just signed in, dismiss the account setup sheet + } header: { + AccountSetupHeader() + } + .onAppear { + isInSetup = true + } + .toolbar { + if !accountRequired { + closeButton + } + } + } + } + } + } + + var closeButton: some ToolbarContent { + ToolbarItem(placement: .cancellationAction) { + Button("CLOSE") { + dismiss() + } + } + } +} +#endif diff --git a/Tests/UITests/TestApp/LLMFog/LLMFogChatTestView.swift b/Tests/UITests/TestApp/LLMFog/LLMFogChatTestView.swift new file mode 100644 index 0000000..afd1ae7 --- /dev/null +++ b/Tests/UITests/TestApp/LLMFog/LLMFogChatTestView.swift @@ -0,0 +1,62 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +#if os(iOS) +import FirebaseAuth +import SpeziAccount +#endif +import SpeziChat +import SpeziLLM +import SpeziLLMFog +import SwiftUI + + +struct LLMFogChatTestView: View { + static let schema = LLMFogSchema( + parameters: .init( + modelType: .gemma2B, + systemPrompt: "You're a helpful assistant that answers questions from users.", + authToken: { + // As SpeziAccount, SpeziFirebase and the firebase-ios-sdk currently don't support visionOS and macOS, perform fog node token authentication only on iOS + #if os(iOS) + // Get Firebase ID token + try? await Auth.auth().currentUser?.getIDToken() + #else + nil + #endif + } + ) + ) + + @State var showOnboarding = false + #if os(iOS) + @State var presentingAccount = false + #endif + + + var body: some View { + Group { + if FeatureFlags.mockMode { + LLMChatViewSchema(with: LLMMockSchema()) + } else { + LLMChatViewSchema(with: Self.schema) + } + } + .navigationTitle("LLM_FOG_CHAT_VIEW_TITLE") + #if os(iOS) + .sheet(isPresented: $presentingAccount) { + AccountSheet() + } + .accountRequired { + AccountSheet() + } + .verifyRequiredAccountDetails() + #endif + .accentColor(.orange) // Fog Orange + } +} diff --git a/Tests/UITests/TestApp/LLMLocal/Onboarding/LLMLocalOnboardingFlow.swift b/Tests/UITests/TestApp/LLMLocal/Onboarding/LLMLocalOnboardingFlow.swift index 104392e..f0a52ac 100644 --- a/Tests/UITests/TestApp/LLMLocal/Onboarding/LLMLocalOnboardingFlow.swift +++ b/Tests/UITests/TestApp/LLMLocal/Onboarding/LLMLocalOnboardingFlow.swift @@ -23,7 +23,7 @@ struct LLMLocalOnboardingFlow: View { LLMLocalOnboardingDownloadView() } } - .interactiveDismissDisabled(!completedOnboardingFlow) + .interactiveDismissDisabled(!completedOnboardingFlow) } } diff --git a/Tests/UITests/TestApp/LLMOpenAI/Functions/LLMOpenAIFunctionHealthData.swift b/Tests/UITests/TestApp/LLMOpenAI/Functions/LLMOpenAIFunctionHealthData.swift index 7184330..767af0d 100644 --- a/Tests/UITests/TestApp/LLMOpenAI/Functions/LLMOpenAIFunctionHealthData.swift +++ b/Tests/UITests/TestApp/LLMOpenAI/Functions/LLMOpenAIFunctionHealthData.swift @@ -15,7 +15,7 @@ struct LLMOpenAIFunctionHealthData: LLMFunction { // swiftlint:disable attributes - @Parameter(description: "The types of health data that are requested", enumValues: ["allergies", "medications", "preconditions"]) + @Parameter(description: "The types of health data that are requested", enum: ["allergies", "medications", "preconditions"]) var healthDataTypes: [String] // swiftlint:enable attributes diff --git a/Tests/UITests/TestApp/LLMOpenAI/LLMOpenAIChatTestView.swift b/Tests/UITests/TestApp/LLMOpenAI/LLMOpenAIChatTestView.swift index dad9e56..8f17ddc 100644 --- a/Tests/UITests/TestApp/LLMOpenAI/LLMOpenAIChatTestView.swift +++ b/Tests/UITests/TestApp/LLMOpenAI/LLMOpenAIChatTestView.swift @@ -39,7 +39,7 @@ struct LLMOpenAIChatTestView: View { // Otherwise use the LLMChatView and pass a LLMSession Binding in there. Use the @LLMSessionProvider wrapper to instantiate the LLMSession LLMChatView(session: $llm) - .speak(llm.context, muted: muted) + .speak(llm.context.chat, muted: muted) .speechToolbarButton(muted: $muted) } } diff --git a/Tests/UITests/TestApp/LLMOpenAI/Onboarding/LLMOpenAITokenOnboarding.swift b/Tests/UITests/TestApp/LLMOpenAI/Onboarding/LLMOpenAITokenOnboarding.swift index a64ad91..f1b9fee 100644 --- a/Tests/UITests/TestApp/LLMOpenAI/Onboarding/LLMOpenAITokenOnboarding.swift +++ b/Tests/UITests/TestApp/LLMOpenAI/Onboarding/LLMOpenAITokenOnboarding.swift @@ -13,7 +13,7 @@ import SwiftUI struct LLMOpenAITokenOnboarding: View { @Environment(OnboardingNavigationPath.self) private var path - #if os(macOS) + #if os(visionOS) @Environment(\.dismiss) private var dismiss #endif diff --git a/Tests/UITests/TestApp/Resources/GoogleService-Info.plist b/Tests/UITests/TestApp/Resources/GoogleService-Info.plist new file mode 100644 index 0000000..c5ce156 --- /dev/null +++ b/Tests/UITests/TestApp/Resources/GoogleService-Info.plist @@ -0,0 +1,34 @@ + + + + + CLIENT_ID + CLIENT_ID + REVERSED_CLIENT_ID + REVERSED_CLIENT_ID + API_KEY + API_KEY + GCM_SENDER_ID + GCM_SENDER_ID + PLIST_VERSION + 1 + BUNDLE_ID + edu.stanford.spezillm.testapp + PROJECT_ID + spezillmfog + STORAGE_BUCKET + STORAGE_BUCKET + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:123456789012:ios:1234567890123456789012 + + \ No newline at end of file diff --git a/Tests/UITests/TestApp/Resources/GoogleService-Info.plist.license b/Tests/UITests/TestApp/Resources/GoogleService-Info.plist.license new file mode 100644 index 0000000..7f16969 --- /dev/null +++ b/Tests/UITests/TestApp/Resources/GoogleService-Info.plist.license @@ -0,0 +1,5 @@ +This source file is part of the Stanford Spezi open-source project + +SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) + +SPDX-License-Identifier: MIT \ No newline at end of file diff --git a/Tests/UITests/TestApp/Resources/Localizable.xcstrings b/Tests/UITests/TestApp/Resources/Localizable.xcstrings index 607b62d..faec6ec 100644 --- a/Tests/UITests/TestApp/Resources/Localizable.xcstrings +++ b/Tests/UITests/TestApp/Resources/Localizable.xcstrings @@ -1,6 +1,56 @@ { "sourceLanguage" : "en", "strings" : { + "ACCOUNT_SETUP_DESCRIPTION" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You may login to your existing account. Or create a new one if you don't have one already." + } + } + } + }, + "ACCOUNT_SIGNED_IN_DESCRIPTION" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You are already logged in with the account shown below. Continue or change your account by logging out." + } + } + } + }, + "ACCOUNT_SUBTITLE" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The authentication is provided via SpeziAccount and the Firebase Account Module." + } + } + } + }, + "ACCOUNT_TITLE" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your Account" + } + } + } + }, + "CLOSE" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Close" + } + } + } + }, "LLM_DOWNLOAD_DESCRIPTION" : { "localizations" : { "en" : { @@ -11,6 +61,16 @@ } } }, + "LLM_FOG_CHAT_VIEW_TITLE" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "LLM Fog Chat" + } + } + } + }, "LLM_LOCAL_CHAT_VIEW_TITLE" : { "localizations" : { "en" : { diff --git a/Tests/UITests/TestApp/TestApp.swift b/Tests/UITests/TestApp/TestApp.swift index 6af57ce..0dea747 100644 --- a/Tests/UITests/TestApp/TestApp.swift +++ b/Tests/UITests/TestApp/TestApp.swift @@ -15,6 +15,7 @@ struct UITestsApp: App { enum Tests: String, CaseIterable, Identifiable { case llmOpenAI = "LLMOpenAI" case llmLocal = "LLMLocal" + case llmFog = "LLMFog" var id: RawValue { @@ -30,6 +31,8 @@ struct UITestsApp: App { LLMOpenAIChatTestView() case .llmLocal: LLMLocalTestView() + case .llmFog: + LLMFogChatTestView() } } } diff --git a/Tests/UITests/TestApp/TestAppDelegate.swift b/Tests/UITests/TestApp/TestAppDelegate.swift index e7dfc2c..0b86691 100644 --- a/Tests/UITests/TestApp/TestAppDelegate.swift +++ b/Tests/UITests/TestApp/TestAppDelegate.swift @@ -6,22 +6,51 @@ // SPDX-License-Identifier: MIT // +import Foundation import Spezi +#if os(iOS) +import SpeziAccount +import SpeziFirebaseAccount +#endif import SpeziLLM +import SpeziLLMFog import SpeziLLMLocal import SpeziLLMOpenAI import SpeziSecureStorage class TestAppDelegate: SpeziAppDelegate { + // Used for production-ready setup including TLS traffic to the fog node + private nonisolated static var caCertificateUrl: URL? { + guard let url = Bundle.main.url(forResource: "ca", withExtension: "crt") else { + preconditionFailure("CA Certificate not found!") + } + + return url + } + override var configuration: Configuration { Configuration { + // As SpeziAccount, SpeziFirebase and the firebase-ios-sdk currently don't support visionOS and macOS, perform fog node token authentication only on iOS + #if os(iOS) + AccountConfiguration(configuration: [ + .requires(\.userId), + .requires(\.password) + ]) + + FirebaseAccountConfiguration( + authenticationMethods: .emailAndPassword, + emulatorSettings: (host: "localhost", port: 9099) // Use Firebase emulator for development purposes + ) + #endif + LLMRunner { LLMMockPlatform() LLMLocalPlatform() + // No CA certificate (meaning no encrypted traffic) for development purposes, see `caCertificateUrl` above + LLMFogPlatform(configuration: .init(host: "spezillmfog.local", caCertificate: nil)) LLMOpenAIPlatform() } - SecureStorage() } } } diff --git a/Tests/UITests/TestApp/TestAppTestingSetup.swift b/Tests/UITests/TestApp/TestAppTestingSetup.swift index d5bac4c..692cb4a 100644 --- a/Tests/UITests/TestApp/TestAppTestingSetup.swift +++ b/Tests/UITests/TestApp/TestAppTestingSetup.swift @@ -13,16 +13,18 @@ import SwiftUI private struct TestAppTestingSetup: ViewModifier { @Environment(SecureStorage.self) var secureStorage + @AppStorage(StorageKeys.onboardingFlowComplete) private var completedOnboardingFlow = false + func body(content: Content) -> some View { content .task { if FeatureFlags.resetSecureStorage { - do { - try secureStorage.deleteAllCredentials() - } catch { - print(error.localizedDescription) - } + try? secureStorage.deleteAllCredentials() + } + + if FeatureFlags.showOnboarding { + completedOnboardingFlow = false } } } diff --git a/Tests/UITests/TestAppUITests/TestAppLLMLocalUITests.swift b/Tests/UITests/TestAppUITests/TestAppLLMLocalUITests.swift index 7ca4593..6b75e69 100644 --- a/Tests/UITests/TestAppUITests/TestAppLLMLocalUITests.swift +++ b/Tests/UITests/TestAppUITests/TestAppLLMLocalUITests.swift @@ -17,8 +17,12 @@ class TestAppLLMLocalUITests: XCTestCase { continueAfterFailure = false let app = XCUIApplication() - app.launchArguments = ["--mockMode"] + app.launchArguments = ["--mockMode", "--showOnboarding", "--testMode"] + #if !os(macOS) app.deleteAndLaunch(withSpringboardAppName: "TestApp") + #else + app.launch() + #endif } func testSpeziLLMLocal() throws { @@ -39,7 +43,11 @@ class TestAppLLMLocalUITests: XCTestCase { sleep(1) // Chat + #if !os(macOS) try app.textViews["Message Input Textfield"].enter(value: "New Message!", dismissKeyboard: false) + #else + try app.textFields["Message Input Textfield"].enter(value: "New Message!", dismissKeyboard: false) + #endif XCTAssert(app.buttons["Send Message"].waitForExistence(timeout: 2)) app.buttons["Send Message"].tap() diff --git a/Tests/UITests/TestAppUITests/TestAppLLMOpenAIUITests.swift b/Tests/UITests/TestAppUITests/TestAppLLMOpenAIUITests.swift index d8b6787..de872d9 100644 --- a/Tests/UITests/TestAppUITests/TestAppLLMOpenAIUITests.swift +++ b/Tests/UITests/TestAppUITests/TestAppLLMOpenAIUITests.swift @@ -17,18 +17,23 @@ class TestAppLLMOpenAIUITests: XCTestCase { continueAfterFailure = false let app = XCUIApplication() - app.launchArguments = ["--mockMode", "--resetSecureStorage"] + app.launchArguments = ["--mockMode", "--resetSecureStorage", "--testMode"] + #if !os(macOS) app.deleteAndLaunch(withSpringboardAppName: "TestApp") + #else + app.launch() + #endif } - func testSpeziLLMOpenAIOnboarding() throws { + func testSpeziLLMOpenAIOnboarding() throws { // swiftlint:disable:this function_body_length let app = XCUIApplication() XCTAssert(app.buttons["LLMOpenAI"].waitForExistence(timeout: 2)) app.buttons["LLMOpenAI"].tap() - app.buttons["Onboarding"].tap() + XCTAssert(app.buttons["Onboarding"].firstMatch.waitForExistence(timeout: 2)) + app.buttons["Onboarding"].firstMatch.tap() try app.textFields["OpenAI API Key"].enter(value: "New Token") sleep(1) @@ -36,20 +41,40 @@ class TestAppLLMOpenAIUITests: XCTestCase { XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) app.buttons["Next"].tap() + #if os(macOS) + XCTAssert(app.popUpButtons["modelPicker"].waitForExistence(timeout: 2)) + app.popUpButtons["modelPicker"].tap() + XCTAssert(app.menuItems["GPT 4 Turbo Preview"].waitForExistence(timeout: 2)) + app.menuItems["GPT 4 Turbo Preview"].tap() + XCTAssert(app.popUpButtons["GPT 4 Turbo Preview"].waitForExistence(timeout: 2)) + #elseif os(visionOS) + app.pickers["modelPicker"].pickerWheels.element(boundBy: 0).swipeUp() + XCTAssert(app.pickerWheels["GPT 4 Turbo Preview"].waitForExistence(timeout: 2)) + #else app.pickers["modelPicker"].pickerWheels.element(boundBy: 0).adjust(toPickerWheelValue: "GPT 4 Turbo Preview") XCTAssert(app.pickerWheels["GPT 4 Turbo Preview"].waitForExistence(timeout: 2)) + #endif + sleep(1) XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) app.buttons["Next"].tap() + #if !os(macOS) let alert = app.alerts["Model Selected"] + XCTAssertTrue(alert.waitForExistence(timeout: 2), "The `Model Selected` alert did not appear.") XCTAssertTrue(alert.staticTexts["gpt-4-turbo-preview"].exists, "The correct model was not registered.") let okButton = alert.buttons["OK"] XCTAssertTrue(okButton.exists, "The OK button on the alert was not found.") okButton.tap() + #else + XCTAssertTrue(app.staticTexts["Model Selected"].waitForExistence(timeout: 2), "The `Model Selected` alert did not appear.") + XCTAssertTrue(app.staticTexts["gpt-4-turbo-preview"].exists, "The correct model was not registered.") + XCTAssert(app.buttons["OK"].firstMatch.waitForExistence(timeout: 2)) + app.buttons["OK"].firstMatch.tap() + #endif XCTAssert(app.textFields["New Token"].waitForExistence(timeout: 2)) @@ -60,36 +85,57 @@ class TestAppLLMOpenAIUITests: XCTestCase { app.buttons["LLMOpenAI"].tap() XCTAssert(app.buttons["Onboarding"].waitForExistence(timeout: 2)) - app.buttons["Onboarding"].tap() + app.buttons["Onboarding"].firstMatch.tap() XCTAssert(app.textFields["New Token"].waitForExistence(timeout: 2)) sleep(1) app.buttons["Next"].tap() + #if !os(macOS) XCTAssert(app.pickerWheels["GPT 3.5 Turbo"].waitForExistence(timeout: 2)) + #else + XCTAssert(app.popUpButtons["GPT 3.5 Turbo"].waitForExistence(timeout: 2)) + #endif app.buttons["Next"].tap() + #if !os(macOS) let alert2 = app.alerts["Model Selected"] + XCTAssertTrue(alert2.waitForExistence(timeout: 2), "The `Model Selected` alert did not appear.") - XCTAssertTrue(alert2.staticTexts["gpt-3.5-turbo"].exists, "The alert message is not correct.") - - let okButton2 = alert2.buttons["OK"] + XCTAssertTrue(alert2.staticTexts["gpt-3.5-turbo"].exists, "The correct model was not registered.") + + let okButton2 = alert.buttons["OK"] XCTAssertTrue(okButton2.exists, "The OK button on the alert was not found.") - okButton2.tap() - + okButton.tap() + #else + XCTAssertTrue(app.staticTexts["Model Selected"].waitForExistence(timeout: 2), "The `Model Selected` alert did not appear.") + XCTAssertTrue(app.staticTexts["gpt-3.5-turbo"].exists, "The correct model was not registered.") + XCTAssert(app.buttons["OK"].firstMatch.waitForExistence(timeout: 2)) + app.buttons["OK"].firstMatch.tap() + #endif + + #if !os(macOS) app.deleteAndLaunch(withSpringboardAppName: "TestApp") + #else + app.terminate() + app.launch() + #endif XCTAssert(app.buttons["LLMOpenAI"].waitForExistence(timeout: 2)) app.buttons["LLMOpenAI"].tap() - app.buttons["Onboarding"].tap() + app.buttons["Onboarding"].firstMatch.tap() XCTAssert(app.textFields["OpenAI API Key"].waitForExistence(timeout: 2)) XCTAssert(app.buttons["Next"].waitForExistence(timeout: 2)) app.buttons["Next"].tap() + #if !os(macOS) XCTAssert(app.pickerWheels["GPT 3.5 Turbo"].waitForExistence(timeout: 2)) + #else + XCTAssert(app.popUpButtons["GPT 3.5 Turbo"].waitForExistence(timeout: 2)) + #endif } func testSpeziLLMOpenAIChat() throws { @@ -104,7 +150,11 @@ class TestAppLLMOpenAIUITests: XCTestCase { XCTAssert(app.buttons["Record Message"].isEnabled) + #if !os(macOS) try app.textViews["Message Input Textfield"].enter(value: "New Message!", dismissKeyboard: false) + #else + try app.textFields["Message Input Textfield"].enter(value: "New Message!", dismissKeyboard: false) + #endif XCTAssert(app.buttons["Send Message"].waitForExistence(timeout: 2)) app.buttons["Send Message"].tap() diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index e665314..fa98ac4 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -29,12 +29,19 @@ 976179502B034E0400E1046E /* TestAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9761794F2B034E0400E1046E /* TestAppDelegate.swift */; }; 976179522B034F0900E1046E /* TestAppLLMLocalUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 976179512B034F0900E1046E /* TestAppLLMLocalUITests.swift */; }; 976179542B03501100E1046E /* FeatureFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 976179532B03501100E1046E /* FeatureFlags.swift */; }; + 976CE5172BA2C05100E21810 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 976CE5162BA2C05100E21810 /* GoogleService-Info.plist */; }; + 976CE5192BA2C49900E21810 /* AccountSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 976CE5182BA2C49900E21810 /* AccountSheet.swift */; }; + 976CE51C2BA2C51E00E21810 /* AccountSetupHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 976CE51B2BA2C51E00E21810 /* AccountSetupHeader.swift */; }; + 9770F2912BB3C40C00478571 /* SpeziFirebaseAccount in Frameworks */ = {isa = PBXBuildFile; platformFilter = ios; productRef = 9770F2902BB3C40C00478571 /* SpeziFirebaseAccount */; }; 9772D6802B03381400E62B9D /* LLMOpenAIOnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9772D67F2B03381400E62B9D /* LLMOpenAIOnboardingView.swift */; }; 9772D6822B033D5500E62B9D /* LLMOpenAIChatTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9772D6812B033D5500E62B9D /* LLMOpenAIChatTestView.swift */; }; 977C052F2B4CCA0A00BA9861 /* LLMOpenAIFunctionWeather.swift in Sources */ = {isa = PBXBuildFile; fileRef = 977C052E2B4CCA0A00BA9861 /* LLMOpenAIFunctionWeather.swift */; }; 977E49A02B035563001485D4 /* LLMLocalTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 977E499F2B035563001485D4 /* LLMLocalTestView.swift */; }; + 979D41902BB3EBD8001953BD /* SpeziAccount in Frameworks */ = {isa = PBXBuildFile; platformFilter = ios; productRef = 979D418F2BB3EBD8001953BD /* SpeziAccount */; }; 97A25C942B28DDAB0073B990 /* LLMOpenAIModelOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97A25C922B28DDAB0073B990 /* LLMOpenAIModelOnboarding.swift */; }; 97A25C952B28DDAB0073B990 /* LLMOpenAITokenOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97A25C932B28DDAB0073B990 /* LLMOpenAITokenOnboarding.swift */; }; + 97A36E152B999EA60034D821 /* SpeziLLMFog in Frameworks */ = {isa = PBXBuildFile; productRef = 97A36E142B999EA60034D821 /* SpeziLLMFog */; }; + 97C93FD02B999DD10023F4B9 /* LLMFogChatTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97C93FCF2B999DD10023F4B9 /* LLMFogChatTestView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -69,6 +76,9 @@ 9761794F2B034E0400E1046E /* TestAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestAppDelegate.swift; sourceTree = ""; }; 976179512B034F0900E1046E /* TestAppLLMLocalUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestAppLLMLocalUITests.swift; sourceTree = ""; }; 976179532B03501100E1046E /* FeatureFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlags.swift; sourceTree = ""; }; + 976CE5162BA2C05100E21810 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 976CE5182BA2C49900E21810 /* AccountSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSheet.swift; sourceTree = ""; }; + 976CE51B2BA2C51E00E21810 /* AccountSetupHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSetupHeader.swift; sourceTree = ""; }; 9772D67F2B03381400E62B9D /* LLMOpenAIOnboardingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LLMOpenAIOnboardingView.swift; sourceTree = ""; }; 9772D6812B033D5500E62B9D /* LLMOpenAIChatTestView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LLMOpenAIChatTestView.swift; sourceTree = ""; }; 977438092B05709700EC6527 /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; }; @@ -77,6 +87,7 @@ 977E499F2B035563001485D4 /* LLMLocalTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLMLocalTestView.swift; sourceTree = ""; }; 97A25C922B28DDAB0073B990 /* LLMOpenAIModelOnboarding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LLMOpenAIModelOnboarding.swift; sourceTree = ""; }; 97A25C932B28DDAB0073B990 /* LLMOpenAITokenOnboarding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LLMOpenAITokenOnboarding.swift; sourceTree = ""; }; + 97C93FCF2B999DD10023F4B9 /* LLMFogChatTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLMFogChatTestView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,9 +96,12 @@ buildActionMask = 2147483647; files = ( 9722A5A02B5B5CB20005645E /* SpeziLLM in Frameworks */, + 9770F2912BB3C40C00478571 /* SpeziFirebaseAccount in Frameworks */, + 97A36E152B999EA60034D821 /* SpeziLLMFog in Frameworks */, 9722A5A22B5B5CB20005645E /* SpeziLLMLocal in Frameworks */, 9722A5A62B5B5CB20005645E /* SpeziLLMOpenAI in Frameworks */, 9722A5A42B5B5CB20005645E /* SpeziLLMLocalDownload in Frameworks */, + 979D41902BB3EBD8001953BD /* SpeziAccount in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -129,6 +143,7 @@ 977447212B992D3A00D1F85E /* TestApp.entitlements */, 9756D25A2B0316790006B6BD /* Resources */, 97DD56B32B02F72D00389331 /* LLMLocal */, + 97C93FCE2B999DA40023F4B9 /* LLMFog */, 97DD56B42B02F72D00389331 /* LLMOpenAI */, 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */, 9761794F2B034E0400E1046E /* TestAppDelegate.swift */, @@ -188,12 +203,22 @@ 9756D25A2B0316790006B6BD /* Resources */ = { isa = PBXGroup; children = ( + 976CE5162BA2C05100E21810 /* GoogleService-Info.plist */, 9756D2562B0316740006B6BD /* Localizable.xcstrings */, 9756D2572B0316740006B6BD /* Localizable.xcstrings.license */, ); path = Resources; sourceTree = ""; }; + 976CE51A2BA2C4C600E21810 /* Account */ = { + isa = PBXGroup; + children = ( + 976CE5182BA2C49900E21810 /* AccountSheet.swift */, + 976CE51B2BA2C51E00E21810 /* AccountSetupHeader.swift */, + ); + path = Account; + sourceTree = ""; + }; 97A25C912B28DDAB0073B990 /* Onboarding */ = { isa = PBXGroup; children = ( @@ -203,6 +228,15 @@ path = Onboarding; sourceTree = ""; }; + 97C93FCE2B999DA40023F4B9 /* LLMFog */ = { + isa = PBXGroup; + children = ( + 976CE51A2BA2C4C600E21810 /* Account */, + 97C93FCF2B999DD10023F4B9 /* LLMFogChatTestView.swift */, + ); + path = LLMFog; + sourceTree = ""; + }; 97DD56B32B02F72D00389331 /* LLMLocal */ = { isa = PBXGroup; children = ( @@ -247,6 +281,9 @@ 9722A5A12B5B5CB20005645E /* SpeziLLMLocal */, 9722A5A32B5B5CB20005645E /* SpeziLLMLocalDownload */, 9722A5A52B5B5CB20005645E /* SpeziLLMOpenAI */, + 97A36E142B999EA60034D821 /* SpeziLLMFog */, + 9770F2902BB3C40C00478571 /* SpeziFirebaseAccount */, + 979D418F2BB3EBD8001953BD /* SpeziAccount */, ); productName = Example; productReference = 2F6D139228F5F384007C25D6 /* TestApp.app */; @@ -303,6 +340,8 @@ mainGroup = 2F6D138928F5F384007C25D6; packageReferences = ( 2FD590502A19E9F000153BE4 /* XCRemoteSwiftPackageReference "XCTestExtensions" */, + 9770F28F2BB3C40C00478571 /* XCRemoteSwiftPackageReference "SpeziFirebase" */, + 979D418E2BB3EBD8001953BD /* XCRemoteSwiftPackageReference "SpeziAccount" */, ); productRefGroup = 2F6D139328F5F384007C25D6 /* Products */; projectDirPath = ""; @@ -320,6 +359,7 @@ buildActionMask = 2147483647; files = ( 9756D2582B0316740006B6BD /* Localizable.xcstrings in Resources */, + 976CE5172BA2C05100E21810 /* GoogleService-Info.plist in Resources */, 2F6D139A28F5F386007C25D6 /* Assets.xcassets in Resources */, 9756D2592B0316740006B6BD /* Localizable.xcstrings.license in Resources */, ); @@ -360,11 +400,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 97C93FD02B999DD10023F4B9 /* LLMFogChatTestView.swift in Sources */, 9748DBEE2B5C811F00B917EE /* LLMOpenAIFunctionPerson.swift in Sources */, 9772D6802B03381400E62B9D /* LLMOpenAIOnboardingView.swift in Sources */, 9756D25E2B0316A30006B6BD /* LLMLocalChatTestView.swift in Sources */, 9756D2532B0316240006B6BD /* LLMLocalOnboardingDownloadView.swift in Sources */, 9756D2542B0316240006B6BD /* LLMLocalOnboardingWelcomeView.swift in Sources */, + 976CE51C2BA2C51E00E21810 /* AccountSetupHeader.swift in Sources */, 97A25C952B28DDAB0073B990 /* LLMOpenAITokenOnboarding.swift in Sources */, 63EF9CF02BA7398C001D92D7 /* TestAppTestingSetup.swift in Sources */, 977E49A02B035563001485D4 /* LLMLocalTestView.swift in Sources */, @@ -377,6 +419,7 @@ 97A25C942B28DDAB0073B990 /* LLMOpenAIModelOnboarding.swift in Sources */, 2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */, 977C052F2B4CCA0A00BA9861 /* LLMOpenAIFunctionWeather.swift in Sources */, + 976CE5192BA2C49900E21810 /* AccountSheet.swift in Sources */, 9756D2512B0316240006B6BD /* Binding+Negate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -687,7 +730,23 @@ repositoryURL = "https://github.com/StanfordBDHG/XCTestExtensions.git"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 0.4.4; + minimumVersion = 0.4.10; + }; + }; + 9770F28F2BB3C40C00478571 /* XCRemoteSwiftPackageReference "SpeziFirebase" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/StanfordSpezi/SpeziFirebase"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 1.0.1; + }; + }; + 979D418E2BB3EBD8001953BD /* XCRemoteSwiftPackageReference "SpeziAccount" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/StanfordSpezi/SpeziAccount"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 1.2.1; }; }; /* End XCRemoteSwiftPackageReference section */ @@ -714,6 +773,20 @@ isa = XCSwiftPackageProductDependency; productName = SpeziLLMOpenAI; }; + 9770F2902BB3C40C00478571 /* SpeziFirebaseAccount */ = { + isa = XCSwiftPackageProductDependency; + package = 9770F28F2BB3C40C00478571 /* XCRemoteSwiftPackageReference "SpeziFirebase" */; + productName = SpeziFirebaseAccount; + }; + 979D418F2BB3EBD8001953BD /* SpeziAccount */ = { + isa = XCSwiftPackageProductDependency; + package = 979D418E2BB3EBD8001953BD /* XCRemoteSwiftPackageReference "SpeziAccount" */; + productName = SpeziAccount; + }; + 97A36E142B999EA60034D821 /* SpeziLLMFog */ = { + isa = XCSwiftPackageProductDependency; + productName = SpeziLLMFog; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 2F6D138A28F5F384007C25D6 /* Project object */; diff --git a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme index 675d8f9..64ae972 100644 --- a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme +++ b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme @@ -133,6 +133,10 @@ + +