From 63572d3e486be19c7689023e08f055b379b83636 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 15 Feb 2026 17:57:10 -0500 Subject: [PATCH 1/8] [dev] [tofikwest] tofik/device-agent-fork-lewis (#2135) * feat(device-agent): new device agent package and CI workflow for releases * feat(devices): add linux platform support to device interfaces and schemas * refactor(people): update Employee component to use button rendering * refactor(devices): simplify device ID handling and update DTO definition * feat(api): add organization membership verification for device check-in * refactor(api): update device ID definition to support multiple types * feat(devices): add linux support to platform labels in device components * refactor(people): update EmployeeCompletionChart to manage perPage state * feat(ci): enhance device agent release workflow with version detection * refactor(ci): update portal URL handling in device agent release workflow * chore(device-agent): remove unused generate-icons script * feat(devices): add mergeDeviceLists utility for device deduplication * refactor(devices): update disk encryption check to use latest result * chore(device-agent): update version and author information in package.json * feat(device-agent): comp-ai device agent * feat(device-agent): update auto update interval time * fix(employee): remove EMPLOYEE_FORM_ID from Employee * fix(fleet-logic): return old condition for fleet * feat(device-agent): s3 logic for stage * fix(device-agent): using correct time for updated_at var * fix(org-training-course): fix for orgs which disable trainings * fix(trainining-videos): corerct calc total tasks --------- Co-authored-by: Lewis Carhart Co-authored-by: Tofik Hasanov --- .github/workflows/device-agent-release.yml | 453 ++++++++++ apps/api/package.json | 1 + apps/api/src/devices/devices.service.ts | 357 +++++--- .../src/devices/dto/device-responses.dto.ts | 15 +- apps/api/src/lib/fleet.service.ts | 6 + apps/api/src/people/people.service.ts | 22 + apps/app/package.json | 1 + .../[employeeId]/components/Employee.tsx | 50 +- .../[employeeId]/components/EmployeeTasks.tsx | 85 ++ .../[orgId]/people/[employeeId]/page.tsx | 99 ++- .../people/all/components/MemberRow.tsx | 23 +- .../all/components/PendingInvitationRow.tsx | 21 +- .../people/all/components/TeamMembers.tsx | 73 ++ .../all/components/TeamMembersClient.tsx | 9 + .../components/DeviceAgentDevicesList.tsx | 235 ++++++ .../components/DeviceComplianceChart.tsx | 1 - .../devices/components/DeviceDetails.tsx | 190 +++++ .../[orgId]/people/devices/data/index.ts | 63 +- .../[orgId]/people/devices/types/index.ts | 41 +- .../app/src/app/(app)/[orgId]/people/page.tsx | 54 +- apps/portal/.env.example | 3 + apps/portal/next.config.ts | 7 +- apps/portal/package.json | 1 + .../[orgId]/components/EmployeeTasksList.tsx | 121 +-- .../components/OrganizationDashboard.tsx | 25 +- .../components/policy/AdvancedEditor.tsx | 4 +- .../[orgId]/components/policy/PolicyCard.tsx | 11 +- .../components/policy/PolicyCarousel.tsx | 8 +- .../components/policy/PolicyContainer.tsx | 12 +- .../[orgId]/components/policy/PolicyGrid.tsx | 4 +- .../components/policy/PortalPdfViewer.tsx | 7 +- .../tasks/DeviceAgentAccordionItem.tsx | 422 +++++----- .../tasks/GeneralTrainingAccordionItem.tsx | 17 +- .../tasks/PoliciesAccordionItem.tsx | 19 +- .../components/video/CarouselControls.tsx | 8 +- .../[orgId]/components/video/YoutubeEmbed.tsx | 30 +- .../src/app/(app)/(home)/[orgId]/page.tsx | 37 +- .../policy/[policyId]/PolicyAcceptButton.tsx | 21 +- .../(home)/[orgId]/policy/[policyId]/page.tsx | 86 +- .../(home)/components/NoAccessMessage.tsx | 4 +- apps/portal/src/app/(app)/layout.tsx | 10 +- .../app/api/device-agent/check-in/route.ts | 120 +++ .../device-agent/my-organizations/route.ts | 48 ++ .../app/api/device-agent/register/route.ts | 153 ++++ .../src/app/api/device-agent/status/route.ts | 55 ++ .../device-agent/updates/[filename]/route.ts | 134 +++ .../src/app/api/download-agent/archive.ts | 74 -- .../src/app/api/download-agent/constants.ts | 34 +- .../src/app/api/download-agent/fleet-label.ts | 127 --- .../src/app/api/download-agent/route.ts | 21 +- .../src/app/api/download-agent/scripts.ts | 4 - .../app/api/download-agent/scripts/common.ts | 46 -- .../app/api/download-agent/scripts/index.ts | 2 - .../src/app/api/download-agent/scripts/mac.ts | 120 --- .../src/app/api/download-agent/token/route.ts | 44 +- .../src/app/api/download-agent/types.ts | 27 +- .../src/app/api/download-agent/utils.ts | 7 + apps/portal/src/app/components/header.tsx | 2 +- apps/portal/src/app/components/user-menu.tsx | 6 - apps/portal/src/app/layout.tsx | 6 +- apps/portal/src/app/lib/auth.ts | 7 +- apps/portal/src/app/portal.css | 11 + apps/portal/src/app/providers.tsx | 15 +- apps/portal/src/utils/os.ts | 6 +- bun.lock | 765 +++++++++++++++-- .../migration.sql | 45 + packages/db/prisma/schema/auth.prisma | 1 + packages/db/prisma/schema/device.prisma | 37 + packages/db/prisma/schema/organization.prisma | 3 + packages/device-agent/.gitignore | 9 + packages/device-agent/BUILD.md | 172 ++++ packages/device-agent/SPEC.md | 780 ++++++++++++++++++ .../device-agent/assets/16x16-default.png | Bin 0 -> 697 bytes packages/device-agent/assets/16x16-fail.png | Bin 0 -> 736 bytes packages/device-agent/assets/16x16-pass.png | Bin 0 -> 777 bytes .../assets/entitlements.mac.plist | 16 + packages/device-agent/assets/icon.icns | Bin 0 -> 202145 bytes packages/device-agent/assets/icon.png | Bin 0 -> 35616 bytes .../assets/linux/after-install.sh | 8 + packages/device-agent/assets/logo.png | Bin 0 -> 20280 bytes .../device-agent/electron-builder.config.js | 88 ++ packages/device-agent/electron.vite.config.ts | 53 ++ packages/device-agent/package.json | 41 + packages/device-agent/src/checks/index.ts | 92 +++ .../src/checks/linux/antivirus.ts | 131 +++ .../src/checks/linux/disk-encryption.ts | 159 ++++ .../src/checks/linux/password-policy.ts | 130 +++ .../src/checks/linux/screen-lock.ts | 268 ++++++ .../src/checks/macos/antivirus.ts | 94 +++ .../src/checks/macos/disk-encryption.ts | 50 ++ .../src/checks/macos/password-policy.ts | 181 ++++ .../src/checks/macos/screen-lock.ts | 195 +++++ packages/device-agent/src/checks/types.ts | 16 + .../src/checks/windows/antivirus.ts | 101 +++ .../src/checks/windows/disk-encryption.ts | 181 ++++ .../src/checks/windows/password-policy.ts | 157 ++++ .../src/checks/windows/screen-lock.ts | 131 +++ packages/device-agent/src/main/auth.ts | 303 +++++++ packages/device-agent/src/main/auto-launch.ts | 36 + packages/device-agent/src/main/device-info.ts | 135 +++ packages/device-agent/src/main/index.ts | 371 +++++++++ packages/device-agent/src/main/logger.ts | 59 ++ packages/device-agent/src/main/reporter.ts | 84 ++ packages/device-agent/src/main/scheduler.ts | 109 +++ packages/device-agent/src/main/store.ts | 79 ++ packages/device-agent/src/main/tray.ts | 312 +++++++ packages/device-agent/src/preload/index.ts | 43 + .../device-agent/src/remediations/index.ts | 99 +++ .../src/remediations/instructions.ts | 162 ++++ .../src/remediations/linux/antivirus.ts | 33 + .../src/remediations/linux/disk-encryption.ts | 34 + .../src/remediations/linux/password-policy.ts | 65 ++ .../src/remediations/linux/screen-lock.ts | 75 ++ .../src/remediations/macos/antivirus.ts | 51 ++ .../src/remediations/macos/disk-encryption.ts | 51 ++ .../src/remediations/macos/password-policy.ts | 62 ++ .../src/remediations/macos/screen-lock.ts | 62 ++ .../device-agent/src/remediations/types.ts | 16 + .../src/remediations/windows/antivirus.ts | 66 ++ .../remediations/windows/disk-encryption.ts | 66 ++ .../remediations/windows/password-policy.ts | 77 ++ .../src/remediations/windows/screen-lock.ts | 65 ++ packages/device-agent/src/renderer/App.tsx | 445 ++++++++++ packages/device-agent/src/renderer/index.html | 13 + packages/device-agent/src/renderer/main.tsx | 13 + packages/device-agent/src/renderer/styles.css | 12 + packages/device-agent/src/shared/constants.ts | 27 + packages/device-agent/src/shared/types.ts | 129 +++ packages/device-agent/tsconfig.json | 21 + packages/docs/openapi.json | 23 +- packages/utils/package.json | 1 + packages/utils/src/devices.ts | 35 + turbo.json | 5 + 133 files changed, 9734 insertions(+), 1134 deletions(-) create mode 100644 .github/workflows/device-agent-release.yml create mode 100644 apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceAgentDevicesList.tsx create mode 100644 apps/app/src/app/(app)/[orgId]/people/devices/components/DeviceDetails.tsx create mode 100644 apps/portal/src/app/api/device-agent/check-in/route.ts create mode 100644 apps/portal/src/app/api/device-agent/my-organizations/route.ts create mode 100644 apps/portal/src/app/api/device-agent/register/route.ts create mode 100644 apps/portal/src/app/api/device-agent/status/route.ts create mode 100644 apps/portal/src/app/api/device-agent/updates/[filename]/route.ts delete mode 100644 apps/portal/src/app/api/download-agent/archive.ts delete mode 100644 apps/portal/src/app/api/download-agent/fleet-label.ts delete mode 100644 apps/portal/src/app/api/download-agent/scripts.ts delete mode 100644 apps/portal/src/app/api/download-agent/scripts/common.ts delete mode 100644 apps/portal/src/app/api/download-agent/scripts/index.ts delete mode 100644 apps/portal/src/app/api/download-agent/scripts/mac.ts create mode 100644 apps/portal/src/app/portal.css create mode 100644 packages/db/prisma/migrations/20260214210154_add_device_models/migration.sql create mode 100644 packages/db/prisma/schema/device.prisma create mode 100644 packages/device-agent/.gitignore create mode 100644 packages/device-agent/BUILD.md create mode 100644 packages/device-agent/SPEC.md create mode 100644 packages/device-agent/assets/16x16-default.png create mode 100644 packages/device-agent/assets/16x16-fail.png create mode 100644 packages/device-agent/assets/16x16-pass.png create mode 100644 packages/device-agent/assets/entitlements.mac.plist create mode 100644 packages/device-agent/assets/icon.icns create mode 100644 packages/device-agent/assets/icon.png create mode 100644 packages/device-agent/assets/linux/after-install.sh create mode 100644 packages/device-agent/assets/logo.png create mode 100644 packages/device-agent/electron-builder.config.js create mode 100644 packages/device-agent/electron.vite.config.ts create mode 100644 packages/device-agent/package.json create mode 100644 packages/device-agent/src/checks/index.ts create mode 100644 packages/device-agent/src/checks/linux/antivirus.ts create mode 100644 packages/device-agent/src/checks/linux/disk-encryption.ts create mode 100644 packages/device-agent/src/checks/linux/password-policy.ts create mode 100644 packages/device-agent/src/checks/linux/screen-lock.ts create mode 100644 packages/device-agent/src/checks/macos/antivirus.ts create mode 100644 packages/device-agent/src/checks/macos/disk-encryption.ts create mode 100644 packages/device-agent/src/checks/macos/password-policy.ts create mode 100644 packages/device-agent/src/checks/macos/screen-lock.ts create mode 100644 packages/device-agent/src/checks/types.ts create mode 100644 packages/device-agent/src/checks/windows/antivirus.ts create mode 100644 packages/device-agent/src/checks/windows/disk-encryption.ts create mode 100644 packages/device-agent/src/checks/windows/password-policy.ts create mode 100644 packages/device-agent/src/checks/windows/screen-lock.ts create mode 100644 packages/device-agent/src/main/auth.ts create mode 100644 packages/device-agent/src/main/auto-launch.ts create mode 100644 packages/device-agent/src/main/device-info.ts create mode 100644 packages/device-agent/src/main/index.ts create mode 100644 packages/device-agent/src/main/logger.ts create mode 100644 packages/device-agent/src/main/reporter.ts create mode 100644 packages/device-agent/src/main/scheduler.ts create mode 100644 packages/device-agent/src/main/store.ts create mode 100644 packages/device-agent/src/main/tray.ts create mode 100644 packages/device-agent/src/preload/index.ts create mode 100644 packages/device-agent/src/remediations/index.ts create mode 100644 packages/device-agent/src/remediations/instructions.ts create mode 100644 packages/device-agent/src/remediations/linux/antivirus.ts create mode 100644 packages/device-agent/src/remediations/linux/disk-encryption.ts create mode 100644 packages/device-agent/src/remediations/linux/password-policy.ts create mode 100644 packages/device-agent/src/remediations/linux/screen-lock.ts create mode 100644 packages/device-agent/src/remediations/macos/antivirus.ts create mode 100644 packages/device-agent/src/remediations/macos/disk-encryption.ts create mode 100644 packages/device-agent/src/remediations/macos/password-policy.ts create mode 100644 packages/device-agent/src/remediations/macos/screen-lock.ts create mode 100644 packages/device-agent/src/remediations/types.ts create mode 100644 packages/device-agent/src/remediations/windows/antivirus.ts create mode 100644 packages/device-agent/src/remediations/windows/disk-encryption.ts create mode 100644 packages/device-agent/src/remediations/windows/password-policy.ts create mode 100644 packages/device-agent/src/remediations/windows/screen-lock.ts create mode 100644 packages/device-agent/src/renderer/App.tsx create mode 100644 packages/device-agent/src/renderer/index.html create mode 100644 packages/device-agent/src/renderer/main.tsx create mode 100644 packages/device-agent/src/renderer/styles.css create mode 100644 packages/device-agent/src/shared/constants.ts create mode 100644 packages/device-agent/src/shared/types.ts create mode 100644 packages/device-agent/tsconfig.json create mode 100644 packages/utils/src/devices.ts diff --git a/.github/workflows/device-agent-release.yml b/.github/workflows/device-agent-release.yml new file mode 100644 index 0000000000..dcb486f447 --- /dev/null +++ b/.github/workflows/device-agent-release.yml @@ -0,0 +1,453 @@ +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 (ignore -staging suffixes) + LATEST_TAG=$(git tag -l 'device-agent-v*' --sort=-v:refname | grep -v '\-staging' | 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 SSL.com eSigner CodeSignTool + uses: sslcom/esigner-codesign@develop + with: + command: get_credential_ids + username: ${{ secrets.ESIGNER_USERNAME }} + password: ${{ secrets.ESIGNER_PASSWORD }} + totp_secret: ${{ secrets.ESIGNER_TOTP_SECRET }} + + - name: Sign Windows EXE with SSL.com eSigner + uses: sslcom/esigner-codesign@develop + with: + command: sign + username: ${{ secrets.ESIGNER_USERNAME }} + password: ${{ secrets.ESIGNER_PASSWORD }} + credential_id: ${{ secrets.ESIGNER_CREDENTIAL_ID }} + totp_secret: ${{ secrets.ESIGNER_TOTP_SECRET }} + file_path: ${{ github.workspace }}/packages/device-agent/release + override: true + + - 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: ${{ secrets.APP_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.APP_AWS_SECRET_ACCESS_KEY }} + AWS_REGION: ${{ secrets.APP_AWS_REGION }} + S3_BUCKET: ${{ secrets.FLEET_AGENT_BUCKET_NAME }} + 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 + aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-x64.deb \ + s3://${S3_BUCKET}/${PREFIX}/linux/CompAI-Device-Agent-${VERSION}-x64.deb + aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-x64.deb \ + s3://${S3_BUCKET}/${PREFIX}/linux/latest-x64.deb + + aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-x64.AppImage \ + s3://${S3_BUCKET}/${PREFIX}/linux/CompAI-Device-Agent-${VERSION}-x64.AppImage + aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-x64.AppImage \ + s3://${S3_BUCKET}/${PREFIX}/linux/latest-x64.AppImage + + - name: Upload auto-update files to S3 + env: + AWS_ACCESS_KEY_ID: ${{ secrets.APP_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.APP_AWS_SECRET_ACCESS_KEY }} + AWS_REGION: ${{ secrets.APP_AWS_REGION }} + S3_BUCKET: ${{ secrets.FLEET_AGENT_BUCKET_NAME }} + 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..f7ee429c69 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-x64.deb`, + filename: 'CompAI-Device-Agent-x64.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 0000000000000000000000000000000000000000..38779d4ca883f56ddd3326f774307c9f428d57e0 GIT binary patch literal 697 zcmV;q0!ICbP)J97)TXG}nE{msbSnL=MnK@|mTVpY_$VRROiR zylgZR;f&BF@`5+~|A4cR5rL#XNA9ezPxG@k5HK6&PgP9NA$bNy{tEyA|NkFly4C;y f00v1!K~w_(0!w|sVNnFn00000NkvXXu0mjfpocnI literal 0 HcmV?d00001 diff --git a/packages/device-agent/assets/16x16-fail.png b/packages/device-agent/assets/16x16-fail.png new file mode 100644 index 0000000000000000000000000000000000000000..e2bf74cebdb50196698f32678b4ba7884a4ee2d1 GIT binary patch literal 736 zcmV<60w4W}P)4x2K!SCVf z>pi;wVmMpCM?0+WY zT@bx@SN~Q8ATLLb0MYHA-!oB-eEawLrk=)@h(!Skk%t5@g7{g9%)Ygn9IV!vSW|#I zR49SfBNsz#%tVHuu9L@L$yLKI?MQ$GAns6-kt451ju5dNhM-mFx=cXw~1Lbv^2q{07XQ!6a#OGKwdsR7z{Zq`<5*VknDQg6A*r^7C{j5oLe*U zHF9~Y0wgdXEfKDyHFD%jBZpU7mjDGV5#rv4HAX1%_*-kY06;`|7dqtE=1fizgOw0eMJ0Maj`A5saWy$bB)$UEeGKX#KLqwu}h2 zl~~;inv`Yi*1GGgrzkG?~aU5p`{EfzzEa@D{Vwmlg^?3hm$PGEJ zC-D33&1`uxAr{y2be6<80l;Z@ zgff6%-=7WPY-x!E9ZKrKeoP>>yab=8hX5S2L*n&gvdR7%iw-dtP8!3$^a%i7wj2O} z{=NsGI`6-a2dlerKw_T&fq+|D(~CJk(&G-vcx-Qa5^}XKtxdq+P=P6^ZaH>?a72NhFvHDnzY?yUu&p?&!18Mg|CNM3@UIq|)RMA;7sywvk=W7dqer zq(K@9%0B7_BoGLl0jFSro1QNaAnC}X(IN5UJXP$xuZdxk=DFz9casClem%%7< zZwYV#Gv)%O=-(l! + + + + 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 0000000000000000000000000000000000000000..88fa6f9962346e100274f739fcec00e8463ab8e3 GIT binary patch literal 202145 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 literal 0 HcmV?d00001 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..79dca4d647 --- /dev/null +++ b/packages/device-agent/package.json @@ -0,0 +1,41 @@ +{ + "name": "@comp/device-agent", + "version": "1.0.1", + "description": "Comp AI Device Compliance Agent - Device Compliance Checks", + "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 }, From b37f2252e9bf220600b2eb229364c4b6964b7cf0 Mon Sep 17 00:00:00 2001 From: Tofik Hasanov <72318342+tofikwest@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:55:23 -0500 Subject: [PATCH 2/8] fix(ci): handle pre-release tags in device agent version detection (#2137) The version detection grep was only filtering out '-staging' suffixes, but failed on tags like 'device-agent-v0.1.0-beta.1'. The arithmetic parser couldn't handle '0-beta.1' as a patch number, causing the version to be empty and electron-builder to fail with "Please specify 'version' in the package.json". Now uses a strict regex to only match clean semver tags (e.g., device-agent-v1.0.0), which correctly falls back to 1.0.0 when no clean tags exist. Co-authored-by: Claude Opus 4.6 --- .github/workflows/device-agent-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/device-agent-release.yml b/.github/workflows/device-agent-release.yml index dcb486f447..3ccaafd55c 100644 --- a/.github/workflows/device-agent-release.yml +++ b/.github/workflows/device-agent-release.yml @@ -31,8 +31,8 @@ jobs: - name: Compute next version id: version run: | - # Get the latest production tag (ignore -staging suffixes) - LATEST_TAG=$(git tag -l 'device-agent-v*' --sort=-v:refname | grep -v '\-staging' | head -1) + # 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 From e82bf175e6211770443035cd6a301cd94f2a9dee Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:06:06 -0500 Subject: [PATCH 3/8] chore(device-agent): align package.json version to 1.0.0 baseline (#2140) CI overrides this at build time, but keeping it in sync with what the version detection computes (1.0.0 since no clean semver tags exist). Co-authored-by: Tofik Hasanov Co-authored-by: Claude Opus 4.6 --- packages/device-agent/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/device-agent/package.json b/packages/device-agent/package.json index 79dca4d647..cf37501f6d 100644 --- a/packages/device-agent/package.json +++ b/packages/device-agent/package.json @@ -1,6 +1,6 @@ { "name": "@comp/device-agent", - "version": "1.0.1", + "version": "1.0.0", "description": "Comp AI Device Compliance Agent - Device Compliance Checks", "author": "Comp AI ", "homepage": "https://trycomp.ai", From 5f35e3500bbc6670464d83fb3c9eb7c5c3f4ec29 Mon Sep 17 00:00:00 2001 From: Tofik Hasanov <72318342+tofikwest@users.noreply.github.com> Date: Mon, 16 Feb 2026 18:31:10 -0500 Subject: [PATCH 4/8] fix(ci): pin Windows code signing to stable sslcom/esigner-codesign@v1.3.2 (#2141) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(device-agent): align package.json version to 1.0.0 baseline CI overrides this at build time, but keeping it in sync with what the version detection computes (1.0.0 since no clean semver tags exist). Co-Authored-By: Claude Opus 4.6 * fix(ci): use stable sslcom/esigner-codesign@v1.3.2 instead of @develop The @develop branch broke Windows code signing — the action stopped passing -username and -password flags to CodeSignTool. Pinning to the latest stable tag v1.3.2. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- .github/workflows/device-agent-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/device-agent-release.yml b/.github/workflows/device-agent-release.yml index 3ccaafd55c..752ca0e12d 100644 --- a/.github/workflows/device-agent-release.yml +++ b/.github/workflows/device-agent-release.yml @@ -180,7 +180,7 @@ jobs: run: bun run package:win - name: Setup SSL.com eSigner CodeSignTool - uses: sslcom/esigner-codesign@develop + uses: sslcom/esigner-codesign@v1.3.2 with: command: get_credential_ids username: ${{ secrets.ESIGNER_USERNAME }} @@ -188,7 +188,7 @@ jobs: totp_secret: ${{ secrets.ESIGNER_TOTP_SECRET }} - name: Sign Windows EXE with SSL.com eSigner - uses: sslcom/esigner-codesign@develop + uses: sslcom/esigner-codesign@v1.3.2 with: command: sign username: ${{ secrets.ESIGNER_USERNAME }} From 9b1b6dd94e523e51179bf458243f756432dcc81f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 18:37:45 -0500 Subject: [PATCH 5/8] chore(device-agent): update package description (#2142) Co-authored-by: Tofik Hasanov Co-authored-by: Claude Opus 4.6 --- packages/device-agent/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/device-agent/package.json b/packages/device-agent/package.json index cf37501f6d..d147f8f2cb 100644 --- a/packages/device-agent/package.json +++ b/packages/device-agent/package.json @@ -1,7 +1,7 @@ { "name": "@comp/device-agent", "version": "1.0.0", - "description": "Comp AI Device Compliance Agent - Device Compliance Checks", + "description": "Comp AI Device Agent - Device Compliance Checks", "author": "Comp AI ", "homepage": "https://trycomp.ai", "private": true, From 884e0d23dfce128b0359b3f5ad66d88aaba0c866 Mon Sep 17 00:00:00 2001 From: Tofik Hasanov <72318342+tofikwest@users.noreply.github.com> Date: Tue, 17 Feb 2026 00:18:08 -0500 Subject: [PATCH 6/8] fix(ci): replace broken sslcom/esigner-codesign action with direct CodeSignTool invocation (#2143) The sslcom/esigner-codesign GitHub Action (both @develop and @v1.3.2) has a bug where it fails to pass -username and -password flags to the CodeSignTool CLI. Replaced with direct download and invocation of CodeSignTool v1.3.0 via PowerShell. Co-authored-by: Claude Opus 4.6 --- .github/workflows/device-agent-release.yml | 63 ++++++++++++++-------- packages/device-agent/package.json | 2 +- 2 files changed, 42 insertions(+), 23 deletions(-) diff --git a/.github/workflows/device-agent-release.yml b/.github/workflows/device-agent-release.yml index 752ca0e12d..be4f0403a8 100644 --- a/.github/workflows/device-agent-release.yml +++ b/.github/workflows/device-agent-release.yml @@ -179,24 +179,43 @@ jobs: AUTO_UPDATE_URL: ${{ needs.detect-version.outputs.auto_update_url }} run: bun run package:win - - name: Setup SSL.com eSigner CodeSignTool - uses: sslcom/esigner-codesign@v1.3.2 + - name: Setup Java for CodeSignTool + uses: actions/setup-java@v4 with: - command: get_credential_ids - username: ${{ secrets.ESIGNER_USERNAME }} - password: ${{ secrets.ESIGNER_PASSWORD }} - totp_secret: ${{ secrets.ESIGNER_TOTP_SECRET }} + distribution: 'corretto' + java-version: '11' - - name: Sign Windows EXE with SSL.com eSigner - uses: sslcom/esigner-codesign@v1.3.2 - with: - command: sign - username: ${{ secrets.ESIGNER_USERNAME }} - password: ${{ secrets.ESIGNER_PASSWORD }} - credential_id: ${{ secrets.ESIGNER_CREDENTIAL_ID }} - totp_secret: ${{ secrets.ESIGNER_TOTP_SECRET }} - file_path: ${{ github.workspace }}/packages/device-agent/release - override: true + - 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 @@ -377,10 +396,10 @@ jobs: - name: Upload installers to S3 env: - AWS_ACCESS_KEY_ID: ${{ secrets.APP_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.APP_AWS_SECRET_ACCESS_KEY }} + 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: ${{ secrets.FLEET_AGENT_BUCKET_NAME }} + 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: | @@ -416,10 +435,10 @@ jobs: - name: Upload auto-update files to S3 env: - AWS_ACCESS_KEY_ID: ${{ secrets.APP_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.APP_AWS_SECRET_ACCESS_KEY }} + 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: ${{ secrets.FLEET_AGENT_BUCKET_NAME }} + 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" diff --git a/packages/device-agent/package.json b/packages/device-agent/package.json index d147f8f2cb..e8c25010a7 100644 --- a/packages/device-agent/package.json +++ b/packages/device-agent/package.json @@ -1,7 +1,7 @@ { "name": "@comp/device-agent", "version": "1.0.0", - "description": "Comp AI Device Agent - Device Compliance Checks", + "description": "Comp AI Device Agent - Endpoint Compliance", "author": "Comp AI ", "homepage": "https://trycomp.ai", "private": true, From cbcf420217c268fdfb35d9c03631a56d8822a028 Mon Sep 17 00:00:00 2001 From: Tofik Hasanov <72318342+tofikwest@users.noreply.github.com> Date: Tue, 17 Feb 2026 00:42:35 -0500 Subject: [PATCH 7/8] fix(ci): fix Linux artifact names and consolidate all CI fixes (#2144) - Replace broken sslcom/esigner-codesign action with direct CodeSignTool jar invocation via Java (the action fails to pass credentials) - Use staging AWS credentials for non-release branches, production for release branch - Fix Linux artifact filenames: .deb uses amd64, .AppImage uses x86_64 (not x64 as previously assumed) - Update portal download constants to match actual .deb filename Co-authored-by: Claude Opus 4.6 --- .github/workflows/device-agent-release.yml | 20 +++++++++---------- .../src/app/api/download-agent/constants.ts | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/device-agent-release.yml b/.github/workflows/device-agent-release.yml index be4f0403a8..821e34b7e2 100644 --- a/.github/workflows/device-agent-release.yml +++ b/.github/workflows/device-agent-release.yml @@ -422,16 +422,16 @@ jobs: aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-setup.exe \ s3://${S3_BUCKET}/${PREFIX}/windows/latest-setup.exe - # Linux - aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-x64.deb \ - s3://${S3_BUCKET}/${PREFIX}/linux/CompAI-Device-Agent-${VERSION}-x64.deb - aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-x64.deb \ - s3://${S3_BUCKET}/${PREFIX}/linux/latest-x64.deb - - aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-x64.AppImage \ - s3://${S3_BUCKET}/${PREFIX}/linux/CompAI-Device-Agent-${VERSION}-x64.AppImage - aws s3 cp artifacts/CompAI-Device-Agent-${VERSION}-x64.AppImage \ - s3://${S3_BUCKET}/${PREFIX}/linux/latest-x64.AppImage + # 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: diff --git a/apps/portal/src/app/api/download-agent/constants.ts b/apps/portal/src/app/api/download-agent/constants.ts index f7ee429c69..f35394e9ff 100644 --- a/apps/portal/src/app/api/download-agent/constants.ts +++ b/apps/portal/src/app/api/download-agent/constants.ts @@ -24,8 +24,8 @@ export const DOWNLOAD_TARGETS: Record< contentType: 'application/octet-stream', }, linux: { - key: `${S3_PREFIX}/linux/latest-x64.deb`, - filename: 'CompAI-Device-Agent-x64.deb', + key: `${S3_PREFIX}/linux/latest-amd64.deb`, + filename: 'CompAI-Device-Agent-amd64.deb', contentType: 'application/vnd.debian.binary-package', }, }; From c6ac355744a9791d3fb74d8768bcdc05ee36daba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 02:48:52 -0500 Subject: [PATCH 8/8] chore(device-agent): trigger CI with all fixes on main (#2145) Co-authored-by: Tofik Hasanov Co-authored-by: Claude Opus 4.6 --- packages/device-agent/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/device-agent/package.json b/packages/device-agent/package.json index e8c25010a7..d0fc91e62d 100644 --- a/packages/device-agent/package.json +++ b/packages/device-agent/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "description": "Comp AI Device Agent - Endpoint Compliance", "author": "Comp AI ", - "homepage": "https://trycomp.ai", + "homepage": "https://trycomp.ai/", "private": true, "main": "dist/main/index.js", "scripts": {