diff --git a/.github/workflows/device-agent-release.yml b/.github/workflows/device-agent-release.yml new file mode 100644 index 0000000000..821e34b7e2 --- /dev/null +++ b/.github/workflows/device-agent-release.yml @@ -0,0 +1,472 @@ +name: Device Agent Release + +on: + push: + branches: ['**'] + paths: + - 'packages/device-agent/**' + +permissions: + contents: write + +jobs: + detect-version: + name: Detect Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + tag_name: ${{ steps.version.outputs.tag_name }} + is_prerelease: ${{ steps.version.outputs.is_prerelease }} + portal_url: ${{ steps.version.outputs.portal_url }} + release_name: ${{ steps.version.outputs.release_name }} + auto_update_url: ${{ steps.version.outputs.auto_update_url }} + s3_env: ${{ steps.version.outputs.s3_env }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Compute next version + id: version + run: | + # Get the latest production tag (only match clean semver tags like device-agent-v1.0.0) + LATEST_TAG=$(git tag -l 'device-agent-v*' --sort=-v:refname | grep -E '^device-agent-v[0-9]+\.[0-9]+\.[0-9]+$' | head -1) + + if [ -z "$LATEST_TAG" ]; then + # No existing tags - start at 1.0.0 + NEXT_VERSION="1.0.0" + else + # Extract version and bump patch + CURRENT_VERSION="${LATEST_TAG#device-agent-v}" + IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" + NEXT_VERSION="$MAJOR.$MINOR.$((PATCH + 1))" + fi + + BRANCH="${GITHUB_REF_NAME}" + + if [ "$BRANCH" = "release" ]; then + TAG_NAME="device-agent-v${NEXT_VERSION}" + IS_PRERELEASE="false" + PORTAL_URL="https://portal.trycomp.ai" + RELEASE_NAME="Device Agent v${NEXT_VERSION}" + S3_ENV="production" + else + TAG_NAME="device-agent-v${NEXT_VERSION}-staging.${GITHUB_RUN_NUMBER}" + IS_PRERELEASE="true" + PORTAL_URL="https://portal.staging.trycomp.ai" + RELEASE_NAME="Device Agent v${NEXT_VERSION} (Staging #${GITHUB_RUN_NUMBER})" + S3_ENV="staging" + fi + + # Auto-update URL: proxied through the portal (no direct S3 access needed) + AUTO_UPDATE_URL="${PORTAL_URL}/api/device-agent/updates" + + echo "version=$NEXT_VERSION" >> $GITHUB_OUTPUT + echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT + echo "is_prerelease=$IS_PRERELEASE" >> $GITHUB_OUTPUT + echo "portal_url=$PORTAL_URL" >> $GITHUB_OUTPUT + echo "release_name=$RELEASE_NAME" >> $GITHUB_OUTPUT + echo "auto_update_url=$AUTO_UPDATE_URL" >> $GITHUB_OUTPUT + echo "s3_env=$S3_ENV" >> $GITHUB_OUTPUT + + echo "--- Version Info ---" + echo "Latest tag: $LATEST_TAG" + echo "Next version: $NEXT_VERSION" + echo "Tag name: $TAG_NAME" + echo "Pre-release: $IS_PRERELEASE" + echo "Portal URL: $PORTAL_URL" + echo "Auto-update URL: $AUTO_UPDATE_URL" + echo "S3 env: $S3_ENV" + + build-macos: + name: Build macOS (.dmg + .zip) + needs: detect-version + runs-on: macos-latest + defaults: + run: + working-directory: packages/device-agent + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Set package.json version + env: + VERSION: ${{ needs.detect-version.outputs.version }} + run: | + node -e " + const pkg = require('./package.json'); + pkg.version = process.env.VERSION; + require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n'); + " + + - name: Build + env: + PORTAL_URL: ${{ needs.detect-version.outputs.portal_url }} + AGENT_VERSION: ${{ needs.detect-version.outputs.version }} + run: bun run build + + - name: Package macOS + env: + CSC_LINK: ${{ secrets.MAC_CSC_LINK }} + CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + AUTO_UPDATE_URL: ${{ needs.detect-version.outputs.auto_update_url }} + run: bun run package:mac + + - name: Upload macOS artifact + uses: actions/upload-artifact@v4 + with: + name: device-agent-macos + path: | + packages/device-agent/release/*.dmg + packages/device-agent/release/*.zip + packages/device-agent/release/*.blockmap + packages/device-agent/release/*.yml + if-no-files-found: error + + build-windows: + name: Build Windows (.exe) + needs: detect-version + runs-on: windows-latest + defaults: + run: + working-directory: packages/device-agent + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Set package.json version + env: + VERSION: ${{ needs.detect-version.outputs.version }} + shell: bash + run: | + node -e " + const pkg = require('./package.json'); + pkg.version = process.env.VERSION; + require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n'); + " + + - name: Build + env: + PORTAL_URL: ${{ needs.detect-version.outputs.portal_url }} + AGENT_VERSION: ${{ needs.detect-version.outputs.version }} + run: bun run build + + - name: Package Windows (unsigned) + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + AUTO_UPDATE_URL: ${{ needs.detect-version.outputs.auto_update_url }} + run: bun run package:win + + - name: Setup Java for CodeSignTool + uses: actions/setup-java@v4 + with: + distribution: 'corretto' + java-version: '11' + + - name: Sign Windows EXE with SSL.com CodeSignTool + shell: powershell + working-directory: packages/device-agent/release + env: + ESIGNER_USERNAME: ${{ secrets.ESIGNER_USERNAME }} + ESIGNER_PASSWORD: ${{ secrets.ESIGNER_PASSWORD }} + ESIGNER_CREDENTIAL_ID: ${{ secrets.ESIGNER_CREDENTIAL_ID }} + ESIGNER_TOTP_SECRET: ${{ secrets.ESIGNER_TOTP_SECRET }} + run: | + # Download and extract CodeSignTool + Invoke-WebRequest -Uri "https://github.com/SSLcom/CodeSignTool/releases/download/v1.3.0/CodeSignTool-v1.3.0-windows.zip" -OutFile "codesigntool.zip" + Expand-Archive -Path "codesigntool.zip" -DestinationPath "codesigntool" + + # Find the jar file + $jar = Get-ChildItem -Path "codesigntool" -Recurse -Filter "code_sign_tool-*.jar" | Select-Object -First 1 + if (-not $jar) { throw "CodeSignTool jar not found" } + Write-Host "Found CodeSignTool jar at: $($jar.FullName)" + + # Sign each .exe file using Java directly (skips .bat which needs bundled JDK) + Get-ChildItem -Filter "*.exe" | ForEach-Object { + Write-Host "Signing $($_.Name)..." + & java -Xmx1024M -jar "$($jar.FullName)" sign ` + -username="$env:ESIGNER_USERNAME" ` + -password="$env:ESIGNER_PASSWORD" ` + -credential_id="$env:ESIGNER_CREDENTIAL_ID" ` + -totp_secret="$env:ESIGNER_TOTP_SECRET" ` + -input_file_path="$($_.FullName)" ` + -override="true" + if ($LASTEXITCODE -ne 0) { throw "Code signing failed for $($_.Name)" } + Write-Host "Signed $($_.Name) successfully" + } + + - name: Recalculate latest.yml hash after signing + shell: bash + working-directory: packages/device-agent/release + run: | + # Code signing changed the exe, so the sha512 in latest.yml is now wrong. + # Recalculate it from the signed binary. + EXE_FILE=$(ls *.exe | head -1) + if [ -z "$EXE_FILE" ]; then + echo "ERROR: No .exe found in release/" + exit 1 + fi + + # Compute base64-encoded sha512 hash (electron-updater format) + NEW_SHA512=$(openssl dgst -sha512 -binary "$EXE_FILE" | openssl base64 -A) + NEW_SIZE=$(wc -c < "$EXE_FILE" | tr -d ' ') + + echo "Signed exe: $EXE_FILE" + echo "New sha512: $NEW_SHA512" + echo "New size: $NEW_SIZE" + + if [ -f "latest.yml" ]; then + # Update the sha512 and size in latest.yml + # Use /blockMapSize/! to skip blockMapSize lines (they also contain "size:") + sed -i.bak "s|sha512: .*|sha512: ${NEW_SHA512}|" latest.yml + sed -i.bak "/blockMapSize/!s|size: .*|size: ${NEW_SIZE}|" latest.yml + rm -f latest.yml.bak + echo "--- Updated latest.yml ---" + cat latest.yml + else + echo "WARNING: latest.yml not found, skipping hash update" + fi + + - name: Upload Windows artifact + uses: actions/upload-artifact@v4 + with: + name: device-agent-windows + path: | + packages/device-agent/release/*.exe + packages/device-agent/release/*.blockmap + packages/device-agent/release/*.yml + if-no-files-found: error + + build-linux: + name: Build Linux (.AppImage, .deb) + needs: detect-version + runs-on: ubuntu-latest + defaults: + run: + working-directory: packages/device-agent + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Set package.json version + env: + VERSION: ${{ needs.detect-version.outputs.version }} + run: | + node -e " + const pkg = require('./package.json'); + pkg.version = process.env.VERSION; + require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n'); + " + + - name: Build + env: + PORTAL_URL: ${{ needs.detect-version.outputs.portal_url }} + AGENT_VERSION: ${{ needs.detect-version.outputs.version }} + run: bun run build + + - name: Package Linux + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + AUTO_UPDATE_URL: ${{ needs.detect-version.outputs.auto_update_url }} + run: bun run package:linux + + - name: Upload Linux artifact + uses: actions/upload-artifact@v4 + with: + name: device-agent-linux + path: | + packages/device-agent/release/*.AppImage + packages/device-agent/release/*.deb + packages/device-agent/release/*.blockmap + packages/device-agent/release/*.yml + if-no-files-found: error + + release: + name: Create GitHub Release + needs: [detect-version, build-macos, build-windows, build-linux] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download macOS artifact + uses: actions/download-artifact@v4 + with: + name: device-agent-macos + path: artifacts/ + + - name: Download Windows artifact + uses: actions/download-artifact@v4 + with: + name: device-agent-windows + path: artifacts/ + + - name: Download Linux artifact + uses: actions/download-artifact@v4 + with: + name: device-agent-linux + path: artifacts/ + + - name: Create git tag + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag "${{ needs.detect-version.outputs.tag_name }}" -m "${{ needs.detect-version.outputs.release_name }}" + git push origin "${{ needs.detect-version.outputs.tag_name }}" + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.detect-version.outputs.tag_name }} + name: ${{ needs.detect-version.outputs.release_name }} + body: | + ## ${{ needs.detect-version.outputs.release_name }} + + **Environment:** ${{ needs.detect-version.outputs.is_prerelease == 'true' && 'Staging' || 'Production' }} + **Portal:** ${{ needs.detect-version.outputs.portal_url }} + + ### Downloads + - **macOS**: Download the `.dmg` file below (universal binary, Apple Silicon + Intel) + - **Windows**: Download the `.exe` installer below + - **Linux**: Download the `.AppImage` (portable) or `.deb` (Debian/Ubuntu) below + + ### What's included + - Disk encryption check (FileVault / BitLocker / LUKS) + - Antivirus detection (XProtect / Windows Defender / ClamAV + AppArmor/SELinux) + - Password policy enforcement (minimum 8 characters) + - Screen lock verification (5 minutes or less) + - Auto-remediation for fixable settings with guided instructions + + ### Installation + 1. Download the installer for your operating system + 2. Run the installer and follow the prompts + 3. Sign in with your Comp AI portal credentials + 4. The agent will run in your system tray and check compliance automatically + draft: false + prerelease: ${{ needs.detect-version.outputs.is_prerelease == 'true' }} + files: artifacts/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + upload-s3: + name: Upload to S3 + needs: [detect-version, build-macos, build-windows, build-linux] + runs-on: ubuntu-latest + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts/ + merge-multiple: true + + - name: List artifacts + run: ls -la artifacts/ + + - name: Upload installers to S3 + env: + AWS_ACCESS_KEY_ID: ${{ needs.detect-version.outputs.s3_env == 'production' && secrets.APP_AWS_ACCESS_KEY_ID || secrets.APP_AWS_ACCESS_KEY_ID_STAGING }} + AWS_SECRET_ACCESS_KEY: ${{ needs.detect-version.outputs.s3_env == 'production' && secrets.APP_AWS_SECRET_ACCESS_KEY || secrets.APP_AWS_SECRET_ACCESS_KEY_STAGING }} + AWS_REGION: ${{ secrets.APP_AWS_REGION }} + S3_BUCKET: ${{ needs.detect-version.outputs.s3_env == 'production' && secrets.FLEET_AGENT_BUCKET_NAME || secrets.FLEET_AGENT_BUCKET_NAME_STAGING }} + VERSION: ${{ needs.detect-version.outputs.version }} + S3_ENV: ${{ needs.detect-version.outputs.s3_env }} + run: | + PREFIX="device-agent/${S3_ENV}" + + # macOS (dmg for portal downloads) + aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-arm64.dmg \ + s3://${S3_BUCKET}/${PREFIX}/macos/CompAI-Device-Agent-${VERSION}-arm64.dmg + aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-arm64.dmg \ + s3://${S3_BUCKET}/${PREFIX}/macos/latest-arm64.dmg + + aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-x64.dmg \ + s3://${S3_BUCKET}/${PREFIX}/macos/CompAI-Device-Agent-${VERSION}-x64.dmg + aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-x64.dmg \ + s3://${S3_BUCKET}/${PREFIX}/macos/latest-x64.dmg + + # Windows + aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-setup.exe \ + s3://${S3_BUCKET}/${PREFIX}/windows/CompAI-Device-Agent-${VERSION}-setup.exe + aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-setup.exe \ + s3://${S3_BUCKET}/${PREFIX}/windows/latest-setup.exe + + # Linux (.deb uses amd64, .AppImage uses x86_64 architecture naming) + aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-amd64.deb \ + s3://${S3_BUCKET}/${PREFIX}/linux/CompAI-Device-Agent-${VERSION}-amd64.deb + aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-amd64.deb \ + s3://${S3_BUCKET}/${PREFIX}/linux/latest-amd64.deb + + aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-x86_64.AppImage \ + s3://${S3_BUCKET}/${PREFIX}/linux/CompAI-Device-Agent-${VERSION}-x86_64.AppImage + aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-x86_64.AppImage \ + s3://${S3_BUCKET}/${PREFIX}/linux/latest-x86_64.AppImage + + - name: Upload auto-update files to S3 + env: + AWS_ACCESS_KEY_ID: ${{ needs.detect-version.outputs.s3_env == 'production' && secrets.APP_AWS_ACCESS_KEY_ID || secrets.APP_AWS_ACCESS_KEY_ID_STAGING }} + AWS_SECRET_ACCESS_KEY: ${{ needs.detect-version.outputs.s3_env == 'production' && secrets.APP_AWS_SECRET_ACCESS_KEY || secrets.APP_AWS_SECRET_ACCESS_KEY_STAGING }} + AWS_REGION: ${{ secrets.APP_AWS_REGION }} + S3_BUCKET: ${{ needs.detect-version.outputs.s3_env == 'production' && secrets.FLEET_AGENT_BUCKET_NAME || secrets.FLEET_AGENT_BUCKET_NAME_STAGING }} + S3_ENV: ${{ needs.detect-version.outputs.s3_env }} + run: | + UPDATE_DIR="device-agent/${S3_ENV}/updates" + + # Upload all .yml files (latest-mac.yml, latest.yml, latest-linux.yml) + for f in artifacts/*.yml; do + [ -f "$f" ] && aws s3 cp "$f" "s3://${S3_BUCKET}/${UPDATE_DIR}/$(basename "$f")" + done + + # Upload all .zip files (macOS auto-update archives) + for f in artifacts/*.zip; do + [ -f "$f" ] && aws s3 cp "$f" "s3://${S3_BUCKET}/${UPDATE_DIR}/$(basename "$f")" + done + + # Upload all .blockmap files (delta update metadata) + for f in artifacts/*.blockmap; do + [ -f "$f" ] && aws s3 cp "$f" "s3://${S3_BUCKET}/${UPDATE_DIR}/$(basename "$f")" + done + + # Upload .exe for Windows auto-update + for f in artifacts/*.exe; do + [ -f "$f" ] && aws s3 cp "$f" "s3://${S3_BUCKET}/${UPDATE_DIR}/$(basename "$f")" + done + + # Upload .AppImage for Linux auto-update + for f in artifacts/*.AppImage; do + [ -f "$f" ] && aws s3 cp "$f" "s3://${S3_BUCKET}/${UPDATE_DIR}/$(basename "$f")" + done + + echo "--- Auto-update files uploaded to s3://${S3_BUCKET}/${UPDATE_DIR}/ ---" + aws s3 ls "s3://${S3_BUCKET}/${UPDATE_DIR}/" diff --git a/apps/api/package.json b/apps/api/package.json index 3f92db0550..cc2bf7730b 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -27,6 +27,7 @@ "@trigger.dev/build": "4.0.6", "@trigger.dev/sdk": "4.0.6", "@trycompai/db": "1.3.22", + "@trycompai/utils": "1.0.0", "@trycompai/email": "workspace:*", "@upstash/redis": "^1.34.2", "@upstash/vector": "^1.2.2", diff --git a/apps/api/src/devices/devices.service.ts b/apps/api/src/devices/devices.service.ts index 0d977c9f02..c55a2d4b9e 100644 --- a/apps/api/src/devices/devices.service.ts +++ b/apps/api/src/devices/devices.service.ts @@ -1,9 +1,15 @@ import { Injectable, NotFoundException, Logger } from '@nestjs/common'; import { db } from '@trycompai/db'; +import { mergeDeviceLists } from '@trycompai/utils/devices'; import { FleetService } from '../lib/fleet.service'; -import type { DeviceResponseDto } from './dto/device-responses.dto'; +import { DeviceResponseDto } from './dto/device-responses.dto'; import type { MemberResponseDto } from './dto/member-responses.dto'; +/** + * Hybrid device service that fetches from both FleetDM and the Device Agent database. + * FleetDM is the legacy system; Device Agent is the new system. + * Results are merged and deduplicated by serial number / hostname. + */ @Injectable() export class DevicesService { private readonly logger = new Logger(DevicesService.name); @@ -13,86 +19,92 @@ export class DevicesService { async findAllByOrganization( organizationId: string, ): Promise { - try { - // Get organization and its FleetDM label ID - const organization = await db.organization.findUnique({ - where: { id: organizationId }, - select: { - id: true, - name: true, - fleetDmLabelId: true, - }, - }); + const organization = await db.organization.findUnique({ + where: { id: organizationId }, + select: { + id: true, + name: true, + fleetDmLabelId: true, + }, + }); - if (!organization) { - throw new NotFoundException( - `Organization with ID ${organizationId} not found`, - ); - } + if (!organization) { + throw new NotFoundException( + `Organization with ID ${organizationId} not found`, + ); + } - if (!organization.fleetDmLabelId) { - this.logger.warn( - `Organization ${organizationId} does not have FleetDM label configured`, - ); - return []; - } + // Fetch from both sources in parallel + const [fleetDevices, agentDevices] = await Promise.all([ + this.getFleetDevicesForOrg(organization.fleetDmLabelId, organizationId), + this.getAgentDevicesForOrg(organizationId), + ]); - // Get all hosts for the organization's label - const labelHosts = await this.fleetService.getHostsByLabel( - organization.fleetDmLabelId, - ); + // Merge and deduplicate (agent devices take priority) + return mergeDeviceLists(agentDevices, fleetDevices, { + getSerialNumber: (d) => d.hardware_serial, + getHostname: (d) => d.hostname, + }); + } - if (!labelHosts.hosts || labelHosts.hosts.length === 0) { - this.logger.log(`No devices found for organization ${organizationId}`); - return []; - } + async findAllByMember( + organizationId: string, + memberId: string, + ): Promise { + // Verify organization exists + const organization = await db.organization.findUnique({ + where: { id: organizationId }, + select: { id: true, name: true }, + }); - // Extract host IDs - const hostIds = labelHosts.hosts.map((host: { id: number }) => host.id); - this.logger.log( - `Found ${hostIds.length} devices for organization ${organizationId}`, + if (!organization) { + throw new NotFoundException( + `Organization with ID ${organizationId} not found`, ); + } - // Get detailed information for each host - const devices = await this.fleetService.getMultipleHosts(hostIds); + // Verify the member exists and belongs to the organization + const member = await db.member.findFirst({ + where: { + id: memberId, + organizationId: organizationId, + deactivated: false, + }, + select: { + id: true, + userId: true, + role: true, + department: true, + isActive: true, + fleetDmLabelId: true, + organizationId: true, + createdAt: true, + }, + }); - this.logger.log( - `Retrieved ${devices.length} device details for organization ${organizationId}`, + if (!member) { + throw new NotFoundException( + `Member with ID ${memberId} not found in organization ${organizationId}`, ); - return devices; - } catch (error) { - if (error instanceof NotFoundException) { - throw error; - } - this.logger.error( - `Failed to retrieve devices for organization ${organizationId}:`, - error, - ); - throw new Error(`Failed to retrieve devices: ${error.message}`); } + + // Fetch from both sources in parallel + const [fleetDevices, agentDevices] = await Promise.all([ + this.getFleetDevicesForMember(member.fleetDmLabelId, memberId), + this.getAgentDevicesForMember(member.id, organizationId), + ]); + + return mergeDeviceLists(agentDevices, fleetDevices, { + getSerialNumber: (d) => d.hardware_serial, + getHostname: (d) => d.hostname, + }); } - async findAllByMember( + async getMemberById( organizationId: string, memberId: string, - ): Promise { + ): Promise { try { - // First verify the organization exists - const organization = await db.organization.findUnique({ - where: { id: organizationId }, - select: { - id: true, - name: true, - }, - }); - - if (!organization) { - throw new NotFoundException( - `Organization with ID ${organizationId} not found`, - ); - } - - // Verify the member exists and belongs to the organization const member = await db.member.findFirst({ where: { id: memberId, @@ -117,85 +129,190 @@ export class DevicesService { ); } - if (!member.fleetDmLabelId) { - this.logger.warn( - `Member ${memberId} does not have FleetDM label configured`, - ); + return member; + } catch (error) { + if (error instanceof NotFoundException) { + throw error; + } + this.logger.error( + `Failed to retrieve member ${memberId} in organization ${organizationId}:`, + error, + ); + throw new Error(`Failed to retrieve member: ${error.message}`); + } + } + + // --- Private helpers --- + + private async getFleetDevicesForOrg( + fleetDmLabelId: number | null, + organizationId: string, + ): Promise { + if (!fleetDmLabelId) { + return []; + } + + try { + const labelHosts = + await this.fleetService.getHostsByLabel(fleetDmLabelId); + + if (!labelHosts.hosts || labelHosts.hosts.length === 0) { return []; } - // Get devices for the member's specific FleetDM label - const labelHosts = await this.fleetService.getHostsByLabel( - member.fleetDmLabelId, + const hostIds = labelHosts.hosts.map((host: { id: number }) => host.id); + const devices = await this.fleetService.getMultipleHosts(hostIds); + + // Tag each device with source + return devices.map((d: DeviceResponseDto) => ({ + ...d, + source: 'fleet' as const, + })); + } catch (error) { + this.logger.warn( + `Failed to fetch FleetDM devices for org ${organizationId}: ${error instanceof Error ? error.message : error}`, ); + return []; + } + } + + private async getFleetDevicesForMember( + fleetDmLabelId: number | null, + memberId: string, + ): Promise { + if (!fleetDmLabelId) { + return []; + } + + try { + const labelHosts = + await this.fleetService.getHostsByLabel(fleetDmLabelId); if (!labelHosts.hosts || labelHosts.hosts.length === 0) { - this.logger.log(`No devices found for member ${memberId}`); return []; } - // Extract host IDs const hostIds = labelHosts.hosts.map((host: { id: number }) => host.id); - this.logger.log(`Found ${hostIds.length} devices for member ${memberId}`); - - // Get detailed information for each host const devices = await this.fleetService.getMultipleHosts(hostIds); - this.logger.log( - `Retrieved ${devices.length} device details for member ${memberId} in organization ${organizationId}`, - ); - return devices; + return devices.map((d: DeviceResponseDto) => ({ + ...d, + source: 'fleet' as const, + })); } catch (error) { - if (error instanceof NotFoundException) { - throw error; - } - this.logger.error( - `Failed to retrieve devices for member ${memberId} in organization ${organizationId}:`, - error, + this.logger.warn( + `Failed to fetch FleetDM devices for member ${memberId}: ${error instanceof Error ? error.message : error}`, ); - throw new Error(`Failed to retrieve member devices: ${error.message}`); + return []; } } - async getMemberById( + private async getAgentDevicesForOrg( organizationId: string, - memberId: string, - ): Promise { + ): Promise { try { - const member = await db.member.findFirst({ - where: { - id: memberId, - organizationId: organizationId, - deactivated: false, - }, - select: { - id: true, - userId: true, - role: true, - department: true, - isActive: true, - fleetDmLabelId: true, - organizationId: true, - createdAt: true, + const devices = await db.device.findMany({ + where: { organizationId }, + include: { + member: { + include: { + user: { + select: { name: true, email: true }, + }, + }, + }, }, }); - if (!member) { - throw new NotFoundException( - `Member with ID ${memberId} not found in organization ${organizationId}`, - ); - } + return devices.map((device) => + this.mapAgentDeviceToDto({ + ...device, + user: device.member.user, + }), + ); + } catch (error) { + this.logger.warn( + `Failed to fetch agent devices for org ${organizationId}: ${error instanceof Error ? error.message : error}`, + ); + return []; + } + } - return member; + private async getAgentDevicesForMember( + memberId: string, + organizationId: string, + ): Promise { + try { + const devices = await db.device.findMany({ + where: { memberId, organizationId }, + include: { + member: { + include: { + user: { + select: { name: true, email: true }, + }, + }, + }, + }, + }); + + return devices.map((device) => + this.mapAgentDeviceToDto({ + ...device, + user: device.member.user, + }), + ); } catch (error) { - if (error instanceof NotFoundException) { - throw error; - } - this.logger.error( - `Failed to retrieve member ${memberId} in organization ${organizationId}:`, - error, + this.logger.warn( + `Failed to fetch agent devices for member ${memberId}: ${error instanceof Error ? error.message : error}`, ); - throw new Error(`Failed to retrieve member: ${error.message}`); + return []; } } + + private mapAgentDeviceToDto(device: { + id: string; + name: string; + hostname: string; + platform: string; + osVersion: string; + serialNumber: string | null; + hardwareModel: string | null; + isCompliant: boolean; + diskEncryptionEnabled: boolean; + lastCheckIn: Date | null; + agentVersion: string | null; + installedAt: Date; + updatedAt: Date; + user: { name: string; email: string }; + }): DeviceResponseDto { + const dto = new DeviceResponseDto(); + dto.id = device.id; + dto.computer_name = device.name; + dto.hostname = device.hostname; + dto.platform = device.platform === 'macos' ? 'darwin' : device.platform; + dto.os_version = device.osVersion; + dto.hardware_serial = device.serialNumber ?? ''; + dto.hardware_model = device.hardwareModel ?? ''; + dto.seen_time = device.lastCheckIn?.toISOString() ?? ''; + dto.created_at = device.installedAt.toISOString(); + dto.updated_at = device.updatedAt.toISOString(); + dto.display_name = device.name; + dto.display_text = device.name; + dto.status = device.isCompliant ? 'compliant' : 'non-compliant'; + dto.disk_encryption_enabled = device.diskEncryptionEnabled; + dto.source = 'device_agent'; + // Default empty values for FleetDM-specific fields + dto.software = []; + dto.pack_stats = []; + dto.users = []; + dto.labels = []; + dto.packs = []; + dto.batteries = []; + dto.end_users = []; + dto.policies = []; + dto.issues = {}; + dto.mdm = {}; + return dto; + } } diff --git a/apps/api/src/devices/dto/device-responses.dto.ts b/apps/api/src/devices/dto/device-responses.dto.ts index 70bc4c0721..c740a7b215 100644 --- a/apps/api/src/devices/dto/device-responses.dto.ts +++ b/apps/api/src/devices/dto/device-responses.dto.ts @@ -79,8 +79,11 @@ export class DeviceResponseDto { }) software_updated_at: string; - @ApiProperty({ description: 'Device ID', example: 123 }) - id: number; + @ApiProperty({ + description: 'Device ID (numeric for Fleet devices, string UUID for device-agent devices)', + oneOf: [{ type: 'number', example: 123 }, { type: 'string', example: 'clx1abc123' }], + }) + id: number | string; @ApiProperty({ description: 'Detail updated at', @@ -319,4 +322,12 @@ export class DeviceResponseDto { @ApiProperty({ description: 'Display name', example: "John's MacBook Pro" }) display_name: string; + + @ApiProperty({ + description: 'Source system that reported this device', + example: 'fleet', + enum: ['fleet', 'device_agent'], + required: false, + }) + source?: 'fleet' | 'device_agent'; } diff --git a/apps/api/src/lib/fleet.service.ts b/apps/api/src/lib/fleet.service.ts index f23c8245ed..977b27ad43 100644 --- a/apps/api/src/lib/fleet.service.ts +++ b/apps/api/src/lib/fleet.service.ts @@ -1,6 +1,12 @@ import { Injectable, Logger } from '@nestjs/common'; import axios, { AxiosInstance } from 'axios'; +/** + * @deprecated FleetDM integration is being replaced by the custom Comp AI Device Agent. + * See packages/device-agent for the new implementation. + * Device data is now stored in the Device database model. + * This service will be removed in a future release. + */ @Injectable() export class FleetService { private readonly logger = new Logger(FleetService.name); diff --git a/apps/api/src/people/people.service.ts b/apps/api/src/people/people.service.ts index 19728361e4..1b0a0d36d4 100644 --- a/apps/api/src/people/people.service.ts +++ b/apps/api/src/people/people.service.ts @@ -4,6 +4,7 @@ import { Logger, BadRequestException, } from '@nestjs/common'; +import { db } from '@trycompai/db'; import { FleetService } from '../lib/fleet.service'; import type { PeopleResponseDto } from './dto/people-responses.dto'; import type { CreatePeopleDto } from './dto/create-people.dto'; @@ -318,6 +319,27 @@ export class PeopleService { } } + // Also delete device-agent Device records for this member + try { + const deleteResult = await db.device.deleteMany({ + where: { + memberId, + organizationId, + }, + }); + if (deleteResult.count > 0) { + this.logger.log( + `Deleted ${deleteResult.count} device-agent device(s) for member ${memberId} in org ${organizationId}`, + ); + } + } catch (deviceError) { + // Log but don't fail the operation + this.logger.error( + `Failed to delete device-agent devices for member ${memberId}:`, + deviceError, + ); + } + const updatedMember = await MemberQueries.unlinkDevice(memberId); this.logger.log( diff --git a/apps/app/package.json b/apps/app/package.json index 19c7b1f2b0..b73268e092 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -55,6 +55,7 @@ "@trigger.dev/react-hooks": "4.0.6", "@trigger.dev/sdk": "4.0.6", "@trycompai/db": "1.3.22", + "@trycompai/utils": "1.0.0", "@trycompai/design-system": "^1.0.32", "@trycompai/email": "workspace:*", "@types/canvas-confetti": "^1.9.0", diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Employee.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Employee.tsx index a263922fc7..4759638335 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Employee.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Employee.tsx @@ -2,12 +2,12 @@ import type { TrainingVideo } from '@/lib/data/training-videos'; import type { EmployeeTrainingVideoCompletion, Member, Organization, Policy, User } from '@db'; -import { Stack } from '@trycompai/design-system'; -import type { FleetPolicy, Host } from '../../devices/types'; +import { PageHeader, PageLayout, Stack } from '@trycompai/design-system'; +import type { DeviceWithChecks, FleetPolicy, Host } from '../../devices/types'; import { EmployeeDetails } from './EmployeeDetails'; import { EmployeeTasks } from './EmployeeTasks'; -interface EmployeeDetailsProps { +interface EmployeeProps { employee: Member & { user: User; }; @@ -19,6 +19,8 @@ interface EmployeeDetailsProps { host: Host; canEdit: boolean; organization: Organization; + memberDevice: DeviceWithChecks | null; + orgId: string; } export function Employee({ @@ -29,18 +31,36 @@ export function Employee({ host, canEdit, organization, -}: EmployeeDetailsProps) { + memberDevice, + orgId, +}: EmployeeProps) { return ( - - - - + + } + > + + + + + ); } diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.tsx index f0a37a86ea..4d8cb67bcf 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.tsx @@ -5,6 +5,7 @@ import type { EmployeeTrainingVideoCompletion, Member, Organization, Policy, Use import { Card, CardContent, CardHeader, CardTitle } from '@comp/ui/card'; import { + Badge, Section, Stack, Tabs, @@ -15,10 +16,24 @@ import { } from '@trycompai/design-system'; import { AlertCircle, Award, CheckCircle2, Download, Info } from 'lucide-react'; import type { FleetPolicy, Host } from '../../devices/types'; +import type { DeviceWithChecks } from '../../devices/types'; import { PolicyItem } from '../../devices/components/PolicyItem'; import { downloadTrainingCertificate } from '../actions/download-training-certificate'; import { cn } from '@/lib/utils'; +const CHECK_FIELDS = [ + { key: 'diskEncryptionEnabled' as const, dbKey: 'disk_encryption', label: 'Disk Encryption' }, + { key: 'antivirusEnabled' as const, dbKey: 'antivirus', label: 'Antivirus' }, + { key: 'passwordPolicySet' as const, dbKey: 'password_policy', label: 'Password Policy' }, + { key: 'screenLockEnabled' as const, dbKey: 'screen_lock', label: 'Screen Lock' }, +]; + +const PLATFORM_LABELS: Record = { + macos: 'macOS', + windows: 'Windows', + linux: 'Linux', +}; + export const EmployeeTasks = ({ employee, policies, @@ -26,6 +41,7 @@ export const EmployeeTasks = ({ host, fleetPolicies, organization, + memberDevice, }: { employee: Member & { user: User; @@ -37,6 +53,7 @@ export const EmployeeTasks = ({ host: Host; fleetPolicies: FleetPolicy[]; organization: Organization; + memberDevice: DeviceWithChecks | null; }) => { // Calculate training completion status const completedVideos = trainingVideos.filter((v) => v.completedAt !== null); @@ -239,6 +256,74 @@ export const EmployeeTasks = ({ + ) : memberDevice ? ( + + + +
+
+ + {memberDevice.name} + + + {PLATFORM_LABELS[memberDevice.platform] ?? memberDevice.platform}{' '} + {memberDevice.osVersion} + {memberDevice.hardwareModel ? ` \u2022 ${memberDevice.hardwareModel}` : ''} + +
+ + {memberDevice.isCompliant ? 'Compliant' : 'Non-Compliant'} + +
+
+ +
+ {CHECK_FIELDS.map(({ key, dbKey, label }) => { + const isFleetUnsupported = memberDevice.source === 'fleet' && key !== 'diskEncryptionEnabled'; + const passed = memberDevice[key]; + const details = memberDevice.checkDetails?.[dbKey]; + return ( +
+
+ {label} + {!isFleetUnsupported && details?.message && ( +

+ {details.message} +

+ )} + {isFleetUnsupported && ( +

+ Not tracked by Fleet +

+ )} + {details?.exception && ( +

+ {details.exception} +

+ )} +
+ {isFleetUnsupported ? ( + N/A + ) : ( + + {passed ? 'Pass' : 'Fail'} + + )} +
+ ); + })} +
+ {memberDevice.lastCheckIn && ( +

+ Last check-in: {new Date(memberDevice.lastCheckIn).toLocaleString()} +

+ )} +
+
+
) : host ? ( diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/page.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/page.tsx index 807a570782..68deb59bec 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/page.tsx @@ -7,10 +7,10 @@ import { import { getFleetInstance } from '@/lib/fleet'; import type { EmployeeTrainingVideoCompletion, Member, User } from '@db'; import { db } from '@db'; -import { PageHeader, PageLayout } from '@trycompai/design-system'; import type { Metadata } from 'next'; import { headers } from 'next/headers'; import { notFound, redirect } from 'next/navigation'; +import type { CheckDetails, DeviceWithChecks } from '../devices/types'; import { Employee } from './components/Employee'; const MDM_POLICY_ID = -9999; @@ -59,29 +59,20 @@ export default async function EmployeeDetailsPage({ } const { fleetPolicies, device } = await getFleetPolicies(employee); + const memberDevice = await getMemberDevice(employee.id, orgId); return ( - - } - > - - + ); } @@ -232,16 +223,27 @@ const getFleetPolicies = async (member: Member & { user: User }) => { return { fleetPolicies: [ ...(host.policies || []), - ...(isMacOS ? [{ id: MDM_POLICY_ID, name: 'MDM Enabled', response: host.mdm.connected_to_fleet ? 'pass' : 'fail' }] : []), + ...(isMacOS + ? [ + { + id: MDM_POLICY_ID, + name: 'MDM Enabled', + response: host.mdm.connected_to_fleet ? 'pass' : 'fail', + }, + ] + : []), ].map((policy) => { const policyResult = results.find((result) => result.fleetPolicyId === policy.id); return { ...policy, - response: policy.response === 'pass' || policyResult?.fleetPolicyResponse === 'pass' ? 'pass' : 'fail', + response: + policy.response === 'pass' || policyResult?.fleetPolicyResponse === 'pass' + ? 'pass' + : 'fail', attachments: policyResult?.attachments || [], }; }), - device: host + device: host, }; } catch (error) { console.error( @@ -251,3 +253,50 @@ const getFleetPolicies = async (member: Member & { user: User }) => { return { fleetPolicies: [], device: null }; } }; + +const getMemberDevice = async ( + memberId: string, + organizationId: string, +): Promise => { + const device = await db.device.findFirst({ + where: { memberId, organizationId }, + include: { + member: { + include: { + user: { + select: { name: true, email: true }, + }, + }, + }, + }, + orderBy: { installedAt: 'desc' }, + }); + + if (!device) { + return null; + } + + return { + id: device.id, + name: device.name, + hostname: device.hostname, + platform: device.platform as 'macos' | 'windows' | 'linux', + osVersion: device.osVersion, + serialNumber: device.serialNumber, + hardwareModel: device.hardwareModel, + isCompliant: device.isCompliant, + diskEncryptionEnabled: device.diskEncryptionEnabled, + antivirusEnabled: device.antivirusEnabled, + passwordPolicySet: device.passwordPolicySet, + screenLockEnabled: device.screenLockEnabled, + checkDetails: (device.checkDetails as CheckDetails) ?? null, + lastCheckIn: device.lastCheckIn?.toISOString() ?? null, + agentVersion: device.agentVersion, + installedAt: device.installedAt.toISOString(), + user: { + name: device.member.user.name, + email: device.member.user.email, + }, + source: 'device_agent' as const, + }; +}; diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx index 6f94b1b737..fced7f07ce 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx @@ -48,6 +48,8 @@ interface MemberRowProps { onReactivate: (memberId: string) => void; canEdit: boolean; isCurrentUserOwner: boolean; + taskCompletion?: { completed: number; total: number }; + hasDeviceAgentDevice?: boolean; } function getInitials(name?: string | null, email?: string | null): string { @@ -97,6 +99,8 @@ export function MemberRow({ onReactivate, canEdit, isCurrentUserOwner, + taskCompletion, + hasDeviceAgentDevice, }: MemberRowProps) { const { orgId } = useParams<{ orgId: string }>(); @@ -222,6 +226,23 @@ export function MemberRow({ + {/* TASKS */} + + {taskCompletion ? ( + + {taskCompletion.completed}/{taskCompletion.total} + + ) : ( + + — + + )} + + {/* ACTIONS */}
@@ -247,7 +268,7 @@ export function MemberRow({ Edit Roles )} - {!isDeactivated && member.fleetDmLabelId && isCurrentUserOwner && ( + {!isDeactivated && (member.fleetDmLabelId || hasDeviceAgentDevice) && isCurrentUserOwner && ( { setDropdownOpen(false); diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/PendingInvitationRow.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/PendingInvitationRow.tsx index ea5c8ab892..16f5d50458 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/PendingInvitationRow.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/PendingInvitationRow.tsx @@ -10,14 +10,11 @@ import { AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, - Avatar, - AvatarFallback, Badge, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, - HStack, TableCell, TableRow, Text, @@ -80,14 +77,9 @@ export function PendingInvitationRow({ {/* NAME */} - - - {invitation.email.charAt(0).toUpperCase()} - -
- {invitation.email} -
-
+
+ {invitation.email} +
{/* STATUS */} @@ -106,6 +98,13 @@ export function PendingInvitationRow({
+ {/* TASKS */} + + + — + + + {/* ACTIONS */}
diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx index dc45fc89c0..6440ee5f2f 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx @@ -1,5 +1,6 @@ 'use server'; +import { trainingVideos as trainingVideosData } from '@/lib/data/training-videos'; import { auth } from '@/utils/auth'; import type { Invitation, Member, User } from '@db'; import { db } from '@db'; @@ -76,6 +77,76 @@ export async function TeamMembers(props: TeamMembersProps) { // Fetch employee sync connections server-side const employeeSyncData = await getEmployeeSyncConnections(organizationId); + // Build task completion map for employees/contractors + const taskCompletionMap: Record = {}; + + const employeeMembers = members.filter((member) => { + const roles = member.role.includes(',') + ? member.role.split(',').map((r) => r.trim()) + : [member.role]; + return roles.includes('employee') || roles.includes('contractor'); + }); + + // Build a set of member IDs that have device-agent devices + const memberIds = members.map((m) => m.id); + const devicesForMembers = await db.device.findMany({ + where: { + organizationId, + memberId: { in: memberIds }, + }, + select: { memberId: true }, + }); + const memberIdsWithDeviceAgent = [ + ...new Set(devicesForMembers.map((d) => d.memberId)), + ]; + + if (employeeMembers.length > 0) { + // Fetch org settings to know which steps are enabled + const org = await db.organization.findUnique({ + where: { id: organizationId }, + select: { securityTrainingStepEnabled: true }, + }); + + // Fetch required policies + const policies = await db.policy.findMany({ + where: { + organizationId, + isRequiredToSign: true, + status: 'published', + isArchived: false, + }, + }); + + // Fetch training video completions (only if training is enabled) + const employeeIds = employeeMembers.map((m) => m.id); + const trainingCompletions = org?.securityTrainingStepEnabled + ? await db.employeeTrainingVideoCompletion.findMany({ + where: { + memberId: { in: employeeIds }, + }, + }) + : []; + + const totalPolicies = policies.length; + const totalTrainingVideos = org?.securityTrainingStepEnabled ? trainingVideosData.length : 0; + const totalTasks = totalPolicies + totalTrainingVideos; + + for (const employee of employeeMembers) { + const policiesCompleted = policies.filter((p) => p.signedBy.includes(employee.id)).length; + + const trainingsCompleted = org?.securityTrainingStepEnabled + ? trainingCompletions.filter( + (tc) => tc.memberId === employee.id && tc.completedAt !== null, + ).length + : 0; + + taskCompletionMap[employee.id] = { + completed: policiesCompleted + trainingsCompleted, + total: totalTasks, + }; + } + } + return ( ); } diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx index 767e439136..487e00af6e 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx @@ -56,6 +56,8 @@ interface TeamMembersClientProps { isAuditor: boolean; isCurrentUserOwner: boolean; employeeSyncData: EmployeeSyncConnectionsData; + taskCompletionMap: Record; + memberIdsWithDeviceAgent: string[]; } // Define a simplified type for merged list items @@ -81,6 +83,8 @@ export function TeamMembersClient({ isAuditor, isCurrentUserOwner, employeeSyncData, + taskCompletionMap, + memberIdsWithDeviceAgent, }: TeamMembersClientProps) { const router = useRouter(); const [searchQuery, setSearchQuery] = useState(''); @@ -506,6 +510,7 @@ export function TeamMembersClient({ NAME STATUS ROLE + TASKS ACTIONS @@ -521,6 +526,10 @@ export function TeamMembersClient({ onReactivate={handleReactivateMember} canEdit={canManageMembers} isCurrentUserOwner={isCurrentUserOwner} + taskCompletion={taskCompletionMap[(item as MemberWithUser).id]} + hasDeviceAgentDevice={memberIdsWithDeviceAgent.includes( + (item as MemberWithUser).id, + )} /> ) : ( = { + macos: 'macOS', + windows: 'Windows', + linux: 'Linux', +}; + +function formatTimeAgo(dateString: string | null): string { + if (!dateString) return 'Never'; + const date = new Date(dateString); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); + + if (diffHours < 1) return 'Just now'; + if (diffHours < 24) return `${diffHours}h ago`; + const diffDays = Math.floor(diffHours / 24); + return `${diffDays}d ago`; +} + +/** Device is considered online if it checked in within the last 2 hours */ +function isDeviceOnline(lastCheckIn: string | null): boolean { + if (!lastCheckIn) return false; + const diffMs = Date.now() - new Date(lastCheckIn).getTime(); + return diffMs < 2 * 60 * 60 * 1000; +} + +function UserNameCell({ device }: { device: DeviceWithChecks }) { + const params = useParams(); + const orgId = params?.orgId as string; + const memberId = device.memberId; + + if (!memberId) { + return ( +
+ {device.user.name} + {device.user.email} +
+ ); + } + + return ( +
+ e.stopPropagation()} + > + {device.user.name} + + {device.user.email} +
+ ); +} + +export const DeviceAgentDevicesList = ({ devices }: DeviceAgentDevicesListProps) => { + const [selectedDevice, setSelectedDevice] = useState(null); + const [searchQuery, setSearchQuery] = useState(''); + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(50); + + const filteredDevices = useMemo(() => { + if (!searchQuery) return devices; + const query = searchQuery.toLowerCase(); + return devices.filter( + (device) => + device.name.toLowerCase().includes(query) || + device.user.name.toLowerCase().includes(query) || + device.user.email.toLowerCase().includes(query) || + device.platform.toLowerCase().includes(query), + ); + }, [devices, searchQuery]); + + const pageCount = Math.max(1, Math.ceil(filteredDevices.length / perPage)); + const paginatedDevices = useMemo(() => { + const start = (page - 1) * perPage; + return filteredDevices.slice(start, start + perPage); + }, [filteredDevices, page, perPage]); + + if (selectedDevice) { + return setSelectedDevice(null)} />; + } + + if (devices.length === 0) { + return null; + } + + return ( + +
+ + + + + { + setSearchQuery(e.target.value); + setPage(1); + }} + /> + +
+ + {filteredDevices.length === 0 ? ( + + + No devices found + Try adjusting your search. + + + ) : ( + { + setPerPage(size); + setPage(1); + }, + }} + > + + + Device Name + User + Platform + Last Check-in + Checks + Compliant + + + + {paginatedDevices.map((device) => ( + setSelectedDevice(device)} + style={{ cursor: 'pointer' }} + > + +
+ + + {device.name} + +
+
+ + + + +
+ {PLATFORM_LABELS[device.platform] ?? device.platform} + + {device.osVersion} + +
+
+ + {formatTimeAgo(device.lastCheckIn)} + + +
+ {CHECK_FIELDS.map(({ key, label }) => ( + + {label} + + ))} +
+
+ + + {device.isCompliant ? 'Yes' : 'No'} + + +
+ ))} +
+
+ )} +
+ ); +}; diff --git a/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceComplianceChart.tsx b/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceComplianceChart.tsx index 13d2cec06d..ec285252d0 100644 --- a/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceComplianceChart.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceComplianceChart.tsx @@ -97,7 +97,6 @@ export function DeviceComplianceChart({ devices }: DeviceComplianceChartProps) { Device Compliance - {/* Optional: Add a subtitle or small description here if needed */} diff --git a/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceDetails.tsx b/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceDetails.tsx new file mode 100644 index 0000000000..e56f08a67c --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceDetails.tsx @@ -0,0 +1,190 @@ +'use client'; + +import { + Badge, + Button, + Card, + CardContent, + CardHeader, + Stack, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + Text, +} from '@trycompai/design-system'; +import { ArrowLeft } from '@trycompai/design-system/icons'; +import type { DeviceWithChecks } from '../types'; + +const CHECK_FIELDS = [ + { key: 'diskEncryptionEnabled' as const, dbKey: 'disk_encryption', label: 'Disk Encryption' }, + { key: 'antivirusEnabled' as const, dbKey: 'antivirus', label: 'Antivirus' }, + { key: 'passwordPolicySet' as const, dbKey: 'password_policy', label: 'Password Policy' }, + { key: 'screenLockEnabled' as const, dbKey: 'screen_lock', label: 'Screen Lock' }, +]; + +const PLATFORM_LABELS: Record = { + macos: 'macOS', + windows: 'Windows', + linux: 'Linux', +}; + +/** Device is considered online if it checked in within the last 2 hours */ +function isDeviceOnline(lastCheckIn: string | null): boolean { + if (!lastCheckIn) return false; + const diffMs = Date.now() - new Date(lastCheckIn).getTime(); + return diffMs < 2 * 60 * 60 * 1000; +} + +interface DeviceDetailsProps { + device: DeviceWithChecks; + onClose: () => void; +} + +export const DeviceDetails = ({ device, onClose }: DeviceDetailsProps) => { + return ( + +
+ +
+ + + +
+
+
+ + + {device.name} + + + {isDeviceOnline(device.lastCheckIn) ? 'Online' : 'Offline'} + + {device.source === 'fleet' && Fleet (Legacy)} +
+ + {PLATFORM_LABELS[device.platform] ?? device.platform} {device.osVersion} + {device.hardwareModel ? ` \u2022 ${device.hardwareModel}` : ''} + +
+ + {device.isCompliant ? 'Compliant' : 'Non-Compliant'} + +
+
+ +
+
+ + User + + + {device.user.name} + + + {device.user.email} + +
+
+ + Hostname + + + {device.hostname} + +
+
+ + Serial Number + + + {device.serialNumber ?? 'N/A'} + +
+
+ + Last Check-in + + + {device.lastCheckIn ? new Date(device.lastCheckIn).toLocaleString() : 'Never'} + +
+
+ + Agent Version + + + {device.agentVersion ?? 'N/A'} + +
+
+ + Installed + + + {new Date(device.installedAt).toLocaleDateString()} + +
+
+
+
+ + + + + Check + Details + Result + Exception + + + + {CHECK_FIELDS.map(({ key, dbKey, label }) => { + const isFleetUnsupported = device.source === 'fleet' && key !== 'diskEncryptionEnabled'; + const passed = device[key]; + const details = device.checkDetails?.[dbKey]; + return ( + + + + {label} + + + + + {isFleetUnsupported ? 'Not tracked by Fleet' : (details?.message ?? '—')} + + + + {isFleetUnsupported ? ( + N/A + ) : ( + + {passed ? 'Pass' : 'Fail'} + + )} + + + + {details?.exception ?? '—'} + + + + ); + })} + +
+
+ ); +}; diff --git a/apps/app/src/app/(app)/[orgId]/people/devices/data/index.ts b/apps/app/src/app/(app)/[orgId]/people/devices/data/index.ts index 8fc65b2d1d..01277d8f5a 100644 --- a/apps/app/src/app/(app)/[orgId]/people/devices/data/index.ts +++ b/apps/app/src/app/(app)/[orgId]/people/devices/data/index.ts @@ -4,11 +4,70 @@ import { getFleetInstance } from '@/lib/fleet'; import { auth } from '@/utils/auth'; import { db } from '@db'; import { headers } from 'next/headers'; +import type { CheckDetails, DeviceWithChecks } from '../types'; import type { Host } from '../types'; const MDM_POLICY_ID = -9999; -export const getEmployeeDevices: () => Promise = async () => { +/** + * Fetches device-agent devices from the DB for the current organization. + */ +export const getEmployeeDevicesFromDB: () => Promise = async () => { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + const organizationId = session?.session.activeOrganizationId; + + if (!organizationId) { + return []; + } + + const devices = await db.device.findMany({ + where: { organizationId }, + include: { + member: { + include: { + user: { + select: { name: true, email: true }, + }, + }, + }, + }, + orderBy: { installedAt: 'desc' }, + }); + + return devices.map((device) => ({ + id: device.id, + name: device.name, + hostname: device.hostname, + platform: device.platform as 'macos' | 'windows' | 'linux', + osVersion: device.osVersion, + serialNumber: device.serialNumber, + hardwareModel: device.hardwareModel, + isCompliant: device.isCompliant, + diskEncryptionEnabled: device.diskEncryptionEnabled, + antivirusEnabled: device.antivirusEnabled, + passwordPolicySet: device.passwordPolicySet, + screenLockEnabled: device.screenLockEnabled, + checkDetails: (device.checkDetails as CheckDetails) ?? null, + lastCheckIn: device.lastCheckIn?.toISOString() ?? null, + agentVersion: device.agentVersion, + installedAt: device.installedAt.toISOString(), + memberId: device.memberId, + user: { + name: device.member.user.name, + email: device.member.user.email, + }, + source: 'device_agent' as const, + })); +}; + +/** + * Fetches Fleet (legacy) devices for the current organization. + * Returns Host[] exactly as main branch — untouched Fleet logic. + */ +export const getFleetHosts: () => Promise = async () => { const session = await auth.api.getSession({ headers: await headers(), }); @@ -52,7 +111,7 @@ export const getEmployeeDevices: () => Promise = async () => { })), ); - // Get all devices by id. in parallel + // Get all devices by id in parallel const devices = await Promise.all(hostRequests.map(({ hostId }) => fleet.get(`/hosts/${hostId}`))); const userIds = hostRequests.map(({ userId }) => userId); const memberIds = hostRequests.map(({ memberId }) => memberId); diff --git a/apps/app/src/app/(app)/[orgId]/people/devices/types/index.ts b/apps/app/src/app/(app)/[orgId]/people/devices/types/index.ts index 64d27eff3b..82ab009768 100644 --- a/apps/app/src/app/(app)/[orgId]/people/devices/types/index.ts +++ b/apps/app/src/app/(app)/[orgId]/people/devices/types/index.ts @@ -1,3 +1,40 @@ +export type CheckDetailEntry = { + method?: string; + raw?: string; + message?: string; + exception?: string; + passed?: boolean; + checkedAt?: string; +}; + +export type CheckDetails = Record; + +export interface DeviceWithChecks { + id: string; + name: string; + hostname: string; + platform: 'macos' | 'windows' | 'linux'; + osVersion: string; + serialNumber: string | null; + hardwareModel: string | null; + isCompliant: boolean; + diskEncryptionEnabled: boolean; + antivirusEnabled: boolean; + passwordPolicySet: boolean; + screenLockEnabled: boolean; + checkDetails: CheckDetails | null; + lastCheckIn: string | null; + agentVersion: string | null; + installedAt: string; + memberId?: string; + user: { + name: string; + email: string; + }; + /** Indicates which system reported this device */ + source: 'device_agent' | 'fleet'; +} + export interface FleetPolicy { id: number; name: string; @@ -11,8 +48,8 @@ export interface FleetPolicy { resolution?: string; platform?: string; calendar_events_enabled?: boolean; - created_at?: string; // ISO date-time string - updated_at?: string; // ISO date-time string + created_at?: string; + updated_at?: string; response: string; attachments?: string[]; } diff --git a/apps/app/src/app/(app)/[orgId]/people/page.tsx b/apps/app/src/app/(app)/[orgId]/people/page.tsx index 0af12e6b1a..80fc2a94f4 100644 --- a/apps/app/src/app/(app)/[orgId]/people/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/page.tsx @@ -10,8 +10,10 @@ import { TeamMembers } from './all/components/TeamMembers'; import { PeoplePageTabs } from './components/PeoplePageTabs'; import { EmployeesOverview } from './dashboard/components/EmployeesOverview'; import { DeviceComplianceChart } from './devices/components/DeviceComplianceChart'; +import { DeviceAgentDevicesList } from './devices/components/DeviceAgentDevicesList'; import { EmployeeDevicesList } from './devices/components/EmployeeDevicesList'; -import { getEmployeeDevices } from './devices/data'; +import { getEmployeeDevicesFromDB, getFleetHosts } from './devices/data'; +import type { DeviceWithChecks } from './devices/types'; import type { Host } from './devices/types'; import { OrgChartContent } from './org-chart/components/OrgChartContent'; @@ -117,16 +119,36 @@ export default async function PeoplePage({ params }: { params: Promise<{ orgId: }; } - // Fetch devices data - let devices: Host[] = []; - try { - const fetchedDevices = await getEmployeeDevices(); - devices = fetchedDevices || []; - } catch (error) { - console.error('Error fetching employee devices:', error); - devices = []; + // Fetch devices from both sources independently — one failing shouldn't break the other + let agentDevices: DeviceWithChecks[] = []; + let fleetDevices: Host[] = []; + + const [agentResult, fleetResult] = await Promise.allSettled([ + getEmployeeDevicesFromDB(), + getFleetHosts(), + ]); + + if (agentResult.status === 'fulfilled') { + agentDevices = agentResult.value; + } else { + console.error('Error fetching device agent devices:', agentResult.reason); + } + + if (fleetResult.status === 'fulfilled') { + fleetDevices = fleetResult.value || []; + } else { + console.error('Error fetching Fleet devices:', fleetResult.reason); } + // Filter out Fleet hosts for members who already have device-agent devices + // Device agent takes priority over Fleet + const memberIdsWithAgent = new Set( + agentDevices.map((d) => d.memberId).filter(Boolean), + ); + const filteredFleetDevices = fleetDevices.filter( + (host) => !host.member_id || !memberIdsWithAgent.has(host.member_id), + ); + return ( : null} devicesContent={ - <> - - - +
+ {/* Device Agent devices (new system) */} + {agentDevices.length > 0 && ( + + )} + + {/* Fleet devices (legacy) — shown exactly as main branch */} + + +
} orgChartContent={ | null; }; +interface DeviceStatus { + id: string; + isCompliant: boolean; +} + interface EmployeeTasksListProps { organizationId: string; policies: PolicyWithVersion[]; @@ -31,39 +37,69 @@ export const EmployeeTasksList = ({ policies, trainingVideos: trainingVideoCompletions, member, - fleetPolicies, - host, + fleetPolicies: initialFleetPolicies, + host: initialHost, deviceAgentStepEnabled, securityTrainingStepEnabled, }: EmployeeTasksListProps) => { - const { data: response, isValidating, mutate: fetchFleetPolicies } = useSWR<{ device: Host | null; fleetPolicies: FleetPolicy[] }>( - `/api/fleet-policies?organizationId=${organizationId}`, - async (url) => { + // Fetch device-agent status filtered by current org + const { data: deviceData } = useSWR<{ devices: DeviceStatus[] }>( + `/api/device-agent/status?organizationId=${organizationId}`, + async (url: string) => { const res = await fetch(url); if (!res.ok) throw new Error('Failed to fetch'); return res.json(); }, { - fallbackData: { device: host, fleetPolicies }, + fallbackData: { devices: [] }, refreshInterval: 0, revalidateOnFocus: false, - revalidateOnMount: false + revalidateOnMount: true, }, ); - if (!response) { - return null; - } + // Fetch fleet policies via SWR (only if member has a fleet device) + const hasFleetDevice = initialHost !== null; + const { + data: fleetResponse, + isValidating: isFleetLoading, + mutate: fetchFleetPolicies, + } = useSWR<{ device: Host | null; fleetPolicies: FleetPolicy[] }>( + hasFleetDevice ? `/api/fleet-policies?organizationId=${organizationId}` : null, + async (url: string) => { + const res = await fetch(url); + if (!res.ok) throw new Error('Failed to fetch'); + return res.json(); + }, + { + fallbackData: { device: initialHost, fleetPolicies: initialFleetPolicies }, + refreshInterval: 0, + revalidateOnFocus: false, + revalidateOnMount: false, + }, + ); + + const devices = deviceData?.devices ?? []; + const fleetPolicies = fleetResponse?.fleetPolicies ?? initialFleetPolicies; + const host = fleetResponse?.device ?? initialHost; // Check completion status const hasAcceptedPolicies = policies.length === 0 || policies.every((p) => p.signedBy.includes(member.id)); - const hasInstalledAgent = response.device !== null; + + // Device-agent takes priority: if installed, only its compliance matters. + // Fleet is only checked when device-agent is not present. + const hasDeviceAgentDevices = devices.length > 0; + const allDeviceAgentCompliant = devices.length > 0 && devices.every((d) => d.isCompliant); + const hasInstalledFleetAgent = host !== null; const allFleetPoliciesPass = - response.fleetPolicies.length === 0 || response.fleetPolicies.every((policy) => policy.response === 'pass'); - const hasCompletedDeviceSetup = hasInstalledAgent && allFleetPoliciesPass; + fleetPolicies.length === 0 || fleetPolicies.every((policy) => policy.response === 'pass'); - // Calculate general training completion (matching logic from GeneralTrainingAccordionItem) + const hasCompletedDeviceSetup = hasDeviceAgentDevices + ? allDeviceAgentCompliant + : hasInstalledFleetAgent && allFleetPoliciesPass; + + // Calculate general training completion const generalTrainingVideoIds = trainingVideos .filter((video) => video.id.startsWith('sat-')) .map((video) => video.id); @@ -93,10 +129,11 @@ export const EmployeeTasksList = ({ title: 'Download and install Comp AI Device Agent', content: ( ), @@ -136,35 +173,27 @@ export const EmployeeTasksList = ({ } return ( -
- - - Overview - - Please complete the following tasks to stay compliant and secure. - - - - {/* Progress indicator */} -
-
- {completedCount} of {accordionItems.length} tasks completed -
-
-
-
+ }> +
+ {/* Progress indicator */} +
+
+ {completedCount} of {accordionItems.length} tasks completed
+
+
+
+
- - {accordionItems.map((item, idx) => ( -
{item.content}
- ))} -
- - -
+ + {accordionItems.map((item, idx) => ( +
{item.content}
+ ))} +
+
+ ); }; diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/components/OrganizationDashboard.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/components/OrganizationDashboard.tsx index fc55604271..a3779fee4c 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/components/OrganizationDashboard.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/components/OrganizationDashboard.tsx @@ -12,7 +12,7 @@ interface MemberWithUserOrg extends Member { interface OrganizationDashboardProps { organizationId: string; - member: MemberWithUserOrg; // Pass the full member object for user info etc. + member: MemberWithUserOrg; fleetPolicies: FleetPolicy[]; host: Host | null; } @@ -24,11 +24,10 @@ export async function OrganizationDashboard({ host, }: OrganizationDashboardProps) { // Fetch policies specific to the selected organization - // Include currentVersion to get the published version's content and pdfUrl const policies = await db.policy.findMany({ where: { organizationId: organizationId, - isRequiredToSign: true, // Keep original logic for required policies + isRequiredToSign: true, status: 'published', }, include: { @@ -44,28 +43,13 @@ export async function OrganizationDashboard({ }); // Fetch training video completions specific to the member - // Note: The original fetched *all* completions for the member, regardless of org - // If videos are org-specific, the schema/query might need adjustment const trainingVideos = await db.employeeTrainingVideoCompletion.findMany({ where: { memberId: member.id, - // Add organizationId filter if EmployeeTrainingVideoCompletion has it - // organizationId: organizationId, }, - // Include video details if needed by EmployeeTasksList - // include: { trainingVideo: true } }); - console.log('[OrganizationDashboard] Training videos fetched:', { - memberId: member.id, - count: trainingVideos.length, - videos: trainingVideos.map((v) => ({ - videoId: v.videoId, - completedAt: v.completedAt, - })), - }); - - // Get Org first to get the label id. + // Get Org first to verify it exists const org = await db.organization.findUnique({ where: { id: organizationId, @@ -76,13 +60,12 @@ export async function OrganizationDashboard({ return ; } - // Display tasks without welcome message for cleaner UI return ( { showSaveStatus={false} showWordCount={false} showToolbar={false} - minHeight="500px" - className="max-w-screen-lg mx-auto sm:mb-[calc(20vh)]" + minHeight="200px" + className="max-w-none" /> ); }; diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/components/policy/PolicyCard.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/components/policy/PolicyCard.tsx index 55d5cc8735..09944d40d9 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/components/policy/PolicyCard.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/components/policy/PolicyCard.tsx @@ -1,6 +1,5 @@ 'use client'; -import { Button } from '@comp/ui/button'; import { Card, CardContent, @@ -11,7 +10,8 @@ import { } from '@comp/ui/card'; import type { Member, Policy, PolicyVersion } from '@db'; import type { JSONContent } from '@tiptap/react'; -import { ArrowRight, Check } from 'lucide-react'; +import { Button } from '@trycompai/design-system'; +import { ArrowRight, Checkmark } from '@trycompai/design-system/icons'; import { useState } from 'react'; import { PolicyEditor } from './PolicyEditor'; import { PortalPdfViewer } from './PortalPdfViewer'; @@ -47,17 +47,16 @@ export function PolicyCard({ policy, onNext, onComplete, member, isLastPolicy }: {isAccepted && (
- +

Policy Accepted

You have accepted this policy

- {!isLastPolicy && ( - )}
diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/components/policy/PolicyCarousel.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/components/policy/PolicyCarousel.tsx index 6b9863cdf9..95f2a8a358 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/components/policy/PolicyCarousel.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/components/policy/PolicyCarousel.tsx @@ -1,8 +1,8 @@ 'use client'; -import { Button } from '@comp/ui/button'; import type { Member, Policy, PolicyVersion } from '@db'; -import { ChevronLeft, ChevronRight } from 'lucide-react'; +import { Button } from '@trycompai/design-system'; +import { ChevronLeft, ChevronRight } from '@trycompai/design-system/icons'; import { useAction } from 'next-safe-action/hooks'; import { useEffect, useRef, useState } from 'react'; import { toast } from 'sonner'; @@ -110,7 +110,7 @@ export function PolicyCarousel({ onClick={handlePrevious} disabled={currentIndex === 0} > - + Policy {currentIndex + 1} of {policies.length} @@ -121,7 +121,7 @@ export function PolicyCarousel({ onClick={handleNext} disabled={currentIndex === policies.length - 1} > - +
diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/components/policy/PolicyContainer.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/components/policy/PolicyContainer.tsx index 4fa5b0d83f..6dc78567ed 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/components/policy/PolicyContainer.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/components/policy/PolicyContainer.tsx @@ -1,8 +1,8 @@ 'use client'; -import { Button } from '@comp/ui/button'; import type { Member, Policy, PolicyVersion } from '@db'; -import { ArrowLeft } from 'lucide-react'; +import { Button } from '@trycompai/design-system'; +import { ArrowLeft } from '@trycompai/design-system/icons'; import { useState } from 'react'; import { PolicyCarousel } from './PolicyCarousel'; import { PolicyGrid } from './PolicyGrid'; @@ -35,8 +35,12 @@ export function PolicyContainer({ policies, member }: PolicyContainerProps) { return (
-

diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/components/policy/PolicyGrid.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/components/policy/PolicyGrid.tsx index 7c69f54b52..189e844cb1 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/components/policy/PolicyGrid.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/components/policy/PolicyGrid.tsx @@ -2,7 +2,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@comp/ui/card'; import type { Member, Policy, PolicyVersion } from '@db'; -import { Check } from 'lucide-react'; +import { Checkmark } from '@trycompai/design-system/icons'; type PolicyWithVersion = Policy & { currentVersion?: Pick | null; @@ -46,7 +46,7 @@ export function PolicyGrid({ policies, onPolicyClick, member }: PolicyGridProps) > {isCompleted && (

- +
)} diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/components/policy/PortalPdfViewer.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/components/policy/PortalPdfViewer.tsx index 6396da781e..254486fb28 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/components/policy/PortalPdfViewer.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/components/policy/PortalPdfViewer.tsx @@ -1,7 +1,8 @@ 'use client'; import { Card, CardContent } from '@comp/ui/card'; -import { FileText, Loader2 } from 'lucide-react'; +import { Spinner } from '@trycompai/design-system'; +import { Document } from '@trycompai/design-system/icons'; import { useAction } from 'next-safe-action/hooks'; import { useEffect, useState } from 'react'; import { toast } from 'sonner'; @@ -42,7 +43,7 @@ export function PortalPdfViewer({ policyId, s3Key, versionId }: PortalPdfViewerP if (isLoading) { return (
- +
); } @@ -62,7 +63,7 @@ export function PortalPdfViewer({ policyId, s3Key, versionId }: PortalPdfViewerP return ( - +

PDF Document Not Available

This policy is stored as a PDF, but it could not be loaded. diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/DeviceAgentAccordionItem.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/DeviceAgentAccordionItem.tsx index 70b1417763..ae4ed20eae 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/DeviceAgentAccordionItem.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/DeviceAgentAccordionItem.tsx @@ -1,130 +1,104 @@ 'use client'; -import { - MAC_APPLE_SILICON_FILENAME, - MAC_INTEL_FILENAME, - WINDOWS_FILENAME, -} from '@/app/api/download-agent/constants'; -import { detectOSFromUserAgent, SupportedOS } from '@/utils/os'; +import { detectOSFromUserAgent, type SupportedOS } from '@/utils/os'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@comp/ui/accordion'; -import { Button } from '@comp/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@comp/ui/card'; import { cn } from '@comp/ui/cn'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@comp/ui/select'; import type { Member } from '@db'; -import { CheckCircle2, Circle, Download, Loader2, RefreshCw } from 'lucide-react'; -import { useEffect, useMemo, useState } from 'react'; -import { toast } from 'sonner'; -import { FleetPolicyItem } from './FleetPolicyItem'; +import { Badge, Button } from '@trycompai/design-system'; +import { CheckmarkFilled, CircleDash, Download, Renew } from '@trycompai/design-system/icons'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import type { FleetPolicy, Host } from '../../types'; +import { FleetPolicyItem } from './FleetPolicyItem'; + +interface DeviceStatus { + id: string; + name: string; + hostname: string; + platform: string; + osVersion: string; + isCompliant: boolean; + diskEncryptionEnabled: boolean; + antivirusEnabled: boolean; + passwordPolicySet: boolean; + screenLockEnabled: boolean; + checkDetails: Record | null; + lastCheckIn: string | null; +} + +const CHECK_FIELDS = [ + { key: 'diskEncryptionEnabled' as const, dbKey: 'disk_encryption', label: 'Disk Encryption' }, + { key: 'antivirusEnabled' as const, dbKey: 'antivirus', label: 'Antivirus' }, + { key: 'passwordPolicySet' as const, dbKey: 'password_policy', label: 'Password Policy' }, + { key: 'screenLockEnabled' as const, dbKey: 'screen_lock', label: 'Screen Lock' }, +]; interface DeviceAgentAccordionItemProps { + organizationId: string; member: Member; host: Host | null; - isLoading: boolean; - fleetPolicies?: FleetPolicy[]; + fleetPolicies: FleetPolicy[]; + isFleetLoading: boolean; fetchFleetPolicies: () => void; } export function DeviceAgentAccordionItem({ + organizationId, member, host, - isLoading, - fleetPolicies = [], + fleetPolicies, + isFleetLoading, fetchFleetPolicies, }: DeviceAgentAccordionItemProps) { - const [isDownloading, setIsDownloading] = useState(false); const [detectedOS, setDetectedOS] = useState(null); + const [isLoadingDeviceAgent, setIsLoadingDeviceAgent] = useState(true); + const [isDownloading, setIsDownloading] = useState(false); + const [devices, setDevices] = useState([]); const isMacOS = useMemo( () => detectedOS === 'macos' || detectedOS === 'macos-intel', [detectedOS], ); + const isLinux = useMemo(() => detectedOS === 'linux', [detectedOS]); - const hasInstalledAgent = host !== null; - const failedPoliciesCount = useMemo(() => fleetPolicies.filter((policy) => policy.response !== 'pass').length, [fleetPolicies]); - - const isCompleted = hasInstalledAgent && failedPoliciesCount === 0; + // Device-agent state + const hasDeviceAgentDevices = devices.length > 0; + const allDeviceAgentCompliant = devices.length > 0 && devices.every((d) => d.isCompliant); + const failingDeviceAgentChecks = devices.reduce( + (count, device) => + count + CHECK_FIELDS.filter(({ key }) => !device[key]).length, + 0, + ); - const handleDownload = async () => { - if (!detectedOS) { - toast.error('Could not detect your OS. Please refresh and try again.'); - return; - } + // Fleet state + const hasFleetDevice = host !== null; + const failedFleetPolicies = useMemo( + () => fleetPolicies.filter((policy) => policy.response !== 'pass').length, + [fleetPolicies], + ); - setIsDownloading(true); + // Device-agent takes priority: if installed, only its compliance matters. + // Fleet is only checked when device-agent is not present. + const isCompleted = hasDeviceAgentDevices + ? allDeviceAgentCompliant + : hasFleetDevice && failedFleetPolicies === 0; + const fetchDeviceStatus = useCallback(async () => { + setIsLoadingDeviceAgent(true); try { - // First, we need to get a download token/session from the API - const tokenResponse = await fetch('/api/download-agent/token', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - orgId: member.organizationId, - employeeId: member.id, - os: detectedOS, - }), - }); - - if (!tokenResponse.ok) { - const errorText = await tokenResponse.text(); - throw new Error(errorText || 'Failed to prepare download.'); - } - - const { token } = await tokenResponse.json(); - - // Now trigger the actual download using the browser's native download mechanism - // This will show in the browser's download UI immediately - const downloadUrl = `/api/download-agent?token=${encodeURIComponent(token)}`; - - // Method 1: Using a temporary link (most reliable) - const a = document.createElement('a'); - a.href = downloadUrl; - - // Set filename based on OS and architecture - if (isMacOS) { - a.download = detectedOS === 'macos' ? MAC_APPLE_SILICON_FILENAME : MAC_INTEL_FILENAME; - } else { - a.download = WINDOWS_FILENAME; + const response = await fetch( + `/api/device-agent/status?organizationId=${encodeURIComponent(organizationId)}`, + ); + if (response.ok) { + const data = await response.json(); + setDevices(data.devices || []); } - - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - - toast.success('Download started! Check your downloads folder.'); } catch (error) { - console.error(error); - toast.error(error instanceof Error ? error.message : 'Failed to download agent.'); + console.error('Failed to fetch device status:', error); } finally { - // Reset after a short delay to allow download to start - setTimeout(() => { - setIsDownloading(false); - }, 1000); - } - }; - - const getButtonContent = () => { - if (isDownloading) { - return ( - <> - - Downloading... - - ); - } else { - return ( - <> - - Download Agent - - ); + setIsLoadingDeviceAgent(false); } - }; - - const handleRefresh = () => { - fetchFleetPolicies(); - }; + }, [organizationId]); useEffect(() => { const detectOS = async () => { @@ -132,120 +106,145 @@ export function DeviceAgentAccordionItem({ setDetectedOS(os); }; detectOS(); - }, []); + fetchDeviceStatus(); + }, [fetchDeviceStatus]); + + const handleRefreshFleet = () => { + fetchFleetPolicies(); + }; + + const handleDownload = async () => { + setIsDownloading(true); + try { + const res = await fetch('/api/download-agent/token', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ orgId: organizationId, employeeId: member.id, os: detectedOS }), + }); + if (!res.ok) throw new Error('Failed to get download token'); + const { token } = await res.json(); + window.location.href = `/api/download-agent?token=${token}`; + } catch (e) { + console.error('Download failed:', e); + } finally { + setIsDownloading(false); + } + }; + + // Determine what kind of failure indicator to show in the accordion trigger + const failureIndicator = useMemo(() => { + if (hasDeviceAgentDevices && failingDeviceAgentChecks > 0) { + return `${failingDeviceAgentChecks} check${failingDeviceAgentChecks !== 1 ? 's' : ''} failing`; + } + if (!hasDeviceAgentDevices && hasFleetDevice && failedFleetPolicies > 0) { + return `${failedFleetPolicies} ${failedFleetPolicies !== 1 ? 'policies' : 'policy'} failing`; + } + return null; + }, [hasDeviceAgentDevices, failingDeviceAgentChecks, hasFleetDevice, failedFleetPolicies]); return (

{isCompleted ? ( - + ) : ( - + )} Download and install Comp AI Device Agent - {hasInstalledAgent && failedPoliciesCount > 0 && ( + {failureIndicator && ( - {failedPoliciesCount} policies failing + {failureIndicator} )}
-

+

Installing Comp AI Device Agent helps you and your security administrator keep your device protected against security threats.

- {!hasInstalledAgent ? ( + {/* THREE-WAY RENDERING */} + {hasDeviceAgentDevices ? ( + /* 1. Device-agent devices found -- show device check results */
-
    -
  1. - Download the Device Agent installer. -

    - Click the download button below to get the Device Agent installer. -

    -
    - {isMacOS && !hasInstalledAgent && ( - - )} - + + +
- -
  • - Install the Comp AI Device Agent -

    - {isMacOS - ? 'Double-click the downloaded DMG file and follow the installation instructions.' - : 'Double-click the downloaded EXE file and follow the installation instructions.'} -

    -
  • - {isMacOS ? ( -
  • - Login with your work email -

    - After installation, login with your work email, select your organization and - then click "Link Device" and "Install Agent". +

    + {CHECK_FIELDS.map(({ key, dbKey, label }) => { + const passed = device[key]; + const details = device.checkDetails?.[dbKey]; + return ( +
    +
    + {label} + {details?.message && ( +

    + {details.message} +

    + )} + {details?.exception && ( +

    + {details.exception} +

    + )} +
    + + {passed ? 'Pass' : 'Fail'} + +
    + ); + })} +
    + {device.lastCheckIn && ( +

    + Last check-in: {new Date(device.lastCheckIn).toLocaleString()}

    -
  • - ) : ( -
  • - Enable MDM -
    -

    - Find the Fleet Desktop app in your system tray (bottom right corner). Click - on it and click My Device. -

    -

    - You should see a banner that asks you to enable MDM. Click the button and - follow the instructions. -

    -

    - After you've enabled MDM, if you refresh the page, the banner will - disappear. Now your computer will automatically enable the necessary - settings on your computer in order to be compliant. -

    -
    -
  • - )} - + )} +
    + ))}
    - ) : ( + ) : hasFleetDevice ? ( + /* 2. Fleet device found (legacy) -- show fleet policy list */
    - {host.computer_name} + {host!.computer_name}
    @@ -257,7 +256,7 @@ export function DeviceAgentAccordionItem({ key={policy.id} policy={policy} organizationId={member.organizationId} - onRefresh={handleRefresh} + onRefresh={handleRefreshFleet} /> ))} @@ -268,12 +267,62 @@ export function DeviceAgentAccordionItem({ )}
    + ) : ( + /* 3. Neither -- show download instructions for new device agent */ +
    +
      +
    1. + Download the Device Agent installer. +

      + Download the latest Device Agent installer for your operating system. +

      +
      + +
      +

      + or{' '} + + download from GitHub + +

      +
    2. +
    3. + Install the Comp AI Device Agent +

      + {isMacOS + ? 'Double-click the downloaded DMG file and drag the app to your Applications folder.' + : isLinux + ? 'Install the downloaded .deb package using your package manager (e.g. sudo dpkg -i CompAI-Device-Agent.deb).' + : 'Double-click the downloaded EXE file and follow the installation instructions.'} +

      +
    4. +
    5. + Sign in with your work email +

      + After installation, the agent will appear in your system tray. Click it and sign + in with the same credentials you use for this portal. The agent will + automatically run compliance checks on your device. +

      +
    6. +
    +
    )}
    - {/* System Requirements */} System Requirements @@ -281,7 +330,8 @@ export function DeviceAgentAccordionItem({

    - Operating Systems: macOS 14+, Windows 10+ + Operating Systems: macOS 14+, Windows 10+, Debian 11+ / Ubuntu + 20.04+

    Memory: 512MB RAM minimum @@ -295,21 +345,25 @@ export function DeviceAgentAccordionItem({ - {/* About Comp AI Device Monitor */} - About Comp AI Device Monitor + About Comp AI Device Agent

    - Comp AI Device Monitor is a lightweight agent that helps ensure your device - meets security compliance requirements. -

    -

    - It monitors device configuration, installed software, and security settings to - help maintain a secure work environment. + Comp AI Device Agent is a lightweight system tray application that monitors your + device's compliance with your organization's security requirements.

    +

    It checks for:

    +
      +
    • + Disk encryption (FileVault on macOS, BitLocker on Windows, LUKS on Linux) +
    • +
    • Antivirus software (XProtect, Windows Defender, ClamAV, or third-party)
    • +
    • Minimum password length policy (8+ characters)
    • +
    • Screen lock timeout (5 minutes or less)
    • +

    Security powered by Comp AI: Your organization uses Comp AI to maintain security and compliance standards. diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/GeneralTrainingAccordionItem.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/GeneralTrainingAccordionItem.tsx index 8bd1919374..15a967587e 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/GeneralTrainingAccordionItem.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/GeneralTrainingAccordionItem.tsx @@ -4,7 +4,8 @@ import { trainingVideos } from '@/lib/data/training-videos'; import { AccordionContent, AccordionItem, AccordionTrigger } from '@comp/ui/accordion'; import { cn } from '@comp/ui/cn'; import type { EmployeeTrainingVideoCompletion } from '@db'; -import { CheckCircle2, Circle } from 'lucide-react'; +import { Badge } from '@trycompai/design-system'; +import { CheckmarkFilled, CircleDash } from '@trycompai/design-system/icons'; import { VideoCarousel } from '../video/VideoCarousel'; interface GeneralTrainingAccordionItemProps { @@ -51,9 +52,9 @@ export function GeneralTrainingAccordionItem({

    {hasCompletedGeneralTraining ? ( - + ) : ( - + )} Complete general security awareness training - {hasCompletedGeneralTraining ? ( - Secure annually - ) : ( - - {completedCount}/{totalCount} completed - + {!hasCompletedGeneralTraining && totalCount > 0 && ( + + {completedCount}/{totalCount} + )}
    diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/PoliciesAccordionItem.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/PoliciesAccordionItem.tsx index 25637a2bf5..c229a06da8 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/PoliciesAccordionItem.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/PoliciesAccordionItem.tsx @@ -2,10 +2,10 @@ import { acceptAllPolicies } from '@/actions/accept-policies'; import { AccordionContent, AccordionItem, AccordionTrigger } from '@comp/ui/accordion'; -import { Button } from '@comp/ui/button'; import { cn } from '@comp/ui/cn'; import type { Member, Policy, PolicyVersion } from '@db'; -import { CheckCircle2, Circle, FileText } from 'lucide-react'; +import { Button } from '@trycompai/design-system'; +import { CheckmarkFilled, CircleDash, Document } from '@trycompai/design-system/icons'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; @@ -58,9 +58,9 @@ export function PoliciesAccordionItem({ policies, member }: PoliciesAccordionIte
    {hasAcceptedPolicies ? ( - + ) : ( - + )} - + {policy.name} - {isAccepted && } + {isAccepted && }
    ); })} @@ -98,12 +98,9 @@ export function PoliciesAccordionItem({ policies, member }: PoliciesAccordionIte size="sm" onClick={handleAcceptAllPolicies} disabled={hasAcceptedPolicies || isAcceptingAll} + loading={isAcceptingAll} > - {isAcceptingAll - ? 'Accepting...' - : hasAcceptedPolicies - ? 'All Policies Accepted' - : 'Accept All'} + {hasAcceptedPolicies ? 'All Policies Accepted' : 'Accept All'} ) : ( diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/components/video/CarouselControls.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/components/video/CarouselControls.tsx index dd5660fecf..6d597131d7 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/components/video/CarouselControls.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/components/video/CarouselControls.tsx @@ -1,5 +1,5 @@ -import { Button } from '@comp/ui/button'; -import { ChevronLeft, ChevronRight } from 'lucide-react'; +import { Button } from '@trycompai/design-system'; +import { ChevronLeft, ChevronRight } from '@trycompai/design-system/icons'; interface CarouselControlsProps { currentIndex: number; @@ -25,7 +25,7 @@ export function CarouselControls({ disabled={isFirstVideo} aria-label="Previous video" > - +
    @@ -39,7 +39,7 @@ export function CarouselControls({ disabled={!onNext} aria-label="Next video" > - +
    ); diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/components/video/YoutubeEmbed.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/components/video/YoutubeEmbed.tsx index ee30171d28..b7dbf1f06d 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/components/video/YoutubeEmbed.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/components/video/YoutubeEmbed.tsx @@ -1,8 +1,8 @@ 'use client'; -import { Button } from '@comp/ui/button'; import type { EmployeeTrainingVideoCompletion } from '@db'; -import { ArrowRight, Check, Loader2 } from 'lucide-react'; +import { Button } from '@trycompai/design-system'; +import { ArrowRight, Checkmark } from '@trycompai/design-system/icons'; import { useState } from 'react'; // Define our own TrainingVideo interface since we can't find the import @@ -53,19 +53,14 @@ export function YoutubeEmbed({ variant={isCompleted ? 'secondary' : 'default'} onClick={onComplete} disabled={isCompleted} - className="gap-2" + loading={isMarkingComplete} + iconLeft={!isMarkingComplete ? : undefined} > - {isMarkingComplete ? ( - <> - - Marking as Complete... - - ) : ( - <> - - {isCompleted ? 'Completed' : 'Mark as Complete'} - - )} + {isMarkingComplete + ? 'Marking as Complete...' + : isCompleted + ? 'Completed' + : 'Mark as Complete'} )}
    @@ -73,16 +68,15 @@ export function YoutubeEmbed({ {isCompleted && !isRewatching && (
    - +

    Video Completed

    - {onNext && ( - )}
    diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx index 4056bbebf5..259751b6b2 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx @@ -53,7 +53,7 @@ export default async function OrganizationPage({ params }: { params: Promise<{ o redirect('/'); } - // Fleet policies - already has graceful error handling in getFleetPolicies + // Fleet policies - only fetch if member has a fleet device label const fleetData = await getFleetPolicies(member); return ( @@ -72,9 +72,8 @@ const getFleetPolicies = async ( ): Promise<{ fleetPolicies: FleetPolicy[]; device: Host | null }> => { const deviceLabelId = member.fleetDmLabelId; - // Return early if no deviceLabelId - if (!deviceLabelId) { - console.log('No fleet device label ID found for member'); + // Return early if no deviceLabelId or FleetDM not configured + if (!deviceLabelId || !process.env.FLEET_URL || !process.env.FLEET_TOKEN) { return { fleetPolicies: [], device: null }; } @@ -82,7 +81,7 @@ const getFleetPolicies = async ( const fleet = await getFleetInstance(); const deviceResponse = await fleet.get(`/labels/${deviceLabelId}/hosts`); - const device: Host | undefined = deviceResponse.data.hosts[0]; // There should only be one device per label. + const device: Host | undefined = deviceResponse.data.hosts[0]; if (!device) { return { fleetPolicies: [], device: null }; @@ -111,20 +110,32 @@ const getFleetPolicies = async ( return { device, fleetPolicies: fleetPolicies.map((policy) => { - const policyResult = fleetPolicyResults.find((result) => result.fleetPolicyId === policy.id); + const policyResult = fleetPolicyResults.find( + (result) => result.fleetPolicyId === policy.id, + ); return { ...policy, - response: policy.response === 'pass' || policyResult?.fleetPolicyResponse === 'pass' ? 'pass' : 'fail', + response: + policy.response === 'pass' || policyResult?.fleetPolicyResponse === 'pass' + ? 'pass' + : 'fail', attachments: policyResult?.attachments || [], - } + }; }), }; - } catch (error: any) { - // Log more details about the error - if (error.response?.status === 404) { - console.log(`Fleet endpoint not found for label ID: ${deviceLabelId}`); + } catch (error: unknown) { + const statusCode = + error && typeof error === 'object' && 'response' in error + ? (error as { response?: { status?: number } }).response?.status + : undefined; + + if (statusCode === 404) { + console.log(`Fleet endpoint not found for label ID: ${member.fleetDmLabelId}`); } else { - console.error('Error fetching fleet policies:', error.message || error); + console.error( + 'Error fetching fleet policies:', + error instanceof Error ? error.message : error, + ); } return { fleetPolicies: [], device: null }; } diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/policy/[policyId]/PolicyAcceptButton.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/policy/[policyId]/PolicyAcceptButton.tsx index 1d049cc3b7..32929f654f 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/policy/[policyId]/PolicyAcceptButton.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/policy/[policyId]/PolicyAcceptButton.tsx @@ -1,8 +1,8 @@ 'use client'; import { acceptPolicy } from '@/actions/accept-policies'; -import { Button } from '@comp/ui/button'; -import { Check } from 'lucide-react'; +import { Button } from '@trycompai/design-system'; +import { Checkmark } from '@trycompai/design-system/icons'; import { useRouter } from 'next/navigation'; import { useState, useTransition } from 'react'; import { toast } from 'sonner'; @@ -48,16 +48,19 @@ export function PolicyAcceptButton({ if (accepted) { return ( - +
    + +
    ); } return ( - +
    + +
    ); } diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/policy/[policyId]/page.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/policy/[policyId]/page.tsx index 158e1648d9..9e6c695181 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/policy/[policyId]/page.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/policy/[policyId]/page.tsx @@ -1,16 +1,7 @@ import { auth } from '@/app/lib/auth'; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from '@comp/ui/card'; import { db } from '@db'; -import { ArrowLeft, Check } from 'lucide-react'; +import { Badge, PageHeader, PageHeaderDescription, PageLayout } from '@trycompai/design-system'; import { headers } from 'next/headers'; -import Link from 'next/link'; import { redirect } from 'next/navigation'; import { PolicyAcceptButton } from './PolicyAcceptButton'; import PolicyViewer from './PolicyViewer'; @@ -64,50 +55,39 @@ export default async function PolicyPage({ const isAccepted = policy.signedBy.includes(member.id); return ( -
    -
    - - - Back to Overview - -
    - - - {isAccepted && ( -
    - - - You have accepted this policy - -
    - )} - - {policy.name} + Accepted : undefined} + > {policy.description && ( - - {policy.description} - - )} - - -
    - -
    - {policy.updatedAt && ( -

    - Last updated: {new Date(policy.updatedAt).toLocaleDateString()} -

    + {policy.description} )} -
    - - - -
    -
    + + } + > +
    + + + {policy.updatedAt && ( +

    + Last updated: {new Date(policy.updatedAt).toLocaleDateString()} +

    + )} + + +
    + ); } diff --git a/apps/portal/src/app/(app)/(home)/components/NoAccessMessage.tsx b/apps/portal/src/app/(app)/(home)/components/NoAccessMessage.tsx index b27c6fd8d9..903c040602 100644 --- a/apps/portal/src/app/(app)/(home)/components/NoAccessMessage.tsx +++ b/apps/portal/src/app/(app)/(home)/components/NoAccessMessage.tsx @@ -1,5 +1,5 @@ import { Alert, AlertDescription, AlertTitle } from '@comp/ui/alert'; -import { AlertTriangle } from 'lucide-react'; +import { WarningAlt } from '@trycompai/design-system/icons'; interface NoAccessMessageProps { message?: string; @@ -8,7 +8,7 @@ interface NoAccessMessageProps { export function NoAccessMessage({ message }: NoAccessMessageProps) { return ( - + Access Denied {message ?? diff --git a/apps/portal/src/app/(app)/layout.tsx b/apps/portal/src/app/(app)/layout.tsx index ce4d3ad673..6bf1cba54f 100644 --- a/apps/portal/src/app/(app)/layout.tsx +++ b/apps/portal/src/app/(app)/layout.tsx @@ -16,13 +16,11 @@ export default async function Layout({ children }: { children: React.ReactNode } } return ( -
    -
    -
    -
    -
    -
    {children}
    +
    +
    +
    +
    {children}
    ); } diff --git a/apps/portal/src/app/api/device-agent/check-in/route.ts b/apps/portal/src/app/api/device-agent/check-in/route.ts new file mode 100644 index 0000000000..d492455e1b --- /dev/null +++ b/apps/portal/src/app/api/device-agent/check-in/route.ts @@ -0,0 +1,120 @@ +import { auth } from '@/app/lib/auth'; +import { db } from '@db'; +import { type NextRequest, NextResponse } from 'next/server'; +import type { Prisma } from '@db'; +import { z } from 'zod'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +const checkResultSchema = z.object({ + checkType: z.enum(['disk_encryption', 'antivirus', 'password_policy', 'screen_lock']), + passed: z.boolean(), + details: z + .object({ + method: z.string().max(100), + raw: z.string().max(2000), + message: z.string().max(1000), + exception: z.string().max(500).optional(), + }) + .optional(), + checkedAt: z.string().datetime(), +}); + +const checkInSchema = z.object({ + deviceId: z.string().min(1), + checks: z.array(checkResultSchema).min(1), + agentVersion: z.string().optional(), +}); + +export async function POST(req: NextRequest) { + try { + const session = await auth.api.getSession({ headers: req.headers }); + + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const body = await req.json(); + const parsed = checkInSchema.safeParse(body); + + if (!parsed.success) { + return NextResponse.json( + { error: 'Invalid request body', details: parsed.error.flatten() }, + { status: 400 }, + ); + } + + const { deviceId, checks, agentVersion } = parsed.data; + + // Verify the device belongs to an active member of the authenticated user + const device = await db.device.findFirst({ + where: { + id: deviceId, + member: { + userId: session.user.id, + deactivated: false, + }, + }, + }); + + if (!device) { + return NextResponse.json({ error: 'Device not found' }, { status: 404 }); + } + + // Build check fields from results + const checkFields: Record = { + diskEncryptionEnabled: device.diskEncryptionEnabled, + antivirusEnabled: device.antivirusEnabled, + passwordPolicySet: device.passwordPolicySet, + screenLockEnabled: device.screenLockEnabled, + }; + + const checkDetails: Record = (device.checkDetails as Record) ?? {}; + + const checkTypeToField: Record = { + disk_encryption: 'diskEncryptionEnabled', + antivirus: 'antivirusEnabled', + password_policy: 'passwordPolicySet', + screen_lock: 'screenLockEnabled', + }; + + for (const check of checks) { + const field = checkTypeToField[check.checkType]; + if (field) { + checkFields[field] = check.passed; + } + checkDetails[check.checkType] = { + ...check.details, + passed: check.passed, + checkedAt: check.checkedAt, + }; + } + + const isCompliant = + checkFields.diskEncryptionEnabled && + checkFields.antivirusEnabled && + checkFields.passwordPolicySet && + checkFields.screenLockEnabled; + + const updatedDevice = await db.device.update({ + where: { id: deviceId }, + data: { + ...checkFields, + checkDetails: checkDetails as Prisma.InputJsonValue, + isCompliant, + lastCheckIn: new Date(), + ...(agentVersion ? { agentVersion } : {}), + }, + select: { isCompliant: true }, + }); + + return NextResponse.json({ + isCompliant: updatedDevice.isCompliant, + nextCheckIn: new Date(Date.now() + 60 * 60 * 1000).toISOString(), + }); + } catch (error) { + console.error('Error processing device check-in:', error); + return NextResponse.json({ error: 'Failed to process check-in' }, { status: 500 }); + } +} diff --git a/apps/portal/src/app/api/device-agent/my-organizations/route.ts b/apps/portal/src/app/api/device-agent/my-organizations/route.ts new file mode 100644 index 0000000000..1aa7ba7b00 --- /dev/null +++ b/apps/portal/src/app/api/device-agent/my-organizations/route.ts @@ -0,0 +1,48 @@ +import { auth } from '@/app/lib/auth'; +import { db } from '@db'; +import { type NextRequest, NextResponse } from 'next/server'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +/** + * Returns all organizations the authenticated user belongs to. + * Used by the device agent to register the device for all orgs. + */ +export async function GET(req: NextRequest) { + try { + const session = await auth.api.getSession({ headers: req.headers }); + + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const memberships = await db.member.findMany({ + where: { + userId: session.user.id, + deactivated: false, + }, + include: { + organization: { + select: { + id: true, + name: true, + slug: true, + }, + }, + }, + }); + + const organizations = memberships.map((m) => ({ + organizationId: m.organization.id, + organizationName: m.organization.name, + organizationSlug: m.organization.slug, + role: m.role, + })); + + return NextResponse.json({ organizations }); + } catch (error) { + console.error('Error fetching user organizations:', error); + return NextResponse.json({ error: 'Failed to fetch organizations' }, { status: 500 }); + } +} diff --git a/apps/portal/src/app/api/device-agent/register/route.ts b/apps/portal/src/app/api/device-agent/register/route.ts new file mode 100644 index 0000000000..ff9f9c1a3e --- /dev/null +++ b/apps/portal/src/app/api/device-agent/register/route.ts @@ -0,0 +1,153 @@ +import { auth } from '@/app/lib/auth'; +import { db } from '@db'; +import { type NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +const registerDeviceSchema = z.object({ + name: z.string().min(1), + hostname: z.string().min(1), + platform: z.enum(['macos', 'windows', 'linux']), + osVersion: z.string().min(1), + serialNumber: z.string().optional(), + hardwareModel: z.string().optional(), + agentVersion: z.string().optional(), + organizationId: z.string().min(1), +}); + +export async function POST(req: NextRequest) { + try { + const session = await auth.api.getSession({ headers: req.headers }); + + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const body = await req.json(); + const parsed = registerDeviceSchema.safeParse(body); + + if (!parsed.success) { + return NextResponse.json( + { error: 'Invalid request body', details: parsed.error.flatten() }, + { status: 400 }, + ); + } + + const { name, hostname, platform, osVersion, serialNumber, hardwareModel, agentVersion, organizationId } = + parsed.data; + + // Verify the user is a member of the organization + const member = await db.member.findFirst({ + where: { + userId: session.user.id, + organizationId, + deactivated: false, + }, + }); + + if (!member) { + return NextResponse.json({ error: 'Not a member of this organization' }, { status: 403 }); + } + + // Branch on serialNumber to avoid collisions for serial-less devices. + // PostgreSQL treats NULLs as distinct in unique constraints, so devices + // without a serial number can safely coexist in the same org. + let device; + + if (serialNumber) { + // Check if a device with this serial number already exists in the org + const existing = await db.device.findUnique({ + where: { + serialNumber_organizationId: { + serialNumber, + organizationId, + }, + }, + select: { id: true, memberId: true }, + }); + + if (existing && existing.memberId !== member.id) { + // Device belongs to a different member — prevent hijacking + return NextResponse.json( + { error: 'Device is already registered to another user in this organization' }, + { status: 409 }, + ); + } + + if (existing) { + // Same member re-registering their own device — update it + device = await db.device.update({ + where: { id: existing.id }, + data: { + name, + hostname, + platform, + osVersion, + hardwareModel, + agentVersion, + }, + }); + } else { + // New device — create it + device = await db.device.create({ + data: { + name, + hostname, + platform, + osVersion, + serialNumber, + hardwareModel, + agentVersion, + memberId: member.id, + organizationId, + }, + }); + } + } else { + // No serial number — find by hostname + member + org (same user re-registering + // the same machine), or create a new record with serialNumber = null. + const existing = await db.device.findFirst({ + where: { + hostname, + memberId: member.id, + organizationId, + serialNumber: null, + }, + }); + + if (existing) { + device = await db.device.update({ + where: { id: existing.id }, + data: { + name, + platform, + osVersion, + hardwareModel, + agentVersion, + }, + }); + } else { + device = await db.device.create({ + data: { + name, + hostname, + platform, + osVersion, + serialNumber: null, + hardwareModel, + agentVersion, + memberId: member.id, + organizationId, + }, + }); + } + } + + return NextResponse.json({ deviceId: device.id }); + } catch (error) { + console.error('Error registering device:', error); + return NextResponse.json({ error: 'Failed to register device' }, { status: 500 }); + } +} diff --git a/apps/portal/src/app/api/device-agent/status/route.ts b/apps/portal/src/app/api/device-agent/status/route.ts new file mode 100644 index 0000000000..ae2f0c1444 --- /dev/null +++ b/apps/portal/src/app/api/device-agent/status/route.ts @@ -0,0 +1,55 @@ +import { auth } from '@/app/lib/auth'; +import { db } from '@db'; +import { type NextRequest, NextResponse } from 'next/server'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +export async function GET(req: NextRequest) { + try { + const session = await auth.api.getSession({ headers: req.headers }); + + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const deviceId = req.nextUrl.searchParams.get('deviceId'); + const organizationId = req.nextUrl.searchParams.get('organizationId'); + + if (!deviceId) { + // Return all devices for this user, optionally filtered by org + const devices = await db.device.findMany({ + where: { + member: { + userId: session.user.id, + deactivated: false, + }, + ...(organizationId ? { organizationId } : {}), + }, + orderBy: { installedAt: 'desc' }, + }); + + return NextResponse.json({ devices }); + } + + // Return a specific device + const device = await db.device.findFirst({ + where: { + id: deviceId, + member: { + userId: session.user.id, + deactivated: false, + }, + }, + }); + + if (!device) { + return NextResponse.json({ error: 'Device not found' }, { status: 404 }); + } + + return NextResponse.json({ device }); + } catch (error) { + console.error('Error fetching device status:', error); + return NextResponse.json({ error: 'Failed to fetch device status' }, { status: 500 }); + } +} diff --git a/apps/portal/src/app/api/device-agent/updates/[filename]/route.ts b/apps/portal/src/app/api/device-agent/updates/[filename]/route.ts new file mode 100644 index 0000000000..7cb3fe88c9 --- /dev/null +++ b/apps/portal/src/app/api/device-agent/updates/[filename]/route.ts @@ -0,0 +1,134 @@ +import { s3Client } from '@/utils/s3'; +import { GetObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3'; +import { type NextRequest, NextResponse } from 'next/server'; +import { Readable } from 'stream'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; +export const maxDuration = 300; + +/** Environment subfolder: 'staging' or 'production' (default) */ +const S3_ENV = process.env.DEVICE_AGENT_S3_ENV || 'production'; +const S3_PREFIX = `device-agent/${S3_ENV}/updates`; + +/** Allowed file extensions for auto-update files */ +const ALLOWED_EXTENSIONS = new Set([ + '.yml', + '.zip', + '.exe', + '.blockmap', + '.AppImage', + '.dmg', +]); + +const CONTENT_TYPES: Record = { + '.yml': 'text/yaml', + '.zip': 'application/zip', + '.exe': 'application/octet-stream', + '.blockmap': 'application/octet-stream', + '.AppImage': 'application/octet-stream', + '.dmg': 'application/x-apple-diskimage', +}; + +function getExtension(filename: string): string { + // Handle .AppImage specially (not a dotted extension from lastIndexOf perspective) + if (filename.endsWith('.AppImage')) return '.AppImage'; + const dotIndex = filename.lastIndexOf('.'); + return dotIndex >= 0 ? filename.slice(dotIndex) : ''; +} + +function isValidFilename(filename: string): boolean { + // Block path traversal + if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) { + return false; + } + const ext = getExtension(filename); + return ALLOWED_EXTENSIONS.has(ext); +} + +export async function GET( + _req: NextRequest, + { params }: { params: Promise<{ filename: string }> }, +) { + const { filename } = await params; + + if (!isValidFilename(filename)) { + return new NextResponse('Not found', { status: 404 }); + } + + const bucketName = process.env.FLEET_AGENT_BUCKET_NAME; + if (!bucketName) { + return new NextResponse('Server configuration error', { status: 500 }); + } + + const key = `${S3_PREFIX}/${filename}`; + const ext = getExtension(filename); + const contentType = CONTENT_TYPES[ext] || 'application/octet-stream'; + + try { + const command = new GetObjectCommand({ Bucket: bucketName, Key: key }); + const s3Response = await s3Client.send(command); + + if (!s3Response.Body) { + return new NextResponse('Not found', { status: 404 }); + } + + const headers: Record = { + 'Content-Type': contentType, + 'Cache-Control': 'public, max-age=300', + }; + + if (typeof s3Response.ContentLength === 'number') { + headers['Content-Length'] = s3Response.ContentLength.toString(); + } + + const s3Stream = s3Response.Body as Readable; + const webStream = Readable.toWeb(s3Stream) as unknown as ReadableStream; + + return new NextResponse(webStream, { headers }); + } catch (error: unknown) { + if (error && typeof error === 'object' && 'name' in error && error.name === 'NoSuchKey') { + return new NextResponse('Not found', { status: 404 }); + } + console.error('Error serving update file:', { key, error }); + return new NextResponse('Internal server error', { status: 500 }); + } +} + +export async function HEAD( + _req: NextRequest, + { params }: { params: Promise<{ filename: string }> }, +) { + const { filename } = await params; + + if (!isValidFilename(filename)) { + return new NextResponse(null, { status: 404 }); + } + + const bucketName = process.env.FLEET_AGENT_BUCKET_NAME; + if (!bucketName) { + return new NextResponse(null, { status: 500 }); + } + + const key = `${S3_PREFIX}/${filename}`; + const ext = getExtension(filename); + const contentType = CONTENT_TYPES[ext] || 'application/octet-stream'; + + try { + const command = new HeadObjectCommand({ Bucket: bucketName, Key: key }); + const s3Response = await s3Client.send(command); + + const headers: Record = { + 'Content-Type': contentType, + 'Cache-Control': 'public, max-age=300', + }; + + if (typeof s3Response.ContentLength === 'number') { + headers['Content-Length'] = s3Response.ContentLength.toString(); + } + + return new NextResponse(null, { headers }); + } catch { + return new NextResponse(null, { status: 404 }); + } +} diff --git a/apps/portal/src/app/api/download-agent/archive.ts b/apps/portal/src/app/api/download-agent/archive.ts deleted file mode 100644 index 87113f5e19..0000000000 --- a/apps/portal/src/app/api/download-agent/archive.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { logger } from '@/utils/logger'; -import { getFleetAgent } from '@/utils/s3'; -import archiver from 'archiver'; -import { exec } from 'node:child_process'; -import { promises as fs } from 'node:fs'; -import path from 'node:path'; -import type { Readable } from 'node:stream'; -import { PassThrough } from 'node:stream'; -import { promisify } from 'node:util'; -import { getPackageFilename, getReadmeContent, getScriptFilename } from './scripts'; -import type { CreateArchiveParams } from './types'; - -const execAsync = promisify(exec); - -export async function createAgentArchive({ - os, - script, - tempDir, -}: CreateArchiveParams): Promise { - const stream = new PassThrough(); - const archive = archiver('zip', { zlib: { level: 9 } }); - archive.pipe(stream); - - try { - // Save the script - const scriptFilename = getScriptFilename(os); - const scriptPath = path.join(tempDir, scriptFilename); - await fs.writeFile(scriptPath, script, { mode: 0o755 }); - - // Codesign only for macOS - if (os === 'macos') { - await codesignMacScript(scriptPath); - } - - // Read the script and add to archive - const scriptContent = await fs.readFile(scriptPath); - archive.append(scriptContent, { name: scriptFilename, mode: 0o755 }); - - // Get and add the agent package - const agentPackage = await getFleetAgent({ os }); - const packageFilename = getPackageFilename(os); - - archive.append(agentPackage as Readable, { - name: packageFilename, - store: true, - }); - - // Add README with installation instructions - const readmeContent = getReadmeContent(os); - archive.append(Buffer.from(readmeContent), { name: 'README.txt' }); - - archive.finalize().catch((err) => { - logger('Error finalizing archive', { error: err }); - stream.destroy(); - }); - - return stream; - } catch (error) { - logger('Error creating archive', { error }); - stream.destroy(); - throw error; - } -} - -async function codesignMacScript(scriptPath: string): Promise { - const codesignIdentity = process.env.CODESIGN_IDENTITY || 'Developer ID Application'; - try { - await execAsync(`codesign --sign "${codesignIdentity}" --timestamp "${scriptPath}"`); - logger('Successfully codesigned command file', { scriptPath }); - } catch (error) { - logger('Failed to codesign command file', { error, scriptPath }); - // Continue without codesigning if it fails (e.g., in development) - } -} diff --git a/apps/portal/src/app/api/download-agent/constants.ts b/apps/portal/src/app/api/download-agent/constants.ts index dc77a5ebaf..f35394e9ff 100644 --- a/apps/portal/src/app/api/download-agent/constants.ts +++ b/apps/portal/src/app/api/download-agent/constants.ts @@ -1,3 +1,31 @@ -export const MAC_APPLE_SILICON_FILENAME = 'Comp AI Agent-1.0.0-arm64.dmg'; -export const MAC_INTEL_FILENAME = 'Comp AI Agent-1.0.0.dmg'; -export const WINDOWS_FILENAME = 'Comp AI Agent 1.0.0.exe'; +import type { SupportedOS } from './types'; + +/** Environment subfolder: 'staging' or 'production' (default) */ +const S3_ENV = process.env.DEVICE_AGENT_S3_ENV || 'production'; +export const S3_PREFIX = `device-agent/${S3_ENV}`; + +export const DOWNLOAD_TARGETS: Record< + SupportedOS, + { key: string; filename: string; contentType: string } +> = { + macos: { + key: `${S3_PREFIX}/macos/latest-arm64.dmg`, + filename: 'CompAI-Device-Agent-arm64.dmg', + contentType: 'application/x-apple-diskimage', + }, + 'macos-intel': { + key: `${S3_PREFIX}/macos/latest-x64.dmg`, + filename: 'CompAI-Device-Agent-x64.dmg', + contentType: 'application/x-apple-diskimage', + }, + windows: { + key: `${S3_PREFIX}/windows/latest-setup.exe`, + filename: 'CompAI-Device-Agent-setup.exe', + contentType: 'application/octet-stream', + }, + linux: { + key: `${S3_PREFIX}/linux/latest-amd64.deb`, + filename: 'CompAI-Device-Agent-amd64.deb', + contentType: 'application/vnd.debian.binary-package', + }, +}; diff --git a/apps/portal/src/app/api/download-agent/fleet-label.ts b/apps/portal/src/app/api/download-agent/fleet-label.ts deleted file mode 100644 index 259143e310..0000000000 --- a/apps/portal/src/app/api/download-agent/fleet-label.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { getFleetInstance } from '@/utils/fleet'; -import { logger } from '@/utils/logger'; -import { db } from '@db'; -import { AxiosError } from 'axios'; -import type { CreateFleetLabelParams } from './types'; - -export async function createFleetLabel({ - employeeId, - memberId, - os, - fleetDevicePathMac, - fleetDevicePathWindows, -}: CreateFleetLabelParams): Promise { - logger('createFleetLabel function called', { - employeeId, - memberId, - os, - fleetDevicePathMac, - fleetDevicePathWindows, - }); - - try { - logger('Getting Fleet instance...'); - const fleet = await getFleetInstance(); - logger('Fleet instance obtained successfully'); - - // OS-specific queries: mac uses file-only; Windows uses UNION with file and registry - const query = - os === 'macos' - ? `SELECT 1 FROM file WHERE path = '${fleetDevicePathMac}/${employeeId}' LIMIT 1;` - : `SELECT 1 FROM file WHERE path = '${fleetDevicePathWindows}\\${employeeId}' - UNION SELECT 1 FROM file WHERE path = 'C:\\Users\\Public\\CompAI\\Fleet\\${employeeId}' - LIMIT 1;`; - - // Normalize whitespace to a single line to avoid issues with newlines/tabs - const normalizedQuery = query.replace(/\s+/g, ' ').trim(); - - logger('Generated Fleet query for label creation', { - employeeId, - os, - query: normalizedQuery, - }); - - logger('Sending POST request to Fleet API to create label...', { - labelName: employeeId, - endpoint: '/labels', - requestBody: { - name: employeeId, - query: normalizedQuery, - }, - }); - - const response = await fleet.post('/labels', { - name: employeeId, - query: normalizedQuery, - }); - - logger('Fleet API response received', { - status: response.status, - statusText: response.statusText, - labelId: response.data?.label?.id, - responseData: response.data, - headers: response.headers, - }); - - const labelId = response.data.label.id; - - logger('Updating member record with Fleet label ID', { - memberId, - labelId, - employeeId, - }); - - await db.member.update({ - where: { - id: memberId, - }, - data: { - fleetDmLabelId: labelId, - }, - }); - - logger('Member record updated successfully with Fleet label ID', { - memberId, - labelId, - employeeId, - }); - } catch (error) { - if (error instanceof AxiosError && error.response?.status === 409) { - // Label already exists, which is fine. - const fleetError = error.response.data; - logger('Fleet label already exists, skipping creation.', { - employeeId, - httpStatus: error.response.status, - httpStatusText: error.response.statusText, - fleetMessage: fleetError?.message, - fleetErrors: fleetError?.errors, - fleetUuid: fleetError?.uuid, - axiosMessage: error.message, - url: error.config?.url, - method: error.config?.method, - fullResponseData: error.response.data, - }); - } else { - // Log the error details before re-throwing - const fleetError = error instanceof AxiosError ? error.response?.data : null; - logger('Error creating Fleet label', { - employeeId, - memberId, - os, - httpStatus: error instanceof AxiosError ? error.response?.status : undefined, - httpStatusText: error instanceof AxiosError ? error.response?.statusText : undefined, - fleetMessage: fleetError?.message, - fleetErrors: fleetError?.errors, - fleetUuid: fleetError?.uuid, - axiosMessage: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, - url: error instanceof AxiosError ? error.config?.url : undefined, - method: error instanceof AxiosError ? error.config?.method : undefined, - fullResponseData: fleetError, - }); - - // Re-throw other errors - throw error; - } - } -} diff --git a/apps/portal/src/app/api/download-agent/route.ts b/apps/portal/src/app/api/download-agent/route.ts index e88a2c12cc..e0fd3a1e6d 100644 --- a/apps/portal/src/app/api/download-agent/route.ts +++ b/apps/portal/src/app/api/download-agent/route.ts @@ -5,7 +5,7 @@ import { client as kv } from '@comp/kv'; import { type NextRequest, NextResponse } from 'next/server'; import { Readable } from 'stream'; -import { MAC_APPLE_SILICON_FILENAME, MAC_INTEL_FILENAME, WINDOWS_FILENAME } from './constants'; +import { DOWNLOAD_TARGETS } from './constants'; import type { SupportedOS } from './types'; export const runtime = 'nodejs'; @@ -27,22 +27,9 @@ interface DownloadTarget { } const getDownloadTarget = (os: SupportedOS): DownloadTarget => { - if (os === 'windows') { - return { - key: `windows/${WINDOWS_FILENAME}`, - filename: WINDOWS_FILENAME, - contentType: 'application/octet-stream', - }; - } - - const isAppleSilicon = os === 'macos'; - const filename = isAppleSilicon ? MAC_APPLE_SILICON_FILENAME : MAC_INTEL_FILENAME; - - return { - key: `macos/${filename}`, - filename, - contentType: 'application/x-apple-diskimage', - }; + const target = DOWNLOAD_TARGETS[os]; + if (!target) throw new Error(`Unsupported OS: ${os}`); + return target; }; const buildResponseHeaders = ( diff --git a/apps/portal/src/app/api/download-agent/scripts.ts b/apps/portal/src/app/api/download-agent/scripts.ts deleted file mode 100644 index d41b836e3e..0000000000 --- a/apps/portal/src/app/api/download-agent/scripts.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { getPackageFilename, getReadmeContent, getScriptFilename } from './scripts/common'; -import { generateMacScript } from './scripts/mac'; - -export { generateMacScript, getPackageFilename, getReadmeContent, getScriptFilename }; diff --git a/apps/portal/src/app/api/download-agent/scripts/common.ts b/apps/portal/src/app/api/download-agent/scripts/common.ts deleted file mode 100644 index 415745cf4b..0000000000 --- a/apps/portal/src/app/api/download-agent/scripts/common.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { SupportedOS } from '../types'; - -export function getScriptFilename(os: SupportedOS): string { - return os === 'macos' || os === 'macos-intel' ? 'run_me_first.command' : 'run_me_first.bat'; -} - -export function getPackageFilename(os: SupportedOS): string { - return os === 'macos' || os === 'macos-intel' - ? 'compai-device-agent.pkg' - : 'compai-device-agent.msi'; -} - -export function getReadmeContent(os: SupportedOS): string { - if (os === 'macos' || os === 'macos-intel') { - return `Installation Instructions for macOS: - -1. First, run the setup script by double-clicking "run_me_first.command" - - This will create the necessary organization markers for device management - - You may need to allow the script to run in System Preferences > Security & Privacy - -2. Then, install the agent by double-clicking "compai-device-agent.pkg" - - Follow the installation wizard - - You may need to allow the installer in System Preferences > Security & Privacy - -3. The agent will start automatically after installation -`; - } - - return `Installation Instructions for Windows: - -1. First, run the setup script: - - Right-click on "run_me_first.bat" and select "Run as administrator" (required) - - This writes organization markers to the device and registry - - If prompted by SmartScreen, click "More info" -> "Run anyway" - -2. Then, install the agent: - - Double-click "compai-device-agent.msi" and follow the wizard - -3. Troubleshooting: - - If setup fails, open the log at: %ProgramData%\\CompAI\\Fleet or %Public%\\CompAI\\Fleet -> setup.log - - Ensure your antivirus or endpoint protection allows running local .bat files - - If you cannot run as administrator, ask IT to assist or install both files and registry keys manually - -4. After installation, the agent will start automatically. -`; -} diff --git a/apps/portal/src/app/api/download-agent/scripts/index.ts b/apps/portal/src/app/api/download-agent/scripts/index.ts deleted file mode 100644 index 78b6f68923..0000000000 --- a/apps/portal/src/app/api/download-agent/scripts/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { getPackageFilename, getReadmeContent, getScriptFilename } from './common'; -export { generateMacScript } from './mac'; diff --git a/apps/portal/src/app/api/download-agent/scripts/mac.ts b/apps/portal/src/app/api/download-agent/scripts/mac.ts deleted file mode 100644 index d51c854f29..0000000000 --- a/apps/portal/src/app/api/download-agent/scripts/mac.ts +++ /dev/null @@ -1,120 +0,0 @@ -import type { ScriptConfig } from '../types'; - -export function generateMacScript(config: ScriptConfig): string { - const { orgId, employeeId, fleetDevicePath } = config; - - return `#!/bin/bash -# CompAI Device Setup (macOS) -# Creates organization markers for Fleet policies/labels with clear, human-readable output - -set -uo pipefail - -ORG_ID="${orgId}" -EMPLOYEE_ID="${employeeId}" -FLEET_DIR="${fleetDevicePath}" -LOG_FILE="/tmp/compai-setup.log" -HAS_ERROR=0 -ERROR_TEXT="" - -# Colors (ANSI escapes) -NC='\x1b[0m' -GREEN='\x1b[0;32m' -YELLOW='\x1b[0;33m' -RED='\x1b[0;31m' -BOLD='\x1b[1m' - -timestamp() { date '+%Y-%m-%d %H:%M:%S'; } -log_info() { printf "[%s] %bINFO%b %s\n" "$(timestamp)" "$GREEN" "$NC" "$1" | tee -a "$LOG_FILE"; } -log_warn() { printf "[%s] %bWARN%b %s\n" "$(timestamp)" "$YELLOW" "$NC" "$1" | tee -a "$LOG_FILE"; } -log_error() { printf "[%s] %bERROR%b %s\n" "$(timestamp)" "$RED" "$NC" "$1" | tee -a "$LOG_FILE"; HAS_ERROR=1; ERROR_TEXT+=" - $1"$'\n'; } - -echo "------------------------------------------------------------" -printf "%b\n" "$BOLD CompAI Device Setup (macOS)$NC" -echo "Organization: $ORG_ID" -echo "Employee: $EMPLOYEE_ID" -echo "Date: $(timestamp)" -echo "Log file: $LOG_FILE" -echo "------------------------------------------------------------" -echo - -# Determine if we need sudo for the fleet directory -SUDO="" -if [ ! -d "$FLEET_DIR" ]; then - log_info "Creating directory: $FLEET_DIR" - if mkdir -p "$FLEET_DIR" 2>>"$LOG_FILE"; then - : - else - log_warn "No write access creating $FLEET_DIR; retrying with sudo (you may be prompted for your password)." - if sudo mkdir -p "$FLEET_DIR" 2>>"$LOG_FILE"; then - SUDO="sudo" - else - log_error "Failed to create directory $FLEET_DIR even with sudo." - fi - fi -fi - -if [ -d "$FLEET_DIR" ] && [ ! -w "$FLEET_DIR" ]; then - SUDO="sudo" -fi - -if [ -z "$SUDO" ]; then - log_info "Using directory: $FLEET_DIR (no sudo needed)" -else - log_info "Using directory: $FLEET_DIR (sudo required)" -fi - -# Write marker files -if [ -d "$FLEET_DIR" ]; then - log_info "Writing organization marker file..." - if printf "%s" "$ORG_ID" | $SUDO tee "$FLEET_DIR/$ORG_ID" >/dev/null 2>>"$LOG_FILE"; then - log_info "[OK] Organization marker: $FLEET_DIR/$ORG_ID" - else - log_error "Failed writing organization marker to $FLEET_DIR/$ORG_ID" - fi - - log_info "Writing employee marker file..." - if printf "%s" "$EMPLOYEE_ID" | $SUDO tee "$FLEET_DIR/$EMPLOYEE_ID" >/dev/null 2>>"$LOG_FILE"; then - log_info "[OK] Employee marker: $FLEET_DIR/$EMPLOYEE_ID" - else - log_error "Failed writing employee marker to $FLEET_DIR/$EMPLOYEE_ID" - fi - - # Permissions - $SUDO chmod 755 "$FLEET_DIR" 2>>"$LOG_FILE" || log_warn "Could not chmod 755 on $FLEET_DIR" - $SUDO chmod 644 "$FLEET_DIR/$ORG_ID" 2>>"$LOG_FILE" || log_warn "Could not chmod 644 on $FLEET_DIR/$ORG_ID" - $SUDO chmod 644 "$FLEET_DIR/$EMPLOYEE_ID" 2>>"$LOG_FILE" || log_warn "Could not chmod 644 on $FLEET_DIR/$EMPLOYEE_ID" -else - log_error "Directory not available: $FLEET_DIR" -fi - -# Verify markers -echo -log_info "Verifying markers..." -if [ -f "$FLEET_DIR/$EMPLOYEE_ID" ]; then - log_info "[OK] Employee marker file present." -else - log_error "Employee marker file missing at $FLEET_DIR/$EMPLOYEE_ID" -fi - -# Summary -echo -echo "------------------------------------------------------------" -if [ "$HAS_ERROR" -eq 0 ]; then - printf "%b\n" "$GREEN RESULT: SUCCESS $NC" - echo "Setup completed successfully for $EMPLOYEE_ID." - echo "Files created in: $FLEET_DIR" -else - printf "%b\n" "$RED RESULT: COMPLETED WITH ISSUES $NC" - echo "One or more steps did not complete successfully. Details:" - printf "%b" "$ERROR_TEXT" - echo - echo "Next steps:" - echo " - Take a screenshot of this window." - echo " - Attach the log file from: $LOG_FILE" - echo " - Share both with your CompAI support contact." -fi -echo "------------------------------------------------------------" -echo -read -r -p "Press Return to close this window..." _ -exit $HAS_ERROR`; -} diff --git a/apps/portal/src/app/api/download-agent/token/route.ts b/apps/portal/src/app/api/download-agent/token/route.ts index b7da419457..4550259117 100644 --- a/apps/portal/src/app/api/download-agent/token/route.ts +++ b/apps/portal/src/app/api/download-agent/token/route.ts @@ -3,11 +3,10 @@ import { logger } from '@/utils/logger'; import { client as kv } from '@comp/kv'; import { randomBytes } from 'crypto'; import { type NextRequest, NextResponse } from 'next/server'; -import { createFleetLabel } from '../fleet-label'; import type { DownloadAgentRequest, SupportedOS } from '../types'; import { detectOSFromUserAgent, validateMemberAndOrg } from '../utils'; -const SUPPORTED_OSES: SupportedOS[] = ['macos', 'macos-intel', 'windows']; +const SUPPORTED_OSES: SupportedOS[] = ['macos', 'macos-intel', 'windows', 'linux']; const isSupportedOS = (value: unknown): value is SupportedOS => typeof value === 'string' && SUPPORTED_OSES.includes(value as SupportedOS); @@ -46,47 +45,6 @@ export async function POST(req: NextRequest) { ); } - logger('Token route: Starting fleet label creation', { - employeeId, - memberId: member.id, - os: detectedOS, - orgId, - userId: session.user.id, - }); - - // Hardcoded device marker paths used by the setup scripts - const fleetDevicePathMac = '/Users/Shared/.fleet'; - const fleetDevicePathWindows = 'C:\\ProgramData\\CompAI\\Fleet'; - - // Create Fleet label - try { - await createFleetLabel({ - employeeId, - memberId: member.id, - os: detectedOS, - fleetDevicePathMac, - fleetDevicePathWindows, - }); - - logger('Token route: Fleet label creation completed successfully', { - employeeId, - memberId: member.id, - os: detectedOS, - orgId, - }); - } catch (error) { - logger('Token route: Error creating fleet label', { - employeeId, - memberId: member.id, - os: detectedOS, - orgId, - error: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, - }); - - return new NextResponse('Failed to create fleet label', { status: 500 }); - } - // Generate a secure random token logger('Generating download token', { employeeId, os: detectedOS, orgId }); const token = randomBytes(32).toString('hex'); diff --git a/apps/portal/src/app/api/download-agent/types.ts b/apps/portal/src/app/api/download-agent/types.ts index 8d7b33c883..ee209a4742 100644 --- a/apps/portal/src/app/api/download-agent/types.ts +++ b/apps/portal/src/app/api/download-agent/types.ts @@ -1,32 +1,7 @@ -export type SupportedOS = 'macos' | 'windows' | 'macos-intel'; - -export interface ScriptConfig { - orgId: string; - employeeId: string; - fleetDevicePath: string; -} - -export interface CreateArchiveParams { - os: SupportedOS; - script: string; - tempDir: string; -} - -export interface CreateFleetLabelParams { - employeeId: string; - memberId: string; - os: SupportedOS; - fleetDevicePathMac: string; - fleetDevicePathWindows: string; -} +export type SupportedOS = 'macos' | 'windows' | 'macos-intel' | 'linux'; export interface DownloadAgentRequest { orgId: string; employeeId: string; os?: SupportedOS; } - -export interface FleetDevicePaths { - mac: string; - windows: string; -} diff --git a/apps/portal/src/app/api/download-agent/utils.ts b/apps/portal/src/app/api/download-agent/utils.ts index 2ed0493c24..5fe2018062 100644 --- a/apps/portal/src/app/api/download-agent/utils.ts +++ b/apps/portal/src/app/api/download-agent/utils.ts @@ -6,11 +6,13 @@ import type { SupportedOS } from './types'; * Detects the operating system (and for macOS, the CPU architecture) from a User-Agent string. * * Returns: + * - 'linux' for Linux OS (excluding Android) * - 'windows' for Windows OS * - 'macos' for Apple Silicon (ARM-based) Macs * - 'macos-intel' for Intel-based Macs * * Examples of User-Agent strings: + * - Linux: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" * - Windows: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" * - macOS (Intel): "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" * - macOS (Apple Silicon): "Mozilla/5.0 (Macintosh; ARM Mac OS X 11_2_3) AppleWebKit/537.36" @@ -30,6 +32,11 @@ export function detectOSFromUserAgent(userAgent: string | null): SupportedOS | n const ua = userAgent.toLowerCase(); + // Check Linux before Windows/Mac — exclude Android which also contains 'linux' + if (ua.includes('linux') && !ua.includes('android')) { + return 'linux'; + } + if (ua.includes('windows') || ua.includes('win32') || ua.includes('win64')) { return 'windows'; } diff --git a/apps/portal/src/app/components/header.tsx b/apps/portal/src/app/components/header.tsx index bee160bb17..4b8c2c5d3b 100644 --- a/apps/portal/src/app/components/header.tsx +++ b/apps/portal/src/app/components/header.tsx @@ -6,7 +6,7 @@ import { Suspense } from 'react'; export async function Header() { return ( -
    +
    diff --git a/apps/portal/src/app/components/user-menu.tsx b/apps/portal/src/app/components/user-menu.tsx index 92859079ce..e43b692d2e 100644 --- a/apps/portal/src/app/components/user-menu.tsx +++ b/apps/portal/src/app/components/user-menu.tsx @@ -9,7 +9,6 @@ import { } from '@comp/ui/dropdown-menu'; import { headers } from 'next/headers'; import { Logout } from './logout'; -import { ThemeSwitch } from './theme-switch'; // Helper function to get initials function getInitials(name?: string | null, email?: string | null): string { @@ -70,11 +69,6 @@ export async function UserMenu() {
    -
    -

    Theme

    - -
    {' '} - diff --git a/apps/portal/src/app/layout.tsx b/apps/portal/src/app/layout.tsx index 0f5b82f9a5..0da299e8d9 100644 --- a/apps/portal/src/app/layout.tsx +++ b/apps/portal/src/app/layout.tsx @@ -1,8 +1,10 @@ +import '@comp/ui/globals.css'; +import '@trycompai/design-system/globals.css'; + import { auth } from '@/app/lib/auth'; import { env } from '@/env.mjs'; import { initializeServer } from '@comp/analytics/server'; import { cn } from '@comp/ui/cn'; -import '@comp/ui/globals.css'; import { GeistMono } from 'geist/font/mono'; import type { Metadata } from 'next'; import localFont from 'next/font/local'; @@ -88,7 +90,7 @@ export default async function Layout(props: { children: React.ReactNode }) { }); return ( - + o.trim()) + : ['http://localhost:3000', 'https://*.trycomp.ai', 'http://localhost:3002'], secret: env.AUTH_SECRET!, plugins: [ organization({ @@ -71,6 +73,7 @@ export const auth = betterAuth({ }), nextCookies(), multiSession(), + bearer(), ], socialProviders: { google: { diff --git a/apps/portal/src/app/portal.css b/apps/portal/src/app/portal.css new file mode 100644 index 0000000000..3921df93df --- /dev/null +++ b/apps/portal/src/app/portal.css @@ -0,0 +1,11 @@ +/* Portal-specific responsive overrides */ + +/* Scale down the page header heading on mobile */ +[data-slot='page-header'] [data-slot='heading'] { + @apply text-2xl sm:text-4xl; +} + +/* Remove the muted background from read-only editor */ +[data-slot='page-layout'] .bg-muted\/50 { + background: transparent; +} diff --git a/apps/portal/src/app/providers.tsx b/apps/portal/src/app/providers.tsx index f29097e6c3..a94adc7249 100644 --- a/apps/portal/src/app/providers.tsx +++ b/apps/portal/src/app/providers.tsx @@ -2,7 +2,6 @@ import { AnalyticsProvider } from '@comp/analytics'; import { Session, User } from 'better-auth'; -import { ThemeProvider } from 'next-themes'; import type { ReactNode } from 'react'; type ProviderProps = { @@ -15,13 +14,11 @@ type ProviderProps = { export function Providers({ children, session }: ProviderProps) { return ( - - - {children} - - + + {children} + ); } diff --git a/apps/portal/src/utils/os.ts b/apps/portal/src/utils/os.ts index 610a4bf979..fbe22f22f3 100644 --- a/apps/portal/src/utils/os.ts +++ b/apps/portal/src/utils/os.ts @@ -1,4 +1,4 @@ -export type SupportedOS = 'macos' | 'windows' | 'macos-intel'; +export type SupportedOS = 'macos' | 'windows' | 'macos-intel' | 'linux'; const isSafariUA = (ua: string) => ua.includes('safari') && @@ -14,6 +14,10 @@ export async function detectOSFromUserAgent(): Promise { try { const ua = navigator.userAgent.toLowerCase(); + if (ua.includes('linux') && !ua.includes('android')) { + return 'linux'; + } + if (ua.includes('win')) { return 'windows'; } diff --git a/bun.lock b/bun.lock index fd430dc286..9351db4a10 100644 --- a/bun.lock +++ b/bun.lock @@ -94,6 +94,7 @@ "@trigger.dev/sdk": "4.0.6", "@trycompai/db": "1.3.22", "@trycompai/email": "workspace:*", + "@trycompai/utils": "1.0.0", "@upstash/redis": "^1.34.2", "@upstash/vector": "^1.2.2", "adm-zip": "^0.5.16", @@ -213,6 +214,7 @@ "@trycompai/db": "1.3.22", "@trycompai/design-system": "^1.0.32", "@trycompai/email": "workspace:*", + "@trycompai/utils": "1.0.0", "@types/canvas-confetti": "^1.9.0", "@types/react-syntax-highlighter": "^15.5.13", "@types/three": "^0.180.0", @@ -322,6 +324,7 @@ "@t3-oss/env-nextjs": "^0.13.8", "@trycompai/analytics": "workspace:*", "@trycompai/db": "1.3.22", + "@trycompai/design-system": "^1.0.32", "@trycompai/email": "workspace:*", "@trycompai/kv": "workspace:*", "@trycompai/ui": "workspace:*", @@ -389,6 +392,30 @@ "typescript": "^5.9.2", }, }, + "packages/device-agent": { + "name": "@comp/device-agent", + "version": "1.0.1", + "dependencies": { + "@trycompai/design-system": "*", + "electron-store": "^10.0.0", + "electron-updater": "^6.3.9", + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.4", + "@types/node": "^22.15.0", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^4.5.2", + "electron": "33.4.0", + "electron-builder": "^25.1.8", + "electron-vite": "^3.1.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "tailwindcss": "^4.1.4", + "typescript": "^5.9.3", + "vite": "^6.3.5", + }, + }, "packages/docs": { "name": "docs", "version": "0.0.0", @@ -588,6 +615,8 @@ }, }, "packages": { + "7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="], + "@actions/core": ["@actions/core@2.0.1", "", { "dependencies": { "@actions/exec": "^2.0.0", "@actions/http-client": "^3.0.0" } }, "sha512-oBfqT3GwkvLlo1fjvhQLQxuwZCGTarTE5OuZ2Wg10hvhBj7LRIlF611WT4aZS6fDhO5ZKlY7lCAZTlpmyaHaeg=="], "@actions/exec": ["@actions/exec@2.0.0", "", { "dependencies": { "@actions/io": "^2.0.0" } }, "sha512-k8ngrX2voJ/RIN6r9xB82NVqKpnMRtxDoiO+g3olkIUpQNqjArXrCQceduQZCQj3P3xm32pChRLqRrtXTlqhIw=="], @@ -848,6 +877,8 @@ "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ=="], + "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA=="], + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="], "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], @@ -954,6 +985,8 @@ "@comp/app": ["@comp/app@workspace:apps/app"], + "@comp/device-agent": ["@comp/device-agent@workspace:packages/device-agent"], + "@comp/integration-platform": ["@comp/integration-platform@workspace:packages/integration-platform"], "@comp/portal": ["@comp/portal@workspace:apps/portal"], @@ -974,6 +1007,8 @@ "@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="], + "@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="], + "@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="], "@discordjs/builders": ["@discordjs/builders@1.13.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.33", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w=="], @@ -1016,6 +1051,18 @@ "@electric-sql/client": ["@electric-sql/client@1.0.14", "", { "dependencies": { "@microsoft/fetch-event-source": "^2.0.1" }, "optionalDependencies": { "@rollup/rollup-darwin-arm64": "^4.18.1" } }, "sha512-LtPAfeMxXRiYS0hyDQ5hue2PjljUiK9stvzsVyVb4nwxWQxfOWTSF42bHTs/o5i3x1T4kAQ7mwHpxa4A+f8X7Q=="], + "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], + + "@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="], + + "@electron/notarize": ["@electron/notarize@2.5.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", "promise-retry": "^2.0.1" } }, "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A=="], + + "@electron/osx-sign": ["@electron/osx-sign@1.3.1", "", { "dependencies": { "compare-version": "^0.1.2", "debug": "^4.3.4", "fs-extra": "^10.0.0", "isbinaryfile": "^4.0.8", "minimist": "^1.2.6", "plist": "^3.0.5" }, "bin": { "electron-osx-flat": "bin/electron-osx-flat.js", "electron-osx-sign": "bin/electron-osx-sign.js" } }, "sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw=="], + + "@electron/rebuild": ["@electron/rebuild@3.6.1", "", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "chalk": "^4.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "fs-extra": "^10.0.0", "got": "^11.7.0", "node-abi": "^3.45.0", "node-api-version": "^0.2.0", "node-gyp": "^9.0.0", "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", "tar": "^6.0.5", "yargs": "^17.0.1" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-f6596ZHpEq/YskUd8emYvOUne89ij8mQgjYFA5ru25QwbrRO+t1SImofdDv7kKOuWCmVOuU5tvfkbgGxIl3E/w=="], + + "@electron/universal": ["@electron/universal@2.0.1", "", { "dependencies": { "@electron/asar": "^3.2.7", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", "dir-compare": "^4.2.0", "fs-extra": "^11.1.1", "minimatch": "^9.0.3", "plist": "^3.1.0" } }, "sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA=="], + "@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], @@ -1108,6 +1155,8 @@ "@fontsource-variable/plus-jakarta-sans": ["@fontsource-variable/plus-jakarta-sans@5.2.8", "", {}, "sha512-iQecBizIdZxezODNHzOn4SvvRMrZL/S8k4MEXGDynCmUrImVW0VmX+tIAMqnADwH4haXlHSXqMgU6+kcfBQJdw=="], + "@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="], + "@google-cloud/precise-date": ["@google-cloud/precise-date@4.0.0", "", {}, "sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA=="], "@google/genai": ["@google/genai@1.34.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.24.0" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-vu53UMPvjmb7PGzlYu6Tzxso8Dfhn+a7eQFaS2uNemVtDZKwzSpJ5+ikqBbXplF7RGB1STcVDqCkPvquiwb2sw=="], @@ -1288,6 +1337,10 @@ "@lukeed/csprng": ["@lukeed/csprng@1.1.0", "", {}, "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA=="], + "@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="], + + "@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="], + "@mediapipe/tasks-vision": ["@mediapipe/tasks-vision@0.10.17", "", {}, "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg=="], "@mendable/firecrawl-js": ["@mendable/firecrawl-js@4.9.3", "", { "dependencies": { "axios": "^1.12.2", "typescript-event-target": "^1.1.1", "zod": "^3.23.8", "zod-to-json-schema": "^3.23.0" } }, "sha512-1k6qv0RiFHanx1XQE+DqEjdaQk0IXbsz/MF7FFrHCQX/oPHXm3TtA5gNNvUIogfX1mghgkVthKObmBNoUhVB1Q=="], @@ -1424,6 +1477,10 @@ "@novu/react": ["@novu/react@3.11.0", "", { "dependencies": { "@novu/js": "3.11.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" }, "optionalPeers": ["react-dom"] }, "sha512-VoDh2DNjyP8JuzsLUXc5md9UEWF1MTQamMWjYeEv8WxUtDuYBkYGYjs7x/80kZXZ0wBJDuPsstypYUzjhsOwnw=="], + "@npmcli/fs": ["@npmcli/fs@2.1.2", "", { "dependencies": { "@gar/promisify": "^1.1.3", "semver": "^7.3.5" } }, "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ=="], + + "@npmcli/move-file": ["@npmcli/move-file@2.0.1", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ=="], + "@number-flow/react": ["@number-flow/react@0.5.11", "", { "dependencies": { "esm-env": "^1.1.4", "number-flow": "0.5.9" }, "peerDependencies": { "react": "^18 || ^19", "react-dom": "^18 || ^19" } }, "sha512-xo7QFAfQlKiIRbLjoIjZNXsNQmY+QxkyxkNWy+BCP9u1/SkqyqQ/1pYWa7d175V2r4OdmvCvlAnACSYOP1pTaw=="], "@nuxt/opencollective": ["@nuxt/opencollective@0.4.1", "", { "dependencies": { "consola": "^3.2.3" }, "bin": { "opencollective": "bin/opencollective.js" } }, "sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ=="], @@ -2002,6 +2059,8 @@ "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], + "@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="], + "@t3-oss/env-core": ["@t3-oss/env-core@0.13.10", "", { "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["arktype", "typescript", "valibot", "zod"] }, "sha512-NNFfdlJ+HmPHkLi2HKy7nwuat9SIYOxei9K10lO2YlcSObDILY7mHZNSHsieIM3A0/5OOzw/P/b+yLvPdaG52g=="], "@t3-oss/env-nextjs": ["@t3-oss/env-nextjs@0.13.10", "", { "dependencies": { "@t3-oss/env-core": "0.13.10" }, "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["arktype", "typescript", "valibot", "zod"] }, "sha512-JfSA2WXOnvcc/uMdp31paMsfbYhhdvLLRxlwvrnlPE9bwM/n0Z+Qb9xRv48nPpvfMhOrkrTYw1I5Yc06WIKBJQ=="], @@ -2040,6 +2099,8 @@ "@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="], + "@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="], + "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.0", "", {}, "sha512-RPfGuk2bDZgcu9bAJodvO2lnZeHuz4/71HjZ0bGb/SPg8+lyTA+RLSKQvo7fSmPSi8/vcH3aKQ8EM9ywf1olaw=="], "@tanstack/form-core": ["@tanstack/form-core@1.27.6", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.0", "@tanstack/pacer-lite": "^0.1.1", "@tanstack/store": "^0.7.7" } }, "sha512-1C4PUpOcCpivddKxtAeqdeqncxnPKiPpTVDRknDExCba+6zCsAjxgL+p3qYA3hu+EFyUAdW71rU+uqYbEa7qqA=="], @@ -2150,6 +2211,8 @@ "@tokenlens/models": ["@tokenlens/models@1.3.0", "", { "dependencies": { "@tokenlens/core": "1.3.0" } }, "sha512-9mx7ZGeewW4ndXAiD7AT1bbCk4OpJeortbjHHyNkgap+pMPPn1chY6R5zqe1ggXIUzZ2l8VOAKfPqOvpcrisJw=="], + "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], + "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], "@trigger.dev/build": ["@trigger.dev/build@4.0.6", "", { "dependencies": { "@trigger.dev/core": "4.0.6", "pkg-types": "^1.1.3", "tinyglobby": "^0.2.2", "tsconfck": "3.1.3" } }, "sha512-NJ7N50mNTW43h7jJ/ynsjX1xT+R7fisxVytmwnjb8ZaCgScNp1zDdzGlJjhIjiIWU7G83qpZE1H8eoqjo7tOyg=="], @@ -2210,6 +2273,8 @@ "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], + "@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="], + "@types/canvas-confetti": ["@types/canvas-confetti@1.9.0", "", {}, "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg=="], "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], @@ -2306,10 +2371,14 @@ "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.0", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA=="], + "@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="], + "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="], + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], "@types/is-stream": ["@types/is-stream@1.1.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-jkZatu4QVbR60mpIzjINmtS1ZF4a/FqdTUTBeQDVOQ2PYyidtwFKr0B5G6ERukKwliq+7mIXvxyppwzG5EgRYg=="], @@ -2336,6 +2405,8 @@ "@types/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="], + "@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="], + "@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="], "@types/lodash": ["@types/lodash@4.17.23", "", {}, "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA=="], @@ -2364,6 +2435,8 @@ "@types/pako": ["@types/pako@2.0.4", "", {}, "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw=="], + "@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="], + "@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="], "@types/promise.allsettled": ["@types/promise.allsettled@1.0.6", "", {}, "sha512-wA0UT0HeT2fGHzIFV9kWpYz5mdoyLxKrTgMdZQM++5h6pYAFH73HXcQhefg24nD1yivUFEn5KU+EF4b+CXJ4Wg=="], @@ -2384,6 +2457,8 @@ "@types/readdir-glob": ["@types/readdir-glob@1.1.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg=="], + "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], + "@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], "@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], @@ -2420,6 +2495,8 @@ "@types/validator": ["@types/validator@13.15.10", "", {}, "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA=="], + "@types/verror": ["@types/verror@1.10.11", "", {}, "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg=="], + "@types/webxr": ["@types/webxr@0.5.24", "", {}, "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="], "@types/ws": ["@types/ws@7.4.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww=="], @@ -2638,6 +2715,10 @@ "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + "app-builder-bin": ["app-builder-bin@5.0.0-alpha.10", "", {}, "sha512-Ev4jj3D7Bo+O0GPD2NMvJl+PGiBAfS7pUGawntBNpCbxtpncfUixqFj9z9Jme7V7s3LBGqsWZZP54fxBX3JKJw=="], + + "app-builder-lib": ["app-builder-lib@25.1.8", "", { "dependencies": { "@develar/schema-utils": "~2.6.5", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.1", "@electron/rebuild": "3.6.1", "@electron/universal": "2.0.1", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", "bluebird-lst": "^1.0.9", "builder-util": "25.1.7", "builder-util-runtime": "9.2.10", "chromium-pickle-js": "^0.2.0", "config-file-ts": "0.2.8-rc1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", "electron-publish": "25.1.7", "form-data": "^4.0.0", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "is-ci": "^3.0.0", "isbinaryfile": "^5.0.0", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", "minimatch": "^10.0.0", "resedit": "^1.7.0", "sanitize-filename": "^1.6.3", "semver": "^7.3.8", "tar": "^6.1.12", "temp-file": "^3.4.0" }, "peerDependencies": { "dmg-builder": "25.1.8", "electron-builder-squirrel-windows": "25.1.8" } }, "sha512-pCqe7dfsQFBABC1jeKZXQWhGcCPF3rPCXDdfqVKjIeWBcXzyC1iOWZdfFhGl+S9MyE/k//DFmC6FzuGAUudNDg=="], + "append-field": ["append-field@1.0.0", "", {}, "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="], "aproba": ["aproba@1.2.0", "", {}, "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="], @@ -2694,16 +2775,24 @@ "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="], + "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + "async-exit-hook": ["async-exit-hook@2.0.1", "", {}, "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw=="], + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], "async-retry": ["async-retry@1.3.3", "", { "dependencies": { "retry": "0.13.1" } }, "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw=="], "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], + "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], + "atomically": ["atomically@2.1.0", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q=="], + "attr-accept": ["attr-accept@2.2.5", "", {}, "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ=="], "autoprefixer": ["autoprefixer@10.4.23", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA=="], @@ -2786,10 +2875,14 @@ "bluebird": ["bluebird@3.4.7", "", {}, "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="], + "bluebird-lst": ["bluebird-lst@1.0.9", "", { "dependencies": { "bluebird": "^3.5.5" } }, "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw=="], + "body-parser": ["body-parser@1.20.4", "", { "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" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="], "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], + "botid": ["botid@1.5.10", "", { "peerDependencies": { "next": "*", "react": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["next", "react"] }, "sha512-hhgty1u0CxozqTqLbTQMtYBwmWdzWZTAsBCvN7/qhkN3fM7MlXacmmcMoyc0f+vV+U6RRoLYdlo32td+PhJyew=="], "bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="], @@ -2820,6 +2913,10 @@ "bufferutil": ["bufferutil@4.1.0", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw=="], + "builder-util": ["builder-util@25.1.7", "", { "dependencies": { "7zip-bin": "~5.2.0", "@types/debug": "^4.1.6", "app-builder-bin": "5.0.0-alpha.10", "bluebird-lst": "^1.0.9", "builder-util-runtime": "9.2.10", "chalk": "^4.1.2", "cross-spawn": "^7.0.3", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "is-ci": "^3.0.0", "js-yaml": "^4.1.0", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0" } }, "sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww=="], + + "builder-util-runtime": ["builder-util-runtime@9.2.10", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw=="], + "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], @@ -2834,6 +2931,12 @@ "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + "cacache": ["cacache@16.1.3", "", { "dependencies": { "@npmcli/fs": "^2.1.0", "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", "fs-minipass": "^2.1.0", "glob": "^8.0.1", "infer-owner": "^1.0.4", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^9.0.0", "tar": "^6.1.11", "unique-filename": "^2.0.0" } }, "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ=="], + + "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="], + + "cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="], + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -2900,6 +3003,8 @@ "chromium-bidi": ["chromium-bidi@12.0.1", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-fGg+6jr0xjQhzpy5N4ErZxQ4wF7KLEvhGZXD6EgvZKDhu7iOhZXnZhcDxPJDcwTcrD48NPzOCo84RP2lv3Z+Cg=="], + "chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="], + "ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], @@ -2924,6 +3029,8 @@ "cli-table3": ["cli-table3@0.6.5", "", { "dependencies": { "string-width": "^4.2.0" }, "optionalDependencies": { "@colors/colors": "1.5.0" } }, "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ=="], + "cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="], + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], @@ -2932,6 +3039,8 @@ "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], + "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], @@ -2950,6 +3059,8 @@ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "color-support": ["color-support@1.1.3", "", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="], + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], @@ -2962,6 +3073,8 @@ "compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="], + "compare-version": ["compare-version@0.1.2", "", {}, "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A=="], + "component-emitter": ["component-emitter@1.3.1", "", {}, "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ=="], "compress-commons": ["compress-commons@6.0.2", "", { "dependencies": { "crc-32": "^1.2.0", "crc32-stream": "^6.0.0", "is-stream": "^2.0.1", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg=="], @@ -2972,10 +3085,14 @@ "concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="], + "conf": ["conf@14.0.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^9.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.7.2", "uint8array-extras": "^1.4.0" } }, "sha512-L6BuueHTRuJHQvQVc6YXYZRtN5vJUtOdCTLn0tRYYV5azfbAFcPghB5zEE40mVrV6w7slMTqUfkDomutIK14fw=="], + "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], "config-chain": ["config-chain@1.1.13", "", { "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ=="], + "config-file-ts": ["config-file-ts@0.2.8-rc1", "", { "dependencies": { "glob": "^10.3.12", "typescript": "^5.4.3" } }, "sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg=="], + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], "console-control-strings": ["console-control-strings@1.1.0", "", {}, "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="], @@ -3022,6 +3139,8 @@ "cosmiconfig-typescript-loader": ["cosmiconfig-typescript-loader@6.2.0", "", { "dependencies": { "jiti": "^2.6.1" }, "peerDependencies": { "@types/node": "*", "cosmiconfig": ">=9", "typescript": ">=5" } }, "sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ=="], + "crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="], + "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], "crc32-stream": ["crc32-stream@6.0.0", "", { "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^4.0.0" } }, "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g=="], @@ -3154,6 +3273,8 @@ "debounce": ["debounce@2.2.0", "", {}, "sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw=="], + "debounce-fn": ["debounce-fn@6.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], @@ -3164,6 +3285,8 @@ "decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="], + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + "dedent": ["dedent@1.7.1", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg=="], "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], @@ -3182,6 +3305,8 @@ "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], + "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], @@ -3212,6 +3337,8 @@ "detect-newline": ["detect-newline@3.1.0", "", {}, "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="], + "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], @@ -3226,6 +3353,8 @@ "dingbat-to-unicode": ["dingbat-to-unicode@1.0.1", "", {}, "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w=="], + "dir-compare": ["dir-compare@4.2.0", "", { "dependencies": { "minimatch": "^3.0.5", "p-limit": "^3.1.0 " } }, "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ=="], + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], "discord-api-types": ["discord-api-types@0.38.37", "", {}, "sha512-Cv47jzY1jkGkh5sv0bfHYqGgKOWO1peOrGMkDFM4UmaGMOTgOW8QSexhvixa9sVOiz8MnVOBryWYyw/CEVhj7w=="], @@ -3234,6 +3363,10 @@ "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + "dmg-builder": ["dmg-builder@25.1.8", "", { "dependencies": { "app-builder-lib": "25.1.8", "builder-util": "25.1.7", "builder-util-runtime": "9.2.10", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" }, "optionalDependencies": { "dmg-license": "^1.0.11" } }, "sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ=="], + + "dmg-license": ["dmg-license@1.0.11", "", { "dependencies": { "@types/plist": "^3.0.1", "@types/verror": "^1.10.3", "ajv": "^6.10.0", "crc": "^3.8.0", "iconv-corefoundation": "^1.1.7", "plist": "^3.0.4", "smart-buffer": "^4.0.2", "verror": "^1.10.0" }, "os": "darwin", "bin": { "dmg-license": "bin/dmg-license.js" } }, "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q=="], + "dnd-core": ["dnd-core@16.0.1", "", { "dependencies": { "@react-dnd/asap": "^5.0.1", "@react-dnd/invariant": "^4.0.1", "redux": "^4.2.0" } }, "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng=="], "docs": ["docs@workspace:packages/docs"], @@ -3254,7 +3387,7 @@ "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], - "dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="], + "dot-prop": ["dot-prop@9.0.0", "", { "dependencies": { "type-fest": "^4.18.2" } }, "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ=="], "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], @@ -3282,8 +3415,24 @@ "effect": ["effect@3.18.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="], + "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], + + "electron": ["electron@33.4.0", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^20.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-AO+Q/ygWwKKs+JtNEFgfS5ntjG3TA2HX7s4IEbiYi6lktaocuLP2oScG1/mmKRuUWoOcow2RRsf995L2mM4bvQ=="], + + "electron-builder": ["electron-builder@25.1.8", "", { "dependencies": { "app-builder-lib": "25.1.8", "builder-util": "25.1.7", "builder-util-runtime": "9.2.10", "chalk": "^4.1.2", "dmg-builder": "25.1.8", "fs-extra": "^10.1.0", "is-ci": "^3.0.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" } }, "sha512-poRgAtUHHOnlzZnc9PK4nzG53xh74wj2Jy7jkTrqZ0MWPoHGh1M2+C//hGeYdA+4K8w4yiVCNYoLXF7ySj2Wig=="], + + "electron-builder-squirrel-windows": ["electron-builder-squirrel-windows@25.1.8", "", { "dependencies": { "app-builder-lib": "25.1.8", "archiver": "^5.3.1", "builder-util": "25.1.7", "fs-extra": "^10.1.0" } }, "sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg=="], + + "electron-publish": ["electron-publish@25.1.7", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "builder-util": "25.1.7", "builder-util-runtime": "9.2.10", "chalk": "^4.1.2", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg=="], + + "electron-store": ["electron-store@10.1.0", "", { "dependencies": { "conf": "^14.0.0", "type-fest": "^4.41.0" } }, "sha512-oL8bRy7pVCLpwhmXy05Rh/L6O93+k9t6dqSw0+MckIc3OmCTZm6Mp04Q4f/J0rtu84Ky6ywkR8ivtGOmrq+16w=="], + "electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="], + "electron-updater": ["electron-updater@6.7.3", "", { "dependencies": { "builder-util-runtime": "9.5.1", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-EgkT8Z9noqXKbwc3u5FkJA+r48jwZ5DTUiOkJMOTEEH//n5Am6wfQGz7nvSFEA2oIAMv9jRzn5JKTyWeSKOPgg=="], + + "electron-vite": ["electron-vite@3.1.0", "", { "dependencies": { "@babel/core": "^7.26.10", "@babel/plugin-transform-arrow-functions": "^7.25.9", "cac": "^6.7.14", "esbuild": "^0.25.1", "magic-string": "^0.30.17", "picocolors": "^1.1.1" }, "peerDependencies": { "@swc/core": "^1.0.0", "vite": "^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["@swc/core"], "bin": { "electron-vite": "bin/electron-vite.js" } }, "sha512-M7aAzaRvSl5VO+6KN4neJCYLHLpF/iWo5ztchI/+wMxIieDZQqpbCYfaEHHHPH6eupEzfvZdLYdPdmvGqoVe0Q=="], + "embla-carousel": ["embla-carousel@8.6.0", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="], "embla-carousel-react": ["embla-carousel-react@8.6.0", "", { "dependencies": { "embla-carousel": "8.6.0", "embla-carousel-reactive-utils": "8.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA=="], @@ -3302,6 +3451,8 @@ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="], + "encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="], "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], @@ -3324,6 +3475,8 @@ "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="], + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], "error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="], @@ -3350,6 +3503,8 @@ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], + "es6-promise": ["es6-promise@4.2.8", "", {}, "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="], "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], @@ -3434,6 +3589,8 @@ "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], + "express": ["express@4.22.1", "", { "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" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="], "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], @@ -3508,6 +3665,8 @@ "file-type": ["file-type@21.1.1", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-ifJXo8zUqbQ/bLbl9sFoqHNTNWbnPY1COImFfM6CCy7z+E+jC1eY9YfOKkx0fckIg+VljAy2/87T61fp0+eEkg=="], + "filelist": ["filelist@1.0.4", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "finalhandler": ["finalhandler@1.3.2", "", { "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" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], @@ -3568,6 +3727,8 @@ "fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], + "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], + "fs-monkey": ["fs-monkey@1.1.0", "", {}, "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw=="], "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], @@ -3640,6 +3801,8 @@ "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], + "global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="], + "global-directory": ["global-directory@4.0.1", "", { "dependencies": { "ini": "4.1.1" } }, "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q=="], "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], @@ -3658,6 +3821,8 @@ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "got": ["got@11.8.6", "", { "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" } }, "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "graphql": ["graphql@16.12.0", "", {}, "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ=="], @@ -3754,12 +3919,16 @@ "htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="], + "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], "http-signature": ["http-signature@1.2.0", "", { "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", "sshpk": "^1.7.0" } }, "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ=="], + "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="], + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], @@ -3772,6 +3941,8 @@ "hyphenate-style-name": ["hyphenate-style-name@1.1.0", "", {}, "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw=="], + "iconv-corefoundation": ["iconv-corefoundation@1.1.7", "", { "dependencies": { "cli-truncate": "^2.1.0", "node-addon-api": "^1.6.3" }, "os": "darwin" }, "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ=="], + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -3796,6 +3967,8 @@ "index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="], + "infer-owner": ["infer-owner@1.0.4", "", {}, "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A=="], + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], @@ -3842,6 +4015,8 @@ "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + "is-ci": ["is-ci@3.0.1", "", { "dependencies": { "ci-info": "^3.2.0" }, "bin": { "is-ci": "bin.js" } }, "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ=="], + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], @@ -3876,6 +4051,8 @@ "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], + "is-lambda": ["is-lambda@1.0.1", "", {}, "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ=="], + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], @@ -3928,6 +4105,8 @@ "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + "isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "isstream": ["isstream@0.1.2", "", {}, "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g=="], @@ -3956,6 +4135,8 @@ "jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="], + "jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="], + "java-properties": ["java-properties@1.0.2", "", {}, "sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ=="], "jest": ["jest@30.2.0", "", { "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", "import-local": "^3.2.0", "jest-cli": "30.2.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"], "bin": "./bin/jest.js" }, "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A=="], @@ -4096,6 +4277,8 @@ "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], + "lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="], + "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="], "leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="], @@ -4228,6 +4411,8 @@ "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], + "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], + "lowlight": ["lowlight@1.20.0", "", { "dependencies": { "fault": "^1.0.0", "highlight.js": "~10.7.0" } }, "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw=="], "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -4248,6 +4433,8 @@ "make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="], + "make-fetch-happen": ["make-fetch-happen@10.2.1", "", { "dependencies": { "agentkeepalive": "^4.2.1", "cacache": "^16.1.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-fetch": "^2.0.3", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.3", "promise-retry": "^2.0.1", "socks-proxy-agent": "^7.0.0", "ssri": "^9.0.0" } }, "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w=="], + "makeerror": ["makeerror@1.0.12", "", { "dependencies": { "tmpl": "1.0.5" } }, "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg=="], "mammoth": ["mammoth@1.11.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.6", "argparse": "~1.0.3", "base64-js": "^1.5.1", "bluebird": "~3.4.0", "dingbat-to-unicode": "^1.0.1", "jszip": "^3.7.1", "lop": "^0.4.2", "path-is-absolute": "^1.0.0", "underscore": "^1.13.1", "xmlbuilder": "^10.0.0" }, "bin": { "mammoth": "bin/mammoth" } }, "sha512-BcEqqY/BOwIcI1iR5tqyVlqc3KIaMRa4egSoK83YAVrBf6+yqdAAbtUcFDCWX8Zef8/fgNZ6rl4VUv+vVX8ddQ=="], @@ -4262,6 +4449,8 @@ "marky": ["marky@1.3.0", "", {}, "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="], + "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "md-to-react-email": ["md-to-react-email@5.0.5", "", { "dependencies": { "marked": "7.0.4" }, "peerDependencies": { "react": "^18.0 || ^19.0" } }, "sha512-OvAXqwq57uOk+WZqFFNCMZz8yDp8BD3WazW1wAKHUrPbbdr89K9DWS6JXY09vd9xNdPNeurI8DU/X4flcfaD8A=="], @@ -4398,6 +4587,8 @@ "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], "minimal-polyfills": ["minimal-polyfills@2.2.3", "", {}, "sha512-oxdmJ9cL+xV72h0xYxp4tP2d5/fTBpP45H8DIOn9pASuF8a3IYTf+25fMGDYGiWW+MFsuog6KD6nfmhZJQ+uUw=="], @@ -4408,6 +4599,16 @@ "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "minipass-collect": ["minipass-collect@1.0.2", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA=="], + + "minipass-fetch": ["minipass-fetch@2.1.2", "", { "dependencies": { "minipass": "^3.1.6", "minipass-sized": "^1.0.3", "minizlib": "^2.1.2" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA=="], + + "minipass-flush": ["minipass-flush@1.0.5", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw=="], + + "minipass-pipeline": ["minipass-pipeline@1.2.4", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="], + + "minipass-sized": ["minipass-sized@1.0.3", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g=="], + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], @@ -4474,10 +4675,14 @@ "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], + "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="], + "node-abort-controller": ["node-abort-controller@3.1.1", "", {}, "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="], "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + "node-api-version": ["node-api-version@0.2.1", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "node-emoji": ["node-emoji@1.11.0", "", { "dependencies": { "lodash": "^4.17.21" } }, "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A=="], @@ -4584,6 +4789,8 @@ "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], + "p-each-series": ["p-each-series@3.0.0", "", {}, "sha512-lastgtAdoH9YaLyDa5i5z64q+kzOcQHsQ5SsZJD3q0VEyI8mq872S3geuNbRUQLVAE9siMfgKrpj7MloKFHruw=="], "p-event": ["p-event@6.0.1", "", { "dependencies": { "p-timeout": "^6.1.2" } }, "sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w=="], @@ -4670,6 +4877,8 @@ "pdfjs-dist": ["pdfjs-dist@5.4.296", "", { "optionalDependencies": { "@napi-rs/canvas": "^0.1.80" } }, "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q=="], + "pe-library": ["pe-library@0.4.1", "", {}, "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw=="], + "peberminta": ["peberminta@0.9.0", "", {}, "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ=="], "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], @@ -4722,6 +4931,8 @@ "playwright-core": ["playwright-core@1.57.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ=="], + "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], + "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="], "points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], @@ -4794,6 +5005,10 @@ "prom-client": ["prom-client@15.1.3", "", { "dependencies": { "@opentelemetry/api": "^1.4.0", "tdigest": "^0.1.1" } }, "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g=="], + "promise-inflight": ["promise-inflight@1.0.1", "", {}, "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="], + + "promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="], + "promise-worker-transferable": ["promise-worker-transferable@1.0.4", "", { "dependencies": { "is-promise": "^2.1.0", "lie": "^3.0.2" } }, "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw=="], "promise.allsettled": ["promise.allsettled@1.0.7", "", { "dependencies": { "array.prototype.map": "^1.0.5", "call-bind": "^1.0.2", "define-properties": "^1.2.0", "es-abstract": "^1.22.1", "get-intrinsic": "^1.2.1", "iterate-value": "^1.0.2" } }, "sha512-hezvKvQQmsFkOdrZfYxUxkyxl8mgFQeT259Ajj9PXdbg9VzBCWrItOev72JyWxkCD5VSSqAeHmlN3tWx4DlmsA=="], @@ -4870,6 +5085,8 @@ "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], + "raf": ["raf@3.4.1", "", { "dependencies": { "performance-now": "^2.1.0" } }, "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA=="], "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], @@ -4946,6 +5163,8 @@ "react-wrap-balancer": ["react-wrap-balancer@1.1.1", "", { "peerDependencies": { "react": ">=16.8.0 || ^17.0.0 || ^18" } }, "sha512-AB+l7FPRWl6uZ28VcJ8skkwLn2+UC62bjiw8tQUrZPlEWDVnR9MG0lghyn7EyxuJSsFEpht4G+yh2WikEqQ/5Q=="], + "read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": { "read-binary-file-arch": "cli.js" } }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="], + "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], "read-package-up": ["read-package-up@11.0.0", "", { "dependencies": { "find-up-simple": "^1.0.0", "read-pkg": "^9.0.0", "type-fest": "^4.6.0" } }, "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ=="], @@ -5022,6 +5241,8 @@ "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], + "resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="], + "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], "resend": ["resend@6.6.0", "", { "dependencies": { "svix": "1.76.1" }, "peerDependencies": { "@react-email/render": "*" }, "optionalPeers": ["@react-email/render"] }, "sha512-d1WoOqSxj5x76JtQMrieNAG1kZkh4NU4f+Je1yq4++JsDpLddhEwnJlNfvkCzvUuZy9ZquWmMMAm2mENd2JvRw=="], @@ -5030,12 +5251,16 @@ "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], + "resolve-cwd": ["resolve-cwd@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg=="], "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="], + "responsive-react-email": ["responsive-react-email@0.0.5", "", { "peerDependencies": { "react": "18.x", "react-email": "1.x" } }, "sha512-r+Z6Yp6G3Sm9eKmttsb8GVij25rXJGN2eoQ9OfMcuVMfBfq1NdytIFLBo/6wdMW1zw+ko1FEUG/zgRyK9UuYLw=="], "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], @@ -5050,6 +5275,8 @@ "rimraf": ["rimraf@6.1.2", "", { "dependencies": { "glob": "^13.0.0", "package-json-from-dist": "^1.0.1" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g=="], + "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], + "robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="], "rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="], @@ -5088,6 +5315,8 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "sanitize-filename": ["sanitize-filename@1.6.3", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg=="], + "sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="], "saxes": ["saxes@5.0.1", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw=="], @@ -5112,12 +5341,16 @@ "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], + "semver-diff": ["semver-diff@5.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-0HbGtOm+S7T6NGQ/pxJSJipJvc4DK3FcRVMRkhsIwJDJ4Jcz5DQC1cPPzB5GhzyHjwttW878HaWQq46CkL3cqg=="], "semver-regex": ["semver-regex@4.0.5", "", {}, "sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw=="], "send": ["send@0.19.2", "", { "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" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], + "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], + "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], "seroval": ["seroval@1.3.2", "", {}, "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="], @@ -5170,6 +5403,8 @@ "signale": ["signale@1.4.0", "", { "dependencies": { "chalk": "^2.3.2", "figures": "^2.0.0", "pkg-conf": "^2.1.0" } }, "sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w=="], + "simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="], + "simple-wcswidth": ["simple-wcswidth@1.1.2", "", {}, "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw=="], "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], @@ -5180,6 +5415,8 @@ "slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], + "slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="], + "slug": ["slug@6.1.0", "", {}, "sha512-x6vLHCMasg4DR2LPiyFGI0gJJhywY6DTiGhCrOMzb3SOk/0JVLIaL4UhyFSHu04SD3uAavrKY/K3zZ3i6iRcgA=="], "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], @@ -5238,6 +5475,8 @@ "sshpk": ["sshpk@1.18.0", "", { "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", "bcrypt-pbkdf": "^1.0.0", "dashdash": "^1.12.0", "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, "bin": { "sshpk-conv": "bin/sshpk-conv", "sshpk-sign": "bin/sshpk-sign", "sshpk-verify": "bin/sshpk-verify" } }, "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ=="], + "ssri": ["ssri@9.0.1", "", { "dependencies": { "minipass": "^3.1.1" } }, "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q=="], + "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="], "stack-generator": ["stack-generator@2.0.10", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ=="], @@ -5254,6 +5493,8 @@ "stacktrace-js": ["stacktrace-js@2.0.2", "", { "dependencies": { "error-stack-parser": "^2.0.6", "stack-generator": "^2.0.5", "stacktrace-gps": "^3.0.4" } }, "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg=="], + "stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="], + "state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="], "stats-gl": ["stats-gl@2.4.2", "", { "dependencies": { "@types/three": "*", "three": "^0.170.0" } }, "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ=="], @@ -5326,6 +5567,10 @@ "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], + "stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="], + + "stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="], + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], @@ -5336,6 +5581,8 @@ "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + "sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="], + "super-regex": ["super-regex@1.1.0", "", { "dependencies": { "function-timeout": "^1.0.1", "make-asynchronous": "^1.0.1", "time-span": "^5.1.0" } }, "sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ=="], "superagent": ["superagent@10.2.3", "", { "dependencies": { "component-emitter": "^1.3.1", "cookiejar": "^2.1.4", "debug": "^4.3.7", "fast-safe-stringify": "^2.1.1", "form-data": "^4.0.4", "formidable": "^3.5.4", "methods": "^1.1.2", "mime": "2.6.0", "qs": "^6.11.2" } }, "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig=="], @@ -5392,6 +5639,8 @@ "temp-dir": ["temp-dir@3.0.0", "", {}, "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw=="], + "temp-file": ["temp-file@3.4.0", "", { "dependencies": { "async-exit-hook": "^2.0.1", "fs-extra": "^10.0.0" } }, "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg=="], + "tempy": ["tempy@3.1.0", "", { "dependencies": { "is-stream": "^3.0.0", "temp-dir": "^3.0.0", "type-fest": "^2.12.2", "unique-string": "^3.0.0" } }, "sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g=="], "terser": ["terser@5.44.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw=="], @@ -5434,6 +5683,8 @@ "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + "tiny-typed-emitter": ["tiny-typed-emitter@2.1.0", "", {}, "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], @@ -5452,6 +5703,8 @@ "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], + "tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="], + "tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -5484,6 +5737,8 @@ "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="], + "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], "ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="], @@ -5600,6 +5855,10 @@ "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + "unique-filename": ["unique-filename@2.0.1", "", { "dependencies": { "unique-slug": "^3.0.0" } }, "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A=="], + + "unique-slug": ["unique-slug@3.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w=="], + "unique-string": ["unique-string@3.0.0", "", { "dependencies": { "crypto-random-string": "^4.0.0" } }, "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ=="], "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], @@ -5660,6 +5919,8 @@ "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + "utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "utility-types": ["utility-types@3.11.0", "", {}, "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw=="], @@ -5694,7 +5955,7 @@ "victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="], - "vite": ["vite@7.3.0", "", { "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" }, "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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="], + "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], "vite-node": ["vite-node@3.2.4", "", { "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" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], @@ -5754,6 +6015,8 @@ "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + "when-exit": ["when-exit@2.1.5", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], @@ -5992,6 +6255,8 @@ "@comp/app/resend": ["resend@4.8.0", "", { "dependencies": { "@react-email/render": "1.1.2" } }, "sha512-R8eBOFQDO6dzRTDmaMEdpqrkmgSjPpVXt4nGfWsZdYOet0kqra0xgbvTES6HmCriZEXbmGk3e0DiGIaLFTFSHA=="], + "@comp/device-agent/@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="], + "@comp/portal/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], @@ -6016,6 +6281,30 @@ "@ecies/ciphers/@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], + "@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], + + "@electron/asar/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "@electron/asar/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@electron/notarize/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + + "@electron/osx-sign/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="], + + "@electron/rebuild/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "@electron/rebuild/node-gyp": ["node-gyp@9.4.1", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^7.1.4", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.0.3", "nopt": "^6.0.0", "npmlog": "^6.0.0", "rimraf": "^3.0.2", "semver": "^7.3.5", "tar": "^6.1.2", "which": "^2.0.2" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ=="], + + "@electron/rebuild/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], + + "@electron/rebuild/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -6066,6 +6355,8 @@ "@langchain/openai/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + "@mendable/firecrawl-js/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@modelcontextprotocol/sdk/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], @@ -6104,14 +6395,16 @@ "@novu/js/socket.io-client": ["socket.io-client@4.7.2", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", "engine.io-client": "~6.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w=="], + "@npmcli/move-file/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "@npmcli/move-file/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + "@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="], "@paralleldrive/cuid2/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], "@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], - "@playwright/experimental-ct-core/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], - "@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], "@prisma/engines/@prisma/debug": ["@prisma/debug@6.18.0", "", {}, "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg=="], @@ -6286,6 +6579,8 @@ "@types/cheerio/cheerio": ["cheerio@1.1.2", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.0.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.12.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg=="], + "@types/plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], + "@types/react-syntax-highlighter/@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], @@ -6310,6 +6605,20 @@ "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "app-builder-lib/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + + "app-builder-lib/dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="], + + "app-builder-lib/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "app-builder-lib/hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], + + "app-builder-lib/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "app-builder-lib/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], + + "app-builder-lib/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], + "archiver-utils/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "archiver-utils/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], @@ -6324,10 +6633,16 @@ "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "bluebird-lst/bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="], + "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "builder-util/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "builder-util/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "c12/confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], "c12/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], @@ -6336,6 +6651,26 @@ "c12/pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + "cacache/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], + + "cacache/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], + + "cacache/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "cacache/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "cacache/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "cacache/p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="], + + "cacache/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + + "cacache/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], + + "cacheable-request/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], + + "cacheable-request/normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="], + "chainsaw/traverse": ["traverse@0.3.9", "", {}, "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="], "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -6354,18 +6689,30 @@ "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], + "cmdk/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + "compare-func/dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="], + "compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], "concat-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "conf/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "conf/env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], + + "config-file-ts/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + "cosmiconfig/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], "cosmiconfig/parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], "cosmiconfig-typescript-loader/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "crc/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + "crypto-random-string/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="], "css-tree/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -6384,9 +6731,13 @@ "degenerator/ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="], + "dir-compare/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "discord.js/undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="], - "dot-prop/is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], + "dmg-builder/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "dmg-builder/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], "dotenv-expand/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], @@ -6398,6 +6749,26 @@ "eciesjs/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "electron/@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="], + + "electron-builder/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "electron-builder-squirrel-windows/archiver": ["archiver@5.3.2", "", { "dependencies": { "archiver-utils": "^2.1.0", "async": "^3.2.4", "buffer-crc32": "^0.2.1", "readable-stream": "^3.6.0", "readdir-glob": "^1.1.2", "tar-stream": "^2.2.0", "zip-stream": "^4.1.0" } }, "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw=="], + + "electron-builder-squirrel-windows/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "electron-publish/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "electron-publish/mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], + + "electron-updater/builder-util-runtime": ["builder-util-runtime@9.5.1", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ=="], + + "electron-updater/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "electron-updater/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "electron-vite/esbuild": ["esbuild@0.25.12", "", { "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" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "engine.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], "engine.io-client/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="], @@ -6454,6 +6825,8 @@ "fetch-cookie/tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="], + "filelist/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "fleetctl/axios": ["axios@1.12.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg=="], @@ -6468,6 +6841,8 @@ "from2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "fstream/mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], "fstream/rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], @@ -6510,10 +6885,14 @@ "humanize-ms/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "iconv-corefoundation/node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="], + "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], + "is-ci/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + "is-wsl/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], "istanbul-lib-report/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -6564,6 +6943,16 @@ "load-json-file/parse-json": ["parse-json@4.0.0", "", { "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" } }, "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw=="], + "make-fetch-happen/http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], + + "make-fetch-happen/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + + "make-fetch-happen/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "make-fetch-happen/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "make-fetch-happen/socks-proxy-agent": ["socks-proxy-agent@7.0.0", "", { "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", "socks": "^2.6.2" } }, "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww=="], + "markdown-it/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "marked-terminal/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -6580,6 +6969,18 @@ "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "minipass-collect/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-fetch/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-fetch/minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], + + "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "monaco-editor/dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="], "monaco-editor/marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], @@ -7008,6 +7409,8 @@ "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], + "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], @@ -7016,6 +7419,8 @@ "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "promise-retry/retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + "promise-worker-transferable/is-promise": ["is-promise@2.2.2", "", {}, "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="], "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], @@ -7064,6 +7469,8 @@ "rimraf/glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="], + "roarr/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], + "safe-array-concat/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], "safe-push-apply/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], @@ -7086,6 +7493,8 @@ "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], + "shadcn/commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], "shadcn/kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], @@ -7114,6 +7523,8 @@ "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "ssri/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], "stacktrace-gps/source-map": ["source-map@0.5.6", "", {}, "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA=="], @@ -7158,6 +7569,8 @@ "tar/mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], + "temp-file/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + "tempy/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], "tempy/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], @@ -7192,8 +7605,14 @@ "verror/core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], + "vite/esbuild": ["esbuild@0.25.12", "", { "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" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "vite-node/vite": ["vite@7.3.0", "", { "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" }, "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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="], + "vitest/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + "vitest/vite": ["vite@7.3.0", "", { "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" }, "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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="], + "webpack/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], "webpack/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], @@ -7282,6 +7701,8 @@ "@comp/app/resend/@react-email/render": ["@react-email/render@1.1.2", "", { "dependencies": { "html-to-text": "^9.0.5", "prettier": "^3.5.3", "react-promise-suspense": "^0.3.4" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw=="], + "@comp/device-agent/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], "@dotenvx/dotenvx/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], @@ -7298,6 +7719,40 @@ "@dub/embed-react/vite/esbuild": ["esbuild@0.20.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.20.2", "@esbuild/android-arm": "0.20.2", "@esbuild/android-arm64": "0.20.2", "@esbuild/android-x64": "0.20.2", "@esbuild/darwin-arm64": "0.20.2", "@esbuild/darwin-x64": "0.20.2", "@esbuild/freebsd-arm64": "0.20.2", "@esbuild/freebsd-x64": "0.20.2", "@esbuild/linux-arm": "0.20.2", "@esbuild/linux-arm64": "0.20.2", "@esbuild/linux-ia32": "0.20.2", "@esbuild/linux-loong64": "0.20.2", "@esbuild/linux-mips64el": "0.20.2", "@esbuild/linux-ppc64": "0.20.2", "@esbuild/linux-riscv64": "0.20.2", "@esbuild/linux-s390x": "0.20.2", "@esbuild/linux-x64": "0.20.2", "@esbuild/netbsd-x64": "0.20.2", "@esbuild/openbsd-x64": "0.20.2", "@esbuild/sunos-x64": "0.20.2", "@esbuild/win32-arm64": "0.20.2", "@esbuild/win32-ia32": "0.20.2", "@esbuild/win32-x64": "0.20.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g=="], + "@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "@electron/rebuild/node-gyp/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "@electron/rebuild/node-gyp/nopt": ["nopt@6.0.0", "", { "dependencies": { "abbrev": "^1.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g=="], + + "@electron/rebuild/node-gyp/npmlog": ["npmlog@6.0.2", "", { "dependencies": { "are-we-there-yet": "^3.0.0", "console-control-strings": "^1.1.0", "gauge": "^4.0.3", "set-blocking": "^2.0.0" } }, "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg=="], + + "@electron/rebuild/node-gyp/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + + "@electron/rebuild/ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], + + "@electron/rebuild/ora/is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], + + "@electron/rebuild/ora/is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], + + "@electron/rebuild/ora/log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], + + "@electron/rebuild/ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@electron/rebuild/tar/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], + + "@electron/rebuild/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + + "@electron/rebuild/tar/minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], + + "@electron/rebuild/tar/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "@electron/rebuild/tar/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "@eslint/eslintrc/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], @@ -7412,9 +7867,9 @@ "@novu/js/socket.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - "@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="], + "@npmcli/move-file/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "@playwright/experimental-ct-core/vite/esbuild": ["esbuild@0.25.12", "", { "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" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="], "@radix-ui/react-dropdown-menu/@radix-ui/react-menu/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], @@ -7522,6 +7977,20 @@ "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "app-builder-lib/hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "app-builder-lib/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "app-builder-lib/tar/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], + + "app-builder-lib/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + + "app-builder-lib/tar/minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], + + "app-builder-lib/tar/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "app-builder-lib/tar/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "archiver-utils/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "archiver-utils/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], @@ -7532,6 +8001,20 @@ "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "builder-util/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "cacache/glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + + "cacache/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "cacache/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "cacache/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + + "cacache/tar/minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], + + "cacache/tar/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "cli-highlight/parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="], "cli-highlight/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], @@ -7540,6 +8023,14 @@ "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "compare-func/dot-prop/is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], + + "conf/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "config-file-ts/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "config-file-ts/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "cosmiconfig/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], @@ -7548,10 +8039,80 @@ "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], + "dir-compare/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "dmg-builder/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "duplexer2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "duplexer2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "electron-builder-squirrel-windows/archiver/archiver-utils": ["archiver-utils@2.1.0", "", { "dependencies": { "glob": "^7.1.4", "graceful-fs": "^4.2.0", "lazystream": "^1.0.0", "lodash.defaults": "^4.2.0", "lodash.difference": "^4.5.0", "lodash.flatten": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.union": "^4.6.0", "normalize-path": "^3.0.0", "readable-stream": "^2.0.0" } }, "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw=="], + + "electron-builder-squirrel-windows/archiver/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], + + "electron-builder-squirrel-windows/archiver/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "electron-builder-squirrel-windows/archiver/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + + "electron-builder-squirrel-windows/archiver/zip-stream": ["zip-stream@4.1.1", "", { "dependencies": { "archiver-utils": "^3.0.4", "compress-commons": "^4.1.2", "readable-stream": "^3.6.0" } }, "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ=="], + + "electron-updater/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "electron-vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "electron-vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "electron-vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "electron-vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "electron-vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "electron-vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "electron-vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "electron-vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "electron-vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "electron-vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "electron-vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "electron-vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "electron-vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "electron-vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "electron-vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "electron-vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "electron-vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "electron-vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "electron-vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "electron-vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "electron-vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "electron-vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "electron-vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "electron-vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "electron-vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "electron-vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "electron/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "engine.io-client/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -7610,6 +8171,8 @@ "from2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "fs-minipass/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "fstream/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "gauge/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@1.0.0", "", { "dependencies": { "number-is-nan": "^1.0.0" } }, "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw=="], @@ -7650,6 +8213,26 @@ "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "make-fetch-happen/http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + + "make-fetch-happen/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + + "make-fetch-happen/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "make-fetch-happen/socks-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + + "minipass-collect/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "minipass-fetch/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "minipass-fetch/minizlib/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "minipass-sized/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "msw/tough-cookie/tldts": ["tldts@7.0.19", "", { "dependencies": { "tldts-core": "^7.0.19" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA=="], "next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], @@ -7812,6 +8395,8 @@ "socket.io-parser/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "ssri/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "stream-combiner2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "stream-combiner2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], @@ -7844,6 +8429,58 @@ "uploadthing/effect/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "webpack/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], "webpack/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], @@ -7936,6 +8573,18 @@ "@dub/embed-react/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.20.2", "", { "os": "win32", "cpu": "x64" }, "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ=="], + "@electron/rebuild/node-gyp/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "@electron/rebuild/node-gyp/npmlog/are-we-there-yet": ["are-we-there-yet@3.0.1", "", { "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" } }, "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg=="], + + "@electron/rebuild/node-gyp/npmlog/gauge": ["gauge@4.0.4", "", { "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.3", "console-control-strings": "^1.1.0", "has-unicode": "^2.0.1", "signal-exit": "^3.0.7", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "wide-align": "^1.1.5" } }, "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg=="], + + "@electron/rebuild/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], + + "@electron/rebuild/ora/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "@electron/rebuild/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "@inquirer/core/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], @@ -7978,57 +8627,7 @@ "@novu/js/socket.io-client/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "@playwright/experimental-ct-core/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@npmcli/move-file/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "@semantic-release/github/aggregate-error/clean-stack/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], @@ -8056,8 +8655,24 @@ "@types/cheerio/cheerio/htmlparser2/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "app-builder-lib/hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "app-builder-lib/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "cacache/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "cacache/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "cli-highlight/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "electron-builder-squirrel-windows/archiver/archiver-utils/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "electron-builder-squirrel-windows/archiver/archiver-utils/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "electron-builder-squirrel-windows/archiver/zip-stream/archiver-utils": ["archiver-utils@3.0.4", "", { "dependencies": { "glob": "^7.2.3", "graceful-fs": "^4.2.0", "lazystream": "^1.0.0", "lodash.defaults": "^4.2.0", "lodash.difference": "^4.5.0", "lodash.flatten": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.union": "^4.6.0", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" } }, "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw=="], + + "electron-builder-squirrel-windows/archiver/zip-stream/compress-commons": ["compress-commons@4.1.2", "", { "dependencies": { "buffer-crc32": "^0.2.13", "crc32-stream": "^4.0.2", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" } }, "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg=="], + "env-ci/execa/npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], "env-ci/execa/onetime/mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], @@ -8176,6 +8791,18 @@ "@comp/app/@prisma/instrumentation/@opentelemetry/instrumentation/import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], + "@electron/rebuild/node-gyp/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "@electron/rebuild/node-gyp/npmlog/are-we-there-yet/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "@electron/rebuild/node-gyp/npmlog/gauge/aproba": ["aproba@2.1.0", "", {}, "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew=="], + + "@electron/rebuild/node-gyp/npmlog/gauge/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "@electron/rebuild/node-gyp/npmlog/gauge/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@electron/rebuild/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "@nestjs/cli/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], @@ -8184,14 +8811,28 @@ "@nestjs/schematics/@angular-devkit/schematics/ora/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@npmcli/move-file/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "@slack/bolt/@slack/web-api/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "@slack/oauth/@slack/web-api/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "@slack/socket-mode/@slack/web-api/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "cacache/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "cli-highlight/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "electron-builder-squirrel-windows/archiver/archiver-utils/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "electron-builder-squirrel-windows/archiver/archiver-utils/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "electron-builder-squirrel-windows/archiver/archiver-utils/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "electron-builder-squirrel-windows/archiver/zip-stream/archiver-utils/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "electron-builder-squirrel-windows/archiver/zip-stream/compress-commons/crc32-stream": ["crc32-stream@4.0.3", "", { "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^3.4.0" } }, "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw=="], + "exceljs/archiver/archiver-utils/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "exceljs/archiver/archiver-utils/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], @@ -8542,10 +9183,16 @@ "@commitlint/top-level/find-up/locate-path/p-locate/p-limit/yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], + "@electron/rebuild/node-gyp/npmlog/gauge/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit/p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], "@nestjs/schematics/@angular-devkit/schematics/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "electron-builder-squirrel-windows/archiver/archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "electron-builder-squirrel-windows/archiver/zip-stream/archiver-utils/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "exceljs/archiver/archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "exceljs/archiver/zip-stream/archiver-utils/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -8564,6 +9211,8 @@ "semantic-release-discord-notifier/semantic-release/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "electron-builder-squirrel-windows/archiver/zip-stream/archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "exceljs/archiver/zip-stream/archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "semantic-release-discord-notifier/semantic-release/@semantic-release/npm/npm/minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], diff --git a/packages/db/prisma/migrations/20260214210154_add_device_models/migration.sql b/packages/db/prisma/migrations/20260214210154_add_device_models/migration.sql new file mode 100644 index 0000000000..0665f8d494 --- /dev/null +++ b/packages/db/prisma/migrations/20260214210154_add_device_models/migration.sql @@ -0,0 +1,45 @@ +-- CreateEnum +CREATE TYPE "DevicePlatform" AS ENUM ('macos', 'windows', 'linux'); + +-- CreateTable +CREATE TABLE "Device" ( + "id" TEXT NOT NULL DEFAULT generate_prefixed_cuid('dev'::text), + "name" TEXT NOT NULL, + "hostname" TEXT NOT NULL, + "platform" "DevicePlatform" NOT NULL, + "osVersion" TEXT NOT NULL, + "serialNumber" TEXT, + "hardwareModel" TEXT, + "memberId" TEXT NOT NULL, + "organizationId" TEXT NOT NULL, + "isCompliant" BOOLEAN NOT NULL DEFAULT false, + "diskEncryptionEnabled" BOOLEAN NOT NULL DEFAULT false, + "antivirusEnabled" BOOLEAN NOT NULL DEFAULT false, + "passwordPolicySet" BOOLEAN NOT NULL DEFAULT false, + "screenLockEnabled" BOOLEAN NOT NULL DEFAULT false, + "checkDetails" JSONB, + "lastCheckIn" TIMESTAMP(3), + "agentVersion" TEXT, + "installedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Device_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "Device_memberId_idx" ON "Device"("memberId"); + +-- CreateIndex +CREATE INDEX "Device_organizationId_idx" ON "Device"("organizationId"); + +-- CreateIndex +CREATE INDEX "Device_isCompliant_idx" ON "Device"("isCompliant"); + +-- CreateIndex +CREATE UNIQUE INDEX "Device_serialNumber_organizationId_key" ON "Device"("serialNumber", "organizationId"); + +-- AddForeignKey +ALTER TABLE "Device" ADD CONSTRAINT "Device_memberId_fkey" FOREIGN KEY ("memberId") REFERENCES "Member"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Device" ADD CONSTRAINT "Device_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/db/prisma/schema/auth.prisma b/packages/db/prisma/schema/auth.prisma index f248ff1893..211870caac 100644 --- a/packages/db/prisma/schema/auth.prisma +++ b/packages/db/prisma/schema/auth.prisma @@ -119,6 +119,7 @@ model Member { createdFindings Finding[] @relation("FindingCreatedBy") publishedPolicyVersions PolicyVersion[] @relation("PolicyVersionPublisher") approvedTasks Task[] @relation("TaskApprover") + devices Device[] } model Invitation { diff --git a/packages/db/prisma/schema/device.prisma b/packages/db/prisma/schema/device.prisma new file mode 100644 index 0000000000..7954894b4b --- /dev/null +++ b/packages/db/prisma/schema/device.prisma @@ -0,0 +1,37 @@ +model Device { + id String @id @default(dbgenerated("generate_prefixed_cuid('dev'::text)")) + name String + hostname String + platform DevicePlatform + osVersion String + serialNumber String? + hardwareModel String? + + memberId String + member Member @relation(fields: [memberId], references: [id], onDelete: Cascade) + organizationId String + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + + isCompliant Boolean @default(false) + diskEncryptionEnabled Boolean @default(false) + antivirusEnabled Boolean @default(false) + passwordPolicySet Boolean @default(false) + screenLockEnabled Boolean @default(false) + checkDetails Json? + + lastCheckIn DateTime? + agentVersion String? + installedAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([serialNumber, organizationId]) + @@index([memberId]) + @@index([organizationId]) + @@index([isCompliant]) +} + +enum DevicePlatform { + macos + windows + linux +} diff --git a/packages/db/prisma/schema/organization.prisma b/packages/db/prisma/schema/organization.prisma index 45cbce5b58..accdce3404 100644 --- a/packages/db/prisma/schema/organization.prisma +++ b/packages/db/prisma/schema/organization.prisma @@ -62,6 +62,9 @@ model Organization { // Findings findings Finding[] + // Device Agent + devices Device[] + // Org Chart organizationChart OrganizationChart? diff --git a/packages/device-agent/.gitignore b/packages/device-agent/.gitignore new file mode 100644 index 0000000000..6900a45efc --- /dev/null +++ b/packages/device-agent/.gitignore @@ -0,0 +1,9 @@ +# Electron-builder output +release/ + +# Electron-vite build output +dist/ +out/ + +# Node modules (also in root .gitignore) +node_modules/ diff --git a/packages/device-agent/BUILD.md b/packages/device-agent/BUILD.md new file mode 100644 index 0000000000..246764c860 --- /dev/null +++ b/packages/device-agent/BUILD.md @@ -0,0 +1,172 @@ +# Comp AI Device Agent - Build Guide + +## Prerequisites + +- **Node.js** >= 18 +- **Bun** >= 1.3 (used as the package manager) +- **macOS**: Xcode Command Line Tools (`xcode-select --install`) +- **Windows**: Visual Studio Build Tools with "Desktop development with C++" workload + +## Project Structure + +``` +packages/device-agent/ + src/ + main/ # Electron main process (tray, auth, scheduler) + preload/ # contextBridge IPC layer + renderer/ # React UI (login + status window) + checks/ # Platform-specific compliance checks + macos/ # FileVault, XProtect, pwpolicy, screen lock + windows/ # BitLocker, Defender, net accounts, screen lock + shared/ # Types and constants shared across processes + assets/ # Icons, entitlements, build resources + electron-builder.yml # Packaging config for dmg/exe + electron.vite.config.ts # Vite build config for Electron +``` + +## Install Dependencies + +From the monorepo root: + +```bash +bun install +``` + +Or from this package directly: + +```bash +cd packages/device-agent +bun install +``` + +## Local Development + +Start the Electron app in development mode with hot-reload: + +```bash +bun run dev +``` + +This launches electron-vite in dev mode. The main process and renderer will +hot-reload on file changes. It also runs as part of `turbo dev` from the +monorepo root. + +> **Note:** If `bun run dev` fails with "Electron uninstall", run +> `node node_modules/electron/install.js` from the monorepo root to download +> the Electron binary. This is handled automatically by the `postinstall` script +> on fresh installs. + +## Type Checking + +```bash +bun run typecheck +``` + +## Building + +Build the app (compile TypeScript, bundle with Vite) without packaging: + +```bash +bun run build +``` + +## Packaging Installers + +### macOS (.dmg) + +```bash +bun run package:mac +``` + +Produces a `.dmg` (universal binary for Apple Silicon + Intel) in the `release/` directory. + +### Windows (.exe) + +```bash +bun run package:win +``` + +Produces an NSIS `.exe` installer in the `release/` directory. + +### Both Platforms + +```bash +bun run package:all +``` + +> **Note:** Cross-compilation has limitations. Building a `.dmg` requires macOS, and +> building a `.exe` works best on Windows (or via CI). The CI workflow handles +> platform-specific builds automatically. + +## Tray Icons + +The system tray requires PNG icons in the `assets/` directory: + +| File | Size | Purpose | +|------|------|---------| +| `tray-green.png` | 16x16 | Compliant status | +| `tray-red.png` | 16x16 | Non-compliant status | +| `tray-gray.png` | 16x16 | Unauthenticated / checking | +| `icon.png` | 256x256 | App icon | +| `icon.icns` | - | macOS app icon (multi-resolution) | +| `icon.ico` | - | Windows app icon | + +On macOS, tray icons should be **template images** (monochrome black with +transparency). Provide `@2x` variants (32x32) for Retina displays by naming +them `tray-green@2x.png`, etc. + +The app handles missing icons gracefully by falling back to an empty image. + +## Code Signing + +### macOS + +Set these environment variables (or CI secrets) for code signing and notarization: + +```bash +CSC_LINK= +CSC_KEY_PASSWORD= +APPLE_ID= +APPLE_APP_SPECIFIC_PASSWORD= +APPLE_TEAM_ID= +``` + +The `assets/entitlements.mac.plist` configures the required entitlements for +hardened runtime (JIT, network access, etc.). + +If these are not set, the build proceeds unsigned (fine for local development). + +### Windows + +Set these environment variables for Authenticode signing: + +```bash +CSC_LINK= +CSC_KEY_PASSWORD= +``` + +## Auto-Updates + +The app uses `electron-updater` to check for updates from GitHub Releases. +The `publish` config in `electron-builder.yml` controls where update metadata +is fetched from. + +When a new GitHub Release is created (via CI or manually), the app will detect +it on next launch and prompt the user to update. + +## CI / Releases + +Releases are automated via GitHub Actions. Push a tag matching `device-agent-v*` +to trigger a build: + +```bash +git tag device-agent-v1.0.0 +git push origin device-agent-v1.0.0 +``` + +The CI workflow will: +1. Build on macOS (produces `.dmg`) +2. Build on Windows (produces `.exe`) +3. Create a GitHub Release with both artifacts attached + +See `.github/workflows/device-agent-release.yml` for details. diff --git a/packages/device-agent/SPEC.md b/packages/device-agent/SPEC.md new file mode 100644 index 0000000000..e82a63e5d6 --- /dev/null +++ b/packages/device-agent/SPEC.md @@ -0,0 +1,780 @@ +# Comp AI Device Agent -- Technical Specification + +> Canonical reference for the device compliance system. +> Last updated: February 2026 + +--- + +## Table of Contents + +1. [Overview and Motivation](#1-overview-and-motivation) +2. [System Architecture](#2-system-architecture) +3. [Electron App Architecture](#3-electron-app-architecture) +4. [Compliance Checks](#4-compliance-checks) +5. [Remediations](#5-remediations) +6. [Authentication Flow](#6-authentication-flow) +7. [Database Schema](#7-database-schema) +8. [Portal API Endpoints](#8-portal-api-endpoints) +9. [Admin App Integration](#9-admin-app-integration) +10. [NestJS API Integration](#10-nestjs-api-integration) +11. [Backward Compatibility with FleetDM](#11-backward-compatibility-with-fleetdm) +12. [CI/CD and Release Process](#12-cicd-and-release-process) +13. [Key File Reference](#13-key-file-reference) +14. [Known Limitations and Future Work](#14-known-limitations-and-future-work) + +--- + +## 1. Overview and Motivation + +The Comp AI Device Agent is a custom Electron-based system tray application that replaces the previous FleetDM-based device compliance system. + +### Why the change + +| Aspect | FleetDM (old) | Device Agent (new) | +| ----------------- | ---------------------------------------------- | -------------------------------------- | +| Infrastructure | External hosted FleetDM server | Self-contained Electron app | +| Device tracking | FleetDM labels + marker files | Direct DB registration via portal API | +| Compliance checks | FleetDM osquery policies | Local platform-specific checks | +| Data storage | FleetDM API (external) | PostgreSQL via Prisma (our DB) | +| User experience | Manual image uploads for proof | Automated checks with auto-remediation | +| Setup flow | Download scripts, marker files, MDM enrollment | Install app, sign in, done | + +### What the agent checks + +The agent runs four compliance checks every hour: + +1. **Disk Encryption** -- FileVault (macOS), BitLocker (Windows), LUKS (Linux) +2. **Antivirus** -- XProtect (macOS), Windows Defender (Windows), ClamAV/AppArmor/SELinux (Linux) +3. **Password Policy** -- Minimum 8-character password enforced at OS level +4. **Screen Lock** -- Automatic screen lock within 5 minutes of inactivity + +A device is **compliant** when all four checks pass. + +--- + +## 2. System Architecture + +```mermaid +flowchart TD + subgraph electronApp [Device Agent - Electron App] + MainProcess["Main Process"] + Scheduler["Scheduler - 1hr interval"] + Checks["Compliance Checks"] + Reporter["Reporter"] + TrayIcon["System Tray"] + Renderer["Status Window - React"] + AuthFlow["Auth - BrowserWindow"] + Store["Encrypted Store"] + Remediations["Remediations"] + end + + subgraph portalAPI [Portal API - Next.js] + RegisterAPI["POST /api/device-agent/register"] + CheckInAPI["POST /api/device-agent/check-in"] + StatusAPI["GET /api/device-agent/status"] + OrgsAPI["GET /api/device-agent/my-organizations"] + FleetPoliciesAPI["GET /api/fleet-policies - Legacy"] + ConfirmFleetAPI["POST /api/confirm-fleet-policy - Legacy"] + end + + subgraph adminApp [Admin App - Next.js] + DevicesPage["Devices Page"] + EmployeeDetail["Employee Detail"] + MemberRow["Member Row"] + end + + subgraph nestAPI [NestJS API] + DevicesEndpoint["GET /v1/devices"] + UnlinkEndpoint["PATCH /v1/people/:id/unlink-device"] + end + + subgraph database [PostgreSQL] + DeviceTable["Device"] + DeviceCheckTable["DeviceCheck"] + FleetPolicyResultTable["FleetPolicyResult - Legacy"] + MemberTable["Member.fleetDmLabelId - Legacy"] + end + + subgraph external [External] + FleetDMAPI["FleetDM API - Legacy"] + BetterAuth["Better Auth"] + end + + MainProcess --> Scheduler + Scheduler --> Checks + Checks --> Reporter + Reporter --> CheckInAPI + MainProcess --> TrayIcon + MainProcess --> Renderer + MainProcess --> AuthFlow + MainProcess --> Store + MainProcess --> Remediations + AuthFlow --> BetterAuth + AuthFlow --> OrgsAPI + AuthFlow --> RegisterAPI + RegisterAPI --> DeviceTable + CheckInAPI --> DeviceTable + CheckInAPI --> DeviceCheckTable + StatusAPI --> DeviceTable + DevicesPage --> DeviceTable + DevicesPage --> FleetDMAPI + EmployeeDetail --> DeviceTable + EmployeeDetail --> FleetDMAPI + FleetPoliciesAPI --> FleetDMAPI + DevicesEndpoint --> DeviceTable + DevicesEndpoint --> FleetDMAPI + UnlinkEndpoint --> DeviceTable + UnlinkEndpoint --> FleetDMAPI +``` + +### Data flow summary + +1. **Employee installs** the device agent and signs in via the portal +2. **Agent registers** the device for all of the user's organizations via `POST /api/device-agent/register` +3. **Every hour**, the scheduler runs all compliance checks locally and reports results via `POST /api/device-agent/check-in` +4. **Admin app** reads device data from the `Device` / `DeviceCheck` tables (and FleetDM for legacy devices) +5. **Portal** shows the employee their device compliance status and remediation options + +--- + +## 3. Electron App Architecture + +The agent lives at `packages/device-agent/` and is built with Electron + electron-vite + React. + +### Directory structure + +``` +packages/device-agent/ + src/ + main/ # Electron main process + index.ts # App lifecycle, IPC handlers, tray, scheduler coordination + auth.ts # BrowserWindow login flow, multi-org registration + auto-launch.ts# Start-at-login management (all platforms) + scheduler.ts # Periodic check runner (1-hour interval) + reporter.ts # Sends check results to portal API for all orgs + store.ts # Encrypted persistent storage (electron-store) + tray.ts # System tray icon, context menu, status window + device-info.ts# Platform-specific device info collection + logger.ts # File-based logging + checks/ # Compliance check modules + types.ts # ComplianceCheck interface + index.ts # Platform router + runAllChecks() + macos/ # macOS-specific checks (4 files) + windows/ # Windows-specific checks (4 files) + linux/ # Linux-specific checks (4 files) + remediations/ # Auto-fix and guided instructions + types.ts # ComplianceRemediation interface + index.ts # Platform router + runRemediation() + instructions.ts # Per-platform guided step text + macos/ # macOS-specific remediations (4 files) + windows/ # Windows-specific remediations (4 files) + linux/ # Linux-specific remediations (4 files) + renderer/ # React UI (status window) + App.tsx # Main component with check cards + remediation buttons + main.tsx # React entry point + index.html # HTML shell + styles.css # Tailwind CSS + preload/ + index.ts # contextBridge IPC API + shared/ + types.ts # Shared TypeScript types + constants.ts # Portal URL, check interval, API routes, version + assets/ # Icons (tray, app icon, logo) + electron-builder.yml # Build/packaging config + electron.vite.config.ts # Vite config for main/preload/renderer +``` + +### Main process lifecycle + +1. App starts, requests single-instance lock +2. Hides dock icon on macOS (tray-only app) +3. Creates system tray with icon and context menu +4. Syncs auto-launch (start at login) setting with the OS (see [Auto-launch](#auto-launch) below) +5. If already authenticated (stored session), starts the scheduler immediately +6. If not authenticated, opens the sign-in BrowserWindow automatically +7. Listens for IPC messages from the renderer (status window) +8. Handles auto-updates via `electron-updater` (when packaged) + +### Auto-launch + +The agent registers itself to start automatically at login using Electron's `app.setLoginItemSettings()` API. This is **enabled by default** so the agent is always running and able to send compliance updates. + +| Platform | Mechanism | User Visibility | +| -------- | ------------------------------------------------------------- | --------------------------------------- | +| macOS | Login Items via Launch Services | System Settings > General > Login Items | +| Windows | Registry `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` | Task Manager > Startup tab | +| Linux | `.desktop` file in `~/.config/autostart/` | Desktop environment startup settings | + +- The preference is persisted in the encrypted store (`openAtLogin`, default `true`) +- On every launch, `initAutoLaunch()` re-syncs the OS setting with the stored preference (handles app path changes after auto-updates) +- Users can toggle it via the "Start at Login" checkbox in the tray context menu +- On macOS, the app starts hidden (`openAsHidden: true`) to avoid flashing a window + +### Check-in cycle + +```mermaid +sequenceDiagram + participant Scheduler + participant Checks + participant Reporter + participant PortalAPI + participant DB + + Scheduler->>Checks: runAllChecks() + Checks-->>Scheduler: CheckResult[] + Scheduler->>Reporter: reportCheckResults(checks) + loop For each registered org + Reporter->>PortalAPI: POST /api/device-agent/check-in + PortalAPI->>DB: Delete old checks of same types + PortalAPI->>DB: Create new DeviceCheck records + PortalAPI->>DB: Compute isCompliant, update Device + PortalAPI-->>Reporter: isCompliant, nextCheckIn + end + Reporter-->>Scheduler: ReportResult + Scheduler->>Scheduler: Update tray icon and notify renderer +``` + +### IPC channels + +Communication between the main process and the renderer uses named IPC channels: + +| Channel | Direction | Purpose | +| ----------------------------- | ---------------- | -------------------------------------- | +| `auth:get-status` | Renderer -> Main | Get current auth state | +| `auth:login` | Renderer -> Main | Trigger sign-in flow | +| `auth:logout` | Renderer -> Main | Sign out | +| `auth:state-changed` | Main -> Renderer | Auth state changed | +| `checks:get-results` | Renderer -> Main | Get last check results | +| `checks:run-now` | Renderer -> Main | Trigger immediate check run | +| `checks:results-updated` | Main -> Renderer | New check results available | +| `device:get-info` | Renderer -> Main | Get device info | +| `remediation:get-info` | Renderer -> Main | Get remediation options for all checks | +| `remediation:remediate-check` | Renderer -> Main | Run remediation for a specific check | + +### Encrypted store + +The agent uses `electron-store` with encryption to persist: + +| Key | Type | Purpose | +| ------------------ | -------------------- | ----------------------------------------------------------- | +| `auth` | `StoredAuth \| null` | Session token, userId, org registrations (deviceId per org) | +| `portalUrl` | `string` | Portal base URL (auto-set based on dev/prod) | +| `lastCheckResults` | `CheckResult[]` | Results from the most recent check run | +| `checkIntervalMs` | `number` | Check interval in ms (default: 1 hour) | +| `openAtLogin` | `boolean` | Start agent at login (default: `true`) | + +--- + +## 4. Compliance Checks + +Each check implements the `ComplianceCheck` interface: + +```typescript +interface ComplianceCheck { + checkType: DeviceCheckType; + displayName: string; + run(): Promise; +} +``` + +### Check matrix + +| Check Type | macOS | Windows | Linux | +| ------------------- | -------------------------------------------- | ------------------------------------ | ---------------------------------------------- | +| **disk_encryption** | `fdesetup status` (FileVault) | `manage-bde -status` (BitLocker) | `lsblk` + `cryptsetup` (LUKS) | +| **antivirus** | XProtect plist existence + version check | `Get-MpComputerStatus` (Defender) | ClamAV daemon + AppArmor/SELinux status | +| **password_policy** | `pwpolicy -getaccountpolicies` (min 8 chars) | `net accounts` (min password length) | `/etc/login.defs` PASS_MIN_LEN + pam_pwquality | +| **screen_lock** | `defaults read` screensaver idle time | `powercfg /query` monitor timeout | `gsettings` idle-delay / `xdg-screensaver` | + +### Check result format + +```typescript +interface CheckResult { + checkType: 'disk_encryption' | 'antivirus' | 'password_policy' | 'screen_lock'; + passed: boolean; + details: { + method: string; // Detection method used (e.g., "fdesetup") + raw: string; // Raw command output + message: string; // Human-readable result (e.g., "FileVault is enabled") + }; + checkedAt: string; // ISO 8601 timestamp +} +``` + +--- + +## 5. Remediations + +Each remediation implements the `ComplianceRemediation` interface: + +```typescript +interface ComplianceRemediation { + checkType: DeviceCheckType; + getInfo(): RemediationInfo; + remediate(): Promise; +} +``` + +### Remediation types + +| Type | Description | Example | +| --------------- | ------------------------------------------- | -------------------------------------- | +| `auto_fix` | Agent can fix it automatically | Setting password policy via `pwpolicy` | +| `admin_fix` | Agent can fix it but needs admin privileges | Setting screen lock timeout | +| `open_settings` | Opens the relevant OS settings pane | Opening FileVault settings | +| `guide_only` | Shows step-by-step instructions only | Enabling BitLocker (requires TPM) | + +The renderer UI shows a "Fix" / "Fix (Admin)" / "Open Settings" / "View Guide" button depending on the remediation type, plus expandable step-by-step instructions for each check. + +After a successful auto-fix or admin-fix remediation, the agent automatically re-runs checks to verify the fix took effect. + +--- + +## 6. Authentication Flow + +The agent authenticates users via a BrowserWindow that opens the portal's login page. After login, it registers the device for **all** of the user's organizations. + +```mermaid +sequenceDiagram + participant User + participant Agent + participant AuthWindow + participant Portal + participant DB + + User->>Agent: Click Sign In + Agent->>AuthWindow: Open portal/auth + User->>AuthWindow: Login via email OTP / Google / Microsoft + AuthWindow->>Portal: Better Auth authentication + Portal-->>AuthWindow: Session cookie set + AuthWindow-->>Agent: Post-login navigation detected + Agent->>Portal: GET /api/auth/get-session + Portal-->>Agent: userId + Agent->>Portal: GET /api/device-agent/my-organizations + Portal-->>Agent: organizations[] + loop For each organization + Agent->>Portal: POST /api/device-agent/register + Portal->>DB: Upsert Device by serialNumber + orgId + Portal-->>Agent: deviceId + end + Agent->>Agent: Store session token + org registrations + Agent->>Agent: Start scheduler +``` + +### Session management + +- The session token (Better Auth cookie) is stored encrypted on disk +- On each check-in, if the API returns 401, the agent triggers a re-authentication flow +- On sign-out, cookies are cleared from the Electron session and stored auth is wiped + +### Multi-org support + +A user who belongs to multiple organizations has the device registered for **each** org. Check results are reported to all orgs on every check-in cycle. Each org gets its own `Device` record with its own `deviceId`. + +--- + +## 7. Database Schema + +### New models (`packages/db/prisma/schema/device.prisma`) + +#### Device + +| Column | Type | Notes | +| ---------------- | ---------------- | --------------------------------- | +| `id` | `String` | Prefixed CUID (`dev_...`) | +| `name` | `String` | Computer name | +| `hostname` | `String` | Network hostname | +| `platform` | `DevicePlatform` | Enum: `macos`, `windows`, `linux` | +| `osVersion` | `String` | e.g., "15.3.1" | +| `serialNumber` | `String?` | Hardware serial | +| `hardwareModel` | `String?` | e.g., "MacBook Pro" | +| `userId` | `String` | FK to User | +| `organizationId` | `String` | FK to Organization | +| `isCompliant` | `Boolean` | All 4 checks passing | +| `lastCheckIn` | `DateTime?` | Last successful check-in | +| `agentVersion` | `String?` | e.g., "1.0.0" | +| `installedAt` | `DateTime` | First registration time | +| `updatedAt` | `DateTime` | Last update | + +**Unique constraint**: `(serialNumber, organizationId)` -- prevents duplicate registrations. + +**Indexes**: `userId`, `organizationId`, `isCompliant` + +#### DeviceCheck + +| Column | Type | Notes | +| ----------- | ----------------- | ---------------------------------------------------------------------- | +| `id` | `String` | Prefixed CUID (`dck_...`) | +| `deviceId` | `String` | FK to Device (CASCADE delete) | +| `checkType` | `DeviceCheckType` | Enum: `disk_encryption`, `antivirus`, `password_policy`, `screen_lock` | +| `passed` | `Boolean` | Check result | +| `details` | `Json?` | `{ method, raw, message }` | +| `checkedAt` | `DateTime` | When the check ran | + +**Indexes**: `deviceId`, `checkType` + +### Legacy models (still in schema) + +These remain in the database for backward compatibility with FleetDM: + +- **`Member.fleetDmLabelId`** (`Int?`) -- FleetDM label ID assigned to a member's device +- **`Organization.fleetDmLabelId`** (`Int?`) -- FleetDM label ID for the organization +- **`Organization.isFleetSetupCompleted`** (`Boolean`) -- Whether fleet setup was completed +- **`FleetPolicyResult`** -- Stores user-submitted compliance proof (images) for fleet policies. Fields: `userId`, `organizationId`, `fleetPolicyId`, `fleetPolicyName`, `fleetPolicyResponse`, `attachments[]` + +--- + +## 8. Portal API Endpoints + +All endpoints are in `apps/portal/src/app/api/device-agent/` and require Better Auth session authentication. + +### POST /api/device-agent/register + +Registers (or updates) a device for an organization. + +**Request body** (Zod validated): + +```typescript +{ + name: string; + hostname: string; + platform: 'macos' | 'windows'; + osVersion: string; + serialNumber?: string; + hardwareModel?: string; + agentVersion?: string; + organizationId: string; +} +``` + +**Behavior**: Upserts by `(serialNumber, organizationId)` unique constraint. Verifies the user is an active member of the organization. + +**Response**: `{ deviceId: string }` + +### POST /api/device-agent/check-in + +Submits compliance check results for a device. + +**Request body** (Zod validated): + +```typescript +{ + deviceId: string; + checks: Array<{ + checkType: 'disk_encryption' | 'antivirus' | 'password_policy' | 'screen_lock'; + passed: boolean; + details?: { method: string; raw: string; message: string }; + checkedAt: string; // ISO 8601 + }>; +} +``` + +**Behavior** (within a transaction): + +1. Deletes previous `DeviceCheck` records of the same types for this device +2. Creates new `DeviceCheck` records +3. Computes `isCompliant` (all 4 check types must exist and pass) +4. Updates `Device.lastCheckIn` and `Device.isCompliant` + +**Response**: `{ isCompliant: boolean; nextCheckIn: string }` + +### GET /api/device-agent/status + +Returns device(s) and their checks for the authenticated user. + +**Query params**: + +- `deviceId` (optional) -- return a specific device +- `organizationId` (optional) -- filter by organization + +**Response**: `{ devices: Device[] }` or `{ device: Device }` + +### GET /api/device-agent/my-organizations + +Returns all organizations the user belongs to. + +**Response**: + +```typescript +{ + organizations: Array<{ + organizationId: string; + organizationName: string; + organizationSlug: string; + role: string; + }>; +} +``` + +### Legacy Fleet endpoints (restored for backward compat) + +| Endpoint | Purpose | +| -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `GET /api/fleet-policies?organizationId=...` | Fetches FleetDM policies for the member's device label, merges with `FleetPolicyResult` DB records, signs S3 attachment URLs | +| `GET /api/fleet-policy?organizationId=...` | Returns `FleetPolicyResult` records with signed S3 URLs | +| `POST /api/confirm-fleet-policy` | Accepts FormData with images, uploads to S3, creates/updates `FleetPolicyResult` | + +These endpoints are **read-only backward compatibility** -- no new fleet enrollments are created. + +--- + +## 9. Admin App Integration + +The admin app (`apps/app`) displays device data on the People page and Employee detail page. + +### Devices list page + +The `getAllDevices()` server action in `apps/app/src/app/(app)/[orgId]/people/devices/data/index.ts` fetches from both sources: + +``` +getAllDevices() + |-- getEmployeeDevicesFromDB() --> Device table (Prisma) + |-- getFleetDevices() --> FleetDM API (if configured) + |-- deduplicateDevices() --> Merge, agent devices take priority +``` + +**Deduplication logic**: Builds sets of serial numbers and hostnames from device-agent results. Fleet devices matching any known serial or hostname are excluded. + +Each device carries a `source: 'device_agent' | 'fleet'` field. Fleet devices display a "Fleet (Legacy)" badge in the UI. + +### Employee detail page + +The employee detail page (`apps/app/src/app/(app)/[orgId]/people/[employeeId]/page.tsx`) fetches from both systems and the `EmployeeTasks` component renders with fallback logic: + +1. If the member has a device-agent `Device` record -- show the new compliance checks table +2. Else if the member has FleetDM data (via `fleetDmLabelId`) -- show fleet policies with `PolicyItem` +3. Else -- show "No device found" + +### Member row actions + +`MemberRow` shows the "Remove Device" action when **either** `member.fleetDmLabelId` exists **or** the member has a device-agent `Device` record (passed as `hasDeviceAgentDevice` prop). + +--- + +## 10. NestJS API Integration + +The NestJS API (`apps/api`) exposes device endpoints for external consumers (API key or session auth). + +### GET /v1/devices and GET /v1/devices/member/:memberId + +`DevicesService` in `apps/api/src/devices/devices.service.ts` is a hybrid service: + +1. Fetches from FleetDM API (if `fleetDmLabelId` exists) -- gracefully returns `[]` on failure +2. Fetches from `Device` table via Prisma +3. Merges with deduplication (agent devices take priority) +4. Each result includes `source: 'fleet' | 'device_agent'` in the DTO + +### PATCH /v1/people/:id/unlink-device + +`PeopleService.unlinkDevice()` in `apps/api/src/people/people.service.ts` handles both systems: + +1. If `member.fleetDmLabelId` exists -- calls `FleetService.removeHostsByLabel()` (non-blocking on failure) +2. Deletes all `Device` records for the member's `userId` + `organizationId` from the DB +3. Sets `member.fleetDmLabelId` to `null` + +--- + +## 11. Backward Compatibility with FleetDM + +The system maintains read-only backward compatibility so employees and admins who previously used FleetDM can still see their data during the migration period. + +### Decision logic (Employee Portal) + +```mermaid +flowchart TD + Start["Employee opens portal"] + Start --> FetchAgent["Fetch /api/device-agent/status"] + FetchAgent --> HasAgent{"Has device-agent device?"} + HasAgent -->|Yes| ShowAgent["Show compliance check results"] + HasAgent -->|No| HasFleet{"member.fleetDmLabelId exists?"} + HasFleet -->|Yes| ShowFleet["Show fleet policy list with FleetPolicyItem"] + HasFleet -->|No| ShowDownload["Show download instructions for new agent"] +``` + +### What is preserved for FleetDM users + +| Feature | Status | +| -------------------------------------- | ---------------------------------------------------------------- | +| View fleet policies on portal | Preserved (restored `FleetPolicyItem`, `PolicyImageUploadModal`) | +| Upload compliance proof images | Preserved (restored `POST /api/confirm-fleet-policy`) | +| Refresh fleet policy data | Preserved (restored `GET /api/fleet-policies`, SWR in portal) | +| View fleet devices in admin | Preserved (`getFleetDevices()`, `DevicesService` hybrid) | +| "Remove Device" for fleet users | Preserved (`fleetDmLabelId` check + FleetDM API removal) | +| View fleet policies on employee detail | Preserved (fallback rendering in `EmployeeTasks`) | + +### What is NOT preserved + +| Feature | Reason | +| --------------------------------------- | ------------------------------------- | +| Fleet agent download scripts | New users should use the device agent | +| Fleet label creation (`fleet-label.ts`) | No new fleet enrollments | +| Fleet setup wizard | Replaced by device agent install flow | + +### Graceful degradation + +- If `FLEET_URL` / `FLEET_TOKEN` env vars are not set, all fleet functions return empty arrays +- If the org/member has no `fleetDmLabelId`, fleet API calls are skipped entirely +- If the FleetDM API is down, errors are logged and the system continues with device-agent data only +- Every fleet API call is wrapped in try/catch with non-blocking error handling + +--- + +## 12. CI/CD and Release Process + +### Triggering a release + +Push a tag matching `device-agent-v*`: + +```bash +git tag device-agent-v1.0.0 +git push origin device-agent-v1.0.0 +``` + +### Build pipeline (`.github/workflows/device-agent-release.yml`) + +```mermaid +flowchart LR + Tag["Push tag device-agent-v*"] + Tag --> MacBuild["macOS runner"] + Tag --> WinBuild["Windows runner"] + Tag --> LinBuild["Linux runner"] + MacBuild --> MacArtifact["DMG - x64 + arm64"] + WinBuild --> WinArtifact["NSIS EXE - x64"] + LinBuild --> LinArtifact["AppImage + .deb - x64"] + MacArtifact --> Release["GitHub Release"] + WinArtifact --> Release + LinArtifact --> Release +``` + +| Platform | Runner | Output | Signing | +| -------- | ---------------- | ---------------------------- | -------------------------------------- | +| macOS | `macos-latest` | `.dmg` (x64 + arm64) | Code signing + notarization (Apple ID) | +| Windows | `windows-latest` | `.exe` (NSIS installer, x64) | Code signing (CSC) | +| Linux | `ubuntu-latest` | `.AppImage` + `.deb` (x64) | None | + +### Auto-updates + +The packaged app includes `electron-updater` which checks for new GitHub releases on startup. The `publish` config in `electron-builder.yml` points to `trycompai/comp` on GitHub. + +### Local development + +```bash +cd packages/device-agent +bun install +bun run dev # Start in dev mode with hot reload +bun run build # Build for production +bun run package:mac # Package macOS DMG +bun run package:win # Package Windows installer +bun run package:linux # Package Linux AppImage + .deb +``` + +--- + +## 13. Key File Reference + +### Device Agent (Electron app) + +| Concept | File | +| ---------------------------- | -------------------------------------------------------- | +| App entry point | `packages/device-agent/src/main/index.ts` | +| Auth / login flow | `packages/device-agent/src/main/auth.ts` | +| Auto-launch (start at login) | `packages/device-agent/src/main/auto-launch.ts` | +| Check scheduler | `packages/device-agent/src/main/scheduler.ts` | +| Check-in reporter | `packages/device-agent/src/main/reporter.ts` | +| Encrypted store | `packages/device-agent/src/main/store.ts` | +| System tray | `packages/device-agent/src/main/tray.ts` | +| Device info collector | `packages/device-agent/src/main/device-info.ts` | +| Check runner | `packages/device-agent/src/checks/index.ts` | +| Check interface | `packages/device-agent/src/checks/types.ts` | +| Remediation runner | `packages/device-agent/src/remediations/index.ts` | +| Remediation instructions | `packages/device-agent/src/remediations/instructions.ts` | +| Renderer UI | `packages/device-agent/src/renderer/App.tsx` | +| Preload bridge | `packages/device-agent/src/preload/index.ts` | +| Shared types | `packages/device-agent/src/shared/types.ts` | +| Shared constants | `packages/device-agent/src/shared/constants.ts` | +| Build config | `packages/device-agent/electron-builder.yml` | +| Vite config | `packages/device-agent/electron.vite.config.ts` | + +### Portal API + +| Concept | File | +| ----------------------------- | ---------------------------------------------------------------- | +| Device registration | `apps/portal/src/app/api/device-agent/register/route.ts` | +| Check-in endpoint | `apps/portal/src/app/api/device-agent/check-in/route.ts` | +| Device status | `apps/portal/src/app/api/device-agent/status/route.ts` | +| User organizations | `apps/portal/src/app/api/device-agent/my-organizations/route.ts` | +| Fleet policies (legacy) | `apps/portal/src/app/api/fleet-policies/route.ts` | +| Fleet policy results (legacy) | `apps/portal/src/app/api/fleet-policy/route.ts` | +| Confirm fleet policy (legacy) | `apps/portal/src/app/api/confirm-fleet-policy/route.ts` | +| Fleet axios client (legacy) | `apps/portal/src/utils/fleet.ts` | + +### Portal UI (Employee-facing) + +| Concept | File | +| ---------------------------- | ---------------------------------------------------------------------------------------- | +| Portal page (data fetching) | `apps/portal/src/app/(app)/(home)/[orgId]/page.tsx` | +| Dashboard (server component) | `apps/portal/src/app/(app)/(home)/[orgId]/components/OrganizationDashboard.tsx` | +| Task list (client component) | `apps/portal/src/app/(app)/(home)/[orgId]/components/EmployeeTasksList.tsx` | +| Device accordion (hybrid) | `apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/DeviceAgentAccordionItem.tsx` | +| Fleet policy item (legacy) | `apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/FleetPolicyItem.tsx` | +| Policy image upload (legacy) | `apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/PolicyImageUploadModal.tsx` | +| Fleet/Host types | `apps/portal/src/app/(app)/(home)/[orgId]/types/index.ts` | + +### Admin App + +| Concept | File | +| -------------------------- | ------------------------------------------------------------------------------------ | +| Devices data layer | `apps/app/src/app/(app)/[orgId]/people/devices/data/index.ts` | +| Device types | `apps/app/src/app/(app)/[orgId]/people/devices/types/index.ts` | +| Devices list UI | `apps/app/src/app/(app)/[orgId]/people/devices/components/EmployeeDevicesList.tsx` | +| Device detail UI | `apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceDetails.tsx` | +| Compliance chart | `apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceComplianceChart.tsx` | +| People page (devices tab) | `apps/app/src/app/(app)/[orgId]/people/page.tsx` | +| Employee detail page | `apps/app/src/app/(app)/[orgId]/people/[employeeId]/page.tsx` | +| Employee tasks (hybrid) | `apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.tsx` | +| Member row (remove device) | `apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx` | + +### NestJS API + +| Concept | File | +| ------------------------ | -------------------------------------------------- | +| Devices service (hybrid) | `apps/api/src/devices/devices.service.ts` | +| Devices controller | `apps/api/src/devices/devices.controller.ts` | +| Devices module | `apps/api/src/devices/devices.module.ts` | +| Device response DTO | `apps/api/src/devices/dto/device-responses.dto.ts` | +| People service (unlink) | `apps/api/src/people/people.service.ts` | +| Fleet service (legacy) | `apps/api/src/lib/fleet.service.ts` | + +### Database + +| Concept | File | +| --------------------------- | ------------------------------------------------------------------------------------ | +| Device + DeviceCheck schema | `packages/db/prisma/schema/device.prisma` | +| Migration | `packages/db/prisma/migrations/20260208120000_add_device_agent_models/migration.sql` | +| FleetPolicyResult schema | `packages/db/prisma/schema/fleet-policy-result.prisma` | +| Organization (fleet fields) | `packages/db/prisma/schema/organization.prisma` | +| Member (fleet fields) | `packages/db/prisma/schema/auth.prisma` | + +### CI/CD + +| Concept | File | +| ---------------- | -------------------------------------------- | +| Release workflow | `.github/workflows/device-agent-release.yml` | + +--- + +## 14. Known Limitations and Future Work + +### Current limitations + +- **No new fleet enrollments**: Fleet download scripts (`fleet-label.ts`, `scripts/*`) were removed. Employees must use the new device agent going forward. +- **Portal download flow references deleted files**: `archive.ts` and `token/route.ts` import from deleted `scripts/` and `fleet-label.ts`. This is a pre-existing issue from the branch and does not affect the new device agent download flow. +- **Pre-existing type error**: `Employee.tsx` uses a `form` prop on the design system `Button` component, which is not in the component's type definitions. This is unrelated to the device agent. + +### Future work + +- **Full FleetDM deprecation**: Once all employees have migrated to the new device agent, remove all FleetDM-related code, DB fields (`fleetDmLabelId`), and API routes. +- **Linux support**: Linux compliance checks and remediations are implemented but the platform enum in the DB registration schema currently only accepts `macos` and `windows`. Extend to include `linux`. +- **Additional compliance checks**: The `DeviceCheckType` enum and check framework are extensible. New checks (e.g., OS update status, VPN connection) can be added by creating new check and remediation modules. +- **Push-based updates**: Currently the agent polls every hour. Consider using WebSockets or server-sent events for real-time policy changes. +- **Agent version tracking**: The `agentVersion` field is stored but not currently used for enforcement. Consider requiring minimum agent versions. diff --git a/packages/device-agent/assets/16x16-default.png b/packages/device-agent/assets/16x16-default.png new file mode 100644 index 0000000000..38779d4ca8 Binary files /dev/null and b/packages/device-agent/assets/16x16-default.png differ diff --git a/packages/device-agent/assets/16x16-fail.png b/packages/device-agent/assets/16x16-fail.png new file mode 100644 index 0000000000..e2bf74cebd Binary files /dev/null and b/packages/device-agent/assets/16x16-fail.png differ diff --git a/packages/device-agent/assets/16x16-pass.png b/packages/device-agent/assets/16x16-pass.png new file mode 100644 index 0000000000..aa0e929278 Binary files /dev/null and b/packages/device-agent/assets/16x16-pass.png differ diff --git a/packages/device-agent/assets/entitlements.mac.plist b/packages/device-agent/assets/entitlements.mac.plist new file mode 100644 index 0000000000..45c71c98d3 --- /dev/null +++ b/packages/device-agent/assets/entitlements.mac.plist @@ -0,0 +1,16 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.network.client + + com.apple.security.files.user-selected.read-write + + + diff --git a/packages/device-agent/assets/icon.icns b/packages/device-agent/assets/icon.icns new file mode 100644 index 0000000000..88fa6f9962 Binary files /dev/null and b/packages/device-agent/assets/icon.icns differ diff --git a/packages/device-agent/assets/icon.png b/packages/device-agent/assets/icon.png new file mode 100644 index 0000000000..ba5e3263ba Binary files /dev/null and b/packages/device-agent/assets/icon.png differ diff --git a/packages/device-agent/assets/linux/after-install.sh b/packages/device-agent/assets/linux/after-install.sh new file mode 100644 index 0000000000..aac26cbf1f --- /dev/null +++ b/packages/device-agent/assets/linux/after-install.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Fix chrome-sandbox SUID permissions for Electron. +# The sandbox helper must be owned by root with setuid (4755). +SANDBOX="/opt/comp-ai-device-agent/chrome-sandbox" +if [ -f "$SANDBOX" ]; then + chown root:root "$SANDBOX" + chmod 4755 "$SANDBOX" +fi diff --git a/packages/device-agent/assets/logo.png b/packages/device-agent/assets/logo.png new file mode 100644 index 0000000000..cfdd3cc0d2 Binary files /dev/null and b/packages/device-agent/assets/logo.png differ diff --git a/packages/device-agent/electron-builder.config.js b/packages/device-agent/electron-builder.config.js new file mode 100644 index 0000000000..200e142b5a --- /dev/null +++ b/packages/device-agent/electron-builder.config.js @@ -0,0 +1,88 @@ +// Use a space-free product name for Linux to avoid path issues +const isLinuxBuild = + process.argv.includes('--linux') || process.env.BUILD_TARGET === 'linux'; + +/** @type {import('electron-builder').Configuration} */ +module.exports = { + appId: 'ai.trycomp.device-agent', + productName: isLinuxBuild ? 'comp-ai-device-agent' : 'Comp AI Device Agent', + directories: { + buildResources: 'assets', + output: 'release', + }, + asar: true, + files: [ + 'dist/main/**/*', + 'dist/preload/**/*', + 'dist/renderer/**/*', + 'assets/**/*', + '!node_modules/**/{test,tests,__tests__,spec}/**', + '!node_modules/**/*.{md,ts,map}', + '!node_modules/**/{.github,.vscode}/**', + ], + electronLanguages: ['en-US'], + extraResources: [ + { + from: 'assets/', + to: 'assets/', + filter: ['**/*.png'], + }, + ], + icon: 'assets/icon.png', + mac: { + category: 'public.app-category.utilities', + icon: 'assets/icon.icns', + artifactName: 'CompAI-Device-Agent-${version}-${arch}.${ext}', + target: [ + { + target: 'dmg', + arch: ['x64', 'arm64'], + }, + { + target: 'zip', + arch: ['x64', 'arm64'], + }, + ], + hardenedRuntime: true, + entitlements: 'assets/entitlements.mac.plist', + entitlementsInherit: 'assets/entitlements.mac.plist', + }, + win: { + target: [ + { + target: 'nsis', + arch: ['x64'], + }, + ], + }, + nsis: { + oneClick: false, + perMachine: true, + allowToChangeInstallationDirectory: false, + artifactName: 'CompAI-Device-Agent-${version}-setup.${ext}', + }, + linux: { + target: [ + { + target: 'AppImage', + arch: ['x64'], + }, + { + target: 'deb', + arch: ['x64'], + }, + ], + category: 'Utility', + artifactName: 'CompAI-Device-Agent-${version}-${arch}.${ext}', + executableName: 'comp-ai-device-agent', + }, + deb: { + afterInstall: 'assets/linux/after-install.sh', + packageName: 'comp-ai-device-agent', + compression: 'xz', + }, + publish: { + provider: 'generic', + url: process.env.AUTO_UPDATE_URL || 'https://portal.trycomp.ai/api/device-agent/updates', + }, +}; diff --git a/packages/device-agent/electron.vite.config.ts b/packages/device-agent/electron.vite.config.ts new file mode 100644 index 0000000000..37c497d614 --- /dev/null +++ b/packages/device-agent/electron.vite.config.ts @@ -0,0 +1,53 @@ +import tailwindcss from '@tailwindcss/vite'; +import react from '@vitejs/plugin-react'; +import { defineConfig, externalizeDepsPlugin } from 'electron-vite'; +import { createRequire } from 'node:module'; +import { resolve } from 'node:path'; + +const require = createRequire(import.meta.url); +const pkg = require('./package.json'); + +export default defineConfig({ + main: { + plugins: [externalizeDepsPlugin({ exclude: ['electron-store'] })], + define: { + __PORTAL_URL__: JSON.stringify( + process.env.PORTAL_URL || 'https://portal.trycomp.ai', + ), + __AGENT_VERSION__: JSON.stringify( + process.env.AGENT_VERSION || pkg.version, + ), + }, + build: { + outDir: 'dist/main', + rollupOptions: { + input: { + index: resolve(__dirname, 'src/main/index.ts'), + }, + }, + }, + }, + preload: { + plugins: [externalizeDepsPlugin()], + build: { + outDir: 'dist/preload', + rollupOptions: { + input: { + index: resolve(__dirname, 'src/preload/index.ts'), + }, + }, + }, + }, + renderer: { + plugins: [react(), tailwindcss()], + root: resolve(__dirname, 'src/renderer'), + build: { + outDir: resolve(__dirname, 'dist/renderer'), + rollupOptions: { + input: { + index: resolve(__dirname, 'src/renderer/index.html'), + }, + }, + }, + }, +}); diff --git a/packages/device-agent/package.json b/packages/device-agent/package.json new file mode 100644 index 0000000000..d0fc91e62d --- /dev/null +++ b/packages/device-agent/package.json @@ -0,0 +1,41 @@ +{ + "name": "@comp/device-agent", + "version": "1.0.0", + "description": "Comp AI Device Agent - Endpoint Compliance", + "author": "Comp AI ", + "homepage": "https://trycomp.ai/", + "private": true, + "main": "dist/main/index.js", + "scripts": { + "postinstall": "node node_modules/electron/install.js || true", + "dev": "electron-vite dev --watch", + "build": "electron-vite build", + "preview": "electron-vite preview", + "typecheck": "tsc --noEmit", + "lint": "prettier --check 'src/**/*.{ts,tsx}'", + "package:mac": "electron-builder --mac --config electron-builder.config.js", + "package:win": "electron-builder --win --config electron-builder.config.js", + "package:linux": "electron-builder --linux --config electron-builder.config.js", + "package:all": "electron-builder --mac --win --linux --config electron-builder.config.js" + }, + "dependencies": { + "@trycompai/design-system": "*", + "electron-store": "^10.0.0", + "electron-updater": "^6.3.9" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.4", + "@types/node": "^22.15.0", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^4.5.2", + "electron": "33.4.0", + "electron-builder": "^25.1.8", + "electron-vite": "^3.1.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "tailwindcss": "^4.1.4", + "typescript": "^5.9.3", + "vite": "^6.3.5" + } +} diff --git a/packages/device-agent/src/checks/index.ts b/packages/device-agent/src/checks/index.ts new file mode 100644 index 0000000000..532af794cb --- /dev/null +++ b/packages/device-agent/src/checks/index.ts @@ -0,0 +1,92 @@ +import { log } from '../main/logger'; +import type { CheckResult } from '../shared/types'; +import type { ComplianceCheck } from './types'; + +// macOS checks +import { MacOSAntivirusCheck } from './macos/antivirus'; +import { MacOSDiskEncryptionCheck } from './macos/disk-encryption'; +import { MacOSPasswordPolicyCheck } from './macos/password-policy'; +import { MacOSScreenLockCheck } from './macos/screen-lock'; + +// Linux checks +import { LinuxAntivirusCheck } from './linux/antivirus'; +import { LinuxDiskEncryptionCheck } from './linux/disk-encryption'; +import { LinuxPasswordPolicyCheck } from './linux/password-policy'; +import { LinuxScreenLockCheck } from './linux/screen-lock'; + +// Windows checks +import { WindowsAntivirusCheck } from './windows/antivirus'; +import { WindowsDiskEncryptionCheck } from './windows/disk-encryption'; +import { WindowsPasswordPolicyCheck } from './windows/password-policy'; +import { WindowsScreenLockCheck } from './windows/screen-lock'; + +/** + * Returns the appropriate compliance checks for the current platform. + */ +function getChecksForPlatform(): ComplianceCheck[] { + const platform = process.platform; + + if (platform === 'darwin') { + return [ + new MacOSDiskEncryptionCheck(), + new MacOSAntivirusCheck(), + new MacOSPasswordPolicyCheck(), + new MacOSScreenLockCheck(), + ]; + } + + if (platform === 'linux') { + return [ + new LinuxDiskEncryptionCheck(), + new LinuxAntivirusCheck(), + new LinuxPasswordPolicyCheck(), + new LinuxScreenLockCheck(), + ]; + } + + if (platform === 'win32') { + return [ + new WindowsDiskEncryptionCheck(), + new WindowsAntivirusCheck(), + new WindowsPasswordPolicyCheck(), + new WindowsScreenLockCheck(), + ]; + } + + log(`Unsupported platform: ${platform}`, 'WARN'); + return []; +} + +/** + * Runs all compliance checks for the current platform. + * Returns an array of check results. + */ +export async function runAllChecks(): Promise { + const checks = getChecksForPlatform(); + const results: CheckResult[] = []; + + for (const check of checks) { + try { + log(`Running check: ${check.displayName}`); + const result = await check.run(); + results.push(result); + log( + ` ${check.displayName}: ${result.passed ? 'PASS' : 'FAIL'} - ${result.details.message}`, + ); + } catch (error) { + log(`Check failed: ${check.displayName} - ${error}`, 'ERROR'); + results.push({ + checkType: check.checkType, + passed: false, + details: { + method: 'error', + raw: error instanceof Error ? error.message : String(error), + message: `Check failed with error: ${error instanceof Error ? error.message : 'Unknown error'}`, + }, + checkedAt: new Date().toISOString(), + }); + } + } + + return results; +} diff --git a/packages/device-agent/src/checks/linux/antivirus.ts b/packages/device-agent/src/checks/linux/antivirus.ts new file mode 100644 index 0000000000..c43a9968a3 --- /dev/null +++ b/packages/device-agent/src/checks/linux/antivirus.ts @@ -0,0 +1,131 @@ +import { execSync } from 'node:child_process'; +import type { CheckResult } from '../../shared/types'; +import type { ComplianceCheck } from '../types'; + +/** + * Checks if antivirus or security software is active on Linux. + * + * Detection methods: + * 1. Check for known AV processes (ClamAV, CrowdStrike, SentinelOne, etc.) + * 2. Check AppArmor enforcement status + * 3. Check SELinux enforcement status + * + * Passes if any AV process is running OR a mandatory access control + * framework (AppArmor/SELinux) is in enforcing mode. + */ +export class LinuxAntivirusCheck implements ComplianceCheck { + checkType = 'antivirus' as const; + displayName = 'Antivirus / Security Software'; + + private static readonly KNOWN_AV_PROCESSES = [ + 'clamd', + 'freshclam', + 'clamav', + 'falcon-sensor', + 'SentinelAgent', + 'sentinelone', + 'sophos', + 'sophosav', + 'savd', + 'esets_daemon', + 'bdagent', + 'McAfeeAgent', + ]; + + async run(): Promise { + try { + const avProcess = this.findAVProcess(); + const appArmorEnforcing = this.checkAppArmor(); + const seLinuxEnforcing = this.checkSELinux(); + + const passed = avProcess !== null || appArmorEnforcing || seLinuxEnforcing; + const details: string[] = []; + + if (avProcess) { + details.push(`Antivirus detected: ${avProcess}`); + } + + if (appArmorEnforcing) { + details.push('AppArmor is in enforcing mode'); + } + + if (seLinuxEnforcing) { + details.push('SELinux is in enforcing mode'); + } + + if (!passed) { + details.push('No antivirus software or mandatory access control detected'); + } + + return { + checkType: this.checkType, + passed, + details: { + method: 'process-scan + apparmor + selinux', + raw: JSON.stringify({ avProcess, appArmorEnforcing, seLinuxEnforcing }), + message: details.join('. '), + }, + checkedAt: new Date().toISOString(), + }; + } catch (error) { + return { + checkType: this.checkType, + passed: false, + details: { + method: 'process-scan', + raw: error instanceof Error ? error.message : String(error), + message: 'Unable to determine antivirus status', + }, + checkedAt: new Date().toISOString(), + }; + } + } + + private findAVProcess(): string | null { + try { + const processes = execSync('ps aux', { encoding: 'utf-8', timeout: 10000 }); + for (const av of LinuxAntivirusCheck.KNOWN_AV_PROCESSES) { + if (processes.toLowerCase().includes(av.toLowerCase())) { + return av; + } + } + } catch { + // ps aux failure is non-critical + } + return null; + } + + private checkAppArmor(): boolean { + try { + const output = execSync('aa-status --enabled 2>/dev/null && echo "enabled"', { + encoding: 'utf-8', + timeout: 5000, + }); + return output.trim().includes('enabled'); + } catch { + // aa-status not available or not enforcing + } + + try { + const output = execSync('cat /sys/module/apparmor/parameters/enabled 2>/dev/null', { + encoding: 'utf-8', + timeout: 5000, + }); + return output.trim() === 'Y'; + } catch { + return false; + } + } + + private checkSELinux(): boolean { + try { + const output = execSync('getenforce 2>/dev/null', { + encoding: 'utf-8', + timeout: 5000, + }); + return output.trim().toLowerCase() === 'enforcing'; + } catch { + return false; + } + } +} diff --git a/packages/device-agent/src/checks/linux/disk-encryption.ts b/packages/device-agent/src/checks/linux/disk-encryption.ts new file mode 100644 index 0000000000..260bf39cf8 --- /dev/null +++ b/packages/device-agent/src/checks/linux/disk-encryption.ts @@ -0,0 +1,159 @@ +import { execSync } from 'node:child_process'; +import type { CheckResult } from '../../shared/types'; +import type { ComplianceCheck } from '../types'; + +interface LsblkDevice { + name: string; + fstype: string | null; + type: string; + mountpoint: string | null; + children?: LsblkDevice[]; +} + +/** + * Checks if disk encryption (LUKS) is enabled on Linux. + * + * If LUKS is not detected, the check passes with a note since enabling + * LUKS requires a full OS reinstall — it cannot be done on a running system. + * + * Detection methods: + * 1. `lsblk` to find block devices backing the root filesystem and check for crypto_LUKS + * 2. `dmsetup status` to detect active dm-crypt mappings + * 3. Check /etc/crypttab for configured encrypted volumes + */ +export class LinuxDiskEncryptionCheck implements ComplianceCheck { + checkType = 'disk_encryption' as const; + displayName = 'Disk Encryption (LUKS)'; + + async run(): Promise { + try { + // Method 1: Check lsblk for LUKS type on devices backing root + const luksDetected = this.checkLsblk(); + if (luksDetected) { + return { + checkType: this.checkType, + passed: true, + details: { + method: 'lsblk', + raw: 'LUKS detected via lsblk', + message: 'LUKS disk encryption is enabled on the root filesystem', + }, + checkedAt: new Date().toISOString(), + }; + } + + // Method 2: Check dmsetup for active crypt targets + const dmCryptActive = this.checkDmsetup(); + if (dmCryptActive) { + return { + checkType: this.checkType, + passed: true, + details: { + method: 'dmsetup', + raw: 'dm-crypt target active', + message: 'Disk encryption is active (dm-crypt detected)', + }, + checkedAt: new Date().toISOString(), + }; + } + + // Method 3: Check /etc/crypttab + const crypttabConfigured = this.checkCrypttab(); + if (crypttabConfigured) { + return { + checkType: this.checkType, + passed: true, + details: { + method: '/etc/crypttab', + raw: 'Encrypted volumes configured in /etc/crypttab', + message: 'Disk encryption is configured via /etc/crypttab', + }, + checkedAt: new Date().toISOString(), + }; + } + + // LUKS is not set up — pass with a note since enabling it requires OS reinstall + return { + checkType: this.checkType, + passed: true, + details: { + method: 'lsblk + dmsetup + crypttab', + raw: 'No LUKS or dm-crypt encryption detected', + message: + 'Disk encryption (LUKS) is not enabled. Enabling LUKS requires an OS reinstall. This check is not applicable to your current setup.', + exception: 'LUKS not enabled (requires OS reinstall to enable)', + }, + checkedAt: new Date().toISOString(), + }; + } catch (error) { + return { + checkType: this.checkType, + passed: true, + details: { + method: 'lsblk + dmsetup', + raw: error instanceof Error ? error.message : String(error), + message: + 'Unable to determine disk encryption status. This check is not applicable.', + exception: 'Unable to check (not applicable)', + }, + checkedAt: new Date().toISOString(), + }; + } + } + + private checkLsblk(): boolean { + try { + const output = execSync('lsblk -o NAME,FSTYPE,TYPE,MOUNTPOINT --json 2>/dev/null', { + encoding: 'utf-8', + timeout: 10000, + }); + + const data = JSON.parse(output); + return this.findLuks(data.blockdevices || []); + } catch { + return false; + } + } + + private findLuks(devices: LsblkDevice[]): boolean { + for (const device of devices) { + if (device.fstype === 'crypto_LUKS' || device.type === 'crypt') { + return true; + } + if (device.children && this.findLuks(device.children)) { + return true; + } + } + return false; + } + + private checkDmsetup(): boolean { + try { + const output = execSync('dmsetup status 2>/dev/null', { + encoding: 'utf-8', + timeout: 10000, + }); + + return output.toLowerCase().includes('crypt'); + } catch { + return false; + } + } + + private checkCrypttab(): boolean { + try { + const output = execSync('cat /etc/crypttab 2>/dev/null', { + encoding: 'utf-8', + timeout: 5000, + }); + + // Filter out comments and empty lines + const entries = output + .split('\n') + .filter((line) => line.trim() && !line.trim().startsWith('#')); + return entries.length > 0; + } catch { + return false; + } + } +} diff --git a/packages/device-agent/src/checks/linux/password-policy.ts b/packages/device-agent/src/checks/linux/password-policy.ts new file mode 100644 index 0000000000..c6faf2e264 --- /dev/null +++ b/packages/device-agent/src/checks/linux/password-policy.ts @@ -0,0 +1,130 @@ +import { readFileSync } from 'node:fs'; +import type { CheckResult } from '../../shared/types'; +import type { ComplianceCheck } from '../types'; + +const REQUIRED_MIN_LENGTH = 8; + +/** + * Checks if a minimum password length policy (>= 8 characters) is enforced on Linux. + * + * Detection methods: + * 1. Parse /etc/login.defs for PASS_MIN_LEN + * 2. Check PAM config for pam_pwquality or pam_cracklib minlen setting + */ +export class LinuxPasswordPolicyCheck implements ComplianceCheck { + checkType = 'password_policy' as const; + displayName = 'Password Policy (Min 8 Characters)'; + + async run(): Promise { + try { + // Check PAM config first (more reliable on modern distros) + const pamResult = this.checkPamConfig(); + if (pamResult !== null) { + return pamResult; + } + + // Fall back to /etc/login.defs + const loginDefsResult = this.checkLoginDefs(); + if (loginDefsResult !== null) { + return loginDefsResult; + } + + return { + checkType: this.checkType, + passed: false, + details: { + method: 'pam + login.defs', + raw: 'No password policy found', + message: `No minimum password length policy detected. A minimum of ${REQUIRED_MIN_LENGTH} characters is required.`, + }, + checkedAt: new Date().toISOString(), + }; + } catch (error) { + return { + checkType: this.checkType, + passed: false, + details: { + method: 'pam + login.defs', + raw: error instanceof Error ? error.message : String(error), + message: 'Unable to determine password policy', + }, + checkedAt: new Date().toISOString(), + }; + } + } + + private checkPamConfig(): CheckResult | null { + // Check common PAM config paths for pam_pwquality or pam_cracklib + const pamPaths = [ + '/etc/pam.d/common-password', + '/etc/pam.d/system-auth', + '/etc/security/pwquality.conf', + ]; + + for (const pamPath of pamPaths) { + try { + const content = readFileSync(pamPath, 'utf-8'); + + // Look for minlen in pam_pwquality or pam_cracklib config + const minlenMatch = content.match(/minlen\s*=\s*(\d+)/i); + if (minlenMatch) { + const minLength = parseInt(minlenMatch[1], 10); + const passed = minLength >= REQUIRED_MIN_LENGTH; + + return { + checkType: this.checkType, + passed, + details: { + method: pamPath, + raw: `minlen=${minLength}`, + message: passed + ? `Password policy enforces minimum ${minLength} characters` + : `Password policy requires only ${minLength} characters (minimum ${REQUIRED_MIN_LENGTH} required)`, + }, + checkedAt: new Date().toISOString(), + }; + } + } catch { + // File not found or not readable, try next + } + } + + return null; + } + + private checkLoginDefs(): CheckResult | null { + try { + const content = readFileSync('/etc/login.defs', 'utf-8'); + + // Look for PASS_MIN_LEN (not commented out) + const lines = content.split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed.startsWith('#') || !trimmed) continue; + + const match = trimmed.match(/^PASS_MIN_LEN\s+(\d+)/); + if (match) { + const minLength = parseInt(match[1], 10); + const passed = minLength >= REQUIRED_MIN_LENGTH; + + return { + checkType: this.checkType, + passed, + details: { + method: '/etc/login.defs', + raw: `PASS_MIN_LEN=${minLength}`, + message: passed + ? `Password policy enforces minimum ${minLength} characters` + : `Password policy requires only ${minLength} characters (minimum ${REQUIRED_MIN_LENGTH} required)`, + }, + checkedAt: new Date().toISOString(), + }; + } + } + } catch { + // /etc/login.defs not readable + } + + return null; + } +} diff --git a/packages/device-agent/src/checks/linux/screen-lock.ts b/packages/device-agent/src/checks/linux/screen-lock.ts new file mode 100644 index 0000000000..db97443124 --- /dev/null +++ b/packages/device-agent/src/checks/linux/screen-lock.ts @@ -0,0 +1,268 @@ +import { execSync } from 'node:child_process'; +import type { CheckResult } from '../../shared/types'; +import type { ComplianceCheck } from '../types'; + +const MAX_IDLE_TIME_SECONDS = 300; // 5 minutes + +/** + * Checks if screen lock is enabled and set to 5 minutes or less on Linux. + * + * Detection methods: + * 1. GNOME: gsettings for org.gnome.desktop.session idle-delay and + * org.gnome.desktop.screensaver lock-enabled + * 2. KDE: kreadconfig5 for the screen locker timeout + * 3. Cinnamon (Linux Mint): gsettings for org.cinnamon.desktop.screensaver + * 4. XFCE: xfconf-query for xfce4-screensaver settings + */ +export class LinuxScreenLockCheck implements ComplianceCheck { + checkType = 'screen_lock' as const; + displayName = 'Screen Lock (5 min or less)'; + + async run(): Promise { + try { + // Try GNOME first (most common desktop environment) + const gnomeResult = this.checkGnome(); + if (gnomeResult !== null) { + return gnomeResult; + } + + // Try KDE Plasma + const kdeResult = this.checkKDE(); + if (kdeResult !== null) { + return kdeResult; + } + + // Try Cinnamon (Linux Mint) + const cinnamonResult = this.checkCinnamon(); + if (cinnamonResult !== null) { + return cinnamonResult; + } + + // Try XFCE + const xfceResult = this.checkXfce(); + if (xfceResult !== null) { + return xfceResult; + } + + return { + checkType: this.checkType, + passed: false, + details: { + method: 'gsettings + kreadconfig5 + xfconf-query', + raw: 'No supported desktop environment detected', + message: 'Unable to determine screen lock settings (unsupported desktop environment)', + exception: 'Unsupported desktop environment', + }, + checkedAt: new Date().toISOString(), + }; + } catch (error) { + return { + checkType: this.checkType, + passed: false, + details: { + method: 'gsettings', + raw: error instanceof Error ? error.message : String(error), + message: 'Unable to determine screen lock settings', + }, + checkedAt: new Date().toISOString(), + }; + } + } + + private checkGnome(): CheckResult | null { + try { + // Check idle delay (in seconds; 0 means disabled) + const idleDelayOutput = execSync( + 'gsettings get org.gnome.desktop.session idle-delay 2>/dev/null', + { encoding: 'utf-8', timeout: 5000 }, + ).trim(); + + // Output format: "uint32 300" or "300" + const idleMatch = idleDelayOutput.match(/(\d+)/); + if (!idleMatch) return null; + + const idleDelay = parseInt(idleMatch[1], 10); + + // Check if screen lock is enabled + const lockEnabledOutput = execSync( + 'gsettings get org.gnome.desktop.screensaver lock-enabled 2>/dev/null', + { encoding: 'utf-8', timeout: 5000 }, + ).trim(); + + const lockEnabled = lockEnabledOutput === 'true'; + const idleOk = idleDelay > 0 && idleDelay <= MAX_IDLE_TIME_SECONDS; + const passed = idleOk && lockEnabled; + + let message: string; + if (passed) { + message = `Screen saver activates after ${idleDelay} seconds with a password required`; + } else if (idleDelay === 0) { + message = 'Screen saver idle time is disabled'; + } else if (!lockEnabled) { + message = `Screen saver activates after ${idleDelay} seconds but screen lock is disabled`; + } else { + message = `Screen saver activates after ${idleDelay} seconds (must be ${MAX_IDLE_TIME_SECONDS} or less)`; + } + + return { + checkType: this.checkType, + passed, + details: { + method: 'gsettings (GNOME)', + raw: JSON.stringify({ idleDelay, lockEnabled }), + message, + }, + checkedAt: new Date().toISOString(), + }; + } catch { + return null; + } + } + + private checkKDE(): CheckResult | null { + try { + // KDE Plasma screen locker timeout (in seconds) + const timeoutOutput = execSync( + 'kreadconfig5 --group Daemon --key Timeout --file kscreenlockerrc 2>/dev/null', + { encoding: 'utf-8', timeout: 5000 }, + ).trim(); + + const timeout = parseInt(timeoutOutput, 10); + if (isNaN(timeout)) return null; + + // Check if autolock is enabled + const autolockOutput = execSync( + 'kreadconfig5 --group Daemon --key Autolock --file kscreenlockerrc 2>/dev/null', + { encoding: 'utf-8', timeout: 5000 }, + ).trim(); + + const autolock = autolockOutput.toLowerCase() !== 'false'; + // KDE timeout is in minutes + const timeoutSeconds = timeout * 60; + const idleOk = timeoutSeconds > 0 && timeoutSeconds <= MAX_IDLE_TIME_SECONDS; + const passed = idleOk && autolock; + + let message: string; + if (passed) { + message = `Screen locks after ${timeout} minutes with autolock enabled`; + } else if (!autolock) { + message = 'Screen lock autolock is disabled'; + } else { + message = `Screen locks after ${timeout} minutes (must be ${MAX_IDLE_TIME_SECONDS / 60} minutes or less)`; + } + + return { + checkType: this.checkType, + passed, + details: { + method: 'kreadconfig5 (KDE)', + raw: JSON.stringify({ timeout, autolock }), + message, + }, + checkedAt: new Date().toISOString(), + }; + } catch { + return null; + } + } + + private checkCinnamon(): CheckResult | null { + try { + const idleDelayOutput = execSync( + 'gsettings get org.cinnamon.desktop.session idle-delay 2>/dev/null', + { encoding: 'utf-8', timeout: 5000 }, + ).trim(); + + const idleMatch = idleDelayOutput.match(/(\d+)/); + if (!idleMatch) return null; + + const idleDelay = parseInt(idleMatch[1], 10); + + const lockEnabledOutput = execSync( + 'gsettings get org.cinnamon.desktop.screensaver lock-enabled 2>/dev/null', + { encoding: 'utf-8', timeout: 5000 }, + ).trim(); + + const lockEnabled = lockEnabledOutput === 'true'; + const idleOk = idleDelay > 0 && idleDelay <= MAX_IDLE_TIME_SECONDS; + const passed = idleOk && lockEnabled; + + let message: string; + if (passed) { + message = `Screen saver activates after ${idleDelay} seconds with lock enabled`; + } else if (idleDelay === 0) { + message = 'Screen saver idle time is disabled'; + } else if (!lockEnabled) { + message = `Screen saver activates after ${idleDelay} seconds but screen lock is disabled`; + } else { + message = `Screen saver activates after ${idleDelay} seconds (must be ${MAX_IDLE_TIME_SECONDS} or less)`; + } + + return { + checkType: this.checkType, + passed, + details: { + method: 'gsettings (Cinnamon)', + raw: JSON.stringify({ idleDelay, lockEnabled }), + message, + }, + checkedAt: new Date().toISOString(), + }; + } catch { + return null; + } + } + + private checkXfce(): CheckResult | null { + try { + const idleOutput = execSync( + 'xfconf-query -c xfce4-screensaver -p /saver/idle-activation/delay 2>/dev/null', + { encoding: 'utf-8', timeout: 5000 }, + ).trim(); + + const idleMinutes = parseInt(idleOutput, 10); + if (isNaN(idleMinutes)) return null; + + const idleSeconds = idleMinutes * 60; + + // Check if lock is enabled + let lockEnabled = false; + try { + const lockOutput = execSync( + 'xfconf-query -c xfce4-screensaver -p /lock/enabled 2>/dev/null', + { encoding: 'utf-8', timeout: 5000 }, + ).trim(); + lockEnabled = lockOutput.toLowerCase() === 'true'; + } catch { + // Lock property may not exist — treat as disabled + } + + const idleOk = idleSeconds > 0 && idleSeconds <= MAX_IDLE_TIME_SECONDS; + const passed = idleOk && lockEnabled; + + let message: string; + if (passed) { + message = `Screen saver activates after ${idleMinutes} minutes with lock enabled`; + } else if (idleMinutes === 0) { + message = 'Screen saver idle time is disabled'; + } else if (!lockEnabled) { + message = `Screen saver activates after ${idleMinutes} minutes but screen lock is disabled`; + } else { + message = `Screen saver activates after ${idleMinutes} minutes (must be ${MAX_IDLE_TIME_SECONDS / 60} minutes or less)`; + } + + return { + checkType: this.checkType, + passed, + details: { + method: 'xfconf-query (XFCE)', + raw: JSON.stringify({ idleMinutes, lockEnabled }), + message, + }, + checkedAt: new Date().toISOString(), + }; + } catch { + return null; + } + } +} diff --git a/packages/device-agent/src/checks/macos/antivirus.ts b/packages/device-agent/src/checks/macos/antivirus.ts new file mode 100644 index 0000000000..a8f6fdee09 --- /dev/null +++ b/packages/device-agent/src/checks/macos/antivirus.ts @@ -0,0 +1,94 @@ +import { execSync } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import type { CheckResult } from '../../shared/types'; +import type { ComplianceCheck } from '../types'; + +/** + * Checks if antivirus protection is active on macOS. + * + * On macOS, XProtect is built-in and always active on supported versions. + * We verify: + * 1. XProtect bundle exists at the expected path + * 2. Optionally detect third-party AV software + */ +export class MacOSAntivirusCheck implements ComplianceCheck { + checkType = 'antivirus' as const; + displayName = 'Antivirus (XProtect)'; + + private static readonly XPROTECT_PATHS = [ + '/Library/Apple/System/Library/CoreServices/XProtect.bundle', + '/System/Library/CoreServices/XProtect.bundle', + ]; + + private static readonly KNOWN_AV_PROCESSES = [ + 'MalwareBytes', + 'Sophos', + 'CrowdStrike', + 'SentinelOne', + 'Norton', + 'McAfee', + 'Avast', + 'AVG', + 'Kaspersky', + 'ESET', + 'Bitdefender', + 'Trend Micro', + 'Webroot', + ]; + + async run(): Promise { + try { + // Check XProtect + const xprotectExists = MacOSAntivirusCheck.XPROTECT_PATHS.some((p) => existsSync(p)); + + // Check for third-party AV by looking at running processes + let thirdPartyAV: string | null = null; + try { + const processes = execSync('ps aux', { encoding: 'utf-8', timeout: 10000 }); + for (const av of MacOSAntivirusCheck.KNOWN_AV_PROCESSES) { + if (processes.toLowerCase().includes(av.toLowerCase())) { + thirdPartyAV = av; + break; + } + } + } catch { + // ps aux failure is non-critical + } + + const passed = xprotectExists; + const details: string[] = []; + + if (xprotectExists) { + details.push('XProtect is active'); + } else { + details.push('XProtect not found'); + } + + if (thirdPartyAV) { + details.push(`Third-party AV detected: ${thirdPartyAV}`); + } + + return { + checkType: this.checkType, + passed, + details: { + method: 'xprotect-bundle-check + process-scan', + raw: JSON.stringify({ xprotectExists, thirdPartyAV }), + message: details.join('. '), + }, + checkedAt: new Date().toISOString(), + }; + } catch (error) { + return { + checkType: this.checkType, + passed: false, + details: { + method: 'xprotect-bundle-check', + raw: error instanceof Error ? error.message : String(error), + message: 'Unable to determine antivirus status', + }, + checkedAt: new Date().toISOString(), + }; + } + } +} diff --git a/packages/device-agent/src/checks/macos/disk-encryption.ts b/packages/device-agent/src/checks/macos/disk-encryption.ts new file mode 100644 index 0000000000..151a096a45 --- /dev/null +++ b/packages/device-agent/src/checks/macos/disk-encryption.ts @@ -0,0 +1,50 @@ +import { execSync } from 'node:child_process'; +import type { CheckResult } from '../../shared/types'; +import type { ComplianceCheck } from '../types'; + +/** + * Checks if FileVault disk encryption is enabled on macOS. + * Uses the `fdesetup status` command. + */ +export class MacOSDiskEncryptionCheck implements ComplianceCheck { + checkType = 'disk_encryption' as const; + displayName = 'Disk Encryption (FileVault)'; + + async run(): Promise { + try { + const output = execSync('fdesetup status', { + encoding: 'utf-8', + timeout: 10000, + }).trim(); + + const isEnabled = output.toLowerCase().includes('filevault is on'); + + return { + checkType: this.checkType, + passed: isEnabled, + details: { + method: 'fdesetup status', + raw: output, + message: isEnabled ? 'FileVault is enabled' : 'FileVault is not enabled', + }, + checkedAt: new Date().toISOString(), + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const isPermission = /permission|not authorized|operation not permitted/i.test(errorMessage); + + return { + checkType: this.checkType, + passed: false, + details: { + method: 'fdesetup status', + raw: errorMessage, + message: isPermission + ? 'Unable to determine FileVault status due to insufficient permissions. If your device is managed by an MDM, try granting Full Disk Access to the Comp AI Device Agent in System Settings > Privacy & Security.' + : 'Unable to determine FileVault status', + }, + checkedAt: new Date().toISOString(), + }; + } + } +} diff --git a/packages/device-agent/src/checks/macos/password-policy.ts b/packages/device-agent/src/checks/macos/password-policy.ts new file mode 100644 index 0000000000..d9b1774604 --- /dev/null +++ b/packages/device-agent/src/checks/macos/password-policy.ts @@ -0,0 +1,181 @@ +import { execSync } from 'node:child_process'; +import type { CheckResult } from '../../shared/types'; +import type { ComplianceCheck } from '../types'; + +const REQUIRED_MIN_LENGTH = 8; + +/** + * Checks if a minimum password length policy (>= 8 characters) is enforced on macOS. + * + * Uses `pwpolicy getaccountpolicies` and `pwpolicy -getglobalpolicy` to read password policies. + * The account policies output is XML that may contain `policyAttributePassword` constraints + * with a `minChars` or `policyAttributeMinimumLength` attribute. + * The global policy output is a key=value string that may contain `minChars`. + * + * Also checks for MDM-enforced profiles via `system_profiler SPConfigurationProfileDataType`. + */ +export class MacOSPasswordPolicyCheck implements ComplianceCheck { + checkType = 'password_policy' as const; + displayName = 'Password Policy (Min 8 Characters)'; + + async run(): Promise { + try { + // Try pwpolicy first + const pwpolicyResult = this.checkPwpolicy(); + if (pwpolicyResult !== null) { + return pwpolicyResult; + } + + // Fall back to checking configuration profiles (MDM-enforced) + const profileResult = this.checkConfigurationProfiles(); + if (profileResult !== null) { + return profileResult; + } + + // If no explicit policy is found, macOS doesn't enforce minimum password length by default + return { + checkType: this.checkType, + passed: false, + details: { + method: 'pwpolicy + configuration-profiles', + raw: 'No password policy found', + message: `No minimum password length policy detected. A minimum of ${REQUIRED_MIN_LENGTH} characters is required.`, + }, + checkedAt: new Date().toISOString(), + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const isPermission = /permission|not authorized|operation not permitted/i.test(errorMessage); + + return { + checkType: this.checkType, + passed: false, + details: { + method: 'pwpolicy', + raw: errorMessage, + message: isPermission + ? 'Unable to determine password policy due to insufficient permissions. If your device is managed by an MDM, the policy may be enforced at the system level.' + : 'Unable to determine password policy', + }, + checkedAt: new Date().toISOString(), + }; + } + } + + private checkPwpolicy(): CheckResult | null { + // Check getaccountpolicies first + const accountResult = this.checkAccountPolicies(); + if (accountResult !== null) { + return accountResult; + } + + // Fall back to getglobalpolicy + return this.checkGlobalPolicy(); + } + + private checkAccountPolicies(): CheckResult | null { + try { + const output = execSync('pwpolicy getaccountpolicies 2>/dev/null', { + encoding: 'utf-8', + timeout: 10000, + }); + + // Look for minimum password length in the XML output + const minCharsMatch = output.match(/policyAttributePasswordMinimumLength\s*=\s*(\d+)/i); + const minCharsMatch2 = output.match(/minChars\s*[=:]\s*(\d+)/i); + const minLengthMatch = output.match(/minimumLength\s*[=:>\s]*(\d+)/i); + + const matches = [minCharsMatch, minCharsMatch2, minLengthMatch].filter(Boolean); + + if (matches.length > 0) { + const minLength = Math.max(...matches.map((m) => parseInt(m![1], 10))); + const passed = minLength >= REQUIRED_MIN_LENGTH; + + return { + checkType: this.checkType, + passed, + details: { + method: 'pwpolicy getaccountpolicies', + raw: output.substring(0, 500), + message: passed + ? `Password policy enforces minimum ${minLength} characters` + : `Password policy requires only ${minLength} characters (minimum ${REQUIRED_MIN_LENGTH} required)`, + }, + checkedAt: new Date().toISOString(), + }; + } + + return null; + } catch { + return null; + } + } + + private checkGlobalPolicy(): CheckResult | null { + try { + const globalOutput = execSync('pwpolicy -getglobalpolicy 2>/dev/null', { + encoding: 'utf-8', + timeout: 10000, + }); + + const globalMinCharsMatch = globalOutput.match(/minChars\s*[=:]\s*(\d+)/i); + + if (globalMinCharsMatch) { + const minLength = parseInt(globalMinCharsMatch[1], 10); + const passed = minLength >= REQUIRED_MIN_LENGTH; + + return { + checkType: this.checkType, + passed, + details: { + method: 'pwpolicy -getglobalpolicy', + raw: globalOutput.substring(0, 500), + message: passed + ? `Password policy enforces minimum ${minLength} characters` + : `Password policy requires only ${minLength} characters (minimum ${REQUIRED_MIN_LENGTH} required)`, + }, + checkedAt: new Date().toISOString(), + }; + } + + return null; + } catch { + return null; + } + } + + private checkConfigurationProfiles(): CheckResult | null { + try { + const output = execSync('system_profiler SPConfigurationProfileDataType 2>/dev/null', { + encoding: 'utf-8', + timeout: 15000, + }); + + // Look for password policy in MDM profiles + const minLengthMatch = output.match(/minLength\s*[=:]\s*(\d+)/i); + const minComplexCharsMatch = output.match(/minComplexChars\s*[=:]\s*(\d+)/i); + + if (minLengthMatch) { + const minLength = parseInt(minLengthMatch[1], 10); + const passed = minLength >= REQUIRED_MIN_LENGTH; + + return { + checkType: this.checkType, + passed, + details: { + method: 'system_profiler SPConfigurationProfileDataType', + raw: `MDM Profile: minLength=${minLength}${minComplexCharsMatch ? `, minComplexChars=${minComplexCharsMatch[1]}` : ''}`, + message: passed + ? `MDM profile enforces minimum ${minLength} character password` + : `MDM profile requires only ${minLength} characters (minimum ${REQUIRED_MIN_LENGTH} required)`, + }, + checkedAt: new Date().toISOString(), + }; + } + + return null; + } catch { + return null; + } + } +} diff --git a/packages/device-agent/src/checks/macos/screen-lock.ts b/packages/device-agent/src/checks/macos/screen-lock.ts new file mode 100644 index 0000000000..f7e488e91b --- /dev/null +++ b/packages/device-agent/src/checks/macos/screen-lock.ts @@ -0,0 +1,195 @@ +import { execSync } from 'node:child_process'; +import type { CheckResult } from '../../shared/types'; +import type { ComplianceCheck } from '../types'; + +const MAX_IDLE_TIME_SECONDS = 300; // 5 minutes + +/** + * Checks if screen lock is enabled and set to 5 minutes or less on macOS. + * + * Checks two settings: + * 1. Screen saver idle time (how long before screen saver activates) + * 2. Whether a password is required after screen saver / sleep + * + * On modern macOS (Ventura+), `sysadminctl -screenLock status` reports the + * screen lock delay. If a delay is reported, screen lock is enabled. + * + * On MDM-managed machines, the screen saver idle time may not be available + * via `defaults read`. In that case, if `sysadminctl` confirms screen lock + * is enabled with an acceptable delay, the check passes. + */ +export class MacOSScreenLockCheck implements ComplianceCheck { + checkType = 'screen_lock' as const; + displayName = 'Screen Lock (5 min or less)'; + + async run(): Promise { + try { + const idleTime = this.getScreenSaverIdleTime(); + const { requiresPassword, screenLockDelay } = this.getPasswordRequirement(); + + const idleTimeOk = idleTime !== null && idleTime > 0 && idleTime <= MAX_IDLE_TIME_SECONDS; + + // On MDM-managed machines, sysadminctl may report screen lock is active + // even when com.apple.screensaver idleTime is not set via defaults. + // If sysadminctl confirms screen lock with an acceptable delay, that's sufficient. + const sysadminctlOk = + requiresPassword && screenLockDelay !== null && screenLockDelay <= MAX_IDLE_TIME_SECONDS; + + const passed = (idleTimeOk && requiresPassword) || sysadminctlOk; + + let message: string; + + if (passed && sysadminctlOk && !idleTimeOk) { + message = + screenLockDelay === 0 + ? 'Screen lock is enforced with immediate password requirement' + : `Screen lock is enforced with ${screenLockDelay} second delay`; + } else if (idleTime === null || idleTime === 0) { + if (requiresPassword && screenLockDelay !== null) { + message = `Screen lock requires password but idle time exceeds ${MAX_IDLE_TIME_SECONDS} seconds`; + } else { + message = 'Screen saver idle time is not configured'; + } + } else if (passed) { + message = `Screen saver activates after ${idleTime} seconds with a password required`; + } else if (idleTime > MAX_IDLE_TIME_SECONDS) { + message = `Screen saver activates after ${idleTime} seconds (must be ${MAX_IDLE_TIME_SECONDS} or less)`; + } else if (!requiresPassword) { + message = `Screen saver activates after ${idleTime} seconds but no password is required`; + } else { + message = `Screen saver activates after ${idleTime} seconds`; + } + + return { + checkType: this.checkType, + passed, + details: { + method: 'defaults-read + sysadminctl', + raw: JSON.stringify({ idleTime, requiresPassword, screenLockDelay }), + message, + }, + checkedAt: new Date().toISOString(), + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const isPermission = /permission|not authorized|operation not permitted/i.test(errorMessage); + + return { + checkType: this.checkType, + passed: false, + details: { + method: 'defaults-read', + raw: errorMessage, + message: isPermission + ? 'Unable to determine screen lock settings due to insufficient permissions. If your device is managed by an MDM, screen lock may be enforced at the system level.' + : 'Unable to determine screen lock settings', + }, + checkedAt: new Date().toISOString(), + }; + } + } + + /** + * Gets the screen saver idle time in seconds. + * Returns null if not set. + */ + private getScreenSaverIdleTime(): number | null { + try { + const output = execSync( + 'defaults -currentHost read com.apple.screensaver idleTime 2>/dev/null', + { encoding: 'utf-8', timeout: 5000 }, + ).trim(); + + const time = parseInt(output, 10); + return isNaN(time) ? null : time; + } catch { + try { + const output = execSync('defaults read com.apple.screensaver idleTime 2>/dev/null', { + encoding: 'utf-8', + timeout: 5000, + }).trim(); + + const time = parseInt(output, 10); + return isNaN(time) ? null : time; + } catch { + return null; + } + } + } + + /** + * Checks if a password is required after screen saver or sleep. + * Uses multiple detection methods for different macOS versions. + */ + private getPasswordRequirement(): { requiresPassword: boolean; screenLockDelay: number | null } { + // Method 1: sysadminctl (macOS Ventura+) + // Output format: "screenLock delay is 300 seconds", "screenLock delay is immediate", or "screenLock is off" + try { + const output = execSync('sysadminctl -screenLock status 2>&1', { + encoding: 'utf-8', + timeout: 5000, + }).trim(); + + // If it reports a numeric delay, screen lock is enabled + const delayMatch = output.match(/screenLock\s+delay\s+is\s+(\d+)/i); + if (delayMatch) { + return { + requiresPassword: true, + screenLockDelay: parseInt(delayMatch[1], 10), + }; + } + + // "screenLock delay is immediate" means password required immediately (MDM-managed) + if (output.toLowerCase().includes('delay is immediate')) { + return { requiresPassword: true, screenLockDelay: 0 }; + } + + // "screenLock is on" also means enabled + if ( + output.toLowerCase().includes('screenlock is on') || + output.toLowerCase().includes('screensaver is on') + ) { + return { requiresPassword: true, screenLockDelay: 0 }; + } + + // "screenLock is off" means disabled + if (output.toLowerCase().includes('is off')) { + return { requiresPassword: false, screenLockDelay: null }; + } + } catch { + // sysadminctl not available + } + + // Method 2: defaults read askForPassword (older macOS) + try { + const output = execSync('defaults read com.apple.screensaver askForPassword 2>/dev/null', { + encoding: 'utf-8', + timeout: 5000, + }).trim(); + + return { + requiresPassword: output === '1', + screenLockDelay: null, + }; + } catch { + // Key doesn't exist + } + + // Method 3: Check if "require password immediately" is set via system prefs + try { + const output = execSync( + 'defaults -currentHost read com.apple.screensaver askForPassword 2>/dev/null', + { encoding: 'utf-8', timeout: 5000 }, + ).trim(); + + return { + requiresPassword: output === '1', + screenLockDelay: null, + }; + } catch { + // Not found + } + + return { requiresPassword: false, screenLockDelay: null }; + } +} diff --git a/packages/device-agent/src/checks/types.ts b/packages/device-agent/src/checks/types.ts new file mode 100644 index 0000000000..0c37081ba8 --- /dev/null +++ b/packages/device-agent/src/checks/types.ts @@ -0,0 +1,16 @@ +import type { CheckResult, DeviceCheckType } from '../shared/types'; + +/** + * Interface for a platform-specific compliance check. + * Each check module must implement this interface. + */ +export interface ComplianceCheck { + /** The type of check this module performs */ + checkType: DeviceCheckType; + + /** Human-readable name for display */ + displayName: string; + + /** Execute the check and return the result */ + run(): Promise; +} diff --git a/packages/device-agent/src/checks/windows/antivirus.ts b/packages/device-agent/src/checks/windows/antivirus.ts new file mode 100644 index 0000000000..285fc2be83 --- /dev/null +++ b/packages/device-agent/src/checks/windows/antivirus.ts @@ -0,0 +1,101 @@ +import { execSync } from 'node:child_process'; +import type { CheckResult } from '../../shared/types'; +import type { ComplianceCheck } from '../types'; + +/** + * Checks if antivirus software is installed and active on Windows. + * + * Uses WMI query through Security Center 2 to detect registered AV products. + * Also checks Windows Defender status via Get-MpComputerStatus. + */ +export class WindowsAntivirusCheck implements ComplianceCheck { + checkType = 'antivirus' as const; + displayName = 'Antivirus'; + + async run(): Promise { + try { + // Query SecurityCenter2 for registered antivirus products + const output = execSync( + 'powershell.exe -NoProfile -NonInteractive -Command "Get-CimInstance -Namespace root/SecurityCenter2 -ClassName AntiVirusProduct | Select-Object -Property displayName,productState | ConvertTo-Json"', + { encoding: 'utf-8', timeout: 15000 }, + ).trim(); + + if (!output || output === '' || output === 'null') { + return this.checkWindowsDefenderFallback(); + } + + // Parse the result - could be a single object or an array + const parsed = JSON.parse(output); + const products = Array.isArray(parsed) ? parsed : [parsed]; + + const activeProducts = products.filter((p: { displayName: string; productState: number }) => { + // productState is a bitmask. Bit 12 (0x1000) indicates the AV is active/enabled. + // Common active states: 397568 (Windows Defender on), 266240 (third-party on) + const stateHex = p.productState.toString(16); + // The second nibble from the left indicates scanner status (1 = on, 0 = off) + const scannerActive = stateHex.length >= 4 && stateHex[stateHex.length - 4] === '1'; + return scannerActive || p.productState > 0; + }); + + const passed = activeProducts.length > 0; + const productNames = activeProducts.map((p: { displayName: string }) => p.displayName).join(', '); + + return { + checkType: this.checkType, + passed, + details: { + method: 'Get-CimInstance AntiVirusProduct', + raw: output.substring(0, 500), + message: passed + ? `Active antivirus detected: ${productNames}` + : 'No active antivirus product detected', + }, + checkedAt: new Date().toISOString(), + }; + } catch { + // SecurityCenter2 might not be available (e.g., Windows Server) + return this.checkWindowsDefenderFallback(); + } + } + + private checkWindowsDefenderFallback(): CheckResult { + try { + const output = execSync( + 'powershell.exe -NoProfile -NonInteractive -Command "Get-MpComputerStatus | Select-Object -Property AntivirusEnabled,RealTimeProtectionEnabled,AntivirusSignatureLastUpdated | ConvertTo-Json"', + { encoding: 'utf-8', timeout: 15000 }, + ).trim(); + + const data = JSON.parse(output); + const isEnabled = data.AntivirusEnabled === true && data.RealTimeProtectionEnabled === true; + + return { + checkType: this.checkType, + passed: isEnabled, + details: { + method: 'Get-MpComputerStatus', + raw: output.substring(0, 500), + message: isEnabled + ? 'Windows Defender is active with real-time protection enabled' + : 'Windows Defender is not active or real-time protection is disabled', + }, + checkedAt: new Date().toISOString(), + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const isPermission = /access.denied|not recognized|unauthorized/i.test(errorMessage); + + return { + checkType: this.checkType, + passed: false, + details: { + method: 'Get-CimInstance + Get-MpComputerStatus', + raw: errorMessage, + message: isPermission + ? 'Unable to determine antivirus status due to insufficient permissions. On Windows Server or MDM-managed devices, try running the agent as Administrator.' + : 'Unable to determine antivirus status', + }, + checkedAt: new Date().toISOString(), + }; + } + } +} diff --git a/packages/device-agent/src/checks/windows/disk-encryption.ts b/packages/device-agent/src/checks/windows/disk-encryption.ts new file mode 100644 index 0000000000..183846a2ef --- /dev/null +++ b/packages/device-agent/src/checks/windows/disk-encryption.ts @@ -0,0 +1,181 @@ +import { execSync } from 'node:child_process'; +import type { CheckResult } from '../../shared/types'; +import type { ComplianceCheck } from '../types'; + +/** + * Checks if BitLocker / Device Encryption is enabled on Windows. + * + * If encryption is not available on the system (Windows Home without TPM, + * or hardware that doesn't support Device Encryption), the check passes + * with a note — we don't fail users for hardware/edition limitations. + */ +export class WindowsDiskEncryptionCheck implements ComplianceCheck { + checkType = 'disk_encryption' as const; + displayName = 'Disk Encryption (BitLocker)'; + + async run(): Promise { + // First, try to get BitLocker status (works on Pro/Enterprise with BitLocker available) + const bitlockerResult = this.checkBitLocker(); + if (bitlockerResult !== null) { + return bitlockerResult; + } + + // BitLocker commands failed — check if encryption is even available + const availability = this.checkEncryptionAvailability(); + + if (!availability.available) { + // Encryption is not available on this system — pass with explanation + return { + checkType: this.checkType, + passed: true, + details: { + method: 'availability-check', + raw: JSON.stringify(availability), + message: `Disk encryption not available: ${availability.reason}. This check is not applicable to your system.`, + exception: `BitLocker not supported (${availability.reason})`, + }, + checkedAt: new Date().toISOString(), + }; + } + + // Encryption should be available but we couldn't determine status + return { + checkType: this.checkType, + passed: false, + details: { + method: 'Get-BitLockerVolume + manage-bde', + raw: 'Commands failed but encryption appears available', + message: 'Disk encryption is not enabled or could not be detected. Enable BitLocker in Settings. If your device is managed by an MDM (e.g. Intune), try running the agent as Administrator.', + }, + checkedAt: new Date().toISOString(), + }; + } + + /** + * Tries Get-BitLockerVolume and manage-bde to check encryption status. + * Returns a CheckResult if status was determined, or null if commands failed. + */ + private checkBitLocker(): CheckResult | null { + // Method 1: Get-BitLockerVolume + try { + const output = execSync( + 'powershell.exe -NoProfile -NonInteractive -Command "Get-BitLockerVolume -MountPoint C: | Select-Object -Property MountPoint,ProtectionStatus,VolumeStatus,EncryptionPercentage | ConvertTo-Json"', + { encoding: 'utf-8', timeout: 15000 }, + ).trim(); + + const data = JSON.parse(output); + const protectionStatus = data.ProtectionStatus; + const isEnabled = protectionStatus === 1; + + return { + checkType: this.checkType, + passed: isEnabled, + details: { + method: 'Get-BitLockerVolume', + raw: output.substring(0, 500), + message: isEnabled + ? 'BitLocker is enabled on the system drive' + : 'BitLocker is not enabled on the system drive', + }, + checkedAt: new Date().toISOString(), + }; + } catch { + // Fall through to manage-bde + } + + // Method 2: manage-bde + try { + const output = execSync('manage-bde -status C:', { + encoding: 'utf-8', + timeout: 15000, + }).trim(); + + const isEnabled = + output.toLowerCase().includes('protection on') || + output.toLowerCase().includes('fully encrypted'); + + return { + checkType: this.checkType, + passed: isEnabled, + details: { + method: 'manage-bde -status', + raw: output.substring(0, 500), + message: isEnabled + ? 'BitLocker is enabled on the system drive' + : 'BitLocker is not enabled on the system drive', + }, + checkedAt: new Date().toISOString(), + }; + } catch { + return null; + } + } + + /** + * Checks whether disk encryption is available on this system. + * Looks at Windows edition and TPM presence. + */ + private checkEncryptionAvailability(): { available: boolean; reason: string } { + const edition = this.getWindowsEdition(); + const hasTpm = this.hasTpm(); + + // Windows Home without TPM = no encryption possible + if (edition === 'home' && !hasTpm) { + return { + available: false, + reason: 'Windows Home edition without TPM — neither BitLocker nor Device Encryption is supported', + }; + } + + // Windows Home with TPM = Device Encryption might be available + // but if Get-BitLockerVolume failed, it's likely not supported by the hardware + if (edition === 'home') { + return { + available: false, + reason: 'Windows Home edition — BitLocker requires Windows Pro. Device Encryption may not be supported by your hardware', + }; + } + + // Pro/Enterprise without TPM + if (!hasTpm) { + return { + available: false, + reason: 'No TPM detected — BitLocker requires a TPM 2.0 chip or compatible security device', + }; + } + + // Pro/Enterprise with TPM — encryption should be available + return { available: true, reason: '' }; + } + + private getWindowsEdition(): 'home' | 'pro' | 'unknown' { + try { + const output = execSync( + 'powershell.exe -NoProfile -NonInteractive -Command "(Get-CimInstance Win32_OperatingSystem).Caption"', + { encoding: 'utf-8', timeout: 10000 }, + ) + .trim() + .toLowerCase(); + + if (output.includes('home')) return 'home'; + if (output.includes('pro') || output.includes('enterprise') || output.includes('education')) + return 'pro'; + return 'unknown'; + } catch { + return 'unknown'; + } + } + + private hasTpm(): boolean { + try { + const output = execSync( + 'powershell.exe -NoProfile -NonInteractive -Command "(Get-Tpm).TpmPresent"', + { encoding: 'utf-8', timeout: 10000 }, + ).trim(); + + return output.toLowerCase() === 'true'; + } catch { + return false; + } + } +} diff --git a/packages/device-agent/src/checks/windows/password-policy.ts b/packages/device-agent/src/checks/windows/password-policy.ts new file mode 100644 index 0000000000..6845d2d5cd --- /dev/null +++ b/packages/device-agent/src/checks/windows/password-policy.ts @@ -0,0 +1,157 @@ +import { execSync } from 'node:child_process'; +import type { CheckResult } from '../../shared/types'; +import type { ComplianceCheck } from '../types'; + +const REQUIRED_MIN_LENGTH = 8; + +/** + * Checks if a minimum password length policy (>= 8 characters) is enforced on Windows. + * + * Uses multiple locale-independent detection methods: + * 1. ADSI WinNT provider (locale-independent, no admin needed) + * 2. `net accounts` with broad regex parsing + * 3. `secedit /export` as fallback (may need admin) + */ +export class WindowsPasswordPolicyCheck implements ComplianceCheck { + checkType = 'password_policy' as const; + displayName = 'Password Policy (Min 8 Characters)'; + + async run(): Promise { + // Method 1: ADSI — locale-independent, works without admin + const adsiResult = this.checkAdsi(); + if (adsiResult !== null) return adsiResult; + + // Method 2: net accounts — works without admin but output is localized + const netResult = this.checkNetAccounts(); + if (netResult !== null) return netResult; + + // Method 3: secedit — locale-independent but may require admin + const seceditResult = this.checkSecedit(); + if (seceditResult !== null) return seceditResult; + + return { + checkType: this.checkType, + passed: false, + details: { + method: 'ADSI + net accounts + secedit', + raw: 'All methods failed', + message: 'Could not determine password policy. Try running the agent as Administrator.', + }, + checkedAt: new Date().toISOString(), + }; + } + + /** + * Uses ADSI WinNT provider to read MinPasswordLength. + * Locale-independent and doesn't require admin. + */ + private checkAdsi(): CheckResult | null { + try { + const output = execSync( + `powershell.exe -NoProfile -NonInteractive -Command "([ADSI]'WinNT://localhost').MinPasswordLength.Value"`, + { encoding: 'utf-8', timeout: 10000 }, + ).trim(); + + const minLength = parseInt(output, 10); + if (isNaN(minLength)) return null; + + const passed = minLength >= REQUIRED_MIN_LENGTH; + + return { + checkType: this.checkType, + passed, + details: { + method: 'ADSI WinNT', + raw: `MinPasswordLength: ${minLength}`, + message: passed + ? `Password policy enforces minimum ${minLength} characters` + : `Password policy requires only ${minLength} characters (minimum ${REQUIRED_MIN_LENGTH} required)`, + }, + checkedAt: new Date().toISOString(), + }; + } catch { + return null; + } + } + + /** + * Uses `net accounts` and parses the output. + * Tries both English regex and a position-based fallback for non-English locales. + */ + private checkNetAccounts(): CheckResult | null { + try { + const output = execSync('net accounts', { + encoding: 'utf-8', + timeout: 10000, + }).trim(); + + // Try English regex first + const englishMatch = output.match(/minimum password length\s+(\d+)/i); + if (englishMatch) { + return this.buildResult('net accounts', output, parseInt(englishMatch[1], 10)); + } + + // Locale-independent fallback: password min length is typically the 4th data line. + // Parse all lines that end with a number or "None"/"Never" and take the 4th one. + const lines = output + .split('\n') + .map((l) => l.trim()) + .filter((l) => l.length > 0 && !l.startsWith('--')); + + // Look for the line with just a number (min password length is usually 0 on default) + // The structure is consistent: each line has a label followed by spaces and a value + for (const line of lines) { + const match = line.match(/\s+(\d+)\s*$/); + if (match) { + const value = parseInt(match[1], 10); + // The minimum password length line typically has a small number (0-128) + // and is the 4th line in net accounts output + const lineIndex = lines.indexOf(line); + if (lineIndex === 3) { + return this.buildResult('net accounts (position)', output, value); + } + } + } + + return null; + } catch { + return null; + } + } + + /** + * Uses secedit to export and parse security policy. + * Locale-independent. May require admin on some configurations. + */ + private checkSecedit(): CheckResult | null { + try { + const output = execSync( + `powershell.exe -NoProfile -NonInteractive -Command "$f='$env:TEMP\\compcheck.cfg'; secedit /export /cfg $f /quiet 2>$null; $c=Get-Content $f -ErrorAction SilentlyContinue; Remove-Item $f -ErrorAction SilentlyContinue; ($c | Select-String 'MinimumPasswordLength\\s*=\\s*(\\d+)').Matches[0].Groups[1].Value"`, + { encoding: 'utf-8', timeout: 15000 }, + ).trim(); + + const minLength = parseInt(output, 10); + if (isNaN(minLength)) return null; + + return this.buildResult('secedit', `MinimumPasswordLength: ${minLength}`, minLength); + } catch { + return null; + } + } + + private buildResult(method: string, raw: string, minLength: number): CheckResult { + const passed = minLength >= REQUIRED_MIN_LENGTH; + return { + checkType: this.checkType, + passed, + details: { + method, + raw: raw.substring(0, 500), + message: passed + ? `Password policy enforces minimum ${minLength} characters` + : `Password policy requires only ${minLength} characters (minimum ${REQUIRED_MIN_LENGTH} required)`, + }, + checkedAt: new Date().toISOString(), + }; + } +} diff --git a/packages/device-agent/src/checks/windows/screen-lock.ts b/packages/device-agent/src/checks/windows/screen-lock.ts new file mode 100644 index 0000000000..b17e9a0d25 --- /dev/null +++ b/packages/device-agent/src/checks/windows/screen-lock.ts @@ -0,0 +1,131 @@ +import { execSync } from 'node:child_process'; +import type { CheckResult } from '../../shared/types'; +import type { ComplianceCheck } from '../types'; + +const MAX_IDLE_TIME_SECONDS = 300; // 5 minutes + +/** + * Checks if screen lock is enabled and set to 5 minutes or less on Windows. + * + * Checks: + * 1. Screen saver timeout (ScreenSaveTimeOut registry key) + * 2. Screen saver is secure (ScreenSaverIsSecure registry key) + * 3. Power settings for display timeout as fallback + */ +export class WindowsScreenLockCheck implements ComplianceCheck { + checkType = 'screen_lock' as const; + displayName = 'Screen Lock (5 min or less)'; + + async run(): Promise { + try { + const screenSaverTimeout = this.getScreenSaverTimeout(); + const isSecure = this.getScreenSaverIsSecure(); + + // Also check power settings as additional signal + const powerTimeout = this.getPowerDisplayTimeout(); + + const screenSaverOk = + screenSaverTimeout !== null && + screenSaverTimeout > 0 && + screenSaverTimeout <= MAX_IDLE_TIME_SECONDS && + isSecure; + + const powerOk = + powerTimeout !== null && powerTimeout > 0 && powerTimeout <= MAX_IDLE_TIME_SECONDS; + + // Pass if either screen saver or power settings are properly configured + const passed = screenSaverOk || (powerOk && isSecure); + + const details: string[] = []; + + if (screenSaverTimeout !== null && screenSaverTimeout > 0) { + details.push(`Screen saver timeout: ${screenSaverTimeout} seconds`); + } else { + details.push('Screen saver timeout not configured'); + } + + details.push(isSecure ? 'Password on resume: enabled' : 'Password on resume: disabled'); + + if (powerTimeout !== null) { + details.push(`Display power timeout: ${powerTimeout} seconds`); + } + + return { + checkType: this.checkType, + passed, + details: { + method: 'registry + powercfg', + raw: JSON.stringify({ screenSaverTimeout, isSecure, powerTimeout }), + message: details.join('. '), + }, + checkedAt: new Date().toISOString(), + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const isPermission = /access.denied|not recognized|unauthorized/i.test(errorMessage); + + return { + checkType: this.checkType, + passed: false, + details: { + method: 'registry', + raw: errorMessage, + message: isPermission + ? 'Unable to determine screen lock settings due to insufficient permissions. If your device is managed by an MDM (e.g. Intune), screen lock may be enforced via Group Policy.' + : 'Unable to determine screen lock settings', + }, + checkedAt: new Date().toISOString(), + }; + } + } + + private getScreenSaverTimeout(): number | null { + try { + const output = execSync( + 'powershell.exe -NoProfile -NonInteractive -Command "(Get-ItemProperty -Path \'HKCU:\\Control Panel\\Desktop\' -Name ScreenSaveTimeOut -ErrorAction SilentlyContinue).ScreenSaveTimeOut"', + { encoding: 'utf-8', timeout: 10000 }, + ).trim(); + + const value = parseInt(output, 10); + return isNaN(value) ? null : value; + } catch { + return null; + } + } + + private getScreenSaverIsSecure(): boolean { + try { + const output = execSync( + 'powershell.exe -NoProfile -NonInteractive -Command "(Get-ItemProperty -Path \'HKCU:\\Control Panel\\Desktop\' -Name ScreenSaverIsSecure -ErrorAction SilentlyContinue).ScreenSaverIsSecure"', + { encoding: 'utf-8', timeout: 10000 }, + ).trim(); + + return output === '1'; + } catch { + return false; + } + } + + /** + * Gets the display power timeout in seconds using powercfg. + * This is the "Turn off display" timeout from power settings. + */ + private getPowerDisplayTimeout(): number | null { + try { + const output = execSync( + 'powershell.exe -NoProfile -NonInteractive -Command "powercfg /query SCHEME_CURRENT SUB_VIDEO VIDEOIDLE | Select-String \'Current AC Power Setting Index\'"', + { encoding: 'utf-8', timeout: 10000 }, + ).trim(); + + // Output format: "Current AC Power Setting Index: 0x0000012c" + const hexMatch = output.match(/0x([0-9a-fA-F]+)/); + if (hexMatch) { + return parseInt(hexMatch[1], 16); + } + + return null; + } catch { + return null; + } + } +} diff --git a/packages/device-agent/src/main/auth.ts b/packages/device-agent/src/main/auth.ts new file mode 100644 index 0000000000..2232f3b091 --- /dev/null +++ b/packages/device-agent/src/main/auth.ts @@ -0,0 +1,303 @@ +import { BrowserWindow, dialog, session } from 'electron'; +import { AGENT_VERSION, API_ROUTES } from '../shared/constants'; +import type { + DeviceInfo, + MyOrganizationsResponse, + OrgRegistration, + RegisterDeviceResponse, + StoredAuth, +} from '../shared/types'; +import { log } from './logger'; +import { getPortalUrl, setAuth } from './store'; + +/** + * Opens a BrowserWindow pointing at the portal login page. + * After successful authentication: + * 1. Fetches the user's organizations + * 2. If no orgs → shows error and signs out + * 3. Registers the device for ALL organizations + */ +export async function performLogin(deviceInfo: DeviceInfo): Promise { + const portalUrl = getPortalUrl(); + const portalOrigin = new URL(portalUrl).origin; + + // Clear any stale session cookies so the auth page always shows fresh + await session.defaultSession.clearStorageData({ + storages: ['cookies'], + }); + log('Cleared session cookies before login'); + + return new Promise((resolve) => { + const authWindow = new BrowserWindow({ + width: 480, + height: 640, + title: 'Comp AI - Sign In', + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + }, + autoHideMenuBar: true, + resizable: true, + }); + + let isResolved = false; + let authPageLoaded = false; + + const finish = (result: StoredAuth | null) => { + if (isResolved) return; + isResolved = true; + resolve(result); + if (!authWindow.isDestroyed()) { + authWindow.close(); + } + }; + + // Handle OAuth popups (Google/Microsoft/GitHub sign-in open new windows) + const ALLOWED_OAUTH_DOMAINS = [ + 'accounts.google.com', + 'login.microsoftonline.com', + 'login.live.com', + 'github.com', + new URL(portalUrl).hostname, + ]; + authWindow.webContents.setWindowOpenHandler(({ url }) => { + try { + const { hostname } = new URL(url); + const isAllowed = ALLOWED_OAUTH_DOMAINS.some( + (domain) => hostname === domain || hostname.endsWith(`.${domain}`), + ); + if (!isAllowed) { + log(`Blocked popup to non-whitelisted domain: ${hostname}`, 'WARN'); + return { action: 'deny' }; + } + } catch { + log(`Blocked popup with invalid URL: ${url}`, 'WARN'); + return { action: 'deny' }; + } + log(`OAuth popup requested: ${url}`); + return { action: 'allow' }; + }); + + // Wait for the auth page to finish loading before watching for navigation + authWindow.webContents.on('did-finish-load', () => { + if (!authPageLoaded) { + authPageLoaded = true; + log('Auth page finished loading, now watching for post-login navigation'); + } + }); + + // Navigate to the portal auth page + log(`Loading auth page: ${portalUrl}/auth`); + authWindow.loadURL(`${portalUrl}/auth`); + + let isExtracting = false; + + const handleNavigation = async (url: string, source: string) => { + if (isResolved || isExtracting) return; + + // Don't react to navigation until the auth page has loaded at least once + if (!authPageLoaded) { + log(`${source}: ${url} (ignored — auth page not yet loaded)`); + return; + } + + const parsed = new URL(url); + log(`${source}: ${parsed.pathname}`); + + // Skip non-portal origins (Google OAuth page, etc.) + if (parsed.origin !== portalOrigin) return; + + // Skip the auth page and API routes + if (parsed.pathname.startsWith('/auth')) return; + if (parsed.pathname.startsWith('/api/')) return; + + // The user has navigated past the auth page — they're logged in. + log(`Post-login page detected: ${parsed.pathname}`); + isExtracting = true; + + // Hide the window immediately so the user doesn't see the web app + if (!authWindow.isDestroyed()) { + authWindow.hide(); + } + + // Wait for cookies to settle + await new Promise((r) => setTimeout(r, 500)); + + try { + const authData = await extractAuthAndRegisterAll(deviceInfo); + if (authData) { + setAuth(authData); + log(`Auth complete: ${authData.organizations.length} org(s) registered`); + finish(authData); + } else { + log('Auth extraction returned null, closing window'); + finish(null); + } + } catch (error) { + log(`Auth extraction failed: ${error}`, 'ERROR'); + finish(null); + } + }; + + authWindow.webContents.on('did-navigate', (_event, url) => { + handleNavigation(url, 'Navigation'); + }); + + authWindow.webContents.on('did-navigate-in-page', (_event, url) => { + handleNavigation(url, 'In-page navigation'); + }); + + authWindow.on('closed', () => { + if (!isResolved) { + log('Auth window closed by user before login completed'); + isResolved = true; + resolve(null); + } + }); + }); +} + +/** + * After login, fetches the user's orgs and registers the device for all of them. + * Shows an error dialog if the user has no organizations. + */ +async function extractAuthAndRegisterAll( + deviceInfo: DeviceInfo, +): Promise { + const portalUrl = getPortalUrl(); + + // 1. Get session cookie + const cookies = await session.defaultSession.cookies.get({ url: portalUrl }); + const sessionCookie = cookies.find( + (c) => + c.name === 'better-auth.session_token' || c.name === '__Secure-better-auth.session_token', + ); + + if (!sessionCookie) { + log('No session cookie found after login', 'WARN'); + return null; + } + + const sessionToken = sessionCookie.value; + const cookieHeader = `${sessionCookie.name}=${sessionToken}`; + + // 2. Get userId from session + const sessionResponse = await fetch(`${portalUrl}/api/auth/get-session`, { + headers: { Cookie: cookieHeader, 'Content-Type': 'application/json' }, + }); + + if (!sessionResponse.ok) { + log(`Session fetch failed: ${sessionResponse.status}`, 'ERROR'); + return null; + } + + const sessionData = await sessionResponse.json(); + const userId = sessionData?.user?.id; + + if (!userId) { + log('No userId in session', 'ERROR'); + return null; + } + + log(`Authenticated as userId=${userId}`); + + // 3. Fetch all organizations the user belongs to + const orgsResponse = await fetch(`${portalUrl}${API_ROUTES.MY_ORGANIZATIONS}`, { + headers: { Cookie: cookieHeader, 'Content-Type': 'application/json' }, + }); + + if (!orgsResponse.ok) { + log(`Failed to fetch organizations: ${orgsResponse.status}`, 'ERROR'); + return null; + } + + const orgsData: MyOrganizationsResponse = await orgsResponse.json(); + + if (!orgsData.organizations || orgsData.organizations.length === 0) { + log('User has no organizations', 'WARN'); + dialog.showMessageBoxSync({ + type: 'error', + title: 'No Organization Found', + message: "You're not part of any organization.", + detail: + 'Contact your administrator to be added to an organization, then try signing in again.', + buttons: ['OK'], + }); + return null; + } + + log( + `Found ${orgsData.organizations.length} org(s): ${orgsData.organizations.map((o) => o.organizationName).join(', ')}`, + ); + + // 4. Register the device for EVERY organization + const registrations: OrgRegistration[] = []; + + for (const org of orgsData.organizations) { + log(`Registering device for org: ${org.organizationName} (${org.organizationId})`); + + try { + const registerResponse = await fetch(`${portalUrl}${API_ROUTES.REGISTER}`, { + method: 'POST', + headers: { Cookie: cookieHeader, 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...deviceInfo, + agentVersion: AGENT_VERSION, + organizationId: org.organizationId, + }), + }); + + if (!registerResponse.ok) { + const errorText = await registerResponse.text(); + log( + `Failed to register for org ${org.organizationName}: ${registerResponse.status} - ${errorText}`, + 'ERROR', + ); + continue; // Skip this org but keep going + } + + const registerData: RegisterDeviceResponse = await registerResponse.json(); + log(`Registered for ${org.organizationName}: deviceId=${registerData.deviceId}`); + + registrations.push({ + organizationId: org.organizationId, + organizationName: org.organizationName, + deviceId: registerData.deviceId, + }); + } catch (error) { + log(`Error registering for org ${org.organizationName}: ${error}`, 'ERROR'); + } + } + + if (registrations.length === 0) { + log('Failed to register device for any organization', 'ERROR'); + dialog.showMessageBoxSync({ + type: 'error', + title: 'Registration Failed', + message: 'Failed to register your device.', + detail: 'Please try again. If the problem persists, contact your administrator.', + buttons: ['OK'], + }); + return null; + } + + return { + sessionToken, + cookieName: sessionCookie.name, + userId, + organizations: registrations, + }; +} + +/** + * Sign out: clear session cookies and stored auth + */ +export async function performLogout(): Promise { + const portalUrl = getPortalUrl(); + try { + await session.defaultSession.cookies.remove(portalUrl, 'better-auth.session_token'); + await session.defaultSession.cookies.remove(portalUrl, '__Secure-better-auth.session_token'); + } catch { + // Ignore cookie removal errors + } +} diff --git a/packages/device-agent/src/main/auto-launch.ts b/packages/device-agent/src/main/auto-launch.ts new file mode 100644 index 0000000000..f7bd51987a --- /dev/null +++ b/packages/device-agent/src/main/auto-launch.ts @@ -0,0 +1,36 @@ +import { app } from 'electron'; +import { log } from './logger'; +import { getOpenAtLogin, setOpenAtLogin } from './store'; + +/** + * Syncs the OS login-item setting with the stored preference. + * Should be called once during app.whenReady() on every launch so the + * registration stays correct even if the app path changes after an update. + */ +export function initAutoLaunch(): void { + const enabled = getOpenAtLogin(); + log(`Initializing auto-launch: ${enabled ? 'enabled' : 'disabled'}`); + app.setLoginItemSettings({ + openAtLogin: enabled, + openAsHidden: true, + }); +} + +/** + * Toggles start-at-login and persists the preference. + */ +export function setAutoLaunch(enabled: boolean): void { + log(`Setting auto-launch to: ${enabled ? 'enabled' : 'disabled'}`); + setOpenAtLogin(enabled); + app.setLoginItemSettings({ + openAtLogin: enabled, + openAsHidden: true, + }); +} + +/** + * Reads the current auto-launch state from the OS. + */ +export function isAutoLaunchEnabled(): boolean { + return app.getLoginItemSettings().openAtLogin; +} diff --git a/packages/device-agent/src/main/device-info.ts b/packages/device-agent/src/main/device-info.ts new file mode 100644 index 0000000000..8b5811977c --- /dev/null +++ b/packages/device-agent/src/main/device-info.ts @@ -0,0 +1,135 @@ +import { execSync } from 'node:child_process'; +import { hostname } from 'node:os'; +import type { DeviceInfo, DevicePlatform } from '../shared/types'; + +/** + * Collects information about the current device. + */ +export function getDeviceInfo(): DeviceInfo { + const platform = getDevicePlatform(); + + return { + name: getComputerName(), + hostname: hostname(), + platform, + osVersion: getOSVersion(platform), + serialNumber: getSerialNumber(platform), + hardwareModel: getHardwareModel(platform), + }; +} + +function getDevicePlatform(): DevicePlatform { + if (process.platform === 'win32') return 'windows'; + if (process.platform === 'linux') return 'linux'; + return 'macos'; +} + +function getComputerName(): string { + try { + if (process.platform === 'darwin') { + return execSync('scutil --get ComputerName', { encoding: 'utf-8', timeout: 5000 }).trim(); + } + if (process.platform === 'win32' || process.platform === 'linux') { + return execSync('hostname', { encoding: 'utf-8', timeout: 5000 }).trim(); + } + } catch { + // Fallback + } + return hostname(); +} + +function getOSVersion(platform: DevicePlatform): string { + try { + if (platform === 'macos') { + return execSync('sw_vers -productVersion', { encoding: 'utf-8', timeout: 5000 }).trim(); + } + if (platform === 'linux') { + // Try lsb_release first, fall back to /etc/os-release + try { + return execSync('lsb_release -ds 2>/dev/null', { + encoding: 'utf-8', + timeout: 5000, + }).trim(); + } catch { + return execSync( + 'cat /etc/os-release 2>/dev/null | grep "^PRETTY_NAME=" | cut -d= -f2 | tr -d \'"\'', + { encoding: 'utf-8', timeout: 5000 }, + ).trim(); + } + } + if (platform === 'windows') { + const output = execSync( + 'powershell.exe -NoProfile -NonInteractive -Command "[System.Environment]::OSVersion.Version.ToString()"', + { encoding: 'utf-8', timeout: 5000 }, + ).trim(); + return output; + } + } catch { + // Fallback + } + return 'Unknown'; +} + +function getSerialNumber(platform: DevicePlatform): string | undefined { + try { + if (platform === 'macos') { + return ( + execSync("system_profiler SPHardwareDataType | awk '/Serial Number/{print $4}'", { + encoding: 'utf-8', + timeout: 5000, + }).trim() || undefined + ); + } + if (platform === 'linux') { + return ( + execSync('cat /sys/class/dmi/id/product_serial 2>/dev/null', { + encoding: 'utf-8', + timeout: 5000, + }).trim() || undefined + ); + } + if (platform === 'windows') { + return ( + execSync( + 'powershell.exe -NoProfile -NonInteractive -Command "(Get-CimInstance Win32_BIOS).SerialNumber"', + { encoding: 'utf-8', timeout: 5000 }, + ).trim() || undefined + ); + } + } catch { + // Serial number is optional + } + return undefined; +} + +function getHardwareModel(platform: DevicePlatform): string | undefined { + try { + if (platform === 'macos') { + return ( + execSync( + 'system_profiler SPHardwareDataType | awk \'/Model Name/{$1=$2=""; print substr($0,3)}\'', + { encoding: 'utf-8', timeout: 5000 }, + ).trim() || undefined + ); + } + if (platform === 'linux') { + return ( + execSync('cat /sys/class/dmi/id/product_name 2>/dev/null', { + encoding: 'utf-8', + timeout: 5000, + }).trim() || undefined + ); + } + if (platform === 'windows') { + return ( + execSync( + 'powershell.exe -NoProfile -NonInteractive -Command "(Get-CimInstance Win32_ComputerSystem).Model"', + { encoding: 'utf-8', timeout: 5000 }, + ).trim() || undefined + ); + } + } catch { + // Hardware model is optional + } + return undefined; +} diff --git a/packages/device-agent/src/main/index.ts b/packages/device-agent/src/main/index.ts new file mode 100644 index 0000000000..50bf80cda8 --- /dev/null +++ b/packages/device-agent/src/main/index.ts @@ -0,0 +1,371 @@ +import { app, ipcMain } from 'electron'; +import { autoUpdater } from 'electron-updater'; +import { getAllRemediationInfo, runRemediation } from '../remediations'; +import type { CheckResult, DeviceCheckType } from '../shared/types'; +import { IPC_CHANNELS } from '../shared/types'; +import { performLogin, performLogout } from './auth'; +import { initAutoLaunch } from './auto-launch'; +import { getDeviceInfo } from './device-info'; +import { log } from './logger'; +import { runChecksNow, setSessionExpiredHandler, startScheduler, stopScheduler } from './scheduler'; +import { clearAuth, getAuth, getLastCheckResults } from './store'; +import { + createTray, + destroyTray, + getStatusWindow, + openStatusWindow, + setAutoUpdateStatus, + type TrayStatus, + updateTrayMenu, +} from './tray'; + +// --- Linux Wayland/Ozone support --- +if (process.platform === 'linux') { + app.commandLine.appendSwitch('ozone-platform-auto'); +} + +// --- Top-level crash logging --- +process.on('uncaughtException', (error) => { + log(`Uncaught exception: ${error.stack ?? error.message}`, 'ERROR'); +}); +process.on('unhandledRejection', (reason) => { + log(`Unhandled rejection: ${reason}`, 'ERROR'); +}); + +log('Comp AI Device Agent starting...'); +log(`Platform: ${process.platform}, Arch: ${process.arch}, Electron: ${process.versions.electron}`); + +// Ensure single instance +const gotTheLock = app.requestSingleInstanceLock(); +if (!gotTheLock) { + log('Another instance is already running, quitting.'); + app.quit(); +} + +// Don't show the app in the dock on macOS (tray-only app) +if (process.platform === 'darwin') { + app.dock?.hide(); +} + +let currentStatus: TrayStatus = 'unauthenticated'; +let currentResults: CheckResult[] = []; + +// Handle session expiry: clear auth, update UI, and re-prompt login +setSessionExpiredHandler(async () => { + log('Session expired — clearing auth and prompting re-login'); + stopScheduler(); + await performLogout(); + clearAuth(); + currentResults = []; + setStatus('unauthenticated'); + notifyRenderer(IPC_CHANNELS.AUTH_STATE_CHANGED, false); + // Auto-open sign-in so the user can re-authenticate immediately + triggerSignIn(); +}); + +async function triggerSignIn(): Promise { + log('Sign-in flow triggered'); + const deviceInfo = getDeviceInfo(); + const auth = await performLogin(deviceInfo); + + if (auth) { + const orgNames = auth.organizations.map((o) => o.organizationName).join(', '); + log(`Login successful: ${auth.organizations.length} org(s) — ${orgNames}`); + notifyRenderer(IPC_CHANNELS.AUTH_STATE_CHANGED, true); + setStatus('checking'); + startScheduler(handleCheckComplete); + } else { + log('Login cancelled or failed'); + } +} + +const trayCallbacks = { + onSignIn: () => { + triggerSignIn(); + }, + onRunChecks: () => { + log('User triggered manual check run'); + setStatus('checking'); + runChecksNow(handleCheckComplete); + }, + onViewDetails: () => { + log('Opening status window'); + openStatusWindow(); + }, + onSignOut: async () => { + log('User signing out'); + stopScheduler(); + await performLogout(); + clearAuth(); + currentResults = []; + setStatus('unauthenticated'); + notifyRenderer(IPC_CHANNELS.AUTH_STATE_CHANGED, false); + }, + onQuit: () => { + log('User quitting app'); + stopScheduler(); + destroyTray(); + app.quit(); + }, +}; + +function setStatus(status: TrayStatus) { + currentStatus = status; + updateTrayMenu(status, currentResults, trayCallbacks); +} + +function handleCheckComplete(results: CheckResult[], isCompliant: boolean) { + currentResults = results; + log(`Check complete: ${isCompliant ? 'COMPLIANT' : 'NON-COMPLIANT'} (${results.length} checks)`); + setStatus(isCompliant ? 'compliant' : 'non-compliant'); + notifyRenderer(IPC_CHANNELS.CHECK_RESULTS_UPDATED, { results, isCompliant }); +} + +function notifyRenderer(channel: string, data: unknown) { + const statusWindow = getStatusWindow(); + if (statusWindow && !statusWindow.isDestroyed()) { + statusWindow.webContents.send(channel, data); + } +} + +// --- IPC Handlers --- + +ipcMain.handle(IPC_CHANNELS.GET_AUTH_STATUS, () => { + const auth = getAuth(); + return { + isAuthenticated: auth !== null, + organizations: auth?.organizations ?? [], + }; +}); + +ipcMain.handle(IPC_CHANNELS.LOGIN, async () => { + await triggerSignIn(); + return getAuth() !== null; +}); + +ipcMain.handle(IPC_CHANNELS.LOGOUT, async () => { + log('Logout via IPC'); + stopScheduler(); + await performLogout(); + clearAuth(); + currentResults = []; + setStatus('unauthenticated'); + notifyRenderer(IPC_CHANNELS.AUTH_STATE_CHANGED, false); +}); + +ipcMain.handle(IPC_CHANNELS.GET_CHECK_RESULTS, () => { + return getLastCheckResults(); +}); + +ipcMain.handle(IPC_CHANNELS.RUN_CHECKS_NOW, async () => { + log('Run checks now via IPC'); + setStatus('checking'); + await runChecksNow(handleCheckComplete); +}); + +ipcMain.handle(IPC_CHANNELS.GET_DEVICE_INFO, () => { + return getDeviceInfo(); +}); + +ipcMain.handle(IPC_CHANNELS.GET_APP_VERSION, () => { + return app.getVersion(); +}); + +ipcMain.handle(IPC_CHANNELS.GET_REMEDIATION_INFO, () => { + return getAllRemediationInfo(); +}); + +ipcMain.handle(IPC_CHANNELS.REMEDIATE_CHECK, async (_event, checkType: DeviceCheckType) => { + log(`Remediation requested for: ${checkType}`); + const result = await runRemediation(checkType); + log(`Remediation result: ${result.success ? 'SUCCESS' : 'FAILED'} - ${result.message}`); + + // Re-run checks after remediation to verify the fix and update status + if (result.success && !result.openedSettings) { + log('Re-running checks after successful remediation...'); + // Small delay to let system changes propagate + await new Promise((resolve) => setTimeout(resolve, 1000)); + await runChecksNow(handleCheckComplete); + } + + return result; +}); + +// --- Auto-Updater --- + +const UPDATE_CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1 hour + +/** + * On Linux, electron-updater can only auto-update AppImage builds. + * For .deb installs (process.env.APPIMAGE is not set), we manually + * fetch the latest-linux.yml manifest, compare versions, and show + * a tray notification directing the user to re-download from the portal. + */ +function isLinuxNonAppImage(): boolean { + return process.platform === 'linux' && !process.env.APPIMAGE; +} + +/** + * Simple semver comparison: returns true if remote > local. + * Handles versions like "1.2.3" — ignores pre-release tags. + */ +function isNewerVersion(remote: string, local: string): boolean { + const r = remote.replace(/^v/, '').split('.').map(Number); + const l = local.replace(/^v/, '').split('.').map(Number); + for (let i = 0; i < 3; i++) { + if ((r[i] ?? 0) > (l[i] ?? 0)) return true; + if ((r[i] ?? 0) < (l[i] ?? 0)) return false; + } + return false; +} + +/** + * Manual update checker for Linux .deb installs. + * Fetches latest-linux.yml from the update server, parses the version, + * and sets a tray notification if a newer version is available. + */ +async function checkForManualUpdate(): Promise { + try { + const updateUrl = + process.env.AUTO_UPDATE_URL || 'https://portal.trycomp.ai/api/device-agent/updates'; + const ymlUrl = `${updateUrl}/latest-linux.yml`; + + log(`Manual update check: fetching ${ymlUrl}`); + const response = await fetch(ymlUrl); + + if (!response.ok) { + log(`Manual update check: server returned ${response.status}`, 'WARN'); + return; + } + + const yml = await response.text(); + // Parse version from YAML (format: "version: 1.2.3") + const versionMatch = yml.match(/^version:\s*(.+)$/m); + if (!versionMatch) { + log('Manual update check: could not parse version from manifest', 'WARN'); + return; + } + + const remoteVersion = versionMatch[1].trim(); + const localVersion = app.getVersion(); + + if (isNewerVersion(remoteVersion, localVersion)) { + log(`Manual update: v${remoteVersion} available (current: v${localVersion})`); + setAutoUpdateStatus('update-available'); + updateTrayMenu(currentStatus, currentResults, trayCallbacks); + } else { + log(`Manual update check: up to date (v${localVersion})`); + } + } catch (err) { + log(`Manual update check failed: ${err}`, 'WARN'); + } +} + +function initAutoUpdater(): void { + if (!app.isPackaged) { + log('Skipping auto-updater in dev mode'); + return; + } + + // Linux .deb installs can't use electron-updater — use manual check instead + if (isLinuxNonAppImage()) { + log('Linux non-AppImage detected — using manual update checker'); + checkForManualUpdate(); + setInterval(checkForManualUpdate, UPDATE_CHECK_INTERVAL_MS); + return; + } + + autoUpdater.autoDownload = true; + autoUpdater.autoInstallOnAppQuit = true; + autoUpdater.autoRunAppAfterInstall = true; + + autoUpdater.on('checking-for-update', () => { + log('Checking for update...'); + }); + + autoUpdater.on('update-available', (info) => { + log(`Update available: v${info.version}`); + setAutoUpdateStatus('downloading'); + updateTrayMenu(currentStatus, currentResults, trayCallbacks); + }); + + autoUpdater.on('update-not-available', () => { + log('No update available'); + }); + + autoUpdater.on('download-progress', (progress) => { + log(`Download progress: ${Math.round(progress.percent)}%`); + }); + + autoUpdater.on('update-downloaded', (info) => { + log(`Update downloaded: v${info.version} — will install on next restart`); + setAutoUpdateStatus('ready'); + updateTrayMenu(currentStatus, currentResults, trayCallbacks); + }); + + autoUpdater.on('error', (err) => { + log(`Auto-update error: ${err.message}`, 'WARN'); + setAutoUpdateStatus(null); + }); + + // Check on launch + autoUpdater.checkForUpdates().catch((err) => { + log(`Auto-update check failed: ${err}`, 'WARN'); + }); + + // Check periodically + setInterval(() => { + autoUpdater.checkForUpdates().catch((err) => { + log(`Auto-update check failed: ${err}`, 'WARN'); + }); + }, UPDATE_CHECK_INTERVAL_MS); +} + +// --- App Lifecycle --- + +app.whenReady().then(() => { + log('App ready, creating tray...'); + + try { + createTray(trayCallbacks); + log('Tray created successfully'); + } catch (error) { + log(`Fatal: Failed to create tray: ${error}`, 'ERROR'); + app.quit(); + return; + } + + // Sync OS login-item setting with stored preference (handles path changes after updates) + initAutoLaunch(); + + // If already authenticated, start the scheduler + const auth = getAuth(); + if (auth) { + log(`Already authenticated (${auth.organizations.length} org(s)), starting scheduler`); + setStatus('checking'); + startScheduler(handleCheckComplete); + } else { + // Not authenticated: auto-open the sign-in window so the user knows what to do + log('Not authenticated, opening sign-in window automatically'); + triggerSignIn(); + } + + // Silent auto-updates via electron-updater + initAutoUpdater(); +}); + +app.on('window-all-closed', () => { + // Don't quit when all windows are closed - keep running in tray +}); + +app.on('before-quit', () => { + log('App quitting...'); + stopScheduler(); + destroyTray(); +}); + +// Handle second instance attempts +app.on('second-instance', () => { + log('Second instance detected, opening status window'); + openStatusWindow(); +}); diff --git a/packages/device-agent/src/main/logger.ts b/packages/device-agent/src/main/logger.ts new file mode 100644 index 0000000000..432b07e018 --- /dev/null +++ b/packages/device-agent/src/main/logger.ts @@ -0,0 +1,59 @@ +import { app } from 'electron'; +import { appendFileSync, existsSync, mkdirSync } from 'node:fs'; +import path from 'node:path'; + +let logFilePath: string | null = null; + +function getLogDir(): string { + if (process.platform === 'darwin') { + return path.join(app.getPath('home'), 'Library', 'Logs', 'CompAI-Device-Agent'); + } + // Windows: %APPDATA%/CompAI-Device-Agent/logs + return path.join(app.getPath('userData'), 'logs'); +} + +function ensureLogFile(): string { + if (logFilePath) return logFilePath; + + const logDir = getLogDir(); + if (!existsSync(logDir)) { + mkdirSync(logDir, { recursive: true }); + } + + const date = new Date().toISOString().split('T')[0]; + logFilePath = path.join(logDir, `device-agent-${date}.log`); + return logFilePath; +} + +/** + * Logs a message to both console and a log file. + * Log file location: + * macOS: ~/Library/Logs/CompAI-Device-Agent/ + * Windows: %APPDATA%/CompAI-Device-Agent/logs/ + */ +export function log(message: string, level: 'INFO' | 'ERROR' | 'WARN' = 'INFO'): void { + const timestamp = new Date().toISOString(); + const line = `[${timestamp}] [${level}] ${message}\n`; + + // Always log to console + if (level === 'ERROR') { + console.error(line.trim()); + } else { + console.log(line.trim()); + } + + // Also write to file + try { + const filePath = ensureLogFile(); + appendFileSync(filePath, line); + } catch { + // If file logging fails, console is still there + } +} + +/** + * Returns the path to the current log file (useful for debugging info in UI). + */ +export function getLogFilePath(): string { + return ensureLogFile(); +} diff --git a/packages/device-agent/src/main/reporter.ts b/packages/device-agent/src/main/reporter.ts new file mode 100644 index 0000000000..d127411ea2 --- /dev/null +++ b/packages/device-agent/src/main/reporter.ts @@ -0,0 +1,84 @@ +import { AGENT_VERSION, API_ROUTES } from '../shared/constants'; +import type { CheckInRequest, CheckInResponse, CheckResult } from '../shared/types'; +import { log } from './logger'; +import { getAuth, getPortalUrl } from './store'; + +export interface ReportResult { + allSucceeded: boolean; + isCompliant: boolean; + /** True if ANY org returned 401 — session has expired */ + sessionExpired: boolean; +} + +/** + * Sends compliance check results to the portal API for ALL registered organizations. + * The same check results are reported to each org's device record. + */ +export async function reportCheckResults(checks: CheckResult[]): Promise { + const auth = getAuth(); + if (!auth) { + log('Cannot report check results: not authenticated', 'ERROR'); + return { allSucceeded: false, isCompliant: false, sessionExpired: false }; + } + + const portalUrl = getPortalUrl(); + const cookieName = auth.cookieName ?? 'better-auth.session_token'; + const cookieHeader = `${cookieName}=${auth.sessionToken}`; + + let allSucceeded = true; + let anyNonCompliant = false; + let sessionExpired = false; + + for (const org of auth.organizations) { + const payload: CheckInRequest = { + deviceId: org.deviceId, + checks, + agentVersion: AGENT_VERSION, + }; + + try { + const response = await fetch(`${portalUrl}${API_ROUTES.CHECK_IN}`, { + method: 'POST', + headers: { + Cookie: cookieHeader, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const errorText = await response.text(); + log( + `Check-in failed for ${org.organizationName} (${response.status}): ${errorText}`, + 'ERROR', + ); + allSucceeded = false; + + if (response.status === 401) { + log('Session expired — user needs to re-authenticate', 'ERROR'); + sessionExpired = true; + break; // No point trying other orgs with the same expired token + } + continue; + } + + const result: CheckInResponse = await response.json(); + log( + `Check-in for ${org.organizationName}: ${result.isCompliant ? 'COMPLIANT' : 'NON-COMPLIANT'}`, + ); + + if (!result.isCompliant) { + anyNonCompliant = true; + } + } catch (error) { + log(`Failed to report to ${org.organizationName}: ${error}`, 'ERROR'); + allSucceeded = false; + } + } + + return { + allSucceeded, + isCompliant: !anyNonCompliant && allSucceeded, + sessionExpired, + }; +} diff --git a/packages/device-agent/src/main/scheduler.ts b/packages/device-agent/src/main/scheduler.ts new file mode 100644 index 0000000000..8ab5fd3f55 --- /dev/null +++ b/packages/device-agent/src/main/scheduler.ts @@ -0,0 +1,109 @@ +import { runAllChecks } from '../checks'; +import { CHECK_INTERVAL_MS } from '../shared/constants'; +import type { CheckResult } from '../shared/types'; +import { log } from './logger'; +import { reportCheckResults } from './reporter'; +import { getAuth, getCheckInterval, setLastCheckResults } from './store'; + +let checkTimer: ReturnType | null = null; +let isRunning = false; + +type CheckCallback = (results: CheckResult[], isCompliant: boolean) => void; +type SessionExpiredCallback = () => void; + +let onSessionExpired: SessionExpiredCallback | null = null; + +/** + * Registers a callback for when the session token is rejected (401). + */ +export function setSessionExpiredHandler(handler: SessionExpiredCallback): void { + onSessionExpired = handler; +} + +/** + * Starts the periodic compliance check scheduler. + * Runs an initial check immediately, then repeats on the configured interval. + */ +export function startScheduler(onCheckComplete: CheckCallback): void { + if (checkTimer) { + clearInterval(checkTimer); + } + + // Run immediately + runChecksAndReport(onCheckComplete); + + // Then schedule periodic checks + const interval = getCheckInterval() || CHECK_INTERVAL_MS; + checkTimer = setInterval(() => { + runChecksAndReport(onCheckComplete); + }, interval); + + log(`Scheduler started: checks every ${interval / 1000 / 60} minutes`); +} + +/** + * Stops the periodic scheduler. + */ +export function stopScheduler(): void { + if (checkTimer) { + clearInterval(checkTimer); + checkTimer = null; + } + log('Scheduler stopped'); +} + +/** + * Triggers an immediate check run outside the normal schedule. + */ +export async function runChecksNow(onCheckComplete: CheckCallback): Promise { + await runChecksAndReport(onCheckComplete); +} + +/** + * Runs all checks and reports results to ALL registered organizations. + */ +async function runChecksAndReport(onCheckComplete: CheckCallback): Promise { + if (isRunning) { + log('Check already in progress, skipping'); + return; + } + + const auth = getAuth(); + if (!auth) { + log('Not authenticated, skipping check'); + return; + } + + isRunning = true; + + try { + log(`Running compliance checks (reporting to ${auth.organizations.length} org(s))...`); + const results = await runAllChecks(); + setLastCheckResults(results); + + // Report to all organizations + const { isCompliant, sessionExpired } = await reportCheckResults(results); + + if (sessionExpired) { + log('Session expired during check-in, triggering re-authentication'); + onSessionExpired?.(); + return; + } + + log(`Check complete: ${isCompliant ? 'COMPLIANT' : 'NON-COMPLIANT'}`); + onCheckComplete(results, isCompliant); + } catch (error) { + log(`Error during check cycle: ${error}`, 'ERROR'); + const results: CheckResult[] = [ + { + checkType: 'disk_encryption' as const, + passed: false, + details: { method: 'error', raw: String(error), message: 'Check cycle failed' }, + checkedAt: new Date().toISOString(), + }, + ]; + onCheckComplete(results, false); + } finally { + isRunning = false; + } +} diff --git a/packages/device-agent/src/main/store.ts b/packages/device-agent/src/main/store.ts new file mode 100644 index 0000000000..633962d0b3 --- /dev/null +++ b/packages/device-agent/src/main/store.ts @@ -0,0 +1,79 @@ +import { app } from 'electron'; +import Store from 'electron-store'; +import type { CheckResult, StoredAuth } from '../shared/types'; + +declare const __PORTAL_URL__: string; + +interface StoreSchema { + auth: StoredAuth | null; + portalUrl: string; + lastCheckResults: CheckResult[]; + checkIntervalMs: number; + openAtLogin: boolean; +} + +const isDev = !app.isPackaged; +const defaultPortalUrl = isDev ? 'http://localhost:3002' : __PORTAL_URL__; + +const store = new Store({ + name: 'comp-device-agent', + encryptionKey: 'comp-device-agent-v1', + defaults: { + auth: null, + portalUrl: defaultPortalUrl, + lastCheckResults: [], + checkIntervalMs: 60 * 60 * 1000, // 1 hour + openAtLogin: true, + }, +}); + +// Always sync the portal URL with the current environment so dev +// doesn't accidentally keep a cached production URL (or vice-versa). +if (store.get('portalUrl') !== defaultPortalUrl) { + store.set('portalUrl', defaultPortalUrl); + // Clear auth too since the session token is for the old portal + store.set('auth', null); +} + +export function getAuth(): StoredAuth | null { + return store.get('auth'); +} + +export function setAuth(auth: StoredAuth): void { + store.set('auth', auth); +} + +export function clearAuth(): void { + store.set('auth', null); + store.set('lastCheckResults', []); +} + +export function getPortalUrl(): string { + return store.get('portalUrl'); +} + +export function setPortalUrl(url: string): void { + store.set('portalUrl', url); +} + +export function getLastCheckResults(): CheckResult[] { + return store.get('lastCheckResults'); +} + +export function setLastCheckResults(results: CheckResult[]): void { + store.set('lastCheckResults', results); +} + +export function getCheckInterval(): number { + return store.get('checkIntervalMs'); +} + +export function getOpenAtLogin(): boolean { + return store.get('openAtLogin'); +} + +export function setOpenAtLogin(value: boolean): void { + store.set('openAtLogin', value); +} + +export default store; diff --git a/packages/device-agent/src/main/tray.ts b/packages/device-agent/src/main/tray.ts new file mode 100644 index 0000000000..0acd831b55 --- /dev/null +++ b/packages/device-agent/src/main/tray.ts @@ -0,0 +1,312 @@ +import { app, BrowserWindow, Menu, nativeImage, shell, Tray } from 'electron'; +import { autoUpdater } from 'electron-updater'; +import path from 'node:path'; +import type { CheckResult } from '../shared/types'; +import { isAutoLaunchEnabled, setAutoLaunch } from './auto-launch'; +import { log } from './logger'; +import { getPortalUrl } from './store'; + +export type AutoUpdateStatus = 'downloading' | 'ready' | 'update-available' | null; + +let tray: Tray | null = null; +let statusWindow: BrowserWindow | null = null; +let currentAutoUpdateStatus: AutoUpdateStatus = null; + +export type TrayStatus = 'compliant' | 'non-compliant' | 'checking' | 'unauthenticated'; + +/** + * Returns the path to the assets directory, handling both dev and packaged modes. + */ +function getAssetsPath(): string { + if (app.isPackaged) { + return path.join(process.resourcesPath, 'assets'); + } + return path.join(__dirname, '../../assets'); +} + +/** + * Loads a tray icon PNG from the assets directory and adds transparent padding + * so the icon has breathing room in the system tray. + * + * Resizes the icon to `innerSize` and centers it on a `canvasSize` transparent canvas. + * Falls back to an empty image if the file is missing. + */ +function loadTrayIcon(filename: string): Electron.NativeImage { + const canvasSize = 20; + const innerSize = 16; + const padding = Math.floor((canvasSize - innerSize) / 2); + + try { + const iconPath = path.join(getAssetsPath(), filename); + const icon = nativeImage.createFromPath(iconPath); + + if (icon.isEmpty()) { + log(`Tray icon not found or empty: ${iconPath}`); + return nativeImage.createEmpty(); + } + + // Resize icon content to innerSize + const resized = icon.resize({ width: innerSize, height: innerSize, quality: 'best' }); + const resizedBitmap = resized.toBitmap(); + + // Create a transparent canvas and center the resized icon + const bytesPerPixel = 4; + const canvas = Buffer.alloc(canvasSize * canvasSize * bytesPerPixel, 0); + + for (let y = 0; y < innerSize; y++) { + const srcOffset = y * innerSize * bytesPerPixel; + const destOffset = ((y + padding) * canvasSize + padding) * bytesPerPixel; + resizedBitmap.copy(canvas, destOffset, srcOffset, srcOffset + innerSize * bytesPerPixel); + } + + return nativeImage.createFromBuffer(canvas, { + width: canvasSize, + height: canvasSize, + scaleFactor: 1.0, + }); + } catch (error) { + log(`Failed to load tray icon ${filename}: ${error}`); + return nativeImage.createEmpty(); + } +} + +function getIconForStatus(status: TrayStatus): Electron.NativeImage { + switch (status) { + case 'compliant': + return loadTrayIcon('16x16-pass.png'); + case 'non-compliant': + return loadTrayIcon('16x16-fail.png'); + case 'checking': + case 'unauthenticated': + return loadTrayIcon('16x16-default.png'); + } +} + +/** + * Creates and manages the system tray icon and menu. + */ +export function createTray(callbacks: { + onSignIn: () => void; + onRunChecks: () => void; + onViewDetails: () => void; + onSignOut: () => void; + onQuit: () => void; +}): Tray { + log('Creating system tray...'); + + try { + const icon = getIconForStatus('unauthenticated'); + tray = new Tray(icon); + tray.setToolTip('Comp AI Device Agent'); + updateTrayMenu('unauthenticated', [], callbacks); + log('System tray created successfully'); + } catch (error) { + log(`Failed to create tray: ${error}`); + throw error; + } + + return tray; +} + +/** + * Updates the auto-update status and refreshes the tray menu. + */ +export function setAutoUpdateStatus(status: AutoUpdateStatus): void { + currentAutoUpdateStatus = status; +} + +/** + * Updates the tray icon and menu based on the current status. + */ +export function updateTrayMenu( + status: TrayStatus, + checkResults: CheckResult[], + callbacks: { + onSignIn: () => void; + onRunChecks: () => void; + onViewDetails: () => void; + onSignOut: () => void; + onQuit: () => void; + }, +): void { + if (!tray) return; + + const isAuthenticated = status !== 'unauthenticated'; + const statusLabel = getStatusLabel(status); + + try { + const icon = getIconForStatus(status); + tray.setImage(icon); + } catch (error) { + log(`Failed to update tray icon: ${error}`); + } + + tray.setToolTip(`Comp AI Device Agent - ${statusLabel}`); + + const checkMenuItems = + checkResults.length > 0 + ? [ + { type: 'separator' as const }, + ...checkResults.map((check) => ({ + label: `${check.passed ? '\u2705' : '\u274C'} ${formatCheckName(check.checkType)}`, + enabled: false, + })), + ] + : []; + + // Build menu based on auth state + const menuItems: Electron.MenuItemConstructorOptions[] = [ + { + label: statusLabel, + enabled: false, + }, + ]; + + if (!isAuthenticated) { + // Not signed in: show Sign In prominently + menuItems.push( + { type: 'separator' }, + { + label: 'Sign In...', + click: callbacks.onSignIn, + }, + ); + } else { + // Signed in: show checks and actions + menuItems.push(...checkMenuItems); + menuItems.push( + { type: 'separator' }, + { + label: 'Run Checks Now', + click: callbacks.onRunChecks, + enabled: status !== 'checking', + }, + { + label: 'View Details', + click: callbacks.onViewDetails, + }, + { type: 'separator' }, + { + label: 'Sign Out', + click: callbacks.onSignOut, + }, + ); + } + + // Show auto-update status + if (currentAutoUpdateStatus === 'downloading') { + menuItems.push( + { type: 'separator' }, + { + label: 'Downloading update...', + enabled: false, + }, + ); + } else if (currentAutoUpdateStatus === 'ready') { + menuItems.push( + { type: 'separator' }, + { + label: 'Update ready — restart to install', + click: () => { + autoUpdater.quitAndInstall(); + }, + }, + ); + } else if (currentAutoUpdateStatus === 'update-available') { + menuItems.push( + { type: 'separator' }, + { + label: 'New version available — download from portal', + click: () => { + const portalUrl = getPortalUrl(); + shell.openExternal(portalUrl); + }, + }, + ); + } + + menuItems.push( + { type: 'separator' }, + { + label: 'Start at Login', + type: 'checkbox', + checked: isAutoLaunchEnabled(), + click: (menuItem) => { + setAutoLaunch(menuItem.checked); + }, + }, + { type: 'separator' }, + { label: 'Quit', click: callbacks.onQuit }, + ); + + const contextMenu = Menu.buildFromTemplate(menuItems); + tray.setContextMenu(contextMenu); +} + +/** + * Opens the status details window showing check results. + */ +export function openStatusWindow(): void { + if (statusWindow && !statusWindow.isDestroyed()) { + statusWindow.focus(); + return; + } + + statusWindow = new BrowserWindow({ + width: 480, + height: 720, + title: 'Comp AI - Device Status', + webPreferences: { + preload: path.join(__dirname, '../preload/index.js'), + nodeIntegration: false, + contextIsolation: true, + }, + autoHideMenuBar: true, + resizable: false, + }); + + // Load the renderer + if (process.env.ELECTRON_RENDERER_URL) { + statusWindow.loadURL(process.env.ELECTRON_RENDERER_URL); + } else { + statusWindow.loadFile(path.join(__dirname, '../renderer/index.html')); + } + + statusWindow.on('closed', () => { + statusWindow = null; + }); +} + +export function getStatusWindow(): BrowserWindow | null { + return statusWindow && !statusWindow.isDestroyed() ? statusWindow : null; +} + +function getStatusLabel(status: TrayStatus): string { + switch (status) { + case 'compliant': + return 'Status: Compliant'; + case 'non-compliant': + return 'Status: Non-Compliant'; + case 'checking': + return 'Status: Checking...'; + case 'unauthenticated': + return 'Status: Not Signed In'; + } +} + +function formatCheckName(checkType: string): string { + const names: Record = { + disk_encryption: 'Disk Encryption', + antivirus: 'Antivirus', + password_policy: 'Password Policy', + screen_lock: 'Screen Lock', + }; + return names[checkType] ?? checkType; +} + +export function destroyTray(): void { + if (tray) { + tray.destroy(); + tray = null; + } +} diff --git a/packages/device-agent/src/preload/index.ts b/packages/device-agent/src/preload/index.ts new file mode 100644 index 0000000000..a380999c8d --- /dev/null +++ b/packages/device-agent/src/preload/index.ts @@ -0,0 +1,43 @@ +import { contextBridge, ipcRenderer } from 'electron'; +import type { DeviceCheckType } from '../shared/types'; +import { IPC_CHANNELS } from '../shared/types'; + +/** + * Preload script exposes a safe API to the renderer via contextBridge. + */ +contextBridge.exposeInMainWorld('compAgent', { + getAuthStatus: () => ipcRenderer.invoke(IPC_CHANNELS.GET_AUTH_STATUS), + login: () => ipcRenderer.invoke(IPC_CHANNELS.LOGIN), + logout: () => ipcRenderer.invoke(IPC_CHANNELS.LOGOUT), + getAppVersion: () => ipcRenderer.invoke(IPC_CHANNELS.GET_APP_VERSION), + getCheckResults: () => ipcRenderer.invoke(IPC_CHANNELS.GET_CHECK_RESULTS), + runChecksNow: () => ipcRenderer.invoke(IPC_CHANNELS.RUN_CHECKS_NOW), + getDeviceInfo: () => ipcRenderer.invoke(IPC_CHANNELS.GET_DEVICE_INFO), + + // Remediation APIs + getRemediationInfo: () => ipcRenderer.invoke(IPC_CHANNELS.GET_REMEDIATION_INFO), + remediateCheck: (checkType: DeviceCheckType) => + ipcRenderer.invoke(IPC_CHANNELS.REMEDIATE_CHECK, checkType), + + // Event listeners + onAuthStateChanged: (callback: (isAuthenticated: boolean) => void) => { + const handler = (_event: Electron.IpcRendererEvent, isAuthenticated: boolean) => { + callback(isAuthenticated); + }; + ipcRenderer.on(IPC_CHANNELS.AUTH_STATE_CHANGED, handler); + return () => ipcRenderer.removeListener(IPC_CHANNELS.AUTH_STATE_CHANGED, handler); + }, + + onCheckResultsUpdated: ( + callback: (data: { results: unknown[]; isCompliant: boolean }) => void, + ) => { + const handler = ( + _event: Electron.IpcRendererEvent, + data: { results: unknown[]; isCompliant: boolean }, + ) => { + callback(data); + }; + ipcRenderer.on(IPC_CHANNELS.CHECK_RESULTS_UPDATED, handler); + return () => ipcRenderer.removeListener(IPC_CHANNELS.CHECK_RESULTS_UPDATED, handler); + }, +}); diff --git a/packages/device-agent/src/remediations/index.ts b/packages/device-agent/src/remediations/index.ts new file mode 100644 index 0000000000..52301d4c73 --- /dev/null +++ b/packages/device-agent/src/remediations/index.ts @@ -0,0 +1,99 @@ +import { log } from '../main/logger'; +import type { DeviceCheckType, RemediationInfo, RemediationResult } from '../shared/types'; +import type { ComplianceRemediation } from './types'; + +// macOS remediations +import { MacOSAntivirusRemediation } from './macos/antivirus'; +import { MacOSDiskEncryptionRemediation } from './macos/disk-encryption'; +import { MacOSPasswordPolicyRemediation } from './macos/password-policy'; +import { MacOSScreenLockRemediation } from './macos/screen-lock'; + +// Linux remediations +import { LinuxAntivirusRemediation } from './linux/antivirus'; +import { LinuxDiskEncryptionRemediation } from './linux/disk-encryption'; +import { LinuxPasswordPolicyRemediation } from './linux/password-policy'; +import { LinuxScreenLockRemediation } from './linux/screen-lock'; + +// Windows remediations +import { WindowsAntivirusRemediation } from './windows/antivirus'; +import { WindowsDiskEncryptionRemediation } from './windows/disk-encryption'; +import { WindowsPasswordPolicyRemediation } from './windows/password-policy'; +import { WindowsScreenLockRemediation } from './windows/screen-lock'; + +/** + * Returns the appropriate remediation modules for the current platform. + */ +function getRemediationsForPlatform(): ComplianceRemediation[] { + const platform = process.platform; + + if (platform === 'darwin') { + return [ + new MacOSDiskEncryptionRemediation(), + new MacOSAntivirusRemediation(), + new MacOSPasswordPolicyRemediation(), + new MacOSScreenLockRemediation(), + ]; + } + + if (platform === 'linux') { + return [ + new LinuxDiskEncryptionRemediation(), + new LinuxAntivirusRemediation(), + new LinuxPasswordPolicyRemediation(), + new LinuxScreenLockRemediation(), + ]; + } + + if (platform === 'win32') { + return [ + new WindowsDiskEncryptionRemediation(), + new WindowsAntivirusRemediation(), + new WindowsPasswordPolicyRemediation(), + new WindowsScreenLockRemediation(), + ]; + } + + log(`Unsupported platform for remediation: ${platform}`, 'WARN'); + return []; +} + +/** + * Returns remediation info for all checks on the current platform. + */ +export function getAllRemediationInfo(): RemediationInfo[] { + const remediations = getRemediationsForPlatform(); + return remediations.map((r) => r.getInfo()); +} + +/** + * Runs remediation for a specific check type. + * Returns the remediation result. + */ +export async function runRemediation(checkType: DeviceCheckType): Promise { + const remediations = getRemediationsForPlatform(); + const remediation = remediations.find((r) => r.checkType === checkType); + + if (!remediation) { + return { + checkType, + success: false, + message: `No remediation available for ${checkType} on this platform`, + }; + } + + try { + log(`Running remediation: ${checkType}`); + const result = await remediation.remediate(); + log( + ` Remediation ${checkType}: ${result.success ? 'SUCCESS' : 'FAILED'} - ${result.message}`, + ); + return result; + } catch (error) { + log(`Remediation failed: ${checkType} - ${error}`, 'ERROR'); + return { + checkType, + success: false, + message: `Remediation failed with error: ${error instanceof Error ? error.message : 'Unknown error'}`, + }; + } +} diff --git a/packages/device-agent/src/remediations/instructions.ts b/packages/device-agent/src/remediations/instructions.ts new file mode 100644 index 0000000000..55212dc92c --- /dev/null +++ b/packages/device-agent/src/remediations/instructions.ts @@ -0,0 +1,162 @@ +import type { DeviceCheckType, DevicePlatform } from '../shared/types'; + +/** + * Per-check, per-platform guided instructions for users when + * auto-remediation is not available or as supplementary guidance. + */ + +interface InstructionSet { + description: string; + steps: string[]; +} + +const MACOS_INSTRUCTIONS: Record = { + screen_lock: { + description: 'Enable screen lock with a 5-minute timeout', + steps: [ + 'Open System Settings', + 'Go to Lock Screen', + 'Set "Start Screen Saver when inactive" to 5 minutes or less', + 'Set "Require password after screen saver begins or display is turned off" to Immediately', + ], + }, + password_policy: { + description: 'Set a minimum password length of 8 characters', + steps: [ + 'An administrator password will be required to apply this setting', + 'Click "Fix" to set the policy, or apply it manually:', + 'Open Terminal', + 'Run: sudo pwpolicy -setglobalpolicy "minChars=8"', + 'Enter your administrator password when prompted', + ], + }, + disk_encryption: { + description: 'Enable FileVault disk encryption', + steps: [ + 'Open System Settings', + 'Go to Privacy & Security', + 'Scroll down to FileVault', + 'Click "Turn On…"', + 'Choose a recovery method (iCloud account or recovery key)', + 'Your Mac will begin encrypting in the background', + ], + }, + antivirus: { + description: 'Ensure antivirus protection is active', + steps: [ + 'macOS includes XProtect, which is built-in and always active', + 'If this check is failing, ensure your macOS is up to date:', + 'Open System Settings > General > Software Update', + 'Install any available updates', + 'Alternatively, install a third-party antivirus (e.g. CrowdStrike, SentinelOne)', + ], + }, +}; + +const LINUX_INSTRUCTIONS: Record = { + screen_lock: { + description: 'Enable screen lock with a 5-minute timeout', + steps: [ + 'GNOME: Open Settings > Privacy > Screen Lock', + 'Set "Blank Screen Delay" to 5 minutes or less', + 'Enable "Automatic Screen Lock"', + 'Set "Automatic Screen Lock Delay" to immediately', + 'KDE: Open System Settings > Workspace Behavior > Screen Locking', + 'Set "Lock screen automatically after" to 5 minutes or less', + ], + }, + password_policy: { + description: 'Set a minimum password length of 8 characters', + steps: [ + 'An administrator password will be required to apply this setting', + 'Click "Fix" to set the policy, or apply it manually:', + 'Open a terminal', + 'Run: sudo sed -i "s/^PASS_MIN_LEN.*/PASS_MIN_LEN\\t8/" /etc/login.defs', + 'Optionally install pam_pwquality: sudo apt install libpam-pwquality', + 'Configure minlen=8 in /etc/security/pwquality.conf', + ], + }, + disk_encryption: { + description: 'Enable LUKS disk encryption', + steps: [ + 'LUKS encryption must be set up during OS installation', + 'Back up all important data before proceeding', + 'Reinstall your Linux distribution and select "Encrypt the new installation" during setup', + 'Choose a strong passphrase for the encryption', + 'Store the recovery key in a safe location', + 'Note: It is not possible to encrypt an existing root partition without reinstalling', + ], + }, + antivirus: { + description: 'Install antivirus or security software', + steps: [ + 'Install ClamAV: sudo apt install clamav clamav-daemon (Debian/Ubuntu)', + 'Or: sudo dnf install clamav clamd (Fedora/RHEL)', + 'Start the service: sudo systemctl enable --now clamav-daemon', + 'Update virus definitions: sudo freshclam', + 'Alternatively, ensure AppArmor or SELinux is in enforcing mode:', + 'AppArmor: sudo aa-enforce /etc/apparmor.d/*', + 'SELinux: sudo setenforce 1', + ], + }, +}; + +const WINDOWS_INSTRUCTIONS: Record = { + screen_lock: { + description: 'Enable screen lock with a 5-minute timeout', + steps: [ + 'Open Settings (Win + I)', + 'Go to System > Power & battery (or Power)', + 'Under "Screen and sleep", set "When plugged in, turn off my screen after" to 5 minutes or less', + 'Then go to Accounts > Sign-in options', + 'Under "If you\'ve been away, when should Windows require you to sign in again?", select "When PC wakes up from sleep"', + ], + }, + password_policy: { + description: 'Set a minimum password length of 8 characters', + steps: [ + 'A UAC (admin) prompt will appear to apply this setting', + 'Click "Fix (Admin)" to set the policy, or apply it manually:', + 'Press Win + X and select "Terminal (Admin)"', + 'Run: net accounts /minpwlen:8', + 'The policy will take effect immediately', + ], + }, + disk_encryption: { + description: 'Enable device encryption', + steps: [ + 'Open Settings (Win + I) and search for "Device encryption"', + 'If found: turn on "Device encryption" and follow the prompts', + 'If not found: your device may need Windows 11 Pro for BitLocker, or your hardware may not support encryption (TPM 2.0 + Secure Boot required)', + 'To check: go to Settings > System > About and verify your Windows edition', + 'Windows 11 Pro: search for "Manage BitLocker" in the Start menu, then click "Turn on BitLocker" for the C: drive', + 'To enable TPM/Secure Boot: restart your PC, enter BIOS/UEFI settings (usually Del or F2 at boot), and enable TPM and Secure Boot', + ], + }, + antivirus: { + description: 'Ensure antivirus protection is active', + steps: [ + 'Open Settings (Win + I)', + 'Go to Privacy & security > Windows Security', + 'Click "Open Windows Security"', + 'Go to "Virus & threat protection"', + 'Ensure "Real-time protection" is turned on', + 'If a third-party antivirus is installed, ensure it is active and up to date', + ], + }, +}; + +/** + * Returns the guided instructions for a specific check on a specific platform. + */ +export function getInstructions( + platform: DevicePlatform, + checkType: DeviceCheckType, +): InstructionSet { + const platformMap: Record> = { + macos: MACOS_INSTRUCTIONS, + linux: LINUX_INSTRUCTIONS, + windows: WINDOWS_INSTRUCTIONS, + }; + return platformMap[platform][checkType]; +} diff --git a/packages/device-agent/src/remediations/linux/antivirus.ts b/packages/device-agent/src/remediations/linux/antivirus.ts new file mode 100644 index 0000000000..b2502b0719 --- /dev/null +++ b/packages/device-agent/src/remediations/linux/antivirus.ts @@ -0,0 +1,33 @@ +import type { RemediationInfo, RemediationResult } from '../../shared/types'; +import { getInstructions } from '../instructions'; +import type { ComplianceRemediation } from '../types'; + +/** + * Linux antivirus remediation. + * Provides guided instructions to install ClamAV or enterprise AV software. + * Also provides guidance on enabling AppArmor/SELinux. + */ +export class LinuxAntivirusRemediation implements ComplianceRemediation { + checkType = 'antivirus' as const; + + getInfo(): RemediationInfo { + const { description, steps } = getInstructions('linux', 'antivirus'); + return { + checkType: this.checkType, + available: true, + type: 'guide_only', + requiresAdmin: false, + description, + instructions: steps, + }; + } + + async remediate(): Promise { + return { + checkType: this.checkType, + success: false, + message: + 'Please install antivirus software manually. Follow the guided instructions for ClamAV installation or enterprise AV setup.', + }; + } +} diff --git a/packages/device-agent/src/remediations/linux/disk-encryption.ts b/packages/device-agent/src/remediations/linux/disk-encryption.ts new file mode 100644 index 0000000000..9fa62693ee --- /dev/null +++ b/packages/device-agent/src/remediations/linux/disk-encryption.ts @@ -0,0 +1,34 @@ +import type { RemediationInfo, RemediationResult } from '../../shared/types'; +import { getInstructions } from '../instructions'; +import type { ComplianceRemediation } from '../types'; + +/** + * Linux disk encryption (LUKS) remediation. + * LUKS encryption must be set up at install time for the root partition; + * it cannot be enabled on a running system without a full reinstall. + * This remediation provides guided instructions only. + */ +export class LinuxDiskEncryptionRemediation implements ComplianceRemediation { + checkType = 'disk_encryption' as const; + + getInfo(): RemediationInfo { + const { description, steps } = getInstructions('linux', 'disk_encryption'); + return { + checkType: this.checkType, + available: true, + type: 'guide_only', + requiresAdmin: false, + description, + instructions: steps, + }; + } + + async remediate(): Promise { + return { + checkType: this.checkType, + success: false, + message: + 'LUKS disk encryption must be configured during OS installation. Please follow the guided instructions to reinstall with encryption enabled.', + }; + } +} diff --git a/packages/device-agent/src/remediations/linux/password-policy.ts b/packages/device-agent/src/remediations/linux/password-policy.ts new file mode 100644 index 0000000000..13cc692367 --- /dev/null +++ b/packages/device-agent/src/remediations/linux/password-policy.ts @@ -0,0 +1,65 @@ +import { execSync } from 'node:child_process'; +import type { RemediationInfo, RemediationResult } from '../../shared/types'; +import { getInstructions } from '../instructions'; +import type { ComplianceRemediation } from '../types'; + +const REQUIRED_MIN_LENGTH = 8; + +/** + * Linux password policy remediation. + * Uses pkexec for admin elevation to update PASS_MIN_LEN in /etc/login.defs. + * pkexec shows a graphical authentication dialog on most Linux desktops. + */ +export class LinuxPasswordPolicyRemediation implements ComplianceRemediation { + checkType = 'password_policy' as const; + + getInfo(): RemediationInfo { + const { description, steps } = getInstructions('linux', 'password_policy'); + return { + checkType: this.checkType, + available: true, + type: 'admin_fix', + requiresAdmin: true, + description, + instructions: steps, + }; + } + + async remediate(): Promise { + try { + // Use pkexec (graphical sudo) to update PASS_MIN_LEN in /etc/login.defs + // sed replaces existing PASS_MIN_LEN line or appends if not found + execSync( + `pkexec bash -c 'if grep -q "^PASS_MIN_LEN" /etc/login.defs; then sed -i "s/^PASS_MIN_LEN.*/PASS_MIN_LEN\\t${REQUIRED_MIN_LENGTH}/" /etc/login.defs; else echo "PASS_MIN_LEN\\t${REQUIRED_MIN_LENGTH}" >> /etc/login.defs; fi'`, + { encoding: 'utf-8', timeout: 60000 }, // 60s timeout to allow for auth dialog + ); + + return { + checkType: this.checkType, + success: true, + message: `Password policy set: minimum ${REQUIRED_MIN_LENGTH} characters required in /etc/login.defs`, + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + + // User cancelled the pkexec dialog + if ( + errorMessage.includes('dismissed') || + errorMessage.includes('Not authorized') || + errorMessage.includes('126') + ) { + return { + checkType: this.checkType, + success: false, + message: 'Administrator authentication was cancelled', + }; + } + + return { + checkType: this.checkType, + success: false, + message: `Failed to set password policy: ${errorMessage}`, + }; + } + } +} diff --git a/packages/device-agent/src/remediations/linux/screen-lock.ts b/packages/device-agent/src/remediations/linux/screen-lock.ts new file mode 100644 index 0000000000..53a174f3c1 --- /dev/null +++ b/packages/device-agent/src/remediations/linux/screen-lock.ts @@ -0,0 +1,75 @@ +import { execSync } from 'node:child_process'; +import type { RemediationInfo, RemediationResult } from '../../shared/types'; +import { getInstructions } from '../instructions'; +import type { ComplianceRemediation } from '../types'; + +const TARGET_IDLE_TIME_SECONDS = 300; // 5 minutes + +/** + * Linux screen lock remediation. + * Auto-fixes without admin privileges by setting GNOME gsettings: + * - idle-delay to 5 minutes + * - lock-enabled to true + * - lock-delay to 0 (immediate) + */ +export class LinuxScreenLockRemediation implements ComplianceRemediation { + checkType = 'screen_lock' as const; + + getInfo(): RemediationInfo { + const { description, steps } = getInstructions('linux', 'screen_lock'); + return { + checkType: this.checkType, + available: true, + type: 'auto_fix', + requiresAdmin: false, + description, + instructions: steps, + }; + } + + async remediate(): Promise { + try { + // Set idle delay to 5 minutes + execSync(`gsettings set org.gnome.desktop.session idle-delay ${TARGET_IDLE_TIME_SECONDS}`, { + encoding: 'utf-8', + timeout: 10000, + }); + + // Enable screen lock + execSync('gsettings set org.gnome.desktop.screensaver lock-enabled true', { + encoding: 'utf-8', + timeout: 10000, + }); + + // Set lock delay to 0 (immediately after screen saver) + execSync('gsettings set org.gnome.desktop.screensaver lock-delay 0', { + encoding: 'utf-8', + timeout: 10000, + }); + + return { + checkType: this.checkType, + success: true, + message: `Screen lock configured: ${TARGET_IDLE_TIME_SECONDS / 60}-minute timeout with immediate lock`, + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + + // gsettings not available (non-GNOME DE) + if (errorMessage.includes('No such schema') || errorMessage.includes('not found')) { + return { + checkType: this.checkType, + success: false, + message: + 'Auto-fix is only available for GNOME desktop. Please configure screen lock manually in your desktop settings.', + }; + } + + return { + checkType: this.checkType, + success: false, + message: `Failed to configure screen lock: ${errorMessage}`, + }; + } + } +} diff --git a/packages/device-agent/src/remediations/macos/antivirus.ts b/packages/device-agent/src/remediations/macos/antivirus.ts new file mode 100644 index 0000000000..539a487d05 --- /dev/null +++ b/packages/device-agent/src/remediations/macos/antivirus.ts @@ -0,0 +1,51 @@ +import { execSync } from 'node:child_process'; +import type { RemediationInfo, RemediationResult } from '../../shared/types'; +import { getInstructions } from '../instructions'; +import type { ComplianceRemediation } from '../types'; + +/** + * macOS antivirus remediation. + * XProtect is built-in and always active on supported macOS versions. + * If this check is failing, the best remediation is to update macOS. + * Opens Software Update settings and provides guided instructions. + */ +export class MacOSAntivirusRemediation implements ComplianceRemediation { + checkType = 'antivirus' as const; + + getInfo(): RemediationInfo { + const { description, steps } = getInstructions('macos', 'antivirus'); + return { + checkType: this.checkType, + available: true, + type: 'open_settings', + requiresAdmin: false, + description, + instructions: steps, + settingsDeepLink: 'x-apple.systempreferences:com.apple.Software-Update-Settings.extension', + }; + } + + async remediate(): Promise { + try { + // Open Software Update settings + execSync('open "x-apple.systempreferences:com.apple.Software-Update-Settings.extension"', { + encoding: 'utf-8', + timeout: 10000, + }); + + return { + checkType: this.checkType, + success: true, + openedSettings: true, + message: + 'Opened Software Update settings. Install any available updates to ensure XProtect is current.', + }; + } catch (error) { + return { + checkType: this.checkType, + success: false, + message: `Failed to open Software Update settings: ${error instanceof Error ? error.message : String(error)}`, + }; + } + } +} diff --git a/packages/device-agent/src/remediations/macos/disk-encryption.ts b/packages/device-agent/src/remediations/macos/disk-encryption.ts new file mode 100644 index 0000000000..fe2079be88 --- /dev/null +++ b/packages/device-agent/src/remediations/macos/disk-encryption.ts @@ -0,0 +1,51 @@ +import { execSync } from 'node:child_process'; +import type { RemediationInfo, RemediationResult } from '../../shared/types'; +import { getInstructions } from '../instructions'; +import type { ComplianceRemediation } from '../types'; + +/** + * macOS disk encryption (FileVault) remediation. + * Opens System Settings to the FileVault pane and provides guided instructions. + * FileVault cannot be enabled silently — it requires the user's password + * and generates a recovery key that must be stored. + */ +export class MacOSDiskEncryptionRemediation implements ComplianceRemediation { + checkType = 'disk_encryption' as const; + + getInfo(): RemediationInfo { + const { description, steps } = getInstructions('macos', 'disk_encryption'); + return { + checkType: this.checkType, + available: true, + type: 'open_settings', + requiresAdmin: false, + description, + instructions: steps, + settingsDeepLink: 'x-apple.systempreferences:com.apple.preference.security?FileVault', + }; + } + + async remediate(): Promise { + try { + // Open System Settings to the FileVault pane + execSync('open "x-apple.systempreferences:com.apple.preference.security?FileVault"', { + encoding: 'utf-8', + timeout: 10000, + }); + + return { + checkType: this.checkType, + success: true, + openedSettings: true, + message: + 'Opened FileVault settings. Follow the on-screen instructions to enable FileVault.', + }; + } catch (error) { + return { + checkType: this.checkType, + success: false, + message: `Failed to open FileVault settings: ${error instanceof Error ? error.message : String(error)}`, + }; + } + } +} diff --git a/packages/device-agent/src/remediations/macos/password-policy.ts b/packages/device-agent/src/remediations/macos/password-policy.ts new file mode 100644 index 0000000000..bf691a5e6c --- /dev/null +++ b/packages/device-agent/src/remediations/macos/password-policy.ts @@ -0,0 +1,62 @@ +import { execSync } from 'node:child_process'; +import type { RemediationInfo, RemediationResult } from '../../shared/types'; +import { getInstructions } from '../instructions'; +import type { ComplianceRemediation } from '../types'; + +const REQUIRED_MIN_LENGTH = 8; + +/** + * macOS password policy remediation. + * Uses osascript to run pwpolicy with administrator privileges, + * which shows the native macOS password dialog. + */ +export class MacOSPasswordPolicyRemediation implements ComplianceRemediation { + checkType = 'password_policy' as const; + + getInfo(): RemediationInfo { + const { description, steps } = getInstructions('macos', 'password_policy'); + return { + checkType: this.checkType, + available: true, + type: 'admin_fix', + requiresAdmin: true, + description, + instructions: steps, + }; + } + + async remediate(): Promise { + try { + // Use osascript to elevate privileges — shows native macOS admin password dialog + // Escaped quotes: osascript uses single-quoted AppleScript, inner shell command uses escaped double quotes + const command = `pwpolicy -setglobalpolicy \\\"minChars=${REQUIRED_MIN_LENGTH}\\\"`; + execSync( + `osascript -e 'do shell script "${command}" with administrator privileges'`, + { encoding: 'utf-8', timeout: 60000 }, // 60s timeout to allow for password entry + ); + + return { + checkType: this.checkType, + success: true, + message: `Password policy set: minimum ${REQUIRED_MIN_LENGTH} characters required`, + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + + // User cancelled the admin dialog + if (errorMessage.includes('User canceled') || errorMessage.includes('-128')) { + return { + checkType: this.checkType, + success: false, + message: 'Administrator authentication was cancelled', + }; + } + + return { + checkType: this.checkType, + success: false, + message: `Failed to set password policy: ${errorMessage}`, + }; + } + } +} diff --git a/packages/device-agent/src/remediations/macos/screen-lock.ts b/packages/device-agent/src/remediations/macos/screen-lock.ts new file mode 100644 index 0000000000..7538e393ea --- /dev/null +++ b/packages/device-agent/src/remediations/macos/screen-lock.ts @@ -0,0 +1,62 @@ +import { execSync } from 'node:child_process'; +import type { RemediationInfo, RemediationResult } from '../../shared/types'; +import { getInstructions } from '../instructions'; +import type { ComplianceRemediation } from '../types'; + +const TARGET_IDLE_TIME_SECONDS = 300; // 5 minutes + +/** + * macOS screen lock remediation. + * Auto-fixes without admin privileges by writing user-level defaults: + * - Screen saver idle time set to 5 minutes + * - Password required immediately after screen saver + */ +export class MacOSScreenLockRemediation implements ComplianceRemediation { + checkType = 'screen_lock' as const; + + getInfo(): RemediationInfo { + const { description, steps } = getInstructions('macos', 'screen_lock'); + return { + checkType: this.checkType, + available: true, + type: 'auto_fix', + requiresAdmin: false, + description, + instructions: steps, + }; + } + + async remediate(): Promise { + try { + // Set screen saver idle time to 5 minutes + execSync( + `defaults -currentHost write com.apple.screensaver idleTime -int ${TARGET_IDLE_TIME_SECONDS}`, + { encoding: 'utf-8', timeout: 10000 }, + ); + + // Require password immediately after screen saver + execSync('defaults write com.apple.screensaver askForPassword -int 1', { + encoding: 'utf-8', + timeout: 10000, + }); + + // Set password delay to 0 (immediately) + execSync('defaults write com.apple.screensaver askForPasswordDelay -int 0', { + encoding: 'utf-8', + timeout: 10000, + }); + + return { + checkType: this.checkType, + success: true, + message: `Screen lock configured: ${TARGET_IDLE_TIME_SECONDS / 60}-minute timeout with immediate password requirement`, + }; + } catch (error) { + return { + checkType: this.checkType, + success: false, + message: `Failed to configure screen lock: ${error instanceof Error ? error.message : String(error)}`, + }; + } + } +} diff --git a/packages/device-agent/src/remediations/types.ts b/packages/device-agent/src/remediations/types.ts new file mode 100644 index 0000000000..5db4e91fa4 --- /dev/null +++ b/packages/device-agent/src/remediations/types.ts @@ -0,0 +1,16 @@ +import type { DeviceCheckType, RemediationInfo, RemediationResult } from '../shared/types'; + +/** + * Interface for a platform-specific compliance remediation. + * Each remediation module must implement this interface. + */ +export interface ComplianceRemediation { + /** The type of check this remediation addresses */ + checkType: DeviceCheckType; + + /** Returns information about this remediation (type, admin requirement, instructions) */ + getInfo(): RemediationInfo; + + /** Attempt to remediate the failing check */ + remediate(): Promise; +} diff --git a/packages/device-agent/src/remediations/windows/antivirus.ts b/packages/device-agent/src/remediations/windows/antivirus.ts new file mode 100644 index 0000000000..c9c8839b02 --- /dev/null +++ b/packages/device-agent/src/remediations/windows/antivirus.ts @@ -0,0 +1,66 @@ +import { execSync } from 'node:child_process'; +import type { RemediationInfo, RemediationResult } from '../../shared/types'; +import { getInstructions } from '../instructions'; +import type { ComplianceRemediation } from '../types'; + +/** + * Windows antivirus remediation. + * Opens Windows Security and optionally attempts to enable + * Windows Defender real-time protection via admin elevation. + */ +export class WindowsAntivirusRemediation implements ComplianceRemediation { + checkType = 'antivirus' as const; + + getInfo(): RemediationInfo { + const { description, steps } = getInstructions('windows', 'antivirus'); + return { + checkType: this.checkType, + available: true, + type: 'open_settings', + requiresAdmin: false, + description, + instructions: steps, + settingsDeepLink: 'windowsdefender://threat', + }; + } + + async remediate(): Promise { + try { + // Open Windows Security > Virus & threat protection directly + execSync( + 'powershell.exe -NoProfile -NonInteractive -Command "Start-Process windowsdefender://threat"', + { encoding: 'utf-8', timeout: 10000 }, + ); + + return { + checkType: this.checkType, + success: true, + openedSettings: true, + message: + 'Opened Windows Security. Turn on "Real-time protection" under Virus & threat protection settings.', + }; + } catch { + // Fallback to Windows Security main page + try { + execSync( + 'powershell.exe -NoProfile -NonInteractive -Command "Start-Process windowsdefender:"', + { encoding: 'utf-8', timeout: 10000 }, + ); + + return { + checkType: this.checkType, + success: true, + openedSettings: true, + message: + 'Opened Windows Security. Go to Virus & threat protection and ensure real-time protection is on.', + }; + } catch (error) { + return { + checkType: this.checkType, + success: false, + message: `Failed to open Windows Security: ${error instanceof Error ? error.message : String(error)}`, + }; + } + } + } +} diff --git a/packages/device-agent/src/remediations/windows/disk-encryption.ts b/packages/device-agent/src/remediations/windows/disk-encryption.ts new file mode 100644 index 0000000000..ff73377b19 --- /dev/null +++ b/packages/device-agent/src/remediations/windows/disk-encryption.ts @@ -0,0 +1,66 @@ +import { execSync } from 'node:child_process'; +import type { RemediationInfo, RemediationResult } from '../../shared/types'; +import { getInstructions } from '../instructions'; +import type { ComplianceRemediation } from '../types'; + +/** + * Windows disk encryption (BitLocker) remediation. + * Opens the BitLocker control panel and provides guided instructions. + * BitLocker cannot be enabled silently — it requires admin privileges, + * TPM, recovery key handling, and potentially a reboot. + */ +export class WindowsDiskEncryptionRemediation implements ComplianceRemediation { + checkType = 'disk_encryption' as const; + + getInfo(): RemediationInfo { + const { description, steps } = getInstructions('windows', 'disk_encryption'); + return { + checkType: this.checkType, + available: true, + type: 'open_settings', + requiresAdmin: false, + description, + instructions: steps, + settingsDeepLink: 'ms-settings:deviceencryption', + }; + } + + async remediate(): Promise { + // Try multiple paths since availability depends on Windows edition and hardware + const attempts = [ + { + cmd: 'powershell.exe -NoProfile -NonInteractive -Command "Start-Process ms-settings:deviceencryption"', + msg: 'Opened Device encryption settings. Turn on "Device encryption" if available.', + }, + { + cmd: 'control /name Microsoft.BitLockerDriveEncryption', + msg: 'Opened BitLocker Drive Encryption. Click "Turn on BitLocker" for the C: drive.', + }, + { + cmd: 'powershell.exe -NoProfile -NonInteractive -Command "Start-Process ms-settings:about"', + msg: 'Opened System About page. Check your Windows edition — Windows 11 Pro is required for BitLocker, or your hardware must support Device encryption (TPM 2.0 + Secure Boot).', + }, + ]; + + for (const attempt of attempts) { + try { + execSync(attempt.cmd, { encoding: 'utf-8', timeout: 10000 }); + return { + checkType: this.checkType, + success: true, + openedSettings: true, + message: attempt.msg, + }; + } catch { + continue; + } + } + + return { + checkType: this.checkType, + success: false, + message: + 'Could not open encryption settings. Search for "Device encryption" or "Manage BitLocker" in the Start menu.', + }; + } +} diff --git a/packages/device-agent/src/remediations/windows/password-policy.ts b/packages/device-agent/src/remediations/windows/password-policy.ts new file mode 100644 index 0000000000..4c4889d023 --- /dev/null +++ b/packages/device-agent/src/remediations/windows/password-policy.ts @@ -0,0 +1,77 @@ +import { execSync } from 'node:child_process'; +import type { RemediationInfo, RemediationResult } from '../../shared/types'; +import { getInstructions } from '../instructions'; +import type { ComplianceRemediation } from '../types'; + +const REQUIRED_MIN_LENGTH = 8; + +/** + * Windows password policy remediation. + * Uses PowerShell Start-Process with -Verb RunAs to trigger a UAC prompt, + * then runs `net accounts /minpwlen:8` with admin privileges. + */ +export class WindowsPasswordPolicyRemediation implements ComplianceRemediation { + checkType = 'password_policy' as const; + + getInfo(): RemediationInfo { + const { description, steps } = getInstructions('windows', 'password_policy'); + return { + checkType: this.checkType, + available: true, + type: 'admin_fix', + requiresAdmin: true, + description, + instructions: steps, + }; + } + + async remediate(): Promise { + // Try multiple methods to set the password policy + const methods = [ + { + name: 'net accounts', + cmd: `powershell.exe -NoProfile -NonInteractive -Command "Start-Process -FilePath 'cmd.exe' -ArgumentList '/c net accounts /minpwlen:${REQUIRED_MIN_LENGTH}' -Verb RunAs -Wait"`, + }, + { + name: 'ADSI', + cmd: `powershell.exe -NoProfile -NonInteractive -Command "Start-Process powershell -Verb RunAs -Wait -ArgumentList '-NoProfile -NonInteractive -Command \\\"$c=[ADSI]''WinNT://localhost''; $c.MinPasswordLength=${REQUIRED_MIN_LENGTH}; $c.SetInfo()\\\"'"`, + }, + ]; + + for (const method of methods) { + try { + execSync(method.cmd, { encoding: 'utf-8', timeout: 60000 }); + + return { + checkType: this.checkType, + success: true, + message: `Password policy set: minimum ${REQUIRED_MIN_LENGTH} characters required (via ${method.name})`, + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + + // User cancelled the UAC dialog — don't try next method + if ( + errorMessage.includes('canceled') || + errorMessage.includes('The operation was canceled') || + errorMessage.includes('cancelled') + ) { + return { + checkType: this.checkType, + success: false, + message: 'Admin elevation was cancelled. Please accept the admin prompt to set the password policy.', + }; + } + + // Try next method + continue; + } + } + + return { + checkType: this.checkType, + success: false, + message: `Failed to set password policy. Try manually: open Terminal as Admin and run "net accounts /minpwlen:${REQUIRED_MIN_LENGTH}"`, + }; + } +} diff --git a/packages/device-agent/src/remediations/windows/screen-lock.ts b/packages/device-agent/src/remediations/windows/screen-lock.ts new file mode 100644 index 0000000000..358807466c --- /dev/null +++ b/packages/device-agent/src/remediations/windows/screen-lock.ts @@ -0,0 +1,65 @@ +import { execSync } from 'node:child_process'; +import type { RemediationInfo, RemediationResult } from '../../shared/types'; +import { getInstructions } from '../instructions'; +import type { ComplianceRemediation } from '../types'; + +const TARGET_IDLE_TIME_SECONDS = 300; // 5 minutes + +/** + * Windows screen lock remediation. + * Auto-fixes without admin privileges by writing user-level HKCU registry keys: + * - ScreenSaveTimeOut = 300 (5 minutes) + * - ScreenSaverIsSecure = 1 (password required on resume) + * - ScreenSaveActive = 1 (screen saver enabled) + */ +export class WindowsScreenLockRemediation implements ComplianceRemediation { + checkType = 'screen_lock' as const; + + getInfo(): RemediationInfo { + const { description, steps } = getInstructions('windows', 'screen_lock'); + return { + checkType: this.checkType, + available: true, + type: 'auto_fix', + requiresAdmin: false, + description, + instructions: steps, + }; + } + + async remediate(): Promise { + try { + const regPath = 'HKCU:\\Control Panel\\Desktop'; + + // Enable screen saver + execSync( + `powershell.exe -NoProfile -NonInteractive -Command "Set-ItemProperty -Path '${regPath}' -Name ScreenSaveActive -Value '1'"`, + { encoding: 'utf-8', timeout: 10000 }, + ); + + // Set timeout to 5 minutes + execSync( + `powershell.exe -NoProfile -NonInteractive -Command "Set-ItemProperty -Path '${regPath}' -Name ScreenSaveTimeOut -Value '${TARGET_IDLE_TIME_SECONDS}'"`, + { encoding: 'utf-8', timeout: 10000 }, + ); + + // Require password on resume + execSync( + `powershell.exe -NoProfile -NonInteractive -Command "Set-ItemProperty -Path '${regPath}' -Name ScreenSaverIsSecure -Value '1'"`, + { encoding: 'utf-8', timeout: 10000 }, + ); + + return { + checkType: this.checkType, + success: true, + message: `Screen lock configured: ${TARGET_IDLE_TIME_SECONDS / 60}-minute timeout with password on resume`, + }; + } catch (error) { + return { + checkType: this.checkType, + success: false, + message: `Failed to configure screen lock: ${error instanceof Error ? error.message : String(error)}`, + }; + } + } +} diff --git a/packages/device-agent/src/renderer/App.tsx b/packages/device-agent/src/renderer/App.tsx new file mode 100644 index 0000000000..2289d26d6b --- /dev/null +++ b/packages/device-agent/src/renderer/App.tsx @@ -0,0 +1,445 @@ +import { + Badge, + Button, + Card, + CardContent, + Heading, + LogoIcon, + Separator, + Stack, + Text, +} from '@trycompai/design-system'; +import { ChevronDown, ChevronUp, Launch, Renew, Settings } from '@trycompai/design-system/icons'; +import React, { useCallback, useEffect, useState } from 'react'; +import type { + CheckResult, + DeviceCheckType, + RemediationInfo, + RemediationResult, +} from '../shared/types'; + +declare global { + interface Window { + compAgent: { + getAppVersion: () => Promise; + getAuthStatus: () => Promise<{ + isAuthenticated: boolean; + organizations: Array<{ organizationName: string }>; + }>; + login: () => Promise; + logout: () => Promise; + getCheckResults: () => Promise; + runChecksNow: () => Promise; + getDeviceInfo: () => Promise<{ + name: string; + hostname: string; + platform: string; + osVersion: string; + } | null>; + getRemediationInfo: () => Promise; + remediateCheck: (checkType: DeviceCheckType) => Promise; + onAuthStateChanged: (callback: (isAuthenticated: boolean) => void) => () => void; + onCheckResultsUpdated: ( + callback: (data: { results: CheckResult[]; isCompliant: boolean }) => void, + ) => () => void; + }; + } +} + +const CHECK_NAMES: Record = { + disk_encryption: 'Disk Encryption', + antivirus: 'Antivirus', + password_policy: 'Password Policy', + screen_lock: 'Screen Lock', +}; + +const CHECK_DESCRIPTIONS: Record = { + disk_encryption: 'FileVault or BitLocker enabled', + antivirus: 'Antivirus software active', + password_policy: 'Minimum 8 character password', + screen_lock: 'Screen locks within 5 minutes', +}; + +/** Label for the remediation button based on remediation type */ +function getRemediationButtonLabel(info: RemediationInfo): string { + switch (info.type) { + case 'auto_fix': + return 'Fix'; + case 'admin_fix': + return 'Fix (Admin)'; + case 'open_settings': + return 'Open Settings'; + case 'guide_only': + return 'View Guide'; + } +} + +/** Icon for the remediation button based on remediation type */ +function getRemediationButtonIcon(info: RemediationInfo): React.ReactNode { + switch (info.type) { + case 'auto_fix': + case 'admin_fix': + return ; + case 'open_settings': + return ; + case 'guide_only': + return ; + } +} + +/** Individual check card with remediation capabilities */ +function CheckCard({ + check, + remediationInfo, +}: { + check: CheckResult; + remediationInfo: RemediationInfo | undefined; +}) { + const [isExpanded, setIsExpanded] = useState(false); + const [isRemediating, setIsRemediating] = useState(false); + const [remediationResult, setRemediationResult] = useState(null); + + const isGuideOnly = remediationInfo?.type === 'guide_only'; + + const handleRemediate = useCallback(async () => { + // For guide_only, just toggle the instructions panel + if (isGuideOnly) { + setIsExpanded((prev) => !prev); + return; + } + setIsRemediating(true); + setRemediationResult(null); + try { + const result = await window.compAgent.remediateCheck(check.checkType); + setRemediationResult(result); + } finally { + setIsRemediating(false); + } + }, [check.checkType, isGuideOnly]); + + const showRemediation = !check.passed && remediationInfo?.available; + + return ( + + + {/* Main check row */} + + + + {CHECK_NAMES[check.checkType]} + + + {check.details.message || CHECK_DESCRIPTIONS[check.checkType]} + + +
    + + {check.passed ? 'Pass' : 'Fail'} + +
    +
    + + {/* Remediation actions for failing checks */} + {showRemediation && ( +
    + {/* Remediation result feedback */} + {remediationResult && ( +
    + {remediationResult.message} +
    + )} + + {/* Action buttons */} + + + + {!isGuideOnly && remediationInfo.instructions.length > 0 && ( + + )} + + + {/* Expandable guided instructions */} + {isExpanded && remediationInfo.instructions.length > 0 && ( +
    +
      + {remediationInfo.instructions.map((step, index) => ( +
    1. + {step} +
    2. + ))} +
    +
    + )} +
    + )} +
    +
    + ); +} + +export default function App() { + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [checkResults, setCheckResults] = useState([]); + const [isCompliant, setIsCompliant] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [isRunning, setIsRunning] = useState(false); + const [remediationInfoMap, setRemediationInfoMap] = useState>({}); + const [isFixingAll, setIsFixingAll] = useState(false); + const [appVersion, setAppVersion] = useState(''); + + /** Load remediation info from main process */ + const loadRemediationInfo = useCallback(async () => { + try { + const infos = await window.compAgent.getRemediationInfo(); + const map: Record = {}; + for (const info of infos) { + map[info.checkType] = info; + } + setRemediationInfoMap(map); + } catch (error) { + console.error('Failed to load remediation info:', error); + } + }, []); + + useEffect(() => { + async function init() { + try { + window.compAgent.getAppVersion().then(setAppVersion).catch(() => {}); + const authStatus = await window.compAgent.getAuthStatus(); + setIsAuthenticated(authStatus.isAuthenticated); + if (authStatus.isAuthenticated) { + const [results] = await Promise.all([ + window.compAgent.getCheckResults(), + loadRemediationInfo(), + ]); + setCheckResults(results); + setIsCompliant(results.length >= 4 && results.every((r) => r.passed)); + } + } catch (error) { + console.error('Failed to load initial state:', error); + } finally { + setIsLoading(false); + } + } + init(); + }, [loadRemediationInfo]); + + useEffect(() => { + const removeAuthListener = window.compAgent.onAuthStateChanged((authenticated) => { + setIsAuthenticated(authenticated); + if (authenticated) { + loadRemediationInfo(); + } else { + setCheckResults([]); + setIsCompliant(false); + setRemediationInfoMap({}); + } + }); + const removeCheckListener = window.compAgent.onCheckResultsUpdated((data) => { + setCheckResults(data.results as CheckResult[]); + setIsCompliant(data.isCompliant); + }); + return () => { + removeAuthListener(); + removeCheckListener(); + }; + }, [loadRemediationInfo]); + + const handleLogin = useCallback(async () => { + setIsLoading(true); + try { + await window.compAgent.login(); + } finally { + setIsLoading(false); + } + }, []); + + const handleRunChecks = useCallback(async () => { + setIsRunning(true); + try { + await window.compAgent.runChecksNow(); + } finally { + setIsRunning(false); + } + }, []); + + /** Fix all failing checks that have auto_fix or admin_fix remediation */ + const handleFixAll = useCallback(async () => { + setIsFixingAll(true); + try { + const failingChecks = checkResults.filter((c) => !c.passed); + const fixableChecks = failingChecks.filter((c) => { + const info = remediationInfoMap[c.checkType]; + return info?.available && (info.type === 'auto_fix' || info.type === 'admin_fix'); + }); + + for (const check of fixableChecks) { + await window.compAgent.remediateCheck(check.checkType); + } + } catch (error) { + console.error('Fix all failed:', error); + } finally { + setIsFixingAll(false); + } + }, [checkResults, remediationInfoMap]); + + if (isLoading) { + return ( +
    + + + + Loading... + + +
    + ); + } + + if (!isAuthenticated) { + return ( +
    + + + + Comp AI Device Agent + + Sign in to your Comp AI portal to begin device compliance monitoring. + + +
    + +
    +
    +
    + + Comp AI Device Agent {appVersion ? `v${appVersion}` : ''} + +
    +
    + ); + } + + const passCount = checkResults.filter((c) => c.passed).length; + const totalCount = checkResults.length; + const failingChecks = checkResults.filter((c) => !c.passed); + const hasFixableChecks = failingChecks.some((c) => { + const info = remediationInfoMap[c.checkType]; + return info?.available && (info.type === 'auto_fix' || info.type === 'admin_fix'); + }); + + return ( +
    + {/* Header */} + + + + + Comp AI + + + Device Compliance + + + + +
    + +
    + + {/* Status */} + + + + + + {isCompliant ? 'Device Compliant' : 'Device Non-Compliant'} + + + {passCount} of {totalCount} checks passing + + +
    + + {isCompliant ? 'Compliant' : 'Action Required'} + +
    +
    + + {/* Fix All button when there are fixable failing checks */} + {!isCompliant && hasFixableChecks && ( +
    + +
    + )} +
    +
    + + {/* Checks */} +
    + {checkResults.map((check) => ( + + ))} +
    + + {/* Footer */} +
    + +
    + + Comp AI Device Agent {appVersion ? `v${appVersion}` : ''} + +
    +
    +
    + ); +} diff --git a/packages/device-agent/src/renderer/index.html b/packages/device-agent/src/renderer/index.html new file mode 100644 index 0000000000..4c349cd212 --- /dev/null +++ b/packages/device-agent/src/renderer/index.html @@ -0,0 +1,13 @@ + + + + + + + Comp AI Device Agent + + +
    + + + diff --git a/packages/device-agent/src/renderer/main.tsx b/packages/device-agent/src/renderer/main.tsx new file mode 100644 index 0000000000..fe80c09e71 --- /dev/null +++ b/packages/device-agent/src/renderer/main.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; +import './styles.css'; + +const rootEl = document.getElementById('root'); +if (rootEl) { + createRoot(rootEl).render( + + + , + ); +} diff --git a/packages/device-agent/src/renderer/styles.css b/packages/device-agent/src/renderer/styles.css new file mode 100644 index 0000000000..85b3e1a53a --- /dev/null +++ b/packages/device-agent/src/renderer/styles.css @@ -0,0 +1,12 @@ +@import 'tailwindcss'; +@import '@trycompai/design-system/globals.css'; + +/* Scan DS component source files so Tailwind includes their utility classes */ +@source "../../../../node_modules/@trycompai/design-system/src/**/*.tsx"; + +@layer base { + body { + user-select: none; + -webkit-user-select: none; + } +} diff --git a/packages/device-agent/src/shared/constants.ts b/packages/device-agent/src/shared/constants.ts new file mode 100644 index 0000000000..556ade6dcb --- /dev/null +++ b/packages/device-agent/src/shared/constants.ts @@ -0,0 +1,27 @@ +declare const __PORTAL_URL__: string; +declare const __AGENT_VERSION__: string; + +/** Default portal base URL - injected at build time via electron-vite define */ +export const DEFAULT_PORTAL_URL = + typeof __PORTAL_URL__ !== 'undefined' ? __PORTAL_URL__ : 'https://app.staging.trycomp.ai'; + +/** How often to run compliance checks (in milliseconds) */ +export const CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1 hour + +/** Agent version reported to the server */ +export const AGENT_VERSION = + typeof __AGENT_VERSION__ !== 'undefined' ? __AGENT_VERSION__ : '1.0.0'; + +/** API route paths on the portal */ +export const API_ROUTES = { + REGISTER: '/api/device-agent/register', + CHECK_IN: '/api/device-agent/check-in', + STATUS: '/api/device-agent/status', + MY_ORGANIZATIONS: '/api/device-agent/my-organizations', +} as const; + +/** Auth callback path used by the Electron BrowserWindow login flow */ +export const AUTH_CALLBACK_PATH = '/api/auth/get-session'; + +/** electron-store encryption key identifier */ +export const STORE_ENCRYPTION_KEY = 'comp-device-agent-store'; diff --git a/packages/device-agent/src/shared/types.ts b/packages/device-agent/src/shared/types.ts new file mode 100644 index 0000000000..dfd04cf654 --- /dev/null +++ b/packages/device-agent/src/shared/types.ts @@ -0,0 +1,129 @@ +export type DevicePlatform = 'macos' | 'windows' | 'linux'; + +export type DeviceCheckType = 'disk_encryption' | 'antivirus' | 'password_policy' | 'screen_lock'; + +export interface CheckResult { + checkType: DeviceCheckType; + passed: boolean; + details: { + method: string; + raw: string; + message: string; + exception?: string; + }; + checkedAt: string; +} + +export interface DeviceInfo { + name: string; + hostname: string; + platform: DevicePlatform; + osVersion: string; + serialNumber?: string; + hardwareModel?: string; +} + +export interface RegisterDeviceRequest { + name: string; + hostname: string; + platform: DevicePlatform; + osVersion: string; + serialNumber?: string; + hardwareModel?: string; + agentVersion?: string; + organizationId: string; +} + +export interface RegisterDeviceResponse { + deviceId: string; +} + +export interface CheckInRequest { + deviceId: string; + checks: CheckResult[]; + agentVersion?: string; +} + +export interface CheckInResponse { + isCompliant: boolean; + nextCheckIn: string; +} + +export interface DeviceStatus { + id: string; + name: string; + hostname: string; + platform: DevicePlatform; + osVersion: string; + isCompliant: boolean; + lastCheckIn: string | null; + checks: Array<{ + checkType: DeviceCheckType; + passed: boolean; + details: Record | null; + checkedAt: string; + }>; +} + +/** A single organization registration with its device ID */ +export interface OrgRegistration { + organizationId: string; + organizationName: string; + deviceId: string; +} + +/** Stored authentication data — supports multiple organizations */ +export interface StoredAuth { + sessionToken: string; + /** The cookie name used by the server (e.g. 'better-auth.session_token' or '__Secure-better-auth.session_token') */ + cookieName: string; + userId: string; + organizations: OrgRegistration[]; +} + +/** Response from the /api/device-agent/my-organizations endpoint */ +export interface MyOrganizationsResponse { + organizations: Array<{ + organizationId: string; + organizationName: string; + organizationSlug: string; + role: string; + }>; +} + +/** The type of remediation available for a check */ +export type RemediationType = 'auto_fix' | 'admin_fix' | 'open_settings' | 'guide_only'; + +/** Describes what remediation is available for a given check */ +export interface RemediationInfo { + checkType: DeviceCheckType; + available: boolean; + type: RemediationType; + requiresAdmin: boolean; + description: string; + instructions: string[]; + settingsDeepLink?: string; +} + +/** Result returned after attempting a remediation */ +export interface RemediationResult { + checkType: DeviceCheckType; + success: boolean; + message: string; + openedSettings?: boolean; +} + +/** IPC channel names for main <-> renderer communication */ +export const IPC_CHANNELS = { + GET_AUTH_STATUS: 'auth:get-status', + LOGIN: 'auth:login', + LOGOUT: 'auth:logout', + AUTH_STATE_CHANGED: 'auth:state-changed', + GET_CHECK_RESULTS: 'checks:get-results', + RUN_CHECKS_NOW: 'checks:run-now', + CHECK_RESULTS_UPDATED: 'checks:results-updated', + GET_DEVICE_INFO: 'device:get-info', + REMEDIATE_CHECK: 'remediation:remediate-check', + GET_REMEDIATION_INFO: 'remediation:get-info', + GET_APP_VERSION: 'app:get-version', +} as const; diff --git a/packages/device-agent/tsconfig.json b/packages/device-agent/tsconfig.json new file mode 100644 index 0000000000..67eec56316 --- /dev/null +++ b/packages/device-agent/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "jsx": "react-jsx", + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/docs/openapi.json b/packages/docs/openapi.json index 7eb1ee6d10..284120fb53 100644 --- a/packages/docs/openapi.json +++ b/packages/docs/openapi.json @@ -16889,9 +16889,17 @@ "example": "2024-01-10T00:00:00Z" }, "id": { - "type": "number", - "description": "Device ID", - "example": 123 + "description": "Device ID (numeric for Fleet devices, string UUID for device-agent devices)", + "oneOf": [ + { + "type": "number", + "example": 123 + }, + { + "type": "string", + "example": "clx1abc123" + } + ] }, "detail_updated_at": { "type": "string", @@ -17194,6 +17202,15 @@ "type": "string", "description": "Display name", "example": "John's MacBook Pro" + }, + "source": { + "type": "string", + "description": "Source system that reported this device", + "example": "fleet", + "enum": [ + "fleet", + "device_agent" + ] } }, "required": [ diff --git a/packages/utils/package.json b/packages/utils/package.json index 203890f166..8ce4ea67fa 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -6,6 +6,7 @@ }, "exports": { ".": "./src/index.ts", + "./devices": "./src/devices.ts", "./envs": "./src/envs.ts", "./format": "./src/format.ts" }, diff --git a/packages/utils/src/devices.ts b/packages/utils/src/devices.ts new file mode 100644 index 0000000000..79a9fd9e7c --- /dev/null +++ b/packages/utils/src/devices.ts @@ -0,0 +1,35 @@ +/** + * Merges two device lists, deduplicating by serial number and hostname. + * Priority devices (first argument) take precedence over secondary devices. + * + * This is used to merge device-agent devices (priority) with FleetDM devices, + * ensuring no duplicates appear when a device is tracked by both systems. + */ +export function mergeDeviceLists( + priorityDevices: T[], + secondaryDevices: T[], + accessors: { + getSerialNumber: (device: T) => string | null | undefined; + getHostname: (device: T) => string | null | undefined; + }, +): T[] { + const knownSerials = new Set(); + const knownHostnames = new Set(); + + for (const device of priorityDevices) { + const serial = accessors.getSerialNumber(device); + const hostname = accessors.getHostname(device); + if (serial) knownSerials.add(serial.toLowerCase()); + if (hostname) knownHostnames.add(hostname.toLowerCase()); + } + + const uniqueSecondaryDevices = secondaryDevices.filter((device) => { + const serial = accessors.getSerialNumber(device); + const hostname = accessors.getHostname(device); + if (serial && knownSerials.has(serial.toLowerCase())) return false; + if (hostname && knownHostnames.has(hostname.toLowerCase())) return false; + return true; + }); + + return [...priorityDevices, ...uniqueSecondaryDevices]; +} diff --git a/turbo.json b/turbo.json index 62022bf62d..3462825b34 100644 --- a/turbo.json +++ b/turbo.json @@ -85,6 +85,11 @@ "persistent": true, "cache": false }, + "@comp/device-agent#dev": { + "persistent": true, + "cache": false, + "interactive": true + }, "clean": { "cache": false },