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
+
+ AI Tooling — Custom instructions, MCP configuration, custom agents, and Copilot skills
+ Build System — Build scripts, CI/CD workflows, and monorepo workspace configuration
+ Testing — Test scripts at root and per-area level
+ Documentation — README, CONTRIBUTING guide, and area-level docs
+ Dev Environment — Lockfile presence and .env.example templates
+ Style & Validation — Linting, formatting, and type checking configuration
+ Security & Governance — LICENSE, CODEOWNERS, SECURITY.md, and Dependabot setup
+ Code Quality & Observability — Formatter config, logging, and OpenTelemetry instrumentation
+
+
+ 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 .
+
+
+
+
+
+
Cloning repository…
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
+
+
+ ${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 ? `
+
+
+
No maturity level achieved yet.
+
` : ""}
+ ${[level, level + 1]
+ .filter((l) => l >= 1 && l <= 5)
+ .map(
+ (l) => `
+
+
+
${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}) `
+ );
+ }
+
+ 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",
+ },
+});