diff --git a/.bun-version b/.bun-version deleted file mode 100644 index aaf8be71..00000000 --- a/.bun-version +++ /dev/null @@ -1 +0,0 @@ -1.3.5 \ No newline at end of file diff --git a/.changeset/README.md b/.changeset/README.md deleted file mode 100644 index 8d31c94f..00000000 --- a/.changeset/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# Changesets - -Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works -with multi-package repos, or single-package repos to help you version and publish your code. You can -find the full documentation for it [in our repository](https://github.com/changesets/changesets) - -We have a quick list of common questions to get you started engaging with this project in -[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) - -## Usage - -To add a changeset, run: - -```bash -bun run changeset -``` - -To version the package based on changesets, run: - -```bash -bun run changeset:version -``` - -To check the status of changesets, run: - -```bash -bun run changeset:status -``` - diff --git a/.changeset/config.json b/.changeset/config.json deleted file mode 100644 index 539672ba..00000000 --- a/.changeset/config.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", - "changelog": ["@changesets/changelog-github", { "repo": "luke-h1/lho-lambda" }], - "commit": false, - "fixed": [], - "linked": [], - "access": "restricted", - "baseBranch": "main", - "updateInternalDependencies": "patch", - "ignore": [], - "privatePackages": { - "version": true, - "tag": false - } -} - diff --git a/.github/actions/deploy/action.yml b/.github/actions/deploy/action.yml index 3526e447..fb61d548 100644 --- a/.github/actions/deploy/action.yml +++ b/.github/actions/deploy/action.yml @@ -13,6 +13,9 @@ inputs: op-service-account-token: description: "1Password service account token" required: true + version: + description: "Application version to expose at runtime" + required: false runs: using: composite steps: @@ -21,6 +24,11 @@ runs: with: terraform_version: "1.14.5" + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v3 with: @@ -44,6 +52,8 @@ runs: SPOTIFY_CLIENT_ID: op://ci-cd/lho-lambda/SPOTIFY_CLIENT_ID SPOTIFY_CLIENT_SECRET: op://ci-cd/lho-lambda/SPOTIFY_CLIENT_SECRET SPOTIFY_REFRESH_TOKEN: op://ci-cd/lho-lambda/SPOTIFY_REFRESH_TOKEN + LASTFM_API_KEY: op://ci-cd/lho-lambda/LASTFM_API_KEY + LASTFM_USERNAME: op://ci-cd/lho-lambda/LASTFM_USERNAME OP_ENV_FILE: ".env" - name: Set Terraform variables @@ -52,6 +62,8 @@ runs: echo "TF_VAR_spotify_client_id=$SPOTIFY_CLIENT_ID" >> $GITHUB_ENV echo "TF_VAR_spotify_client_secret=$SPOTIFY_CLIENT_SECRET" >> $GITHUB_ENV echo "TF_VAR_spotify_refresh_token=$SPOTIFY_REFRESH_TOKEN" >> $GITHUB_ENV + echo "TF_VAR_lastfm_api_key=$LASTFM_API_KEY" >> $GITHUB_ENV + echo "TF_VAR_lastfm_username=$LASTFM_USERNAME" >> $GITHUB_ENV - name: Terraform init shell: bash @@ -77,31 +89,33 @@ runs: - name: Build functions for Lambda shell: bash run: | - docker run --rm \ - --platform linux/amd64 \ - -v "${{ github.workspace }}:/workspace" \ - -w /workspace \ - swift:6.1-amazonlinux2 \ - bash -c "swift build -c release --static-swift-stdlib && \ - cp .build/release/lambda .build/release/bootstrap && \ - chmod 755 .build/release/bootstrap && \ - mkdir -p .build/release/authorizer-package && \ - cp .build/release/authorizer .build/release/authorizer-package/bootstrap && \ - chmod 755 .build/release/authorizer-package/bootstrap" - sudo chown -R $(id -u):$(id -g) .build + dotnet publish src/Lho.Lambda/Lho.Lambda.csproj \ + --configuration Release \ + --runtime linux-x64 \ + --self-contained false \ + -p:PublishReadyToRun=true \ + --output .build/release/lambda-package + dotnet publish src/Lho.Lambda.Authorizer/Lho.Lambda.Authorizer.csproj \ + --configuration Release \ + --runtime linux-x64 \ + --self-contained false \ + -p:PublishReadyToRun=true \ + --output .build/release/authorizer-package - name: Get version id: version shell: bash + env: + DEPLOY_VERSION: ${{ inputs.version }} run: | - # Use VERSION env var if set (for pre-releases), otherwise read from package.json - if [ -n "$VERSION" ]; then - echo "Using provided version: $VERSION" - echo "APP_VERSION=$VERSION" >> $GITHUB_ENV + if [ -n "$DEPLOY_VERSION" ]; then + echo "Using provided version: $DEPLOY_VERSION" + echo "APP_VERSION=$DEPLOY_VERSION" >> $GITHUB_ENV else - PKG_VERSION=$(jq -r .version package.json) - echo "Using package.json version: $PKG_VERSION" - echo "APP_VERSION=$PKG_VERSION" >> $GITHUB_ENV + SHORT_SHA="${GITHUB_SHA:0:7}" + APP_VERSION="${{ inputs.environment }}-${SHORT_SHA}" + echo "Using generated version: $APP_VERSION" + echo "APP_VERSION=$APP_VERSION" >> $GITHUB_ENV fi - name: Terraform plan diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index 7a2bb373..ea118d1a 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -7,29 +7,22 @@ runs: run: git stash && git pull --rebase shell: bash - - name: Setup Swift - uses: swift-actions/setup-swift@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v4 with: - swift-version: "6.1.0" + dotnet-version: "8.0.x" - - name: Resolve Swift dependencies + - name: Restore dependencies shell: bash - run: swift package resolve + run: dotnet restore Lho.Lambda.slnx - - name: 🥟 Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version-file: ".bun-version" - - - name: Setup Node - uses: actions/setup-node@v4.1.0 - with: - node-version-file: .nvmrc - - - name: Install dependencies + - name: Build shell: bash - run: bun install + run: | + dotnet build src/Lho.Lambda/Lho.Lambda.csproj --no-restore + dotnet build src/Lho.Lambda.Authorizer/Lho.Lambda.Authorizer.csproj --no-restore + dotnet build src/Lho.Lambda.Local/Lho.Lambda.Local.csproj --no-restore - - name: Check commit message + - name: Test shell: bash - run: bun commitlint --from=HEAD^1 + run: dotnet test src/Lho.Lambda.Tests/Lho.Lambda.Tests.csproj --no-restore diff --git a/.github/actions/validate/action.yml b/.github/actions/validate/action.yml index c53bef2d..dc5dfb45 100644 --- a/.github/actions/validate/action.yml +++ b/.github/actions/validate/action.yml @@ -3,6 +3,17 @@ description: Validate runs: using: composite steps: + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + - name: Build functions shell: bash - run: swift build -c release + run: | + dotnet build src/Lho.Lambda/Lho.Lambda.csproj + dotnet build src/Lho.Lambda.Authorizer/Lho.Lambda.Authorizer.csproj + + - name: Test functions + shell: bash + run: dotnet test src/Lho.Lambda.Tests/Lho.Lambda.Tests.csproj --no-restore diff --git a/.github/workflows/build-prerelease-version.yml b/.github/workflows/build-prerelease-version.yml index d63a061f..10a390cf 100644 --- a/.github/workflows/build-prerelease-version.yml +++ b/.github/workflows/build-prerelease-version.yml @@ -33,20 +33,19 @@ jobs: with: fetch-depth: 0 - - name: Setup Bun - uses: oven-sh/setup-bun@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v4 with: - bun-version-file: ".bun-version" + dotnet-version: "8.0.x" - - name: Install dependencies - run: bun install --frozen-lockfile + - name: Restore dependencies + run: dotnet restore Lho.Lambda.slnx - name: Get pre-release version id: version run: | - BASE_VERSION=$(jq -r .version package.json) - SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7) - PRERELEASE_VERSION="${BASE_VERSION}-staging.${SHORT_SHA}" + SHORT_SHA="${GITHUB_SHA:0:7}" + PRERELEASE_VERSION="staging.${SHORT_SHA}" echo "version=${PRERELEASE_VERSION}" >> $GITHUB_OUTPUT echo "Pre-release version: ${PRERELEASE_VERSION}" @@ -57,8 +56,7 @@ jobs: aws-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} op-service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} - env: - VERSION: ${{ steps.version.outputs.version }} + version: ${{ steps.version.outputs.version }} - name: Summary run: | diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 7c8ae279..e44ad6dd 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -39,21 +39,23 @@ jobs: - name: Ensure rebased with main run: ./scripts/ensure-rebased.sh - - name: Setup Bun - uses: oven-sh/setup-bun@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v4 with: - bun-version-file: ".bun-version" + dotnet-version: "8.0.x" - - name: Install dependencies - run: bun install --frozen-lockfile + - name: Restore dependencies + run: dotnet restore Lho.Lambda.slnx - name: Get pre-release version id: version run: | - BASE_VERSION=$(jq -r .version package.json) - PR_NUMBER=${{ github.event.pull_request.number }} - SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7) - PRERELEASE_VERSION="${BASE_VERSION}-pr.${PR_NUMBER}.${SHORT_SHA}" + PR_NUMBER="${{ github.event.pull_request.number }}" + if [ -z "$PR_NUMBER" ]; then + PR_NUMBER="manual.${GITHUB_RUN_NUMBER}" + fi + SHORT_SHA="${GITHUB_SHA:0:7}" + PRERELEASE_VERSION="staging-pr.${PR_NUMBER}.${SHORT_SHA}" echo "version=${PRERELEASE_VERSION}" >> $GITHUB_OUTPUT echo "Pre-release version: ${PRERELEASE_VERSION}" @@ -64,8 +66,7 @@ jobs: aws-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} op-service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} - env: - VERSION: ${{ steps.version.outputs.version }} + version: ${{ steps.version.outputs.version }} - name: Comment on PR uses: actions/github-script@v7 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1a219165..f4ae49bc 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -14,6 +14,10 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true + +permissions: + contents: write + env: # terraform variables; TODO: move to secretmanager TF_VAR_env: ${{ github.event.inputs.env == 'staging' && 'staging' || 'live' }} @@ -46,11 +50,8 @@ jobs: - name: Install uses: ./.github/actions/install - # - name: Validate - # uses: ./.github/actions/validate - - # - name: Changelogs - # uses: ./.github/actions/changelog + - name: Validate + uses: ./.github/actions/validate - name: Deploy uses: ./.github/actions/deploy @@ -59,3 +60,21 @@ jobs: aws-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} op-service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + + - name: Update changelog + if: ${{ github.event.inputs.env == 'live' && github.ref == 'refs/heads/main' }} + run: | + SHORT_SHA="${GITHUB_SHA:0:7}" + DEPLOY_VERSION="live-${SHORT_SHA}" + + bash scripts/update-changelog.sh "${DEPLOY_VERSION}" + + if git diff --quiet -- CHANGELOG.md; then + echo "CHANGELOG.md already up to date" + else + git config user.name "${{ github.actor }}" + git config user.email "${{ github.actor }}@users.noreply.github.com" + git add CHANGELOG.md + git commit -m "chore(release): publish [skip ci]" + git push + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 98d9c5d4..9a148e4f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,8 @@ on: push: branches: - main + paths-ignore: + - CHANGELOG.md concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -38,44 +40,59 @@ jobs: with: fetch-depth: 0 - - name: Setup Bun - uses: oven-sh/setup-bun@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v4 with: - bun-version-file: ".bun-version" + dotnet-version: "8.0.x" - - name: Install dependencies - run: bun install --frozen-lockfile + - name: Restore dependencies + run: dotnet restore Lho.Lambda.slnx - - name: Create Release PR or Deploy - id: changesets - uses: changesets/action@v1 - with: - title: "chore(release): version packages" - commit: "chore(release): version packages" - version: bun run changeset:version - publish: echo "Skip npm publish" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Test + run: dotnet test src/Lho.Lambda.Tests/Lho.Lambda.Tests.csproj --no-restore + + - name: Set release version + id: version + run: | + SHORT_SHA="${GITHUB_SHA:0:7}" + RELEASE_VERSION="live.${SHORT_SHA}" + echo "version=${RELEASE_VERSION}" >> $GITHUB_OUTPUT + echo "Release version: ${RELEASE_VERSION}" - # Deploy to production when changesets are released - name: Deploy to Production - if: steps.changesets.outputs.hasChangesets == 'false' uses: ./.github/actions/deploy with: environment: live aws-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} op-service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + version: ${{ steps.version.outputs.version }} + + - name: Update changelog + id: changelog + run: | + bash scripts/update-changelog.sh "${{ steps.version.outputs.version }}" + + if git diff --quiet -- CHANGELOG.md; then + echo "updated=false" >> "$GITHUB_OUTPUT" + else + git config user.name "${{ github.actor }}" + git config user.email "${{ github.actor }}@users.noreply.github.com" + git add CHANGELOG.md + git commit -m "chore(release): publish [skip ci]" + git push + echo "updated=true" >> "$GITHUB_OUTPUT" + fi + + echo "release_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - name: Create GitHub Release - if: steps.changesets.outputs.hasChangesets == 'false' uses: actions/github-script@v7 with: script: | - const fs = require('fs'); - const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); - const version = pkg.version; + const version = '${{ steps.version.outputs.version }}'; const tag = `v${version}`; + const releaseSha = '${{ steps.changelog.outputs.release_sha }}' || context.sha; // Check if tag already exists try { @@ -95,7 +112,7 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, ref: `refs/tags/${tag}`, - sha: context.sha + sha: releaseSha }); // Create release @@ -108,4 +125,3 @@ jobs: draft: false, prerelease: false }); - diff --git a/.gitignore b/.gitignore index 6862192c..d431a87c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,140 +1,34 @@ -# Created by https://www.toptal.com/developers/gitignore/api/terraform -# Edit at https://www.toptal.com/developers/gitignore?templates=terraform - ### Terraform ### -# Local .terraform directories **/.terraform/* - -# .tfstate files *.tfstate *.tfstate.* - - *.tfvars *.tfvars.json - -# Ignore override files as they are usually used to override resources locally and so -# are not checked in override.tf override.tf.json *_override.tf *_override.tf.json .terraformrc terraform.rc - - certs .terraform.lock.hcl .terraform *.zip.DS_Store lambda.zip +authorizer.zip + +### General ### .DS_Store .turbo *.turbo .idea - - -# Created by https://www.toptal.com/developers/gitignore/api/swift -# Edit at https://www.toptal.com/developers/gitignore?templates=swift - -### Swift ### -# Xcode -# -# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore - -## User settings -xcuserdata/ - -## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) -*.xcscmblueprint -*.xccheckout - -## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) -build/ -DerivedData/ -*.moved-aside -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 - -## Obj-C/Swift specific -*.hmap - -## App packaging -*.ipa -*.dSYM.zip -*.dSYM - -## Playgrounds -timeline.xctimeline -playground.xcworkspace - -# Swift Package Manager -# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. -# Packages/ -# Package.pins -# Package.resolved -# *.xcodeproj -# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata -# hence it is not needed unless you have added a package configuration file to your project -# .swiftpm - -.build/ - -# CocoaPods -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control -# Pods/ -# Add this line if you want to avoid checking in source code from the Xcode workspace -# *.xcworkspace - -# Carthage -# Add this line if you want to avoid checking in source code from Carthage dependencies. -# Carthage/Checkouts - -Carthage/Build/ - -# Accio dependency management -Dependencies/ -.accio/ - -# fastlane -# It is recommended to not store the screenshots in the git repo. -# Instead, use fastlane to re-generate the screenshots whenever they are needed. -# For more information about the recommended setup visit: -# https://docs.fastlane.tools/best-practices/source-control/#source-control - -fastlane/report.xml -fastlane/Preview.html -fastlane/screenshots/**/*.png -fastlane/test_output - -# Code Injection -# After new code Injection tools there's a generated folder /iOSInjectionProject -# https://github.com/johnno1962/injectionforxcode - -iOSInjectionProject/ - -# End of https://www.toptal.com/developers/gitignore/api/swift -.DS_Store -/.build -/Packages -xcuserdata/ -DerivedData/ -.swiftpm/configuration/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc - .vscode +node_modules -*.swiftpm +### .NET ### +**/bin/ +**/obj/ -*.resolved -node_modules \ No newline at end of file +.idea diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index 8fdd954d..00000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22 \ No newline at end of file diff --git a/.swift-version b/.swift-version deleted file mode 100644 index a435f5a5..00000000 --- a/.swift-version +++ /dev/null @@ -1 +0,0 @@ -6.1 diff --git a/.swiftlint.yml b/.swiftlint.yml deleted file mode 100644 index 055e8fa2..00000000 --- a/.swiftlint.yml +++ /dev/null @@ -1,43 +0,0 @@ -# SwiftLint Configuration -# https://github.com/realm/SwiftLint - -# Paths to include/exclude -included: - - Sources - -excluded: - - .build - - .swiftpm - -opt_in_rules: - - empty_count - - empty_string - - first_where - - sorted_first_last - - vertical_whitespace_opening_braces - -line_length: - warning: 120 - error: 200 - ignores_function_declarations: true - ignores_comments: true - ignores_urls: true - -function_body_length: - warning: 100 - error: 200 - -function_parameter_count: - warning: 8 - error: 10 - -type_body_length: - warning: 400 - error: 600 - -file_length: - warning: 500 - error: 1000 - -# Reporter -reporter: "xcode" diff --git a/Lho.Lambda.slnx b/Lho.Lambda.slnx new file mode 100644 index 00000000..4f3c8ea9 --- /dev/null +++ b/Lho.Lambda.slnx @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Package.swift b/Package.swift deleted file mode 100644 index 803bb133..00000000 --- a/Package.swift +++ /dev/null @@ -1,38 +0,0 @@ -// swift-tools-version: 6.1 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -let package = Package( - name: "lambda", - platforms: [.macOS(.v15)], - products: [ - .executable(name: "lambda", targets: ["lambda"]), - .executable(name: "authorizer", targets: ["authorizer"]), - ], - dependencies: [ - .package( - url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "2.6.0"), - .package(url: "https://github.com/awslabs/swift-aws-lambda-events.git", from: "1.5.0"), - .package(url: "https://github.com/apple/swift-http-types.git", from: "1.5.1"), - .package(url: "https://github.com/apple/swift-log.git", "1.0.0"..<"2.0.0"), - ], - targets: [ - .executableTarget( - name: "authorizer", - dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), - - ] - ), - .executableTarget( - name: "lambda", - dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), - .product(name: "HTTPTypes", package: "swift-http-types"), - ], - ), - ] -) diff --git a/Sources/authorizer/main.swift b/Sources/authorizer/main.swift deleted file mode 100644 index 011c61e1..00000000 --- a/Sources/authorizer/main.swift +++ /dev/null @@ -1,72 +0,0 @@ -import AWSLambdaRuntime -import Foundation - -private func secureCompare(_ a: String?, _ b: String?) -> Bool { - guard let a = a, let b = b else { return a == nil && b == nil } - guard a.count == b.count else { return false } - return zip(a.utf8, b.utf8).reduce(0 as UInt8) { $0 | ($1.0 ^ $1.1) } == 0 -} - -struct AuthorizerRequest: Codable { - let version: String? - let type: String? - let routeArn: String? - let routeKey: String? - let rawPath: String? - let rawQueryString: String? - let headers: [String: String]? - let requestContext: RequestContext? - - struct RequestContext: Codable { - let http: HTTPContext? - - struct HTTPContext: Codable { - let method: String? - let path: String? - } - } -} - -struct AuthorizerSimpleResponse: Codable { - let isAuthorized: Bool -} - -func getHeaderValue(_ headers: [String: String]?, key: String) -> String? { - guard let headers = headers else { - return nil - } - - for (headerKey, value) in headers { - if headerKey.caseInsensitiveCompare(key) == .orderedSame { - return value - } - } - - return nil -} - -let runtime = LambdaRuntime { - (event: AuthorizerRequest, context: LambdaContext) -> AuthorizerSimpleResponse in - context.logger.debug("Authorizer invoked") - - let consumer = getHeaderValue(event.headers, key: "x-consumer") - let validConsumers = ["lhowsam-dev", "lhowsam-prod", "lhowsam-local"] - - let apiKey = getHeaderValue(event.headers, key: "x-api-key") - let validKey = ProcessInfo.processInfo.environment["API_KEY"] - - if !secureCompare(apiKey, validKey) { - context.logger.info("Deny - API key invalid") - return AuthorizerSimpleResponse(isAuthorized: false) - } - - if let consumer = consumer, !validConsumers.contains(consumer) { - context.logger.info("Deny - Invalid consumer") - return AuthorizerSimpleResponse(isAuthorized: false) - } - - context.logger.debug("Allow") - return AuthorizerSimpleResponse(isAuthorized: true) -} - -try await runtime.run() diff --git a/Sources/lambda/Models/Spotify.swift b/Sources/lambda/Models/Spotify.swift deleted file mode 100644 index 0eb345eb..00000000 --- a/Sources/lambda/Models/Spotify.swift +++ /dev/null @@ -1,59 +0,0 @@ -import Foundation - -struct NowPlayingResponse: Codable { - let isPlaying: Bool - let maintenance: Bool? - let status: Int - let album: String - let albumImageUrl: String - let artist: String - let songUrl: String - let title: String -} - -struct SpotifyResponse: Codable { - let isPlaying: Bool - let item: SpotifyItem? -} - -struct SpotifyItem: Codable { - let name: String - let artists: [SpotifyArtist] - let album: SpotifyAlbum - let externalUrls: SpotifyExternalUrls -} - -struct SpotifyArtist: Codable { - let name: String -} - -struct SpotifyImage: Codable { - let url: String - let height: Int? - let width: Int? -} - -struct SpotifyAlbum: Codable { - let name: String - let images: [SpotifyImage] -} - -struct SpotifyExternalUrls: Codable { - let spotify: String -} - -struct SpotifyTopTracksResponse: Codable { - let items: [SpotifyItem] -} - -struct TopTrackResponseItem: Codable { - let title: String - let artist: String - let album: String - let albumImageUrl: String - let songUrl: String -} - -struct TopTracksApiResponse: Codable { - let tracks: [TopTrackResponseItem] -} diff --git a/Sources/lambda/Services/NowPlayingService.swift b/Sources/lambda/Services/NowPlayingService.swift deleted file mode 100644 index c9986194..00000000 --- a/Sources/lambda/Services/NowPlayingService.swift +++ /dev/null @@ -1,93 +0,0 @@ -import AWSLambdaEvents -import AWSLambdaRuntime -import Foundation -import Logging - -actor NowPlayingService { - private static let nowPlayingCacheKey = "NowPlaying" - private static let revalidateSeconds = 3 - - private let cache: MemoryCache - private let spotifyApi: SpotifyApi - private let logger: Logger - - init(cache: MemoryCache, spotifyApi: SpotifyApi, logger: Logger) { - self.cache = cache - self.spotifyApi = spotifyApi - self.logger = logger - } - - func handleNowPlaying() async throws -> NowPlayingResponse { - do { - if let cachedResponse: NowPlayingResponse = await cache.get(Self.nowPlayingCacheKey) { - logger.info("Returning cached now playing response") - return cachedResponse - } - - if !Environment.shouldCallSpotify { - return NowPlayingResponse( - isPlaying: false, maintenance: true, status: 200, album: "", - albumImageUrl: "", artist: "", songUrl: "", title: "" - ) - } - - let nowPlayingResponse = try await spotifyApi.getNowPlaying() - - if nowPlayingResponse?.item == nil { - logger.info("No song currently playing") - - return NowPlayingResponse( - isPlaying: false, - maintenance: nil, - status: 200, - album: "", - albumImageUrl: "", - artist: "", - songUrl: "", - title: "" - ) - } - - guard let item = nowPlayingResponse?.item else { - throw NowPlayingError.missingItem - } - - let response = NowPlayingResponse( - isPlaying: nowPlayingResponse?.isPlaying ?? false, - maintenance: nil, - status: 200, - album: item.album.name, - albumImageUrl: item.album.images.first?.url ?? "", - artist: item.artists.map { $0.name }.joined(separator: ", "), - songUrl: item.externalUrls.spotify, - title: item.name - ) - - await cache.set(Self.nowPlayingCacheKey, value: response, expiration: 5.0) - - return response - } catch { - if let spotifyError = error as? SpotifyServiceError { - logger.error("Error fetching nowplaying data from spotify: \(spotifyError.description)") - } else { - logger.error( - "Error fetching nowplaying data from spotify: \(error.localizedDescription)") - } - - return NowPlayingResponse( - isPlaying: false, - maintenance: nil, - status: 500, - album: "", - albumImageUrl: "", - artist: "", - songUrl: "", - title: "" - ) - } - } -} - -enum NowPlayingError: Error { - case missingItem -} diff --git a/Sources/lambda/Services/TopTracksService.swift b/Sources/lambda/Services/TopTracksService.swift deleted file mode 100644 index e8a860f2..00000000 --- a/Sources/lambda/Services/TopTracksService.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Foundation -import Logging - -actor TopTracksService { - private let spotifyApi: SpotifyApi - private let logger: Logger - - init(spotifyApi: SpotifyApi, logger: Logger) { - self.spotifyApi = spotifyApi - self.logger = logger - } - - func handleTopTracks(timeRange: String, limit: Int) async throws -> TopTracksApiResponse { - do { - let response = try await spotifyApi.getTopTracks(timeRange: timeRange, limit: limit) - - let tracks = response.items.map { item in - TopTrackResponseItem( - title: item.name, - artist: item.artists.map(\.name).joined(separator: ", "), - album: item.album.name, - albumImageUrl: item.album.images.first?.url ?? "", - songUrl: item.externalUrls.spotify - ) - } - - return TopTracksApiResponse(tracks: tracks) - } catch { - if let spotifyError = error as? SpotifyServiceError { - logger.error("Top tracks fetch failed: \(spotifyError.description)") - } else { - logger.error("Top tracks fetch failed: \(error.localizedDescription)") - } - throw error - } - } -} diff --git a/Sources/lambda/Services/VersionService.swift b/Sources/lambda/Services/VersionService.swift deleted file mode 100644 index 7f5674bf..00000000 --- a/Sources/lambda/Services/VersionService.swift +++ /dev/null @@ -1,23 +0,0 @@ -import AWSLambdaEvents -import AWSLambdaRuntime -import Foundation -import Logging - -struct VersionResponse: Codable { - let version: String - let deployedAt: String - let deployedBy: String - let gitSha: String -} - -actor VersionService { - func handleVersion() async throws -> APIGatewayV2Response { - let response = VersionResponse( - version: Environment.Deploy.version, - deployedAt: Environment.Deploy.deployedAt, - deployedBy: Environment.Deploy.deployedBy, - gitSha: Environment.Deploy.gitSha - ) - return try ResponseBuilder.createResponse(body: response, includeCacheControl: false) - } -} diff --git a/Sources/lambda/Utils/Cache.swift b/Sources/lambda/Utils/Cache.swift deleted file mode 100644 index fc0e1dff..00000000 --- a/Sources/lambda/Utils/Cache.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation - -actor MemoryCache { - private var cache: [String: CacheEntry] = [:] - - struct CacheEntry { - let value: Any - let expirationDate: Date - } - - func get(_ key: String) -> T? { - guard let entry = cache[key] else { - return nil - } - - if entry.expirationDate < Date() { - cache.removeValue(forKey: key) - return nil - } - - return entry.value as? T - } - - func set(_ key: String, value: T, expiration: TimeInterval) { - let expirationDate = Date().addingTimeInterval(expiration) - cache[key] = CacheEntry(value: value, expirationDate: expirationDate) - } - - func remove(_ key: String) { - cache.removeValue(forKey: key) - } - - func clear() { - cache.removeAll() - } -} diff --git a/Sources/lambda/Utils/Environment.swift b/Sources/lambda/Utils/Environment.swift deleted file mode 100644 index ff0e3fa9..00000000 --- a/Sources/lambda/Utils/Environment.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation - -enum Environment { - private static let env = ProcessInfo.processInfo.environment - - static func string(_ key: String, default defaultValue: String = "") -> String { - env[key] ?? defaultValue - } - - static func bool(_ key: String, default defaultValue: Bool = false) -> Bool { - guard let value = env[key] else { return defaultValue } - return value.lowercased() == "true" || value == "1" - } - - enum Spotify { - static var clientId: String? { env["SPOTIFY_CLIENT_ID"] } - static var clientSecret: String? { env["SPOTIFY_CLIENT_SECRET"] } - static var refreshToken: String? { env["SPOTIFY_REFRESH_TOKEN"] } - static var accessToken: String? { env["SPOTIFY_ACCESS_TOKEN"] } - } - - enum Deploy { - static var version: String { Environment.string("VERSION", default: "unknown") } - static var deployedAt: String { Environment.string("DEPLOYED_AT", default: "unknown") } - static var deployedBy: String { Environment.string("DEPLOYED_BY", default: "unknown") } - static var gitSha: String { Environment.string("GIT_SHA", default: "unknown") } - } - - static var shouldCallSpotify: Bool { - Environment.bool("SHOULD_CALL_SPOTIFY", default: true) - } -} diff --git a/Sources/lambda/Utils/ResponseBuilder.swift b/Sources/lambda/Utils/ResponseBuilder.swift deleted file mode 100644 index c9e99678..00000000 --- a/Sources/lambda/Utils/ResponseBuilder.swift +++ /dev/null @@ -1,45 +0,0 @@ -import AWSLambdaEvents -import Foundation -import HTTPTypes - -enum ResponseBuilder { - static let defaultCORSHeaders: [String: String] = [ - "content-type": "application/json", - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET,OPTIONS,POST,PUT,DELETE" - ] - - static func createResponse( - body: T, - includeCacheControl: Bool = true, - revalidateSeconds: Int = 3 - ) throws -> APIGatewayV2Response { - let encoder = JSONEncoder() - var headers = defaultCORSHeaders - - if includeCacheControl { - headers["Cache-Control"] = - "max-age=\(revalidateSeconds), s-maxage=\(revalidateSeconds), stale-while-revalidate=\(revalidateSeconds), stale-if-error=\(revalidateSeconds)" - } else { - headers["Cache-Control"] = "no-cache" - } - - let bodyData = try encoder.encode(body) - let bodyString = String(data: bodyData, encoding: .utf8) ?? "{}" - - return APIGatewayV2Response( - statusCode: .ok, - headers: headers, - body: bodyString - ) - } - - static func errorResponse(statusCode: HTTPResponse.Status, message: String) -> APIGatewayV2Response { - let body = (try? JSONEncoder().encode(["error": message])).flatMap { String(data: $0, encoding: .utf8) } ?? #"{"error":"Unknown error"}"# - return APIGatewayV2Response( - statusCode: statusCode, - headers: defaultCORSHeaders, - body: body - ) - } -} diff --git a/Sources/lambda/Utils/Router.swift b/Sources/lambda/Utils/Router.swift deleted file mode 100644 index bb710ca3..00000000 --- a/Sources/lambda/Utils/Router.swift +++ /dev/null @@ -1,171 +0,0 @@ -import AWSLambdaEvents -import AWSLambdaRuntime -import Foundation -import HTTPTypes - -final class Router: @unchecked Sendable { - typealias Handler = (APIGatewayV2Request, LambdaContext) throws -> APIGatewayV2Response - typealias AsyncHandler = (APIGatewayV2Request, LambdaContext) async throws -> - APIGatewayV2Response - - private var routes: [Route] = [] - private var asyncRoutes: [AsyncRoute] = [] - - func add(method: HTTPMethod, path: String, handler: @escaping Handler) { - routes.append(Route(method: method, path: path, handler: handler)) - } - - func add(method: HTTPMethod, path: String, handler: @escaping AsyncHandler) { - asyncRoutes.append(AsyncRoute(method: method, path: path, handler: handler)) - } - - func handle(event: APIGatewayV2Request, context: LambdaContext) async throws - -> APIGatewayV2Response { - let methodString = String(describing: event.context.http.method) - let requestMethod = HTTPMethod(rawValue: methodString) ?? .GET - var requestPath = event.rawPath - - // Strip stage prefix if present (e.g., /test/api/health -> /api/health) - let knownStages = ["/test", "/staging", "/live", "/prod"] - for stage in knownStages { - if requestPath.hasPrefix(stage + "/") { - requestPath = String(requestPath.dropFirst(stage.count)) - break - } else if requestPath == stage { - requestPath = "/" - break - } - } - - if requestPath.hasPrefix("/invoke") { - requestPath = String(requestPath.dropFirst(7)) - if requestPath.isEmpty { - requestPath = "/" - } else if !requestPath.hasPrefix("/") { - requestPath = "/" + requestPath - } - } - - context.logger.debug( - "Request \(methodString) \(requestPath)" - ) - - for asyncRoute in asyncRoutes { - if asyncRoute.method == requestMethod && asyncRoute.matches(path: requestPath) { - return try await asyncRoute.handler(event, context) - } - } - - for route in routes { - if route.method == requestMethod && route.matches(path: requestPath) { - return try route.handler(event, context) - } - } - - return ResponseBuilder.errorResponse(statusCode: HTTPResponse.Status.notFound, message: "Not Found") - } -} - -struct Route { - let method: HTTPMethod - let path: String - let handler: Router.Handler - - func matches(path: String) -> Bool { - return self.path == path - } -} - -struct AsyncRoute { - let method: HTTPMethod - let path: String - let handler: Router.AsyncHandler - - func matches(path: String) -> Bool { - return self.path == path - } -} - -enum HTTPMethod: String { - case GET - case POST - case PUT - case DELETE - case PATCH - case OPTIONS -} - -func createRouter() -> Router { - let router = Router() - let cache = MemoryCache() - let spotifyApi = SpotifyApi() - - router.add(method: .GET, path: "/api/health") { _, _ in - (try? ResponseBuilder.createResponse(body: ["status": "OK"], includeCacheControl: false)) ?? ResponseBuilder.errorResponse(statusCode: HTTPResponse.Status.internalServerError, message: "Encoding error") - } - - router.add(method: .GET, path: "/api/version") { _, context async throws in - context.logger.info("Version endpoint called") - let versionService = VersionService() - return try await versionService.handleVersion() - } - - router.add(method: .GET, path: "/api/now-playing") { - _, context async throws -> APIGatewayV2Response in - let nowplayingService = NowPlayingService( - cache: cache, - spotifyApi: spotifyApi, - logger: context.logger - ) - let response = try await nowplayingService.handleNowPlaying() - return try ResponseBuilder.createResponse(body: response, revalidateSeconds: 3) - } - - router.add(method: .GET, path: "/api/top-tracks") { - event, context async throws -> APIGatewayV2Response in - let timeRange = parseQueryParam( - event: event, key: "time_range", - defaultValue: "medium_term", - allowedValues: ["short_term", "medium_term", "long_term"] - ) - let limit = parseQueryParam(event: event, key: "limit", defaultValue: "20") - let limitInt = min(50, max(1, Int(limit) ?? 20)) - - let topTracksService = TopTracksService( - spotifyApi: spotifyApi, - logger: context.logger - ) - do { - let response = try await topTracksService.handleTopTracks( - timeRange: timeRange, - limit: limitInt - ) - return try ResponseBuilder.createResponse(body: response, revalidateSeconds: 300) - } catch { - context.logger.error("Top tracks failed: \(error)") - return ResponseBuilder.errorResponse( - statusCode: HTTPResponse.Status.internalServerError, - message: "Unable to fetch top tracks" - ) - } - } - - return router -} - -private func parseQueryParam( - event: APIGatewayV2Request, key: String, defaultValue: String, - allowedValues: [String]? = nil -) -> String { - let raw = event.rawQueryString - guard let queryItems = URLComponents(string: "?\(raw)")?.queryItems else { - return defaultValue - } - guard let value = queryItems.first(where: { $0.name == key })?.value, !value.isEmpty else { - return defaultValue - } - if let allowed = allowedValues, !allowed.contains(value) { - return defaultValue - } - return value -} diff --git a/Sources/lambda/http/SpotifyApi.swift b/Sources/lambda/http/SpotifyApi.swift deleted file mode 100644 index d54da0d6..00000000 --- a/Sources/lambda/http/SpotifyApi.swift +++ /dev/null @@ -1,305 +0,0 @@ -import Foundation - -#if canImport(FoundationNetworking) - import FoundationNetworking -#endif - -enum SpotifyServiceError: Error, CustomStringConvertible, Sendable { - case missingAccessToken - case missingRefreshToken - case missingClientCredentials - case invalidURL(urlString: String) - case invalidResponse - case httpError(statusCode: Int, message: String? = nil) - case decodingError(String) - - var description: String { - switch self { - case .missingAccessToken: - return "Missing Spotify access token" - case .missingRefreshToken: - return "Missing Spotify refresh token" - case .missingClientCredentials: - return "Missing Spotify client ID or client secret" - case .invalidURL(let urlString): - return "Invalid URL: \(urlString)" - case .invalidResponse: - return "Invalid response from Spotify API" - case .httpError(let statusCode, let message): - if let message = message { - return "HTTP error with status code: \(statusCode) - \(message)" - } - return "HTTP error with status code: \(statusCode)" - case .decodingError(let details): - return "Failed to decode response: \(details)" - } - } -} - -struct TokenResponse: Codable { - let accessToken: String - let tokenType: String - let expiresIn: Int - - enum CodingKeys: String, CodingKey { - case accessToken = "access_token" - case tokenType = "token_type" - case expiresIn = "expires_in" - } -} - -actor SpotifyApi { - private static let requestTimeout: TimeInterval = 10 - private let baseURL: String - private let session: URLSession - private let accessToken: String? - private let clientId: String? - private let clientSecret: String? - private let refreshToken: String? - private var cachedAccessToken: String? - private var tokenExpiresAt: Date? - - init(baseURL: String? = nil, accessToken: String? = nil, session: URLSession? = nil) { - self.baseURL = baseURL ?? "https://api.spotify.com/v1" - let config: URLSessionConfiguration = { - let c = URLSessionConfiguration.default - c.timeoutIntervalForRequest = Self.requestTimeout - c.timeoutIntervalForResource = Self.requestTimeout - return c - }() - self.session = session ?? URLSession(configuration: config) - self.accessToken = accessToken ?? Environment.Spotify.accessToken - self.clientId = Environment.Spotify.clientId - self.clientSecret = Environment.Spotify.clientSecret - self.refreshToken = Environment.Spotify.refreshToken - } - - private func getAccessToken() async throws -> String { - if let accessToken = accessToken { - return accessToken - } - - if let cachedToken = cachedAccessToken, - let expiresAt = tokenExpiresAt, - expiresAt > Date() { - return cachedToken - } - - guard let refreshToken = refreshToken else { - throw SpotifyServiceError.missingRefreshToken - } - - guard let clientId = clientId, let clientSecret = clientSecret else { - throw SpotifyServiceError.missingClientCredentials - } - - let tokenURL = "https://accounts.spotify.com/api/token" - guard let url = URL(string: tokenURL) else { - throw SpotifyServiceError.invalidURL(urlString: tokenURL) - } - - let credentials = "\(clientId):\(clientSecret)" - guard let credentialsData = credentials.data(using: .utf8) else { - throw SpotifyServiceError.invalidResponse - } - let base64Credentials = credentialsData.base64EncodedString() - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("Basic \(base64Credentials)", forHTTPHeaderField: "Authorization") - request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") - - var components = URLComponents() - components.scheme = "https" - components.host = "accounts.spotify.com" - components.path = "/api/token" - components.queryItems = [ - URLQueryItem(name: "grant_type", value: "refresh_token"), - URLQueryItem(name: "refresh_token", value: refreshToken) - ] - guard let queryString = components.url?.query else { - throw SpotifyServiceError.invalidResponse - } - request.httpBody = queryString.data(using: .utf8) - - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw SpotifyServiceError.invalidResponse - } - - guard (200...299).contains(httpResponse.statusCode) else { - let errorMessage = - String(data: data, encoding: .utf8) ?? "Unable to read error response" - throw SpotifyServiceError.httpError( - statusCode: httpResponse.statusCode, message: errorMessage) - } - - guard !data.isEmpty else { - throw SpotifyServiceError.decodingError("Empty response from token endpoint") - } - - let responseString = String(data: data, encoding: .utf8) ?? "Unable to decode response" - - let lowercasedResponse = responseString.lowercased() - if lowercasedResponse.contains("\"error\"") - || lowercasedResponse.contains("error_description") { - throw SpotifyServiceError.httpError( - statusCode: httpResponse.statusCode, message: responseString) - } - - if responseString.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("<") { - throw SpotifyServiceError.httpError( - statusCode: httpResponse.statusCode, - message: "Received HTML response instead of JSON: \(responseString.prefix(200))") - } - - let decoder = JSONDecoder() - do { - let tokenResponse = try decoder.decode(TokenResponse.self, from: data) - cachedAccessToken = tokenResponse.accessToken - tokenExpiresAt = Date().addingTimeInterval(TimeInterval(tokenResponse.expiresIn - 60)) - - return tokenResponse.accessToken - } catch { - let errorDetails = "\(error)" - let errorMessage: String - if let decodingError = error as? DecodingError { - switch decodingError { - case .keyNotFound(let key, let context): - errorMessage = - "Missing key '\(key.stringValue)' at \(context.codingPath.map { $0.stringValue }.joined(separator: ".")). Response body: \(responseString)" - case .typeMismatch(let type, let context): - errorMessage = - "Type mismatch for '\(context.codingPath.map { $0.stringValue }.joined(separator: "."))': expected \(type). Response body: \(responseString)" - case .valueNotFound(let type, let context): - errorMessage = - "Value not found for '\(context.codingPath.map { $0.stringValue }.joined(separator: "."))': expected \(type). Response body: \(responseString)" - case .dataCorrupted(let context): - errorMessage = - "Data corrupted at \(context.codingPath.map { $0.stringValue }.joined(separator: ".")): \(context.debugDescription). Response body: \(responseString)" - @unknown default: - errorMessage = - "Decoding error: \(errorDetails). Response body: \(responseString)" - } - } else { - errorMessage = - "Failed to decode token response: \(errorDetails). Response body: \(responseString)" - } - throw SpotifyServiceError.decodingError(errorMessage) - } - - } - - func getNowPlaying() async throws -> SpotifyResponse? { - let accessToken = try await getAccessToken() - - let urlString = "\(baseURL)/me/player/currently-playing" - guard let url = URL(string: urlString) else { - throw SpotifyServiceError.invalidURL(urlString: urlString) - } - - var request = URLRequest(url: url) - request.httpMethod = "GET" - request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw SpotifyServiceError.invalidResponse - } - - if httpResponse.statusCode == 204 { - return nil - } - - guard (200...299).contains(httpResponse.statusCode) else { - let errorMessage = - String(data: data, encoding: .utf8) ?? "Unable to read error response" - throw SpotifyServiceError.httpError( - statusCode: httpResponse.statusCode, message: errorMessage) - } - - guard !data.isEmpty else { - throw SpotifyServiceError.decodingError("Empty response from Spotify API") - } - - return try decodeSpotifyResponse(SpotifyResponse.self, from: data) - } - - func getTopTracks(timeRange: String = "medium_term", limit: Int = 10) async throws - -> SpotifyTopTracksResponse { - let accessToken = try await getAccessToken() - - var components = URLComponents(string: "\(baseURL)/me/top/tracks")! - components.queryItems = [ - URLQueryItem(name: "time_range", value: timeRange), - URLQueryItem(name: "limit", value: String(min(50, max(1, limit)))) - ] - - guard let url = components.url else { - throw SpotifyServiceError.invalidURL( - urlString: components.string ?? "\(baseURL)/me/top/tracks") - } - - var request = URLRequest(url: url) - request.httpMethod = "GET" - request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw SpotifyServiceError.invalidResponse - } - - guard (200...299).contains(httpResponse.statusCode) else { - let errorMessage = - String(data: data, encoding: .utf8) ?? "Unable to read error response" - throw SpotifyServiceError.httpError( - statusCode: httpResponse.statusCode, message: errorMessage) - } - - guard !data.isEmpty else { - throw SpotifyServiceError.decodingError("Empty response from Spotify API") - } - - return try decodeSpotifyResponse(SpotifyTopTracksResponse.self, from: data) - } - - private func decodeSpotifyResponse(_ type: T.Type, from data: Data) throws - -> T { - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - do { - return try decoder.decode(type, from: data) - } catch { - let errorDetails = "\(error)" - if let decodingError = error as? DecodingError { - switch decodingError { - case .keyNotFound(let key, let context): - throw SpotifyServiceError.decodingError( - "Missing key '\(key.stringValue)' at \(context.codingPath.map { $0.stringValue }.joined(separator: "."))" - ) - case .typeMismatch(let type, let context): - throw SpotifyServiceError.decodingError( - "Type mismatch for '\(context.codingPath.map { $0.stringValue }.joined(separator: "."))': expected \(type)" - ) - case .valueNotFound(let type, let context): - throw SpotifyServiceError.decodingError( - "Value not found for '\(context.codingPath.map { $0.stringValue }.joined(separator: "."))': expected \(type)" - ) - case .dataCorrupted(let context): - throw SpotifyServiceError.decodingError( - "Data corrupted at \(context.codingPath.map { $0.stringValue }.joined(separator: ".")): \(context.debugDescription)" - ) - @unknown default: - throw SpotifyServiceError.decodingError("Decoding error: \(errorDetails)") - } - } - throw SpotifyServiceError.decodingError( - "Failed to decode Spotify response: \(errorDetails)") - } - } -} diff --git a/Sources/lambda/main.swift b/Sources/lambda/main.swift deleted file mode 100644 index 119feeb3..00000000 --- a/Sources/lambda/main.swift +++ /dev/null @@ -1,12 +0,0 @@ -import AWSLambdaEvents -import AWSLambdaRuntime -import Foundation - -let router = createRouter() - -let runtime = LambdaRuntime { - (event: APIGatewayV2Request, context: LambdaContext) -> APIGatewayV2Response in - return try await router.handle(event: event, context: context) -} - -try await runtime.run() diff --git a/bun.lock b/bun.lock deleted file mode 100644 index 890aa8f6..00000000 --- a/bun.lock +++ /dev/null @@ -1,946 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "root", - "devDependencies": { - "@changesets/changelog-github": "^0.5.0", - "@changesets/cli": "^2.29.8", - "@commitlint/cli": "^19.8.1", - "@commitlint/config-conventional": "^19.8.1", - "commitizen": "4.3.1", - "conventional-changelog-cli": "^3.0.0", - "git-cz": "^4.9.0", - "lint-staged": "^16.1.6", - }, - }, - }, - "packages": { - "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - - "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], - - "@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.0.14", "", { "dependencies": { "@changesets/config": "^3.1.2", "@changesets/get-version-range-type": "^0.4.0", "@changesets/git": "^3.0.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "detect-indent": "^6.0.0", "fs-extra": "^7.0.1", "lodash.startcase": "^4.4.0", "outdent": "^0.5.0", "prettier": "^2.7.1", "resolve-from": "^5.0.0", "semver": "^7.5.3" } }, "sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA=="], - - "@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@6.0.9", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "semver": "^7.5.3" } }, "sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ=="], - - "@changesets/changelog-git": ["@changesets/changelog-git@0.2.1", "", { "dependencies": { "@changesets/types": "^6.1.0" } }, "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q=="], - - "@changesets/changelog-github": ["@changesets/changelog-github@0.5.2", "", { "dependencies": { "@changesets/get-github-info": "^0.7.0", "@changesets/types": "^6.1.0", "dotenv": "^8.1.0" } }, "sha512-HeGeDl8HaIGj9fQHo/tv5XKQ2SNEi9+9yl1Bss1jttPqeiASRXhfi0A2wv8yFKCp07kR1gpOI5ge6+CWNm1jPw=="], - - "@changesets/cli": ["@changesets/cli@2.29.8", "", { "dependencies": { "@changesets/apply-release-plan": "^7.0.14", "@changesets/assemble-release-plan": "^6.0.9", "@changesets/changelog-git": "^0.2.1", "@changesets/config": "^3.1.2", "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/get-release-plan": "^4.0.14", "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.6", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@changesets/write": "^0.4.0", "@inquirer/external-editor": "^1.0.2", "@manypkg/get-packages": "^1.1.3", "ansi-colors": "^4.1.3", "ci-info": "^3.7.0", "enquirer": "^2.4.1", "fs-extra": "^7.0.1", "mri": "^1.2.0", "p-limit": "^2.2.0", "package-manager-detector": "^0.2.0", "picocolors": "^1.1.0", "resolve-from": "^5.0.0", "semver": "^7.5.3", "spawndamnit": "^3.0.1", "term-size": "^2.1.0" }, "bin": { "changeset": "bin.js" } }, "sha512-1weuGZpP63YWUYjay/E84qqwcnt5yJMM0tep10Up7Q5cS/DGe2IZ0Uj3HNMxGhCINZuR7aO9WBMdKnPit5ZDPA=="], - - "@changesets/config": ["@changesets/config@3.1.2", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/logger": "^0.1.1", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1", "micromatch": "^4.0.8" } }, "sha512-CYiRhA4bWKemdYi/uwImjPxqWNpqGPNbEBdX1BdONALFIDK7MCUj6FPkzD+z9gJcvDFUQJn9aDVf4UG7OT6Kog=="], - - "@changesets/errors": ["@changesets/errors@0.2.0", "", { "dependencies": { "extendable-error": "^0.1.5" } }, "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow=="], - - "@changesets/get-dependents-graph": ["@changesets/get-dependents-graph@2.1.3", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "picocolors": "^1.1.0", "semver": "^7.5.3" } }, "sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ=="], - - "@changesets/get-github-info": ["@changesets/get-github-info@0.7.0", "", { "dependencies": { "dataloader": "^1.4.0", "node-fetch": "^2.5.0" } }, "sha512-+i67Bmhfj9V4KfDeS1+Tz3iF32btKZB2AAx+cYMqDSRFP7r3/ZdGbjCo+c6qkyViN9ygDuBjzageuPGJtKGe5A=="], - - "@changesets/get-release-plan": ["@changesets/get-release-plan@4.0.14", "", { "dependencies": { "@changesets/assemble-release-plan": "^6.0.9", "@changesets/config": "^3.1.2", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.6", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-yjZMHpUHgl4Xl5gRlolVuxDkm4HgSJqT93Ri1Uz8kGrQb+5iJ8dkXJ20M2j/Y4iV5QzS2c5SeTxVSKX+2eMI0g=="], - - "@changesets/get-version-range-type": ["@changesets/get-version-range-type@0.4.0", "", {}, "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ=="], - - "@changesets/git": ["@changesets/git@3.0.4", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@manypkg/get-packages": "^1.1.3", "is-subdir": "^1.1.1", "micromatch": "^4.0.8", "spawndamnit": "^3.0.1" } }, "sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw=="], - - "@changesets/logger": ["@changesets/logger@0.1.1", "", { "dependencies": { "picocolors": "^1.1.0" } }, "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg=="], - - "@changesets/parse": ["@changesets/parse@0.4.2", "", { "dependencies": { "@changesets/types": "^6.1.0", "js-yaml": "^4.1.1" } }, "sha512-Uo5MC5mfg4OM0jU3up66fmSn6/NE9INK+8/Vn/7sMVcdWg46zfbvvUSjD9EMonVqPi9fbrJH9SXHn48Tr1f2yA=="], - - "@changesets/pre": ["@changesets/pre@2.0.2", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1" } }, "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug=="], - - "@changesets/read": ["@changesets/read@0.6.6", "", { "dependencies": { "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/parse": "^0.4.2", "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "p-filter": "^2.1.0", "picocolors": "^1.1.0" } }, "sha512-P5QaN9hJSQQKJShzzpBT13FzOSPyHbqdoIBUd2DJdgvnECCyO6LmAOWSV+O8se2TaZJVwSXjL+v9yhb+a9JeJg=="], - - "@changesets/should-skip-package": ["@changesets/should-skip-package@0.1.2", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw=="], - - "@changesets/types": ["@changesets/types@6.1.0", "", {}, "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA=="], - - "@changesets/write": ["@changesets/write@0.4.0", "", { "dependencies": { "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "human-id": "^4.1.1", "prettier": "^2.7.1" } }, "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q=="], - - "@commitlint/cli": ["@commitlint/cli@19.8.1", "", { "dependencies": { "@commitlint/format": "^19.8.1", "@commitlint/lint": "^19.8.1", "@commitlint/load": "^19.8.1", "@commitlint/read": "^19.8.1", "@commitlint/types": "^19.8.1", "tinyexec": "^1.0.0", "yargs": "^17.0.0" }, "bin": { "commitlint": "./cli.js" } }, "sha512-LXUdNIkspyxrlV6VDHWBmCZRtkEVRpBKxi2Gtw3J54cGWhLCTouVD/Q6ZSaSvd2YaDObWK8mDjrz3TIKtaQMAA=="], - - "@commitlint/config-conventional": ["@commitlint/config-conventional@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "conventional-changelog-conventionalcommits": "^7.0.2" } }, "sha512-/AZHJL6F6B/G959CsMAzrPKKZjeEiAVifRyEwXxcT6qtqbPwGw+iQxmNS+Bu+i09OCtdNRW6pNpBvgPrtMr9EQ=="], - - "@commitlint/config-validator": ["@commitlint/config-validator@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "ajv": "^8.11.0" } }, "sha512-0jvJ4u+eqGPBIzzSdqKNX1rvdbSU1lPNYlfQQRIFnBgLy26BtC0cFnr7c/AyuzExMxWsMOte6MkTi9I3SQ3iGQ=="], - - "@commitlint/ensure": ["@commitlint/ensure@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "lodash.camelcase": "^4.3.0", "lodash.kebabcase": "^4.1.1", "lodash.snakecase": "^4.1.1", "lodash.startcase": "^4.4.0", "lodash.upperfirst": "^4.3.1" } }, "sha512-mXDnlJdvDzSObafjYrOSvZBwkD01cqB4gbnnFuVyNpGUM5ijwU/r/6uqUmBXAAOKRfyEjpkGVZxaDsCVnHAgyw=="], - - "@commitlint/execute-rule": ["@commitlint/execute-rule@19.8.1", "", {}, "sha512-YfJyIqIKWI64Mgvn/sE7FXvVMQER/Cd+s3hZke6cI1xgNT/f6ZAz5heND0QtffH+KbcqAwXDEE1/5niYayYaQA=="], - - "@commitlint/format": ["@commitlint/format@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "chalk": "^5.3.0" } }, "sha512-kSJj34Rp10ItP+Eh9oCItiuN/HwGQMXBnIRk69jdOwEW9llW9FlyqcWYbHPSGofmjsqeoxa38UaEA5tsbm2JWw=="], - - "@commitlint/is-ignored": ["@commitlint/is-ignored@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "semver": "^7.6.0" } }, "sha512-AceOhEhekBUQ5dzrVhDDsbMaY5LqtN8s1mqSnT2Kz1ERvVZkNihrs3Sfk1Je/rxRNbXYFzKZSHaPsEJJDJV8dg=="], - - "@commitlint/lint": ["@commitlint/lint@19.8.1", "", { "dependencies": { "@commitlint/is-ignored": "^19.8.1", "@commitlint/parse": "^19.8.1", "@commitlint/rules": "^19.8.1", "@commitlint/types": "^19.8.1" } }, "sha512-52PFbsl+1EvMuokZXLRlOsdcLHf10isTPlWwoY1FQIidTsTvjKXVXYb7AvtpWkDzRO2ZsqIgPK7bI98x8LRUEw=="], - - "@commitlint/load": ["@commitlint/load@19.8.1", "", { "dependencies": { "@commitlint/config-validator": "^19.8.1", "@commitlint/execute-rule": "^19.8.1", "@commitlint/resolve-extends": "^19.8.1", "@commitlint/types": "^19.8.1", "chalk": "^5.3.0", "cosmiconfig": "^9.0.0", "cosmiconfig-typescript-loader": "^6.1.0", "lodash.isplainobject": "^4.0.6", "lodash.merge": "^4.6.2", "lodash.uniq": "^4.5.0" } }, "sha512-9V99EKG3u7z+FEoe4ikgq7YGRCSukAcvmKQuTtUyiYPnOd9a2/H9Ak1J9nJA1HChRQp9OA/sIKPugGS+FK/k1A=="], - - "@commitlint/message": ["@commitlint/message@19.8.1", "", {}, "sha512-+PMLQvjRXiU+Ae0Wc+p99EoGEutzSXFVwQfa3jRNUZLNW5odZAyseb92OSBTKCu+9gGZiJASt76Cj3dLTtcTdg=="], - - "@commitlint/parse": ["@commitlint/parse@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "conventional-changelog-angular": "^7.0.0", "conventional-commits-parser": "^5.0.0" } }, "sha512-mmAHYcMBmAgJDKWdkjIGq50X4yB0pSGpxyOODwYmoexxxiUCy5JJT99t1+PEMK7KtsCtzuWYIAXYAiKR+k+/Jw=="], - - "@commitlint/read": ["@commitlint/read@19.8.1", "", { "dependencies": { "@commitlint/top-level": "^19.8.1", "@commitlint/types": "^19.8.1", "git-raw-commits": "^4.0.0", "minimist": "^1.2.8", "tinyexec": "^1.0.0" } }, "sha512-03Jbjb1MqluaVXKHKRuGhcKWtSgh3Jizqy2lJCRbRrnWpcM06MYm8th59Xcns8EqBYvo0Xqb+2DoZFlga97uXQ=="], - - "@commitlint/resolve-extends": ["@commitlint/resolve-extends@19.8.1", "", { "dependencies": { "@commitlint/config-validator": "^19.8.1", "@commitlint/types": "^19.8.1", "global-directory": "^4.0.1", "import-meta-resolve": "^4.0.0", "lodash.mergewith": "^4.6.2", "resolve-from": "^5.0.0" } }, "sha512-GM0mAhFk49I+T/5UCYns5ayGStkTt4XFFrjjf0L4S26xoMTSkdCf9ZRO8en1kuopC4isDFuEm7ZOm/WRVeElVg=="], - - "@commitlint/rules": ["@commitlint/rules@19.8.1", "", { "dependencies": { "@commitlint/ensure": "^19.8.1", "@commitlint/message": "^19.8.1", "@commitlint/to-lines": "^19.8.1", "@commitlint/types": "^19.8.1" } }, "sha512-Hnlhd9DyvGiGwjfjfToMi1dsnw1EXKGJNLTcsuGORHz6SS9swRgkBsou33MQ2n51/boIDrbsg4tIBbRpEWK2kw=="], - - "@commitlint/to-lines": ["@commitlint/to-lines@19.8.1", "", {}, "sha512-98Mm5inzbWTKuZQr2aW4SReY6WUukdWXuZhrqf1QdKPZBCCsXuG87c+iP0bwtD6DBnmVVQjgp4whoHRVixyPBg=="], - - "@commitlint/top-level": ["@commitlint/top-level@19.8.1", "", { "dependencies": { "find-up": "^7.0.0" } }, "sha512-Ph8IN1IOHPSDhURCSXBz44+CIu+60duFwRsg6HqaISFHQHbmBtxVw4ZrFNIYUzEP7WwrNPxa2/5qJ//NK1FGcw=="], - - "@commitlint/types": ["@commitlint/types@19.8.1", "", { "dependencies": { "@types/conventional-commits-parser": "^5.0.0", "chalk": "^5.3.0" } }, "sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw=="], - - "@hutson/parse-repository-url": ["@hutson/parse-repository-url@3.0.2", "", {}, "sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q=="], - - "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], - - "@manypkg/find-root": ["@manypkg/find-root@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@types/node": "^12.7.1", "find-up": "^4.1.0", "fs-extra": "^8.1.0" } }, "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA=="], - - "@manypkg/get-packages": ["@manypkg/get-packages@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@changesets/types": "^4.0.1", "@manypkg/find-root": "^1.1.0", "fs-extra": "^8.1.0", "globby": "^11.0.0", "read-yaml-file": "^1.1.0" } }, "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A=="], - - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - - "@types/conventional-commits-parser": ["@types/conventional-commits-parser@5.0.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g=="], - - "@types/minimist": ["@types/minimist@1.2.5", "", {}, "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag=="], - - "@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], - - "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], - - "JSONStream": ["JSONStream@1.3.5", "", { "dependencies": { "jsonparse": "^1.2.0", "through": ">=2.2.7 <3" }, "bin": { "JSONStream": "./bin.js" } }, "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ=="], - - "add-stream": ["add-stream@1.0.0", "", {}, "sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ=="], - - "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - - "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], - - "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], - - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "array-ify": ["array-ify@1.0.0", "", {}, "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng=="], - - "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], - - "arrify": ["arrify@1.0.1", "", {}, "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA=="], - - "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], - - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - - "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], - - "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - - "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - - "cachedir": ["cachedir@2.3.0", "", {}, "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw=="], - - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - - "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], - - "camelcase-keys": ["camelcase-keys@6.2.2", "", { "dependencies": { "camelcase": "^5.3.1", "map-obj": "^4.0.0", "quick-lru": "^4.0.1" } }, "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg=="], - - "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - - "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], - - "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], - - "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], - - "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], - - "cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A=="], - - "cli-width": ["cli-width@3.0.0", "", {}, "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw=="], - - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - - "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], - - "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - - "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], - - "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], - - "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], - - "commitizen": ["commitizen@4.3.1", "", { "dependencies": { "cachedir": "2.3.0", "cz-conventional-changelog": "3.3.0", "dedent": "0.7.0", "detect-indent": "6.1.0", "find-node-modules": "^2.1.2", "find-root": "1.1.0", "fs-extra": "9.1.0", "glob": "7.2.3", "inquirer": "8.2.5", "is-utf8": "^0.2.1", "lodash": "4.17.21", "minimist": "1.2.7", "strip-bom": "4.0.0", "strip-json-comments": "3.1.1" }, "bin": { "cz": "bin/git-cz", "git-cz": "bin/git-cz", "commitizen": "bin/commitizen" } }, "sha512-gwAPAVTy/j5YcOOebcCRIijn+mSjWJC+IYKivTu6aG8Ei/scoXgfsMRnuAk6b0GRste2J4NGxVdMN3ZpfNaVaw=="], - - "compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="], - - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - - "conventional-changelog": ["conventional-changelog@4.0.0", "", { "dependencies": { "conventional-changelog-angular": "^6.0.0", "conventional-changelog-atom": "^3.0.0", "conventional-changelog-codemirror": "^3.0.0", "conventional-changelog-conventionalcommits": "^6.0.0", "conventional-changelog-core": "^5.0.0", "conventional-changelog-ember": "^3.0.0", "conventional-changelog-eslint": "^4.0.0", "conventional-changelog-express": "^3.0.0", "conventional-changelog-jquery": "^4.0.0", "conventional-changelog-jshint": "^3.0.0", "conventional-changelog-preset-loader": "^3.0.0" } }, "sha512-JbZjwE1PzxQCvm+HUTIr+pbSekS8qdOZzMakdFyPtdkEWwFvwEJYONzjgMm0txCb2yBcIcfKDmg8xtCKTdecNQ=="], - - "conventional-changelog-angular": ["conventional-changelog-angular@6.0.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-6qLgrBF4gueoC7AFVHu51nHL9pF9FRjXrH+ceVf7WmAfH3gs+gEYOkvxhjMPjZu57I4AGUGoNTY8V7Hrgf1uqg=="], - - "conventional-changelog-atom": ["conventional-changelog-atom@3.0.0", "", {}, "sha512-pnN5bWpH+iTUWU3FaYdw5lJmfWeqSyrUkG+wyHBI9tC1dLNnHkbAOg1SzTQ7zBqiFrfo55h40VsGXWMdopwc5g=="], - - "conventional-changelog-cli": ["conventional-changelog-cli@3.0.0", "", { "dependencies": { "add-stream": "^1.0.0", "conventional-changelog": "^4.0.0", "meow": "^8.1.2", "tempfile": "^3.0.0" }, "bin": { "conventional-changelog": "cli.js" } }, "sha512-3zMYi0IrfNd6AAHdPMrcgCg5DbcffiqNaEBf8cYrlntXPbBIXaELTbnRmUy5TQAe0Hkgi0J6+/VmRCkkJQflcQ=="], - - "conventional-changelog-codemirror": ["conventional-changelog-codemirror@3.0.0", "", {}, "sha512-wzchZt9HEaAZrenZAUUHMCFcuYzGoZ1wG/kTRMICxsnW5AXohYMRxnyecP9ob42Gvn5TilhC0q66AtTPRSNMfw=="], - - "conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@7.0.2", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w=="], - - "conventional-changelog-core": ["conventional-changelog-core@5.0.2", "", { "dependencies": { "add-stream": "^1.0.0", "conventional-changelog-writer": "^6.0.0", "conventional-commits-parser": "^4.0.0", "dateformat": "^3.0.3", "get-pkg-repo": "^4.2.1", "git-raw-commits": "^3.0.0", "git-remote-origin-url": "^2.0.0", "git-semver-tags": "^5.0.0", "normalize-package-data": "^3.0.3", "read-pkg": "^3.0.0", "read-pkg-up": "^3.0.0" } }, "sha512-RhQOcDweXNWvlRwUDCpaqXzbZemKPKncCWZG50Alth72WITVd6nhVk9MJ6w1k9PFNBcZ3YwkdkChE+8+ZwtUug=="], - - "conventional-changelog-ember": ["conventional-changelog-ember@3.0.0", "", {}, "sha512-7PYthCoSxIS98vWhVcSphMYM322OxptpKAuHYdVspryI0ooLDehRXWeRWgN+zWSBXKl/pwdgAg8IpLNSM1/61A=="], - - "conventional-changelog-eslint": ["conventional-changelog-eslint@4.0.0", "", {}, "sha512-nEZ9byP89hIU0dMx37JXQkE1IpMmqKtsaR24X7aM3L6Yy/uAtbb+ogqthuNYJkeO1HyvK7JsX84z8649hvp43Q=="], - - "conventional-changelog-express": ["conventional-changelog-express@3.0.0", "", {}, "sha512-HqxihpUMfIuxvlPvC6HltA4ZktQEUan/v3XQ77+/zbu8No/fqK3rxSZaYeHYant7zRxQNIIli7S+qLS9tX9zQA=="], - - "conventional-changelog-jquery": ["conventional-changelog-jquery@4.0.0", "", {}, "sha512-TTIN5CyzRMf8PUwyy4IOLmLV2DFmPtasKN+x7EQKzwSX8086XYwo+NeaeA3VUT8bvKaIy5z/JoWUvi7huUOgaw=="], - - "conventional-changelog-jshint": ["conventional-changelog-jshint@3.0.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-bQof4byF4q+n+dwFRkJ/jGf9dCNUv4/kCDcjeCizBvfF81TeimPZBB6fT4HYbXgxxfxWXNl/i+J6T0nI4by6DA=="], - - "conventional-changelog-preset-loader": ["conventional-changelog-preset-loader@3.0.0", "", {}, "sha512-qy9XbdSLmVnwnvzEisjxdDiLA4OmV3o8db+Zdg4WiFw14fP3B6XNz98X0swPPpkTd/pc1K7+adKgEDM1JCUMiA=="], - - "conventional-changelog-writer": ["conventional-changelog-writer@6.0.1", "", { "dependencies": { "conventional-commits-filter": "^3.0.0", "dateformat": "^3.0.3", "handlebars": "^4.7.7", "json-stringify-safe": "^5.0.1", "meow": "^8.1.2", "semver": "^7.0.0", "split": "^1.0.1" }, "bin": { "conventional-changelog-writer": "cli.js" } }, "sha512-359t9aHorPw+U+nHzUXHS5ZnPBOizRxfQsWT5ZDHBfvfxQOAik+yfuhKXG66CN5LEWPpMNnIMHUTCKeYNprvHQ=="], - - "conventional-commit-types": ["conventional-commit-types@3.0.0", "", {}, "sha512-SmmCYnOniSsAa9GqWOeLqc179lfr5TRu5b4QFDkbsrJ5TZjPJx85wtOr3zn+1dbeNiXDKGPbZ72IKbPhLXh/Lg=="], - - "conventional-commits-filter": ["conventional-commits-filter@3.0.0", "", { "dependencies": { "lodash.ismatch": "^4.4.0", "modify-values": "^1.0.1" } }, "sha512-1ymej8b5LouPx9Ox0Dw/qAO2dVdfpRFq28e5Y0jJEU8ZrLdy0vOSkkIInwmxErFGhg6SALro60ZrwYFVTUDo4Q=="], - - "conventional-commits-parser": ["conventional-commits-parser@5.0.0", "", { "dependencies": { "JSONStream": "^1.3.5", "is-text-path": "^2.0.0", "meow": "^12.0.1", "split2": "^4.0.0" }, "bin": { "conventional-commits-parser": "cli.mjs" } }, "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA=="], - - "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], - - "cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="], - - "cosmiconfig-typescript-loader": ["cosmiconfig-typescript-loader@6.2.0", "", { "dependencies": { "jiti": "^2.6.1" }, "peerDependencies": { "@types/node": "*", "cosmiconfig": ">=9", "typescript": ">=5" } }, "sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - - "cz-conventional-changelog": ["cz-conventional-changelog@3.3.0", "", { "dependencies": { "chalk": "^2.4.1", "commitizen": "^4.0.3", "conventional-commit-types": "^3.0.0", "lodash.map": "^4.5.1", "longest": "^2.0.1", "word-wrap": "^1.0.3" }, "optionalDependencies": { "@commitlint/load": ">6.1.1" } }, "sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw=="], - - "dargs": ["dargs@8.1.0", "", {}, "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw=="], - - "dataloader": ["dataloader@1.4.0", "", {}, "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw=="], - - "dateformat": ["dateformat@3.0.3", "", {}, "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q=="], - - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], - - "decamelize-keys": ["decamelize-keys@1.1.1", "", { "dependencies": { "decamelize": "^1.1.0", "map-obj": "^1.0.0" } }, "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg=="], - - "dedent": ["dedent@0.7.0", "", {}, "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA=="], - - "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], - - "detect-file": ["detect-file@1.0.0", "", {}, "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q=="], - - "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], - - "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], - - "dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="], - - "dotenv": ["dotenv@8.6.0", "", {}, "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g=="], - - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], - - "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], - - "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], - - "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], - - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - - "escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - - "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - - "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], - - "expand-tilde": ["expand-tilde@2.0.2", "", { "dependencies": { "homedir-polyfill": "^1.0.1" } }, "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw=="], - - "extendable-error": ["extendable-error@0.1.7", "", {}, "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg=="], - - "external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - - "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], - - "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], - - "figures": ["figures@3.2.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg=="], - - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - - "find-node-modules": ["find-node-modules@2.1.3", "", { "dependencies": { "findup-sync": "^4.0.0", "merge": "^2.1.1" } }, "sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg=="], - - "find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="], - - "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], - - "findup-sync": ["findup-sync@4.0.0", "", { "dependencies": { "detect-file": "^1.0.0", "is-glob": "^4.0.0", "micromatch": "^4.0.2", "resolve-dir": "^1.0.1" } }, "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ=="], - - "fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], - - "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - - "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], - - "get-pkg-repo": ["get-pkg-repo@4.2.1", "", { "dependencies": { "@hutson/parse-repository-url": "^3.0.0", "hosted-git-info": "^4.0.0", "through2": "^2.0.0", "yargs": "^16.2.0" }, "bin": { "get-pkg-repo": "src/cli.js" } }, "sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA=="], - - "git-cz": ["git-cz@4.9.0", "", { "bin": { "git-cz": "bin/git-cz.js", "gitcz": "bin/git-cz.js" } }, "sha512-cSRL8IIOXU7UFLdbziCYqg8f8InwLwqHezkiRHNSph7oZqGv0togId1kMTfKil6gzK0VaSXeVBb4oDl0fQCHiw=="], - - "git-raw-commits": ["git-raw-commits@4.0.0", "", { "dependencies": { "dargs": "^8.0.0", "meow": "^12.0.1", "split2": "^4.0.0" }, "bin": { "git-raw-commits": "cli.mjs" } }, "sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ=="], - - "git-remote-origin-url": ["git-remote-origin-url@2.0.0", "", { "dependencies": { "gitconfiglocal": "^1.0.0", "pify": "^2.3.0" } }, "sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw=="], - - "git-semver-tags": ["git-semver-tags@5.0.1", "", { "dependencies": { "meow": "^8.1.2", "semver": "^7.0.0" }, "bin": { "git-semver-tags": "cli.js" } }, "sha512-hIvOeZwRbQ+7YEUmCkHqo8FOLQZCEn18yevLHADlFPZY02KJGsu5FZt9YW/lybfK2uhWFI7Qg/07LekJiTv7iA=="], - - "gitconfiglocal": ["gitconfiglocal@1.0.0", "", { "dependencies": { "ini": "^1.3.2" } }, "sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ=="], - - "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - - "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "global-directory": ["global-directory@4.0.1", "", { "dependencies": { "ini": "4.1.1" } }, "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q=="], - - "global-modules": ["global-modules@1.0.0", "", { "dependencies": { "global-prefix": "^1.0.1", "is-windows": "^1.0.1", "resolve-dir": "^1.0.0" } }, "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg=="], - - "global-prefix": ["global-prefix@1.0.2", "", { "dependencies": { "expand-tilde": "^2.0.2", "homedir-polyfill": "^1.0.1", "ini": "^1.3.4", "is-windows": "^1.0.1", "which": "^1.2.14" } }, "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg=="], - - "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], - - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - - "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], - - "hard-rejection": ["hard-rejection@2.1.0", "", {}, "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA=="], - - "has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "homedir-polyfill": ["homedir-polyfill@1.0.3", "", { "dependencies": { "parse-passwd": "^1.0.0" } }, "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA=="], - - "hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], - - "human-id": ["human-id@4.1.3", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q=="], - - "iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="], - - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - - "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], - - "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], - - "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ini": ["ini@4.1.1", "", {}, "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g=="], - - "inquirer": ["inquirer@8.2.5", "", { "dependencies": { "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.21", "mute-stream": "0.0.8", "ora": "^5.4.1", "run-async": "^2.4.0", "rxjs": "^7.5.5", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", "wrap-ansi": "^7.0.0" } }, "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ=="], - - "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], - - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - - "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], - - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - - "is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], - - "is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], - - "is-subdir": ["is-subdir@1.2.0", "", { "dependencies": { "better-path-resolve": "1.0.0" } }, "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw=="], - - "is-text-path": ["is-text-path@2.0.0", "", { "dependencies": { "text-extensions": "^2.0.0" } }, "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw=="], - - "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], - - "is-utf8": ["is-utf8@0.2.1", "", {}, "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q=="], - - "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], - - "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], - - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "json-parse-better-errors": ["json-parse-better-errors@1.0.2", "", {}, "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="], - - "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], - - "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], - - "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], - - "jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="], - - "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], - - "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], - - "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], - - "lint-staged": ["lint-staged@16.1.6", "", { "dependencies": { "chalk": "^5.6.0", "commander": "^14.0.0", "debug": "^4.4.1", "lilconfig": "^3.1.3", "listr2": "^9.0.3", "micromatch": "^4.0.8", "nano-spawn": "^1.0.2", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.8.1" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-U4kuulU3CKIytlkLlaHcGgKscNfJPNTiDF2avIUGFCv7K95/DCYQ7Ra62ydeRWmgQGg9zJYw2dzdbztwJlqrow=="], - - "listr2": ["listr2@9.0.5", "", { "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g=="], - - "load-json-file": ["load-json-file@4.0.0", "", { "dependencies": { "graceful-fs": "^4.1.2", "parse-json": "^4.0.0", "pify": "^3.0.0", "strip-bom": "^3.0.0" } }, "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw=="], - - "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - - "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - - "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], - - "lodash.ismatch": ["lodash.ismatch@4.4.0", "", {}, "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g=="], - - "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], - - "lodash.kebabcase": ["lodash.kebabcase@4.1.1", "", {}, "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g=="], - - "lodash.map": ["lodash.map@4.6.0", "", {}, "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q=="], - - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - - "lodash.mergewith": ["lodash.mergewith@4.6.2", "", {}, "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="], - - "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], - - "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="], - - "lodash.uniq": ["lodash.uniq@4.5.0", "", {}, "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="], - - "lodash.upperfirst": ["lodash.upperfirst@4.3.1", "", {}, "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg=="], - - "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], - - "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], - - "longest": ["longest@2.0.1", "", {}, "sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q=="], - - "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - - "map-obj": ["map-obj@4.3.0", "", {}, "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ=="], - - "meow": ["meow@8.1.2", "", { "dependencies": { "@types/minimist": "^1.2.0", "camelcase-keys": "^6.2.2", "decamelize-keys": "^1.1.0", "hard-rejection": "^2.1.0", "minimist-options": "4.1.0", "normalize-package-data": "^3.0.0", "read-pkg-up": "^7.0.1", "redent": "^3.0.0", "trim-newlines": "^3.0.0", "type-fest": "^0.18.0", "yargs-parser": "^20.2.3" } }, "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q=="], - - "merge": ["merge@2.1.1", "", {}, "sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w=="], - - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - - "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - - "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], - - "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], - - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "minimist": ["minimist@1.2.7", "", {}, "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g=="], - - "minimist-options": ["minimist-options@4.1.0", "", { "dependencies": { "arrify": "^1.0.1", "is-plain-obj": "^1.1.0", "kind-of": "^6.0.3" } }, "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A=="], - - "modify-values": ["modify-values@1.0.1", "", {}, "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw=="], - - "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "mute-stream": ["mute-stream@0.0.8", "", {}, "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="], - - "nano-spawn": ["nano-spawn@1.0.3", "", {}, "sha512-jtpsQDetTnvS2Ts1fiRdci5rx0VYws5jGyC+4IYOTnIQ/wwdf6JdomlHBwqC3bJYOvaKu0C2GSZ1A60anrYpaA=="], - - "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], - - "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - - "normalize-package-data": ["normalize-package-data@3.0.3", "", { "dependencies": { "hosted-git-info": "^4.0.1", "is-core-module": "^2.5.0", "semver": "^7.3.4", "validate-npm-package-license": "^3.0.1" } }, "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - - "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], - - "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], - - "outdent": ["outdent@0.5.0", "", {}, "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="], - - "p-filter": ["p-filter@2.1.0", "", { "dependencies": { "p-map": "^2.0.0" } }, "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw=="], - - "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - - "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - - "p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="], - - "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], - - "package-manager-detector": ["package-manager-detector@0.2.11", "", { "dependencies": { "quansync": "^0.2.7" } }, "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ=="], - - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - - "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], - - "parse-passwd": ["parse-passwd@1.0.0", "", {}, "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q=="], - - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - - "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - - "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], - - "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], - - "prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], - - "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], - - "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], - - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - - "quick-lru": ["quick-lru@4.0.1", "", {}, "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g=="], - - "read-pkg": ["read-pkg@3.0.0", "", { "dependencies": { "load-json-file": "^4.0.0", "normalize-package-data": "^2.3.2", "path-type": "^3.0.0" } }, "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA=="], - - "read-pkg-up": ["read-pkg-up@7.0.1", "", { "dependencies": { "find-up": "^4.1.0", "read-pkg": "^5.2.0", "type-fest": "^0.8.1" } }, "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg=="], - - "read-yaml-file": ["read-yaml-file@1.1.0", "", { "dependencies": { "graceful-fs": "^4.1.5", "js-yaml": "^3.6.1", "pify": "^4.0.1", "strip-bom": "^3.0.0" } }, "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA=="], - - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - - "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], - - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - - "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "resolve-dir": ["resolve-dir@1.0.1", "", { "dependencies": { "expand-tilde": "^2.0.0", "global-modules": "^1.0.0" } }, "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg=="], - - "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], - - "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], - - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - - "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], - - "run-async": ["run-async@2.4.1", "", {}, "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ=="], - - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - - "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], - - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - - "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - - "slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], - - "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - - "spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "^7.0.5", "signal-exit": "^4.0.1" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="], - - "spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="], - - "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], - - "spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], - - "spdx-license-ids": ["spdx-license-ids@3.0.22", "", {}, "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ=="], - - "split": ["split@1.0.1", "", { "dependencies": { "through": "2" } }, "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg=="], - - "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], - - "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], - - "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], - - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "strip-bom": ["strip-bom@4.0.0", "", {}, "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w=="], - - "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], - - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - - "supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], - - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - - "temp-dir": ["temp-dir@2.0.0", "", {}, "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg=="], - - "tempfile": ["tempfile@3.0.0", "", { "dependencies": { "temp-dir": "^2.0.0", "uuid": "^3.3.2" } }, "sha512-uNFCg478XovRi85iD42egu+eSFUmmka750Jy7L5tfHI5hQKKtbPnxaSaXAbBqCDYrw3wx4tXjKwci4/QmsZJxw=="], - - "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="], - - "text-extensions": ["text-extensions@2.4.0", "", {}, "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g=="], - - "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], - - "through2": ["through2@2.0.5", "", { "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ=="], - - "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], - - "tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="], - - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - - "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], - - "trim-newlines": ["trim-newlines@3.0.1", "", {}, "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "type-fest": ["type-fest@0.18.1", "", {}, "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], - - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="], - - "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], - - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - - "uuid": ["uuid@3.4.0", "", { "bin": { "uuid": "./bin/uuid" } }, "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="], - - "validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="], - - "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], - - "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - - "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - - "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], - - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - - "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], - - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - - "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - - "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], - - "yargs": ["yargs@17.7.2", "", { "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" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - - "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - - "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], - - "@commitlint/parse/conventional-changelog-angular": ["conventional-changelog-angular@7.0.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ=="], - - "@commitlint/read/minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "@commitlint/top-level/find-up": ["find-up@7.0.0", "", { "dependencies": { "locate-path": "^7.2.0", "path-exists": "^5.0.0", "unicorn-magic": "^0.1.0" } }, "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g=="], - - "@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], - - "@manypkg/get-packages/@changesets/types": ["@changesets/types@4.1.0", "", {}, "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw=="], - - "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], - - "@types/conventional-commits-parser/@types/node": ["@types/node@25.0.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA=="], - - "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], - - "cli-truncate/string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="], - - "commitizen/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], - - "conventional-changelog/conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@6.1.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-3cS3GEtR78zTfMzk0AizXKKIdN4OvSh7ibNz6/DPbhWWQu7LqE/8+/GqSodV+sywUR2gpJAdP/1JFf4XtN7Zpw=="], - - "conventional-changelog-core/conventional-commits-parser": ["conventional-commits-parser@4.0.0", "", { "dependencies": { "JSONStream": "^1.3.5", "is-text-path": "^1.0.1", "meow": "^8.1.2", "split2": "^3.2.2" }, "bin": { "conventional-commits-parser": "cli.js" } }, "sha512-WRv5j1FsVM5FISJkoYMR6tPk07fkKT0UodruX4je86V4owk451yjXAKzKAPOs9l7y59E2viHUS9eQ+dfUA9NSg=="], - - "conventional-changelog-core/git-raw-commits": ["git-raw-commits@3.0.0", "", { "dependencies": { "dargs": "^7.0.0", "meow": "^8.1.2", "split2": "^3.2.2" }, "bin": { "git-raw-commits": "cli.js" } }, "sha512-b5OHmZ3vAgGrDn/X0kS+9qCfNKWe4K/jFnhwzVWWg0/k5eLa3060tZShrRg8Dja5kPc+YjS0Gc6y7cRr44Lpjw=="], - - "conventional-changelog-core/read-pkg-up": ["read-pkg-up@3.0.0", "", { "dependencies": { "find-up": "^2.0.0", "read-pkg": "^3.0.0" } }, "sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw=="], - - "conventional-commits-parser/meow": ["meow@12.1.1", "", {}, "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw=="], - - "cz-conventional-changelog/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], - - "decamelize-keys/map-obj": ["map-obj@1.0.1", "", {}, "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg=="], - - "external-editor/chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="], - - "external-editor/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - - "get-pkg-repo/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], - - "git-raw-commits/meow": ["meow@12.1.1", "", {}, "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw=="], - - "git-remote-origin-url/pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], - - "gitconfiglocal/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - - "global-prefix/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - - "global-prefix/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], - - "handlebars/minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - - "inquirer/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "listr2/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], - - "load-json-file/parse-json": ["parse-json@4.0.0", "", { "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" } }, "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw=="], - - "load-json-file/pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="], - - "load-json-file/strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], - - "log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "log-update/ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="], - - "log-update/cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], - - "log-update/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - - "log-update/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], - - "meow/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], - - "ora/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "read-pkg/normalize-package-data": ["normalize-package-data@2.5.0", "", { "dependencies": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" } }, "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA=="], - - "read-pkg/path-type": ["path-type@3.0.0", "", { "dependencies": { "pify": "^3.0.0" } }, "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg=="], - - "read-pkg-up/read-pkg": ["read-pkg@5.2.0", "", { "dependencies": { "@types/normalize-package-data": "^2.4.0", "normalize-package-data": "^2.5.0", "parse-json": "^5.0.0", "type-fest": "^0.6.0" } }, "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg=="], - - "read-pkg-up/type-fest": ["type-fest@0.8.1", "", {}, "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA=="], - - "read-yaml-file/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], - - "read-yaml-file/strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], - - "restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - - "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - - "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], - - "through2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - - "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "@commitlint/top-level/find-up/locate-path": ["locate-path@7.2.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA=="], - - "@commitlint/top-level/find-up/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="], - - "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - - "commitizen/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - - "commitizen/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "conventional-changelog-core/conventional-commits-parser/is-text-path": ["is-text-path@1.0.1", "", { "dependencies": { "text-extensions": "^1.0.0" } }, "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w=="], - - "conventional-changelog-core/conventional-commits-parser/split2": ["split2@3.2.2", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg=="], - - "conventional-changelog-core/git-raw-commits/dargs": ["dargs@7.0.0", "", {}, "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg=="], - - "conventional-changelog-core/git-raw-commits/split2": ["split2@3.2.2", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg=="], - - "conventional-changelog-core/read-pkg-up/find-up": ["find-up@2.1.0", "", { "dependencies": { "locate-path": "^2.0.0" } }, "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ=="], - - "get-pkg-repo/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], - - "get-pkg-repo/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], - - "inquirer/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "inquirer/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "listr2/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - - "listr2/wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - - "listr2/wrap-ansi/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - - "log-symbols/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "log-symbols/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "log-update/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], - - "log-update/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - - "log-update/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - - "log-update/wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - - "ora/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "ora/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "read-pkg-up/read-pkg/normalize-package-data": ["normalize-package-data@2.5.0", "", { "dependencies": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" } }, "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA=="], - - "read-pkg-up/read-pkg/type-fest": ["type-fest@0.6.0", "", {}, "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg=="], - - "read-pkg/normalize-package-data/hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="], - - "read-pkg/normalize-package-data/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], - - "read-pkg/path-type/pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="], - - "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - - "through2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - - "through2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - - "wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "@commitlint/top-level/find-up/locate-path/p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="], - - "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - - "conventional-changelog-core/conventional-commits-parser/is-text-path/text-extensions": ["text-extensions@1.9.0", "", {}, "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ=="], - - "conventional-changelog-core/read-pkg-up/find-up/locate-path": ["locate-path@2.0.0", "", { "dependencies": { "p-locate": "^2.0.0", "path-exists": "^3.0.0" } }, "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA=="], - - "inquirer/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "inquirer/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "listr2/wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], - - "listr2/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - - "log-symbols/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "log-symbols/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "log-update/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - - "log-update/wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], - - "ora/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "ora/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "read-pkg-up/read-pkg/normalize-package-data/hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="], - - "read-pkg-up/read-pkg/normalize-package-data/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], - - "wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "@commitlint/top-level/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="], - - "conventional-changelog-core/read-pkg-up/find-up/locate-path/p-locate": ["p-locate@2.0.0", "", { "dependencies": { "p-limit": "^1.1.0" } }, "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg=="], - - "conventional-changelog-core/read-pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], - - "inquirer/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "log-symbols/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "ora/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "conventional-changelog-core/read-pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@1.3.0", "", { "dependencies": { "p-try": "^1.0.0" } }, "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q=="], - - "conventional-changelog-core/read-pkg-up/find-up/locate-path/p-locate/p-limit/p-try": ["p-try@1.0.0", "", {}, "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww=="], - } -} diff --git a/commitlint.config.js b/commitlint.config.js deleted file mode 100644 index 3347cb96..00000000 --- a/commitlint.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = {extends: ['@commitlint/config-conventional']}; diff --git a/commitzen.config.js b/commitzen.config.js deleted file mode 100644 index 94e30c06..00000000 --- a/commitzen.config.js +++ /dev/null @@ -1,37 +0,0 @@ -module.exports = { - types: [ - {value: 'feat', name: 'feat:\tAdding a new feature'}, - {value: 'fix', name: 'fix:\tFixing a bug'}, - { - value: 'style', - name: 'style: Add or update styles', - }, - { - value: 'refactor', - name: 'refactor:\tCode change that neither fixes a bug nor adds a feature', - }, - { - value: 'perf', - name: '\tCode change that improves performance', - }, - { - value: 'test', - name: 'test:\tAdding tests cases / adds missing tests', - }, - { - value: 'chore', - name: 'chore:\tChanges to the build process or auxiliary tools\n\t\tand libraries such as documentation generation', - }, - {value: 'revert', name: 'revert:\tRevert to a commit'}, - ], - scopes: [ - {name: 'lambda'}, - {name: 'infrastructure'}, - {name: 'documentation'}, - {name: 'ci'}, - ], - allowCustomScopes: true, - allowBreakingChanges: ['feat', 'fix', 'perf', 'refactor'], - subjectLimit: 100, - skipQuestions: ['body'], -}; diff --git a/eslint.config.mjs b/eslint.config.mjs deleted file mode 100644 index 3a48ba0d..00000000 --- a/eslint.config.mjs +++ /dev/null @@ -1,115 +0,0 @@ -import globals from "globals"; -import tseslint from "typescript-eslint"; -import importPlugin from "eslint-plugin-import"; -import jestPlugin from "eslint-plugin-jest"; - -/** @type {import('eslint').Linter.Config[]} */ -export default [ - ...tseslint.configs.recommendedTypeChecked, - importPlugin.flatConfigs.recommended, - { - files: ["apps/**/*.{js,mjs,cjs,ts,jsx,tsx}", "scripts/**/*.{js,mjs,cjs}"], - ignores: ["eslint.config.mjs"], - }, - { - files: ["scripts/**/*.js"], - languageOptions: { - sourceType: "module", - parserOptions: { - ecmaVersion: "latest", - }, - globals: { - ...globals.node, - }, - }, - rules: { - "@typescript-eslint/no-var-requires": "off", - "no-undef": "error", - }, - }, - { - files: ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"], - plugins: { - jest: jestPlugin, - }, - rules: { - ...jestPlugin.configs.recommended.rules, - "jest/no-disabled-tests": "warn", - "jest/no-focused-tests": "error", - "jest/no-identical-title": "error", - "jest/prefer-to-have-length": "warn", - "jest/valid-expect": "error", - }, - }, - { - languageOptions: { - parserOptions: { - projectService: true, - tsconfigRootDir: import.meta.dirname, - }, - globals: { - ...globals.node, - ...globals.jest, - }, - }, - }, - { - settings: { - "import/resolver": { - typescript: { - project: ["./tsconfig.json"], - alwaysTryTypes: true, - extensions: [".ts", ".tsx", ".js", ".jsx"], - }, - node: { - extensions: [".js", ".jsx", ".ts", ".tsx"], - }, - alias: { - map: [ - ["@lambda", "./apps/lho-lambda/src"], - ["@lambda-test", "./apps/lho-lambda/src/test"], - ["@authorizer", "./apps/lho-authorizer/src"], - ["@authorizer-test", "./apps/lho-authorizer/src/test"], - ], - extensions: [".ts", ".tsx", ".js", ".jsx", ".json"], - }, - }, - react: { - version: "detect", - }, - }, - }, - { - rules: { - "arrow-parens": 0, - camelcase: 0, - "comma-dangle": ["error", "always-multiline"], - "consistent-return": 0, - "function-paren-newline": 0, - "global-require": 0, - "implicit-arrow-linebreak": 0, - "import/no-cycle": 0, - "no-console": ["off"], - "no-extra-boolean-cast": 0, - "no-nested-ternary": 0, - "import/no-cycle": "error", - "no-return-assign": 0, - "no-undef": ["warn"], - "no-underscore-dangle": 0, - "no-unused-expressions": 0, - "no-fallthrough": "error", - "@typescript-eslint/no-unused-vars": ["error"], - "object-curly-newline": 0, - "object-curly-spacing": ["error", "always"], - "operator-linebreak": 0, - "quote-props": 0, - quotes: ["error", "single", {avoidEscape: true}], - semi: ["error", "always"], - "spaced-comment": 0, - "no-console": "warn", - "@typescript-eslint/no-unsafe-assignment": "off", - "@typescript-eslint/no-unsafe-call": "off", - "@typescript-eslint/no-unsafe-member-access": "off", - }, - }, -]; diff --git a/package.json b/package.json deleted file mode 100644 index 3c8b87ef..00000000 --- a/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "root", - "version": "1.4.2", - "private": true, - "engineStrict": true, - "engines": { - "node": ">=22", - "bun": "1.3.5" - }, - "scripts": { - "commit": "cz", - "fmt:tf": "terraform fmt terraform", - "changeset": "changeset", - "changeset:version": "changeset version", - "changeset:status": "changeset status" - }, - "devDependencies": { - "@changesets/changelog-github": "^0.5.0", - "@changesets/cli": "^2.29.8", - "@commitlint/cli": "^19.8.1", - "@commitlint/config-conventional": "^19.8.1", - "commitizen": "4.3.1", - "conventional-changelog-cli": "^3.0.0", - "git-cz": "^4.9.0", - "lint-staged": "16.1.6" - }, - "config": { - "commitizen": { - "path": "cz-conventional-changelog" - } - }, - "lint-staged": { - "*.swift": "swiftlint lint" - }, - "husky": { - "hooks": { - "prepare-commit-msg": "exec < /dev/tty && npx cz --hook || true" - } - } -} diff --git a/scripts/run-local.sh b/scripts/run-local.sh index 0e4fe416..e4e66f4a 100755 --- a/scripts/run-local.sh +++ b/scripts/run-local.sh @@ -7,15 +7,11 @@ BLUE='\033[0;34m' NC='\033[0m' echo -e "${BLUE}Building Lambda function...${NC}" -swift build -c release +dotnet build src/Lho.Lambda.Local/Lho.Lambda.Local.csproj echo -e "${GREEN}Starting local Lambda server...${NC}" echo -e "${BLUE}Server will be available at http://localhost:7000/invoke${NC}" echo -e "${BLUE}Press Ctrl+C to stop${NC}" echo "" - - - -LOCAL_LAMBDA_SERVER_ENABLED=true .build/release/lambda - +dotnet run --project src/Lho.Lambda.Local/Lho.Lambda.Local.csproj diff --git a/scripts/update-changelog.sh b/scripts/update-changelog.sh new file mode 100644 index 00000000..e1492277 --- /dev/null +++ b/scripts/update-changelog.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +set -euo pipefail + +version="${1:?version is required}" +tag="v${version}" +repo="${GITHUB_REPOSITORY:-luke-h1/lho-lambda}" +date="${CHANGELOG_DATE:-$(date +%F)}" +changelog="${CHANGELOG_FILE:-CHANGELOG.md}" +previous_tag="${2:-}" + +if [ -z "$previous_tag" ]; then + previous_tag="$(git describe --tags --abbrev=0 --match 'v*' 2>/dev/null || true)" +fi + +if [ -f "$changelog" ] && grep -q "^# \\[${version}\\]" "$changelog"; then + echo "CHANGELOG already contains ${version}" + exit 0 +fi + +range="HEAD" +compare_url="" +if [ -n "$previous_tag" ]; then + range="${previous_tag}..HEAD" + compare_url="https://github.com/${repo}/compare/${previous_tag}...${tag}" +fi + +tmp="$(mktemp)" +section="$(mktemp)" +trap 'rm -f "$tmp" "$section"' EXIT + +if [ -n "$compare_url" ]; then + printf '# [%s](%s) (%s)\n\n' "$version" "$compare_url" "$date" > "$section" +else + printf '# [%s] (%s)\n\n' "$version" "$date" > "$section" +fi + +append_group() { + local title="$1" + local pattern="$2" + local output + + output="$( + git log --reverse --no-merges --pretty=format:'%H%x09%s' "$range" | + awk -F '\t' -v pattern="$pattern" -v repo="$repo" ' + BEGIN { IGNORECASE = 1 } + $2 ~ pattern { + sha = substr($1, 1, 7) + subject = $2 + scope = "" + text = subject + + if (match(subject, /^[a-z]+(\([^)]+\))?!?: /)) { + prefix = substr(subject, 1, RLENGTH - 2) + text = substr(subject, RLENGTH + 1) + if (match(prefix, /\([^)]+\)/)) { + scope = substr(prefix, RSTART + 1, RLENGTH - 2) + } + } + + if (scope != "") { + printf "* **%s:** %s ([%s](https://github.com/%s/commit/%s))\n", scope, text, sha, repo, $1 + } else { + printf "* %s ([%s](https://github.com/%s/commit/%s))\n", text, sha, repo, $1 + } + } + ' + )" + + if [ -n "$output" ]; then + printf '### %s\n\n%s\n\n' "$title" "$output" >> "$section" + fi +} + +append_group "Features" '^feat(\(.+\))?!?: ' +append_group "Bug Fixes" '^fix(\(.+\))?!?: ' +append_group "Performance Improvements" '^perf(\(.+\))?!?: ' +append_group "Other Changes" '^(chore|docs|refactor|test|ci|build|style|revert)(\(.+\))?!?: ' + +if ! grep -q '^\* ' "$section"; then + git log --reverse --no-merges --pretty=format:'* %s ([%h](https://github.com/'"$repo"'/commit/%H))' "$range" >> "$section" + printf '\n\n' >> "$section" +fi + +cat "$section" > "$tmp" +if [ -f "$changelog" ]; then + cat "$changelog" >> "$tmp" +fi +mv "$tmp" "$changelog" diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 00000000..ed548234 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,7 @@ + + + enable + enable + false + + diff --git a/src/Lho.Lambda.Authorizer/Functions/AuthorizerFunction.cs b/src/Lho.Lambda.Authorizer/Functions/AuthorizerFunction.cs new file mode 100644 index 00000000..278992f6 --- /dev/null +++ b/src/Lho.Lambda.Authorizer/Functions/AuthorizerFunction.cs @@ -0,0 +1,67 @@ +using System.Security.Cryptography; +using System.Text; +using Amazon.Lambda.Core; +using Lho.Lambda.Authorizer.Models; + +namespace Lho.Lambda.Authorizer.Functions; + +public class AuthorizerFunction +{ + private static readonly HashSet ValidConsumers = ["lhowsam-dev", "lhowsam-prod", "lhowsam-local"]; + + public Task FunctionHandler(AuthorizerRequest request, ILambdaContext context) + { + context.Logger.LogLine("Authorizer invoked"); + + var apiKey = GetHeaderValue(request.Headers, "x-api-key"); + var validKey = Environment.GetEnvironmentVariable("API_KEY"); + + if (!SecureCompare(apiKey, validKey)) + { + context.Logger.LogLine("Deny - API key invalid"); + return Task.FromResult(new AuthorizerSimpleResponse(false)); + } + + var consumer = GetHeaderValue(request.Headers, "x-consumer"); + if (consumer is not null && !ValidConsumers.Contains(consumer)) + { + context.Logger.LogLine("Deny - Invalid consumer"); + return Task.FromResult(new AuthorizerSimpleResponse(false)); + } + + context.Logger.LogLine("Allow"); + return Task.FromResult(new AuthorizerSimpleResponse(true)); + } + + private static string? GetHeaderValue(IReadOnlyDictionary? headers, string key) + { + if (headers is null) + { + return null; + } + + foreach (var (headerKey, value) in headers) + { + if (string.Equals(headerKey, key, StringComparison.OrdinalIgnoreCase)) + { + return value; + } + } + + return null; + } + + private static bool SecureCompare(string? first, string? second) + { + if (first is null || second is null) + { + return first is null && second is null; + } + + var firstBytes = Encoding.UTF8.GetBytes(first); + var secondBytes = Encoding.UTF8.GetBytes(second); + + return firstBytes.Length == secondBytes.Length && + CryptographicOperations.FixedTimeEquals(firstBytes, secondBytes); + } +} diff --git a/src/Lho.Lambda.Authorizer/Lho.Lambda.Authorizer.csproj b/src/Lho.Lambda.Authorizer/Lho.Lambda.Authorizer.csproj new file mode 100644 index 00000000..ff5922f1 --- /dev/null +++ b/src/Lho.Lambda.Authorizer/Lho.Lambda.Authorizer.csproj @@ -0,0 +1,11 @@ + + + net8.0 + true + + + + + + + diff --git a/src/Lho.Lambda.Authorizer/Models/AuthorizerRequest.cs b/src/Lho.Lambda.Authorizer/Models/AuthorizerRequest.cs new file mode 100644 index 00000000..642f6d05 --- /dev/null +++ b/src/Lho.Lambda.Authorizer/Models/AuthorizerRequest.cs @@ -0,0 +1,37 @@ +using System.Text.Json.Serialization; + +namespace Lho.Lambda.Authorizer.Models; + +public class AuthorizerRequest +{ + public string? Version { get; init; } + + public string? Type { get; init; } + + public string? RouteArn { get; init; } + + public string? RouteKey { get; init; } + + public string? RawPath { get; init; } + + public string? RawQueryString { get; init; } + + public Dictionary? Headers { get; init; } + + public AuthorizerRequestContext? RequestContext { get; init; } +} + +public class AuthorizerRequestContext +{ + public HttpContext? Http { get; init; } +} + +public class HttpContext +{ + public string? Method { get; init; } + + public string? Path { get; init; } +} + +public record AuthorizerSimpleResponse( + [property: JsonPropertyName("isAuthorized")] bool IsAuthorized); diff --git a/src/Lho.Lambda.Authorizer/Properties/AssemblyInfo.cs b/src/Lho.Lambda.Authorizer/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..ebee51cb --- /dev/null +++ b/src/Lho.Lambda.Authorizer/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using Amazon.Lambda.Core; + +[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] diff --git a/src/Lho.Lambda.Local/Lho.Lambda.Local.csproj b/src/Lho.Lambda.Local/Lho.Lambda.Local.csproj new file mode 100644 index 00000000..7f7fb383 --- /dev/null +++ b/src/Lho.Lambda.Local/Lho.Lambda.Local.csproj @@ -0,0 +1,17 @@ + + + Exe + net8.0 + enable + enable + true + Lambda + + true + + true + + + + + diff --git a/src/Lho.Lambda.Local/Program.cs b/src/Lho.Lambda.Local/Program.cs new file mode 100644 index 00000000..412a008d --- /dev/null +++ b/src/Lho.Lambda.Local/Program.cs @@ -0,0 +1,97 @@ +using System.Net; +using System.Text; +using System.Text.Json; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.Core; +using Lho.Lambda.Functions; + +var listener = new HttpListener(); +listener.Prefixes.Add("http://localhost:7000/"); +listener.Start(); + +Console.WriteLine("Listening on http://localhost:7000/invoke"); + +var function = new ApiFunction(); +var jsonOptions = new JsonSerializerOptions +{ + PropertyNameCaseInsensitive = true +}; + +while (true) +{ + var context = await listener.GetContextAsync(); + _ = Task.Run(async () => + { + try + { + if (context.Request.HttpMethod != "POST" || context.Request.Url?.AbsolutePath != "/invoke") + { + context.Response.StatusCode = 404; + context.Response.Close(); + return; + } + + using var reader = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding); + var body = await reader.ReadToEndAsync(); + var request = JsonSerializer.Deserialize(body, jsonOptions) + ?? new APIGatewayHttpApiV2ProxyRequest(); + + var response = await function.FunctionHandler(request, new LocalLambdaContext()); + context.Response.StatusCode = response.StatusCode; + + foreach (var (key, value) in response.Headers) + { + context.Response.Headers[key] = value; + } + + var bytes = Encoding.UTF8.GetBytes(response.Body ?? ""); + context.Response.ContentLength64 = bytes.Length; + await context.Response.OutputStream.WriteAsync(bytes); + context.Response.Close(); + } + catch (Exception exception) + { + Console.Error.WriteLine(exception); + context.Response.StatusCode = 500; + context.Response.Close(); + } + }); +} + +internal class LocalLambdaContext : ILambdaContext +{ + public string AwsRequestId => Guid.NewGuid().ToString("N"); + + public IClientContext ClientContext => null!; + + public string FunctionName => "lho-lambda-local"; + + public string FunctionVersion => "local"; + + public ICognitoIdentity Identity => null!; + + public string InvokedFunctionArn => "local"; + + public ILambdaLogger Logger { get; } = new LocalLambdaLogger(); + + public string LogGroupName => "local"; + + public string LogStreamName => "local"; + + public int MemoryLimitInMB => 256; + + public TimeSpan RemainingTime => TimeSpan.FromMinutes(5); +} + +internal class LocalLambdaLogger : ILambdaLogger +{ + public void Log(string message) + { + Console.Write(message); + } + + public void LogLine(string message) + { + Console.WriteLine(message); + } +} diff --git a/src/Lho.Lambda.Tests/ApiFunctionTests.cs b/src/Lho.Lambda.Tests/ApiFunctionTests.cs new file mode 100644 index 00000000..b9d01d5c --- /dev/null +++ b/src/Lho.Lambda.Tests/ApiFunctionTests.cs @@ -0,0 +1,76 @@ +using System.Net; +using System.Text.Json; +using Amazon.Lambda.APIGatewayEvents; +using Lho.Lambda.Functions; +using Xunit; + +namespace Lho.Lambda.Tests; + +public class ApiFunctionTests +{ + [Fact] + public async Task HealthEndpointReturnsOkWithoutCacheHeader() + { + var function = new ApiFunction(); + + var response = await function.FunctionHandler(CreateRequest("/test/api/health"), new TestLambdaContext()); + + Assert.Equal((int)HttpStatusCode.OK, response.StatusCode); + Assert.Equal("no-cache", response.Headers["Cache-Control"]); + Assert.Equal("application/json", response.Headers["content-type"]); + Assert.Equal("OK", JsonDocument.Parse(response.Body).RootElement.GetProperty("status").GetString()); + } + + [Fact] + public async Task MissingRouteReturnsNotFound() + { + var function = new ApiFunction(); + + var response = await function.FunctionHandler(CreateRequest("/api/missing"), new TestLambdaContext()); + + Assert.Equal((int)HttpStatusCode.NotFound, response.StatusCode); + Assert.Equal("Not Found", JsonDocument.Parse(response.Body).RootElement.GetProperty("error").GetString()); + } + + [Fact] + public async Task VersionEndpointUsesDeploymentEnvironmentValues() + { + SetEnvironment("VERSION", "1.2.3"); + SetEnvironment("DEPLOYED_AT", "2026-05-10T09:00:00Z"); + SetEnvironment("DEPLOYED_BY", "tests"); + SetEnvironment("GIT_SHA", "abc123"); + + var function = new ApiFunction(); + + var response = await function.FunctionHandler(CreateRequest("/invoke/api/version"), new TestLambdaContext()); + var body = JsonDocument.Parse(response.Body).RootElement; + + Assert.Equal((int)HttpStatusCode.OK, response.StatusCode); + Assert.Equal("1.2.3", body.GetProperty("version").GetString()); + Assert.Equal("2026-05-10T09:00:00Z", body.GetProperty("deployedAt").GetString()); + Assert.Equal("tests", body.GetProperty("deployedBy").GetString()); + Assert.Equal("abc123", body.GetProperty("gitSha").GetString()); + } + + private static APIGatewayHttpApiV2ProxyRequest CreateRequest(string path) + { + return new APIGatewayHttpApiV2ProxyRequest + { + RawPath = path, + RawQueryString = "", + RequestContext = new APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext + { + Http = new APIGatewayHttpApiV2ProxyRequest.HttpDescription + { + Method = "GET", + Path = path + } + } + }; + } + + private static void SetEnvironment(string key, string value) + { + Environment.SetEnvironmentVariable(key, value); + } +} diff --git a/src/Lho.Lambda.Tests/AuthorizerFunctionTests.cs b/src/Lho.Lambda.Tests/AuthorizerFunctionTests.cs new file mode 100644 index 00000000..f90488a3 --- /dev/null +++ b/src/Lho.Lambda.Tests/AuthorizerFunctionTests.cs @@ -0,0 +1,66 @@ +using Lho.Lambda.Authorizer.Functions; +using Lho.Lambda.Authorizer.Models; +using Xunit; + +namespace Lho.Lambda.Tests; + +public class AuthorizerFunctionTests +{ + [Fact] + public async Task AuthorizesKnownConsumerWithMatchingApiKey() + { + Environment.SetEnvironmentVariable("API_KEY", "secret"); + var function = new AuthorizerFunction(); + + var response = await function.FunctionHandler( + CreateRequest(new Dictionary + { + ["X-API-Key"] = "secret", + ["X-Consumer"] = "lhowsam-prod" + }), + new TestLambdaContext()); + + Assert.True(response.IsAuthorized); + } + + [Fact] + public async Task DeniesUnknownConsumer() + { + Environment.SetEnvironmentVariable("API_KEY", "secret"); + var function = new AuthorizerFunction(); + + var response = await function.FunctionHandler( + CreateRequest(new Dictionary + { + ["x-api-key"] = "secret", + ["x-consumer"] = "someone-else" + }), + new TestLambdaContext()); + + Assert.False(response.IsAuthorized); + } + + [Fact] + public async Task DeniesMismatchedApiKey() + { + Environment.SetEnvironmentVariable("API_KEY", "secret"); + var function = new AuthorizerFunction(); + + var response = await function.FunctionHandler( + CreateRequest(new Dictionary + { + ["x-api-key"] = "wrong" + }), + new TestLambdaContext()); + + Assert.False(response.IsAuthorized); + } + + private static AuthorizerRequest CreateRequest(Dictionary headers) + { + return new AuthorizerRequest + { + Headers = headers + }; + } +} diff --git a/src/Lho.Lambda.Tests/Lho.Lambda.Tests.csproj b/src/Lho.Lambda.Tests/Lho.Lambda.Tests.csproj new file mode 100644 index 00000000..9f610d08 --- /dev/null +++ b/src/Lho.Lambda.Tests/Lho.Lambda.Tests.csproj @@ -0,0 +1,24 @@ + + + net8.0 + enable + enable + false + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/src/Lho.Lambda.Tests/NowPlayingServiceTests.cs b/src/Lho.Lambda.Tests/NowPlayingServiceTests.cs new file mode 100644 index 00000000..feb958a5 --- /dev/null +++ b/src/Lho.Lambda.Tests/NowPlayingServiceTests.cs @@ -0,0 +1,61 @@ +using System.Net; +using System.Text; +using Lho.Lambda.Clients.LastFm; +using Lho.Lambda.Clients.Spotify; +using Lho.Lambda.Services; +using Lho.Lambda.Utils; +using Xunit; + +namespace Lho.Lambda.Tests; + +public class NowPlayingServiceTests +{ + [Fact] + public async Task SpotifyReturnsEmptyResponseWhenItemIsNotPlaying() + { + Environment.SetEnvironmentVariable("SHOULD_CALL_SPOTIFY", "true"); + var spotifyApi = new SpotifyApi( + new HttpClient(new StaticJsonHandler(""" + { + "is_playing": false, + "item": { + "name": "Paused song", + "artists": [{ "name": "Paused artist" }], + "album": { + "name": "Paused album", + "images": [{ "url": "https://example.com/cover.jpg" }] + }, + "external_urls": { "spotify": "https://open.spotify.com/track/paused" } + } + } + """)), + "https://api.spotify.test/v1", + accessToken: "test-token"); + var lastFmApi = new LastFmApi(new HttpClient(new StaticJsonHandler("{}")), "https://lastfm.test/", "api-key", "user"); + var service = new NowPlayingService(new MemoryCache(), spotifyApi, lastFmApi, new TestLambdaLogger()); + + var response = await service.HandleNowPlaying("spotify"); + + Assert.False(response.IsPlaying); + Assert.Null(response.Maintenance); + Assert.Equal(200, response.Status); + Assert.Equal("", response.Album); + Assert.Equal("", response.AlbumImageUrl); + Assert.Equal("", response.Artist); + Assert.Equal("", response.SongUrl); + Assert.Equal("", response.Title); + } + + private sealed class StaticJsonHandler(string json) : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(json, Encoding.UTF8, "application/json") + }; + + return Task.FromResult(response); + } + } +} diff --git a/src/Lho.Lambda.Tests/TestLambdaContext.cs b/src/Lho.Lambda.Tests/TestLambdaContext.cs new file mode 100644 index 00000000..9fb2699b --- /dev/null +++ b/src/Lho.Lambda.Tests/TestLambdaContext.cs @@ -0,0 +1,39 @@ +using Amazon.Lambda.Core; + +namespace Lho.Lambda.Tests; + +public class TestLambdaContext : ILambdaContext +{ + public string AwsRequestId => "test-request"; + + public IClientContext ClientContext => null!; + + public string FunctionName => "test-function"; + + public string FunctionVersion => "test-version"; + + public ICognitoIdentity Identity => null!; + + public string InvokedFunctionArn => "arn:aws:lambda:eu-west-2:123456789012:function:test"; + + public ILambdaLogger Logger { get; } = new TestLambdaLogger(); + + public string LogGroupName => "/aws/lambda/test"; + + public string LogStreamName => "test-stream"; + + public int MemoryLimitInMB => 256; + + public TimeSpan RemainingTime => TimeSpan.FromMinutes(1); +} + +public class TestLambdaLogger : ILambdaLogger +{ + public void Log(string message) + { + } + + public void LogLine(string message) + { + } +} diff --git a/src/Lho.Lambda/Clients/LastFm/LastFmApi.cs b/src/Lho.Lambda/Clients/LastFm/LastFmApi.cs new file mode 100644 index 00000000..168bdb52 --- /dev/null +++ b/src/Lho.Lambda/Clients/LastFm/LastFmApi.cs @@ -0,0 +1,78 @@ +using System.Text.Json; +using Lho.Lambda.Models; +using Lho.Lambda.Utils; + +namespace Lho.Lambda.Clients.LastFm; + +public class LastFmApi +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + private readonly HttpClient _httpClient; + private readonly string _baseUrl; + private readonly string? _apiKey; + private readonly string? _username; + + + public LastFmApi() + : this(new HttpClient { Timeout = TimeSpan.FromSeconds(10) }, "https://ws.audioscrobbler.com/2.0/") + { + } + + public LastFmApi(HttpClient httpClient, string baseUrl, string? apiKey = null, string? username = null) + { + _httpClient = httpClient; + _baseUrl = baseUrl; + _apiKey = apiKey ?? EnvironmentConfig.LastFm.ApiKey; + _username = username ?? EnvironmentConfig.LastFm.Username; + } + + public async Task GetRecentTracks() + { + if (string.IsNullOrEmpty(_apiKey)) + { + throw new LastFmServiceException("Missing Last.fm API key"); + } + + if (string.IsNullOrEmpty(_username)) + { + throw new LastFmServiceException("Missing Last.fm username"); + } + + var requestUri = $"{_baseUrl}?method=user.getrecenttracks&user={Uri.EscapeDataString(_username)}&api_key={Uri.EscapeDataString(_apiKey)}&format=json&limit=1"; + + var response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri)); + await EnsureSuccess(response); + + return await ReadJson(response) ?? + throw new LastFmServiceException("Empty response from last.fm API"); + } + + private static async Task EnsureSuccess(HttpResponseMessage response) + { + if (response.IsSuccessStatusCode) + { + return; + } + + var body = await response.Content.ReadAsStringAsync(); + throw new LastFmServiceException($"HTTP error with status code: {(int)response.StatusCode} - {body}"); + } + + private static async Task ReadJson(HttpResponseMessage response) + { + var body = await response.Content.ReadAsStringAsync(); + // ReSharper disable once ConvertIfStatementToReturnStatement + if (string.IsNullOrWhiteSpace(body)) + { + throw new LastFmServiceException("Empty response from Last.fm API"); + } + + return JsonSerializer.Deserialize(body, JsonOptions); + } +} + +public class LastFmServiceException(string message) : Exception(message); diff --git a/src/Lho.Lambda/Clients/Spotify/SpotifyApi.cs b/src/Lho.Lambda/Clients/Spotify/SpotifyApi.cs new file mode 100644 index 00000000..74f90a89 --- /dev/null +++ b/src/Lho.Lambda/Clients/Spotify/SpotifyApi.cs @@ -0,0 +1,138 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using Lho.Lambda.Models; +using Lho.Lambda.Utils; + +namespace Lho.Lambda.Clients.Spotify; + +public class SpotifyApi +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + private readonly HttpClient _httpClient; + private readonly string _baseUrl; + private readonly string? _accessToken; + private readonly string? _clientId; + private readonly string? _clientSecret; + private readonly string? _refreshToken; + private string? _cachedAccessToken; + private DateTimeOffset? _tokenExpiresAt; + + public SpotifyApi() + : this(new HttpClient { Timeout = TimeSpan.FromSeconds(10) }, "https://api.spotify.com/v1") + { + } + + public SpotifyApi(HttpClient httpClient, string baseUrl, string? accessToken = null) + { + _httpClient = httpClient; + _baseUrl = baseUrl; + _accessToken = accessToken ?? EnvironmentConfig.Spotify.AccessToken; + _clientId = EnvironmentConfig.Spotify.ClientId; + _clientSecret = EnvironmentConfig.Spotify.ClientSecret; + _refreshToken = EnvironmentConfig.Spotify.RefreshToken; + } + + public async Task GetNowPlaying() + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{_baseUrl}/me/player/currently-playing"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await GetAccessToken()); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + var response = await _httpClient.SendAsync(request); + if (response.StatusCode == HttpStatusCode.NoContent) + { + return null; + } + + await EnsureSuccess(response); + return await ReadJson(response); + } + + public async Task GetTopTracks(string timeRange = "medium_term", int limit = 10) + { + var boundedLimit = Math.Clamp(limit, 1, 50); + var requestUri = $"{_baseUrl}/me/top/tracks?time_range={Uri.EscapeDataString(timeRange)}&limit={boundedLimit}"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await GetAccessToken()); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + var response = await _httpClient.SendAsync(request); + await EnsureSuccess(response); + + return await ReadJson(response) + ?? throw new SpotifyServiceException("Empty response from Spotify API"); + } + + private async Task GetAccessToken() + { + if (!string.IsNullOrEmpty(_accessToken)) + { + return _accessToken; + } + + if (!string.IsNullOrEmpty(_cachedAccessToken) && _tokenExpiresAt > DateTimeOffset.UtcNow) + { + return _cachedAccessToken; + } + + if (string.IsNullOrEmpty(_refreshToken)) + { + throw new SpotifyServiceException("Missing Spotify refresh token"); + } + + if (string.IsNullOrEmpty(_clientId) || string.IsNullOrEmpty(_clientSecret)) + { + throw new SpotifyServiceException("Missing Spotify client ID or client secret"); + } + + using var request = new HttpRequestMessage(HttpMethod.Post, "https://accounts.spotify.com/api/token"); + var credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_clientId}:{_clientSecret}")); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials); + request.Content = new FormUrlEncodedContent(new Dictionary + { + ["grant_type"] = "refresh_token", + ["refresh_token"] = _refreshToken + }); + + var response = await _httpClient.SendAsync(request); + await EnsureSuccess(response); + + var tokenResponse = await ReadJson(response) + ?? throw new SpotifyServiceException("Empty response from token endpoint"); + + _cachedAccessToken = tokenResponse.AccessToken; + _tokenExpiresAt = DateTimeOffset.UtcNow.AddSeconds(tokenResponse.ExpiresIn - 60); + + return tokenResponse.AccessToken; + } + + private static async Task EnsureSuccess(HttpResponseMessage response) + { + if (response.IsSuccessStatusCode) + { + return; + } + + var body = await response.Content.ReadAsStringAsync(); + throw new SpotifyServiceException($"HTTP error with status code: {(int)response.StatusCode} - {body}"); + } + + private static async Task ReadJson(HttpResponseMessage response) + { + var body = await response.Content.ReadAsStringAsync(); + if (string.IsNullOrWhiteSpace(body)) + { + throw new SpotifyServiceException("Empty response from Spotify API"); + } + + return JsonSerializer.Deserialize(body, JsonOptions); + } +} + +public class SpotifyServiceException(string message) : Exception(message); diff --git a/src/Lho.Lambda/Functions/ApiFunction.cs b/src/Lho.Lambda/Functions/ApiFunction.cs new file mode 100644 index 00000000..c8986e0d --- /dev/null +++ b/src/Lho.Lambda/Functions/ApiFunction.cs @@ -0,0 +1,130 @@ +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.Core; +using Lho.Lambda.Clients.LastFm; +using Lho.Lambda.Clients.Spotify; +using Lho.Lambda.Services; +using Lho.Lambda.Utils; + +namespace Lho.Lambda.Functions; + + +public class ApiFunction +{ + private static readonly MemoryCache Cache = new(); + private static readonly SpotifyApi SpotifyApi = new(); + private static readonly LastFmApi LastFmApi = new(); + + private readonly MemoryCache _cache; + private readonly SpotifyApi _spotifyApi; + private readonly LastFmApi _lastFmApi; + + public ApiFunction() + : this(Cache, SpotifyApi, LastFmApi) + { + + } + + public ApiFunction(MemoryCache cache, SpotifyApi spotifyApi, LastFmApi lastFmApi) + { + _cache = cache; + _spotifyApi = spotifyApi; + _lastFmApi = lastFmApi; + } + + public async Task FunctionHandler( + APIGatewayHttpApiV2ProxyRequest request, + ILambdaContext ctx + ) + { + var method = request.RequestContext?.Http?.Method ?? "GET"; + var path = NormalisePath(request.RawPath); + ctx.Logger.LogLine($"Request {method} {path}"); + + if (!string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase)) + { + return ResponseBuilder.ErrorResponse(404, "Not Found"); + } + + return path switch + { + "/api/health" => ResponseBuilder.CreateResponse(new { status = "OK" }, includeCacheControl: false), + "/api/version" => ResponseBuilder.CreateResponse(VersionService.GetVersion(), includeCacheControl: false), + "/api/now-playing" => await HandleNowPlaying(request, ctx), + "/api/top-tracks" => await HandleTopTracks(request, ctx), + _ => ResponseBuilder.ErrorResponse(404, "Not Found") + }; + } + + + private async Task HandleNowPlaying( + APIGatewayHttpApiV2ProxyRequest request, + ILambdaContext context) + { + var provider = QueryStringParser.Get( + request.RawQueryString, + "provider", + "lastfm", + ["lastfm", "spotify"]); + var response = await new NowPlayingService(_cache, _spotifyApi, _lastFmApi, context.Logger).HandleNowPlaying(provider); + + return ResponseBuilder.CreateResponse(response, revalidateSeconds: 3); + } + + private async Task HandleTopTracks( + APIGatewayHttpApiV2ProxyRequest request, + ILambdaContext context) + { + var timeRange = QueryStringParser.Get( + request.RawQueryString, + "time_range", + "medium_term", + ["short_term", "medium_term", "long_term"]); + var rawLimit = QueryStringParser.Get(request.RawQueryString, "limit", "20"); + var limit = int.TryParse(rawLimit, out var parsedLimit) ? Math.Clamp(parsedLimit, 1, 50) : 20; + + try + { + var response = await new TopTracksService(_spotifyApi, context.Logger).HandleTopTracks(timeRange, limit); + return ResponseBuilder.CreateResponse(response, revalidateSeconds: 300); + } + catch (Exception exception) + { + context.Logger.LogLine($"Top tracks failed: {exception}"); + return ResponseBuilder.ErrorResponse(500, "Unable to fetch top tracks"); + } + } + + private static string NormalisePath(string? rawPath) + { + var path = string.IsNullOrEmpty(rawPath) ? "/" : rawPath; + var knownStages = new[] { "/test", "/staging", "/live", "/prod" }; + + foreach (var stage in knownStages) + { + if (path.StartsWith(stage + "/", StringComparison.Ordinal)) + { + path = path[stage.Length..]; + break; + } + + if (string.Equals(path, stage, StringComparison.Ordinal)) + { + path = "/"; + break; + } + } + + if (!path.StartsWith("/invoke", StringComparison.Ordinal)) + { + return path; + } + + path = path[7..]; + return path switch + { + "" => "/", + var value when value.StartsWith("/", StringComparison.Ordinal) => value, + _ => "/" + path + }; + } +} diff --git a/src/Lho.Lambda/Lho.Lambda.csproj b/src/Lho.Lambda/Lho.Lambda.csproj new file mode 100644 index 00000000..a11fcc5a --- /dev/null +++ b/src/Lho.Lambda/Lho.Lambda.csproj @@ -0,0 +1,12 @@ + + + net8.0 + true + + + + + + + + diff --git a/src/Lho.Lambda/Models/LastFm.cs b/src/Lho.Lambda/Models/LastFm.cs new file mode 100644 index 00000000..48d0b651 --- /dev/null +++ b/src/Lho.Lambda/Models/LastFm.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace Lho.Lambda.Models; + +public record LastFmRecentTracksResponse( + [property: JsonPropertyName("recenttracks")] LastFmRecentTracks RecentTracks); + +public record LastFmRecentTracks( + [property: JsonPropertyName("track")] IReadOnlyList Tracks); + +public record LastFmTrack( + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("artist")] LastFmTextValue Artist, + [property: JsonPropertyName("album")] LastFmTextValue Album, + [property: JsonPropertyName("url")] string Url, + [property: JsonPropertyName("image")] IReadOnlyList Images, + [property: JsonPropertyName("@attr")] LastFmTrackAttributes? Attributes); + +public record LastFmTextValue( + [property: JsonPropertyName("#text")] string Text); + +public record LastFmImage( + [property: JsonPropertyName("#text")] string Url, + [property: JsonPropertyName("size")] string Size); + +public record LastFmTrackAttributes( + [property: JsonPropertyName("nowplaying")] string? NowPlaying); diff --git a/src/Lho.Lambda/Models/Spotify.cs b/src/Lho.Lambda/Models/Spotify.cs new file mode 100644 index 00000000..14ce6546 --- /dev/null +++ b/src/Lho.Lambda/Models/Spotify.cs @@ -0,0 +1,58 @@ +using System.Text.Json.Serialization; + +namespace Lho.Lambda.Models; + +public record NowPlayingResponse( + bool IsPlaying, + bool? Maintenance, + int Status, + string Album, + string AlbumImageUrl, + string Artist, + string SongUrl, + string Title); + +public record SpotifyResponse( + [property: JsonPropertyName("is_playing")] bool IsPlaying, + SpotifyItem? Item); + +public record SpotifyItem( + string Name, + IReadOnlyList Artists, + SpotifyAlbum Album, + [property: JsonPropertyName("external_urls")] SpotifyExternalUrls ExternalUrls); + +public record SpotifyArtist(string Name); + +public record SpotifyImage( + string Url, + int? Height, + int? Width); + +public record SpotifyAlbum( + string Name, + IReadOnlyList Images); + +public record SpotifyExternalUrls(string Spotify); + +public record SpotifyTopTracksResponse(IReadOnlyList Items); + +public record TopTrackResponseItem( + string Title, + string Artist, + string Album, + string AlbumImageUrl, + string SongUrl); + +public record TopTracksApiResponse(IReadOnlyList Tracks); + +public record VersionResponse( + string Version, + string DeployedAt, + string DeployedBy, + string GitSha); + +public record TokenResponse( + [property: JsonPropertyName("access_token")] string AccessToken, + [property: JsonPropertyName("token_type")] string TokenType, + [property: JsonPropertyName("expires_in")] int ExpiresIn); diff --git a/src/Lho.Lambda/Properties/AssemblyInfo.cs b/src/Lho.Lambda/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..ebee51cb --- /dev/null +++ b/src/Lho.Lambda/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using Amazon.Lambda.Core; + +[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] diff --git a/src/Lho.Lambda/Services/NowPlayingService.cs b/src/Lho.Lambda/Services/NowPlayingService.cs new file mode 100644 index 00000000..ef1997c9 --- /dev/null +++ b/src/Lho.Lambda/Services/NowPlayingService.cs @@ -0,0 +1,110 @@ +using Amazon.Lambda.Core; +using Lho.Lambda.Clients.LastFm; +using Lho.Lambda.Clients.Spotify; +using Lho.Lambda.Models; +using Lho.Lambda.Utils; + +namespace Lho.Lambda.Services; + +public class NowPlayingService(MemoryCache cache, SpotifyApi spotifyApi, LastFmApi lastFmApi, ILambdaLogger logger) +{ + private const string LastFmProvider = "lastfm"; + private const string SpotifyProvider = "spotify"; + + public async Task HandleNowPlaying(string provider = LastFmProvider) + { + try + { + var cacheKey = $"NowPlaying:{provider}"; + var cachedResponse = cache.Get(cacheKey); + if (cachedResponse is not null) + { + logger.LogLine("Returning cached now playing response"); + return cachedResponse; + } + + var response = provider switch + { + SpotifyProvider => await HandleSpotifyNowPlaying(), + _ => await HandleLastFmNowPlaying() + }; + + if (response.Status == 200 && !string.IsNullOrEmpty(response.Title)) + { + cache.Set(cacheKey, response, TimeSpan.FromSeconds(5)); + } + + return response; + } + catch (Exception exception) + { + logger.LogLine($"Error fetching now playing data from {provider}: {exception}"); + return EmptyResponse(maintenance: null, status: 500); + } + } + + private async Task HandleLastFmNowPlaying() + { + var recentTracksResponse = await lastFmApi.GetRecentTracks(); + var track = recentTracksResponse.RecentTracks.Tracks.FirstOrDefault(); + if (track is null || !IsNowPlaying(track)) + { + logger.LogLine("No song currently playing"); + return EmptyResponse(maintenance: null, status: 200); + } + + return new NowPlayingResponse( + IsPlaying: true, + Maintenance: null, + Status: 200, + Album: track.Album.Text, + AlbumImageUrl: track.Images.LastOrDefault(image => !string.IsNullOrEmpty(image.Url))?.Url ?? "", + Artist: track.Artist.Text, + SongUrl: track.Url, + Title: track.Name); + } + + private async Task HandleSpotifyNowPlaying() + { + if (!EnvironmentConfig.ShouldCallSpotify) + { + return EmptyResponse(maintenance: true, status: 200); + } + + var nowPlayingResponse = await spotifyApi.GetNowPlaying(); + if (nowPlayingResponse?.Item is null || !nowPlayingResponse.IsPlaying) + { + logger.LogLine("No song currently playing"); + return EmptyResponse(maintenance: null, status: 200); + } + + var item = nowPlayingResponse.Item; + return new NowPlayingResponse( + IsPlaying: nowPlayingResponse.IsPlaying, + Maintenance: null, + Status: 200, + Album: item.Album.Name, + AlbumImageUrl: item.Album.Images.FirstOrDefault()?.Url ?? "", + Artist: string.Join(", ", item.Artists.Select(artist => artist.Name)), + SongUrl: item.ExternalUrls.Spotify, + Title: item.Name); + } + + private static bool IsNowPlaying(LastFmTrack track) + { + return string.Equals(track.Attributes?.NowPlaying, "true", StringComparison.OrdinalIgnoreCase); + } + + private static NowPlayingResponse EmptyResponse(bool? maintenance, int status) + { + return new NowPlayingResponse( + IsPlaying: false, + Maintenance: maintenance, + Status: status, + Album: "", + AlbumImageUrl: "", + Artist: "", + SongUrl: "", + Title: ""); + } +} diff --git a/src/Lho.Lambda/Services/TopTracksService.cs b/src/Lho.Lambda/Services/TopTracksService.cs new file mode 100644 index 00000000..7b6a3dd5 --- /dev/null +++ b/src/Lho.Lambda/Services/TopTracksService.cs @@ -0,0 +1,31 @@ +using Amazon.Lambda.Core; +using Lho.Lambda.Clients.Spotify; +using Lho.Lambda.Models; + +namespace Lho.Lambda.Services; + +public class TopTracksService(SpotifyApi spotifyApi, ILambdaLogger logger) +{ + public async Task HandleTopTracks(string timeRange, int limit) + { + try + { + var response = await spotifyApi.GetTopTracks(timeRange, limit); + var tracks = response.Items + .Select(item => new TopTrackResponseItem( + Title: item.Name, + Artist: string.Join(", ", item.Artists.Select(artist => artist.Name)), + Album: item.Album.Name, + AlbumImageUrl: item.Album.Images.FirstOrDefault()?.Url ?? "", + SongUrl: item.ExternalUrls.Spotify)) + .ToArray(); + + return new TopTracksApiResponse(tracks); + } + catch (Exception exception) + { + logger.LogLine($"Top tracks fetch failed: {exception}"); + throw; + } + } +} diff --git a/src/Lho.Lambda/Services/VersionService.cs b/src/Lho.Lambda/Services/VersionService.cs new file mode 100644 index 00000000..ae4ca952 --- /dev/null +++ b/src/Lho.Lambda/Services/VersionService.cs @@ -0,0 +1,16 @@ +using Lho.Lambda.Models; +using Lho.Lambda.Utils; + +namespace Lho.Lambda.Services; + +public static class VersionService +{ + public static VersionResponse GetVersion() + { + return new VersionResponse( + Version: EnvironmentConfig.Deploy.Version, + DeployedAt: EnvironmentConfig.Deploy.DeployedAt, + DeployedBy: EnvironmentConfig.Deploy.DeployedBy, + GitSha: EnvironmentConfig.Deploy.GitSha); + } +} diff --git a/src/Lho.Lambda/Utils/EnvironmentConfig.cs b/src/Lho.Lambda/Utils/EnvironmentConfig.cs new file mode 100644 index 00000000..dd2808b6 --- /dev/null +++ b/src/Lho.Lambda/Utils/EnvironmentConfig.cs @@ -0,0 +1,46 @@ +namespace Lho.Lambda.Utils; + +public static class EnvironmentConfig +{ + public static string String(string key, string defaultValue = "") + { + return Environment.GetEnvironmentVariable(key) ?? defaultValue; + } + + public static bool Bool(string key, bool defaultValue = false) + { + var value = Environment.GetEnvironmentVariable(key); + return value is null ? defaultValue : string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) || value == "1"; + } + + public static bool ShouldCallSpotify => Bool("SHOULD_CALL_SPOTIFY", defaultValue: true); + + public static class Spotify + { + public static string? ClientId => Environment.GetEnvironmentVariable("SPOTIFY_CLIENT_ID"); + + public static string? ClientSecret => Environment.GetEnvironmentVariable("SPOTIFY_CLIENT_SECRET"); + + public static string? RefreshToken => Environment.GetEnvironmentVariable("SPOTIFY_REFRESH_TOKEN"); + + public static string? AccessToken => Environment.GetEnvironmentVariable("SPOTIFY_ACCESS_TOKEN"); + } + + public static class LastFm + { + public static string? ApiKey => Environment.GetEnvironmentVariable("LASTFM_API_KEY"); + + public static string? Username => Environment.GetEnvironmentVariable("LASTFM_USERNAME"); + } + + public static class Deploy + { + public static string Version => String("VERSION", "unknown"); + + public static string DeployedAt => String("DEPLOYED_AT", "unknown"); + + public static string DeployedBy => String("DEPLOYED_BY", "unknown"); + + public static string GitSha => String("GIT_SHA", "unknown"); + } +} diff --git a/src/Lho.Lambda/Utils/MemoryCache.cs b/src/Lho.Lambda/Utils/MemoryCache.cs new file mode 100644 index 00000000..8c9c80da --- /dev/null +++ b/src/Lho.Lambda/Utils/MemoryCache.cs @@ -0,0 +1,36 @@ +namespace Lho.Lambda.Utils; + +public class MemoryCache +{ + private readonly object _lock = new(); + private readonly Dictionary _cache = []; + + public T? Get(string key) + { + lock (_lock) + { + if (!_cache.TryGetValue(key, out var entry)) + { + return default; + } + + if (entry.ExpirationDate >= DateTimeOffset.UtcNow) + { + return entry.Value is T value ? value : default; + } + + _cache.Remove(key); + return default; + } + } + + public void Set(string key, T value, TimeSpan expiration) + { + lock (_lock) + { + _cache[key] = new CacheEntry(value!, DateTimeOffset.UtcNow.Add(expiration)); + } + } + + private record CacheEntry(object Value, DateTimeOffset ExpirationDate); +} diff --git a/src/Lho.Lambda/Utils/QueryStringParser.cs b/src/Lho.Lambda/Utils/QueryStringParser.cs new file mode 100644 index 00000000..1e233bf4 --- /dev/null +++ b/src/Lho.Lambda/Utils/QueryStringParser.cs @@ -0,0 +1,23 @@ +using System.Web; + +namespace Lho.Lambda.Utils; + +public static class QueryStringParser +{ + public static string Get(string? rawQueryString, string key, string defaultValue, IReadOnlyCollection? allowedValues = null) + { + if (string.IsNullOrEmpty(rawQueryString)) + { + return defaultValue; + } + + var query = HttpUtility.ParseQueryString(rawQueryString); + var value = query[key]; + if (string.IsNullOrEmpty(value)) + { + return defaultValue; + } + + return allowedValues is not null && !allowedValues.Contains(value) ? defaultValue : value; + } +} diff --git a/src/Lho.Lambda/Utils/ResponseBuilder.cs b/src/Lho.Lambda/Utils/ResponseBuilder.cs new file mode 100644 index 00000000..e4107535 --- /dev/null +++ b/src/Lho.Lambda/Utils/ResponseBuilder.cs @@ -0,0 +1,51 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Amazon.Lambda.APIGatewayEvents; + +namespace Lho.Lambda.Utils; + +public static class ResponseBuilder +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.Never + }; + + private static readonly Dictionary DefaultCorsHeaders = new() + { + ["content-type"] = "application/json", + ["Access-Control-Allow-Origin"] = "*", + ["Access-Control-Allow-Methods"] = "GET,OPTIONS,POST,PUT,DELETE" + }; + + public static APIGatewayHttpApiV2ProxyResponse CreateResponse( + T body, + bool includeCacheControl = true, + int revalidateSeconds = 3) + { + var headers = new Dictionary(DefaultCorsHeaders) + { + ["Cache-Control"] = includeCacheControl + ? $"max-age={revalidateSeconds}, s-maxage={revalidateSeconds}, stale-while-revalidate={revalidateSeconds}, stale-if-error={revalidateSeconds}" + : "no-cache" + }; + + return new APIGatewayHttpApiV2ProxyResponse + { + StatusCode = 200, + Headers = headers, + Body = JsonSerializer.Serialize(body, JsonOptions) + }; + } + + public static APIGatewayHttpApiV2ProxyResponse ErrorResponse(int statusCode, string message) + { + return new APIGatewayHttpApiV2ProxyResponse + { + StatusCode = statusCode, + Headers = new Dictionary(DefaultCorsHeaders), + Body = JsonSerializer.Serialize(new { error = message }, JsonOptions) + }; + } +} diff --git a/terraform/authorizer.tf b/terraform/authorizer.tf index a8b3b81e..12937f1b 100644 --- a/terraform/authorizer.tf +++ b/terraform/authorizer.tf @@ -8,9 +8,9 @@ resource "aws_lambda_function" "api_authorizer" { filename = "${path.module}/../authorizer.zip" function_name = "${var.project_name}-api-authorizer-${var.env}" role = aws_iam_role.lambda_exec.arn - handler = "bootstrap" + handler = "Lho.Lambda.Authorizer::Lho.Lambda.Authorizer.Functions.AuthorizerFunction::FunctionHandler" source_code_hash = data.archive_file.auth_archive.output_base64sha256 - runtime = "provided.al2" + runtime = "dotnet8" memory_size = 256 architectures = ["x86_64"] timeout = 10 @@ -46,4 +46,3 @@ resource "aws_lambda_permission" "api_gw_authorizer" { principal = "apigateway.amazonaws.com" source_arn = "${aws_apigatewayv2_api.lambda.execution_arn}/authorizers/${aws_apigatewayv2_authorizer.api_key.id}" } - diff --git a/terraform/lambda.tf b/terraform/lambda.tf index 461a2fb5..74b052a8 100644 --- a/terraform/lambda.tf +++ b/terraform/lambda.tf @@ -1,6 +1,6 @@ data "archive_file" "lambda_archive" { type = "zip" - source_file = "${path.module}/../.build/release/bootstrap" + source_dir = "${path.module}/../.build/release/lambda-package" output_path = "${path.module}/../lambda.zip" } @@ -36,8 +36,8 @@ resource "aws_iam_role_policy_attachment" "lambda_policy" { resource "aws_lambda_function" "lambda" { function_name = "${var.project_name}-lambda-${var.env}" - runtime = "provided.al2" - handler = "bootstrap" + runtime = "dotnet8" + handler = "Lho.Lambda::Lho.Lambda.Functions.ApiFunction::FunctionHandler" role = aws_iam_role.lambda_exec.arn filename = "${path.module}/../lambda.zip" source_code_hash = data.archive_file.lambda_archive.output_base64sha256 @@ -53,6 +53,8 @@ resource "aws_lambda_function" "lambda" { SPOTIFY_CLIENT_ID = var.spotify_client_id SPOTIFY_CLIENT_SECRET = var.spotify_client_secret SPOTIFY_REFRESH_TOKEN = var.spotify_refresh_token + LASTFM_API_KEY = var.lastfm_api_key + LASTFM_USERNAME = var.lastfm_username VERSION = var.app_version DEPLOYED_AT = timestamp() DEPLOYED_BY = var.deployed_by diff --git a/terraform/variables.tf b/terraform/variables.tf index ab37eaaa..fa611351 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -27,6 +27,18 @@ variable "spotify_refresh_token" { sensitive = true } +variable "lastfm_api_key" { + type = string + description = "Last.fm API key" + sensitive = true +} + +variable "lastfm_username" { + type = string + description = "Last.fm username" + sensitive = true +} + variable "zone_id" { type = string description = "The zone id for the route53 record" @@ -95,4 +107,3 @@ variable "discord_webhook_url" { type = string sensitive = true } -