diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..017dcd1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,41 @@ +# Dependencies (re-installed inside Docker) +node_modules + +# Build outputs +dist +*.tgz + +# Git / CI +.git +.github +.husky + +# CLI source (not needed for webapp) +src +plugin +infra + +# VS Code extension +vscode-extension + +# Docs / examples +docs +examples + +# Non-essential root files +eval-results.* +readiness-report.html +CHANGELOG.md +CODE_OF_CONDUCT.md +CONTRIBUTING.md +SECURITY.md +SUPPORT.md +CODEOWNERS +LICENSE +AGENTS.md +agentrc.eval.json +eslint.config.js +vitest.config.ts +tsconfig.json +tsup.config.ts +.vscode diff --git a/.github/prompts/align-cli-webapp-reports.prompt.md b/.github/prompts/align-cli-webapp-reports.prompt.md new file mode 100644 index 0000000..aefaa05 --- /dev/null +++ b/.github/prompts/align-cli-webapp-reports.prompt.md @@ -0,0 +1,161 @@ +--- +description: Compare CLI visual readiness report with local webapp report for a given repo, identify differences in checks/rendering/scoring, and fix them +--- + +You are debugging consistency between two readiness report outputs for the **AgentRC** project: + +1. **CLI visual report** — generated by `npm run dev -- readiness --visual` from the repo root (produces an HTML file) +2. **Webapp report** — rendered by the local dev server at `http://localhost:3000/{owner}/{repo}` + +Both should produce identical results for the same repository because they share the same core engine (`packages/core/src/services/readiness.ts`). In practice they can diverge due to rendering differences or scoring logic bugs. + +## Architecture Reference + +### Shared Core (source of truth for checks) + +- `packages/core/src/services/readiness.ts` — All criteria definitions, `countStatus()`, `buildCriteria()`, `buildExtras()`, pillar/level aggregation +- Criteria scopes: `repo` (always), `app` (per-application), `area` (only with `--per-area`) +- `countStatus()` **excludes** skipped checks from the denominator when computing pillar pass/total +- Extras (bonus checks) are **not scored** — they don't affect levels or totals + +### CLI Rendering + +- `packages/core/src/services/visualReport.ts` — Generates the standalone HTML report +- `calculateAiToolingData()` — Aggregates AI criteria across repos (counts all including skipped in the hero display) +- Total checks: `report.pillars.reduce((s, p) => s + p.total, 0)` +- Does **not** render bonus/extras section in HTML output + +### Webapp Backend + +- `webapp/backend/src/services/scanner.js` — Clones repo, calls `runReadinessReport()` from `@agentrc/core` +- `webapp/backend/src/routes/scan.js` — `POST /api/scan` endpoint +- Returns the raw `ReadinessReport` JSON (same shape as CLI) +- Uses `@agentrc/core` as a `file:../../packages/core` dependency — always uses local source code + +### Webapp Frontend + +- `webapp/frontend/src/report.js` — Independent rendering implementation (NOT shared with CLI) +- `buildHero()` — Total from `report.pillars.reduce((s, p) => s + p.total, 0)` +- `buildAiToolingHero()` — Renders all AI criteria (including skipped) with pass/total count +- `buildPillarDetails()` — Shows per-pillar expandable cards +- **Does** render bonus checks section (unlike CLI) +- Has Service Information section (policy chain, engine signals) — CLI doesn't + +### Known Inconsistency Patterns + +- **AI Hero vs Pillar scoring**: Both implementations count skipped checks as non-passing in the AI Hero but exclude them from pillar denominator via `countStatus()` +- **Rendering gaps**: Webapp shows bonus checks + service info; CLI doesn't +- **Icon mapping**: CLI uses HTML entities via `getAiCriterionIcon()`; webapp uses emoji via `AI_ICONS` map — new criteria IDs may get fallback icon (`🔧`) in webapp + +## Step-by-Step Procedure + +### Phase 0: Start Local Webapp + +1. Start the webapp backend dev server (from the repo root): + + ``` + cd webapp/backend && npm run dev + ``` + + This starts the Express server at `http://localhost:3000` with the local `@agentrc/core` source. + +2. Optionally serve the frontend for full visual testing: + ``` + cd webapp/frontend && npx vite --port 5173 + ``` + +### Phase 1: Generate Both Reports + +3. Run the CLI against the target repo to produce the visual HTML report: + + ``` + npm run dev -- readiness --visual --repo {owner}/{repo} + ``` + + Save the output HTML (typically `readiness-report.html`). + +4. Hit the local webapp API to get the raw JSON: + + ``` + POST http://localhost:3000/api/scan + Body: {"repo_url":"https://github.com/{owner}/{repo}"} + ``` + + Example with PowerShell: + + ```powershell + $response = Invoke-RestMethod -Uri "http://localhost:3000/api/scan" -Method POST -ContentType "application/json" -Body '{"repo_url":"https://github.com/{owner}/{repo}"}' -TimeoutSec 120 + ``` + +5. Also open the local webapp page for visual comparison: `http://localhost:5173/{owner}/{repo}` (if frontend dev server is running) or `http://localhost:3000/{owner}/{repo}` (if backend serves static files). + +### Phase 2: Compare Data Layer + +6. Extract from CLI HTML: total checks, per-pillar passed/total, AI hero passed/total/percentage, criteria list with statuses, achieved level, fix-first items. + +7. Extract from webapp JSON: same fields. Use: + + ```powershell + $pillars = $response.pillars + $totalPassed = ($pillars | Measure-Object -Property passed -Sum).Sum + $totalChecks = ($pillars | Measure-Object -Property total -Sum).Sum + Write-Host "Total: $totalPassed of $totalChecks checks" + Write-Host "Criteria count: $($response.criteria.Count)" + ``` + +8. Diff the two — check for: + - **Missing criteria** in either side (criteria list length mismatch) + - **Status mismatches** for the same criterion ID + - **Total check count** differences (pillar aggregation) + - **AI Tooling hero** percentage/label differences + - **Achieved level** and next-level calculation differences + - **Fix-first** list ordering differences + +### Phase 3: Compare Rendering Layer + +9. Compare how both renderers handle: + - Skipped checks display (icon, text, inclusion in totals) + - Bonus/extras section presence + - Pillar grouping (repo-health vs ai-setup) + - AI criterion icons for new/unknown IDs + - Score thresholds for labels (Excellent/Good/Fair/Getting Started/Not Started) + +### Phase 4: Root Cause & Fix + +10. For each difference found, classify as: + - **Rendering divergence** → Fix in either `visualReport.ts` (CLI) or `report.js` (webapp) to align + - **Scoring logic bug** → Fix in `readiness.ts` (core) which fixes both + - **Icon/label mapping gap** → Update the icon map in the affected renderer + +11. Implement the fixes directly in the source files. + +12. After fixing, restart the webapp dev server and re-run Phase 1-2 to verify alignment. + +### Phase 5: Validate + +13. Confirm both reports show identical: + - Total check count (e.g., "11 of 20") + - Per-pillar passed/total + - AI Tooling hero percentage and label + - Achieved maturity level + - Fix-first items (same set, same order) + +14. Note any **acceptable differences** that are by-design (e.g., webapp shows bonus checks, CLI doesn't). + +15. Run existing tests to ensure no regressions: + ``` + npm test + cd webapp/backend && npm test + ``` + +## Output Format + +Produce a comparison table: + +| Aspect | CLI Value | Webapp Value | Match? | Root Cause | Fix | +| ------------ | --------- | ------------ | ------ | ---------- | --- | +| Total checks | X of Y | X of Z | ... | ... | ... | +| AI Hero % | ...% | ...% | ... | ... | ... | +| ... | ... | ... | ... | ... | ... | + +Then implement the fixes and verify. diff --git a/.github/workflows/webapp-cd.yml b/.github/workflows/webapp-cd.yml new file mode 100644 index 0000000..24c7f0c --- /dev/null +++ b/.github/workflows/webapp-cd.yml @@ -0,0 +1,178 @@ +name: Webapp CD + +on: + workflow_dispatch: + push: + branches: [main] + paths: + - "webapp/**" + - "packages/core/**" + - "Dockerfile.webapp" + - "infra/webapp/**" + +concurrency: + group: agentrc-webapp-production + cancel-in-progress: false + +permissions: + contents: read + packages: write + id-token: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/agentrc-webapp + RESOURCE_GROUP: agentrc-webapp-rg + BICEP_FILE: infra/webapp/main.bicep + +jobs: + build-push: + runs-on: ubuntu-latest + outputs: + image-tag: ${{ steps.meta.outputs.version }} + image-full: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} + steps: + - uses: actions/checkout@v4 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=sha,prefix=sha- + type=raw,value=latest + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile.webapp + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + security-scan: + runs-on: ubuntu-latest + needs: build-push + permissions: + security-events: write + packages: read + steps: + - uses: actions/checkout@v4 + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@v0.35.0 + with: + image-ref: ${{ needs.build-push.outputs.image-full }} + format: sarif + output: trivy-results.sarif + severity: CRITICAL,HIGH + - name: Upload Trivy scan results + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: trivy-results.sarif + + deploy: + runs-on: ubuntu-latest + needs: [build-push, security-scan] + environment: production + steps: + - uses: actions/checkout@v4 + + - name: Azure Login (OIDC) + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Ensure resource group + run: | + az group create \ + --name ${{ env.RESOURCE_GROUP }} \ + --location eastus \ + --tags application=agentrc-webapp managedBy=bicep + + - name: Push image to ACR + run: | + ACR_NAME=$(az acr list --resource-group ${{ env.RESOURCE_GROUP }} --query "[0].name" -o tsv) + if [ -z "$ACR_NAME" ]; then + echo "ACR not yet provisioned — will be created by Bicep and image imported after" + else + az acr import \ + --name "$ACR_NAME" \ + --source ${{ needs.build-push.outputs.image-full }} \ + --image agentrc-webapp:${{ needs.build-push.outputs.image-tag }} \ + --image agentrc-webapp:latest \ + --force + fi + + - name: Pre-deploy cleanup (Container Apps storage update limitation) + run: | + az containerapp delete \ + --name agentrc-webapp \ + --resource-group ${{ env.RESOURCE_GROUP }} \ + --yes 2>&1 || echo "Container app does not exist yet" + SUB_ID=$(az account show --query id -o tsv) + az rest --method DELETE \ + --url "https://management.azure.com/subscriptions/${SUB_ID}/resourceGroups/${{ env.RESOURCE_GROUP }}/providers/Microsoft.App/managedEnvironments/agentrc-env/storages/reportsshare?api-version=2024-03-01" \ + 2>&1 || echo "Storage mount does not exist yet" + sleep 10 + + - name: Deploy infrastructure + uses: azure/arm-deploy@v2 + with: + resourceGroupName: ${{ env.RESOURCE_GROUP }} + template: ${{ env.BICEP_FILE }} + parameters: > + containerImageTag=${{ needs.build-push.outputs.image-tag }} + ghTokenForScan=${{ secrets.GH_TOKEN_FOR_SCAN }} + + - name: Ensure image in ACR + run: | + ACR_NAME=$(az acr list --resource-group ${{ env.RESOURCE_GROUP }} --query "[0].name" -o tsv) + az acr import \ + --name "$ACR_NAME" \ + --source ${{ needs.build-push.outputs.image-full }} \ + --image agentrc-webapp:${{ needs.build-push.outputs.image-tag }} \ + --image agentrc-webapp:latest \ + --force + + - name: Restart container app to pick up new image + run: | + az containerapp revision restart \ + --name agentrc-webapp \ + --resource-group ${{ env.RESOURCE_GROUP }} \ + --revision "$(az containerapp revision list --name agentrc-webapp --resource-group ${{ env.RESOURCE_GROUP }} --query '[0].name' -o tsv)" + + - name: Smoke test + run: | + APP_URL=$(az containerapp show \ + --name agentrc-webapp \ + --resource-group ${{ env.RESOURCE_GROUP }} \ + --query "properties.configuration.ingress.fqdn" -o tsv) + echo "Testing https://${APP_URL}" + for i in 1 2 3 4 5; do + if curl -sf "https://${APP_URL}/api/health" | grep -q '"ok"'; then + break + fi + echo "Attempt $i failed, retrying in 15s..." + sleep 15 + done + curl -sf "https://${APP_URL}/api/health" | grep -q '"ok"' + curl -sf "https://${APP_URL}/" | grep -q "AgentRC" + echo "Smoke tests passed!" diff --git a/.github/workflows/webapp-ci.yml b/.github/workflows/webapp-ci.yml new file mode 100644 index 0000000..2a5421c --- /dev/null +++ b/.github/workflows/webapp-ci.yml @@ -0,0 +1,74 @@ +name: Webapp CI + +on: + push: + branches-ignore: [main] + paths: + - "webapp/**" + - "packages/core/**" + - "Dockerfile.webapp" + - "infra/webapp/**" + pull_request: + branches: [main] + paths: + - "webapp/**" + - "packages/core/**" + - "Dockerfile.webapp" + - "infra/webapp/**" + +concurrency: + group: webapp-ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + test-backend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "24" + cache: npm + cache-dependency-path: webapp/backend/package-lock.json + - name: Install root dependencies + run: npm ci + - name: Install backend dependencies + run: cd webapp/backend && npm ci + - name: Run tests + run: cd webapp/backend && npm test + + test-frontend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "24" + cache: npm + cache-dependency-path: webapp/frontend/package-lock.json + - name: Install frontend dependencies + run: cd webapp/frontend && npm ci + - name: Run tests + run: cd webapp/frontend && npm test + + build-and-scan: + runs-on: ubuntu-latest + needs: [test-backend, test-frontend] + permissions: + security-events: write + steps: + - uses: actions/checkout@v4 + - name: Build Docker image + run: docker build -f Dockerfile.webapp -t agentrc-webapp:test . + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@v0.35.0 + with: + image-ref: agentrc-webapp:test + format: sarif + output: trivy-results.sarif + severity: CRITICAL,HIGH + - name: Upload Trivy scan results + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: trivy-results.sarif diff --git a/.prettierignore b/.prettierignore index a8353ce..4114632 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,7 @@ # Auto-generated gh-aw lock files *.lock.yml .github/workflows/agentics-maintenance.yml + +# Webapp has its own formatting; Dockerfile is not parseable by Prettier +webapp/ +Dockerfile.webapp diff --git a/Dockerfile.webapp b/Dockerfile.webapp new file mode 100644 index 0000000..5844213 --- /dev/null +++ b/Dockerfile.webapp @@ -0,0 +1,33 @@ +FROM node:24-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +COPY packages/core/package.json ./packages/core/package.json +COPY packages/core/src ./packages/core/src +COPY webapp/backend/package.json ./webapp/backend/package.json +COPY webapp/backend/package-lock.json ./webapp/backend/package-lock.json +RUN npm ci --ignore-scripts +RUN cd ./webapp/backend && npm ci + +FROM node:24-alpine AS build +WORKDIR /app +COPY --from=deps /app /app +COPY webapp/backend/esbuild.config.js ./webapp/backend/esbuild.config.js +COPY webapp/backend/src ./webapp/backend/src +RUN cd ./webapp/backend && node esbuild.config.js + +FROM node:24-alpine +RUN apk add --no-cache git +WORKDIR /app/webapp/backend +COPY webapp/backend/package.json ./package.json +COPY --from=deps /app/webapp/backend/node_modules ./node_modules +COPY --from=deps /app/node_modules /app/node_modules +COPY --from=build /app/webapp/backend/dist ./dist +COPY webapp/frontend/ /app/frontend/ +ENV REPORTS_DIR=/app/data/reports +RUN mkdir -p /app/data \ + && chown -R node:node /app +USER node +EXPOSE 3000 +HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \ + CMD node -e "fetch('http://localhost:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" +CMD ["node", "dist/server.js"] diff --git a/eslint.config.js b/eslint.config.js index 5dd065d..4669f48 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -16,6 +16,7 @@ export default [ "node_modules/**", "coverage/**", "vscode-extension/**", + "webapp/**", "docs/**", "eslint.config.js", "*.config.ts" diff --git a/infra/webapp/main.bicep b/infra/webapp/main.bicep new file mode 100644 index 0000000..2137c6a --- /dev/null +++ b/infra/webapp/main.bicep @@ -0,0 +1,297 @@ +// AgentRC Web App Infrastructure — Azure Container Apps (Consumption plan) +// Resources: Log Analytics → App Insights (optional) → Container Apps Environment → Storage (optional) → Container App + +@description('Name prefix for all resources (lowercase letters and numbers only, max 10 chars)') +@minLength(1) +@maxLength(10) +param namePrefix string = 'agentrc' + +@description('Azure region') +param location string = resourceGroup().location + +@description('Container image to deploy (tag only — registry is derived from the ACR resource)') +param containerImageTag string = 'latest' + +@description('Enable Application Insights') +param enableAppInsights bool = true + +@description('Enable report sharing (requires Azure Files)') +param enableSharing bool = true + +@description('GitHub token for scanning private repos') +@secure() +param ghTokenForScan string = '' + +@description('Container startup strategy') +@allowed(['scale-to-zero', 'keep-warm']) +param containerStartupStrategy string = 'keep-warm' + +@description('Custom domain (optional, leave empty to skip)') +param customDomain string = '' + +@description('Tags for all resources') +param tags object = {} + +// ===== Log Analytics Workspace ===== +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { + name: '${namePrefix}-logs' + location: location + tags: tags + properties: { + sku: { + name: 'PerGB2018' + } + retentionInDays: 30 + } +} + +// ===== Application Insights (optional) ===== +resource appInsights 'Microsoft.Insights/components@2020-02-02' = if (enableAppInsights) { + name: '${namePrefix}-insights' + location: location + tags: tags + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalytics.id + } +} + +// ===== Container Apps Environment ===== +resource containerAppsEnv 'Microsoft.App/managedEnvironments@2024-03-01' = { + name: '${namePrefix}-env' + location: location + tags: tags + properties: { + appLogsConfiguration: { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: logAnalytics.properties.customerId + sharedKey: logAnalytics.listKeys().primarySharedKey + } + } + } +} + +// ===== Storage Account + File Share (for report sharing) ===== +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = if (enableSharing) { + name: take(toLower(replace('${namePrefix}st${uniqueString(resourceGroup().id)}', '-', '')), 24) + location: location + tags: tags + sku: { + name: 'Standard_LRS' + } + kind: 'StorageV2' + properties: { + minimumTlsVersion: 'TLS1_2' + supportsHttpsTrafficOnly: true + allowBlobPublicAccess: false + } +} + +resource fileService 'Microsoft.Storage/storageAccounts/fileServices@2023-05-01' = if (enableSharing) { + parent: storageAccount + name: 'default' +} + +resource fileShare 'Microsoft.Storage/storageAccounts/fileServices/shares@2023-05-01' = if (enableSharing) { + parent: fileService + name: 'reports' + properties: { + shareQuota: 1 // 1 GB + } +} + +// ===== Azure Container Registry ===== +resource acr 'Microsoft.ContainerRegistry/registries@2023-07-01' = { + name: take(toLower(replace('${namePrefix}webapp', '-', '')), 50) + location: location + tags: tags + sku: { + name: 'Basic' + } + properties: { + adminUserEnabled: false + } +} + +// ===== Container Apps Environment Storage (for Azure Files mount) ===== +resource envStorage 'Microsoft.App/managedEnvironments/storages@2024-03-01' = if (enableSharing) { + parent: containerAppsEnv + name: 'reportsshare' + properties: { + azureFile: { + accountName: storageAccount!.name + accountKey: storageAccount!.listKeys().keys[0].value + shareName: 'reports' + accessMode: 'ReadWrite' + } + } +} + +// ===== AcrPull Role Assignment (system-assigned managed identity) ===== +@description('AcrPull built-in role') +var acrPullRoleId = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') + +resource acrPullRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(acr.id, containerApp.id, acrPullRoleId) + scope: acr + properties: { + principalId: containerApp.identity.principalId + principalType: 'ServicePrincipal' + roleDefinitionId: acrPullRoleId + } +} + +resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { + name: '${namePrefix}-webapp' + location: location + tags: tags + identity: { + type: 'SystemAssigned' + } + properties: { + managedEnvironmentId: containerAppsEnv.id + configuration: { + ingress: { + external: true + targetPort: 3000 + transport: 'http' + allowInsecure: false + customDomains: !empty(customDomain) ? [ + { + name: customDomain + bindingType: 'SniEnabled' + } + ] : [] + } + registries: [ + { + server: acr.properties.loginServer + identity: 'system' + } + ] + secrets: concat( + [], + !empty(ghTokenForScan) ? [ + { + name: 'gh-token-for-scan' + value: ghTokenForScan + } + ] : [], + enableAppInsights ? [ + { + name: 'app-insights-connection-string' + value: appInsights!.properties.ConnectionString + } + ] : [] + ) + } + template: { + containers: [ + { + name: 'webapp' + image: '${acr.properties.loginServer}/agentrc-webapp:${containerImageTag}' + resources: { + cpu: json('0.25') + memory: '0.5Gi' + } + env: concat([ + { + name: 'NODE_ENV' + value: 'production' + } + { + name: 'PORT' + value: '3000' + } + { + name: 'ENABLE_SHARING' + value: enableSharing ? 'true' : 'false' + } + { + name: 'REPORTS_DIR' + value: enableSharing ? '/app/data/reports' : ':memory:' + } + ], + !empty(ghTokenForScan) ? [ + { + name: 'GH_TOKEN_FOR_SCAN' + secretRef: 'gh-token-for-scan' + } + ] : [], + enableAppInsights ? [ + { + name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' + secretRef: 'app-insights-connection-string' + } + ] : []) + probes: [ + { + type: 'Liveness' + httpGet: { + path: '/api/health' + port: 3000 + } + periodSeconds: 30 + failureThreshold: 3 + } + { + type: 'Readiness' + httpGet: { + path: '/api/health' + port: 3000 + } + periodSeconds: 10 + failureThreshold: 3 + } + ] + volumeMounts: enableSharing ? [ + { + volumeName: 'reportsdata' + mountPath: '/app/data' + } + ] : [] + } + ] + volumes: enableSharing ? [ + { + name: 'reportsdata' + storageName: 'reportsshare' + storageType: 'AzureFile' + } + ] : [] + scale: { + minReplicas: containerStartupStrategy == 'keep-warm' ? 1 : 0 + maxReplicas: 4 + rules: [ + { + name: 'http-rule' + http: { + metadata: { + concurrentRequests: '20' + } + } + } + ] + } + } + } + dependsOn: enableSharing ? [envStorage] : [] +} + +// ===== Outputs ===== +@description('Container App FQDN') +output appFqdn string = containerApp.properties.configuration.ingress.fqdn + +@description('Container App URL') +output appUrl string = 'https://${containerApp.properties.configuration.ingress.fqdn}' + +@description('ACR login server') +output acrLoginServer string = acr.properties.loginServer + +@description('Application Insights connection string') +output appInsightsConnectionString string = enableAppInsights ? appInsights!.properties.ConnectionString : '' + +@description('Log Analytics Workspace ID') +output logAnalyticsWorkspaceId string = logAnalytics.id diff --git a/infra/webapp/main.bicepparam b/infra/webapp/main.bicepparam new file mode 100644 index 0000000..6eb0d31 --- /dev/null +++ b/infra/webapp/main.bicepparam @@ -0,0 +1,12 @@ +using './main.bicep' + +param namePrefix = 'agentrc' +param containerImageTag = 'latest' +param enableSharing = true +param enableAppInsights = true +param containerStartupStrategy = 'keep-warm' +param tags = { + application: 'agentrc-webapp' + managedBy: 'bicep' + environment: 'production' +} diff --git a/vitest.config.ts b/vitest.config.ts index 9587a1a..1d823cf 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -10,6 +10,7 @@ export default defineConfig({ test: { environment: "node", testTimeout: 10_000, + exclude: ["webapp/**", "node_modules/**", "dist/**", "vscode-extension/**"], coverage: { provider: "v8", reporter: ["text", "html", "json-summary"], diff --git a/webapp/.env.example b/webapp/.env.example new file mode 100644 index 0000000..74e1989 --- /dev/null +++ b/webapp/.env.example @@ -0,0 +1,11 @@ +# GitHub token for scanning private repos (optional) +GH_TOKEN_FOR_SCAN= + +# Enable report sharing +ENABLE_SHARING=true + +# Report storage directory (use :memory: for in-memory storage in dev/tests) +REPORTS_DIR=:memory: + +# Environment +NODE_ENV=development diff --git a/webapp/backend/esbuild.config.js b/webapp/backend/esbuild.config.js new file mode 100644 index 0000000..766dcac --- /dev/null +++ b/webapp/backend/esbuild.config.js @@ -0,0 +1,21 @@ +import { build } from "esbuild"; + +await build({ + entryPoints: ["src/server.js"], + bundle: true, + platform: "node", + target: "node20", + format: "esm", + outfile: "dist/server.js", + sourcemap: true, + // Keep node_modules external — they're installed via npm ci + packages: "external", + // Bundle @agentrc/core (TypeScript source) into the output + alias: { + "@agentrc/core": "../../packages/core/src", + }, + banner: { + js: 'import { createRequire } from "node:module";\nconst require = createRequire(import.meta.url);', + }, + loader: { ".ts": "ts" }, +}); diff --git a/webapp/backend/package-lock.json b/webapp/backend/package-lock.json new file mode 100644 index 0000000..e5ffbf5 --- /dev/null +++ b/webapp/backend/package-lock.json @@ -0,0 +1,3501 @@ +{ + "name": "@agentrc/webapp-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@agentrc/webapp-backend", + "version": "1.0.0", + "dependencies": { + "cors": "^2.8.5", + "express": "^4.21.2", + "express-rate-limit": "^7.5.0", + "helmet": "^8.1.0" + }, + "devDependencies": { + "@agentrc/core": "file:../../packages/core", + "esbuild": "^0.25.0", + "tsx": "^4.21.0", + "vitest": "^3.1.1" + } + }, + "../../packages/core": { + "name": "@agentrc/core", + "version": "2.1.0", + "dev": true + }, + "node_modules/@agentrc/core": { + "resolved": "../../packages/core", + "link": true + }, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "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==", + "dev": true, + "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/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "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": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/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/vite-node/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/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/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/vitest/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/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/webapp/backend/package.json b/webapp/backend/package.json new file mode 100644 index 0000000..e3273f9 --- /dev/null +++ b/webapp/backend/package.json @@ -0,0 +1,24 @@ +{ + "name": "@agentrc/webapp-backend", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "node esbuild.config.js", + "start": "node dist/server.js", + "dev": "node --import tsx --env-file-if-exists=../.env --watch src/server.js", + "test": "vitest run" + }, + "dependencies": { + "cors": "^2.8.5", + "express": "^4.21.2", + "express-rate-limit": "^7.5.0", + "helmet": "^8.1.0" + }, + "devDependencies": { + "@agentrc/core": "file:../../packages/core", + "esbuild": "^0.25.0", + "tsx": "^4.21.0", + "vitest": "^3.1.1" + } +} diff --git a/webapp/backend/src/middleware/error-handler.js b/webapp/backend/src/middleware/error-handler.js new file mode 100644 index 0000000..6968dce --- /dev/null +++ b/webapp/backend/src/middleware/error-handler.js @@ -0,0 +1,34 @@ +/** + * Error handler middleware — maps known error types to HTTP status codes. + * Never exposes stack traces or tokens. + */ +import { ValidationError } from "../utils/url-parser.js"; +import { ReportValidationError } from "../services/report-validator.js"; +import { ConcurrencyError, CloneTimeoutError, GitCloneError } from "../services/scanner.js"; + +const errorMap = [ + { type: ValidationError, status: 400 }, + { type: ReportValidationError, status: 400 }, + { type: ConcurrencyError, status: 429 }, + { type: CloneTimeoutError, status: 504 }, + { type: GitCloneError, status: 502 } +]; + +/** Express error handler — must have 4 params. */ +export function errorHandler(err, _req, res, _next) { + const match = errorMap.find((e) => err instanceof e.type); + const status = match?.status ?? 500; + const message = status === 500 ? "Internal server error" : err.message; + + // Log server errors + if (status >= 500) { + console.error("[error]", err.message); + } + + res.status(status).json({ error: message }); +} + +/** 404 handler for unknown routes. */ +export function notFoundHandler(_req, res) { + res.status(404).json({ error: "Not found" }); +} diff --git a/webapp/backend/src/middleware/rate-limiter.js b/webapp/backend/src/middleware/rate-limiter.js new file mode 100644 index 0000000..fdcac91 --- /dev/null +++ b/webapp/backend/src/middleware/rate-limiter.js @@ -0,0 +1,32 @@ +/** + * Rate limiting middleware using express-rate-limit. + */ +import rateLimit from "express-rate-limit"; + +/** + * Create the scan endpoint rate limiter. + */ +export function createScanRateLimiter(runtime) { + return rateLimit({ + windowMs: runtime.scanRateLimitWindowMs, + max: runtime.scanRateLimitMax, + standardHeaders: true, + legacyHeaders: false, + skip: (req) => req.method === "OPTIONS", + message: { error: "Too many scan requests. Please try again later." } + }); +} + +/** + * Create the report endpoint rate limiter. + */ +export function createReportRateLimiter(runtime) { + return rateLimit({ + windowMs: runtime.reportRateLimitWindowMs, + max: runtime.reportRateLimitMax, + standardHeaders: true, + legacyHeaders: false, + skip: (req) => req.method === "OPTIONS", + message: { error: "Too many report requests. Please try again later." } + }); +} diff --git a/webapp/backend/src/routes/config.js b/webapp/backend/src/routes/config.js new file mode 100644 index 0000000..b9f4bc1 --- /dev/null +++ b/webapp/backend/src/routes/config.js @@ -0,0 +1,17 @@ +/** + * GET /api/config — Return public configuration for the frontend. + */ +import { Router } from "express"; + +export function createConfigRouter(runtime) { + const router = Router(); + + router.get("/", (_req, res) => { + res.json({ + sharingEnabled: runtime.sharingEnabled, + githubTokenProvided: runtime.githubTokenProvided + }); + }); + + return router; +} diff --git a/webapp/backend/src/routes/report.js b/webapp/backend/src/routes/report.js new file mode 100644 index 0000000..901de6f --- /dev/null +++ b/webapp/backend/src/routes/report.js @@ -0,0 +1,53 @@ +/** + * POST + GET /api/report — Share and retrieve readiness reports. + */ +import { Router } from "express"; +import { normalizeSharedReportResult } from "../services/report-validator.js"; + +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +export function createReportRouter(runtime) { + const router = Router(); + + // POST / — Save a shared report + router.post("/", async (req, res, next) => { + try { + if (!runtime.sharingEnabled) { + return res.status(503).json({ error: "Report sharing is not enabled." }); + } + + const normalized = normalizeSharedReportResult(req.body.result ?? req.body); + const id = await runtime.storage.saveReport(normalized); + const url = `/_/report/${id}`; + + res.status(201).json({ id, url }); + } catch (err) { + next(err); + } + }); + + // GET /:id — Retrieve a shared report + router.get("/:id", async (req, res, next) => { + try { + if (!runtime.sharingEnabled) { + return res.status(503).json({ error: "Report sharing is not enabled." }); + } + + const { id } = req.params; + if (!UUID_RE.test(id)) { + return res.status(400).json({ error: "Invalid report ID format." }); + } + + const report = await runtime.storage.getReport(id); + if (!report) { + return res.status(404).json({ error: "Report not found." }); + } + + res.json(report); + } catch (err) { + next(err); + } + }); + + return router; +} diff --git a/webapp/backend/src/routes/scan.js b/webapp/backend/src/routes/scan.js new file mode 100644 index 0000000..fc77551 --- /dev/null +++ b/webapp/backend/src/routes/scan.js @@ -0,0 +1,32 @@ +/** + * POST /api/scan — Clone a GitHub repo and run readiness report. + */ +import { Router } from "express"; +import { parseRepoUrl } from "../utils/url-parser.js"; +import { scanGitHubRepo } from "../services/scanner.js"; + +export function createScanRouter(runtime) { + const router = Router(); + + router.post("/", async (req, res, next) => { + try { + const { repo_url } = req.body; + + // Validate URL + const { owner, repo } = parseRepoUrl(repo_url); + + // Run scan + const report = await scanGitHubRepo(owner, repo, { + token: runtime.githubToken, + timeoutMs: runtime.cloneTimeoutMs, + maxConcurrent: runtime.maxConcurrentScans + }); + + res.json(report); + } catch (err) { + next(err); + } + }); + + return router; +} diff --git a/webapp/backend/src/server.js b/webapp/backend/src/server.js new file mode 100644 index 0000000..3f03d31 --- /dev/null +++ b/webapp/backend/src/server.js @@ -0,0 +1,146 @@ +/** + * Express server factory and startup. + * createRuntime() → createApp(runtime) → listen + */ +import { fileURLToPath } from "node:url"; +import { dirname, resolve } from "node:path"; +import express from "express"; +import cors from "cors"; +import helmet from "helmet"; + +import { createScanRouter } from "./routes/scan.js"; +import { createReportRouter } from "./routes/report.js"; +import { createConfigRouter } from "./routes/config.js"; +import { createScanRateLimiter, createReportRateLimiter } from "./middleware/rate-limiter.js"; +import { errorHandler, notFoundHandler } from "./middleware/error-handler.js"; +import { createStorage, startReportCleanup, stopReportCleanup } from "./services/storage.js"; +import { startStaleDirSweeper, stopStaleDirSweeper } from "./services/scanner.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +/** Load env vars and build computed runtime config. */ +export function createRuntime() { + const port = parseInt(process.env.PORT || "3000", 10); + const githubToken = process.env.GH_TOKEN_FOR_SCAN || ""; + const sharingEnabled = process.env.ENABLE_SHARING === "true"; + const reportsDir = process.env.REPORTS_DIR || ":memory:"; + const frontendPath = resolve(__dirname, "../../frontend"); + const appInsightsConnectionString = + process.env.APPLICATIONINSIGHTS_CONNECTION_STRING || + process.env.PUBLIC_APPLICATIONINSIGHTS_CONNECTION_STRING || + ""; + + return { + port, + githubToken, + githubTokenProvided: !!githubToken, + sharingEnabled, + reportsDir, + frontendPath, + appInsightsConnectionString, + storage: createStorage(reportsDir), + cloneTimeoutMs: parseInt(process.env.SCAN_CLONE_TIMEOUT_MS || "60000", 10), + maxConcurrentScans: parseInt(process.env.MAX_CONCURRENT_SCANS || "5", 10), + scanRateLimitWindowMs: parseInt(process.env.SCAN_RATE_LIMIT_WINDOW_MS || "900000", 10), + scanRateLimitMax: parseInt(process.env.SCAN_RATE_LIMIT_MAX || "30", 10), + reportRateLimitWindowMs: parseInt(process.env.REPORT_RATE_LIMIT_WINDOW_MS || "900000", 10), + reportRateLimitMax: parseInt(process.env.REPORT_RATE_LIMIT_MAX || "60", 10) + }; +} + +/** Build Express app from runtime config. */ +export function createApp(runtime) { + const app = express(); + + // Trust one proxy hop (Azure Container Apps load balancer) + app.set("trust proxy", 1); + + // Security headers + app.use( + helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'"], + imgSrc: ["'self'", "data:"], + connectSrc: ["'self'"] + } + } + }) + ); + + const corsOrigin = process.env.CORS_ORIGIN || false; + app.use(cors({ origin: corsOrigin, credentials: false })); + app.use(express.json({ limit: "1mb" })); + + // API routes + app.get("/api/health", (_req, res) => { + res.json({ + status: "ok", + githubTokenProvided: runtime.githubTokenProvided, + sharingEnabled: runtime.sharingEnabled + }); + }); + + app.use("/api/config", createConfigRouter(runtime)); + app.use("/api/scan", createScanRateLimiter(runtime), createScanRouter(runtime)); + app.use("/api/report", createReportRateLimiter(runtime), createReportRouter(runtime)); + + // Static frontend files + app.use(express.static(runtime.frontendPath)); + + // SPA catch-all: serve index.html for non-API routes + app.get(/^\/(?!api\/).*/, (_req, res, next) => { + res.sendFile("index.html", { root: runtime.frontendPath }, (err) => { + if (err) next(err); + }); + }); + + // Error handling + app.use(notFoundHandler); + app.use(errorHandler); + + return app; +} + +/** Start the server (only when run directly, not imported). */ +function start() { + const runtime = createRuntime(); + const app = createApp(runtime); + + startStaleDirSweeper(); + if (runtime.sharingEnabled) startReportCleanup(); + + const server = app.listen(runtime.port, () => { + console.log(`AgentRC webapp listening on http://localhost:${runtime.port}`); + console.log(` GitHub token: ${runtime.githubTokenProvided ? "provided" : "not set"}`); + console.log(` Sharing: ${runtime.sharingEnabled ? "enabled" : "disabled"}`); + console.log(` Reports dir: ${runtime.reportsDir}`); + }); + + // Graceful shutdown + const shutdown = () => { + console.log("\nShutting down..."); + stopStaleDirSweeper(); + stopReportCleanup(); + server.close(() => { + console.log("Server closed."); + process.exit(0); + }); + // Force exit after 10s + setTimeout(() => process.exit(1), 10_000).unref(); + }; + + process.on("SIGTERM", shutdown); + process.on("SIGINT", shutdown); +} + +// Auto-start when run directly +const isMain = + process.argv[1] && + (process.argv[1] === fileURLToPath(import.meta.url) || process.argv[1].endsWith("server.js")); + +if (isMain) { + start(); +} diff --git a/webapp/backend/src/services/report-validator.js b/webapp/backend/src/services/report-validator.js new file mode 100644 index 0000000..d7ff72c --- /dev/null +++ b/webapp/backend/src/services/report-validator.js @@ -0,0 +1,295 @@ +/** + * Shared report validation — normalizes and validates ReadinessReport + * before persisting for sharing. + */ + +export class ReportValidationError extends Error { + constructor(message) { + super(message); + this.name = "ReportValidationError"; + } +} + +const MAX_STRING_LEN = 10_000; +const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/; +const GITHUB_URL_RE = /^https:\/\/github\.com\/[a-zA-Z0-9]([a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?\/[a-zA-Z0-9._-]{1,100}$/; + +const ALLOWED_STATUS = new Set(["pass", "fail", "skip"]); +const ALLOWED_IMPACT = new Set(["high", "medium", "low"]); +const ALLOWED_EFFORT = new Set(["high", "medium", "low"]); + +/** + * Validate and normalize a ReadinessReport for sharing. + * Strips internal fields and validates structure. + */ +export function normalizeSharedReportResult(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new ReportValidationError("Report must be a non-null object."); + } + + // Prevent prototype pollution + if ( + Object.prototype.hasOwnProperty.call(value, "__proto__") || + Object.prototype.hasOwnProperty.call(value, "prototype") + ) { + throw new ReportValidationError("Invalid report structure."); + } + + const { + generatedAt, + isMonorepo, + apps, + pillars, + levels, + achievedLevel, + criteria, + extras, + areaReports, + policies, + // Intentionally strip: + repoPath: _repoPath, + engine: _engine, + // Allow pass-through of webapp-added fields: + repo_url, + repo_name, + durationMs, + ...rest + } = value; + + // Reject unknown fields + if (Object.keys(rest).length > 0) { + throw new ReportValidationError(`Unknown fields: ${Object.keys(rest).join(", ")}`); + } + + // Required fields + if (!generatedAt || !ISO_DATE_RE.test(generatedAt)) { + throw new ReportValidationError("generatedAt must be a valid ISO timestamp."); + } + + if (typeof isMonorepo !== "boolean") { + throw new ReportValidationError("isMonorepo must be a boolean."); + } + + if ( + typeof achievedLevel !== "number" || + !Number.isInteger(achievedLevel) || + achievedLevel < 0 || + achievedLevel > 5 + ) { + throw new ReportValidationError("achievedLevel must be an integer 0-5."); + } + + if (!Array.isArray(pillars)) { + throw new ReportValidationError("pillars must be an array."); + } + if (pillars.length > 50) { + throw new ReportValidationError("pillars array too large."); + } + for (const p of pillars) { + if (!p || typeof p !== "object" || Array.isArray(p)) { + throw new ReportValidationError("Each pillar must be a non-null object."); + } + if (typeof p.id !== "string" || p.id.length > 200) { + throw new ReportValidationError("Pillar id must be a string (max 200 chars)."); + } + if (typeof p.name !== "string" || p.name.length > MAX_STRING_LEN) { + throw new ReportValidationError("Pillar name must be a string within length limits."); + } + p.passed = typeof p.passed === "number" && Number.isFinite(p.passed) ? p.passed : 0; + p.total = typeof p.total === "number" && Number.isFinite(p.total) ? p.total : 0; + p.passRate = typeof p.passRate === "number" && Number.isFinite(p.passRate) ? p.passRate : 0; + } + + if (!Array.isArray(levels)) { + throw new ReportValidationError("levels must be an array."); + } + if (levels.length > 10) { + throw new ReportValidationError("levels array too large."); + } + for (const l of levels) { + if (!l || typeof l !== "object" || Array.isArray(l)) { + throw new ReportValidationError("Each level must be a non-null object."); + } + if (typeof l.level !== "number" || !Number.isInteger(l.level) || l.level < 0 || l.level > 5) { + throw new ReportValidationError("Level.level must be an integer 0-5."); + } + if (typeof l.name !== "string" || l.name.length > MAX_STRING_LEN) { + throw new ReportValidationError("Level name must be a string within length limits."); + } + if (typeof l.achieved !== "boolean") { + throw new ReportValidationError("Level.achieved must be a boolean."); + } + l.passed = typeof l.passed === "number" && Number.isFinite(l.passed) ? l.passed : 0; + l.total = typeof l.total === "number" && Number.isFinite(l.total) ? l.total : 0; + l.passRate = typeof l.passRate === "number" && Number.isFinite(l.passRate) ? l.passRate : 0; + } + + if (!Array.isArray(criteria)) { + throw new ReportValidationError("criteria must be an array."); + } + if (criteria.length > 500) { + throw new ReportValidationError("criteria array too large."); + } + + // Validate and whitelist criteria fields to prevent XSS via unvalidated nested objects + const ALLOWED_CRITERIA_KEYS = new Set([ + "id", "title", "pillar", "level", "scope", "impact", "effort", + "status", "reason", "evidence", "passRate", + "appSummary", "areaSummary", "appFailures", "areaFailures" + ]); + for (let i = 0; i < criteria.length; i++) { + const c = criteria[i]; + if (!c || typeof c !== "object" || Array.isArray(c)) { + throw new ReportValidationError("Each criteria item must be a non-null object."); + } + // Strip unknown keys + for (const key of Object.keys(c)) { + if (!ALLOWED_CRITERIA_KEYS.has(key)) { + delete c[key]; + } + } + if (c.title !== undefined) { + if (typeof c.title !== "string") { + delete c.title; + } else if (c.title.length > MAX_STRING_LEN) { + throw new ReportValidationError("Criteria title too long."); + } + } + if (c.reason !== undefined) { + if (typeof c.reason !== "string") { + delete c.reason; + } else if (c.reason.length > MAX_STRING_LEN) { + throw new ReportValidationError("Criteria reason too long."); + } + } + if (c.status !== undefined && !ALLOWED_STATUS.has(c.status)) { + delete c.status; + } + if (c.impact !== undefined && !ALLOWED_IMPACT.has(c.impact)) { + delete c.impact; + } + if (c.effort !== undefined && !ALLOWED_EFFORT.has(c.effort)) { + delete c.effort; + } + // Coerce appSummary/areaSummary to { passed: number, total: number } or remove + if (c.appSummary !== undefined) { + if (c.appSummary && typeof c.appSummary === "object" && !Array.isArray(c.appSummary)) { + const passed = Number(c.appSummary.passed); + const total = Number(c.appSummary.total); + c.appSummary = { + passed: Number.isFinite(passed) ? passed : 0, + total: Number.isFinite(total) ? total : 0 + }; + } else { + delete c.appSummary; + } + } + if (c.areaSummary !== undefined) { + if (c.areaSummary && typeof c.areaSummary === "object" && !Array.isArray(c.areaSummary)) { + const passed = Number(c.areaSummary.passed); + const total = Number(c.areaSummary.total); + c.areaSummary = { + passed: Number.isFinite(passed) ? passed : 0, + total: Number.isFinite(total) ? total : 0 + }; + } else { + delete c.areaSummary; + } + } + // Coerce appFailures/areaFailures to arrays of strings or remove + if (c.appFailures !== undefined) { + if (Array.isArray(c.appFailures)) { + c.appFailures = c.appFailures.filter((f) => typeof f === "string").map((f) => f.slice(0, MAX_STRING_LEN)); + } else { + delete c.appFailures; + } + } + if (c.areaFailures !== undefined) { + if (Array.isArray(c.areaFailures)) { + c.areaFailures = c.areaFailures.filter((f) => typeof f === "string").map((f) => f.slice(0, MAX_STRING_LEN)); + } else { + delete c.areaFailures; + } + } + // Coerce evidence to array of strings or remove + if (c.evidence !== undefined) { + if (Array.isArray(c.evidence)) { + c.evidence = c.evidence.filter((e) => typeof e === "string").map((e) => e.slice(0, MAX_STRING_LEN)); + } else { + delete c.evidence; + } + } + // Coerce passRate to finite number or remove + if (c.passRate !== undefined) { + const pr = Number(c.passRate); + c.passRate = Number.isFinite(pr) ? pr : undefined; + if (c.passRate === undefined) delete c.passRate; + } + } + + const normalized = { + generatedAt, + isMonorepo, + apps: Array.isArray(apps) ? apps : [], + pillars, + levels, + achievedLevel, + criteria, + extras: Array.isArray(extras) ? extras.filter((e) => e && typeof e === "object" && !Array.isArray(e)) : [] + }; + + // Sanitize extras enum fields + for (const e of normalized.extras) { + if (e.status !== undefined && !ALLOWED_STATUS.has(e.status)) { + delete e.status; + } + } + + // Validate areaReports: must be an array of objects with { area, criteria[], pillars[] } + if (areaReports != null) { + if (!Array.isArray(areaReports) || areaReports.length > 50) { + throw new ReportValidationError("areaReports must be an array with at most 50 entries."); + } + for (const ar of areaReports) { + if (!ar || typeof ar !== "object" || Array.isArray(ar)) { + throw new ReportValidationError("Each areaReport must be a non-null object."); + } + if (!ar.area || typeof ar.area !== "object" || Array.isArray(ar.area)) { + throw new ReportValidationError("Each areaReport must have an area object."); + } + if (!Array.isArray(ar.criteria)) { + throw new ReportValidationError("Each areaReport must have a criteria array."); + } + if (!Array.isArray(ar.pillars)) { + throw new ReportValidationError("Each areaReport must have a pillars array."); + } + } + normalized.areaReports = areaReports; + } + + // Validate policies: must be { chain: string[], criteriaCount: number } + if (policies != null) { + if (!policies || typeof policies !== "object" || Array.isArray(policies)) { + throw new ReportValidationError("policies must be a non-null object."); + } + if (!Array.isArray(policies.chain) || !policies.chain.every((c) => typeof c === "string")) { + throw new ReportValidationError("policies.chain must be an array of strings."); + } + if ( + typeof policies.criteriaCount !== "number" || + !Number.isInteger(policies.criteriaCount) || + policies.criteriaCount < 0 + ) { + throw new ReportValidationError("policies.criteriaCount must be a non-negative integer."); + } + normalized.policies = { chain: policies.chain, criteriaCount: policies.criteriaCount }; + } + if (repo_url) { + const urlStr = String(repo_url).slice(0, 500); + if (GITHUB_URL_RE.test(urlStr)) normalized.repo_url = urlStr; + } + if (repo_name) normalized.repo_name = String(repo_name).slice(0, 200); + if (typeof durationMs === "number" && Number.isFinite(durationMs)) normalized.durationMs = durationMs; + + return normalized; +} diff --git a/webapp/backend/src/services/scanner.js b/webapp/backend/src/services/scanner.js new file mode 100644 index 0000000..f0dcb50 --- /dev/null +++ b/webapp/backend/src/services/scanner.js @@ -0,0 +1,131 @@ +/** + * Scan orchestrator — clones a GitHub repo and runs readiness report. + */ +import { cloneRepo, setRemoteUrl } from "@agentrc/core/services/git"; +import { runReadinessReport } from "@agentrc/core/services/readiness"; +import { createTempDir, removeTempDir, sweepStaleTempDirs } from "../utils/cleanup.js"; + +const DEFAULT_CLONE_TIMEOUT_MS = 60_000; +const DEFAULT_MAX_CONCURRENT = 5; +const SWEEP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes + +let activeScans = 0; +let sweepTimer = null; + +export class ConcurrencyError extends Error { + constructor() { + super("Too many concurrent scans. Please try again later."); + this.name = "ConcurrencyError"; + } +} + +export class CloneTimeoutError extends Error { + constructor() { + super("Repository clone timed out."); + this.name = "CloneTimeoutError"; + } +} + +export class GitCloneError extends Error { + constructor(message) { + super(message); + this.name = "GitCloneError"; + } +} + +/** + * Clone a GitHub repo to a temp dir and run readiness report. + */ +export async function scanGitHubRepo( + owner, + repo, + { + token, + timeoutMs = DEFAULT_CLONE_TIMEOUT_MS, + maxConcurrent = DEFAULT_MAX_CONCURRENT + } = {} +) { + if (activeScans >= maxConcurrent) { + throw new ConcurrencyError(); + } + + activeScans++; + let tempDir; + const startMs = Date.now(); + + try { + tempDir = await createTempDir(); + + // Build clone URL + const baseUrl = `https://github.com/${owner}/${repo}.git`; + const cloneUrl = token + ? `https://x-access-token:${encodeURIComponent(token)}@github.com/${owner}/${repo}.git` + : baseUrl; + + // Clone + try { + await cloneRepo(cloneUrl, tempDir, { + shallow: true, + timeoutMs + }); + // Best-effort: strip credentials from the git remote to avoid + // token persistence in .git/config (matches @agentrc/core/services/batch.ts) + if (token) { + await setRemoteUrl(tempDir, baseUrl).catch(() => {}); + } + } catch (err) { + const rawMessage = err instanceof Error ? err.message : String(err); + if (rawMessage.includes("timed out") || rawMessage.includes("timeout")) { + throw new CloneTimeoutError(); + } + // Strip embedded credentials from error messages to avoid leaking tokens + const safeMessage = rawMessage + .replace(/https:\/\/[^@]+@/g, "https://***@"); + throw new GitCloneError(`Failed to clone repository: ${safeMessage}`); + } + + // Run readiness report + const report = await runReadinessReport({ + repoPath: tempDir, + includeExtras: true + }); + + const durationMs = Date.now() - startMs; + + // Strip repoPath from report (privacy) and add repo info + const { repoPath: _stripped, ...rest } = report; + return { + ...rest, + repo_url: `https://github.com/${owner}/${repo}`, + repo_name: `${owner}/${repo}`, + durationMs + }; + } finally { + activeScans--; + if (tempDir) { + removeTempDir(tempDir).catch(() => {}); + } + } +} + +/** Get current number of active scans. */ +export function getActiveScans() { + return activeScans; +} + +/** Start background sweep of stale temp dirs. */ +export function startStaleDirSweeper() { + if (sweepTimer) return; + sweepTimer = setInterval(() => { + sweepStaleTempDirs().catch(() => {}); + }, SWEEP_INTERVAL_MS); + sweepTimer.unref(); +} + +/** Stop the background sweeper. */ +export function stopStaleDirSweeper() { + if (sweepTimer) { + clearInterval(sweepTimer); + sweepTimer = null; + } +} diff --git a/webapp/backend/src/services/storage.js b/webapp/backend/src/services/storage.js new file mode 100644 index 0000000..10fcfd9 --- /dev/null +++ b/webapp/backend/src/services/storage.js @@ -0,0 +1,140 @@ +/** + * Report storage — file-based JSON persistence or in-memory for tests. + */ +import { randomUUID } from "node:crypto"; +import { mkdir, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +const TTL_MS = 90 * 24 * 60 * 60 * 1000; // 90 days +const CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 60 minutes + +let cleanupTimer = null; +let activeStorage = null; + +/** + * Create a storage backend. + * When reportsDir is ":memory:", uses an in-memory Map (for tests). + */ +export function createStorage(reportsDir) { + if (reportsDir === ":memory:") { + activeStorage = createMemoryStorage(); + } else { + activeStorage = createFileStorage(reportsDir); + } + return activeStorage; +} + +/** Start periodic cleanup of expired reports. */ +export function startReportCleanup() { + if (cleanupTimer) return; + cleanupTimer = setInterval(() => { + if (activeStorage) activeStorage.cleanupExpired().catch(() => {}); + }, CLEANUP_INTERVAL_MS); + cleanupTimer.unref(); +} + +/** Stop the periodic cleanup timer. */ +export function stopReportCleanup() { + if (cleanupTimer) { + clearInterval(cleanupTimer); + cleanupTimer = null; + } +} + +function createMemoryStorage() { + const store = new Map(); + + return { + async saveReport(report) { + const id = randomUUID(); + store.set(id, { report, createdAt: Date.now() }); + return id; + }, + + async getReport(id) { + if (!UUID_RE.test(id)) return null; + const entry = store.get(id); + if (!entry) return null; + if (Date.now() - entry.createdAt > TTL_MS) { + store.delete(id); + return null; + } + return entry.report; + }, + + async cleanupExpired() { + const now = Date.now(); + let cleaned = 0; + for (const [id, entry] of store) { + if (now - entry.createdAt > TTL_MS) { + store.delete(id); + cleaned++; + } + } + return cleaned; + } + }; +} + +function createFileStorage(reportsDir) { + // Eagerly create the directory so first writes never fail on an empty volume + const dirReady = mkdir(reportsDir, { recursive: true }); + + async function ensureDir() { + await dirReady; + } + + return { + async saveReport(report) { + await ensureDir(); + const id = randomUUID(); + const filePath = join(reportsDir, `${id}.json`); + const tmpPath = join(reportsDir, `${id}.tmp`); + const payload = JSON.stringify(report); + // Atomic write: temp file + rename + await writeFile(tmpPath, payload, "utf-8"); + await rename(tmpPath, filePath); + return id; + }, + + async getReport(id) { + if (!UUID_RE.test(id)) return null; + await ensureDir(); + const filePath = join(reportsDir, `${id}.json`); + try { + const info = await stat(filePath); + if (Date.now() - info.mtimeMs > TTL_MS) { + await rm(filePath, { force: true }); + return null; + } + const data = await readFile(filePath, "utf-8"); + return JSON.parse(data); + } catch (err) { + if (err.code === "ENOENT") return null; + throw err; + } + }, + + async cleanupExpired() { + await ensureDir(); + const entries = await readdir(reportsDir); + const now = Date.now(); + let cleaned = 0; + for (const entry of entries) { + if (!entry.endsWith(".json")) continue; + const filePath = join(reportsDir, entry); + try { + const info = await stat(filePath); + if (now - info.mtimeMs > TTL_MS) { + await rm(filePath, { force: true }); + cleaned++; + } + } catch { + // ignore + } + } + return cleaned; + } + }; +} diff --git a/webapp/backend/src/utils/cleanup.js b/webapp/backend/src/utils/cleanup.js new file mode 100644 index 0000000..d918686 --- /dev/null +++ b/webapp/backend/src/utils/cleanup.js @@ -0,0 +1,45 @@ +import { mkdtemp, rm, readdir, lstat } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const PREFIX = "agentrc-scan-"; + +/** Create a uniquely-named temp directory for a scan. */ +export async function createTempDir() { + return mkdtemp(join(tmpdir(), PREFIX)); +} + +/** Remove a temp directory, ignoring ENOENT. */ +export async function removeTempDir(dirPath) { + await rm(dirPath, { recursive: true, force: true }); +} + +/** + * Sweep stale temp directories older than maxAgeMs. + * Returns the number of directories cleaned. + */ +export async function sweepStaleTempDirs({ maxAgeMs = 10 * 60 * 1000, prefix = PREFIX } = {}) { + const base = tmpdir(); + let cleaned = 0; + let entries; + try { + entries = await readdir(base); + } catch { + return 0; + } + const now = Date.now(); + for (const entry of entries) { + if (!entry.startsWith(prefix)) continue; + const fullPath = join(base, entry); + try { + const info = await lstat(fullPath); + if (info.isDirectory() && !info.isSymbolicLink() && now - info.mtimeMs > maxAgeMs) { + await rm(fullPath, { recursive: true, force: true }); + cleaned++; + } + } catch { + // ignore individual cleanup failures + } + } + return cleaned; +} diff --git a/webapp/backend/src/utils/url-parser.js b/webapp/backend/src/utils/url-parser.js new file mode 100644 index 0000000..325cba2 --- /dev/null +++ b/webapp/backend/src/utils/url-parser.js @@ -0,0 +1,92 @@ +/** + * GitHub URL parser with SSRF protection. + * Only allows github.com URLs and owner/repo shorthand. + */ + +export class ValidationError extends Error { + constructor(message) { + super(message); + this.name = "ValidationError"; + } +} + +const OWNER_RE = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$/; +const REPO_RE = /^[a-zA-Z0-9._-]{1,100}$/; + +/** + * Parse a GitHub repo reference into { owner, repo, url }. + * Accepts: "owner/repo", "https://github.com/owner/repo", "https://github.com/owner/repo.git" + * Rejects non-GitHub URLs (SSRF protection). + */ +export function parseRepoUrl(input) { + if (!input || typeof input !== "string") { + throw new ValidationError("repo_url is required"); + } + + const trimmed = input.trim(); + + // Shorthand: owner/repo + if (!trimmed.includes("://")) { + const parts = trimmed.split("/").filter(Boolean); + if (parts.length !== 2) { + throw new ValidationError('Invalid repo reference. Expected "owner/repo" or a GitHub URL.'); + } + const [owner, repo] = parts; + validateOwnerRepo(owner, repo); + return { + owner, + repo, + url: `https://github.com/${owner}/${repo}.git` + }; + } + + // Full URL + let parsed; + try { + parsed = new URL(trimmed); + } catch { + throw new ValidationError("Invalid URL format."); + } + + // SSRF protection: only github.com + if (parsed.hostname !== "github.com") { + throw new ValidationError("Only github.com repositories are supported."); + } + + if (parsed.protocol !== "https:") { + throw new ValidationError("Only HTTPS URLs are supported."); + } + + // Reject query params and hash + if (parsed.search || parsed.hash) { + throw new ValidationError("URL must not contain query parameters or hash."); + } + + // Extract owner/repo from path + const pathParts = parsed.pathname + .replace(/\.git$/, "") + .split("/") + .filter(Boolean); + + if (pathParts.length !== 2) { + throw new ValidationError("URL must point to a repository: https://github.com/owner/repo"); + } + + const [owner, repo] = pathParts; + validateOwnerRepo(owner, repo); + + return { + owner, + repo, + url: `https://github.com/${owner}/${repo}.git` + }; +} + +function validateOwnerRepo(owner, repo) { + if (!OWNER_RE.test(owner)) { + throw new ValidationError(`Invalid GitHub owner: "${owner}"`); + } + if (!REPO_RE.test(repo)) { + throw new ValidationError(`Invalid GitHub repository name: "${repo}"`); + } +} diff --git a/webapp/backend/tests/cleanup.test.js b/webapp/backend/tests/cleanup.test.js new file mode 100644 index 0000000..a800c7a --- /dev/null +++ b/webapp/backend/tests/cleanup.test.js @@ -0,0 +1,30 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { createTempDir, removeTempDir } from "../src/utils/cleanup.js"; +import { existsSync } from "node:fs"; + +describe("cleanup", () => { + let tempDir; + + afterEach(async () => { + if (tempDir && existsSync(tempDir)) { + await removeTempDir(tempDir); + } + }); + + it("createTempDir creates a directory", async () => { + tempDir = await createTempDir(); + expect(tempDir).toContain("agentrc-scan-"); + expect(existsSync(tempDir)).toBe(true); + }); + + it("removeTempDir removes an existing directory", async () => { + tempDir = await createTempDir(); + await removeTempDir(tempDir); + expect(existsSync(tempDir)).toBe(false); + tempDir = null; // Already cleaned + }); + + it("removeTempDir ignores non-existent path", async () => { + await expect(removeTempDir("/tmp/agentrc-scan-nonexistent-12345")).resolves.toBeUndefined(); + }); +}); diff --git a/webapp/backend/tests/report-validator.test.js b/webapp/backend/tests/report-validator.test.js new file mode 100644 index 0000000..0a7d896 --- /dev/null +++ b/webapp/backend/tests/report-validator.test.js @@ -0,0 +1,270 @@ +import { describe, it, expect } from "vitest"; +import { + normalizeSharedReportResult, + ReportValidationError +} from "../src/services/report-validator.js"; + +const validReport = { + generatedAt: "2025-01-15T12:00:00.000Z", + isMonorepo: false, + apps: [], + pillars: [{ id: "documentation", name: "Documentation", passed: 2, total: 3, passRate: 0.67 }], + levels: [{ level: 1, name: "Functional", achieved: true, passed: 3, total: 3, passRate: 1 }], + achievedLevel: 2, + criteria: [ + { + id: "readme", + title: "Has README", + status: "pass", + level: 1, + pillar: "documentation", + impact: "high", + effort: "low", + reason: "Found README.md", + evidence: ["README.md exists"] + } + ], + extras: [], + repo_url: "https://github.com/microsoft/agentrc", + repo_name: "microsoft/agentrc" +}; + +describe("normalizeSharedReportResult", () => { + it("accepts a valid report", () => { + const result = normalizeSharedReportResult(validReport); + expect(result.achievedLevel).toBe(2); + expect(result.repo_url).toBe("https://github.com/microsoft/agentrc"); + }); + + it("strips repoPath for privacy", () => { + const withPath = { ...validReport, repoPath: "/tmp/secret-path" }; + const result = normalizeSharedReportResult(withPath); + expect(result.repoPath).toBeUndefined(); + }); + + it("strips engine field", () => { + const withEngine = { ...validReport, engine: { score: 95 } }; + const result = normalizeSharedReportResult(withEngine); + expect(result.engine).toBeUndefined(); + }); + + it("rejects null", () => { + expect(() => normalizeSharedReportResult(null)).toThrow(ReportValidationError); + }); + + it("rejects arrays", () => { + expect(() => normalizeSharedReportResult([])).toThrow(ReportValidationError); + }); + + it("rejects missing generatedAt", () => { + const { generatedAt: _, ...partial } = validReport; + expect(() => normalizeSharedReportResult(partial)).toThrow(/generatedAt/); + }); + + it("rejects invalid achievedLevel", () => { + expect(() => normalizeSharedReportResult({ ...validReport, achievedLevel: 6 })).toThrow( + /achievedLevel/ + ); + + expect(() => normalizeSharedReportResult({ ...validReport, achievedLevel: -1 })).toThrow( + /achievedLevel/ + ); + }); + + it("accepts achievedLevel 0 (no levels achieved)", () => { + const result = normalizeSharedReportResult({ ...validReport, achievedLevel: 0 }); + expect(result.achievedLevel).toBe(0); + }); + + it("rejects non-integer achievedLevel", () => { + expect(() => normalizeSharedReportResult({ ...validReport, achievedLevel: 2.5 })).toThrow( + /achievedLevel/ + ); + expect(() => normalizeSharedReportResult({ ...validReport, achievedLevel: "3" })).toThrow( + /achievedLevel/ + ); + }); + + it("rejects non-boolean isMonorepo", () => { + expect(() => normalizeSharedReportResult({ ...validReport, isMonorepo: "yes" })).toThrow( + /isMonorepo/ + ); + }); + + it("rejects prototype pollution attempts", () => { + const obj = Object.create(null); + Object.assign(obj, validReport); + // Explicitly set __proto__ as own property + Object.defineProperty(obj, "__proto__", { + value: { polluted: true }, + enumerable: true, + configurable: true + }); + expect(() => normalizeSharedReportResult(obj)).toThrow(/Invalid report structure/); + }); + + it("rejects unknown fields", () => { + expect(() => normalizeSharedReportResult({ ...validReport, malicious: true })).toThrow( + /Unknown fields/ + ); + }); + + it("accepts valid GitHub repo_url", () => { + const result = normalizeSharedReportResult({ + ...validReport, + repo_url: "https://github.com/microsoft/agentrc" + }); + expect(result.repo_url).toBe("https://github.com/microsoft/agentrc"); + }); + + it("omits non-GitHub repo_url", () => { + const result = normalizeSharedReportResult({ + ...validReport, + repo_url: "javascript:alert(1)" + }); + expect(result.repo_url).toBeUndefined(); + }); + + it("omits repo_url with non-HTTPS scheme", () => { + const result = normalizeSharedReportResult({ + ...validReport, + repo_url: "http://github.com/owner/repo" + }); + expect(result.repo_url).toBeUndefined(); + }); + + // --- areaReports validation --- + it("accepts valid areaReports", () => { + const result = normalizeSharedReportResult({ + ...validReport, + areaReports: [ + { + area: { name: "packages/core", path: "packages/core" }, + criteria: [{ id: "readme", status: "pass" }], + pillars: [{ name: "Documentation", passed: 1, failed: 0 }] + } + ] + }); + expect(result.areaReports).toHaveLength(1); + }); + + it("omits areaReports when not provided", () => { + const result = normalizeSharedReportResult(validReport); + expect(result.areaReports).toBeUndefined(); + }); + + it("rejects non-array areaReports", () => { + expect(() => + normalizeSharedReportResult({ ...validReport, areaReports: "bad" }) + ).toThrow(/areaReports must be an array/); + }); + + it("rejects areaReports with non-object entries", () => { + expect(() => + normalizeSharedReportResult({ ...validReport, areaReports: [null] }) + ).toThrow(/Each areaReport must be a non-null object/); + }); + + it("rejects areaReport missing area object", () => { + expect(() => + normalizeSharedReportResult({ + ...validReport, + areaReports: [{ area: "not-an-object", criteria: [], pillars: [] }] + }) + ).toThrow(/must have an area object/); + }); + + it("rejects areaReport with non-array criteria", () => { + expect(() => + normalizeSharedReportResult({ + ...validReport, + areaReports: [{ area: { name: "x" }, criteria: "bad", pillars: [] }] + }) + ).toThrow(/must have a criteria array/); + }); + + it("rejects areaReport with non-array pillars", () => { + expect(() => + normalizeSharedReportResult({ + ...validReport, + areaReports: [{ area: { name: "x" }, criteria: [], pillars: {} }] + }) + ).toThrow(/must have a pillars array/); + }); + + it("rejects areaReports exceeding 50 entries", () => { + const big = Array.from({ length: 51 }, (_, i) => ({ + area: { name: `area-${i}` }, + criteria: [], + pillars: [] + })); + expect(() => + normalizeSharedReportResult({ ...validReport, areaReports: big }) + ).toThrow(/at most 50/); + }); + + // --- policies validation --- + it("accepts valid policies", () => { + const result = normalizeSharedReportResult({ + ...validReport, + policies: { chain: ["builtin", "custom"], criteriaCount: 30 } + }); + expect(result.policies.chain).toEqual(["builtin", "custom"]); + expect(result.policies.criteriaCount).toBe(30); + }); + + it("omits policies when not provided", () => { + const result = normalizeSharedReportResult(validReport); + expect(result.policies).toBeUndefined(); + }); + + it("rejects non-object policies", () => { + expect(() => + normalizeSharedReportResult({ ...validReport, policies: "bad" }) + ).toThrow(/policies must be a non-null object/); + }); + + it("rejects policies with non-array chain", () => { + expect(() => + normalizeSharedReportResult({ + ...validReport, + policies: { chain: "builtin", criteriaCount: 10 } + }) + ).toThrow(/policies\.chain must be an array of strings/); + }); + + it("rejects policies with non-string chain entries", () => { + expect(() => + normalizeSharedReportResult({ + ...validReport, + policies: { chain: [42], criteriaCount: 10 } + }) + ).toThrow(/policies\.chain must be an array of strings/); + }); + + it("rejects policies with non-integer criteriaCount", () => { + expect(() => + normalizeSharedReportResult({ + ...validReport, + policies: { chain: ["builtin"], criteriaCount: 2.5 } + }) + ).toThrow(/policies\.criteriaCount must be a non-negative integer/); + }); + + it("rejects policies with negative criteriaCount", () => { + expect(() => + normalizeSharedReportResult({ + ...validReport, + policies: { chain: ["builtin"], criteriaCount: -1 } + }) + ).toThrow(/policies\.criteriaCount must be a non-negative integer/); + }); + + it("strips extra fields from policies", () => { + const result = normalizeSharedReportResult({ + ...validReport, + policies: { chain: ["builtin"], criteriaCount: 10, extra: "injected" } + }); + expect(result.policies).toEqual({ chain: ["builtin"], criteriaCount: 10 }); + }); +}); diff --git a/webapp/backend/tests/routes.test.js b/webapp/backend/tests/routes.test.js new file mode 100644 index 0000000..bfe302a --- /dev/null +++ b/webapp/backend/tests/routes.test.js @@ -0,0 +1,223 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { fileURLToPath } from "node:url"; +import { dirname, resolve } from "node:path"; + +// Mock @agentrc/core modules before importing routes +vi.mock("@agentrc/core/services/git", () => ({ + cloneRepo: vi.fn().mockResolvedValue(undefined) +})); + +vi.mock("@agentrc/core/services/readiness", () => ({ + runReadinessReport: vi.fn().mockResolvedValue({ + repoPath: "/tmp/fake", + generatedAt: "2025-01-15T12:00:00.000Z", + isMonorepo: false, + apps: [], + pillars: [ + { id: "documentation", name: "Documentation", passed: 2, total: 3, passRate: 0.67 }, + { id: "testing", name: "Testing", passed: 1, total: 2, passRate: 0.5 } + ], + levels: [ + { level: 1, name: "Functional", passed: 3, total: 4, passRate: 0.75, achieved: true }, + { level: 2, name: "Documented", passed: 2, total: 3, passRate: 0.67, achieved: true }, + { level: 3, name: "Standardized", passed: 0, total: 2, passRate: 0, achieved: false } + ], + achievedLevel: 2, + criteria: [], + extras: [] + }) +})); + +// We need to mock cleanup too since scanner uses it +vi.mock("../src/utils/cleanup.js", () => ({ + createTempDir: vi.fn().mockResolvedValue("/tmp/agentrc-scan-fake"), + removeTempDir: vi.fn().mockResolvedValue(undefined), + sweepStaleTempDirs: vi.fn().mockResolvedValue(0) +})); + +const { createApp, createRuntime } = await import("../src/server.js"); +const { createStorage } = await import("../src/services/storage.js"); + +/** Start Express on an ephemeral port and return base URL + close handle */ +function listen(app) { + return new Promise((resolve) => { + const server = app.listen(0, () => { + const { port } = server.address(); + resolve({ base: `http://127.0.0.1:${port}`, server }); + }); + }); +} + +describe("API routes", () => { + let app; + let runtime; + let base; + let server; + + beforeEach(async () => { + runtime = { + ...createRuntime(), + githubToken: "", + githubTokenProvided: false, + sharingEnabled: true, + reportsDir: ":memory:", + frontendPath: resolve(dirname(fileURLToPath(import.meta.url)), "../../../webapp/frontend"), + cloneTimeoutMs: 60000, + maxConcurrentScans: 5, + scanRateLimitWindowMs: 900000, + scanRateLimitMax: 100, + reportRateLimitWindowMs: 900000, + reportRateLimitMax: 100, + appInsightsConnectionString: "" + }; + runtime.storage = createStorage(":memory:"); + app = createApp(runtime); + ({ base, server } = await listen(app)); + }); + + afterEach(() => server?.close()); + + describe("GET /api/health", () => { + it("returns ok status", async () => { + const res = await fetch(`${base}/api/health`); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.status).toBe("ok"); + }); + }); + + describe("GET /api/config", () => { + it("returns config", async () => { + const res = await fetch(`${base}/api/config`); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body).toHaveProperty("sharingEnabled"); + expect(body).toHaveProperty("githubTokenProvided"); + }); + }); + + describe("POST /api/scan", () => { + it("returns validation error for missing repo_url", async () => { + const res = await fetch(`${base}/api/scan`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}) + }); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toContain("required"); + }); + + it("returns validation error for non-GitHub URL", async () => { + const res = await fetch(`${base}/api/scan`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ repo_url: "https://evil.com/hack/repo" }) + }); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toContain("github.com"); + }); + + it("scans a valid repo reference", async () => { + const res = await fetch(`${base}/api/scan`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ repo_url: "microsoft/agentrc" }) + }); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body).toHaveProperty("achievedLevel"); + expect(body).toHaveProperty("repo_url"); + expect(body).toHaveProperty("repo_name"); + expect(body.repoPath).toBeUndefined(); + }); + }); + + describe("POST /api/report", () => { + const validResult = { + generatedAt: "2025-01-15T12:00:00.000Z", + isMonorepo: false, + apps: [], + pillars: [ + { id: "documentation", name: "Documentation", passed: 2, total: 3, passRate: 0.67 }, + { id: "testing", name: "Testing", passed: 1, total: 2, passRate: 0.5 } + ], + levels: [ + { level: 1, name: "Functional", passed: 3, total: 4, passRate: 0.75, achieved: true }, + { level: 2, name: "Documented", passed: 2, total: 3, passRate: 0.67, achieved: true }, + { level: 3, name: "Standardized", passed: 0, total: 2, passRate: 0, achieved: false } + ], + achievedLevel: 2, + criteria: [], + extras: [] + }; + + it("saves and retrieves a report", async () => { + const postRes = await fetch(`${base}/api/report`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ result: validResult }) + }); + expect(postRes.status).toBe(201); + const postBody = await postRes.json(); + expect(postBody).toHaveProperty("id"); + expect(postBody).toHaveProperty("url"); + + const getRes = await fetch(`${base}/api/report/${postBody.id}`); + expect(getRes.status).toBe(200); + const getBody = await getRes.json(); + expect(getBody.achievedLevel).toBe(2); + }); + + it("returns 503 when sharing is disabled", async () => { + server?.close(); + runtime.sharingEnabled = false; + app = createApp(runtime); + ({ base, server } = await listen(app)); + + const res = await fetch(`${base}/api/report`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ result: validResult }) + }); + expect(res.status).toBe(503); + }); + + it("returns 400 for invalid report", async () => { + const res = await fetch(`${base}/api/report`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ result: { bad: "data" } }) + }); + expect(res.status).toBe(400); + }); + + it("returns 404 for unknown report ID", async () => { + const res = await fetch(`${base}/api/report/00000000-0000-0000-0000-000000000000`); + expect(res.status).toBe(404); + }); + + it("returns 400 for invalid report ID format", async () => { + const res = await fetch(`${base}/api/report/not-a-uuid`); + expect(res.status).toBe(400); + }); + + it("saves and retrieves a report with achievedLevel 0", async () => { + const levelZeroResult = { ...validResult, achievedLevel: 0 }; + const postRes = await fetch(`${base}/api/report`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ result: levelZeroResult }) + }); + expect(postRes.status).toBe(201); + const postBody = await postRes.json(); + expect(postBody).toHaveProperty("id"); + + const getRes = await fetch(`${base}/api/report/${postBody.id}`); + expect(getRes.status).toBe(200); + const getBody = await getRes.json(); + expect(getBody.achievedLevel).toBe(0); + }); + }); +}); diff --git a/webapp/backend/tests/storage.test.js b/webapp/backend/tests/storage.test.js new file mode 100644 index 0000000..b8b3915 --- /dev/null +++ b/webapp/backend/tests/storage.test.js @@ -0,0 +1,80 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; +import { createStorage, startReportCleanup, stopReportCleanup } from "../src/services/storage.js"; + +describe("storage (memory mode)", () => { + afterEach(() => { + stopReportCleanup(); + }); + + it("saves and retrieves a report", async () => { + const storage = createStorage(":memory:"); + const report = { achievedLevel: 3, generatedAt: "2025-01-01T00:00:00Z" }; + const id = await storage.saveReport(report); + expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i); + const retrieved = await storage.getReport(id); + expect(retrieved).toEqual(report); + }); + + it("returns null for unknown ID", async () => { + const storage = createStorage(":memory:"); + const result = await storage.getReport("00000000-0000-0000-0000-000000000000"); + expect(result).toBeNull(); + }); + + it("returns null for invalid UUID format", async () => { + const storage = createStorage(":memory:"); + const result = await storage.getReport("not-a-uuid"); + expect(result).toBeNull(); + }); + + it("cleanupExpired removes nothing when none expired", async () => { + const storage = createStorage(":memory:"); + await storage.saveReport({ test: true }); + const cleaned = await storage.cleanupExpired(); + expect(cleaned).toBe(0); + }); + + it("startReportCleanup and stopReportCleanup run without errors", () => { + createStorage(":memory:"); + startReportCleanup(); + // calling twice is a no-op + startReportCleanup(); + stopReportCleanup(); + // stopping when already stopped is safe + stopReportCleanup(); + }); + + it("cleanup scheduler invokes cleanupExpired", async () => { + const storage = createStorage(":memory:"); + const spy = vi.spyOn(storage, "cleanupExpired"); + + // Advance past the 60-minute interval + vi.useFakeTimers(); + startReportCleanup(); + vi.advanceTimersByTime(60 * 60 * 1000); + expect(spy.mock.calls.length).toBeGreaterThanOrEqual(1); + + stopReportCleanup(); + vi.useRealTimers(); + }); +}); + +describe("storage (file mode)", () => { + it("auto-creates REPORTS_DIR on saveReport", async () => { + const { mkdtemp, rm } = await import("node:fs/promises"); + const { join } = await import("node:path"); + const { tmpdir } = await import("node:os"); + + const base = await mkdtemp(join(tmpdir(), "agentrc-test-")); + const deepDir = join(base, "nested", "reports"); + + try { + const storage = createStorage(deepDir); + const id = await storage.saveReport({ test: true }); + const result = await storage.getReport(id); + expect(result).toEqual({ test: true }); + } finally { + await rm(base, { recursive: true, force: true }); + } + }); +}); diff --git a/webapp/backend/tests/url-parser.test.js b/webapp/backend/tests/url-parser.test.js new file mode 100644 index 0000000..98e5187 --- /dev/null +++ b/webapp/backend/tests/url-parser.test.js @@ -0,0 +1,91 @@ +import { describe, it, expect } from "vitest"; +import { parseRepoUrl, ValidationError } from "../src/utils/url-parser.js"; + +describe("parseRepoUrl", () => { + it("parses owner/repo shorthand", () => { + const result = parseRepoUrl("microsoft/agentrc"); + expect(result).toEqual({ + owner: "microsoft", + repo: "agentrc", + url: "https://github.com/microsoft/agentrc.git" + }); + }); + + it("parses full GitHub HTTPS URL", () => { + const result = parseRepoUrl("https://github.com/microsoft/agentrc"); + expect(result).toEqual({ + owner: "microsoft", + repo: "agentrc", + url: "https://github.com/microsoft/agentrc.git" + }); + }); + + it("parses GitHub URL with .git suffix", () => { + const result = parseRepoUrl("https://github.com/microsoft/agentrc.git"); + expect(result).toEqual({ + owner: "microsoft", + repo: "agentrc", + url: "https://github.com/microsoft/agentrc.git" + }); + }); + + it("trims whitespace", () => { + const result = parseRepoUrl(" microsoft/agentrc "); + expect(result.owner).toBe("microsoft"); + }); + + it("rejects empty input", () => { + expect(() => parseRepoUrl("")).toThrow(ValidationError); + expect(() => parseRepoUrl(null)).toThrow(ValidationError); + expect(() => parseRepoUrl(undefined)).toThrow(ValidationError); + }); + + it("rejects non-GitHub URLs (SSRF protection)", () => { + expect(() => parseRepoUrl("https://evil.com/microsoft/agentrc")).toThrow(/Only github\.com/); + }); + + it("rejects non-HTTPS URLs", () => { + expect(() => parseRepoUrl("http://github.com/microsoft/agentrc")).toThrow(/Only HTTPS/); + }); + + it("rejects URLs with query params", () => { + expect(() => parseRepoUrl("https://github.com/microsoft/agentrc?tab=readme")).toThrow( + /query parameters/ + ); + }); + + it("rejects URLs with hash", () => { + expect(() => parseRepoUrl("https://github.com/microsoft/agentrc#readme")).toThrow( + /query parameters or hash/ + ); + }); + + it("rejects URLs with extra path segments", () => { + expect(() => parseRepoUrl("https://github.com/microsoft/agentrc/tree/main")).toThrow( + /must point to a repository/ + ); + }); + + it("rejects invalid owner format", () => { + expect(() => parseRepoUrl("-invalid/repo")).toThrow(/Invalid GitHub owner/); + }); + + it("rejects invalid repo format", () => { + expect(() => parseRepoUrl("owner/")).toThrow(ValidationError); + }); + + it("rejects three-part shorthand", () => { + expect(() => parseRepoUrl("a/b/c")).toThrow(ValidationError); + }); + + it("accepts owner with hyphens", () => { + const result = parseRepoUrl("my-org/my-repo"); + expect(result.owner).toBe("my-org"); + expect(result.repo).toBe("my-repo"); + }); + + it("accepts repo with dots and underscores", () => { + const result = parseRepoUrl("owner/my.repo_v2"); + expect(result.repo).toBe("my.repo_v2"); + }); +}); diff --git a/webapp/backend/vitest.config.js b/webapp/backend/vitest.config.js new file mode 100644 index 0000000..9dfc738 --- /dev/null +++ b/webapp/backend/vitest.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + testTimeout: 10_000 + } +}); diff --git a/webapp/docker-compose.yml b/webapp/docker-compose.yml new file mode 100644 index 0000000..7297f0d --- /dev/null +++ b/webapp/docker-compose.yml @@ -0,0 +1,15 @@ +services: + app: + build: + context: .. + dockerfile: Dockerfile.webapp + ports: + - "${HOST_PORT:-3000}:3000" + env_file: + - .env + volumes: + - reports-data:/app/data +volumes: + # Persists shared reports across container restarts. + # Set REPORTS_DIR=/app/data/reports in .env to enable (default is :memory:). + reports-data: diff --git a/webapp/frontend/index.html b/webapp/frontend/index.html new file mode 100644 index 0000000..172f871 --- /dev/null +++ b/webapp/frontend/index.html @@ -0,0 +1,95 @@ + + + + + + AgentRC — AI Readiness Scanner + + + + + + + + + + +
+
+ + +
+
+ +
+
+

