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
}
-