From a71be797e262a93c7c05194a539c42848d218794 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Wed, 29 Apr 2026 15:18:00 +0900 Subject: [PATCH 1/4] feat(infra): migrate scratch-api-proxy to CDK and deploy stg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 旧 SAM 実装 (smalruby/smalruby-infra) の 4 エンドポイントを CDK プロジェクト infra/smalruby-api/ に移行し、HTTP API v2 + TypeScript Lambda で再実装した。 - REST API v1 → HTTP API v2 (built-in CORS で OPTIONS Lambda 不要に) - Ruby 3.3 → Node.js 20 ARM64 (TypeScript) で他 infra と言語統一 - scratch-api-proxy/projects のステータスコード透過バグを修正 (旧実装は Net::HTTP.get でボディだけ取得 → 常に 200 だった) - mesh-zone-get の secret key を環境変数化 (旧実装はハードコード) - stg 環境を新設 (旧実装は prod のみ) - 23 ユニットテスト追加 (mock fetch) stg デプロイ完了: - Custom domain: https://stg.api.smalruby.app - 検証: 公開済みプロジェクトは 200, 存在しないプロジェクトは 404, CORS preflight は 204 + 適切なヘッダー、translate も透過動作 prod カットオーバーは別作業 (旧 SAM スタックのドメイン解放と協調が必要): 1. .env.prod に旧 secret_key を設定 (mesh domain identity を維持) 2. SAM スタック smalruby-infra-prod のドメインマッピング解除 3. cdk deploy --context stage=prod 4. SAM スタック decommission Refs: #573 --- .claude/rules/infra/development.md | 2 + .claude/rules/infra/smalruby-api.md | 85 + CLAUDE.md | 1 + bin/sync-worktree-env | 3 + infra/smalruby-api/.env.example | 29 + infra/smalruby-api/.gitignore | 14 + infra/smalruby-api/.node-version | 1 + infra/smalruby-api/README.md | 79 + infra/smalruby-api/bin/smalruby-api.ts | 20 + infra/smalruby-api/cdk.context.json | 6 + infra/smalruby-api/cdk.json | 43 + infra/smalruby-api/jest.config.js | 8 + infra/smalruby-api/lambda/cors-proxy.ts | 158 + infra/smalruby-api/lambda/mesh-zone-get.ts | 51 + .../lambda/scratch-api-projects.ts | 38 + .../lambda/scratch-api-translate.ts | 40 + .../lambda/tests/cors-proxy.test.ts | 116 + .../lambda/tests/mesh-zone-get.test.ts | 55 + .../lambda/tests/scratch-api-projects.test.ts | 87 + .../tests/scratch-api-translate.test.ts | 64 + infra/smalruby-api/lib/smalruby-api-stack.ts | 218 + infra/smalruby-api/package-lock.json | 4924 +++++++++++++++++ infra/smalruby-api/package.json | 29 + infra/smalruby-api/tsconfig.json | 18 + 24 files changed, 6089 insertions(+) create mode 100644 .claude/rules/infra/smalruby-api.md create mode 100644 infra/smalruby-api/.env.example create mode 100644 infra/smalruby-api/.gitignore create mode 100644 infra/smalruby-api/.node-version create mode 100644 infra/smalruby-api/README.md create mode 100644 infra/smalruby-api/bin/smalruby-api.ts create mode 100644 infra/smalruby-api/cdk.context.json create mode 100644 infra/smalruby-api/cdk.json create mode 100644 infra/smalruby-api/jest.config.js create mode 100644 infra/smalruby-api/lambda/cors-proxy.ts create mode 100644 infra/smalruby-api/lambda/mesh-zone-get.ts create mode 100644 infra/smalruby-api/lambda/scratch-api-projects.ts create mode 100644 infra/smalruby-api/lambda/scratch-api-translate.ts create mode 100644 infra/smalruby-api/lambda/tests/cors-proxy.test.ts create mode 100644 infra/smalruby-api/lambda/tests/mesh-zone-get.test.ts create mode 100644 infra/smalruby-api/lambda/tests/scratch-api-projects.test.ts create mode 100644 infra/smalruby-api/lambda/tests/scratch-api-translate.test.ts create mode 100644 infra/smalruby-api/lib/smalruby-api-stack.ts create mode 100644 infra/smalruby-api/package-lock.json create mode 100644 infra/smalruby-api/package.json create mode 100644 infra/smalruby-api/tsconfig.json diff --git a/.claude/rules/infra/development.md b/.claude/rules/infra/development.md index 539bc2f3010..5602dda3e7d 100644 --- a/.claude/rules/infra/development.md +++ b/.claude/rules/infra/development.md @@ -16,11 +16,13 @@ AWS CDK infrastructure projects live in `infra/`. Each project is independent fr | smalruby-mesh-v2 | `infra/smalruby-mesh-v2/` | Mesh v2 networking service (AppSync + DynamoDB) | | smalruby-rubytee-relay | `infra/smalruby-rubytee-relay/` | Rubytee AI relay (API Gateway + Lambda + DynamoDB) | | smalruby-classroom | `infra/smalruby-classroom/` | Classroom service (API Gateway + Lambda + DynamoDB + S3) | +| smalruby-api | `infra/smalruby-api/` | Smalruby API endpoints (HTTP API v2 + Lambda): cors-proxy, mesh-domain, scratch-api-proxy/* | See project-specific rules for details: - `.claude/rules/infra/smalruby-mesh-v2.md` - `.claude/rules/infra/smalruby-classroom.md` - `.claude/rules/infra/smalruby-rubytee-relay.md` +- `.claude/rules/infra/smalruby-api.md` ## Docker Service diff --git a/.claude/rules/infra/smalruby-api.md b/.claude/rules/infra/smalruby-api.md new file mode 100644 index 00000000000..ffb6f0d86c6 --- /dev/null +++ b/.claude/rules/infra/smalruby-api.md @@ -0,0 +1,85 @@ +# smalruby-api + +CDK project for Smalruby's API Gateway endpoints (HTTP API v2 + Lambda). + +旧 SAM 実装 (`smalruby/smalruby-infra` リポジトリ) の置き換え。 +4 エンドポイントを TypeScript Lambda + HTTP API v2 (built-in CORS) に移行する。 + +## Endpoints + +| Path | Method | Lambda 関数名 (prod) | 説明 | +|------|--------|----------------------|------| +| `/cors-proxy` | GET | `smalruby-cors-proxy` | 任意 URL のフェッチ + Google Drive URL 変換 + バイナリ Base64 化 | +| `/mesh-domain` | GET | `smalruby-mesh-zone-get` | source IP から Mesh ドメイン (CRC32) を生成 | +| `/scratch-api-proxy/projects/{projectId}` | GET | `smalruby-scratch-api-projects` | Scratch API のプロジェクト情報取得プロキシ (status pass-through) | +| `/scratch-api-proxy/translate` | GET | `smalruby-scratch-api-translate` | Scratch translate サービスプロキシ | + +stg では Lambda 関数名に `-stg` サフィックスが付く。 +OPTIONS (preflight) は HTTP API v2 の built-in CORS で自動処理 — 旧 `cors-for-smalruby` Lambda は不要。 + +## Custom Domains + +| Stage | Domain | +|-------|--------| +| stg | `stg.api.smalruby.app` | +| prod | `api.smalruby.app` | + +prod ドメインは旧 SAM スタック (`smalruby-infra-prod`) が現在保持しているため、 +prod カットオーバーは旧スタックのドメイン解放と協調が必要 (後続作業)。 + +## Commands + +```bash +# Install +docker compose run --rm -w /app/infra/smalruby-api infra npm install + +# Synth / diff / deploy +docker compose run --rm -w /app/infra/smalruby-api infra npx cdk synth +docker compose run --rm -w /app/infra/smalruby-api infra npx cdk diff +docker compose run --rm -w /app/infra/smalruby-api infra npx cdk deploy + +# Unit tests +docker compose run --rm -w /app/infra/smalruby-api infra npm test +``` + +## Stage Switching + +```bash +cd infra/smalruby-api +rm .env && ln -s .env.stg .env # → stg +rm .env && ln -s .env.prod .env # → prod +``` + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `STAGE` | Deployment stage (`stg`, `stg2`, `prod`) | +| `CORS_ALLOWED_ORIGINS` | Comma-separated allowed origins | +| `ROUTE53_PARENT_ZONE_NAME` | Parent zone for custom domain (default: `api.smalruby.app`) | +| `SMALRUBY_API_CUSTOM_DOMAIN` | Override custom domain. Set `false` to disable | +| `MESH_ZONE_SECRET_KEY` | **Secret** — used to derive Mesh group identity from source IP | + +## Migration Notes + +旧 SAM スタックとの主な差分: + +1. **REST API v1 → HTTP API v2** へ変更。built-in CORS で OPTIONS Lambda 不要に +2. **Ruby 3.3 → Node.js 20.x (TypeScript)** で他 infra プロジェクトと言語を統一 +3. **`scratch-api-proxy/projects/{projectId}` のステータスコード透過** — 旧実装は `Net::HTTP.get` でボディだけ取得 → 常に 200 を返していた (関連 Issue #573) +4. **`mesh-zone-get` の secret key を環境変数化** — 旧実装はハードコード +5. **stg 環境を新設** — 旧実装は prod のみ + +## Cutover (prod) 手順 — 後続作業 + +1. stg で動作確認 + frontend を `stg.api.smalruby.app` で結合テスト +2. `MESH_ZONE_SECRET_KEY` を旧実装と同値で `.env.prod` に設定 (mesh ドメインが既存ユーザーで変わらないようにする) +3. SAM スタック (`smalruby-infra-prod`) のドメインマッピング解除 (`api.smalruby.app`) +4. `cdk deploy --context stage=prod` で新スタックに `api.smalruby.app` を紐付け +5. 動作確認後、SAM スタック (`smalruby-infra-prod`) を CloudFormation から削除 +6. smalruby/smalruby-infra リポジトリの該当ファイルを deprecate + +## Source + +実装場所: `lambda/*.ts`, `lib/smalruby-api-stack.ts`, `bin/smalruby-api.ts` +ユニットテスト: `lambda/tests/*.test.ts` diff --git a/CLAUDE.md b/CLAUDE.md index 1e23b3e14c5..87bd042289c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,6 +21,7 @@ The `infra/` directory contains AWS CDK infrastructure projects (independent fro - **`infra/smalruby-mesh-v2`**: AWS CDK project for the Mesh v2 networking service (AppSync + DynamoDB) - **`infra/smalruby-rubytee-relay`**: AWS CDK project for the Rubytee AI relay service (Anthropic Claude API + DynamoDB) - **`infra/smalruby-classroom`**: AWS CDK project for the Classroom service (API Gateway + Lambda + DynamoDB) +- **`infra/smalruby-api`**: AWS CDK project for general API endpoints (HTTP API v2 + Lambda): cors-proxy, mesh-domain, scratch-api-proxy The `ruby/` directory contains the smalruby3 Ruby gem and its native dependencies (git submodules): diff --git a/bin/sync-worktree-env b/bin/sync-worktree-env index 367f1239590..109e3abe2ac 100755 --- a/bin/sync-worktree-env +++ b/bin/sync-worktree-env @@ -59,6 +59,9 @@ declare -a FILES=( "infra/smalruby-classroom/.env.stg" "infra/smalruby-classroom/.env.stg2" "infra/smalruby-classroom/.env.production" + "infra/smalruby-api/.env.stg" + "infra/smalruby-api/.env.stg2" + "infra/smalruby-api/.env.prod" ) copied=0 diff --git a/infra/smalruby-api/.env.example b/infra/smalruby-api/.env.example new file mode 100644 index 00000000000..633a6f8926a --- /dev/null +++ b/infra/smalruby-api/.env.example @@ -0,0 +1,29 @@ +# Smalruby API Environment Variables +# +# Environment files are managed per-stage and gitignored. +# Copy this template per stage and create a symlink: +# +# cp .env.example .env.stg # then fill in real values +# cp .env.example .env.prod # then fill in real values +# ln -s .env.stg .env # switch active environment + +# AWS CDK Stage (stg / stg2 / prod) +STAGE=stg + +# AWS Configuration (optional: CDK_DEFAULT_ACCOUNT/CDK_DEFAULT_REGION from AWS CLI take priority) +# AWS_ACCOUNT_ID=your-account-id +# AWS_REGION=ap-northeast-1 + +# CORS (comma-separated origins) +CORS_ALLOWED_ORIGINS=https://smalruby.app,https://smalruby.jp,http://localhost:8601 + +# Custom domain configuration +# - Default: stg => stg.api.smalruby.app, prod => api.smalruby.app +# - To disable custom domain (use the *.execute-api endpoint), set SMALRUBY_API_CUSTOM_DOMAIN=false +# - To override, set explicit hostname (e.g. SMALRUBY_API_CUSTOM_DOMAIN=stg2.api.smalruby.app) +ROUTE53_PARENT_ZONE_NAME=api.smalruby.app +# SMALRUBY_API_CUSTOM_DOMAIN= + +# Mesh-zone-get secret key (used to derive Mesh group identity from source IP) +# IMPORTANT: keep this stable across stages once set; changing rotates all derived domains +MESH_ZONE_SECRET_KEY=replace-me-with-a-long-random-string diff --git a/infra/smalruby-api/.gitignore b/infra/smalruby-api/.gitignore new file mode 100644 index 00000000000..79506ca2831 --- /dev/null +++ b/infra/smalruby-api/.gitignore @@ -0,0 +1,14 @@ +node_modules/ +dist/ +cdk.out/ +cdk.out.* + +# Environment variables (all are gitignored; use symlink: ln -s .env.stg .env) +.env +.env.prod +.env.stg +.env.stg2 + +*.js.map +*.d.ts +!jest.config.js diff --git a/infra/smalruby-api/.node-version b/infra/smalruby-api/.node-version new file mode 100644 index 00000000000..b4040276043 --- /dev/null +++ b/infra/smalruby-api/.node-version @@ -0,0 +1 @@ +24.8.0 diff --git a/infra/smalruby-api/README.md b/infra/smalruby-api/README.md new file mode 100644 index 00000000000..aefc125a704 --- /dev/null +++ b/infra/smalruby-api/README.md @@ -0,0 +1,79 @@ +# smalruby-api + +CDK project for Smalruby's API Gateway endpoints (HTTP API v2 + Lambda). + +旧 SAM 実装 (`smalruby/smalruby-infra`) の置き換え。4 つのエンドポイントを TypeScript Lambda + HTTP API v2 (built-in CORS) に移行する。 + +## Endpoints + +| Path | Method | Lambda | 説明 | +|------|--------|--------|------| +| `/cors-proxy` | GET | `smalruby-cors-proxy` | 任意 URL のフェッチ + Google Drive URL 変換 + バイナリ Base64 化 | +| `/mesh-domain` | GET | `smalruby-mesh-zone-get` | source IP から Mesh ドメイン (CRC32) を生成 | +| `/scratch-api-proxy/projects/{projectId}` | GET | `smalruby-scratch-api-projects` | Scratch API のプロジェクト情報取得プロキシ (status pass-through) | +| `/scratch-api-proxy/translate` | GET | `smalruby-scratch-api-translate` | Scratch translate サービスプロキシ | + +OPTIONS は API Gateway HTTP API v2 の built-in CORS で処理 (旧 `cors-for-smalruby` Lambda は不要)。 + +## Custom Domain + +| Stage | Domain | +|-------|--------| +| stg | `stg.api.smalruby.app` | +| prod | `api.smalruby.app` | + +prod ドメインへのカットオーバーは旧 SAM スタック (`smalruby-infra-prod`) のドメイン解放後に実施する。 + +## Commands + +```bash +# Install dependencies +docker compose run --rm -w /app/infra/smalruby-api infra npm install + +# Synthesize CloudFormation template +docker compose run --rm -w /app/infra/smalruby-api infra npx cdk synth + +# Show diff against deployed stack +docker compose run --rm -w /app/infra/smalruby-api infra npx cdk diff + +# Deploy (uses STAGE from .env symlink) +docker compose run --rm -w /app/infra/smalruby-api infra npx cdk deploy + +# Run unit tests +docker compose run --rm -w /app/infra/smalruby-api infra npm test +``` + +## Stage Switching + +```bash +cd infra/smalruby-api +rm .env && ln -s .env.stg .env # → stg +rm .env && ln -s .env.prod .env # → prod +``` + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `STAGE` | Deployment stage (`stg`, `stg2`, `prod`) | +| `CORS_ALLOWED_ORIGINS` | Comma-separated allowed origins | +| `ROUTE53_PARENT_ZONE_NAME` | Parent zone for custom domain (default: `api.smalruby.app`) | +| `SMALRUBY_API_CUSTOM_DOMAIN` | Override custom domain. Set `false` to disable | +| `MESH_ZONE_SECRET_KEY` | Secret key used to derive Mesh group identity from source IP | + +## Migration Notes + +旧 SAM スタックとの主な差分: + +1. **REST API v1 → HTTP API v2** へ変更。built-in CORS で `cors-for-smalruby` Lambda 不要に +2. **Ruby 3.3 → Node.js 20.x (TypeScript)** で他 infra プロジェクトと言語を統一 +3. **`scratch-api-proxy/projects/{projectId}` のステータスコード透過** バグ修正 — 旧実装は `Net::HTTP.get` でボディだけ取得 → 常に 200 を返していた +4. **`mesh-zone-get` の secret key を環境変数化** — 旧実装はハードコード +5. **stg 環境を新設** — 旧実装は prod のみ + +## Cutover (prod) — 後続作業 + +1. stg で動作確認 + frontend を `stg.api.smalruby.app` で結合テスト +2. SAM スタック (`smalruby-infra-prod`) のドメイン マッピング解除 +3. `.env.prod` を作成 → `cdk deploy` で `api.smalruby.app` を新スタックに紐付け +4. SAM スタック (`smalruby-infra-prod`) を削除 diff --git a/infra/smalruby-api/bin/smalruby-api.ts b/infra/smalruby-api/bin/smalruby-api.ts new file mode 100644 index 00000000000..1b8a04b0289 --- /dev/null +++ b/infra/smalruby-api/bin/smalruby-api.ts @@ -0,0 +1,20 @@ +#!/usr/bin/env node +import * as cdk from 'aws-cdk-lib/core'; +import * as dotenv from 'dotenv'; +import * as path from 'path'; +import { SmalrubyApiStack } from '../lib/smalruby-api-stack'; + +dotenv.config({ path: path.join(__dirname, '../.env') }); + +const app = new cdk.App(); + +const stage = app.node.tryGetContext('stage') || process.env.STAGE || 'stg'; +const stackName = stage === 'prod' ? 'SmalrubyApiStack' : `SmalrubyApiStack-${stage}`; + +new SmalrubyApiStack(app, stackName, { + stackName, + env: { + account: process.env.CDK_DEFAULT_ACCOUNT || process.env.AWS_ACCOUNT_ID, + region: process.env.CDK_DEFAULT_REGION || process.env.AWS_REGION || 'ap-northeast-1', + }, +}); diff --git a/infra/smalruby-api/cdk.context.json b/infra/smalruby-api/cdk.context.json new file mode 100644 index 00000000000..e68f5c58196 --- /dev/null +++ b/infra/smalruby-api/cdk.context.json @@ -0,0 +1,6 @@ +{ + "hosted-zone:account=007325983811:domainName=api.smalruby.app:region=ap-northeast-1": { + "Id": "/hostedzone/Z04680371PP076PW332Q5", + "Name": "api.smalruby.app." + } +} diff --git a/infra/smalruby-api/cdk.json b/infra/smalruby-api/cdk.json new file mode 100644 index 00000000000..7c92301b533 --- /dev/null +++ b/infra/smalruby-api/cdk.json @@ -0,0 +1,43 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/smalruby-api.ts", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "dist" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": ["aws", "aws-cn"], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDestroyWithFunctionTimeout": true, + "@aws-cdk/aws-apigatewayv2:useBeta1Names": true + } +} diff --git a/infra/smalruby-api/jest.config.js b/infra/smalruby-api/jest.config.js new file mode 100644 index 00000000000..c2cd7d09cdf --- /dev/null +++ b/infra/smalruby-api/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + roots: ['/lambda/tests'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, +}; diff --git a/infra/smalruby-api/lambda/cors-proxy.ts b/infra/smalruby-api/lambda/cors-proxy.ts new file mode 100644 index 00000000000..b397db9c820 --- /dev/null +++ b/infra/smalruby-api/lambda/cors-proxy.ts @@ -0,0 +1,158 @@ +import type { + APIGatewayProxyEventV2, + APIGatewayProxyStructuredResultV2, +} from 'aws-lambda'; + +const REDIRECT_LIMIT = 5; +const FETCH_TIMEOUT_MS = 30_000; + +const TEXT_PREFIXES = [ + 'text/', + 'application/json', + 'application/xml', + 'application/javascript', + 'application/x-javascript', +]; + +const BINARY_PREFIXES = [ + 'image/', + 'video/', + 'audio/', + 'application/pdf', + 'application/zip', + 'application/gzip', + 'application/x-tar', + 'application/x-rar-compressed', + 'application/x-7z-compressed', + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument', + 'application/msword', + 'application/vnd.ms-powerpoint', + 'application/octet-stream', +]; + +const isBinaryContent = (contentType: string): boolean => { + const lower = contentType.toLowerCase(); + if (TEXT_PREFIXES.some(p => lower.startsWith(p))) return false; + if (BINARY_PREFIXES.some(p => lower.startsWith(p))) return true; + return true; +}; + +const extractGoogleDriveFileId = (url: string): string | null => { + let m = url.match(/drive\.google\.com\/file\/d\/([a-zA-Z0-9_-]+)/); + if (m) return m[1]; + m = url.match(/drive\.google\.com\/open\?id=([a-zA-Z0-9_-]+)/); + if (m) return m[1]; + m = url.match(/drive\.google\.com\/uc\?.*id=([a-zA-Z0-9_-]+)/); + if (m) return m[1]; + return null; +}; + +const convertGoogleDriveUrl = (url: string): string => { + const fileId = extractGoogleDriveFileId(url); + if (fileId) { + return `https://drive.google.com/uc?export=download&id=${fileId}`; + } + return url; +}; + +interface FetchResult { + statusCode: number; + contentType: string; + body: string; + isBase64Encoded: boolean; +} + +const fetchContent = async (url: string, redirectsRemaining = REDIRECT_LIMIT): Promise => { + const parsed = new URL(url); + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + throw new Error(`Invalid URL scheme: ${parsed.protocol}`); + } + + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + let res: Response; + try { + res = await fetch(url, { + redirect: 'manual', + headers: { 'User-Agent': 'Mozilla/5.0 (compatible; AWS-Lambda-Proxy/1.0)' }, + signal: controller.signal, + }); + } finally { + clearTimeout(timer); + } + + if (res.status >= 300 && res.status < 400) { + const location = res.headers.get('location'); + if (!location) { + throw new Error('Redirect without location header'); + } + if (redirectsRemaining <= 0) { + throw new Error('Too many redirects'); + } + const next = new URL(location, url).toString(); + return fetchContent(next, redirectsRemaining - 1); + } + + const contentType = res.headers.get('content-type') || 'application/octet-stream'; + if (res.status >= 200 && res.status < 300) { + if (isBinaryContent(contentType)) { + const buf = Buffer.from(await res.arrayBuffer()); + return { + statusCode: 200, + contentType, + body: buf.toString('base64'), + isBase64Encoded: true, + }; + } + return { + statusCode: 200, + contentType, + body: await res.text(), + isBase64Encoded: false, + }; + } + + return { + statusCode: res.status, + contentType: 'application/json', + body: JSON.stringify({ + code: 'HTTP Error', + message: `HTTP ${res.status}: ${res.statusText}`, + }), + isBase64Encoded: false, + }; +}; + +export const handler = async ( + event: APIGatewayProxyEventV2, +): Promise => { + const url = (event.queryStringParameters?.url ?? '').trim(); + if (!url) { + return { + statusCode: 400, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code: 'Bad Request', message: 'invalid url' }), + isBase64Encoded: false, + }; + } + + try { + const targetUrl = convertGoogleDriveUrl(url); + const result = await fetchContent(targetUrl); + return { + statusCode: result.statusCode, + headers: { 'Content-Type': result.contentType }, + body: result.body, + isBase64Encoded: result.isBase64Encoded, + }; + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + return { + statusCode: 500, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code: 'Internal Server Error', message }), + isBase64Encoded: false, + }; + } +}; diff --git a/infra/smalruby-api/lambda/mesh-zone-get.ts b/infra/smalruby-api/lambda/mesh-zone-get.ts new file mode 100644 index 00000000000..e04383fcdaa --- /dev/null +++ b/infra/smalruby-api/lambda/mesh-zone-get.ts @@ -0,0 +1,51 @@ +import type { + APIGatewayProxyEventV2, + APIGatewayProxyStructuredResultV2, +} from 'aws-lambda'; + +const SECRET_KEY = process.env.MESH_ZONE_SECRET_KEY; + +const CRC32_TABLE: number[] = (() => { + const table = new Array(256); + for (let i = 0; i < 256; i++) { + let c = i; + for (let k = 0; k < 8; k++) { + c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + } + table[i] = c >>> 0; + } + return table; +})(); + +const crc32 = (input: string): number => { + const buf = Buffer.from(input, 'utf8'); + let crc = 0xffffffff; + for (let i = 0; i < buf.length; i++) { + crc = CRC32_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8); + } + return (crc ^ 0xffffffff) >>> 0; +}; + +export const handler = async ( + event: APIGatewayProxyEventV2, +): Promise => { + if (!SECRET_KEY) { + return { + statusCode: 500, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + code: 'ConfigError', + message: 'MESH_ZONE_SECRET_KEY is not configured', + }), + }; + } + + const sourceIp = event.requestContext?.http?.sourceIp || 'none'; + const domain = crc32(SECRET_KEY + sourceIp).toString(16); + + return { + statusCode: 200, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ domain }), + }; +}; diff --git a/infra/smalruby-api/lambda/scratch-api-projects.ts b/infra/smalruby-api/lambda/scratch-api-projects.ts new file mode 100644 index 00000000000..b8feff06b3f --- /dev/null +++ b/infra/smalruby-api/lambda/scratch-api-projects.ts @@ -0,0 +1,38 @@ +import type { + APIGatewayProxyEventV2, + APIGatewayProxyStructuredResultV2, +} from 'aws-lambda'; + +const API_HOST = 'https://api.scratch.mit.edu'; + +export const handler = async ( + event: APIGatewayProxyEventV2, +): Promise => { + const projectId = event.pathParameters?.projectId; + if (!projectId) { + return { + statusCode: 400, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code: 'BadRequest', message: 'projectId required' }), + }; + } + + try { + const apiUrl = `${API_HOST}/projects/${encodeURIComponent(projectId)}`; + const res = await fetch(apiUrl); + const text = await res.text(); + const contentType = res.headers.get('content-type') || 'application/json'; + return { + statusCode: res.status, + headers: { 'Content-Type': contentType }, + body: text, + }; + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + return { + statusCode: 502, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code: 'BadGateway', message }), + }; + } +}; diff --git a/infra/smalruby-api/lambda/scratch-api-translate.ts b/infra/smalruby-api/lambda/scratch-api-translate.ts new file mode 100644 index 00000000000..22231a7aa33 --- /dev/null +++ b/infra/smalruby-api/lambda/scratch-api-translate.ts @@ -0,0 +1,40 @@ +import type { + APIGatewayProxyEventV2, + APIGatewayProxyStructuredResultV2, +} from 'aws-lambda'; + +const API_HOST = 'https://translate-service.scratch.mit.edu'; + +export const handler = async ( + event: APIGatewayProxyEventV2, +): Promise => { + const language = (event.queryStringParameters?.language ?? '').trim(); + const text = event.queryStringParameters?.text ?? ''; + + if (!language) { + return { + statusCode: 400, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code: 'BadRequest', message: 'invalid locale code' }), + }; + } + + try { + const params = new URLSearchParams({ language, text }); + const res = await fetch(`${API_HOST}/translate?${params.toString()}`); + const body = await res.text(); + const contentType = res.headers.get('content-type') || 'application/json'; + return { + statusCode: res.status, + headers: { 'Content-Type': contentType }, + body, + }; + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + return { + statusCode: 502, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code: 'BadGateway', message }), + }; + } +}; diff --git a/infra/smalruby-api/lambda/tests/cors-proxy.test.ts b/infra/smalruby-api/lambda/tests/cors-proxy.test.ts new file mode 100644 index 00000000000..60fc326fb9d --- /dev/null +++ b/infra/smalruby-api/lambda/tests/cors-proxy.test.ts @@ -0,0 +1,116 @@ +import { handler } from '../cors-proxy'; + +const mockFetch = jest.fn(); +global.fetch = mockFetch as unknown as typeof fetch; + +const event = (qs?: Record) => + ({ + queryStringParameters: qs, + }) as never; + +beforeEach(() => { + mockFetch.mockReset(); +}); + +describe('cors-proxy handler', () => { + test('returns 400 when url is missing', async () => { + const res = await handler(event({})); + expect(res.statusCode).toBe(400); + const body = JSON.parse(res.body as string); + expect(body.code).toBe('Bad Request'); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + test('proxies text content as-is', async () => { + mockFetch.mockResolvedValueOnce( + new Response('Hello world', { + status: 200, + headers: { 'content-type': 'text/plain; charset=utf-8' }, + }), + ); + + const res = await handler(event({ url: 'https://example.com/foo.txt' })); + expect(res.statusCode).toBe(200); + expect(res.body).toBe('Hello world'); + expect(res.isBase64Encoded).toBe(false); + }); + + test('proxies JSON as text', async () => { + mockFetch.mockResolvedValueOnce( + new Response('{"a":1}', { + status: 200, + headers: { 'content-type': 'application/json' }, + }), + ); + + const res = await handler(event({ url: 'https://example.com/foo.json' })); + expect(res.statusCode).toBe(200); + expect(res.body).toBe('{"a":1}'); + expect(res.isBase64Encoded).toBe(false); + }); + + test('base64-encodes binary content', async () => { + const buf = Buffer.from([0x89, 0x50, 0x4e, 0x47]); // PNG header + mockFetch.mockResolvedValueOnce( + new Response(buf, { + status: 200, + headers: { 'content-type': 'image/png' }, + }), + ); + + const res = await handler(event({ url: 'https://example.com/x.png' })); + expect(res.statusCode).toBe(200); + expect(res.isBase64Encoded).toBe(true); + expect(Buffer.from(res.body as string, 'base64')).toEqual(buf); + }); + + test('converts Google Drive file URL to direct download URL', async () => { + mockFetch.mockResolvedValueOnce( + new Response('content', { + status: 200, + headers: { 'content-type': 'text/plain' }, + }), + ); + + const driveUrl = 'https://drive.google.com/file/d/abc123_-/view'; + await handler(event({ url: driveUrl })); + expect(mockFetch).toHaveBeenCalledWith( + 'https://drive.google.com/uc?export=download&id=abc123_-', + expect.any(Object), + ); + }); + + test('follows redirects (Location header)', async () => { + mockFetch + .mockResolvedValueOnce( + new Response(null, { status: 302, headers: { location: 'https://final.example.com/x' } }), + ) + .mockResolvedValueOnce( + new Response('final', { + status: 200, + headers: { 'content-type': 'text/plain' }, + }), + ); + + const res = await handler(event({ url: 'https://start.example.com/x' })); + expect(res.statusCode).toBe(200); + expect(res.body).toBe('final'); + expect(mockFetch).toHaveBeenCalledTimes(2); + }); + + test('returns 500 on fetch error', async () => { + mockFetch.mockRejectedValueOnce(new Error('network down')); + const res = await handler(event({ url: 'https://example.com/x' })); + expect(res.statusCode).toBe(500); + const body = JSON.parse(res.body as string); + expect(body.code).toBe('Internal Server Error'); + }); + + test('rejects non-http(s) URL schemes', async () => { + const res = await handler(event({ url: 'file:///etc/passwd' })); + expect(res.statusCode).toBe(500); + const body = JSON.parse(res.body as string); + expect(body.message).toContain('Invalid URL scheme'); + expect(mockFetch).not.toHaveBeenCalled(); + }); +}); diff --git a/infra/smalruby-api/lambda/tests/mesh-zone-get.test.ts b/infra/smalruby-api/lambda/tests/mesh-zone-get.test.ts new file mode 100644 index 00000000000..53f75ca09cd --- /dev/null +++ b/infra/smalruby-api/lambda/tests/mesh-zone-get.test.ts @@ -0,0 +1,55 @@ +describe('mesh-zone-get handler', () => { + const ORIGINAL_ENV = process.env; + + afterEach(() => { + jest.resetModules(); + process.env = ORIGINAL_ENV; + }); + + const loadHandler = (env: Record) => { + jest.resetModules(); + process.env = { ...ORIGINAL_ENV, ...env }; + // eslint-disable-next-line @typescript-eslint/no-require-imports + return require('../mesh-zone-get').handler as typeof import('../mesh-zone-get').handler; + }; + + const event = (sourceIp: string) => + ({ + requestContext: { http: { sourceIp } }, + }) as never; + + test('returns deterministic domain (Ruby Zlib.crc32 compatibility)', async () => { + const handler = loadHandler({ MESH_ZONE_SECRET_KEY: 'uXM1VAA6MO39yJ+djz4kbpVGy3Rg1V3Z' }); + const res = await handler(event('203.0.113.5')); + expect(res.statusCode).toBe(200); + const body = JSON.parse(res.body as string); + expect(body).toHaveProperty('domain'); + expect(typeof body.domain).toBe('string'); + // Same input must always produce the same output (CRC32 is deterministic) + const res2 = await handler(event('203.0.113.5')); + expect(JSON.parse(res2.body as string).domain).toBe(body.domain); + }); + + test('returns different domain for different source IP', async () => { + const handler = loadHandler({ MESH_ZONE_SECRET_KEY: 'secret' }); + const r1 = JSON.parse((await handler(event('1.1.1.1'))).body as string); + const r2 = JSON.parse((await handler(event('2.2.2.2'))).body as string); + expect(r1.domain).not.toBe(r2.domain); + }); + + test('falls back to "none" when sourceIp is missing', async () => { + const handler = loadHandler({ MESH_ZONE_SECRET_KEY: 'secret' }); + const res = await handler({} as never); + expect(res.statusCode).toBe(200); + const body = JSON.parse(res.body as string); + expect(body.domain).toBeTruthy(); + }); + + test('returns 500 when MESH_ZONE_SECRET_KEY is not configured', async () => { + const handler = loadHandler({ MESH_ZONE_SECRET_KEY: undefined }); + const res = await handler(event('1.1.1.1')); + expect(res.statusCode).toBe(500); + const body = JSON.parse(res.body as string); + expect(body.code).toBe('ConfigError'); + }); +}); diff --git a/infra/smalruby-api/lambda/tests/scratch-api-projects.test.ts b/infra/smalruby-api/lambda/tests/scratch-api-projects.test.ts new file mode 100644 index 00000000000..f53a1feac3c --- /dev/null +++ b/infra/smalruby-api/lambda/tests/scratch-api-projects.test.ts @@ -0,0 +1,87 @@ +import { handler } from '../scratch-api-projects'; + +const mockFetch = jest.fn(); +global.fetch = mockFetch as unknown as typeof fetch; + +const event = (projectId?: string) => + ({ + pathParameters: projectId ? { projectId } : undefined, + requestContext: { http: { sourceIp: '1.2.3.4' } }, + }) as never; + +beforeEach(() => { + mockFetch.mockReset(); +}); + +describe('scratch-api-projects handler', () => { + test('returns 400 when projectId is missing', async () => { + const res = await handler(event(undefined)); + expect(res.statusCode).toBe(400); + const body = JSON.parse(res.body as string); + expect(body.code).toBe('BadRequest'); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + test('passes through 200 with project_token', async () => { + const projectJson = { id: 1, project_token: 'tok' }; + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify(projectJson), { + status: 200, + headers: { 'content-type': 'application/json' }, + }), + ); + + const res = await handler(event('123')); + expect(res.statusCode).toBe(200); + expect(JSON.parse(res.body as string)).toEqual(projectJson); + expect(mockFetch).toHaveBeenCalledWith( + 'https://api.scratch.mit.edu/projects/123', + ); + }); + + test('passes through 404 from upstream (regression: was 200 in old proxy)', async () => { + mockFetch.mockResolvedValueOnce( + new Response('{"code":"NotFound","message":""}', { + status: 404, + headers: { 'content-type': 'application/json' }, + }), + ); + + const res = await handler(event('99999')); + expect(res.statusCode).toBe(404); + const body = JSON.parse(res.body as string); + expect(body.code).toBe('NotFound'); + }); + + test('passes through 5xx from upstream', async () => { + mockFetch.mockResolvedValueOnce( + new Response('upstream down', { + status: 503, + headers: { 'content-type': 'text/plain' }, + }), + ); + + const res = await handler(event('123')); + expect(res.statusCode).toBe(503); + }); + + test('returns 502 on network error', async () => { + mockFetch.mockRejectedValueOnce(new Error('connect ECONNREFUSED')); + + const res = await handler(event('123')); + expect(res.statusCode).toBe(502); + const body = JSON.parse(res.body as string); + expect(body.code).toBe('BadGateway'); + expect(body.message).toContain('ECONNREFUSED'); + }); + + test('encodes projectId for URL safety', async () => { + mockFetch.mockResolvedValueOnce( + new Response('{}', { status: 200, headers: { 'content-type': 'application/json' } }), + ); + await handler(event('a/b')); + expect(mockFetch).toHaveBeenCalledWith( + 'https://api.scratch.mit.edu/projects/a%2Fb', + ); + }); +}); diff --git a/infra/smalruby-api/lambda/tests/scratch-api-translate.test.ts b/infra/smalruby-api/lambda/tests/scratch-api-translate.test.ts new file mode 100644 index 00000000000..a28ce910375 --- /dev/null +++ b/infra/smalruby-api/lambda/tests/scratch-api-translate.test.ts @@ -0,0 +1,64 @@ +import { handler } from '../scratch-api-translate'; + +const mockFetch = jest.fn(); +global.fetch = mockFetch as unknown as typeof fetch; + +const event = (qs?: Record) => + ({ + queryStringParameters: qs, + requestContext: { http: { sourceIp: '1.2.3.4' } }, + }) as never; + +beforeEach(() => { + mockFetch.mockReset(); +}); + +describe('scratch-api-translate handler', () => { + test('returns 400 when language is missing', async () => { + const res = await handler(event({})); + expect(res.statusCode).toBe(400); + const body = JSON.parse(res.body as string); + expect(body.code).toBe('BadRequest'); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + test('proxies translate request with language and text', async () => { + mockFetch.mockResolvedValueOnce( + new Response('{"result":"こんにちは"}', { + status: 200, + headers: { 'content-type': 'application/json' }, + }), + ); + + const res = await handler(event({ language: 'ja', text: 'hello' })); + expect(res.statusCode).toBe(200); + expect(mockFetch).toHaveBeenCalledTimes(1); + const calledUrl = mockFetch.mock.calls[0][0] as string; + expect(calledUrl).toMatch(/^https:\/\/translate-service\.scratch\.mit\.edu\/translate\?/); + expect(calledUrl).toContain('language=ja'); + expect(calledUrl).toContain('text=hello'); + }); + + test('properly url-encodes special characters in text', async () => { + mockFetch.mockResolvedValueOnce( + new Response('{}', { status: 200, headers: { 'content-type': 'application/json' } }), + ); + await handler(event({ language: 'ja', text: 'hello world & goodbye' })); + const calledUrl = mockFetch.mock.calls[0][0] as string; + expect(calledUrl).toContain('text=hello+world+%26+goodbye'); + }); + + test('passes through upstream errors', async () => { + mockFetch.mockResolvedValueOnce(new Response('boom', { status: 503 })); + const res = await handler(event({ language: 'ja', text: 'x' })); + expect(res.statusCode).toBe(503); + }); + + test('returns 502 on network error', async () => { + mockFetch.mockRejectedValueOnce(new Error('ETIMEDOUT')); + const res = await handler(event({ language: 'ja', text: 'x' })); + expect(res.statusCode).toBe(502); + const body = JSON.parse(res.body as string); + expect(body.code).toBe('BadGateway'); + }); +}); diff --git a/infra/smalruby-api/lib/smalruby-api-stack.ts b/infra/smalruby-api/lib/smalruby-api-stack.ts new file mode 100644 index 00000000000..d801e4adaa6 --- /dev/null +++ b/infra/smalruby-api/lib/smalruby-api-stack.ts @@ -0,0 +1,218 @@ +import * as cdk from 'aws-cdk-lib/core'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs'; +import * as apigatewayv2 from 'aws-cdk-lib/aws-apigatewayv2'; +import * as apigatewayv2Integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations'; +import * as logs from 'aws-cdk-lib/aws-logs'; +import * as route53 from 'aws-cdk-lib/aws-route53'; +import * as route53Targets from 'aws-cdk-lib/aws-route53-targets'; +import * as acm from 'aws-cdk-lib/aws-certificatemanager'; +import * as path from 'path'; +import { Construct } from 'constructs'; + +export class SmalrubyApiStack extends cdk.Stack { + public readonly api: apigatewayv2.HttpApi; + + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const stage = this.node.tryGetContext('stage') || process.env.STAGE || 'stg'; + const stageSuffix = stage === 'prod' ? '' : `-${stage}`; + + const corsOriginsEnv = + process.env.CORS_ALLOWED_ORIGINS || + 'https://smalruby.app,https://smalruby.jp,http://localhost:8601'; + const corsAllowOrigins = corsOriginsEnv.split(',').map(o => o.trim()); + + const meshZoneSecretKey = process.env.MESH_ZONE_SECRET_KEY || ''; + if (!meshZoneSecretKey) { + throw new Error( + 'MESH_ZONE_SECRET_KEY is required. Set it in .env (gitignored).', + ); + } + + cdk.Tags.of(this).add('Project', 'SmalrubyApi'); + cdk.Tags.of(this).add('Stage', stage); + cdk.Tags.of(this).add('Service', 'Lambda'); + cdk.Tags.of(this).add('ManagedBy', 'CDK'); + + // --- Lambda functions --- + + const makeLambda = ( + constructId: string, + functionName: string, + entry: string, + extraEnv: Record = {}, + memorySize = 128, + timeoutSec = 10, + ): lambdaNodejs.NodejsFunction => { + const logGroup = new logs.LogGroup(this, `${constructId}LogGroup`, { + logGroupName: `/aws/lambda/${functionName}`, + retention: + stage === 'prod' + ? logs.RetentionDays.ONE_MONTH + : logs.RetentionDays.ONE_WEEK, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + return new lambdaNodejs.NodejsFunction(this, constructId, { + functionName, + runtime: lambda.Runtime.NODEJS_20_X, + entry: path.join(__dirname, `../lambda/${entry}`), + handler: 'handler', + timeout: cdk.Duration.seconds(timeoutSec), + memorySize, + logGroup, + architecture: lambda.Architecture.ARM_64, + environment: { + STAGE: stage, + ...extraEnv, + }, + bundling: { + minify: true, + sourceMap: stage !== 'prod', + externalModules: [], + }, + }); + }; + + const corsProxyFn = makeLambda( + 'CorsProxy', + `smalruby-cors-proxy${stageSuffix}`, + 'cors-proxy.ts', + {}, + 512, + 30, + ); + + const meshZoneGetFn = makeLambda( + 'MeshZoneGet', + `smalruby-mesh-zone-get${stageSuffix}`, + 'mesh-zone-get.ts', + { MESH_ZONE_SECRET_KEY: meshZoneSecretKey }, + ); + + const scratchProjectsFn = makeLambda( + 'ScratchApiProjects', + `smalruby-scratch-api-projects${stageSuffix}`, + 'scratch-api-projects.ts', + ); + + const scratchTranslateFn = makeLambda( + 'ScratchApiTranslate', + `smalruby-scratch-api-translate${stageSuffix}`, + 'scratch-api-translate.ts', + ); + + // --- Custom Domain --- + + const parentZoneName = process.env.ROUTE53_PARENT_ZONE_NAME || 'api.smalruby.app'; + const defaultCustomDomain = + stage === 'prod' ? parentZoneName : `${stage}.${parentZoneName}`; + const customDomain = + process.env.SMALRUBY_API_CUSTOM_DOMAIN === 'false' + ? undefined + : process.env.SMALRUBY_API_CUSTOM_DOMAIN || defaultCustomDomain; + + let domainName: apigatewayv2.DomainName | undefined; + let zone: route53.IHostedZone | undefined; + + if (customDomain) { + zone = route53.HostedZone.fromLookup(this, 'HostedZone', { + domainName: parentZoneName, + }); + + const certificate = new acm.Certificate(this, 'ApiCertificate', { + domainName: customDomain, + validation: acm.CertificateValidation.fromDns(zone), + }); + + domainName = new apigatewayv2.DomainName(this, 'ApiDomainName', { + domainName: customDomain, + certificate, + }); + } + + // --- HTTP API --- + + this.api = new apigatewayv2.HttpApi(this, 'SmalrubyApi', { + apiName: `SmalrubyApi${stageSuffix}`, + corsPreflight: { + allowOrigins: corsAllowOrigins, + allowMethods: [apigatewayv2.CorsHttpMethod.GET, apigatewayv2.CorsHttpMethod.OPTIONS], + allowHeaders: ['Content-Type'], + maxAge: cdk.Duration.hours(24), + }, + defaultDomainMapping: domainName ? { domainName } : undefined, + }); + + const integrationFor = (constructId: string, fn: lambda.IFunction) => + new apigatewayv2Integrations.HttpLambdaIntegration(constructId, fn); + + this.api.addRoutes({ + path: '/cors-proxy', + methods: [apigatewayv2.HttpMethod.GET], + integration: integrationFor('CorsProxyIntegration', corsProxyFn), + }); + + this.api.addRoutes({ + path: '/mesh-domain', + methods: [apigatewayv2.HttpMethod.GET], + integration: integrationFor('MeshZoneGetIntegration', meshZoneGetFn), + }); + + this.api.addRoutes({ + path: '/scratch-api-proxy/projects/{projectId}', + methods: [apigatewayv2.HttpMethod.GET], + integration: integrationFor('ScratchApiProjectsIntegration', scratchProjectsFn), + }); + + this.api.addRoutes({ + path: '/scratch-api-proxy/translate', + methods: [apigatewayv2.HttpMethod.GET], + integration: integrationFor('ScratchApiTranslateIntegration', scratchTranslateFn), + }); + + // Throttling + const defaultStage = this.api.defaultStage?.node.defaultChild as apigatewayv2.CfnStage; + if (defaultStage) { + defaultStage.defaultRouteSettings = { + throttlingRateLimit: stage === 'prod' ? 200 : 50, + throttlingBurstLimit: stage === 'prod' ? 200 : 50, + }; + } + + cdk.Tags.of(this.api).add('ResourceType', 'APIGatewayHTTPAPI'); + + // Route53 Alias record + if (customDomain && zone && domainName) { + const subdomain = customDomain === parentZoneName + ? '' + : customDomain.replace(`.${parentZoneName}`, ''); + + new route53.ARecord(this, 'ApiAliasRecord', { + zone, + recordName: subdomain || undefined, + target: route53.RecordTarget.fromAlias( + new route53Targets.ApiGatewayv2DomainProperties( + domainName.regionalDomainName, + domainName.regionalHostedZoneId, + ), + ), + }); + } + + // Outputs + new cdk.CfnOutput(this, 'HttpApiEndpoint', { + value: this.api.apiEndpoint, + description: 'API Gateway HTTP API default endpoint', + }); + + if (customDomain) { + new cdk.CfnOutput(this, 'CustomDomainUrl', { + value: `https://${customDomain}`, + description: 'Smalruby API custom domain URL', + }); + } + } +} diff --git a/infra/smalruby-api/package-lock.json b/infra/smalruby-api/package-lock.json new file mode 100644 index 00000000000..bea8c8d0b71 --- /dev/null +++ b/infra/smalruby-api/package-lock.json @@ -0,0 +1,4924 @@ +{ + "name": "smalruby-api", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "smalruby-api", + "version": "0.1.0", + "dependencies": { + "aws-cdk-lib": "2.232.1", + "constructs": "^10.0.0", + "esbuild": "^0.25.0" + }, + "bin": { + "smalruby-api": "bin/smalruby-api.js" + }, + "devDependencies": { + "@types/aws-lambda": "^8.10.149", + "@types/jest": "^29.5.14", + "@types/node": "^24.10.1", + "aws-cdk": "2.1100.1", + "dotenv": "^17.2.3", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "typescript": "~5.9.3" + } + }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.242", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.242.tgz", + "integrity": "sha512-4c1bAy2ISzcdKXYS1k4HYZsNrgiwbiDzj36ybwFVxEWZXVAP0dimQTCaB9fxu7sWzEjw3d+eaw6Fon+QTfTIpQ==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.1.tgz", + "integrity": "sha512-We4bmHaowOPHr+IQR4/FyTGjRfjgBj4ICMjtqmJeBDWad3Q/6St12NT07leNtyuukv2qMhtSZJQorD8KpKTwRA==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "48.20.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-48.20.0.tgz", + "integrity": "sha512-+eeiav9LY4wbF/EFuCt/vfvi/Zoxo8bf94PW5clbMraChEliq83w4TbRVy0jB9jE0v1ooFTtIjSQkowSPkfISg==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.2" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.7.2", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/aws-lambda": { + "version": "8.10.161", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.161.tgz", + "integrity": "sha512-rUYdp+MQwSFocxIOcSsYSF3YYYC/uUpMbCY/mbO21vGqfrEYvNSoPyKYDj6RhXXpPfS0KstW9RwG3qXh9sL7FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aws-cdk": { + "version": "2.1100.1", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1100.1.tgz", + "integrity": "sha512-q2poFrQh90TK6eqeI0zznA8r1JkDI63WVOSqC7gFGo6qjQjAnvFk/utxHoNRgAC0RL0CLd19uCcHh3jfX9NiSg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 18.0.0" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.232.1", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.232.1.tgz", + "integrity": "sha512-F1vNcpWBo85pSxa0DJ5DO4k7Ok4vVp0vh1cFO4Y12LLX07ixOcnJn/6B97/XVC0fgZNvzPx/sYgioEd0u8oKkQ==", + "bundleDependencies": [ + "@balena/dockerignore", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml", + "mime-types" + ], + "license": "Apache-2.0", + "dependencies": { + "@aws-cdk/asset-awscli-v1": "2.2.242", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", + "@aws-cdk/cloud-assembly-schema": "^48.20.0", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.3.2", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^3.1.2", + "punycode": "^2.3.1", + "semver": "^7.7.3", + "table": "^6.9.0", + "yaml": "1.10.2" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "constructs": "^10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.17.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "1.1.12", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/concat-map": { + "version": "0.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.1.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.3.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.3.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.2.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "3.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.7.3", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.9.0", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.22", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.22.tgz", + "integrity": "sha512-6qruVrb5rse6WylFkU0FhBKKGuecWseqdpQfhkawn6ztyk2QlfwSRjsDxMCLJrkfmfN21qvhl9ABgaMeRkuwww==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001790", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", + "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/constructs": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.6.0.tgz", + "integrity": "sha512-TxHOnBO5zMo/G76ykzGF/wMpEHu257TbWiIxP9K0Yv/+t70UzgBQiTqjkAsWOPC6jW91DzJI0+ehQV6xDRNBuQ==", + "license": "Apache-2.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.344", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", + "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-jest": { + "version": "29.4.9", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz", + "integrity": "sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.9", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.4", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/infra/smalruby-api/package.json b/infra/smalruby-api/package.json new file mode 100644 index 00000000000..e89916ba169 --- /dev/null +++ b/infra/smalruby-api/package.json @@ -0,0 +1,29 @@ +{ + "name": "smalruby-api", + "version": "0.1.0", + "bin": { + "smalruby-api": "bin/smalruby-api.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk" + }, + "devDependencies": { + "@types/aws-lambda": "^8.10.149", + "@types/jest": "^29.5.14", + "@types/node": "^24.10.1", + "aws-cdk": "2.1100.1", + "dotenv": "^17.2.3", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "typescript": "~5.9.3" + }, + "dependencies": { + "aws-cdk-lib": "2.232.1", + "constructs": "^10.0.0", + "esbuild": "^0.25.0" + } +} diff --git a/infra/smalruby-api/tsconfig.json b/infra/smalruby-api/tsconfig.json new file mode 100644 index 00000000000..850c72feecd --- /dev/null +++ b/infra/smalruby-api/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "outDir": "./dist", + "rootDir": "./", + "esModuleInterop": true, + "skipLibCheck": true + }, + "exclude": ["node_modules", "dist", "lambda/tests"] +} From efc7d5c7bdaf74ed90804855238fa533f37961d9 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Wed, 29 Apr 2026 15:25:04 +0900 Subject: [PATCH 2/4] test(infra/smalruby-api): add CI workflow + integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .github/workflows/ci-infra.yml: rubytee-relay と同形式の 3 ジョブを追加 - smalruby-api-unit-tests (npm test) - smalruby-api-cdk-build (build + cdk synth、ダミー env で実行) - smalruby-api-security (npm audit) - jest.config.js: integration test を unit から除外 (.integration.test.ts) - jest.integration.config.js を追加 (rubytee-relay と同様) - package.json に test:integration スクリプト追加 - lambda/tests/*.integration.test.ts × 4 を追加 (合計 18 テスト) - scratch-api-projects: 200/404/CORS/不許可 origin (Issue #573 デグレ防止) - scratch-api-translate: 200/400/特殊文字エンコード/CORS - mesh-zone-get: hex domain 形式/決定性/CORS - cors-proxy: text 透過/PNG バイナリ/不正スキーム/CORS stg エンドポイントに対して 18 テスト全パスを確認。 --- .claude/rules/infra/smalruby-api.md | 15 ++- .github/workflows/ci-infra.yml | 63 +++++++++++ infra/smalruby-api/.env.example | 3 + infra/smalruby-api/jest.config.js | 1 + infra/smalruby-api/jest.integration.config.js | 13 +++ .../tests/cors-proxy.integration.test.ts | 82 ++++++++++++++ .../tests/mesh-zone-get.integration.test.ts | 47 ++++++++ .../scratch-api-projects.integration.test.ts | 100 ++++++++++++++++++ .../scratch-api-translate.integration.test.ts | 64 +++++++++++ infra/smalruby-api/package.json | 1 + 10 files changed, 388 insertions(+), 1 deletion(-) create mode 100644 infra/smalruby-api/jest.integration.config.js create mode 100644 infra/smalruby-api/lambda/tests/cors-proxy.integration.test.ts create mode 100644 infra/smalruby-api/lambda/tests/mesh-zone-get.integration.test.ts create mode 100644 infra/smalruby-api/lambda/tests/scratch-api-projects.integration.test.ts create mode 100644 infra/smalruby-api/lambda/tests/scratch-api-translate.integration.test.ts diff --git a/.claude/rules/infra/smalruby-api.md b/.claude/rules/infra/smalruby-api.md index ffb6f0d86c6..0dd670528b9 100644 --- a/.claude/rules/infra/smalruby-api.md +++ b/.claude/rules/infra/smalruby-api.md @@ -38,10 +38,23 @@ docker compose run --rm -w /app/infra/smalruby-api infra npx cdk synth docker compose run --rm -w /app/infra/smalruby-api infra npx cdk diff docker compose run --rm -w /app/infra/smalruby-api infra npx cdk deploy -# Unit tests +# Unit tests (mocked fetch, fast) docker compose run --rm -w /app/infra/smalruby-api infra npm test + +# Integration tests (実際の stg エンドポイントへ HTTP 送信) +docker compose run --rm -w /app/infra/smalruby-api infra npm run test:integration ``` +## Integration Tests + +`lambda/tests/*.integration.test.ts` は **デプロイ済み stg エンドポイント** に対して +実際の HTTP リクエストを送信して動作を検証する。コーナーケース確認とデグレ防止が目的。 + +- Issue #573 の **404 透過バグ再発防止** が最重要のテストケース +- 必要な環境変数: `SMALRUBY_API_ENDPOINT` (`.env.stg` で設定済み、デフォルト `https://stg.api.smalruby.app`) +- デプロイ後は必ず `npm run test:integration` を実行して 18 テストすべて緑であることを確認する +- CI では実行しない (npm test のユニットテストとは独立。ローカル/手動運用) + ## Stage Switching ```bash diff --git a/.github/workflows/ci-infra.yml b/.github/workflows/ci-infra.yml index 9c0582f2672..69aaa5b95a9 100644 --- a/.github/workflows/ci-infra.yml +++ b/.github/workflows/ci-infra.yml @@ -147,3 +147,66 @@ jobs: - name: Run npm audit run: npm audit --audit-level=moderate continue-on-error: true + + # --------------------------------------------------------------------------- + # smalruby-api + # --------------------------------------------------------------------------- + smalruby-api-unit-tests: + name: "[smalruby-api] Unit Tests" + runs-on: ubuntu-latest + defaults: + run: + working-directory: infra/smalruby-api + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6 + with: + node-version-file: infra/smalruby-api/.node-version + cache: npm + cache-dependency-path: infra/smalruby-api/package-lock.json + - name: Install dependencies + run: npm ci + - name: Run unit tests + run: npm test + + smalruby-api-cdk-build: + name: "[smalruby-api] CDK Build & Synth" + runs-on: ubuntu-latest + defaults: + run: + working-directory: infra/smalruby-api + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6 + with: + node-version-file: infra/smalruby-api/.node-version + cache: npm + cache-dependency-path: infra/smalruby-api/package-lock.json + - name: Install dependencies + run: npm ci + - name: Build TypeScript + run: npm run build + - name: Synthesize CDK stack + env: + MESH_ZONE_SECRET_KEY: 'dummy-key-for-synth' + SMALRUBY_API_CUSTOM_DOMAIN: 'false' + run: npx cdk synth --context stage=stg + + smalruby-api-security: + name: "[smalruby-api] Security Audit" + runs-on: ubuntu-latest + defaults: + run: + working-directory: infra/smalruby-api + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6 + with: + node-version-file: infra/smalruby-api/.node-version + cache: npm + cache-dependency-path: infra/smalruby-api/package-lock.json + - name: Install dependencies + run: npm ci + - name: Run npm audit + run: npm audit --audit-level=moderate + continue-on-error: true diff --git a/infra/smalruby-api/.env.example b/infra/smalruby-api/.env.example index 633a6f8926a..3b279318587 100644 --- a/infra/smalruby-api/.env.example +++ b/infra/smalruby-api/.env.example @@ -24,6 +24,9 @@ CORS_ALLOWED_ORIGINS=https://smalruby.app,https://smalruby.jp,http://localhost:8 ROUTE53_PARENT_ZONE_NAME=api.smalruby.app # SMALRUBY_API_CUSTOM_DOMAIN= +# Endpoint used by integration tests (npm run test:integration) +# SMALRUBY_API_ENDPOINT=https://stg.api.smalruby.app + # Mesh-zone-get secret key (used to derive Mesh group identity from source IP) # IMPORTANT: keep this stable across stages once set; changing rotates all derived domains MESH_ZONE_SECRET_KEY=replace-me-with-a-long-random-string diff --git a/infra/smalruby-api/jest.config.js b/infra/smalruby-api/jest.config.js index c2cd7d09cdf..25fb00bfdee 100644 --- a/infra/smalruby-api/jest.config.js +++ b/infra/smalruby-api/jest.config.js @@ -2,6 +2,7 @@ module.exports = { testEnvironment: 'node', roots: ['/lambda/tests'], testMatch: ['**/*.test.ts'], + testPathIgnorePatterns: ['\\.integration\\.test\\.ts$'], transform: { '^.+\\.tsx?$': 'ts-jest', }, diff --git a/infra/smalruby-api/jest.integration.config.js b/infra/smalruby-api/jest.integration.config.js new file mode 100644 index 00000000000..3063b9577c4 --- /dev/null +++ b/infra/smalruby-api/jest.integration.config.js @@ -0,0 +1,13 @@ +module.exports = { + testEnvironment: 'node', + roots: ['/lambda/tests'], + testMatch: ['**/*.integration.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, + // Each network call to upstream Scratch API may be slow under cold start; + // give individual tests up to 30 seconds. + testTimeout: 30000, + // Load .env so SMALRUBY_API_ENDPOINT is picked up automatically. + setupFiles: ['dotenv/config'], +}; diff --git a/infra/smalruby-api/lambda/tests/cors-proxy.integration.test.ts b/infra/smalruby-api/lambda/tests/cors-proxy.integration.test.ts new file mode 100644 index 00000000000..66fb89b2044 --- /dev/null +++ b/infra/smalruby-api/lambda/tests/cors-proxy.integration.test.ts @@ -0,0 +1,82 @@ +/** + * smalruby-api / cors-proxy 結合テスト + * + * 任意の URL を中継するプロキシ。 + * - text/json はそのまま返す + * - バイナリは base64 エンコードして isBase64Encoded=true で返す + * (HTTP API v2 + Lambda の構造化レスポンス互換) + * - Google Drive URL は uc?export=download 形式に自動変換 + * - リダイレクト追従 + * - 不正な URL スキームはエラー + */ + +const ENDPOINT = process.env.SMALRUBY_API_ENDPOINT || ''; + +const proxy = async (url: string) => { + const params = new URLSearchParams({ url }); + return fetch(`${ENDPOINT}/cors-proxy?${params}`); +}; + +beforeAll(() => { + if (!ENDPOINT) { + throw new Error( + 'SMALRUBY_API_ENDPOINT が設定されていません。\n' + + '.env.stg に SMALRUBY_API_ENDPOINT=https://stg.api.smalruby.app を追加してください。', + ); + } +}); + +describe('GET /cors-proxy', () => { + test('url パラメータが無いと 400', async () => { + const res = await fetch(`${ENDPOINT}/cors-proxy`); + expect(res.status).toBe(400); + const data = (await res.json()) as Record; + expect(data.code).toBe('Bad Request'); + }); + + test('テキストコンテンツをそのまま中継する', async () => { + // Scratch project の json を直接取りに行く (公開済みの軽いデータ) + // → application/json なのでテキストとしてそのまま返るはず + const r = await proxy('https://api.scratch.mit.edu/projects/1209008277'); + expect(r.status).toBe(200); + const text = await r.text(); + const json = JSON.parse(text) as Record; + expect(json.id).toBe(1209008277); + }); + + test('PNG など画像は base64 エンコードされて返る (binary content)', async () => { + // 軽量のテスト用 PNG (Scratch のサムネイル画像) + const imageUrl = 'https://cdn2.scratch.mit.edu/get_image/project/1209008277_100x80.png'; + const r = await proxy(imageUrl); + expect(r.status).toBe(200); + // Lambda の isBase64Encoded を API Gateway が解釈するため、 + // クライアント側ではバイナリとして受け取れる (Content-Type: image/png) + expect(r.headers.get('content-type')).toMatch(/^image\/png/); + const buf = Buffer.from(await r.arrayBuffer()); + // PNG マジックナンバーを検証 (89 50 4E 47 0D 0A 1A 0A) + expect(buf[0]).toBe(0x89); + expect(buf[1]).toBe(0x50); + expect(buf[2]).toBe(0x4e); + expect(buf[3]).toBe(0x47); + }); + + test('不正な URL スキーム (file://) は 500 エラー', async () => { + const r = await proxy('file:///etc/passwd'); + expect(r.status).toBe(500); + const data = (await r.json()) as Record; + expect(data.code).toBe('Internal Server Error'); + expect(data.message as string).toMatch(/Invalid URL scheme/i); + }); + + test('CORS preflight (OPTIONS) は 204', async () => { + const res = await fetch(`${ENDPOINT}/cors-proxy`, { + method: 'OPTIONS', + headers: { + Origin: 'http://localhost:8601', + 'Access-Control-Request-Method': 'GET', + }, + }); + expect(res.status).toBe(204); + expect(res.headers.get('access-control-allow-origin')).toBe('http://localhost:8601'); + }); +}); diff --git a/infra/smalruby-api/lambda/tests/mesh-zone-get.integration.test.ts b/infra/smalruby-api/lambda/tests/mesh-zone-get.integration.test.ts new file mode 100644 index 00000000000..d4498062922 --- /dev/null +++ b/infra/smalruby-api/lambda/tests/mesh-zone-get.integration.test.ts @@ -0,0 +1,47 @@ +/** + * smalruby-api / mesh-domain 結合テスト + * + * sourceIp + 環境変数 MESH_ZONE_SECRET_KEY から CRC32 で導出した + * 16進数文字列が `domain` として返されることを検証する。 + */ + +const ENDPOINT = process.env.SMALRUBY_API_ENDPOINT || ''; + +beforeAll(() => { + if (!ENDPOINT) { + throw new Error( + 'SMALRUBY_API_ENDPOINT が設定されていません。\n' + + '.env.stg に SMALRUBY_API_ENDPOINT=https://stg.api.smalruby.app を追加してください。', + ); + } +}); + +describe('GET /mesh-domain', () => { + test('domain が 16 進数文字列で返る', async () => { + const res = await fetch(`${ENDPOINT}/mesh-domain`); + expect(res.status).toBe(200); + const data = (await res.json()) as Record; + expect(typeof data.domain).toBe('string'); + expect(data.domain).toMatch(/^[0-9a-f]+$/); + }); + + test('連続呼び出しで同じ source IP からは同じ domain が返る (CRC32 決定性)', async () => { + const r1 = await (await fetch(`${ENDPOINT}/mesh-domain`)).json(); + const r2 = await (await fetch(`${ENDPOINT}/mesh-domain`)).json(); + expect((r1 as Record).domain).toBe( + (r2 as Record).domain, + ); + }); + + test('CORS preflight (OPTIONS) は 204', async () => { + const res = await fetch(`${ENDPOINT}/mesh-domain`, { + method: 'OPTIONS', + headers: { + Origin: 'http://localhost:8601', + 'Access-Control-Request-Method': 'GET', + }, + }); + expect(res.status).toBe(204); + expect(res.headers.get('access-control-allow-origin')).toBe('http://localhost:8601'); + }); +}); diff --git a/infra/smalruby-api/lambda/tests/scratch-api-projects.integration.test.ts b/infra/smalruby-api/lambda/tests/scratch-api-projects.integration.test.ts new file mode 100644 index 00000000000..a07431c3e21 --- /dev/null +++ b/infra/smalruby-api/lambda/tests/scratch-api-projects.integration.test.ts @@ -0,0 +1,100 @@ +/** + * smalruby-api / scratch-api-proxy/projects/{projectId} 結合テスト + * + * 実際にデプロイされたエンドポイント (デフォルトは stg) に対して + * HTTP リクエストを送信し、ステータスコード透過とレスポンス内容を検証する。 + * + * このエンドポイントは [Scratch API](https://api.scratch.mit.edu) の + * `/projects/{id}` をそのまま中継するシンプルなプロキシ。旧 SAM 実装は + * 常に 200 を返すバグを持っていたため、ステータスコード透過の確認が + * このテストの主目的。Issue #573 の回帰防止。 + * + * 実行方法: + * docker compose run --rm -w /app/infra/smalruby-api infra npm run test:integration + * + * 必要な環境変数 (.env で設定): + * SMALRUBY_API_ENDPOINT=https://stg.api.smalruby.app + */ + +const ENDPOINT = process.env.SMALRUBY_API_ENDPOINT || ''; + +interface JsonResponse { + status: number; + body: Record | string; + headers: Record; +} + +const get = async (path: string, init?: RequestInit): Promise => { + const res = await fetch(`${ENDPOINT}${path}`, init); + const text = await res.text(); + let body: Record | string; + try { + body = JSON.parse(text) as Record; + } catch { + body = text; + } + const headers: Record = {}; + res.headers.forEach((v, k) => { + headers[k.toLowerCase()] = v; + }); + return { status: res.status, body, headers }; +}; + +beforeAll(() => { + if (!ENDPOINT) { + throw new Error( + 'SMALRUBY_API_ENDPOINT が設定されていません。\n' + + '.env.stg に SMALRUBY_API_ENDPOINT=https://stg.api.smalruby.app を追加してください。', + ); + } +}); + +describe('GET /scratch-api-proxy/projects/{projectId}', () => { + test('公開プロジェクトは 200 + project_token を含む JSON', async () => { + const { status, body } = await get('/scratch-api-proxy/projects/1209008277'); + expect(status).toBe(200); + expect(typeof body).toBe('object'); + const json = body as Record; + expect(json.id).toBe(1209008277); + expect(typeof json.project_token).toBe('string'); + expect((json.project_token as string).length).toBeGreaterThan(0); + }); + + test('存在しない / 共有解除されたプロジェクトは 404 (旧実装は 200 を返したバグ)', async () => { + const { status, body } = await get('/scratch-api-proxy/projects/9999999999'); + expect(status).toBe(404); + expect(typeof body).toBe('object'); + const json = body as Record; + expect(json.code).toBe('NotFound'); + }); + + test('Content-Type は application/json で透過される', async () => { + const { headers } = await get('/scratch-api-proxy/projects/1209008277'); + expect(headers['content-type']).toMatch(/application\/json/); + }); + + test('CORS preflight (OPTIONS) は 204 + CORS ヘッダー', async () => { + const res = await fetch(`${ENDPOINT}/scratch-api-proxy/projects/1209008277`, { + method: 'OPTIONS', + headers: { + Origin: 'http://localhost:8601', + 'Access-Control-Request-Method': 'GET', + }, + }); + expect(res.status).toBe(204); + expect(res.headers.get('access-control-allow-origin')).toBe('http://localhost:8601'); + expect(res.headers.get('access-control-allow-methods')?.toUpperCase()).toContain('GET'); + }); + + test('許可されていない Origin は CORS ヘッダーを返さない', async () => { + const res = await fetch(`${ENDPOINT}/scratch-api-proxy/projects/1209008277`, { + method: 'OPTIONS', + headers: { + Origin: 'https://evil.example.com', + 'Access-Control-Request-Method': 'GET', + }, + }); + // HTTP API v2 は許可されない origin に対しては Access-Control-Allow-Origin を返さない + expect(res.headers.get('access-control-allow-origin')).not.toBe('https://evil.example.com'); + }); +}); diff --git a/infra/smalruby-api/lambda/tests/scratch-api-translate.integration.test.ts b/infra/smalruby-api/lambda/tests/scratch-api-translate.integration.test.ts new file mode 100644 index 00000000000..e0ca0fdbcbe --- /dev/null +++ b/infra/smalruby-api/lambda/tests/scratch-api-translate.integration.test.ts @@ -0,0 +1,64 @@ +/** + * smalruby-api / scratch-api-proxy/translate 結合テスト + * + * 実際にデプロイされた stg エンドポイントに対し、Scratch translate サービスへの + * 中継動作を検証する。 + */ + +const ENDPOINT = process.env.SMALRUBY_API_ENDPOINT || ''; + +beforeAll(() => { + if (!ENDPOINT) { + throw new Error( + 'SMALRUBY_API_ENDPOINT が設定されていません。\n' + + '.env.stg に SMALRUBY_API_ENDPOINT=https://stg.api.smalruby.app を追加してください。', + ); + } +}); + +describe('GET /scratch-api-proxy/translate', () => { + test('language と text を渡すと翻訳結果が返る', async () => { + const params = new URLSearchParams({ language: 'ja', text: 'hello' }); + const res = await fetch(`${ENDPOINT}/scratch-api-proxy/translate?${params}`); + expect(res.status).toBe(200); + const data = (await res.json()) as Record; + expect(typeof data.result).toBe('string'); + expect((data.result as string).length).toBeGreaterThan(0); + }); + + test('language が無いと 400 (バリデーションエラー)', async () => { + const res = await fetch(`${ENDPOINT}/scratch-api-proxy/translate`); + expect(res.status).toBe(400); + const data = (await res.json()) as Record; + expect(data.code).toBe('BadRequest'); + }); + + test('text が空でも language があれば 200', async () => { + const params = new URLSearchParams({ language: 'ja', text: '' }); + const res = await fetch(`${ENDPOINT}/scratch-api-proxy/translate?${params}`); + expect(res.status).toBe(200); + }); + + test('特殊文字が URL エンコードされて渡される', async () => { + const params = new URLSearchParams({ + language: 'ja', + text: 'hello & goodbye', + }); + const res = await fetch(`${ENDPOINT}/scratch-api-proxy/translate?${params}`); + expect(res.status).toBe(200); + const data = (await res.json()) as Record; + expect(typeof data.result).toBe('string'); + }); + + test('CORS preflight (OPTIONS) は 204 + CORS ヘッダー', async () => { + const res = await fetch(`${ENDPOINT}/scratch-api-proxy/translate`, { + method: 'OPTIONS', + headers: { + Origin: 'http://localhost:8601', + 'Access-Control-Request-Method': 'GET', + }, + }); + expect(res.status).toBe(204); + expect(res.headers.get('access-control-allow-origin')).toBe('http://localhost:8601'); + }); +}); diff --git a/infra/smalruby-api/package.json b/infra/smalruby-api/package.json index e89916ba169..b80d7f885cb 100644 --- a/infra/smalruby-api/package.json +++ b/infra/smalruby-api/package.json @@ -8,6 +8,7 @@ "build": "tsc", "watch": "tsc -w", "test": "jest", + "test:integration": "jest --config jest.integration.config.js", "cdk": "cdk" }, "devDependencies": { From 3ce3baadb2947f42071109694ce51d7f86be8370 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Wed, 29 Apr 2026 15:45:05 +0900 Subject: [PATCH 3/4] fix(gui): show specific error messages in Scratch URL loader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit URL ローダーの失敗時、proxy のステータスコードに応じた具体的なエラーを モーダル内に表示するようにする。Issue #573 の本丸 UX バグ修正。 修正前の挙動: - proxy が 200/404/403/500 何を返してもアラートで「プロジェクトURLの読み 込みに失敗しました。」を表示するのみ - モーダル内のエラー表示は invalidUrl のメッセージで固定 (state.error の 内容は無視) 修正後の挙動: - proxy 修正前と修正後の両方に対応 (data.code === 'NotFound' 互換チェック) - 404 → 「プロジェクトが見つかりません。共有が解除されたか、URLが間違って いる可能性があります。」 - 403 → 「このプロジェクトにはアクセスできません。」 - 5xx → 「Scratchサーバー側でエラーが発生しました。しばらく経ってから再度 お試しください。」 - TypeError (ネットワーク失敗) → 「ネットワークエラーが発生しました。」 - proxy が 200 + project_token なし → 502 扱い - 不正な URL → 既存の「有効なScratchプロジェクトURL...」メッセージ - モーダルは送信時にクローズせず、成功時のみクローズ。失敗時はメッセージ を表示してリトライさせる 実装上の構造変更: - src/lib/url-loader.js を新設し、formatLoadError と fetchProjectInfo を pure function として分離 (HOC からテスト容易な形に) - src/lib/url-loader-hoc.jsx は React/Redux 連携の thin wrapper に縮小 - url-loader-modal.jsx の固定 FormattedMessage を動的 error string 表示に - ロケール (ja, ja-Hira, en) に 4 つの新メッセージ追加 - data-testid="url-loader-error" を追加 (integration test 用) エンドポイント設定: - SCRATCH_API_PROXY_ENDPOINT 環境変数を追加 - デフォルト: https://api.smalruby.app (prod) - ローカル開発時は .env で https://stg.api.smalruby.app に上書き - webpack.config.js の DefinePlugin で注入 (Smalruby マーカー付き) - .github/workflows/ci-cd.yml の build-and-deploy 3 ジョブ全部に追加 (vars.SCRATCH_API_PROXY_ENDPOINT) テスト: src/lib/url-loader.test.js に 18 ユニットテスト追加 - formatLoadError × 8 (404 / 403 / 5xx / TypeError / fallback) - fetchProjectInfo × 10 (各 status / legacy NotFound / token 欠損 / 特殊文字エンコード / TypeError 透過) stg エンドポイントを使った Playwright 動作確認: - 不正 URL → モーダル内に invalidUrl メッセージ表示 ✓ - 9999999999 (404) → 「プロジェクトが見つかりません」表示 ✓ - 108317385 (正常) → モーダル閉じてプロジェクトロード ✓ Refs: #573 --- .claude/rules/scratch-gui/smalruby-markers.md | 1 + .../scratch-gui/smalruby-prettier-files.md | 2 + .env.example | 6 + .github/workflows/ci-cd.yml | 3 + packages/scratch-gui/.prettierignore | 2 + .../url-loader-modal/url-loader-modal.jsx | 11 +- .../scratch-gui/src/lib/url-loader-hoc.jsx | 104 ++++------ packages/scratch-gui/src/lib/url-loader.js | 115 +++++++++++ packages/scratch-gui/src/locales/en.js | 5 + packages/scratch-gui/src/locales/ja-Hira.js | 7 + packages/scratch-gui/src/locales/ja.js | 6 + .../test/unit/lib/url-loader.test.js | 193 ++++++++++++++++++ packages/scratch-gui/webpack.config.js | 3 + 13 files changed, 382 insertions(+), 76 deletions(-) create mode 100644 packages/scratch-gui/src/lib/url-loader.js create mode 100644 packages/scratch-gui/test/unit/lib/url-loader.test.js diff --git a/.claude/rules/scratch-gui/smalruby-markers.md b/.claude/rules/scratch-gui/smalruby-markers.md index aa48d952faf..cbad04806cd 100644 --- a/.claude/rules/scratch-gui/smalruby-markers.md +++ b/.claude/rules/scratch-gui/smalruby-markers.md @@ -52,6 +52,7 @@ upstream ファイルに追加した Smalruby 固有コードのマーカー一 | `src/components/menu-bar/menu-bar.jsx` | classroom button | クラスルームボタンの import、レンダリング、Redux 接続 | | `src/components/menu-bar/settings-menu.jsx` | classroom management menu | クラス管理メニューアイテムの import、レンダリング、Redux 接続 | | `webpack.config.js` | classroom API | CLASSROOM_API_ENDPOINT 環境変数注入 | +| `webpack.config.js` | scratch api proxy endpoint | SCRATCH_API_PROXY_ENDPOINT 環境変数注入 | | `src/lib/blocks.js` | gesture recovery import | ジェスチャー復旧モジュールの import | | `src/lib/blocks.js` | gesture recovery | ジェスチャー復旧ハンドラーのインストール | | `src/playground/render-gui.jsx` | URL params for Playwright | URL パラメーター import | diff --git a/.claude/rules/scratch-gui/smalruby-prettier-files.md b/.claude/rules/scratch-gui/smalruby-prettier-files.md index 3982e95ba08..83ee66b7978 100644 --- a/.claude/rules/scratch-gui/smalruby-prettier-files.md +++ b/.claude/rules/scratch-gui/smalruby-prettier-files.md @@ -119,6 +119,7 @@ upstream (Scratch) ファイルは対象外。 - `src/lib/rubytee-api.js` - `src/lib/rubytee-context.js` - `src/lib/url-loader-hoc.jsx` +- `src/lib/url-loader.js` - `src/lib/url-params.js` - `src/lib/url-parser.js` - `src/lib/version-checker.js` @@ -203,6 +204,7 @@ upstream (Scratch) ファイルは対象外。 - `test/unit/lib/furigana-annotator-perf.test.js` - `test/unit/lib/furigana-annotator.test.js` - `test/unit/lib/google-drive-api.test.js` +- `test/unit/lib/url-loader.test.js` - `test/unit/lib/insert-class.test.js` - `test/unit/lib/join-code-history.test.js` - `test/unit/lib/layout-constants.test.js` diff --git a/.env.example b/.env.example index 463d376e5a1..68d5d1c4e98 100644 --- a/.env.example +++ b/.env.example @@ -62,6 +62,12 @@ MESH_PERIODIC_DATA_SYNC_INTERVAL_MS=15000 # Example: https://xxxxx.execute-api.ap-northeast-1.amazonaws.com CLASSROOM_API_ENDPOINT= +# Scratch API Proxy Configuration (cors-proxy / mesh-domain / scratch-api-proxy/*) +# See infra/smalruby-api/ for deployment details +# Default: https://api.smalruby.app (prod). For local dev override with stg: +# SCRATCH_API_PROXY_ENDPOINT=https://stg.api.smalruby.app +SCRATCH_API_PROXY_ENDPOINT= + # Dev bypass token for automated testing (stg/local only) # Allows skipping Google auth with ?devlogin=1 URL parameter # Must match DEV_BYPASS_TOKEN in infra/smalruby-classroom/.env.stg diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index cf9ea34bd48..25e88d9303a 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -263,6 +263,7 @@ jobs: MESH_PERIODIC_DATA_SYNC_INTERVAL_MS: ${{ vars.MESH_PERIODIC_DATA_SYNC_INTERVAL_MS }} RUBYTEE_RELAY_ENDPOINT: ${{ secrets.RUBYTEE_RELAY_ENDPOINT }} CLASSROOM_API_ENDPOINT: ${{ secrets.CLASSROOM_API_ENDPOINT }} + SCRATCH_API_PROXY_ENDPOINT: ${{ vars.SCRATCH_API_PROXY_ENDPOINT }} MICROSOFT_CLIENT_ID: ${{ secrets.MICROSOFT_CLIENT_ID }} MAX_USER_MESSAGE_LENGTH: ${{ vars.MAX_USER_MESSAGE_LENGTH }} MIN_USER_MESSAGE_LENGTH: ${{ vars.MIN_USER_MESSAGE_LENGTH }} @@ -310,6 +311,7 @@ jobs: MESH_PERIODIC_DATA_SYNC_INTERVAL_MS: ${{ vars.MESH_PERIODIC_DATA_SYNC_INTERVAL_MS }} RUBYTEE_RELAY_ENDPOINT: ${{ secrets.RUBYTEE_RELAY_ENDPOINT }} CLASSROOM_API_ENDPOINT: ${{ secrets.CLASSROOM_API_ENDPOINT }} + SCRATCH_API_PROXY_ENDPOINT: ${{ vars.SCRATCH_API_PROXY_ENDPOINT }} MICROSOFT_CLIENT_ID: ${{ secrets.MICROSOFT_CLIENT_ID }} MAX_USER_MESSAGE_LENGTH: ${{ vars.MAX_USER_MESSAGE_LENGTH }} MIN_USER_MESSAGE_LENGTH: ${{ vars.MIN_USER_MESSAGE_LENGTH }} @@ -351,6 +353,7 @@ jobs: MESH_PERIODIC_DATA_SYNC_INTERVAL_MS: ${{ vars.MESH_PERIODIC_DATA_SYNC_INTERVAL_MS }} RUBYTEE_RELAY_ENDPOINT: ${{ secrets.RUBYTEE_RELAY_ENDPOINT }} CLASSROOM_API_ENDPOINT: ${{ secrets.CLASSROOM_API_ENDPOINT }} + SCRATCH_API_PROXY_ENDPOINT: ${{ vars.SCRATCH_API_PROXY_ENDPOINT }} MICROSOFT_CLIENT_ID: ${{ secrets.MICROSOFT_CLIENT_ID }} MAX_USER_MESSAGE_LENGTH: ${{ vars.MAX_USER_MESSAGE_LENGTH }} MIN_USER_MESSAGE_LENGTH: ${{ vars.MIN_USER_MESSAGE_LENGTH }} diff --git a/packages/scratch-gui/.prettierignore b/packages/scratch-gui/.prettierignore index a4640ff66f8..b8291cf86b0 100644 --- a/packages/scratch-gui/.prettierignore +++ b/packages/scratch-gui/.prettierignore @@ -142,6 +142,7 @@ src/lib/* !src/lib/rubytee-api.js !src/lib/rubytee-context.js !src/lib/url-loader-hoc.jsx +!src/lib/url-loader.js !src/lib/url-params.js !src/lib/url-parser.js !src/lib/version-checker.js @@ -280,6 +281,7 @@ test/unit/lib/* !test/unit/lib/furigana-annotator-perf.test.js !test/unit/lib/furigana-annotator.test.js !test/unit/lib/google-drive-api.test.js +!test/unit/lib/url-loader.test.js !test/unit/lib/insert-class.test.js !test/unit/lib/join-code-history.test.js !test/unit/lib/layout-constants.test.js diff --git a/packages/scratch-gui/src/components/url-loader-modal/url-loader-modal.jsx b/packages/scratch-gui/src/components/url-loader-modal/url-loader-modal.jsx index 9bde9ff8958..af8496fa4f7 100644 --- a/packages/scratch-gui/src/components/url-loader-modal/url-loader-modal.jsx +++ b/packages/scratch-gui/src/components/url-loader-modal/url-loader-modal.jsx @@ -41,11 +41,6 @@ const messages = defineMessages({ defaultMessage: 'Cancel', description: 'Label for cancel button', id: 'gui.urlLoader.cancelButton' - }, - invalidUrlError: { - defaultMessage: 'Please enter a valid Scratch project URL.', - description: 'Error message for invalid URL', - id: 'gui.urlLoader.invalidUrl' } }); @@ -125,10 +120,8 @@ class URLLoaderModal extends React.Component { autoFocus /> {error && ( -
- +
+ {error}
)} diff --git a/packages/scratch-gui/src/lib/url-loader-hoc.jsx b/packages/scratch-gui/src/lib/url-loader-hoc.jsx index 5f08d89d4f9..81b46598830 100644 --- a/packages/scratch-gui/src/lib/url-loader-hoc.jsx +++ b/packages/scratch-gui/src/lib/url-loader-hoc.jsx @@ -1,7 +1,7 @@ import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; import React from 'react'; -import { defineMessages, injectIntl } from 'react-intl'; +import { injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { GUIStoragePropType } from '../gui-config'; import log from '../lib/log'; @@ -23,29 +23,18 @@ import { loadProjectWithChecks } from './project-loader-utils'; import { persistRubyVersion } from './settings/ruby-version/persistence'; import sharedMessages from './shared-messages'; import { extractScratchProjectId, isValidScratchProjectUrl } from './url-parser'; +import { + UrlLoaderError, + fetchProjectInfo, + formatLoadError, + urlLoaderMessages, +} from './url-loader'; -const messages = defineMessages({ - loadError: { - id: 'gui.urlLoader.loadError', - defaultMessage: 'The project URL that was entered failed to load.', - description: 'An error that displays when a project URL fails to load.', - }, - invalidUrl: { - id: 'gui.urlLoader.invalidUrl', - defaultMessage: 'Please enter a valid Scratch project URL.', - description: 'An error that displays when an invalid URL is entered.', - }, -}); +const SCRATCH_API_PROXY_ENDPOINT = (process.env.SCRATCH_API_PROXY_ENDPOINT || 'https://api.smalruby.app').replace( + /\/+$/, + '', +); -/** - * Higher Order Component to provide behavior for loading project from URL into editor. - * @param {React.Component} WrappedComponent the component to add URL loading functionality to - * @returns {React.Component} WrappedComponent with URL loading functionality added - * - * - * - * - */ const URLLoaderHOC = function (WrappedComponent) { class URLLoaderComponent extends React.Component { constructor(props) { @@ -57,7 +46,9 @@ const URLLoaderHOC = function (WrappedComponent) { 'handleFinishedLoadingUpload', 'clearLoadingReferences', ]); + this.urlLoaderErrorCallback = null; } + componentDidUpdate(prevProps) { if (this.props.isLoadingUpload && !prevProps.isLoadingUpload) { this.handleFinishedLoadingUpload(); @@ -72,22 +63,17 @@ const URLLoaderHOC = function (WrappedComponent) { handleUrlSubmit(url, errorCallback) { const { intl, isShowingWithoutId, loadingState, projectChanged, userOwnsProject } = this.props; - // Validate Scratch project URL if (!isValidScratchProjectUrl(url)) { - // Instead of alert, pass error to modal via callback if (errorCallback) { - errorCallback(intl.formatMessage(messages.invalidUrl)); + errorCallback(intl.formatMessage(urlLoaderMessages.invalidUrl)); } return; } - // Store project ID for loading this.projectIdToLoad = extractScratchProjectId(url); this.projectUrlToLoad = url; + this.urlLoaderErrorCallback = errorCallback || null; - // If user owns the project, or user has changed the project, - // we must confirm with the user that they really intend to - // replace it. let uploadAllowed = true; if (userOwnsProject || (projectChanged && isShowingWithoutId)) { // eslint-disable-next-line no-alert @@ -95,13 +81,14 @@ const URLLoaderHOC = function (WrappedComponent) { } if (uploadAllowed) { - // Start the loading process + // Keep the modal open during the fetch so that the errorCallback + // can post the error back into the *same* modal instance on + // failure. The modal is closed in `loadScratchProjectFromUrl` + // only after the project has loaded successfully. this.props.requestProjectUpload(loadingState); - // Close modal only when validation passes and user confirms - this.props.closeUrlLoaderModal(); } else { - // Close modal if user cancels the replacement this.props.closeUrlLoaderModal(); + this.urlLoaderErrorCallback = null; } } @@ -115,34 +102,11 @@ const URLLoaderHOC = function (WrappedComponent) { loadScratchProjectFromUrl(projectId) { this.props.onLoadingStarted(); - - // Set project ID in Redux state first (like project-fetcher-hoc.jsx) this.props.setProjectId(projectId.toString()); - // Use the same approach as project-fetcher-hoc.jsx - // First get the project token via the proxy API - const options = { - method: 'GET', - uri: `https://api.smalruby.app/scratch-api-proxy/projects/${projectId}`, - json: true, - }; - - fetch(options.uri, { - method: options.method, - headers: { - 'Content-Type': 'application/json', - }, - }) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } - return response.json(); - }) + fetchProjectInfo(SCRATCH_API_PROXY_ENDPOINT, projectId) .then(data => { const projectToken = data.project_token; - - // Now load the project using storage system (like project-fetcher-hoc.jsx) const storage = this.props.storage; storage.setProjectToken?.(projectToken); @@ -162,24 +126,31 @@ const URLLoaderHOC = function (WrappedComponent) { this.props.onSetRubyVersion, ); } - throw new Error('Could not find project'); + throw new UrlLoaderError('Could not find project', 404); }) .then(() => { - // Set project title based on the project data or URL const projectTitle = `Project ${this.projectIdToLoad}`; this.props.onSetProjectTitle(projectTitle); - - // Use onLoadedProject for LOADING_VM_FILE_UPLOAD state this.props.onLoadedProject(this.props.loadingState, true, true); + // Close the modal only after a successful load. + this.props.closeUrlLoaderModal(); }) .catch(error => { log.warn('URL loader error:', error); this.props.onError(error); - alert(this.props.intl.formatMessage(messages.loadError)); // eslint-disable-line no-alert + const message = formatLoadError(error, this.props.intl); + if (this.urlLoaderErrorCallback) { + // The modal is still mounted (we did not close it on + // submit), so the callback can put the error back into + // the same modal instance for the user to retry. + this.urlLoaderErrorCallback(message); + } else { + // eslint-disable-next-line no-alert + alert(message); + } }) .then(() => { this.props.onLoadingFinished(); - // Clear the project reference this.clearLoadingReferences(); }); } @@ -187,6 +158,7 @@ const URLLoaderHOC = function (WrappedComponent) { clearLoadingReferences() { this.projectIdToLoad = null; this.projectUrlToLoad = null; + this.urlLoaderErrorCallback = null; } render() { @@ -280,8 +252,7 @@ const URLLoaderHOC = function (WrappedComponent) { closeFileMenu: () => dispatch(closeFileMenu()), closeUrlLoaderModal: () => dispatch(closeUrlLoaderModal()), onError: error => dispatch(projectError(error)), - onLoadedProject: (loadingState, canSave, success) => - dispatch(onLoadedProject(loadingState, canSave, success)), + onLoadedProject: (loadingState, canSave, success) => dispatch(onLoadedProject(loadingState, canSave, success)), onLoadingFinished: () => { dispatch(closeLoadingProject()); dispatch(closeFileMenu()); @@ -297,8 +268,7 @@ const URLLoaderHOC = function (WrappedComponent) { setProjectId: projectId => dispatch(setProjectId(projectId)), }); - const mergeProps = (stateProps, dispatchProps, ownProps) => - Object.assign({}, stateProps, dispatchProps, ownProps); + const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign({}, stateProps, dispatchProps, ownProps); return injectIntl(connect(mapStateToProps, mapDispatchToProps, mergeProps)(URLLoaderComponent)); }; diff --git a/packages/scratch-gui/src/lib/url-loader.js b/packages/scratch-gui/src/lib/url-loader.js new file mode 100644 index 00000000000..44ae8e05525 --- /dev/null +++ b/packages/scratch-gui/src/lib/url-loader.js @@ -0,0 +1,115 @@ +import { defineMessages } from 'react-intl'; + +/** + * Localized messages used by the URL loader UI and HOC. + * Exported so the HOC and unit tests reference the same source of truth. + */ +const urlLoaderMessages = defineMessages({ + loadError: { + id: 'gui.urlLoader.loadError', + defaultMessage: 'The project URL that was entered failed to load.', + description: 'Generic fallback when project URL loading fails.', + }, + invalidUrl: { + id: 'gui.urlLoader.invalidUrl', + defaultMessage: 'Please enter a valid Scratch project URL.', + description: 'Error shown when the entered URL is not a Scratch project URL.', + }, + projectNotFound: { + id: 'gui.urlLoader.projectNotFound', + defaultMessage: + 'Project not found. It may have been unshared, or the URL may be incorrect.', + description: 'Shown when the upstream Scratch API returns 404.', + }, + projectAccessDenied: { + id: 'gui.urlLoader.projectAccessDenied', + defaultMessage: 'Access to this project is denied.', + description: 'Shown when the upstream Scratch API returns 403.', + }, + serverError: { + id: 'gui.urlLoader.serverError', + defaultMessage: 'Scratch server-side error. Please try again later.', + description: 'Shown when the upstream Scratch API returns 5xx.', + }, + networkError: { + id: 'gui.urlLoader.networkError', + defaultMessage: 'Network error. Please check your internet connection.', + description: + 'Shown when fetch throws a TypeError (offline, DNS failure, blocked by extension).', + }, +}); + +/** + * Error thrown by the URL loader pipeline. Carries an HTTP-like `status` code so + * the formatter can map it to a user-facing message. + */ +class UrlLoaderError extends Error { + /** + * @param {string} message - Internal error message (logged, not user-visible). + * @param {number} status - HTTP-like status (404 / 403 / 5xx / 502 etc.). + */ + constructor(message, status) { + super(message); + this.name = 'UrlLoaderError'; + this.status = status; + } +} + +/** + * Map a thrown error from the load pipeline to a user-facing localized message. + * @param {Error|UrlLoaderError} error - The error caught from the load pipeline. + * @param {object} intl - react-intl `intl` object (must have formatMessage). + * @returns {string} Localized error message. + */ +const formatLoadError = (error, intl) => { + if (error && error.status === 404) { + return intl.formatMessage(urlLoaderMessages.projectNotFound); + } + if (error && error.status === 403) { + return intl.formatMessage(urlLoaderMessages.projectAccessDenied); + } + if (error && typeof error.status === 'number' && error.status >= 500) { + return intl.formatMessage(urlLoaderMessages.serverError); + } + // fetch() throws TypeError on network failures (offline, DNS, CORS, etc.) + if (error && error.name === 'TypeError') { + return intl.formatMessage(urlLoaderMessages.networkError); + } + return intl.formatMessage(urlLoaderMessages.loadError); +}; + +/** + * Fetch project metadata (including project_token) via the smalruby-api proxy. + * + * Returns the parsed JSON body when successful, or throws `UrlLoaderError` with + * the appropriate status: + * - 404 / 403 / 5xx are propagated from the proxy + * - Legacy SAM proxy returned 200 with `{code: "NotFound"}` for missing projects + * — we detect this and treat it as 404 for backwards compatibility + * - Missing `project_token` in a 200 response → 502 (bad gateway) + * @param {string} endpoint - Base endpoint (no trailing slash), e.g. https://stg.api.smalruby.app + * @param {string|number} projectId - Scratch project id (numeric string). + * @returns {Promise<{project_token: string, [key: string]: unknown}>} Resolves with project info. + */ +const fetchProjectInfo = async (endpoint, projectId) => { + const uri = `${endpoint}/scratch-api-proxy/projects/${encodeURIComponent(projectId)}`; + const response = await fetch(uri, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }); + if (!response.ok) { + throw new UrlLoaderError(`HTTP ${response.status}`, response.status); + } + const data = await response.json(); + // Backwards compatibility with the legacy SAM proxy that returned + // `200 OK` with `{code: 'NotFound'}` for missing projects. + if (data && data.code === 'NotFound') { + throw new UrlLoaderError('Project not found (legacy proxy)', 404); + } + if (!data || typeof data.project_token !== 'string' || data.project_token.length === 0) { + throw new UrlLoaderError('Project token missing in proxy response', 502); + } + return data; +}; + +export { UrlLoaderError, fetchProjectInfo, formatLoadError, urlLoaderMessages }; diff --git a/packages/scratch-gui/src/locales/en.js b/packages/scratch-gui/src/locales/en.js index a74facd0d2c..6d2d1176b9b 100644 --- a/packages/scratch-gui/src/locales/en.js +++ b/packages/scratch-gui/src/locales/en.js @@ -425,6 +425,11 @@ export default { // URL Loader messages 'gui.urlLoader.loadError': 'The project URL that was entered failed to load.', 'gui.urlLoader.invalidUrl': 'Please enter a valid Scratch project URL or Google Drive URL.', + 'gui.urlLoader.projectNotFound': + 'Project not found. It may have been unshared, or the URL may be incorrect.', + 'gui.urlLoader.projectAccessDenied': 'Access to this project is denied.', + 'gui.urlLoader.serverError': 'Scratch server-side error. Please try again later.', + 'gui.urlLoader.networkError': 'Network error. Please check your internet connection.', 'gui.urlLoader.title': 'Load from URL', 'gui.urlLoader.urlPlaceholder': 'Enter project URL...', 'gui.urlLoader.openButton': 'Open', diff --git a/packages/scratch-gui/src/locales/ja-Hira.js b/packages/scratch-gui/src/locales/ja-Hira.js index 93ec378571c..457fb5aa167 100644 --- a/packages/scratch-gui/src/locales/ja-Hira.js +++ b/packages/scratch-gui/src/locales/ja-Hira.js @@ -257,6 +257,13 @@ export default { 'フォルダピッカーのひょうじにしっぱいしました。もういちどおためしください。', 'gui.urlLoader.loadError': 'プロジェクトURLのよみこみにしっぱいしました。', 'gui.urlLoader.invalidUrl': 'ゆうこうなScratchプロジェクトURLまたはGoogle DriveのURLをにゅうりょくしてください。', + 'gui.urlLoader.projectNotFound': + 'プロジェクトがみつかりません。きょうゆうがかいじょされたか、URLがまちがっているかもしれません。', + 'gui.urlLoader.projectAccessDenied': 'このプロジェクトにはアクセスできません。', + 'gui.urlLoader.serverError': + 'Scratchサーバーがわでエラーがはっせいしました。しばらくたってからもういちどおためしください。', + 'gui.urlLoader.networkError': + 'ネットワークエラーがはっせいしました。インターネットせつぞくをかくにんしてください。', 'gui.urlLoader.title': 'Scratchからよみこむ', 'gui.urlLoader.urlPlaceholder': 'プロジェクトのURLをにゅうりょく...', 'gui.urlLoader.openButton': 'ひらく', diff --git a/packages/scratch-gui/src/locales/ja.js b/packages/scratch-gui/src/locales/ja.js index 5699db45301..7b56b198749 100644 --- a/packages/scratch-gui/src/locales/ja.js +++ b/packages/scratch-gui/src/locales/ja.js @@ -248,6 +248,12 @@ export default { 'gui.googleDriveSaveDialog.folderPickerError': 'フォルダピッカーの表示に失敗しました。もう一度お試しください。', 'gui.urlLoader.loadError': 'プロジェクトURLの読み込みに失敗しました。', 'gui.urlLoader.invalidUrl': '有効なScratchプロジェクトURLまたはGoogle DriveのURLを入力してください。', + 'gui.urlLoader.projectNotFound': + 'プロジェクトが見つかりません。共有が解除されたか、URLが間違っている可能性があります。', + 'gui.urlLoader.projectAccessDenied': 'このプロジェクトにはアクセスできません。', + 'gui.urlLoader.serverError': + 'Scratchサーバー側でエラーが発生しました。しばらく経ってから再度お試しください。', + 'gui.urlLoader.networkError': 'ネットワークエラーが発生しました。インターネット接続を確認してください。', 'gui.urlLoader.title': 'Scratchから読み込む', 'gui.urlLoader.urlPlaceholder': 'プロジェクトのURLを入力...', 'gui.urlLoader.openButton': '開く', diff --git a/packages/scratch-gui/test/unit/lib/url-loader.test.js b/packages/scratch-gui/test/unit/lib/url-loader.test.js new file mode 100644 index 00000000000..38c43bb4659 --- /dev/null +++ b/packages/scratch-gui/test/unit/lib/url-loader.test.js @@ -0,0 +1,193 @@ +import { + UrlLoaderError, + fetchProjectInfo, + formatLoadError, + urlLoaderMessages, +} from '../../../src/lib/url-loader'; + +const mockIntl = { + formatMessage: msg => msg.defaultMessage, +}; + +describe('formatLoadError', () => { + test('404 → projectNotFound', () => { + const err = new UrlLoaderError('hi', 404); + expect(formatLoadError(err, mockIntl)).toBe(urlLoaderMessages.projectNotFound.defaultMessage); + }); + + test('403 → projectAccessDenied', () => { + const err = new UrlLoaderError('hi', 403); + expect(formatLoadError(err, mockIntl)).toBe(urlLoaderMessages.projectAccessDenied.defaultMessage); + }); + + test('500 → serverError', () => { + const err = new UrlLoaderError('hi', 500); + expect(formatLoadError(err, mockIntl)).toBe(urlLoaderMessages.serverError.defaultMessage); + }); + + test('502 → serverError (any 5xx)', () => { + const err = new UrlLoaderError('hi', 502); + expect(formatLoadError(err, mockIntl)).toBe(urlLoaderMessages.serverError.defaultMessage); + }); + + test('TypeError (network failure) → networkError', () => { + const err = new TypeError('Failed to fetch'); + expect(formatLoadError(err, mockIntl)).toBe(urlLoaderMessages.networkError.defaultMessage); + }); + + test('unknown error (no status) → loadError fallback', () => { + const err = new Error('something broke'); + expect(formatLoadError(err, mockIntl)).toBe(urlLoaderMessages.loadError.defaultMessage); + }); + + test('null/undefined error → loadError fallback', () => { + expect(formatLoadError(null, mockIntl)).toBe(urlLoaderMessages.loadError.defaultMessage); + expect(formatLoadError(undefined, mockIntl)).toBe(urlLoaderMessages.loadError.defaultMessage); + }); + + test('400 (Bad Request) → loadError fallback (not specifically mapped)', () => { + const err = new UrlLoaderError('hi', 400); + expect(formatLoadError(err, mockIntl)).toBe(urlLoaderMessages.loadError.defaultMessage); + }); +}); + +describe('fetchProjectInfo', () => { + let originalFetch; + + beforeEach(() => { + originalFetch = global.fetch; + }); + + afterEach(() => { + global.fetch = originalFetch; + }); + + test('200 with project_token → resolves with the JSON body', async () => { + const projectJson = { id: 12345, project_token: 'token-abc' }; + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve(projectJson), + }); + + const data = await fetchProjectInfo('https://stg.api.smalruby.app', '12345'); + + expect(global.fetch).toHaveBeenCalledWith( + 'https://stg.api.smalruby.app/scratch-api-proxy/projects/12345', + expect.objectContaining({ method: 'GET' }), + ); + expect(data).toEqual(projectJson); + }); + + test('404 from proxy → throws UrlLoaderError with status 404', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: false, + status: 404, + json: () => Promise.resolve({ code: 'NotFound' }), + }); + + await expect(fetchProjectInfo('https://stg.api.smalruby.app', '99999')).rejects.toMatchObject({ + name: 'UrlLoaderError', + status: 404, + }); + }); + + test('403 from proxy → throws UrlLoaderError with status 403', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: false, + status: 403, + json: () => Promise.resolve({}), + }); + + await expect(fetchProjectInfo('https://stg.api.smalruby.app', '12345')).rejects.toMatchObject({ + status: 403, + }); + }); + + test('5xx from proxy → throws UrlLoaderError with that status', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: false, + status: 503, + json: () => Promise.resolve({}), + }); + + await expect(fetchProjectInfo('https://stg.api.smalruby.app', '12345')).rejects.toMatchObject({ + status: 503, + }); + }); + + test('200 + {code: "NotFound"} (legacy SAM proxy) → throws UrlLoaderError with status 404', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ code: 'NotFound', message: '' }), + }); + + await expect(fetchProjectInfo('https://api.smalruby.app', '99999')).rejects.toMatchObject({ + name: 'UrlLoaderError', + status: 404, + }); + }); + + test('200 + missing project_token → throws UrlLoaderError with status 502', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ id: 12345 }), + }); + + await expect(fetchProjectInfo('https://stg.api.smalruby.app', '12345')).rejects.toMatchObject({ + status: 502, + }); + }); + + test('200 + empty project_token → throws UrlLoaderError with status 502', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ id: 12345, project_token: '' }), + }); + + await expect(fetchProjectInfo('https://stg.api.smalruby.app', '12345')).rejects.toMatchObject({ + status: 502, + }); + }); + + test('fetch throws TypeError (network failure) → propagates as TypeError', async () => { + global.fetch = jest.fn().mockRejectedValue(new TypeError('Failed to fetch')); + + await expect(fetchProjectInfo('https://stg.api.smalruby.app', '12345')).rejects.toMatchObject({ + name: 'TypeError', + }); + }); + + test('encodes special characters in projectId', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ project_token: 'tok' }), + }); + + await fetchProjectInfo('https://stg.api.smalruby.app', 'a/b'); + + expect(global.fetch).toHaveBeenCalledWith( + 'https://stg.api.smalruby.app/scratch-api-proxy/projects/a%2Fb', + expect.any(Object), + ); + }); + + test('endpoint with trailing slash works (HOC strips it before passing in)', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ project_token: 'tok' }), + }); + + // The HOC strips trailing slashes; here we verify the function does not double-slash if given a clean endpoint. + await fetchProjectInfo('https://stg.api.smalruby.app', '12345'); + expect(global.fetch).toHaveBeenCalledWith( + 'https://stg.api.smalruby.app/scratch-api-proxy/projects/12345', + expect.any(Object), + ); + }); +}); diff --git a/packages/scratch-gui/webpack.config.js b/packages/scratch-gui/webpack.config.js index a8024398a8b..ae20631df86 100644 --- a/packages/scratch-gui/webpack.config.js +++ b/packages/scratch-gui/webpack.config.js @@ -112,6 +112,9 @@ const baseConfig = new ScratchWebpackConfigBuilder( 'process.env.MESH_PERIODIC_DATA_SYNC_INTERVAL_MS': `"${process.env.MESH_PERIODIC_DATA_SYNC_INTERVAL_MS || ''}"`, 'process.env.MESH_NETWORK_FILTER': `"${process.env.MESH_NETWORK_FILTER || ''}"`, 'process.env.RUBYTEE_RELAY_ENDPOINT': `"${process.env.RUBYTEE_RELAY_ENDPOINT || ''}"`, + // === Smalruby: Start of scratch api proxy endpoint === + 'process.env.SCRATCH_API_PROXY_ENDPOINT': `"${process.env.SCRATCH_API_PROXY_ENDPOINT || 'https://api.smalruby.app'}"`, + // === Smalruby: End of scratch api proxy endpoint === // === Smalruby: Start of classroom API === 'process.env.CLASSROOM_API_ENDPOINT': `"${process.env.CLASSROOM_API_ENDPOINT || ''}"`, 'process.env.CLASSROOM_REFRESH_INTERVAL_MS': `"${process.env.CLASSROOM_REFRESH_INTERVAL_MS || '30000'}"`, From be67af55bdf43bd8bf0691d76a0bd85b39a5f758 Mon Sep 17 00:00:00 2001 From: Kouji Takao Date: Wed, 29 Apr 2026 16:23:57 +0900 Subject: [PATCH 4/4] style: apply prettier formatting to url-loader files --- packages/scratch-gui/src/lib/url-loader-hoc.jsx | 13 +++++-------- packages/scratch-gui/src/lib/url-loader.js | 6 ++---- packages/scratch-gui/src/locales/en.js | 3 +-- packages/scratch-gui/src/locales/ja.js | 3 +-- .../scratch-gui/test/unit/lib/url-loader.test.js | 7 +------ 5 files changed, 10 insertions(+), 22 deletions(-) diff --git a/packages/scratch-gui/src/lib/url-loader-hoc.jsx b/packages/scratch-gui/src/lib/url-loader-hoc.jsx index 81b46598830..27f6d61324c 100644 --- a/packages/scratch-gui/src/lib/url-loader-hoc.jsx +++ b/packages/scratch-gui/src/lib/url-loader-hoc.jsx @@ -22,13 +22,8 @@ import intlShape from './intlShape'; import { loadProjectWithChecks } from './project-loader-utils'; import { persistRubyVersion } from './settings/ruby-version/persistence'; import sharedMessages from './shared-messages'; +import { UrlLoaderError, fetchProjectInfo, formatLoadError, urlLoaderMessages } from './url-loader'; import { extractScratchProjectId, isValidScratchProjectUrl } from './url-parser'; -import { - UrlLoaderError, - fetchProjectInfo, - formatLoadError, - urlLoaderMessages, -} from './url-loader'; const SCRATCH_API_PROXY_ENDPOINT = (process.env.SCRATCH_API_PROXY_ENDPOINT || 'https://api.smalruby.app').replace( /\/+$/, @@ -252,7 +247,8 @@ const URLLoaderHOC = function (WrappedComponent) { closeFileMenu: () => dispatch(closeFileMenu()), closeUrlLoaderModal: () => dispatch(closeUrlLoaderModal()), onError: error => dispatch(projectError(error)), - onLoadedProject: (loadingState, canSave, success) => dispatch(onLoadedProject(loadingState, canSave, success)), + onLoadedProject: (loadingState, canSave, success) => + dispatch(onLoadedProject(loadingState, canSave, success)), onLoadingFinished: () => { dispatch(closeLoadingProject()); dispatch(closeFileMenu()); @@ -268,7 +264,8 @@ const URLLoaderHOC = function (WrappedComponent) { setProjectId: projectId => dispatch(setProjectId(projectId)), }); - const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign({}, stateProps, dispatchProps, ownProps); + const mergeProps = (stateProps, dispatchProps, ownProps) => + Object.assign({}, stateProps, dispatchProps, ownProps); return injectIntl(connect(mapStateToProps, mapDispatchToProps, mergeProps)(URLLoaderComponent)); }; diff --git a/packages/scratch-gui/src/lib/url-loader.js b/packages/scratch-gui/src/lib/url-loader.js index 44ae8e05525..f222fa4f82c 100644 --- a/packages/scratch-gui/src/lib/url-loader.js +++ b/packages/scratch-gui/src/lib/url-loader.js @@ -17,8 +17,7 @@ const urlLoaderMessages = defineMessages({ }, projectNotFound: { id: 'gui.urlLoader.projectNotFound', - defaultMessage: - 'Project not found. It may have been unshared, or the URL may be incorrect.', + defaultMessage: 'Project not found. It may have been unshared, or the URL may be incorrect.', description: 'Shown when the upstream Scratch API returns 404.', }, projectAccessDenied: { @@ -34,8 +33,7 @@ const urlLoaderMessages = defineMessages({ networkError: { id: 'gui.urlLoader.networkError', defaultMessage: 'Network error. Please check your internet connection.', - description: - 'Shown when fetch throws a TypeError (offline, DNS failure, blocked by extension).', + description: 'Shown when fetch throws a TypeError (offline, DNS failure, blocked by extension).', }, }); diff --git a/packages/scratch-gui/src/locales/en.js b/packages/scratch-gui/src/locales/en.js index 6d2d1176b9b..bdda0c767f2 100644 --- a/packages/scratch-gui/src/locales/en.js +++ b/packages/scratch-gui/src/locales/en.js @@ -425,8 +425,7 @@ export default { // URL Loader messages 'gui.urlLoader.loadError': 'The project URL that was entered failed to load.', 'gui.urlLoader.invalidUrl': 'Please enter a valid Scratch project URL or Google Drive URL.', - 'gui.urlLoader.projectNotFound': - 'Project not found. It may have been unshared, or the URL may be incorrect.', + 'gui.urlLoader.projectNotFound': 'Project not found. It may have been unshared, or the URL may be incorrect.', 'gui.urlLoader.projectAccessDenied': 'Access to this project is denied.', 'gui.urlLoader.serverError': 'Scratch server-side error. Please try again later.', 'gui.urlLoader.networkError': 'Network error. Please check your internet connection.', diff --git a/packages/scratch-gui/src/locales/ja.js b/packages/scratch-gui/src/locales/ja.js index 7b56b198749..c9ca5430f1c 100644 --- a/packages/scratch-gui/src/locales/ja.js +++ b/packages/scratch-gui/src/locales/ja.js @@ -251,8 +251,7 @@ export default { 'gui.urlLoader.projectNotFound': 'プロジェクトが見つかりません。共有が解除されたか、URLが間違っている可能性があります。', 'gui.urlLoader.projectAccessDenied': 'このプロジェクトにはアクセスできません。', - 'gui.urlLoader.serverError': - 'Scratchサーバー側でエラーが発生しました。しばらく経ってから再度お試しください。', + 'gui.urlLoader.serverError': 'Scratchサーバー側でエラーが発生しました。しばらく経ってから再度お試しください。', 'gui.urlLoader.networkError': 'ネットワークエラーが発生しました。インターネット接続を確認してください。', 'gui.urlLoader.title': 'Scratchから読み込む', 'gui.urlLoader.urlPlaceholder': 'プロジェクトのURLを入力...', diff --git a/packages/scratch-gui/test/unit/lib/url-loader.test.js b/packages/scratch-gui/test/unit/lib/url-loader.test.js index 38c43bb4659..c00c343d6f6 100644 --- a/packages/scratch-gui/test/unit/lib/url-loader.test.js +++ b/packages/scratch-gui/test/unit/lib/url-loader.test.js @@ -1,9 +1,4 @@ -import { - UrlLoaderError, - fetchProjectInfo, - formatLoadError, - urlLoaderMessages, -} from '../../../src/lib/url-loader'; +import { UrlLoaderError, fetchProjectInfo, formatLoadError, urlLoaderMessages } from '../../../src/lib/url-loader'; const mockIntl = { formatMessage: msg => msg.defaultMessage,