AI Readiness Scanner

+

Scan any public GitHub repository to see how ready it is for AI-assisted development.

+
+ +
+
+ + +
+
+ +
+

What the readiness report evaluates

+ +

+ Checks are scored across 5 maturity levels — from Functional to Autonomous — with actionable recommendations for each gap. +

+

+ This web scanner runs a subset of the full AgentRC analysis. For the complete experience — including init, generate, policy enforcement, and batch org-wide scans — use the CLI or the VS Code extension. +

+
+ + + + + +
+
+ + + + + + + + diff --git a/webapp/frontend/package-lock.json b/webapp/frontend/package-lock.json new file mode 100644 index 0000000..875f0f8 --- /dev/null +++ b/webapp/frontend/package-lock.json @@ -0,0 +1,1585 @@ +{ + "name": "@agentrc/webapp-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@agentrc/webapp-frontend", + "version": "1.0.0", + "devDependencies": { + "vitest": "^3.1.1" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "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/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "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/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "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": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/webapp/frontend/package.json b/webapp/frontend/package.json new file mode 100644 index 0000000..7de7161 --- /dev/null +++ b/webapp/frontend/package.json @@ -0,0 +1,12 @@ +{ + "name": "@agentrc/webapp-frontend", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "test": "vitest run" + }, + "devDependencies": { + "vitest": "^3.1.1" + } +} diff --git a/webapp/frontend/src/api.js b/webapp/frontend/src/api.js new file mode 100644 index 0000000..13e11a3 --- /dev/null +++ b/webapp/frontend/src/api.js @@ -0,0 +1,51 @@ +/** + * HTTP client for the AgentRC webapp API. + */ + +/** Fetch public config from the backend. */ +export async function fetchConfig(signal) { + const res = await fetch("/api/config", { signal }); + if (!res.ok) throw new Error("Failed to fetch config"); + return res.json(); +} + +/** Scan a repository. */ +export async function scanRepo(repoUrl, signal) { + const res = await fetch("/api/scan", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ repo_url: repoUrl }), + signal + }); + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error(body.error || `Scan failed (${res.status})`); + } + return res.json(); +} + +/** Share a report for public access. */ +export async function shareReport(result, signal) { + const res = await fetch("/api/report", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ result }), + signal + }); + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error(body.error || `Share failed (${res.status})`); + } + return res.json(); +} + +/** Fetch a shared report by ID. */ +export async function fetchSharedReport(id, signal) { + const res = await fetch(`/api/report/${encodeURIComponent(id)}`, { signal }); + if (!res.ok) { + if (res.status === 404) return null; + const body = await res.json().catch(() => ({})); + throw new Error(body.error || `Fetch failed (${res.status})`); + } + return res.json(); +} diff --git a/webapp/frontend/src/app.js b/webapp/frontend/src/app.js new file mode 100644 index 0000000..30ff758 --- /dev/null +++ b/webapp/frontend/src/app.js @@ -0,0 +1,211 @@ +/** + * Main application logic — orchestrates scanning, rendering, and routing. + */ +import { fetchConfig, scanRepo, fetchSharedReport } from "./api.js"; +import { renderReport } from "./report.js"; +import { + parseGitHubReference, + getRepoFromPath, + getSharedReportId, + syncRepoPathInBrowser +} from "./repo-location.js"; + +let appConfig = { sharingEnabled: false, githubTokenProvided: false }; +let currentAbortController = null; + +document.addEventListener("DOMContentLoaded", async () => { + initThemeToggle(); + + // Fetch runtime config + try { + appConfig = await fetchConfig(); + } catch { + // Continue with defaults + } + + setupForm(); + + // Check for shared report URL + const reportId = getSharedReportId(window.location.pathname); + if (reportId) { + await loadSharedReport(reportId); + return; + } + + // Check for auto-scan from URL path or query + const repoFromPath = getRepoFromPath(window.location.pathname); + const repoFromQuery = new URLSearchParams(window.location.search).get("repo"); + const autoRepo = repoFromPath || parseGitHubReference(repoFromQuery); + + if (autoRepo) { + document.getElementById("repo-input").value = autoRepo; + await executeScan(autoRepo); + } +}); + +// Handle browser back/forward +window.addEventListener("popstate", async (e) => { + if (e.state?.repo) { + document.getElementById("repo-input").value = e.state.repo; + await executeScan(e.state.repo); + } +}); + +function setupForm() { + const form = document.getElementById("scan-form"); + form.addEventListener("submit", async (e) => { + e.preventDefault(); + const input = document.getElementById("repo-input").value.trim(); + const ref = parseGitHubReference(input); + if (!ref) { + showError('Invalid repository. Enter "owner/repo" or a GitHub URL.'); + return; + } + await executeScan(ref); + }); + + const dismissBtn = document.getElementById("error-dismiss"); + if (dismissBtn) { + dismissBtn.addEventListener("click", hideError); + } +} + +async function executeScan(repoRef) { + // Abort any in-progress scan + if (currentAbortController) { + currentAbortController.abort(); + } + const controller = new AbortController(); + currentAbortController = controller; + + hideError(); + clearReport(); + hideDescription(); + showProgress("Cloning repository…"); + setFormBusy(true); + + try { + showProgress("Analyzing repository…"); + + const report = await scanRepo(repoRef, controller.signal); + + syncRepoPathInBrowser(repoRef); + showProgress("Rendering report…"); + renderReport(report, { sharingEnabled: appConfig.sharingEnabled }); + + hideProgress(); + } catch (err) { + if (err.name === "AbortError") return; + hideProgress(); + showError(err.message || "Scan failed. Please try again."); + } finally { + // Only clear state if this is still the active scan — avoids + // a stale aborted scan from resetting a newer in-progress scan. + if (currentAbortController === controller) { + setFormBusy(false); + currentAbortController = null; + } + } +} + +async function loadSharedReport(id) { + hideDescription(); + showProgress("Loading shared report…"); + setFormBusy(true); + + try { + const report = await fetchSharedReport(id); + if (!report) { + hideProgress(); + showError("Shared report not found or has expired."); + return; + } + hideProgress(); + renderReport(report, { sharingEnabled: false, shared: true }); + + if (report.repo_name) { + document.getElementById("repo-input").value = report.repo_name; + } + } catch (err) { + hideProgress(); + showError(err.message || "Failed to load shared report."); + } finally { + setFormBusy(false); + } +} + +// ===== UI Helpers ===== + +function showProgress(text) { + const area = document.getElementById("progress"); + const textEl = document.getElementById("progress-text"); + area.hidden = false; + textEl.textContent = text; +} + +function hideProgress() { + document.getElementById("progress").hidden = true; +} + +function showError(message) { + const banner = document.getElementById("error-banner"); + const msg = document.getElementById("error-message"); + banner.hidden = false; + msg.textContent = message; +} + +function hideError() { + document.getElementById("error-banner").hidden = true; +} + +function clearReport() { + document.getElementById("report").innerHTML = ""; +} + +function hideDescription() { + const desc = document.getElementById("scan-description"); + if (desc) desc.classList.add("hidden"); + document.querySelector(".container").classList.add("scanning"); +} + +function setFormBusy(busy) { + const btn = document.getElementById("scan-btn"); + const input = document.getElementById("repo-input"); + const label = btn.querySelector(".btn-label"); + const spinner = btn.querySelector(".btn-spinner"); + + btn.disabled = busy; + input.disabled = busy; + label.textContent = busy ? "Scanning…" : "Scan"; + spinner.hidden = !busy; +} + +// ===== Theme Toggle ===== + +function initThemeToggle() { + const btn = document.getElementById("theme-toggle"); + if (!btn) return; + + updateThemeIcon(btn); + + btn.addEventListener("click", () => { + const current = document.documentElement.getAttribute("data-theme"); + const next = current === "dark" ? "light" : "dark"; + document.documentElement.setAttribute("data-theme", next); + try { + localStorage.setItem("theme", next); + } catch { + // Storage may be unavailable (Safari private mode, quota exceeded, etc.) + } + const metaThemeColor = document.querySelector('meta[name="theme-color"]'); + if (metaThemeColor) { + metaThemeColor.content = next === "dark" ? "#0d1117" : "#ffffff"; + } + updateThemeIcon(btn); + }); +} + +function updateThemeIcon(btn) { + const theme = document.documentElement.getAttribute("data-theme"); + btn.textContent = theme === "dark" ? "☀️" : "🌙"; +} diff --git a/webapp/frontend/src/main.css b/webapp/frontend/src/main.css new file mode 100644 index 0000000..84ab10e --- /dev/null +++ b/webapp/frontend/src/main.css @@ -0,0 +1,815 @@ +/* ===== CSS Custom Properties (GitHub-flavored, matches CLI --html) ===== */ +:root, [data-theme="dark"] { + --color-canvas-default: #0d1117; + --color-canvas-subtle: #161b22; + --color-canvas-inset: #010409; + --color-border-default: #30363d; + --color-border-muted: #21262d; + --color-fg-default: #e6edf3; + --color-fg-muted: #8b949e; + --color-fg-subtle: #6e7681; + --color-accent-fg: #58a6ff; + --color-accent-emphasis: #1f6feb; + --color-success-fg: #3fb950; + --color-success-emphasis: #238636; + --color-danger-fg: #f85149; + --color-danger-emphasis: #da3633; + --color-attention-fg: #d29922; + --color-done-fg: #a371f7; +} + +[data-theme="light"] { + --color-canvas-default: #ffffff; + --color-canvas-subtle: #f6f8fa; + --color-canvas-inset: #eff2f5; + --color-border-default: #d0d7de; + --color-border-muted: #d8dee4; + --color-fg-default: #1f2328; + --color-fg-muted: #656d76; + --color-fg-subtle: #6e7781; + --color-accent-fg: #0969da; + --color-accent-emphasis: #0550ae; + --color-success-fg: #1a7f37; + --color-success-emphasis: #116329; + --color-danger-fg: #cf222e; + --color-danger-emphasis: #a40e26; + --color-attention-fg: #9a6700; + --color-done-fg: #8250df; +} + +/* ===== Reset & Base ===== */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; + background: var(--color-canvas-default); + color: var(--color-fg-default); + line-height: 1.5; + font-size: 14px; + min-height: 100vh; + display: flex; + flex-direction: column; + -webkit-font-smoothing: antialiased; +} + +a { color: var(--color-accent-fg); text-decoration: none; } +a:hover { text-decoration: underline; } + +/* ===== Topbar ===== */ +.topbar { + background: var(--color-canvas-subtle); + border-bottom: 1px solid var(--color-border-default); + position: sticky; + top: 0; + z-index: 100; +} + +.topbar-inner { + max-width: 1280px; + margin: 0 auto; + padding: 12px 24px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.logo { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + font-size: 16px; + color: var(--color-fg-default); +} +.logo:hover { text-decoration: none; } +.logo-icon { font-size: 16px; } + +.topbar-nav { + display: flex; + align-items: center; + gap: 12px; + font-size: 13px; +} + +.btn-icon { + background: var(--color-canvas-default); + border: 1px solid var(--color-border-default); + border-radius: 6px; + cursor: pointer; + font-size: 14px; + padding: 4px 12px; + color: var(--color-fg-muted); + display: flex; + align-items: center; + gap: 6px; + transition: border-color 0.15s; +} +.btn-icon:hover { border-color: var(--color-accent-fg); color: var(--color-fg-default); } + +/* ===== Container ===== */ +.container { + max-width: 1280px; + margin: 0 auto; + padding: 24px; + flex: 1; + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + transition: justify-content 0.3s ease; +} +.container.scanning { + justify-content: flex-start; +} + +/* ===== Hero (scan form area) ===== */ +.hero-intro { text-align: left; margin-bottom: 16px; } +.hero-intro h1 { + font-size: 20px; + font-weight: 600; + margin-bottom: 4px; + color: var(--color-fg-default); +} +.hero-sub { + color: var(--color-fg-muted); + font-size: 13px; +} + +/* ===== Scan Form ===== */ +.scan-form { margin-bottom: 16px; } + +.input-group { + display: flex; + gap: 0; + max-width: 100%; +} + +.input-group input { + flex: 1; + padding: 10px 14px; + border: 1px solid var(--color-border-default); + border-right: none; + border-radius: 6px 0 0 6px; + font-size: 16px; + background: var(--color-canvas-default); + color: var(--color-fg-default); + font-family: inherit; +} +.input-group input::placeholder { color: var(--color-fg-subtle); } +.input-group input:focus { + outline: none; + border-color: var(--color-accent-fg); + box-shadow: 0 0 0 3px rgba(31, 111, 235, 0.3); +} + +.btn { + padding: 10px 20px; + border: 1px solid var(--color-border-default); + border-radius: 6px; + font-size: 16px; + font-weight: 500; + font-family: inherit; + cursor: pointer; + display: flex; + align-items: center; + gap: 6px; + transition: background-color 0.15s, border-color 0.15s; +} +.btn:disabled { opacity: 0.5; cursor: not-allowed; } + +.btn-primary { + background: var(--color-success-emphasis); + color: #fff; + border-color: var(--color-success-emphasis); + border-radius: 0 6px 6px 0; +} +.btn-primary:hover:not(:disabled) { + background: var(--color-success-fg); + border-color: var(--color-success-fg); +} + +.btn-secondary { + background: transparent; + color: var(--color-accent-fg); + border: 1px solid var(--color-border-default); +} +.btn-secondary:hover:not(:disabled) { + background: var(--color-canvas-subtle); + border-color: var(--color-accent-fg); +} + +.btn-spinner { + display: inline-block; + width: 14px; + height: 14px; + border: 2px solid rgba(255,255,255,0.3); + border-top-color: #fff; + border-radius: 50%; + animation: spin 0.6s linear infinite; +} +.btn-spinner[hidden] { + display: none; +} +@keyframes spin { to { transform: rotate(360deg); } } + +/* ===== Scan Description ===== */ +.scan-description { + margin-top: 24px; + margin-bottom: 24px; + max-width: 720px; + transition: opacity 0.25s ease, max-height 0.3s ease; + overflow: hidden; +} +.scan-description.hidden { + opacity: 0; + max-height: 0; + margin: 0; + pointer-events: none; +} +.scan-description h2 { + font-size: 14px; + font-weight: 600; + color: var(--color-fg-muted); + margin-bottom: 10px; + text-transform: uppercase; + letter-spacing: 0.5px; +} +.scan-description ul { + list-style: none; + padding: 0; +} +.scan-description li { + position: relative; + padding-left: 18px; + margin-bottom: 6px; + font-size: 13px; + color: var(--color-fg-muted); + line-height: 1.5; +} +.scan-description li::before { + content: "•"; + position: absolute; + left: 4px; + color: var(--color-accent-fg); +} +.scan-description li strong { + color: var(--color-fg-default); +} +.scan-description code { + font-size: 12px; + background: var(--color-canvas-subtle); + padding: 1px 5px; + border-radius: 4px; + border: 1px solid var(--color-border-muted); +} +.scan-description-note { + font-size: 13px; + color: var(--color-fg-muted); + margin-top: 12px; +} +.scan-description-surfaces { + font-size: 12px; + color: var(--color-fg-subtle); + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid var(--color-border-muted); +} + +/* ===== Progress (scan spinner) ===== */ +.progress-area { + margin-bottom: 16px; + display: flex; + align-items: center; + gap: 8px; +} + +.spinner { + display: inline-block; + width: 16px; + height: 16px; + border: 2px solid var(--color-border-muted); + border-top-color: var(--color-accent-fg); + border-radius: 50%; + animation: spin 0.8s linear infinite; + flex-shrink: 0; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.progress-text { + font-size: 12px; + color: var(--color-fg-muted); + margin: 0; +} + +/* ===== Progress bars (report pillar bars) ===== */ +.progress-bar { + height: 8px; + background: var(--color-border-muted); + border-radius: 4px; + overflow: hidden; + margin-bottom: 6px; +} + +.progress-fill { + height: 100%; + background: var(--color-accent-fg); + border-radius: 4px; + width: 0%; + transition: width 0.4s ease; +} + +/* ===== Error Banner ===== */ +.error-banner { + background: rgba(248, 81, 73, 0.1); + border: 1px solid var(--color-danger-fg); + border-radius: 6px; + padding: 12px 16px; + margin-bottom: 16px; + display: flex; + align-items: center; + justify-content: space-between; + color: var(--color-danger-fg); + font-size: 13px; +} + +.error-banner[hidden] { + display: none; +} + +/* ===== Toast ===== */ +.toast { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%) translateY(100%); + background: var(--color-canvas-subtle); + color: var(--color-fg-default); + padding: 8px 16px; + border-radius: 6px; + border: 1px solid var(--color-border-default); + font-size: 13px; + font-family: inherit; + z-index: 200; + opacity: 0; + transition: opacity 0.15s, transform 0.15s; +} +.toast.show { + opacity: 1; + transform: translateX(-50%) translateY(0); +} + +/* ===== Report Container ===== */ +.report-container { margin-bottom: 16px; } + +/* ===== Section (generic card) ===== */ +.section { + background: var(--color-canvas-subtle); + border: 1px solid var(--color-border-default); + padding: 24px; + border-radius: 6px; + margin-bottom: 16px; +} +.section-title { + font-size: 16px; + font-weight: 600; + color: var(--color-fg-default); + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 1px solid var(--color-border-muted); +} +.section-title-warn { color: var(--color-attention-fg); } +.section-title-success { color: var(--color-success-fg); } + +/* ===== Hero (report level circle) ===== */ +.hero { + background: var(--color-canvas-subtle); + border: 1px solid var(--color-border-default); + padding: 24px; + border-radius: 6px; + margin-bottom: 16px; + display: flex; + align-items: center; + gap: 20px; +} + +.hero-level { + width: 80px; + height: 80px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 28px; + font-weight: 700; + flex-shrink: 0; +} +.hero-level.level-high { background: rgba(63,185,80,0.12); border: 2px solid var(--color-success-fg); color: var(--color-success-fg); } +.hero-level.level-mid { background: rgba(210,153,34,0.12); border: 2px solid var(--color-attention-fg); color: var(--color-attention-fg); } +.hero-level.level-low { background: rgba(88,166,255,0.12); border: 2px solid var(--color-accent-fg); color: var(--color-accent-fg); } + +.hero-info { flex: 1; } +.hero-name { font-size: 20px; font-weight: 600; color: var(--color-fg-default); margin-bottom: 2px; } +.hero-name a { color: var(--color-accent-fg); } +.hero-subtitle { color: var(--color-fg-muted); font-size: 14px; } +.hero-meta { color: var(--color-fg-subtle); font-size: 12px; margin-top: 2px; } +.hero-next { margin-top: 8px; font-size: 13px; color: var(--color-fg-subtle); } +.hero-next strong { color: var(--color-fg-muted); } +.hero-next-done { color: var(--color-success-fg); } + +/* ===== Snapshot Banner ===== */ +.snapshot-banner { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px 16px; + margin-bottom: 12px; + border-radius: 6px; + background: rgba(210,153,34,0.08); + border: 1px solid var(--color-attention-fg); + color: var(--color-fg-default); + font-size: 13px; + line-height: 1.5; +} +.snapshot-icon { flex-shrink: 0; margin-top: 1px; } +.snapshot-text strong { color: var(--color-fg-default); } + +/* ===== Fix First ===== */ +.fix-first .section-title { color: var(--color-attention-fg); } +.fix-list { display: grid; gap: 8px; } +.fix-item { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 12px 14px; + border-radius: 6px; + background: var(--color-canvas-default); + border: 1px solid var(--color-border-muted); +} +.fix-icon { + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + flex-shrink: 0; + font-weight: 700; + background: rgba(248,81,73,0.12); + color: var(--color-danger-fg); +} +.fix-text { flex: 1; min-width: 0; } +.fix-title { font-weight: 600; font-size: 13px; color: var(--color-fg-default); } +.fix-reason { font-size: 12px; color: var(--color-fg-muted); margin-top: 2px; } +.fix-badges { display: flex; gap: 6px; margin-top: 4px; } +.fix-badge { + font-size: 11px; + padding: 1px 8px; + border-radius: 2em; + border: 1px solid transparent; +} +.fix-badge.impact-high { color: var(--color-danger-fg); background: rgba(248,81,73,0.08); border-color: rgba(248,81,73,0.2); } +.fix-badge.impact-medium { color: var(--color-attention-fg); background: rgba(210,153,34,0.08); border-color: rgba(210,153,34,0.2); } +.fix-badge.impact-low { color: var(--color-fg-muted); background: rgba(139,148,158,0.08); border-color: rgba(139,148,158,0.15); } +.fix-badge.effort-low { color: var(--color-success-fg); background: rgba(63,185,80,0.08); border-color: rgba(63,185,80,0.2); } +.fix-badge.effort-medium { color: var(--color-attention-fg); background: rgba(210,153,34,0.08); border-color: rgba(210,153,34,0.2); } +.fix-badge.effort-high { color: var(--color-fg-muted); background: rgba(139,148,158,0.08); border-color: rgba(139,148,158,0.15); } + +/* ===== AI Tooling Hero ===== */ +.ai-hero { position: relative; overflow: hidden; } +.ai-hero::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; + height: 3px; + background: linear-gradient(90deg, var(--color-accent-fg), var(--color-done-fg), var(--color-success-fg)); +} +.ai-hero-subtitle { color: var(--color-fg-muted); font-size: 13px; margin-bottom: 16px; } +.ai-score-header { display: flex; align-items: center; gap: 16px; margin-bottom: 16px; } +.ai-score-ring { + width: 72px; height: 72px; + border-radius: 50%; + display: flex; align-items: center; justify-content: center; + font-size: 20px; font-weight: 700; flex-shrink: 0; +} +.ai-score-ring.score-high { background: rgba(63,185,80,0.1); border: 2px solid var(--color-success-fg); color: var(--color-success-fg); } +.ai-score-ring.score-medium { background: rgba(210,153,34,0.1); border: 2px solid var(--color-attention-fg); color: var(--color-attention-fg); } +.ai-score-ring.score-low { background: rgba(248,81,73,0.1); border: 2px solid var(--color-danger-fg); color: var(--color-danger-fg); } +.ai-score-detail { flex: 1; } +.ai-score-label { font-size: 16px; font-weight: 600; color: var(--color-fg-default); margin-bottom: 2px; } +.ai-score-desc { color: var(--color-fg-muted); font-size: 13px; } +.ai-criteria-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 8px; } +.ai-criterion { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + border-radius: 6px; + background: var(--color-canvas-default); + border: 1px solid var(--color-border-muted); + transition: border-color 0.15s; +} +.ai-criterion:hover { border-color: var(--color-border-default); } +.ai-criterion-icon { + width: 28px; height: 28px; + border-radius: 50%; + display: flex; align-items: center; justify-content: center; + font-size: 12px; flex-shrink: 0; font-weight: 700; +} +.ai-criterion-icon.pass { background: rgba(63,185,80,0.12); color: var(--color-success-fg); } +.ai-criterion-icon.fail { background: rgba(248,81,73,0.12); color: var(--color-danger-fg); } +.ai-criterion-text { flex: 1; min-width: 0; } +.ai-criterion-title { font-weight: 600; font-size: 13px; color: var(--color-fg-default); } +.ai-criterion-reason { font-size: 12px; color: var(--color-fg-muted); margin-top: 1px; } + +/* ===== Pillar Performance ===== */ +.group-label { + font-size: 13px; + font-weight: 600; + color: var(--color-fg-muted); + margin-bottom: 8px; + margin-top: 12px; +} + +.pillar-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 12px; +} + +.pillar-card { + padding: 12px 16px; + border-radius: 6px; + background: var(--color-canvas-default); + border: 1px solid var(--color-border-muted); +} +.pillar-card.all-passing { opacity: 0.7; } +.pillar-card.has-failures { border-color: var(--color-attention-fg); } +.pillar-card.all-passing .pillar-name::before { content: '✓ '; color: var(--color-success-fg); } +.pillar-name { font-size: 13px; font-weight: 600; color: var(--color-fg-default); margin-bottom: 8px; } +.pillar-stats { display: flex; align-items: center; gap: 12px; } +.pillar-stats span { font-size: 12px; color: var(--color-fg-muted); white-space: nowrap; } + +.progress-bar { flex: 1; height: 8px; background: var(--color-border-muted); border-radius: 4px; overflow: hidden; } +.progress-fill { height: 100%; border-radius: 4px; transition: width 0.3s ease; } +.progress-fill.low { background: var(--color-danger-fg); } +.progress-fill.medium { background: var(--color-attention-fg); } +.progress-fill.high { background: var(--color-success-fg); } + +/* ===== Maturity Model ===== */ +.maturity-progress { + display: flex; + gap: 4px; + margin-bottom: 16px; + height: 8px; +} +.maturity-segment { + flex: 1; + border-radius: 4px; + background: var(--color-border-muted); +} +.maturity-segment.achieved { background: var(--color-accent-fg); } +.maturity-segment.current { background: var(--color-accent-fg); box-shadow: 0 0 0 2px var(--color-accent-emphasis); } + +.maturity-labels { + display: flex; + gap: 4px; + margin-bottom: 16px; +} +.maturity-label { + flex: 1; + text-align: center; + font-size: 11px; + color: var(--color-fg-subtle); +} +.maturity-label.current { color: var(--color-accent-fg); font-weight: 600; } + +.maturity-item { + padding: 10px 14px; + border-radius: 6px; + background: var(--color-canvas-default); + border: 1px solid var(--color-border-muted); + margin-bottom: 8px; +} +.maturity-item.active { border-color: var(--color-accent-fg); } +.maturity-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 4px; +} +.maturity-name { font-size: 14px; font-weight: 600; color: var(--color-fg-default); } +.maturity-count { margin-left: auto; font-size: 12px; color: var(--color-fg-muted); } +.maturity-desc { font-size: 12px; color: var(--color-fg-muted); line-height: 1.5; } + +/* ===== Level Badges ===== */ +.level-badge { + padding: 2px 10px; + border-radius: 2em; + font-size: 12px; + font-weight: 500; + border: 1px solid transparent; +} +.level-badge.level-1 { background: rgba(88,166,255,0.12); color: var(--color-accent-fg); border-color: rgba(88,166,255,0.3); } +.level-badge.level-2 { background: rgba(121,192,255,0.12); color: #79c0ff; border-color: rgba(121,192,255,0.3); } +.level-badge.level-3 { background: rgba(63,185,80,0.12); color: var(--color-success-fg); border-color: rgba(63,185,80,0.3); } +.level-badge.level-4 { background: rgba(210,153,34,0.12); color: var(--color-attention-fg); border-color: rgba(210,153,34,0.3); } +.level-badge.level-5 { background: rgba(163,113,247,0.12); color: var(--color-done-fg); border-color: rgba(163,113,247,0.3); } + +/* ===== Pillar Details (expandable) ===== */ +.pillar-details-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 8px; +} + +.repo-pillar { + background: var(--color-canvas-default); + border: 1px solid var(--color-border-muted); + border-radius: 6px; + font-size: 13px; + overflow: hidden; +} +.repo-pillar details { cursor: pointer; } +.repo-pillar summary { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + list-style: none; + user-select: none; +} +.repo-pillar summary::-webkit-details-marker { display: none; } +.repo-pillar summary::before { + content: '▸'; + color: var(--color-fg-subtle); + margin-right: 6px; + font-size: 10px; +} +.repo-pillar details[open] summary::before { content: '▾'; } +.repo-pillar summary:hover { background: rgba(177,186,196,0.04); } +.repo-pillar-name { color: var(--color-fg-muted); } +.repo-pillar-value { font-weight: 600; color: var(--color-fg-default); font-size: 12px; } +.repo-pillar-value.passing { color: var(--color-success-fg); } + +.pillar-criteria-list { padding: 4px 12px 8px; border-top: 1px solid var(--color-border-muted); } +.criterion-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 4px 0; + font-size: 12px; + color: var(--color-fg-muted); +} +.criterion-row + .criterion-row { border-top: 1px solid rgba(33,38,45,0.5); } +.criterion-row-title { flex: 1; min-width: 0; } +.criterion-status { + font-size: 12px; + font-weight: 500; + padding: 1px 8px; + border-radius: 2em; + border: 1px solid transparent; + flex-shrink: 0; + margin-left: 8px; +} +.criterion-status.pass { color: var(--color-success-fg); background: rgba(63,185,80,0.1); border-color: rgba(63,185,80,0.2); } +.criterion-status.fail { color: var(--color-danger-fg); background: rgba(248,81,73,0.1); border-color: rgba(248,81,73,0.2); } +.criterion-status.skip { color: var(--color-fg-muted); background: rgba(139,148,158,0.08); border-color: rgba(139,148,158,0.15); } + +/* ===== Area Breakdown ===== */ +.area-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 8px; +} +.area-item { + background: var(--color-canvas-default); + border: 1px solid var(--color-border-muted); + border-radius: 6px; + overflow: hidden; +} +.area-item details { cursor: pointer; } +.area-item summary { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 12px; + list-style: none; + user-select: none; +} +.area-item summary::-webkit-details-marker { display: none; } +.area-name { font-weight: 600; font-size: 13px; color: var(--color-fg-default); } +.area-summary { display: flex; align-items: center; gap: 8px; } +.area-source { + font-size: 10px; + padding: 1px 6px; + border-radius: 2em; + background: rgba(139,148,158,0.08); + color: var(--color-fg-subtle); + border: 1px solid var(--color-border-muted); +} +.area-score { font-weight: 600; font-size: 12px; } +.area-score.score-good { color: var(--color-success-fg); } +.area-score.score-mid { color: var(--color-attention-fg); } +.area-score.score-bad { color: var(--color-danger-fg); } +.area-detail { padding: 4px 12px 8px; border-top: 1px solid var(--color-border-muted); } +.area-apply-to { font-size: 11px; color: var(--color-fg-subtle); margin-bottom: 6px; } + +/* ===== Share Area ===== */ +.share-area { margin-bottom: 16px; } +.share-area .btn { position: relative; } +.share-area .btn[title]:hover::after { + content: attr(title); + position: absolute; + bottom: calc(100% + 6px); + left: 50%; + transform: translateX(-50%); + background: var(--color-canvas-overlay, #1c2128); + color: var(--color-fg-on-emphasis, #fff); + padding: 6px 10px; + border-radius: 6px; + font-size: 12px; + white-space: nowrap; + pointer-events: none; + z-index: 10; + box-shadow: 0 2px 8px rgba(0,0,0,.25); +} + +/* ===== Service Information (collapsed) ===== */ +.service-info { + text-align: left; + max-width: 1280px; + margin: 0 auto 16px auto; + border: 1px solid var(--color-border-muted); + border-radius: 6px; + font-size: 12px; + background: var(--color-canvas-subtle); +} +.service-info summary { + padding: 8px 12px; + cursor: pointer; + color: var(--color-fg-subtle); + font-weight: 600; + font-size: 12px; +} +.service-info summary:hover { color: var(--color-fg-muted); } +.service-info-body { + padding: 8px 12px; + border-top: 1px solid var(--color-border-muted); +} +.svc-block { margin-bottom: 12px; } +.svc-block h5 { + font-size: 12px; + color: var(--color-fg-muted); + margin-bottom: 4px; +} +.svc-block p { color: var(--color-fg-subtle); margin-bottom: 2px; } +.svc-block ul { list-style: none; padding: 0; } +.svc-block li { padding: 2px 0; color: var(--color-fg-subtle); } +.svc-table { + width: 100%; + border-collapse: collapse; + font-size: 11px; +} +.svc-table th, .svc-table td { + text-align: left; + padding: 3px 8px; + border-bottom: 1px solid var(--color-border-muted); +} +.svc-table th { color: var(--color-fg-muted); font-weight: 600; } +.svc-table td { color: var(--color-fg-subtle); } +.svc-table .status-detected { color: var(--color-success-fg); } +.svc-table .status-not-detected { color: var(--color-fg-muted); } +.svc-table .status-error { color: var(--color-danger-fg); } + +/* ===== Utility classes ===== */ +.muted { color: var(--color-fg-muted); } +.small { font-size: 12px; } + +/* ===== Footer ===== */ +.footer { + text-align: center; + color: var(--color-fg-subtle); + margin-top: 24px; + padding: 16px; + border-top: 1px solid var(--color-border-muted); + font-size: 12px; +} +.footer a { color: var(--color-accent-fg); } + +/* ===== Responsive ===== */ +@media (max-width: 768px) { + .container { padding: 16px; } + .pillar-grid { grid-template-columns: 1fr; } + .ai-criteria-grid { grid-template-columns: 1fr; } + .area-grid { grid-template-columns: 1fr; } + .pillar-details-grid { grid-template-columns: 1fr; } + .hero { flex-direction: column; text-align: center; } + .hero-level { margin: 0 auto; } + .input-group { flex-direction: column; } + .input-group input { border-right: 1px solid var(--color-border-default); border-bottom: none; border-radius: 6px 6px 0 0; } + .btn-primary { border-radius: 0 0 6px 6px; width: 100%; justify-content: center; } +} diff --git a/webapp/frontend/src/repo-location.js b/webapp/frontend/src/repo-location.js new file mode 100644 index 0000000..a13f610 --- /dev/null +++ b/webapp/frontend/src/repo-location.js @@ -0,0 +1,81 @@ +/** + * URL parsing and browser history management for repo references. + */ + +const OWNER_RE = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$/; +const REPO_RE = /^[a-zA-Z0-9._-]{1,100}$/; +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +/** + * Parse various GitHub reference formats into "owner/repo" or null. + */ +export function parseGitHubReference(input) { + if (!input || typeof input !== "string") return null; + const trimmed = input.trim(); + + // owner/repo shorthand + if (!trimmed.includes("://")) { + const parts = trimmed.split("/").filter(Boolean); + if (parts.length === 2 && OWNER_RE.test(parts[0]) && REPO_RE.test(parts[1])) { + return `${parts[0]}/${parts[1]}`; + } + return null; + } + + // Full URL + try { + const url = new URL(trimmed); + if (url.protocol !== "https:" || url.hostname !== "github.com") return null; + const parts = url.pathname + .replace(/\.git$/, "") + .split("/") + .filter(Boolean); + if (parts.length === 2 && OWNER_RE.test(parts[0]) && REPO_RE.test(parts[1])) { + return `${parts[0]}/${parts[1]}`; + } + } catch { + // ignore + } + return null; +} + +/** + * Normalize a repo reference: trim, lower owner, return "owner/repo" or null. + */ +export function normalizeRepoReference(value) { + const ref = parseGitHubReference(value); + return ref || null; +} + +/** + * Extract repo reference from URL pathname like /owner/repo. + */ +export function getRepoFromPath(pathname) { + if (!pathname || pathname === "/") return null; + const parts = pathname.split("/").filter(Boolean); + if (parts.length === 2 && OWNER_RE.test(parts[0]) && REPO_RE.test(parts[1])) { + return `${parts[0]}/${parts[1]}`; + } + return null; +} + +/** + * Extract shared report ID from pathname like /_/report/{uuid}. + */ +export function getSharedReportId(pathname) { + if (!pathname) return null; + const match = pathname.match(/^\/_\/report\/([^/]+)$/); + if (match && UUID_RE.test(match[1])) return match[1]; + return null; +} + +/** + * Push the scanned repo path into browser history. + */ +export function syncRepoPathInBrowser(repoReference) { + if (!repoReference) return; + const newPath = `/${repoReference}`; + if (window.location.pathname !== newPath) { + window.history.pushState({ repo: repoReference }, "", newPath); + } +} diff --git a/webapp/frontend/src/report.js b/webapp/frontend/src/report.js new file mode 100644 index 0000000..0537737 --- /dev/null +++ b/webapp/frontend/src/report.js @@ -0,0 +1,590 @@ +/** + * Report renderer — mirrors the CLI `readiness --html` visual report structure. + * + * Sections: Hero → Fix First → AI Tooling → Pillar Performance → + * Maturity Model → Pillar Details → Area Breakdown → + * (Service Information collapsed) + */ +import { shareReport } from "./api.js"; + +const LEVEL_NAMES = { + 1: "Functional", + 2: "Documented", + 3: "Standardized", + 4: "Optimized", + 5: "Autonomous" +}; +const LEVEL_DESCRIPTIONS = { + 1: "Repo builds, tests run, and basic tooling (linter, lockfile) is in place. AI agents can clone and get started.", + 2: "README, CONTRIBUTING guide, and custom instructions exist. Agents understand project context and conventions.", + 3: "CI/CD, security policies, CODEOWNERS, and observability are configured. Agents operate within well-defined guardrails.", + 4: "MCP servers, custom agents, and AI skills are set up. Agents have deep integration with project-specific tools and workflows.", + 5: "Full AI-native development: agents can independently plan, implement, test, and ship changes with minimal human oversight." +}; + +const PILLAR_GROUPS = { + "style-validation": "repo-health", + "build-system": "repo-health", + testing: "repo-health", + documentation: "repo-health", + "dev-environment": "repo-health", + "code-quality": "repo-health", + observability: "repo-health", + "security-governance": "repo-health", + "ai-tooling": "ai-setup" +}; + +const PILLAR_GROUP_LABELS = { "repo-health": "Repo Health", "ai-setup": "AI Setup" }; + +const AI_ICONS = { + "custom-instructions": "📝", + "mcp-config": "🔌", + "custom-agents": "🤖", + "copilot-skills": "⚡", + "apm-config": "📦", + "apm-locked-deps": "🔒", + "apm-ci-integration": "⚙️" +}; + +// ===================================================================== +// Main entry +// ===================================================================== + +export function renderReport(report, { sharingEnabled = false, shared = false } = {}) { + const container = document.getElementById("report"); + if (!container) return; + container.innerHTML = ""; + + // Snapshot banner for shared reports + if (shared) container.appendChild(buildSnapshotBanner(report)); + + // Hero + container.appendChild(buildHero(report)); + + // Share button (top) + if (sharingEnabled) container.appendChild(buildShareButton(report)); + + // Fix First + container.appendChild(buildFixFirst(report)); + + // AI Tooling Readiness + const aiHtml = buildAiToolingHero(report); + if (aiHtml) container.appendChild(aiHtml); + + // Pillar Performance + if (report.pillars?.length) container.appendChild(buildPillarPerformance(report)); + + // Maturity Model + container.appendChild(buildMaturityModel(report)); + + // Pillar Details (expandable) + if (report.pillars?.length) container.appendChild(buildPillarDetails(report)); + + // Area Breakdown + if (report.areaReports?.length) container.appendChild(buildAreaBreakdown(report)); + + // Share button (bottom) + if (sharingEnabled) container.appendChild(buildShareButton(report)); + + // Service Information (collapsed in footer) + const svcInfo = buildServiceInfo(report); + if (svcInfo) { + const footer = document.querySelector(".footer"); + if (footer) footer.insertBefore(svcInfo, footer.firstChild); + else container.appendChild(svcInfo); + } +} + +// ===================================================================== +// Hero section +// ===================================================================== + +function buildHero(report) { + const level = report.achievedLevel ?? 1; + const name = LEVEL_NAMES[level] || `Level ${level}`; + const levelClass = level >= 4 ? "level-high" : level >= 2 ? "level-mid" : "level-low"; + + const totalPassed = (report.pillars || []).reduce((s, p) => s + (Number.isFinite(p.passed) ? p.passed : 0), 0); + const totalChecks = (report.pillars || []).reduce((s, p) => s + (Number.isFinite(p.total) ? p.total : 0), 0); + + const nextLevel = (report.levels || []).find((l) => l.level === level + 1); + let nextHtml = ""; + if (nextLevel && !nextLevel.achieved) { + const nextName = LEVEL_NAMES[nextLevel.level] || `Level ${nextLevel.level}`; + const remaining = nextLevel.total - nextLevel.passed; + nextHtml = `
Next: Level ${nextLevel.level} — ${esc(nextName)} (${remaining} more check${remaining !== 1 ? "s" : ""} needed)
`; + } else if (level === 5) { + nextHtml = `
✓ Maximum level achieved
`; + } + + const repoLabel = report.repo_name || report.repo_url || ""; + const meta = []; + if (report.durationMs) meta.push(`${(report.durationMs / 1000).toFixed(1)}s`); + if (report.isMonorepo) meta.push(`Monorepo (${(report.apps || []).length} apps)`); + if (report.generatedAt) meta.push(new Date(report.generatedAt).toLocaleString()); + + const el = createElement("div", "hero"); + el.innerHTML = ` +
${level}
+
+
${report.repo_url && isGitHubUrl(report.repo_url) ? `${esc(repoLabel)}` : esc(repoLabel)}
+
Level ${level}: ${esc(name)} — ${totalPassed} of ${totalChecks} checks passing
+ ${meta.length ? `
${esc(meta.join(" · "))}
` : ""} + ${nextHtml} +
+ `; + return el; +} + +// ===================================================================== +// Fix First section +// ===================================================================== + +function buildFixFirst(report) { + const failing = (report.criteria || []) + .filter((c) => c.status === "fail") + .sort((a, b) => { + const iw = { high: 3, medium: 2, low: 1 }; + const ew = { low: 1, medium: 2, high: 3 }; + const d = (iw[b.impact] || 0) - (iw[a.impact] || 0); + return d !== 0 ? d : (ew[a.effort] || 0) - (ew[b.effort] || 0); + }) + .slice(0, 5); + + const el = createElement("div", "section fix-first"); + if (failing.length === 0) { + el.innerHTML = ` +

✓ All Checks Passing

+

This repository passes all readiness criteria.

`; + return el; + } + + el.innerHTML = ` +

⚠ Fix First

+
+ ${failing + .map( + (c) => ` +
+
+
+
${esc(c.title)}
+ ${c.reason ? `
${esc(c.reason)}
` : ""} +
+ ${c.impact ? `${esc(c.impact)} impact` : ""} + ${c.effort ? `${esc(c.effort)} effort` : ""} +
+
+
+ ` + ) + .join("")} +
`; + return el; +} + +// ===================================================================== +// AI Tooling Hero +// ===================================================================== + +function buildAiToolingHero(report) { + const criteria = (report.criteria || []).filter((c) => c.pillar === "ai-tooling"); + if (criteria.length === 0) return null; + + const passed = criteria.filter((c) => c.status === "pass").length; + const total = criteria.length; + const pct = total > 0 ? Math.round((passed / total) * 100) : 0; + const scoreClass = pct >= 60 ? "score-high" : pct >= 30 ? "score-medium" : "score-low"; + const scoreLabel = + pct >= 80 + ? "Excellent" + : pct >= 60 + ? "Good" + : pct >= 40 + ? "Fair" + : pct >= 20 + ? "Getting Started" + : "Not Started"; + + const el = createElement("div", "section ai-hero"); + el.innerHTML = ` +

AI Tooling Readiness

+

How well prepared this repository is for AI-assisted development

+
+
${pct}%
+
+
${esc(scoreLabel)}
+
${passed} of ${total} AI tooling checks passing
+
+
+
+ ${criteria + .map((c) => { + const icon = AI_ICONS[c.id] || "🔧"; + return ` +
+
${c.status === "pass" ? "✓" : "✗"}
+
+
${icon} ${esc(c.title)}
+
${c.status === "pass" ? "Detected" : esc(c.reason || "")}
+
+
`; + }) + .join("")} +
`; + return el; +} + +// ===================================================================== +// Pillar Performance (progress bars grouped by repo-health / ai-setup) +// ===================================================================== + +function buildPillarPerformance(report) { + const el = createElement("div", "section"); + let inner = `

Pillar Performance

`; + + for (const [group, label] of Object.entries(PILLAR_GROUP_LABELS)) { + const pillars = (report.pillars || []).filter((p) => PILLAR_GROUPS[p.id] === group); + if (pillars.length === 0) continue; + + inner += `

${esc(label)}

`; + for (const p of pillars) { + const passed = Number.isFinite(p.passed) ? p.passed : 0; + const total = Number.isFinite(p.total) ? p.total : 0; + const allPass = passed === total && total > 0; + const rawPct = total > 0 ? (passed / total) * 100 : 0; + const pct = Math.max(Math.min(rawPct, 100), total > 0 ? 2 : 0); + const ratio = total > 0 ? passed / total : 0; + const cls = ratio >= 0.8 ? "high" : ratio >= 0.5 ? "medium" : "low"; + inner += ` +
+
${esc(p.name)}
+
+
+ ${allPass ? "All passing" : `${passed} of ${total}`} +
+
`; + } + inner += `
`; + } + + el.innerHTML = inner; + return el; +} + +// ===================================================================== +// Maturity Model +// ===================================================================== + +function buildMaturityModel(report) { + const level = report.achievedLevel ?? 0; + + const el = createElement("div", "section"); + el.innerHTML = ` +

Maturity Model

+
+ ${[1, 2, 3, 4, 5].map((l) => `
`).join("")} +
+
+ ${[1, 2, 3, 4, 5].map((l) => `
${l}. ${esc(LEVEL_NAMES[l])}
`).join("")} +
+ ${level === 0 ? ` +
+
+ 0 + Not yet assessed + Current +
+
No maturity level achieved yet.
+
` : ""} + ${[level, level + 1] + .filter((l) => l >= 1 && l <= 5) + .map( + (l) => ` +
+
+ ${l} + ${esc(LEVEL_NAMES[l])} + ${l === level ? "Current" : "Next"} +
+
${esc(LEVEL_DESCRIPTIONS[l] || "")}
+
+ ` + ) + .join("")}`; + return el; +} + +// ===================================================================== +// Pillar Details (expandable per-pillar criterion lists) +// ===================================================================== + +function buildPillarDetails(report) { + const el = createElement("div", "section"); + let inner = `

Pillar Details

`; + + if (report.isMonorepo) { + inner += `
Monorepo · ${(report.apps || []).length} apps
`; + } + + inner += `
`; + for (const pillar of report.pillars || []) { + const items = (report.criteria || []).filter((c) => c.pillar === pillar.id); + const pPassed = Number.isFinite(pillar.passed) ? pillar.passed : 0; + const pTotal = Number.isFinite(pillar.total) ? pillar.total : 0; + const pRate = Number.isFinite(pillar.passRate) ? pillar.passRate : 0; + const allPass = pPassed === pTotal && pTotal > 0; + inner += ` +
+ + + ${allPass ? "✓ " : ""}${esc(pillar.name)} + ${pPassed}/${pTotal}${allPass ? "" : ` (${Math.round(pRate * 100)}%)`} + +
+ ${ + items.length > 0 + ? items + .map( + (c) => ` +
+ ${esc(c.title)}${c.appSummary ? ` (${safeNum(c.appSummary.passed)}/${safeNum(c.appSummary.total)} apps)` : ""}${c.areaSummary ? ` (${safeNum(c.areaSummary.passed)}/${safeNum(c.areaSummary.total)} areas)` : ""} + ${c.status === "pass" ? "Pass" : c.status === "fail" ? "Fail" : "Skip"} +
+ ` + ) + .join("") + : `
No criteria
` + } +
+ +
`; + } + inner += `
`; + + // Extras + if (report.extras?.length) { + inner += `

Bonus Checks

`; + for (const e of report.extras) { + inner += ` +
+ ${esc(e.title || e.id)} + ${e.status === "pass" ? "Pass" : e.status === "fail" ? "Fail" : "Skip"} +
`; + } + inner += `
`; + } + + el.innerHTML = inner; + return el; +} + +// ===================================================================== +// Area Breakdown +// ===================================================================== + +function buildAreaBreakdown(report) { + const el = createElement("div", "section"); + let inner = ` +

Per-Area Breakdown

+
`; + + for (const ar of report.areaReports) { + const relevant = ar.criteria.filter((c) => c.status !== "skip"); + const passed = relevant.filter((c) => c.status === "pass").length; + const total = relevant.length; + const pct = total ? Math.round((passed / total) * 100) : 0; + const areaName = ar.area?.name || (typeof ar.area === "string" ? ar.area : "Area"); + const sourceLabel = ar.area?.source === "config" ? "config" : "auto"; + const applyTo = ar.area?.applyTo + ? Array.isArray(ar.area.applyTo) + ? ar.area.applyTo.join(", ") + : ar.area.applyTo + : ""; + + inner += ` +
+
+ + ${esc(areaName)} + + ${esc(sourceLabel)} + = 50 ? " score-mid" : " score-bad"}">${passed}/${total} (${pct}%) + + +
+ ${applyTo ? `
${esc(applyTo)}
` : ""} + ${ar.criteria + .map( + (c) => ` +
+ ${esc(c.title)} + ${c.status === "pass" ? "Pass" : c.status === "fail" ? "Fail" : "Skip"} +
+ ` + ) + .join("")} +
+
+
`; + } + + inner += `
`; + el.innerHTML = inner; + return el; +} + +// ===================================================================== +// Service Information (collapsed) +// ===================================================================== + +function buildServiceInfo(report) { + const blocks = []; + + if (report.policies) { + blocks.push( + `
Policy Chain

${esc((report.policies.chain || []).join(" → "))}

Criteria count: ${report.policies.criteriaCount ?? "—"}

` + ); + } + + if (report.engine?.signals?.length) { + const rows = report.engine.signals + .map( + (s) => + `${esc(s.id)}${esc(s.label)}${esc(s.kind)}${esc(s.status)}` + ) + .join(""); + blocks.push( + `
Signals (${report.engine.signals.length})
${rows}
IDLabelKindStatus
` + ); + } + + if (report.engine?.policyWarnings?.length) { + const items = report.engine.policyWarnings + .map( + (w) => `
  • [${esc(w.stage)}] ${esc(w.pluginName)}: ${esc(w.message)}
  • ` + ) + .join(""); + blocks.push( + `
    Policy Warnings (${report.engine.policyWarnings.length})
    ` + ); + } + + if (report.engine && typeof report.engine.score === "number") { + blocks.push( + `
    Engine

    Score: ${report.engine.score}${report.engine.grade ? ` · Grade: ${esc(report.engine.grade)}` : ""}

    ` + ); + } + + if (!blocks.length) return null; + + const section = document.createElement("details"); + section.className = "service-info"; + section.innerHTML = `Service Information
    ${blocks.join("")}
    `; + return section; +} + +// ===================================================================== +// Snapshot banner (shared reports) +// ===================================================================== + +function buildSnapshotBanner(report) { + const el = createElement("div", "snapshot-banner"); + const ts = report.generatedAt ? new Date(report.generatedAt).toLocaleString() : "unknown date"; + el.innerHTML = ` + 📸 + This is a snapshot of the readiness report generated on ${esc(ts)}. Results may differ if the repository has changed since then. + `; + return el; +} + +// ===================================================================== +// Share button + toast +// ===================================================================== + +function buildShareButton(report) { + const wrap = createElement("div", "share-area"); + const btn = document.createElement("button"); + btn.className = "btn btn-secondary"; + btn.textContent = "Share Report"; + btn.title = "Anyone with the link will be able to view this report"; + btn.addEventListener("click", async () => { + btn.disabled = true; + btn.textContent = "Sharing…"; + try { + const { url } = await shareReport(report); + const fullUrl = `${window.location.origin}${url}`; + if (navigator.share) { + await navigator.share({ title: "AgentRC Readiness Report", url: fullUrl }); + showToast("Report shared!"); + } else { + await navigator.clipboard.writeText(fullUrl); + showToast("Link copied to clipboard!"); + } + } catch (err) { + if (err.name === "AbortError") return; + showToast(`Share failed: ${err.message}`); + } finally { + btn.disabled = false; + btn.textContent = "Share Report"; + } + }); + wrap.appendChild(btn); + return wrap; +} + +function showToast(message) { + const toast = document.getElementById("toast"); + if (!toast) return; + toast.textContent = message; + toast.hidden = false; + toast.classList.add("show"); + setTimeout(() => { + toast.classList.remove("show"); + setTimeout(() => { + toast.hidden = true; + }, 200); + }, 3000); +} + +// ===================================================================== +// Helpers +// ===================================================================== + +function createElement(tag, className) { + const el = document.createElement(tag); + if (className) el.className = className; + return el; +} + +function esc(str) { + if (!str) return ""; + return String(str) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +const ALLOWED_STATUS = new Set(["pass", "fail", "skip"]); +const ALLOWED_IMPACT = new Set(["high", "medium", "low"]); +const ALLOWED_EFFORT = new Set(["high", "medium", "low"]); +const ALLOWED_SIGNAL_STATUS = new Set(["detected", "not-detected", "error"]); +function safeNum(val) { + const n = Number(val); + return Number.isFinite(n) ? n : 0; +} + +function safeClass(val, allowed) { + return allowed.has(val) ? val : "unknown"; +} + +function isGitHubUrl(url) { + try { + const parsed = new URL(url); + return parsed.protocol === "https:" && parsed.hostname === "github.com"; + } catch { + return false; + } +} diff --git a/webapp/frontend/src/theme-init.js b/webapp/frontend/src/theme-init.js new file mode 100644 index 0000000..3a07d16 --- /dev/null +++ b/webapp/frontend/src/theme-init.js @@ -0,0 +1,11 @@ +// Theme init — respect saved preference, default dark +(function () { + var theme = "dark"; + try { + var saved = localStorage.getItem("theme"); + if (saved === "dark" || saved === "light") theme = saved; + } catch (_) {} + document.documentElement.setAttribute("data-theme", theme); + var meta = document.querySelector('meta[name="theme-color"]'); + if (meta) meta.content = theme === "light" ? "#ffffff" : "#0d1117"; +})(); diff --git a/webapp/frontend/tests/repo-location.test.js b/webapp/frontend/tests/repo-location.test.js new file mode 100644 index 0000000..d9e772c --- /dev/null +++ b/webapp/frontend/tests/repo-location.test.js @@ -0,0 +1,81 @@ +import { describe, it, expect } from "vitest"; +import { + parseGitHubReference, + normalizeRepoReference, + getRepoFromPath, + getSharedReportId, +} from "../src/repo-location.js"; + +describe("parseGitHubReference", () => { + it("parses owner/repo", () => { + expect(parseGitHubReference("microsoft/agentrc")).toBe("microsoft/agentrc"); + }); + + it("parses full GitHub URL", () => { + expect(parseGitHubReference("https://github.com/microsoft/agentrc")).toBe("microsoft/agentrc"); + }); + + it("parses URL with .git suffix", () => { + expect(parseGitHubReference("https://github.com/microsoft/agentrc.git")).toBe( + "microsoft/agentrc" + ); + }); + + it("returns null for non-GitHub URL", () => { + expect(parseGitHubReference("https://gitlab.com/user/repo")).toBeNull(); + }); + + it("returns null for empty input", () => { + expect(parseGitHubReference("")).toBeNull(); + expect(parseGitHubReference(null)).toBeNull(); + }); + + it("returns null for invalid shorthand", () => { + expect(parseGitHubReference("just-a-string")).toBeNull(); + expect(parseGitHubReference("a/b/c")).toBeNull(); + }); +}); + +describe("normalizeRepoReference", () => { + it("normalizes valid references", () => { + expect(normalizeRepoReference("microsoft/agentrc")).toBe("microsoft/agentrc"); + }); + + it("returns null for invalid references", () => { + expect(normalizeRepoReference("invalid")).toBeNull(); + }); +}); + +describe("getRepoFromPath", () => { + it("extracts owner/repo from path", () => { + expect(getRepoFromPath("/microsoft/agentrc")).toBe("microsoft/agentrc"); + }); + + it("returns null for root path", () => { + expect(getRepoFromPath("/")).toBeNull(); + }); + + it("returns null for single segment", () => { + expect(getRepoFromPath("/microsoft")).toBeNull(); + }); + + it("returns null for deep paths", () => { + expect(getRepoFromPath("/microsoft/agentrc/extra")).toBeNull(); + }); +}); + +describe("getSharedReportId", () => { + it("extracts UUID from report path", () => { + expect(getSharedReportId("/_/report/550e8400-e29b-41d4-a716-446655440000")).toBe( + "550e8400-e29b-41d4-a716-446655440000" + ); + }); + + it("returns null for non-report paths", () => { + expect(getSharedReportId("/microsoft/agentrc")).toBeNull(); + }); + + it("returns null for invalid UUID", () => { + expect(getSharedReportId("/_/report/not-a-uuid")).toBeNull(); + }); +}); diff --git a/webapp/frontend/vitest.config.js b/webapp/frontend/vitest.config.js new file mode 100644 index 0000000..f624398 --- /dev/null +++ b/webapp/frontend/vitest.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + }, +});