Repository: bebiksior/EvenBetter Branch: main Commit: 136b320acb5d Files: 67 Total size: 114.5 KB Directory structure: gitextract_jrkrjd9f/ ├── .cursor/ │ └── rules/ │ ├── caido-backend.mdc │ ├── caido-frontend.mdc │ ├── caido.mdc │ ├── linter.mdc │ ├── style.mdc │ └── typescript.mdc ├── .github/ │ └── workflows/ │ ├── release.yml │ └── validate.yml ├── .gitignore ├── LICENSE ├── README.md ├── caido.config.ts ├── eslint.config.mjs ├── package.json ├── packages/ │ ├── backend/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── api/ │ │ │ │ ├── flags.ts │ │ │ │ └── settings.ts │ │ │ ├── features/ │ │ │ │ ├── backend-test/ │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ └── manager.ts │ │ │ ├── index.ts │ │ │ ├── stores/ │ │ │ │ ├── flags.ts │ │ │ │ └── settings.ts │ │ │ ├── types.ts │ │ │ └── utils/ │ │ │ └── files.ts │ │ └── tsconfig.json │ ├── frontend/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── FlagsList/ │ │ │ │ │ ├── Container.vue │ │ │ │ │ ├── index.ts │ │ │ │ │ └── useForm.ts │ │ │ │ └── Settings/ │ │ │ │ ├── Container.vue │ │ │ │ ├── index.ts │ │ │ │ └── useForm.ts │ │ │ ├── dom/ │ │ │ │ └── index.ts │ │ │ ├── features/ │ │ │ │ ├── clear-all-findings/ │ │ │ │ │ └── index.ts │ │ │ │ ├── colorize-by-method/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── style.css │ │ │ │ ├── command-palette-workflows/ │ │ │ │ │ └── index.ts │ │ │ │ ├── common-filters/ │ │ │ │ │ └── index.ts │ │ │ │ ├── exclude-host-path/ │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── manager.ts │ │ │ │ ├── quick-decode/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── quick-decode.css │ │ │ │ ├── quick-mar/ │ │ │ │ │ └── index.ts │ │ │ │ ├── share-filters/ │ │ │ │ │ └── index.ts │ │ │ │ ├── share-replay-collections/ │ │ │ │ │ └── index.ts │ │ │ │ └── share-scope/ │ │ │ │ └── index.ts │ │ │ ├── fonts/ │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── plugins/ │ │ │ │ └── sdk.ts │ │ │ ├── queries/ │ │ │ │ ├── flags.ts │ │ │ │ └── settings.ts │ │ │ ├── styles/ │ │ │ │ └── index.css │ │ │ ├── types.ts │ │ │ ├── utils/ │ │ │ │ └── file-utils.ts │ │ │ └── views/ │ │ │ └── App.vue │ │ └── tsconfig.json │ └── shared/ │ ├── package.json │ ├── src/ │ │ ├── flags.ts │ │ ├── fonts.ts │ │ ├── index.ts │ │ ├── result.ts │ │ └── settings.ts │ └── tsconfig.json ├── pnpm-workspace.yaml └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cursor/rules/caido-backend.mdc ================================================ --- globs: - "**/packages/backend/**" alwaysApply: true description: Caido Backend SDK Rules and Patterns --- ## Caido Backend SDK ### Overview The Caido Backend SDK is used for server-side logic, data processing, and creating API endpoints that can be called from frontend plugins. ### Entry Point Backend plugins are initialized via `packages/backend/src/index.ts`: ```typescript import { SDK, DefineAPI } from "caido:plugin"; // Define your API functions function myCustomFunction(sdk: SDK, param: string) { sdk.console.log(`Called with: ${param}`); return `Processed: ${param}`; } // Export the API type definition export type API = DefineAPI<{ myCustomFunction: typeof myCustomFunction; }>; // Plugin initialization export function init(sdk: SDK) { // Register API endpoints sdk.api.register("myCustomFunction", myCustomFunction); } ``` ### SDK Type Definitions #### Backend SDK with events: ```typescript import { DefineEvents, SDK } from "caido:plugin"; export type BackendEvents = DefineEvents<{ "data-updated": { message: string }; "status-changed": { status: "active" | "inactive" }; }>; export type CaidoBackendSDK = SDK; ``` ### Best Practices When building API endpoints in the backend and calling them from the frontend, use Result types to handle errors gracefully without throwing exceptions: ```typescript // Define the Result type export type Result = | { kind: "Error"; error: string } | { kind: "Ok"; value: T }; // Backend API function returning Result function processData(sdk: SDK, input: string): Result { try { // Your processing logic here const processed = doSomeProcessing(input); return { kind: "Ok", value: processed }; } catch (error) { return { kind: "Error", error: error.message }; } } // Frontend usage - no try/catch needed const handleProcess = async () => { const result = await sdk.backend.processData(inputValue); if (result.kind === "Error") { sdk.window.showToast(result.error, { variant: "error" }); return; } // Handle successful result const data = result.value; sdk.window.showToast("Processing completed!", { variant: "success" }); }; ``` #### Registering Multiple API Endpoints ```typescript // Define multiple API functions function getData(sdk: SDK, id: string): Result { // Implementation } function saveData(sdk: SDK, data: Data): Result { // Implementation } function deleteData(sdk: SDK, id: string): Result { // Implementation } // Export Caido Backend API export type API = DefineAPI<{ getData: typeof getData; saveData: typeof saveData; deleteData: typeof deleteData; }>; // Register all endpoints export function init(sdk: SDK) { sdk.api.register("getData", getData); sdk.api.register("saveData", saveData); sdk.api.register("deleteData", deleteData); } ``` ================================================ FILE: .cursor/rules/caido-frontend.mdc ================================================ --- globs: - "**/packages/frontend/**" alwaysApply: true description: Caido Frontend SDK Rules and Patterns --- ## Caido Frontend SDK ### Overview The Caido Frontend SDK is used for creating UI components, pages, and handling user interactions in Caido plugins. ### Entry Point Frontend plugins are initialized via `packages/frontend/src/index.ts`: ```typescript import { Caido } from "@caido/sdk-frontend"; import { API, BackendEvents } from "backend"; // Define SDK type with backend API export type FrontendSDK = Caido; // Plugin initialization export const init = (sdk: FrontendSDK) => { // Create pages and UI createPage(sdk); // Register sidebar items sdk.sidebar.registerItem("My Plugin", "/my-plugin-page", { icon: "fas fa-rocket" }); // Register commands sdk.commands.register("my-command", { name: "My Custom Command", run: () => sdk.backend.myCustomFunction("Hello"), }); }; ``` ### SDK Type Definitions #### For plugins WITHOUT backend, this is fine: ```typescript export type FrontendSDK = Caido, Record>; ``` #### For plugins WITH backend: ```typescript import { Caido } from "@caido/sdk-frontend"; import { API, BackendEvents } from "backend"; export type FrontendSDK = Caido; ``` ### Command Pattern Commands provide a unified way to register actions that can be triggered from: - Command palette (Ctrl/Cmd+Shift+P) - Context menus (right-click) - UI buttons - Keyboard shortcuts Commands is a frontend-only concept. ```typescript // Define command IDs as constants const Commands = { processData: "my-plugin.process-data", exportResults: "my-plugin.export-results", } as const; // Register commands sdk.commands.register(Commands.processData, { name: "Process Data", run: async () => { const result = await sdk.backend.processData(); sdk.window.showToast(`Processed ${result.count} items`, { variant: "success" }); }, group: "My Plugin", }); // Add to command palette sdk.commandPalette.register(Commands.processData); // Add to context menus sdk.menu.registerItem({ type: "Request", commandId: Commands.processData, leadingIcon: "fas fa-cog", }); ``` ### Working with Requests and Responses #### Creating and Sending Requests ```typescript import { RequestSpec } from "caido:utils"; import { type Request, type Response } from "caido:utils"; // Create a new request const spec = new RequestSpec("https://api.example.com/data"); spec.setMethod("POST"); spec.setHeader("Content-Type", "application/json"); spec.setBody(JSON.stringify({ key: "value" })); // Send the request const result = await sdk.requests.send(spec); if (result.response) { const statusCode = result.response.getCode(); const responseBody = result.response.getBody()?.toText(); } ``` #### Working with Request/Response Editors ```typescript // Create editors for viewing/editing HTTP data const reqEditor = sdk.ui.httpRequestEditor(); const respEditor = sdk.ui.httpResponseEditor(); // Get DOM elements const reqElement = reqEditor.getElement(); const respElement = respEditor.getElement(); // Style and layout reqElement.style.width = "50%"; respElement.style.width = "50%"; const editorsContainer = document.createElement("div"); editorsContainer.style.display = "flex"; editorsContainer.appendChild(reqElement); editorsContainer.appendChild(respElement); ``` ### Frontend Error Handling When calling backend APIs from the frontend, handle Result types gracefully: ```typescript // Frontend usage - no try/catch needed const handleProcess = async () => { const result = await sdk.backend.processData(inputValue); if (result.kind === "Error") { sdk.window.showToast(result.error, { variant: "error" }); return; } // Handle successful result const data = result.value; sdk.window.showToast("Processing completed!", { variant: "success" }); }; ``` ================================================ FILE: .cursor/rules/caido.mdc ================================================ --- globs: alwaysApply: true description: Caido HTTP Proxy Overview --- ## What is Caido Caido is a lightweight web application security auditing toolkit designed to help security professionals audit web applications with efficiency and ease Key features include: - HTTP proxy for intercepting and viewing requests in real-time - Replay functionality for resending and modifying requests to test endpoints - Automate feature for testing requests against wordlists - Match & Replace for automatically modifying requests with regex rules - HTTPQL query language for filtering through HTTP traffic - Workflow system for creating custom encoders/decoders and plugins - Project management for organizing different security assessments ## Environment We are running in a plugin environment where we can interact with Caido through the Caido Backend or Frontend SDK. ### Plugin Structure - `packages/backend` → Backend plugin code - handles server-side logic, data processing, and API endpoints - `packages/frontend` → Frontend plugin code - handles UI components, user interactions, and calls to backend ### Plugin Development Plugins consist of: - A `caido.config.ts` configuration file - Frontend plugin (optional) - provides UI using Caido Frontend SDK - Backend plugin (optional) - provides server-side functionality using Caido Backend SDK These are packaged together as a single plugin package that can be installed in Caido. ### Key Development Concepts - Frontend plugins create pages, UI components, and handle user interactions - Backend plugins register API endpoints that can be called from frontend - Communication between frontend and backend happens through registered API calls ### Caido Findings SDK Findings allow you to create alerts when Caido detects notable characteristics in requests/responses based on conditional statements. When triggered, they generate alerts to draw attention to interesting traffic. Example - Create a finding for successful responses: ```typescript await sdk.findings.create({ title: `Success Response ${response.getCode()}`, description: `Request ID: ${request.getId()}\nResponse Code: ${response.getCode()}`, reporter: "Response Logger Plugin", request: request, dedupeKey: `${request.getPath()}-${response.getCode()}` // Prevents duplicates }); ``` ### Important Caido SDK Types ```typescript export type Request = { getId(): ID; getHost(): string; getPort(): number; getTls(): boolean; getMethod(): string; getPath(): string; getQuery(): string; getUrl(): string; getHeaders(): Record>; getHeader(name: string): Array | undefined; getBody(): Body | undefined; getRaw(): RequestRaw; getCreatedAt(): Date; toSpec(): RequestSpec; toSpecRaw(): RequestSpecRaw; }; export type Response = { getId(): ID; getCode(): number; getHeaders(): Record>; getHeader(name: string): Array | undefined; getBody(): Body | undefined; getRaw(): ResponseRaw; getRoundtripTime(): number; getCreatedAt(): Date; }; ``` For Body and Raw you can use methods like `getBody()?.toText()` to extract text content. These types can be imported by: ``` import { type Request, type Response } from "caido:utils"; ``` ================================================ FILE: .cursor/rules/linter.mdc ================================================ --- globs: alwaysApply: true description: Linter Guidelines --- # Linter We have a built-in ESLint linter configured at the root folder. After making any significant change, always run the linter with `pnpm lint` and fix all potential issues. ## Most common mistakes that lead to linter errors ### Lint Rule: Unexpected nullable string value in conditional. Please handle nullish or empty cases explicitly To prevent this, when comparing strings, instead of writing: ``` if (!str) {} ``` do this: ``` if (str !== undefined) {} ``` ================================================ FILE: .cursor/rules/style.mdc ================================================ --- globs: **/**.vue alwaysApply: false --- ## UI Style Guidelines ### PrimeVue - Prefer to use PrimeVue compontents where possible - A custom PrimeVue theme is configured with dark mode as default, handling most color-related styles for us. ### General Theme - Dark Mode is default — all UI elements follow a dark, low-contrast background with light text for high readability. - For text / background colors, prefer to use `...-surface-...` f.e. `border-surface-700`. - Caido uses `bg-surface-800` as the main app background, `bg-surface-700` is the background used for `Card` component. - Follow minimalistic color palette. AVOID using too much colors where possible. ### Layout & Components - Often use PrimeVue `Splitter` and `SplitterPanel` for vertical or horizontal layout. - Prefer to use `Card` PrimeVue components a lot, if needed add `h-full` to them via `pt` params. Example: ``` ``` ### Enviroment - Keep in mind that we are building a plugin that's inside a Caido web app, we can modify frontend by adding sidebar pages using Caido Frontend SDK. - Our plugin content is rendered within a dedicated window/panel that Caido provides for our sidebar page. - The plugin UI should integrate seamlessly with Caido's existing interface and theming. ### Data Representation - Prefer `DataTable` component for displaying structured data: * Caido often uses `stripedRows`, use it where possible * Actions column at the end (e.g. “Install” buttons). - Empty states use friendly, minimal messages with icons. ### Icons - Always use `fas fa-[...]` for icons. We don't support any other icon libraries. ================================================ FILE: .cursor/rules/typescript.mdc ================================================ --- globs: alwaysApply: true description: TypeScript Guidelines --- # TypeScript Guidelines - Use TypeScript for all files. - NEVER use `any` type. - Use `undefined` over `null`. - Try to keep things in one function unless composable or reusable. - Prefer single word variable names where possible. - DO NOT do unnecessary destructuring of variables. - AVOID `else` statements where possible. - AVOID `try` / `catch` where possible. - AVOID using interfaces where possible. ================================================ FILE: .github/workflows/release.yml ================================================ name: 🚀 Release on: workflow_dispatch: env: NODE_VERSION: 20 PNPM_VERSION: 9 jobs: release: name: Release runs-on: ubuntu-latest permissions: contents: write steps: - name: Checkout project uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: ${{ env.PNPM_VERSION }} run_install: true - name: Build package run: pnpm build - name: Sign package working-directory: dist run: | if [[ -z "${{ secrets.PRIVATE_KEY }}" ]]; then echo "Set an ed25519 key as PRIVATE_KEY in GitHub Action secret to sign." else echo "${{ secrets.PRIVATE_KEY }}" > private_key.pem openssl pkeyutl -sign -inkey private_key.pem -out plugin_package.zip.sig -rawin -in plugin_package.zip rm private_key.pem fi - name: Check version id: meta working-directory: dist run: | VERSION=$(unzip -p plugin_package.zip manifest.json | jq -r .version) echo "version=${VERSION}" >> $GITHUB_OUTPUT - name: Create release uses: caido/action-release@v1 with: tag: ${{ steps.meta.outputs.version }} commit: ${{ github.sha }} body: 'Release ${{ steps.meta.outputs.version }}' artifacts: 'dist/plugin_package.zip,dist/plugin_package.zip.sig' immutableCreate: true ================================================ FILE: .github/workflows/validate.yml ================================================ name: Validate on: push: branches: - 'main' workflow_call: concurrency: group: validate-${{ github.ref_name }} cancel-in-progress: true env: CAIDO_NODE_VERSION: 20 CAIDO_PNPM_VERSION: 9 jobs: typecheck: name: 'Typecheck' runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout repository uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.CAIDO_NODE_VERSION }} - name: Setup pnpm uses: pnpm/action-setup@v3.0.0 with: version: ${{ env.CAIDO_PNPM_VERSION }} - name: Install dependencies run: pnpm install - name: Run typechecker run: pnpm typecheck lint: name: 'Lint' runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.CAIDO_NODE_VERSION }} - name: Setup pnpm uses: pnpm/action-setup@v4.1.0 with: version: ${{ env.CAIDO_PNPM_VERSION }} - name: Install dependencies run: pnpm install - name: Run linter run: pnpm lint ================================================ FILE: .gitignore ================================================ node_modules dist .DS_Store ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2024 [Author] Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # EvenBetter [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=Follow%20me)](https://twitter.com/bebiksior) [![Static Badge](https://img.shields.io/badge/TODO%20List-00000?style=flat&color=%233251ed)](https://github.com/users/bebiksior/projects/2) `EvenBetter` is a frontend Caido plugin that makes the [Caido](https://github.com/caido) experience even better 😎 Here's what **EvenBetter** implements: - **Quick Decode**: quickly decode and edit encoded values within the request body on the Replay page - **Font picker**: feature in the EvenBetter settings that allows you to change the font of the Caido UI. - **EvenBetter Settings**: customize and toggle EvenBetter features - **Scope Share**: export/import scope presets - **Send to Match & Replace**: custom right-click menu button that sends selected text to the Match & Replace page - ... more small tweaks that improve overall [Caido](https://github.com/caido) experience ## Sponsors Maintenance of EvenBetter is possible thanks to the following sponsors: User avatar: CRIT Software ## Installation [Recommended] 1. Open Caido, navigate to the `Plugins` sidebar page and then to the `Community Store` tab 2. Find `EvenBetter` and click `Install` 3. Done! 🎉 ## Installation [without auto-updates] 1. Go to the [EvenBetter Releases tab](https://github.com/bebiksior/EvenBetter/releases) and download the latest `plugin_package.zip` file 2. In your Caido instance, navigate to the `Plugins` page, click `Install` and select the downloaded `plugin_package.zip` file 3. Done! 🎉 ## Changelog v4.0.1 - Fixed Ctrl + Z issue in QuickDecode ## Changelog v4.0.0 - Migrated frontend from React to Vue - Made some features more stable and reliable - Removed `hide-sidebar-groups` feature (might return at some point) - Fixed bugs affecting latest Caido version ## Contribution Feel free to contribute! If you'd like to request a feature or report a bug, please [create a GitHub Issue](https://github.com/bebiksior/EvenBetter/issues/new). ================================================ FILE: caido.config.ts ================================================ import { defineConfig } from '@caido-community/dev'; import vue from '@vitejs/plugin-vue'; import tailwindcss from "tailwindcss"; import tailwindPrimeui from "tailwindcss-primeui"; import tailwindCaido from "@caido/tailwindcss"; import path from "path"; import prefixwrap from "postcss-prefixwrap"; const id = "evenbetter"; export default defineConfig({ id, name: "EvenBetter", description: "Collection of tweaks and improvements for Caido", version: "4.0.3", author: { name: "bebiks", email: "lukasz@caido.io", url: "https://github.com/caido-community/EvenBetter", }, plugins: [ { kind: "backend", id: "evenbetter-backend", root: "packages/backend", }, { kind: 'frontend', id: "evenbetter-frontend", root: 'packages/frontend', backend: { id: "evenbetter-backend", }, vite: { plugins: [vue()], build: { rollupOptions: { external: [ '@caido/frontend-sdk', "@codemirror/autocomplete", "@codemirror/commands", "@codemirror/language", "@codemirror/lint", "@codemirror/search", "@codemirror/state", "@codemirror/view", "@lezer/common", "@lezer/highlight", "@lezer/lr", "vue", ] } }, resolve: { alias: [ { find: "@", replacement: path.resolve(__dirname, "packages/frontend/src"), }, ], }, css: { postcss: { plugins: [ // This plugin wraps the root element in a unique ID // This is necessary to prevent styling conflicts between plugins prefixwrap(`#plugin--${id}`), tailwindcss({ corePlugins: { preflight: false, }, content: [ './packages/frontend/src/**/*.{vue,ts}', './node_modules/@caido/primevue/dist/primevue.mjs' ], // Check the [data-mode="dark"] attribute on the element to determine the mode // This attribute is set in the Caido core application darkMode: ["selector", '[data-mode="dark"]'], plugins: [ // This plugin injects the necessary Tailwind classes for PrimeVue components tailwindPrimeui, // This plugin injects the necessary Tailwind classes for the Caido theme tailwindCaido, ], }) ] } } } } ] }); ================================================ FILE: eslint.config.mjs ================================================ import { defaultConfig } from "@caido/eslint-config"; /** @type {import('eslint').Linter.Config } */ export default [ ...defaultConfig(), ] ================================================ FILE: package.json ================================================ { "name": "evenbetter", "version": "0.0.0", "private": true, "scripts": { "typecheck": "pnpm -r typecheck", "lint": "eslint ./packages/**/src --fix", "build": "caido-dev build", "watch": "caido-dev watch" }, "devDependencies": { "@caido-community/dev": "^0.1.3", "@caido/eslint-config": "^0.6.0", "@caido/tailwindcss": "0.0.1", "@vitejs/plugin-vue": "5.2.1", "postcss-prefixwrap": "1.51.0", "tailwindcss": "3.4.13", "tailwindcss-primeui": "0.6.1", "typescript": "5.5.4" } } ================================================ FILE: packages/backend/package.json ================================================ { "name": "backend", "version": "0.0.0", "type": "module", "types": "src/index.ts", "scripts": { "typecheck": "tsc --noEmit" }, "devDependencies": { "@caido/sdk-backend": "^0.50.2", "shared": "workspace:*" } } ================================================ FILE: packages/backend/src/api/flags.ts ================================================ import { error, type FeatureFlag, type FeatureFlagTag, ok, type Result, } from "shared"; import { FeatureFlagsStore } from "../stores/flags"; import { type BackendSDK } from "../types"; export const getFlags = ( _: BackendSDK, filters?: Partial, ): Result => { const flagsStore = FeatureFlagsStore.get(); const flags = flagsStore.getFlags(); return ok( flags.filter((flag) => { if (filters?.tag && flag.tag !== filters.tag) { return false; } return true; }), ); }; export const updateFlags = async ( _: BackendSDK, flags: FeatureFlag[], ): Promise> => { const flagsStore = FeatureFlagsStore.get(); await flagsStore.setFlags(flags); return ok(undefined); }; export const getFlag = ( _: BackendSDK, tag: FeatureFlagTag, ): Result => { const flagsStore = FeatureFlagsStore.get(); const flags = flagsStore.getFlags(); const flag = flags.find((f) => f.tag === tag); if (!flag) { return error(`Flag ${tag} not found`); } return ok(flag.enabled); }; export const setFlag = async ( _: BackendSDK, tag: FeatureFlagTag, value: boolean, ): Promise> => { const flagsStore = FeatureFlagsStore.get(); await flagsStore.setFlag(tag, value); return ok(undefined); }; ================================================ FILE: packages/backend/src/api/settings.ts ================================================ import { ok, type Result, type SettingKey, type Settings, type SettingValue, } from "shared"; import { SettingsStore } from "../stores/settings"; import { type BackendSDK } from "../types"; export const getSettings = (): Result => { const settingsStore = SettingsStore.get(); return ok(settingsStore.getSettings()); }; export const updateSetting = ( _: BackendSDK, key: K, value: SettingValue, ): Result => { const settingsStore = SettingsStore.get(); settingsStore.updateSetting(key, value); return ok(undefined); }; ================================================ FILE: packages/backend/src/features/backend-test/index.ts ================================================ import { type BackendSDK } from "../../types"; import { createFeature } from "../manager"; export const backendTest = createFeature("backend-test", { onFlagEnabled: (sdk: BackendSDK) => { sdk.console.log("Backend test flag enabled"); }, onFlagDisabled: (sdk: BackendSDK) => { sdk.console.log("Backend test flag disabled"); }, }); ================================================ FILE: packages/backend/src/features/index.ts ================================================ // This file is used to import all the backend features. import "./backend-test"; ================================================ FILE: packages/backend/src/features/manager.ts ================================================ import { type FeatureFlag, type FeatureFlagTag } from "shared"; import { type BackendSDK } from "../types"; /** * Example flag: * export const backendTest = createFeature("backend-test", { onFlagEnabled: (sdk: CaidoBackendSDK) => { console.log("Backend test flag enabled"); }, onFlagDisabled: (sdk: CaidoBackendSDK) => { console.log("Backend test flag disabled"); }, }); * * This FeatureManager is responsible for managing the feature flags on the backend. * It handles only the flags with kind "backend". * It creates a map of feature tags and their functions for enable and disable. * FlagStore will call these functions when a flag is enabled or disabled. */ type FeatureHandlers = { onFlagEnabled: (sdk: BackendSDK) => void; onFlagDisabled: (sdk: BackendSDK) => void; }; const featureMap = new Map(); export function createFeature(tag: FeatureFlagTag, handlers: FeatureHandlers) { featureMap.set(tag, handlers); return { tag, ...handlers }; } export function backendHandleFlagToggle( tag: FeatureFlagTag, enabled: boolean, sdk: BackendSDK, ) { const handlers = featureMap.get(tag); if (handlers) { if (enabled) { handlers.onFlagEnabled(sdk); } else { handlers.onFlagDisabled(sdk); } } else { sdk.console.warn(`No handlers for feature flag ${tag}`); } } export function initializeFeatures(flags: FeatureFlag[], sdk: BackendSDK) { flags.forEach((flag) => { if (flag.kind === "backend" && flag.enabled) { const handlers = featureMap.get(flag.tag); if (handlers) { handlers.onFlagEnabled(sdk); } else { sdk.console.warn(`No handlers for feature flag ${flag.tag}`); } } }); } ================================================ FILE: packages/backend/src/index.ts ================================================ import { type DefineAPI, type SDK } from "caido:plugin"; import { getFlag, getFlags, setFlag, updateFlags } from "./api/flags"; import { getSettings, updateSetting } from "./api/settings"; import { FeatureFlagsStore } from "./stores/flags"; import { SettingsStore } from "./stores/settings"; import { type BackendEvents } from "./types"; export type { BackendEvents } from "./types"; export type API = DefineAPI<{ getFlags: typeof getFlags; setFlag: typeof setFlag; getFlag: typeof getFlag; updateFlags: typeof updateFlags; getSettings: typeof getSettings; updateSetting: typeof updateSetting; }>; export async function init(sdk: SDK) { await SettingsStore.initialize(sdk); await FeatureFlagsStore.initialize(sdk); sdk.api.register("getFlags", getFlags); sdk.api.register("setFlag", setFlag); sdk.api.register("getFlag", getFlag); sdk.api.register("updateFlags", updateFlags); sdk.api.register("getSettings", getSettings); sdk.api.register("updateSetting", updateSetting); sdk.events.onProjectChange((projectId) => { sdk.api.send("caido:project-change", projectId); }); } ================================================ FILE: packages/backend/src/stores/flags.ts ================================================ import { readFile, writeFile } from "fs/promises"; import * as path from "path"; import { type FeatureFlag, type FeatureFlagTag } from "shared"; import { backendHandleFlagToggle, initializeFeatures as initializeBackendFeatures, } from "../features/manager"; import { type BackendSDK } from "../types"; import { exists, getFlagsPath } from "../utils/files"; interface StoredFlag { tag: FeatureFlagTag; enabled: boolean; } export class FeatureFlagsStore { private static instance: FeatureFlagsStore; private flags: FeatureFlag[]; private sdk: BackendSDK; private constructor(sdk: BackendSDK) { this.sdk = sdk; this.flags = [ // { // tag: "backend-test", // description: "Test backend flag", // enabled: true, // kind: "backend", // }, { tag: "share-scope", description: "Share scope context menu button", enabled: true, kind: "frontend", requiresReload: false, }, { tag: "quick-decode", description: "Decode & encode selection on the Replay page", enabled: true, kind: "frontend", }, { tag: "clear-all-findings", description: "Adds a button to clear all findings", enabled: true, kind: "frontend", }, { tag: "share-replay-collections", description: "Export & import replay collections", enabled: true, kind: "frontend", }, { tag: "exclude-host-path", description: "Exclude Host/Path context menu buttons on the HTTP History page", enabled: true, kind: "frontend", requiresReload: true, }, { tag: "quick-mar", description: "Quick Match and Replace context menu button", enabled: true, kind: "frontend", requiresReload: true, }, { tag: "colorize-by-method", description: "Colorize session tabs by their HTTP methods in the Replay page", enabled: false, requiresReload: true, kind: "frontend", knownIssues: [ "It's a bit unstable, so it's disabled by default. Working on a fix. If you want to try it, you can enable it by setting the flag to true.", ], }, { tag: "share-filters", description: "Export & import filter presets", enabled: true, kind: "frontend", }, { tag: "common-filters", description: "Creates and automatically updates common filters you may want to use. 1hr, recent, 24hr, 6hr, 12hr", enabled: true, kind: "frontend", requiresReload: false, }, { tag: "command-palette-workflows", description: "Adds all your convert workflows to the command palette", enabled: true, kind: "frontend", requiresReload: true, }, ]; } static async initialize(sdk: BackendSDK): Promise { this.instance = new FeatureFlagsStore(sdk); await this.instance.readFlags(); initializeBackendFeatures(this.instance.flags, sdk); return this.instance; } public async readFlags() { const flagsPath = getFlagsPath(this.sdk); const fileExists = await exists(flagsPath); if (!fileExists) { this.sdk.console.log("Flags file not found. Creating a new flags file."); const flagsPath = await this.saveFlagsToFile(this.flags); this.sdk.console.log("Flags file created at " + path.resolve(flagsPath)); } try { const storedFlags: StoredFlag[] = JSON.parse( await readFile(flagsPath, "utf-8"), ); storedFlags.forEach((storedFlag) => { const flag = this.flags.find((f) => f.tag === storedFlag.tag); if (flag) { flag.enabled = storedFlag.enabled; } }); } catch (error) { this.sdk.console.error( "Unexpected error reading flags: " + String(error), ); } } private async saveFlagsToFile(flags: FeatureFlag[]): Promise { const flagsPath = getFlagsPath(this.sdk); const storedFlags: StoredFlag[] = flags.map((flag) => ({ tag: flag.tag, enabled: flag.enabled, })); await writeFile(flagsPath, JSON.stringify(storedFlags, null, 2)); return flagsPath; } static get(): FeatureFlagsStore { if (FeatureFlagsStore.instance === undefined) { throw new Error("FeatureFlagsStore not initialized"); } return FeatureFlagsStore.instance; } getFlags(): FeatureFlag[] { return this.flags; } async setFlags(flags: FeatureFlag[]): Promise { this.flags = flags; await this.saveFlagsToFile(flags); } async setFlag(tag: FeatureFlagTag, enabled: boolean): Promise { const flag = this.flags.find((f) => f.tag === tag); if (flag) { flag.enabled = enabled; this.handleFlagToggle(flag, enabled); } await this.saveFlagsToFile(this.flags); } /** * If flag has kind of "frontend", it will be sent to the frontend via sdk.api.send(eventName, value) * If flag has kind of "backend", it will be handled by the backend */ private handleFlagToggle(flag: FeatureFlag, enabled: boolean): void { switch (flag.kind) { case "frontend": this.sdk.api.send("flag:toggled", flag.tag, enabled); break; case "backend": backendHandleFlagToggle(flag.tag, enabled, this.sdk); break; default: this.sdk.console.warn(`Unknown flag kind: ${flag.kind}`); } } } ================================================ FILE: packages/backend/src/stores/settings.ts ================================================ import { readFile, writeFile } from "fs/promises"; import * as path from "path"; import { DEFAULT_SETTINGS, getFontUrl, type SettingKey, type Settings, type SettingValue, } from "shared"; import { type BackendSDK } from "../types"; import { exists, getSettingsPath } from "../utils/files"; export class SettingsStore { private static instance?: SettingsStore; private settings: Settings; private sdk: BackendSDK; private constructor(sdk: BackendSDK) { this.settings = { ...DEFAULT_SETTINGS }; this.sdk = sdk; } public async readSettings() { const settingsPath = getSettingsPath(this.sdk); const fileExists = await exists(settingsPath); if (!fileExists) { this.sdk.console.log( "Settings file not found. Creating a new settings file.", ); const newSettingsPath = await this.saveSettingsToFile(this.settings); this.sdk.console.log( "Settings file created at " + path.resolve(newSettingsPath), ); } try { const fileContent = await readFile(settingsPath, "utf-8"); const _settings = JSON.parse(fileContent); Object.assign(this.settings, _settings); } catch (error) { this.sdk.console.error( "Unexpected error reading settings: " + String(error), ); } } private async saveSettingsToFile(settings: Settings): Promise { const settingsPath = getSettingsPath(this.sdk); await writeFile(settingsPath, JSON.stringify(settings, null, 2)); return settingsPath; } static async initialize(sdk: BackendSDK): Promise { if (SettingsStore.instance) { throw new Error("SettingsStore already initialized"); } SettingsStore.instance = new SettingsStore(sdk); await this.instance?.readSettings(); return SettingsStore.instance; } static get(): SettingsStore { if (!SettingsStore.instance) { throw new Error("SettingsStore not initialized"); } return SettingsStore.instance; } getSettings(): Settings { return this.settings; } updateSetting(key: K, value: SettingValue): void { this.settings[key] = value; this.saveSettingsToFile(this.settings); if (key === "customFont") { this.sdk.api.send("font:load", value, getFontUrl(value as string)); } } } ================================================ FILE: packages/backend/src/types.ts ================================================ import { type DefineEvents, type SDK } from "caido:plugin"; import { type FeatureFlagTag } from "shared"; export type BackendEvents = DefineEvents<{ "flag:toggled": (tag: FeatureFlagTag, enabled: boolean) => void; "font:load": (fontName: string, fontUrl: string) => void; "caido:project-change": () => void; }>; export type BackendSDK = SDK; ================================================ FILE: packages/backend/src/utils/files.ts ================================================ import { mkdir, stat } from "fs/promises"; import path from "path"; import { type BackendSDK } from "../types"; export async function ensureDir( sdk: BackendSDK, directory: string, ): Promise { try { const dir = path.join(sdk.meta.path(), directory); await mkdir(dir, { recursive: true }); return true; } catch { return false; } } export function getSettingsPath(sdk: BackendSDK): string { return path.join(sdk.meta.path(), "settings.json"); } export function getFlagsPath(sdk: BackendSDK): string { return path.join(sdk.meta.path(), "flags.json"); } export async function exists(f: string): Promise { try { await stat(f); return true; } catch { return false; } } ================================================ FILE: packages/backend/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "types": ["@caido/sdk-backend"] }, "include": ["./src/**/*.ts"] } ================================================ FILE: packages/frontend/package.json ================================================ { "name": "frontend", "version": "0.0.0", "type": "module", "scripts": { "typecheck": "vue-tsc --noEmit" }, "dependencies": { "@caido/primevue": "0.2.1", "@pinia/colada": "^0.17.1", "pinia": "3.0.3", "primevue": "4.1.0", "vue": "3.5.18" }, "devDependencies": { "@caido/sdk-backend": "^0.50.2", "@caido/sdk-frontend": "^0.50.2", "@codemirror/view": "6.38.1", "backend": "workspace:*", "shared": "workspace:*", "vue-tsc": "3.0.5" } } ================================================ FILE: packages/frontend/src/components/FlagsList/Container.vue ================================================ ================================================ FILE: packages/frontend/src/components/FlagsList/index.ts ================================================ export { default as FlagsList } from "./Container.vue"; ================================================ FILE: packages/frontend/src/components/FlagsList/useForm.ts ================================================ import { type FeatureFlag, type FeatureFlagTag } from "shared"; import { ref } from "vue"; import { useSDK } from "@/plugins/sdk"; import { useFlagsQuery, useSetFlag } from "@/queries/flags"; export const useForm = () => { const sdk = useSDK(); const { data: flags, isLoading } = useFlagsQuery(); const { setFlag } = useSetFlag(); const dialogOpen = ref(false); const dialogFlagTag = ref(undefined); const handleDialogOpen = (flagTag: FeatureFlagTag) => { dialogOpen.value = true; dialogFlagTag.value = flagTag; }; const handleDialogClose = () => { dialogOpen.value = false; dialogFlagTag.value = undefined; }; const handleFlagChange = async (flag: FeatureFlag) => { if (flag.requiresReload === true && flag.enabled === true) { handleDialogOpen(flag.tag); return; } const newValue = !flag.enabled; await setFlag({ flag: flag.tag, value: newValue }); const status = newValue ? "enabled" : "disabled"; sdk.window.showToast(`Feature ${flag.tag} has been ${status}`, { variant: "success", }); }; const confirmFlagChange = async () => { if (dialogFlagTag.value !== undefined) { await setFlag({ flag: dialogFlagTag.value, value: false }); window.location.reload(); } }; const hasKnownIssues = (issues?: string[]) => issues !== undefined && issues.length > 0; return { flags, isLoading, dialogOpen, dialogFlagTag, handleDialogOpen, handleDialogClose, handleFlagChange, confirmFlagChange, hasKnownIssues, }; }; ================================================ FILE: packages/frontend/src/components/Settings/Container.vue ================================================ ================================================ FILE: packages/frontend/src/components/Settings/index.ts ================================================ export { default as Settings } from "./Container.vue"; ================================================ FILE: packages/frontend/src/components/Settings/useForm.ts ================================================ import { computed, ref, watch } from "vue"; import { useSettingsQuery, useUpdateSetting } from "@/queries/settings"; export const useForm = () => { const fontOptions = [ "Default", "JetBrains Mono", "Fira Code", "Roboto Mono", "Inconsolata", ]; const { data, isLoading, error } = useSettingsQuery(); const { updateSetting, isPending } = useUpdateSetting(); const localSettings = ref({ customFont: "" }); const hasChanges = computed(() => { return ( data.value !== undefined && localSettings.value.customFont !== data.value.customFont ); }); watch( () => data.value, (v) => { if (v !== undefined) { localSettings.value = { customFont: v.customFont }; } }, { immediate: true }, ); const handleSave = async () => { if (localSettings.value.customFont !== undefined) { await updateSetting({ key: "customFont", value: localSettings.value.customFont, }); } }; const openLink = (href: string) => { window.open(href, "_blank", "noopener,noreferrer"); }; return { data, isLoading, error, isPending, fontOptions, localSettings, hasChanges, handleSave, openLink, }; }; ================================================ FILE: packages/frontend/src/dom/index.ts ================================================ export const initDOMManager = () => { patchHistoryMethod("pushState"); patchHistoryMethod("replaceState"); window.addEventListener("popstate", notify); window.addEventListener("hashchange", notify); window.addEventListener("locationchange", notify); // we need to wait for the Caido app to be fully loaded, this is obviously a hack while we wait for the APIs to be ready setTimeout(() => { let attempts = 0; const maxAttempts = 10; const checkInterval = setInterval(() => { attempts++; if (document.querySelector(".c-topbar__environment")) { clearInterval(checkInterval); for (const cb of subscribers) { cb({ oldHash: lastHash, newHash: lastHash }); } return; } if (attempts >= maxAttempts) { clearInterval(checkInterval); for (const cb of subscribers) { cb({ oldHash: lastHash, newHash: lastHash }); } } }, 50); }, 150); }; // Temporary workaround for missing sdk.navigation.onPageChange type LocationChange = { oldHash: string; newHash: string; }; export type Callback = (change: LocationChange) => void; const subscribers = new Set(); let lastHash = window.location.hash; function notify(): void { const newHash = window.location.hash; if (newHash === lastHash) return; setTimeout(() => { for (const cb of subscribers) { try { cb({ oldHash: lastHash, newHash }); } catch { // ignore } } }, 1); lastHash = newHash; } const patchHistoryMethod = (method: "pushState" | "replaceState"): void => { const original = history[method]; history[method] = function (...args: Parameters) { const result = original.apply(this, args); window.dispatchEvent(new Event("locationchange")); return result; }; }; export function onLocationChange(cb: Callback): () => void { subscribers.add(cb); return () => { subscribers.delete(cb); }; } ================================================ FILE: packages/frontend/src/features/clear-all-findings/index.ts ================================================ import { onLocationChange } from "@/dom"; import { createFeature } from "@/features/manager"; import { type FrontendSDK } from "@/types"; let eventCancelFunction: (() => void) | undefined; let clearAllButton: HTMLElement | undefined; const deleteAllFindings = (sdk: FrontendSDK) => { eventCancelFunction = onLocationChange((data) => { if (data.newHash !== "#/findings") return; attachClearAllButton(sdk); }); }; const attachClearAllButton = (sdk: FrontendSDK) => { if (document.querySelector("#clear-all-findings")) return; clearAllButton = sdk.ui.button({ label: "Clear All", size: "small", variant: "primary", leadingIcon: "fas fa-trash", }); clearAllButton.id = "clear-all-findings"; clearAllButton.addEventListener("click", async () => { try { const allFindingIds: string[] = []; let hasNextPage = true; let cursor: string | undefined = undefined; const batchSize = 1000; while (hasNextPage) { let response; if (cursor !== undefined) { response = await sdk.graphql.getFindingsAfter({ after: cursor, first: batchSize, filter: {}, order: { by: "ID", ordering: "DESC" }, }); const findings = response.findings.edges; allFindingIds.push(...findings.map((finding) => finding.node.id)); hasNextPage = response.findings.pageInfo.hasNextPage; cursor = response.findings.pageInfo.endCursor ?? undefined; } else { response = await sdk.graphql.getFindingsByOffset({ limit: batchSize, offset: 0, filter: {}, order: { by: "ID", ordering: "DESC" }, }); const findings = response.findingsByOffset.edges; allFindingIds.push(...findings.map((finding) => finding.node.id)); hasNextPage = response.findingsByOffset.pageInfo.hasNextPage; cursor = response.findingsByOffset.pageInfo.endCursor ?? undefined; } } if (allFindingIds.length > 0) { await sdk.graphql.deleteFindings({ input: { ids: allFindingIds, }, }); } } catch (error) { console.error("Error clearing all findings:", error); } }); const cardHeader = document.querySelector( ".c-finding-table .c-card__header", ) as HTMLElement; if (cardHeader !== null) { cardHeader.appendChild(clearAllButton); cardHeader.style.display = "flex"; cardHeader.style.justifyContent = "space-between"; cardHeader.style.alignItems = "center"; } }; function cleanup() { if (clearAllButton) { clearAllButton.remove(); } if (eventCancelFunction) { eventCancelFunction(); eventCancelFunction = undefined; } } export const clearAllFindings = createFeature("clear-all-findings", { onFlagEnabled: (sdk: FrontendSDK) => { deleteAllFindings(sdk); }, onFlagDisabled: (sdk: FrontendSDK) => { cleanup(); }, }); ================================================ FILE: packages/frontend/src/features/colorize-by-method/index.ts ================================================ import { createFeature } from "@/features/manager"; import { type FrontendSDK } from "@/types"; import "./style.css"; import { onLocationChange } from "@/dom"; let abortController: AbortController | undefined = undefined; let observer: MutationObserver | undefined = undefined; function setTabMethodAttributes(sdk: FrontendSDK) { const tabs = document.querySelectorAll( ".c-tab-list__body .c-tab-list__tab [data-session-id]", ); const promises = Array.from(tabs).map(async (tab) => { const sessionId = tab.getAttribute("data-session-id"); if (sessionId === null) return; const method = await getHTTPMethod(sessionId, sdk); tab.setAttribute("http-method", method); }); Promise.all(promises).then(() => { setTimeout(() => { updateSelectedTabColor(); }, 100); }); } function updateCurrentTabHTTPMethod(newMethod: string) { const element = document.querySelector( ".c-tab-list__tab [data-is-selected=true][data-session-id]", ); if (!element) return; element.setAttribute("http-method", newMethod); } function updateSelectedTabColor() { const httpMethod = document.querySelector( ".c-lang-http-request__method", )?.textContent; if (httpMethod === null || httpMethod === undefined) return; updateCurrentTabHTTPMethod(httpMethod); } function liveUpdateHTTPMethod() { const controller = new AbortController(); document.addEventListener( "keyup", () => { if (location.hash !== "#/replay") return; updateSelectedTabColor(); }, { signal: controller.signal }, ); return controller; } type HTTPMethod = | "GET" | "POST" | "PUT" | "DELETE" | "OPTIONS" | "PATCH" | "HEAD" | "TRACE" | "CONNECT" | "UNKNOWN"; async function getHTTPMethod( sessionId: string, sdk: FrontendSDK, ): Promise { const data = await sdk.graphql.replayEntry({ id: sessionId }); if (data.replayEntry?.raw === undefined) return "UNKNOWN"; const method = data.replayEntry.raw.split("\n")[0]?.split(" ")[0]; return (method as HTTPMethod) || "UNKNOWN"; } function handleTabListChanges(sdk: FrontendSDK) { setTabMethodAttributes(sdk); const observer = new MutationObserver(() => { setTabMethodAttributes(sdk); }); const tabList = document.querySelector(".c-tab-list__body"); if (tabList) { observer.observe(tabList, { childList: true, }); } return observer; } const cleanup = () => { if (observer) { observer.disconnect(); observer = undefined; } if (abortController) { abortController.abort(); abortController = undefined; } }; function setup(sdk: FrontendSDK) { cleanup(); if (window.location.hash === "#/replay") { observer = handleTabListChanges(sdk); } } export const colorizeByMethod = createFeature("colorize-by-method", { onFlagEnabled: (sdk) => { setup(sdk); setTimeout(() => { abortController = liveUpdateHTTPMethod(); }, 2000); onLocationChange((data) => { cleanup(); if (data.newHash === "#/replay") { observer = handleTabListChanges(sdk); } }); sdk.backend.onEvent("caido:project-change", () => { let attempts = 0; const maxAttempts = 25; const interval = setInterval(() => { const tabList = document.querySelector(".c-tab-list__body"); if (tabList) { setup(sdk); clearInterval(interval); } attempts++; if (attempts >= maxAttempts) { clearInterval(interval); } }, 200); }); }, onFlagDisabled: () => { cleanup(); }, }); ================================================ FILE: packages/frontend/src/features/colorize-by-method/style.css ================================================ [http-method="GET"] { border-bottom: 2px solid #2196f3 !important; } [http-method="POST"] { border-bottom: 2px solid #4caf50 !important; } [http-method="PUT"] { border-bottom: 2px solid #ff9800 !important; } [http-method="PATCH"] { border-bottom: 2px solid #ffeb3b !important; } [http-method="DELETE"] { border-bottom: 2px solid #f44336 !important; } [http-method="HEAD"] { border-bottom: 2px solid #9c27b0 !important; } [http-method="UNKNOWN"] { border-bottom: 2px solid gray !important; } ================================================ FILE: packages/frontend/src/features/command-palette-workflows/index.ts ================================================ import { createFeature } from "@/features/manager"; import { type FrontendSDK } from "@/types"; interface Workflow { id: string; name: string; kind: string; } let registeredCommandIds: string[] = []; const registerWorkflowCommand = (workflow: Workflow, sdk: FrontendSDK) => { // Skip if not a convert workflow if (workflow.kind !== "Convert") { return; } const commandId = `evenbetter:workflow:${workflow.id}`; // Check if command is already registered to avoid duplicates if (registeredCommandIds.includes(commandId)) { return; } try { sdk.commands.register(commandId, { name: `c ${workflow.name}`, group: "Convert Workflows", run: async () => { try { // Get the selected text from the active editor const selectedText = sdk.window.getActiveEditor()?.getSelectedText(); if (selectedText === undefined) { sdk.window.showToast("No text selected", { variant: "warning", }); return; } // Run the convert workflow with the selected text const result = await sdk.graphql.runConvertWorkflow({ id: workflow.id, input: selectedText, }); if (result.runConvertWorkflow.error) { const errorMessage = typeof result.runConvertWorkflow.error === "string" ? result.runConvertWorkflow.error : JSON.stringify(result.runConvertWorkflow.error); sdk.window.showToast(`Workflow error: ${errorMessage}`, { variant: "error", }); return; } // Get the output const output = result.runConvertWorkflow.output; if (output !== undefined && output !== null) { // Check if the active editor is read-only const activeEditor = sdk.window.getActiveEditor(); if (!activeEditor) { sdk.window.showToast("No active editor", { variant: "error", }); return; } if (activeEditor.isReadOnly()) { // Copy output to clipboard navigator.clipboard .writeText(output) .then(() => { sdk.window.showToast( "Copied: " + output.substring(0, 30) + "...", { variant: "info", duration: 7000, }, ); }) .catch(() => { sdk.window.showToast("Failed to copy output", { variant: "error", }); }); } else { // Replace the selected text with the workflow output for editable editors activeEditor.replaceSelectedText(output); } } } catch (error) { console.error("Error running workflow:", error); sdk.window.showToast("Failed to run workflow", { variant: "error", }); } }, }); registeredCommandIds.push(commandId); sdk.commandPalette.register(commandId); } catch (error) { console.error( `Failed to register command for workflow ${workflow.name}:`, error, ); } }; const loadExistingWorkflows = (sdk: FrontendSDK) => { try { const workflows = sdk.workflows.getWorkflows(); // Register commands for each existing convert workflow workflows.forEach((workflow) => { registerWorkflowCommand(workflow, sdk); }); } catch (error) { console.error("Failed to load existing workflows:", error); sdk.window.showToast("[EvenBetter] Failed to load existing workflows", { variant: "error", }); } }; const setupWorkflowListener = (sdk: FrontendSDK) => { // Listen for new workflows being created sdk.workflows.onCreatedWorkflow((workflow) => { registerWorkflowCommand(workflow.workflow, sdk); }); }; const init = (sdk: FrontendSDK) => { // Load existing workflows and register their commands loadExistingWorkflows(sdk); // Set up listener for new workflows setupWorkflowListener(sdk); }; const cleanup = (sdk: FrontendSDK) => { // Clear registered command IDs when feature is disabled registeredCommandIds = []; window.location.reload(); //This will reload the page and it wont trigger the onFlagEnabled again }; export const commandPaletteWorkflows = createFeature( "command-palette-workflows", { onFlagEnabled: init, onFlagDisabled: cleanup, }, ); ================================================ FILE: packages/frontend/src/features/common-filters/index.ts ================================================ import { createFeature } from "@/features/manager"; import { type FrontendSDK } from "@/types"; let intervalId: Timeout | undefined; interface TimeFilter { name: string; minutes: number; } const TIME_FILTERS: TimeFilter[] = [ { name: "recent", minutes: 5 }, { name: "1hr", minutes: 60 }, { name: "6hr", minutes: 360 }, { name: "12hr", minutes: 720 }, { name: "24hr", minutes: 1440 }, ]; const createTimeBasedFilterQuery = (minutes: number): string => { const now = new Date(); const pastTime = new Date(now.getTime() - minutes * 60 * 1000); const formattedDate = pastTime.getFullYear() + "-" + String(pastTime.getMonth() + 1).padStart(2, "0") + "-" + String(pastTime.getDate()).padStart(2, "0") + " " + String(pastTime.getHours()).padStart(2, "0") + ":" + String(pastTime.getMinutes()).padStart(2, "0") + ":" + String(pastTime.getSeconds()).padStart(2, "0"); return `req.created_at.gt:"${formattedDate}"`; }; const maintainTimeFilters = async (sdk: FrontendSDK) => { try { // Get all existing filters const existingFilters = sdk.filters.getAll(); for (const timeFilter of TIME_FILTERS) { const filterQuery = createTimeBasedFilterQuery(timeFilter.minutes); // Check if filter already exists const existingFilter = existingFilters.find( (filter) => filter.name === timeFilter.name, ); if (existingFilter) { // Update existing filter if the query has changed if (existingFilter.query !== filterQuery) { await sdk.filters.update(existingFilter.id, { name: timeFilter.name, alias: timeFilter.name, query: filterQuery, }); } } else { // Create new filter await sdk.filters.create({ name: timeFilter.name, alias: timeFilter.name, query: filterQuery, }); } } } catch (error) { console.error("Error maintaining time filters:", error); } }; const startFilterMaintenance = (sdk: FrontendSDK) => { // Run immediately maintainTimeFilters(sdk); // Then run every minute intervalId = setInterval(() => { maintainTimeFilters(sdk); }, 60000); // 60,000ms = 1 minute }; const stopFilterMaintenance = async (sdk: FrontendSDK) => { if (intervalId) { clearInterval(intervalId); intervalId = undefined; } try { // Get all existing filters const existingFilters = sdk.filters.getAll(); // Remove filters that match our time filter names for (const timeFilter of TIME_FILTERS) { const existingFilter = existingFilters.find( (filter) => filter.name === timeFilter.name, ); if (existingFilter) { await sdk.filters.delete(existingFilter.id); } } } catch (error) { console.error("Error cleaning up time filters:", error); sdk.window.showToast("[EvenBetter] Error cleaning up time filters", { variant: "error", }); } }; export default createFeature("common-filters", { onFlagEnabled: (sdk: FrontendSDK) => { startFilterMaintenance(sdk); }, onFlagDisabled: (sdk: FrontendSDK) => { stopFilterMaintenance(sdk); }, }); ================================================ FILE: packages/frontend/src/features/exclude-host-path/index.ts ================================================ import { createFeature } from "@/features/manager"; import { type FrontendSDK } from "@/types"; const excludeHostPathFunctionality = (sdk: FrontendSDK) => { sdk.commands.register("eb:excludehost", { name: "Exclude Host", run: async () => { const selectedRequest = await getSelectedRequest(sdk); if (!selectedRequest) return; const currentQuery = sdk.httpHistory.getQuery(); const newQuery = currentQuery ? `${currentQuery} AND req.host.ne:"${selectedRequest.host}"` : `req.host.ne:"${selectedRequest.host}"`; sdk.httpHistory.setQuery(newQuery); }, }); sdk.menu.registerItem({ type: "RequestRow", commandId: "eb:excludehost", leadingIcon: "fa fa-ban", }); sdk.commands.register("eb:excludepath", { name: "Exclude Path", run: async () => { const selectedRequest = await getSelectedRequest(sdk); if (!selectedRequest) return; const currentQuery = sdk.httpHistory.getQuery(); const newQuery = currentQuery ? `${currentQuery} AND req.path.ne:"${selectedRequest.path}"` : `req.path.ne:"${selectedRequest.path}"`; sdk.httpHistory.setQuery(newQuery); }, }); sdk.menu.registerItem({ type: "RequestRow", commandId: "eb:excludepath", leadingIcon: "fa fa-ban", }); }; const getSelectedRequestID = () => { return document .querySelector("[data-request-id]") ?.getAttribute("data-request-id"); }; const getSelectedRequest = async (sdk: FrontendSDK) => { const selectedRequestID = getSelectedRequestID(); if (selectedRequestID === null || selectedRequestID === undefined) return; const request = await sdk.graphql.request({ id: selectedRequestID, }); return request?.request; }; export const excludeHostPath = createFeature("exclude-host-path", { onFlagEnabled: (sdk: FrontendSDK) => { excludeHostPathFunctionality(sdk); }, onFlagDisabled: (sdk: FrontendSDK) => { location.reload(); }, }); ================================================ FILE: packages/frontend/src/features/index.ts ================================================ // This file is used to import all the frontend features. import "./quick-decode"; import "./clear-all-findings"; import "./exclude-host-path"; import "./quick-mar"; import "./share-scope"; import "./share-replay-collections"; import "./colorize-by-method"; import "./share-filters"; import "./common-filters"; import "./command-palette-workflows"; ================================================ FILE: packages/frontend/src/features/manager.ts ================================================ // This is the features manager for the frontend. // It creates a map of feature tags and their functions for enable and disable. // FlagStore will call these functions when a flag is enabled or disabled. import { type FeatureFlag, type FeatureFlagTag } from "shared"; import { type FrontendSDK } from "../types"; type FeatureHandlers = { onFlagEnabled: (sdk: FrontendSDK) => void; onFlagDisabled: (sdk: FrontendSDK) => void; }; const featureMap = new Map(); export function createFeature(tag: FeatureFlagTag, handlers: FeatureHandlers) { featureMap.set(tag, handlers); return { tag, ...handlers }; } function handleFlagToggle( tag: FeatureFlagTag, enabled: boolean, sdk: FrontendSDK, ) { const handlers = featureMap.get(tag); if (handlers) { if (enabled) { handlers.onFlagEnabled(sdk); } else { handlers.onFlagDisabled(sdk); } } else { console.warn(`No handlers for feature flag ${tag}`); } } function initializeFeatures(flags: FeatureFlag[], sdk: FrontendSDK) { flags.forEach((flag) => { if (flag.kind === "frontend" && flag.enabled) { const handlers = featureMap.get(flag.tag); if (handlers) { handlers.onFlagEnabled(sdk); } else { console.warn(`No handlers for feature flag ${flag.tag}`); } } }); } export const initialize = async (sdk: FrontendSDK) => { sdk.backend.onEvent("flag:toggled", (tag, enabled) => { handleFlagToggle(tag, enabled, sdk); }); const flags = await sdk.backend.getFlags({ kind: "frontend" }); switch (flags.kind) { case "Success": initializeFeatures(flags.value, sdk); break; case "Error": console.error(flags.error); sdk.window.showToast("Error initializing features", { variant: "error", }); break; } }; ================================================ FILE: packages/frontend/src/features/quick-decode/index.ts ================================================ import { type FrontendSDK } from "@/types"; import { createFeature } from "@/features/manager"; import "./quick-decode.css"; import { onLocationChange } from "@/dom"; interface CodeMirrorEditor { state: { readOnly: boolean; doc: { lineAt: (pos: number) => { number: number; from: number; text: string; }; }; selection: { main: { from: number; to: number; head: number; }; }; sliceDoc: (from: number, to: number) => string; }; contentDOM: HTMLElement; dispatch: (changes: any) => void; } interface Selection { from: number; to: number; text: string; } function unicodeEncode(str: string): string { return str .split("") .map((char) => { const unicode = char.charCodeAt(0).toString(16).padStart(4, "0"); return `\\u${unicode}`; }) .join(""); } interface HistoryEntry { content: string; selectionStart: number; selectionEnd: number; } class QuickDecode { private HTMLElement!: HTMLDivElement; private quickDecode!: HTMLDivElement; private textArea!: HTMLTextAreaElement; private encodeMethodSelect!: HTMLSelectElement; private encodeMethod: string; private activeEditor: CodeMirrorEditor | undefined = undefined; private selectionInterval: Timeout | undefined; private copyIconElement: HTMLElement | undefined; private undoStack: HistoryEntry[] = []; private redoStack: HistoryEntry[] = []; private isUpdatingFromHistory: boolean = false; private lastSavedContent: string = ""; constructor() { this.initializeHTMLElement(); this.initializeResizer(); this.initializeSelectedTextDiv(); this.initializeTextArea(); this.initializeEncodingMethodSelect(); this.initializeCopyIcon(); this.encodeMethod = "none"; this.startMonitoringSelection(); } private initializeHTMLElement(): void { this.HTMLElement = document.createElement("div"); this.HTMLElement.id = "plugin--evenbetter"; this.quickDecode = document.createElement("div"); this.quickDecode.classList.add("evenbetter__qd-body"); this.quickDecode.style.display = "none"; this.HTMLElement.appendChild(this.quickDecode); } private initializeResizer(): void { const resizer = document.createElement("div"); resizer.id = "evenbetter__qd-resizer"; let isResizing = false; let startY: number; const resize = (e: MouseEvent) => { if (!isResizing) return; const diffY = startY - e.clientY; const newHeight = Math.max(10, this.quickDecode.offsetHeight + diffY); this.quickDecode.style.height = `${newHeight}px`; startY = e.clientY; }; const stopResize = () => { isResizing = false; document.removeEventListener("mousemove", resize); document.removeEventListener("mouseup", stopResize); }; resizer.addEventListener("mousedown", (e: MouseEvent) => { isResizing = true; startY = e.clientY; document.addEventListener("mousemove", resize); document.addEventListener("mouseup", stopResize); }); this.quickDecode.appendChild(resizer); } private initializeSelectedTextDiv(): void { const selectedTextDiv = document.createElement("div"); selectedTextDiv.classList.add("evenbetter__qd-selected-text"); const selectedTextTopDiv = document.createElement("div"); selectedTextTopDiv.classList.add("evenbetter__qd-selected-text-top"); selectedTextDiv.appendChild(selectedTextTopDiv); this.quickDecode.appendChild(selectedTextDiv); } private initializeTextArea(): void { this.textArea = document.createElement("textarea"); this.textArea.classList.add("evenbetter__qd-selected-text-box"); this.textArea.setAttribute("autocomplete", "off"); this.textArea.setAttribute("autocorrect", "off"); this.textArea.setAttribute("autocapitalize", "off"); this.textArea.setAttribute("spellcheck", "false"); this.textArea.addEventListener("input", this.handleInput.bind(this)); this.textArea.addEventListener("keydown", this.handleKeyDown.bind(this)); const selectedTextDiv = this.quickDecode.querySelector( ".evenbetter__qd-selected-text", ); if (selectedTextDiv) { selectedTextDiv.appendChild(this.textArea); } } private initializeEncodingMethodSelect(): void { this.encodeMethodSelect = document.createElement("select"); this.encodeMethodSelect.classList.add( "evenbetter__qd-selected-text-top-select", ); const options = [ { value: "none", label: "None" }, { value: "base64", label: "Base64" }, { value: "unicode", label: "Unicode" }, { value: "url", label: "URL" }, { value: "url+base64", label: "URL + Base64" }, { value: "base64+url", label: "Base64 + URL" }, ]; options.forEach(({ value, label }) => { const optionElement = document.createElement("option"); optionElement.value = value; optionElement.textContent = label; this.encodeMethodSelect.appendChild(optionElement); }); this.encodeMethodSelect.addEventListener("change", (e) => { const target = e.target as HTMLSelectElement; this.encodeMethod = target.value; this.handleInput(); }); const selectedTextTopDiv = this.quickDecode.querySelector( ".evenbetter__qd-selected-text-top", ); if (selectedTextTopDiv) { selectedTextTopDiv.appendChild(this.encodeMethodSelect); } } private initializeCopyIcon(): void { this.copyIconElement = document.createElement("i"); this.copyIconElement.classList.add("c-icon", "fas", "fa-copy"); this.copyIconElement.addEventListener( "click", this.copyToClipboard.bind(this), ); const selectedTextTopDiv = this.quickDecode.querySelector( ".evenbetter__qd-selected-text-top", ); if (selectedTextTopDiv) { selectedTextTopDiv.appendChild(this.copyIconElement); } } private copyToClipboard(): void { const decodedText = this.textArea.value; if (decodedText) { navigator.clipboard.writeText(decodedText); } } private handleKeyDown(e: KeyboardEvent): void { const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0; const isUndo = (isMac ? e.metaKey : e.ctrlKey) && e.key === "z" && !e.shiftKey; const isRedo = (isMac ? e.metaKey : e.ctrlKey) && (e.key === "y" || (e.key === "z" && e.shiftKey)); if (isUndo) { e.preventDefault(); this.undo(); } else if (isRedo) { e.preventDefault(); this.redo(); } } private saveHistory(): void { if (this.isUpdatingFromHistory) return; const currentContent = this.textArea.value; if (currentContent === this.lastSavedContent) return; const historyEntry: HistoryEntry = { content: this.lastSavedContent, selectionStart: this.textArea.selectionStart, selectionEnd: this.textArea.selectionEnd, }; this.undoStack.push(historyEntry); if (this.undoStack.length > 100) { this.undoStack.shift(); } this.redoStack = []; this.lastSavedContent = currentContent; } private undo(): void { if (this.undoStack.length === 0) return; const currentEntry: HistoryEntry = { content: this.textArea.value, selectionStart: this.textArea.selectionStart, selectionEnd: this.textArea.selectionEnd, }; this.redoStack.push(currentEntry); const previousEntry = this.undoStack.pop(); if (previousEntry) { this.isUpdatingFromHistory = true; this.textArea.value = previousEntry.content; this.textArea.setSelectionRange( previousEntry.selectionStart, previousEntry.selectionEnd, ); this.lastSavedContent = previousEntry.content; this.isUpdatingFromHistory = false; this.handleInput(); } } private redo(): void { if (this.redoStack.length === 0) return; const currentEntry: HistoryEntry = { content: this.textArea.value, selectionStart: this.textArea.selectionStart, selectionEnd: this.textArea.selectionEnd, }; this.undoStack.push(currentEntry); const nextEntry = this.redoStack.pop(); if (nextEntry) { this.isUpdatingFromHistory = true; this.textArea.value = nextEntry.content; this.textArea.setSelectionRange( nextEntry.selectionStart, nextEntry.selectionEnd, ); this.lastSavedContent = nextEntry.content; this.isUpdatingFromHistory = false; this.handleInput(); } } private handleInput(): void { if (!this.isUpdatingFromHistory) { this.saveHistory(); } let newContent = this.textArea.value; if ( newContent.length <= 0 || !this.activeEditor || this.activeEditor.state.readOnly ) return; newContent = this.encodeContent(newContent); this.activeEditor.dispatch({ changes: [ { from: this.activeEditor.state.selection.main.from, to: this.activeEditor.state.selection.main.to, insert: newContent, }, ], }); } private encodeContent(content: string): string { switch (this.encodeMethod) { case "base64": return btoa(content); case "unicode": return unicodeEncode(content); case "url": return encodeURIComponent(content); case "url+base64": return encodeURIComponent(btoa(content)); case "base64+url": return btoa(encodeURIComponent(content)); default: return content; } } public updateText(text: string): void { this.textArea.value = text; this.lastSavedContent = text; this.undoStack = []; this.redoStack = []; } public updateEncodeMethod(encodeMethod?: string): void { this.encodeMethod = encodeMethod || "none"; this.encodeMethodSelect.value = this.encodeMethod; } public show(): void { this.quickDecode.style.display = "flex"; } public hide(): void { this.quickDecode.style.display = "none"; } public getElement(): HTMLDivElement { return this.HTMLElement; } private getActiveEditor(): CodeMirrorEditor | undefined { const activeElement = document.activeElement; if (!activeElement) return; const cmContent = activeElement.closest(".cm-content"); if (!cmContent) return; return (cmContent as any)?.cmView?.view as CodeMirrorEditor; } private getCurrentSelection(): Selection { const activeEditor = this.getActiveEditor(); if (!activeEditor) { return { from: 0, to: 0, text: "" }; } const { from, to } = activeEditor.state.selection.main; return { from, to, text: activeEditor.state.sliceDoc(from, to), }; } private startMonitoringSelection(): void { const INTERVAL_DELAY = 50; let lastSelection = this.getCurrentSelection(); this.selectionInterval = setInterval(() => { const newSelection = this.getCurrentSelection(); if ( newSelection.from !== lastSelection.from || newSelection.to !== lastSelection.to ) { lastSelection = newSelection; this.onSelectionChange(newSelection); } }, INTERVAL_DELAY); } public stopMonitoringSelection(): void { if (this.selectionInterval) { clearInterval(this.selectionInterval); } } private isMouseOver(element: HTMLElement): boolean { if (!element) return false; return Array.from(document.querySelectorAll(":hover")).includes(element); } private onSelectionChange(selection: Selection): void { if (this.isMouseOver(this.HTMLElement)) return; const contextMenu = document.querySelector(".p-contextmenu"); if (contextMenu && this.isMouseOver(contextMenu as HTMLElement)) return; if (selection.text === "") { this.hide(); return; } this.activeEditor = this.getActiveEditor(); this.setReadOnly(this.activeEditor?.state.readOnly ?? false); this.showQuickDecode(selection.text); } private showQuickDecode(text: string): void { const decoded = this.tryToDecode(text); this.updateText(decoded.decodedContent); this.updateEncodeMethod(decoded.encodeMethod); this.show(); } private setReadOnly(readOnly: boolean): void { this.textArea.disabled = readOnly; this.encodeMethodSelect.disabled = readOnly; if (readOnly) { this.encodeMethodSelect.value = "none"; } } private isUrlEncoded(str: string): boolean { const urlRegex = /(%[0-9A-Fa-f]{2})+/g; return urlRegex.test(str); } private base64Decode(input: string): { encodeMethod: string; decodedContent: string; } { const modifiedInput = input.padEnd(Math.ceil(input.length / 4) * 4, "="); const base64Regex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/; if (base64Regex.test(modifiedInput)) { try { const decodedBase64 = atob(modifiedInput); return { encodeMethod: "base64", decodedContent: decodedBase64 }; } catch (error) { // If decoding fails, return the original input } } return { encodeMethod: "none", decodedContent: input }; } private tryToDecode(input: string): { encodeMethod: string; decodedContent: string; } { const base64Decoded = this.base64Decode(input); if (base64Decoded.encodeMethod !== "none") { if (this.isUrlEncoded(base64Decoded.decodedContent)) { try { const decodedUrl = decodeURIComponent(base64Decoded.decodedContent); return { encodeMethod: "base64+url", decodedContent: decodedUrl }; } catch (error) { return base64Decoded; } } return base64Decoded; } const unicodeRegex = /\\u([0-9a-fA-F]{4})/g; if (unicodeRegex.test(input)) { try { const decodedUnicode = input.replace(unicodeRegex, (_, code) => String.fromCharCode(parseInt(code, 16)), ); return { encodeMethod: "unicode", decodedContent: decodedUnicode }; } catch (error) { // If decoding fails, continue to the next decoding attempt } } if (this.isUrlEncoded(input)) { try { const decodedUrl = decodeURIComponent(input); const base64Decoded = this.base64Decode(decodedUrl); if (base64Decoded.encodeMethod !== "none" && input.length > 8) { return { encodeMethod: "url+base64", decodedContent: base64Decoded.decodedContent, }; } return { encodeMethod: "url", decodedContent: decodedUrl }; } catch (error) { // If decoding fails, continue to the next decoding attempt } } return { encodeMethod: "none", decodedContent: input }; } public cleanup(): void { this.stopMonitoringSelection(); this.textArea.removeEventListener("input", this.handleInput); this.textArea.removeEventListener("keydown", this.handleKeyDown); this.encodeMethodSelect.removeEventListener("change", this.handleInput); if (this.copyIconElement) { this.copyIconElement.removeEventListener("click", this.copyToClipboard); } this.HTMLElement.remove(); } } class QuickDecodeManager { private sdk: FrontendSDK; private quickDecode: QuickDecode | undefined = undefined; private cleanupListener: (() => void) | undefined = undefined; private projectChangeListener: (() => Promise) | undefined = undefined; private pageOpenListener: ((newHash: string) => void) | undefined = undefined; private isCleaned: boolean = false; constructor(sdk: FrontendSDK) { this.sdk = sdk; } private removeExistingQuickDecode(): void { const existingElements = document.getElementsByClassName( "evenbetter__qd-body", ); Array.from(existingElements).forEach((element) => { element.remove(); }); } private attachQuickDecode(): void { this.removeExistingQuickDecode(); const sessionListBody = document.querySelector(".size-full.flex.flex-col .size-full.flex.flex-col"); if (!sessionListBody) return; this.quickDecode = new QuickDecode(); sessionListBody.appendChild(this.quickDecode.getElement()); } public init(): void { const MAX_ATTEMPTS = 80; const INTERVAL_DELAY = 25; const attach = (): void => { if (this.isCleaned) return; let attemptCount = 0; const interval = setInterval(() => { if (this.isCleaned) { clearInterval(interval); return; } attemptCount++; if (attemptCount > MAX_ATTEMPTS) { console.error("[EvenBetter QuickDecode] Could not find editors"); clearInterval(interval); return; } const editors = document.querySelectorAll(".cm-editor .cm-content"); if (!editors.length) return; clearInterval(interval); this.attachQuickDecode(); }, INTERVAL_DELAY); }; this.pageOpenListener = (newHash: string) => { if (this.isCleaned) return; if (newHash === "#/replay") { this.cleanup(false); attach(); } else { this.cleanup(false); } }; this.projectChangeListener = async () => { if (this.isCleaned) return; this.cleanup(false); await new Promise((resolve) => setTimeout(resolve, 500)); if (window.location.hash === "#/replay") attach(); }; this.sdk.backend.onEvent( "caido:project-change", this.projectChangeListener, ); this.cleanupListener = onLocationChange((data) => { this.pageOpenListener?.(data.newHash); }); } public cleanup(fullCleanup: boolean = true): void { if (this.quickDecode) { this.quickDecode.cleanup(); this.quickDecode = undefined; } this.removeExistingQuickDecode(); if (fullCleanup) { if (this.cleanupListener) { this.cleanupListener(); this.cleanupListener = undefined; } if (this.projectChangeListener) { this.projectChangeListener = undefined; } if (this.pageOpenListener) { this.pageOpenListener = undefined; } this.isCleaned = true; } } } let manager: QuickDecodeManager | undefined = undefined; export const quickDecode = createFeature("quick-decode", { onFlagEnabled: (sdk: FrontendSDK) => { if (!manager) { manager = new QuickDecodeManager(sdk); manager.init(); } }, onFlagDisabled: (sdk: FrontendSDK) => { if (manager) { manager.cleanup(); manager = undefined; } }, }); ================================================ FILE: packages/frontend/src/features/quick-decode/quick-decode.css ================================================ .evenbetter__qd-body { max-height: 40vh; height: 160px; background-color: var(--c-bg-default); word-wrap: break-word; flex-direction: column; overflow: hidden; position: relative; } #evenbetter__qd-resizer { cursor: ns-resize; position: absolute; height: 10px; width: 100%; } .evenbetter__qd-selected-text-box { white-space: pre-wrap; padding: 0.4em; background: var(--c-bg-default); flex: 1; resize: none; font-size: 14px; } .evenbetter__qd-selected-text-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5em; } .evenbetter__qd-selected-text-top i { cursor: pointer; transition: 0.2s ease transform; } .evenbetter__qd-selected-text-top i:active { transform: scale(0.9); } .evenbetter__qd-selected-text-top-select { background-color: var(--c-bg-default); } .evenbetter__qd-selected-text { padding: 0.6em; background: var(--c-bg-subtle); height: 100%; flex: 1; display: flex; flex-direction: column; } ================================================ FILE: packages/frontend/src/features/quick-mar/index.ts ================================================ import { createFeature } from "@/features/manager"; import { type FrontendSDK } from "@/types"; const init = (sdk: FrontendSDK) => { sdk.commands.register("evenbetter:quickmar", { name: "Send to Match & Replace", run: (context) => { if ( context.type === "RequestContext" || context.type === "ResponseContext" ) { const selection = context.selection; if (selection === "") { sdk.window.showToast("No selection", { variant: "warning", }); return; } const type = context.type === "RequestContext" ? "request" : "response"; sendToMatchAndReplace(selection, sdk, type); } }, }); sdk.menu.registerItem({ commandId: "evenbetter:quickmar", leadingIcon: "fas fa-wrench", type: "Request", }); sdk.menu.registerItem({ commandId: "evenbetter:quickmar", leadingIcon: "fas fa-wrench", type: "Response", }); }; const sendToMatchAndReplace = async ( selection: string, sdk: FrontendSDK, type: "request" | "response", ) => { if (!selection) return; sdk.navigation.goTo("/tamper"); const collections = sdk.matchReplace.getCollections(); let collectionID: string; if (collections.length === 0) { const newCollection = await sdk.matchReplace.createCollection({ name: "EvenBetter Collection", }); collectionID = newCollection.id; } else { const firstCollection = collections[0]; if (!firstCollection) return; collectionID = firstCollection.id; } let name = selection; if (selection.length > 30) { name = selection.substring(0, 30) + "..."; } sdk.matchReplace .createRule({ name, query: "", section: { kind: type === "request" ? "SectionRequestBody" : "SectionResponseBody", operation: { kind: "OperationBodyRaw", matcher: { kind: "MatcherRawValue", value: selection, }, replacer: { kind: "ReplacerTerm", term: "", }, }, }, collectionId: collectionID, }) .catch((err) => { console.error(err); sdk.window.showToast("Error occured while creating M&R rule.", { variant: "error", }); }); }; export const quickMatchAndReplace = createFeature("quick-mar", { onFlagEnabled: (sdk: FrontendSDK) => { init(sdk); }, onFlagDisabled: (sdk: FrontendSDK) => { location.reload(); }, }); ================================================ FILE: packages/frontend/src/features/share-filters/index.ts ================================================ import { onLocationChange } from "@/dom"; import { createFeature } from "@/features/manager"; import { type FrontendSDK } from "@/types"; import { downloadFile, importFile } from "@/utils/file-utils"; let filterTabObserver: MutationObserver | undefined = undefined; let cancelListener: () => void; let filterButtons: HTMLElement[] = []; export const shareFilters = createFeature("share-filters", { onFlagEnabled: (sdk: FrontendSDK) => { cancelListener = onLocationChange((data) => { cleanupFilterElements(); if (data.newHash === "#/filter") { addImportButton(sdk); observeFilterTab(sdk); } }); }, onFlagDisabled: () => { cleanupFilterElements(); if (cancelListener) { cancelListener(); } }, }); const cleanupFilterElements = () => { if (filterTabObserver) { filterTabObserver.disconnect(); filterTabObserver = undefined; } filterButtons.forEach((b) => b.remove()); filterButtons = []; }; const addImportButton = (sdk: FrontendSDK) => { const topbarLeft = document.querySelector( ".c-topbar .c-topbar__left", ) as HTMLElement; if (topbarLeft === null || document.querySelector("#filter-presets-import")) return; const importButton = sdk.ui.button({ label: "Import", leadingIcon: "fas fa-file-upload", variant: "tertiary", size: "small", }); importButton.id = "filter-presets-import"; importButton.addEventListener("click", () => { importFile(".json", (content: string) => { try { const data = JSON.parse(content); sdk.graphql .createFilterPreset({ input: { alias: data.alias, clause: data.clause, name: data.name, }, }) .then(() => { sdk.window.showToast("Filter preset imported successfully", { duration: 3000, variant: "success", }); }) .catch((error) => { sdk.window.showToast( `Failed to import filter preset: ${error.message}`, { duration: 3000, variant: "error", }, ); }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); sdk.window.showToast( `Failed to import filter preset: ${errorMessage}`, { duration: 3000, variant: "error", }, ); } }); }); filterButtons.push(importButton); topbarLeft.appendChild(importButton); }; const observeFilterTab = (sdk: FrontendSDK) => { const formBody = document.querySelector(".c-form-body__actions"); if (formBody !== null) { attachDownloadButton(sdk); } const filterContainer = document.querySelector(".c-filter"); if (filterTabObserver) { filterTabObserver.disconnect(); filterTabObserver = undefined; } filterTabObserver = new MutationObserver((mutations) => { if (mutations.every((m) => m.attributeName === "style")) return; if (!document.querySelector("#filter-presets-download")) { attachDownloadButton(sdk); } }); if (formBody) { filterTabObserver.observe(formBody, { childList: true, attributes: true, subtree: true, }); } if (filterContainer) { filterTabObserver.observe(filterContainer, { childList: true, attributes: true, subtree: true, }); } }; const attachDownloadButton = (sdk: FrontendSDK) => { if (document.querySelector("#filter-presets-download")) return; const formActions = document.querySelector(".c-form-body__actions"); if (!formActions) return; const downloadButton = sdk.ui.button({ label: "Download", leadingIcon: "fas fa-file-arrow-down", variant: "tertiary", size: "small", }); filterButtons.push(downloadButton); downloadButton.id = "filter-presets-download"; const button = downloadButton.querySelector("button"); if (!button) return; button.addEventListener("click", () => { const id = getActiveFilterPreset(); if (id === null || id === undefined) { sdk.window.showToast("No filter preset selected", { duration: 3000, variant: "error", }); return; } sdk.graphql .filterPresets() .then((response) => { const presets = response.filterPresets; const preset = presets.find((p) => p.id === id); if (preset === undefined) { sdk.window.showToast("Filter preset not found", { duration: 3000, variant: "error", }); return; } const presetData = { id: preset.id, alias: preset.alias, name: preset.name, clause: preset.clause, }; downloadFile(`filter-${preset.alias}.json`, JSON.stringify(presetData)); sdk.window.showToast("Filter preset downloaded successfully", { duration: 3000, variant: "success", }); }) .catch((error) => { sdk.window.showToast( `Failed to download filter preset: ${error.message}`, { duration: 3000, variant: "error", }, ); }); }); formActions.appendChild(downloadButton); }; const getActiveFilterPreset = () => { return document .querySelector(`.c-preset[data-is-selected="true"]`) ?.getAttribute("data-preset-id"); }; ================================================ FILE: packages/frontend/src/features/share-replay-collections/index.ts ================================================ import { type RequestRawInput } from "@caido/sdk-frontend/src/types/__generated__/graphql-sdk"; import { onLocationChange } from "@/dom"; import { createFeature } from "@/features/manager"; import { type FrontendSDK } from "@/types"; import { downloadFile, importFile } from "@/utils/file-utils"; const shareReplayCollectionsElements: HTMLElement[] = []; let mutationObserver: MutationObserver | undefined = undefined; const cancelFunctions: (() => void)[] = []; export const shareReplayCollections = createFeature( "share-replay-collections", { onFlagEnabled: (sdk: FrontendSDK) => { collectionsShare(sdk); }, onFlagDisabled: (sdk: FrontendSDK) => { shareReplayCollectionsElements.forEach((element) => { element.remove(); }); cancelFunctions.forEach((cancelFunction) => cancelFunction()); if (mutationObserver) { mutationObserver.disconnect(); mutationObserver = undefined; } }, }, ); const collectionsShare = (sdk: FrontendSDK) => { const { stop: stopProjectChange } = sdk.backend.onEvent( "caido:project-change", () => { if (window.location.hash === "#/replay") { attachImportButton(sdk); attachExportButton(sdk); } }, ); const stopPageOpen = onLocationChange((data) => { if (data.newHash === "#/replay") { attachImportButton(sdk); attachExportButton(sdk); if (mutationObserver) mutationObserver.disconnect(); mutationObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.addedNodes.length > 0) { attachExportButton(sdk); } }); }); const tree = document.querySelector(".c-session-list-body__tree .c-tree"); if (!tree) return; mutationObserver.observe(tree, { childList: true, subtree: true, }); } }); cancelFunctions.push(stopProjectChange, stopPageOpen); }; const getCollectionByID = async (collectionID: string, sdk: FrontendSDK) => { return await sdk.graphql.replaySessionCollections().then((data) => { const collections = data.replaySessionCollections.edges; return collections.find( (collection) => collection.node.id === collectionID, ); }); }; const createSession = async ( collectionID: string, request: RequestRawInput, sdk: FrontendSDK, ) => { return await sdk.graphql.createReplaySession({ input: { collectionId: collectionID, requestSource: { raw: request, }, }, }); }; const createCollection = async (collectionName: string, sdk: FrontendSDK) => { return await sdk.graphql.createReplaySessionCollection({ input: { name: collectionName, }, }); }; const downloadCollection = async (collectionID: string, sdk: FrontendSDK) => { const collection = await getCollectionByID(collectionID, sdk); if (!collection) return new Error("Collection not found"); const replayEntries = []; const sessions = collection.node.sessions; if (sessions && sessions.length > 0) { for (const session of sessions) { const entryID = session.activeEntry?.id; if (!entryID) continue; const replayEntry = await sdk.graphql.replayEntry({ id: entryID, }); replayEntries.push({ ...replayEntry.replayEntry, name: session.name }); } } const collectionExport = { name: collection.node.name, replayEntries: replayEntries, }; const collectionName = collection.node.name.replaceAll(" ", "_"); downloadFile( "collection_" + collectionName + ".json", JSON.stringify(collectionExport), ); sdk.window.showToast("Collection downloaded successfully!", { duration: 3000, variant: "success", }); }; const importCollection = async (collection: any, sdk: FrontendSDK) => { const collectionName = collection.name; const newCollection = await createCollection(collectionName, sdk); const newCollectionID = newCollection.createReplaySessionCollection.collection?.id; if (!newCollectionID) return; const replayEntries = collection.replayEntries; if (replayEntries && replayEntries.length > 0) { for (const replayEntry of replayEntries) { const requestRawInput: RequestRawInput = { connectionInfo: { host: replayEntry.connection.host, port: replayEntry.connection.port, isTLS: replayEntry.connection.isTLS, }, raw: replayEntry.raw, }; const newSession = await createSession( newCollectionID, requestRawInput, sdk, ); const sesionID = newSession.createReplaySession.session?.id; if (!sesionID) return; await sdk.graphql.renameReplaySession({ id: sesionID, name: replayEntry.name, }); } } sdk.window.showToast("Collection imported successfully!", { duration: 3000, variant: "success", }); return newCollectionID; }; const attachImportButton = (sdk: FrontendSDK) => { if (document.querySelector("#import-collection")) return; const topbarLeft = document.querySelector(".c-topbar__left"); if (!topbarLeft) return; const importButton = sdk.ui.button({ label: "Import Collection", variant: "tertiary", size: "small", leadingIcon: "fas fa-file-import", }); shareReplayCollectionsElements.push(importButton); importButton.id = "import-collection"; importButton.style.float = "left"; importButton.style.marginRight = "1em"; importButton.addEventListener("click", async () => { importFile(".json", async (content: string) => { try { const collection = JSON.parse(content); await importCollection(collection, sdk); } catch (error) { console.error("Failed to import collection:", error); sdk.window.showToast("Failed to import collection", { duration: 3000, variant: "error", }); } }); }); topbarLeft.prepend(importButton); }; const attachExportButton = (sdk: FrontendSDK) => { const collections = document.querySelectorAll(".c-tree-collection"); if (!collections || collections.length === 0) return; collections.forEach((collection) => { if (collection.querySelector("#download-collection")) return; const actions = collection.querySelector(".c-tree-collection__actions"); if (!actions) return; const newElement = actions.childNodes[0]?.cloneNode(true) as HTMLElement; if (!newElement) return; shareReplayCollectionsElements.push(newElement); const icon = newElement.querySelector("i"); if (!icon) return; newElement.id = "download-collection"; icon.classList.value = "c-icon fas fa-file-arrow-down"; newElement.addEventListener("click", async () => { const collectionID = collection.getAttribute("data-collection-id"); if (!collectionID) return; const err = await downloadCollection(collectionID, sdk); if (err) { sdk.window.showToast("Failed to download collection: " + err, { duration: 3000, variant: "error", }); } }); actions.prepend(newElement); }); }; ================================================ FILE: packages/frontend/src/features/share-scope/index.ts ================================================ import { onLocationChange } from "@/dom"; import { createFeature } from "@/features/manager"; import { type FrontendSDK } from "@/types"; import { downloadFile, importFile } from "@/utils/file-utils"; let scopeTabObserver: MutationObserver | undefined = undefined; let cancelListener: () => void; let scopeButtons: HTMLElement[] = []; export const shareScope = createFeature("share-scope", { onFlagEnabled: (sdk: FrontendSDK) => { cancelListener = onLocationChange((data) => { if (data.newHash === "#/scope") { observeScopeTab(sdk); attachDownloadButton(sdk); addImportButton(sdk); } else { if (scopeTabObserver !== undefined) { scopeTabObserver.disconnect(); scopeTabObserver = undefined; } } }); }, onFlagDisabled: () => { if (scopeTabObserver !== undefined) { scopeTabObserver.disconnect(); scopeTabObserver = undefined; } if (cancelListener !== undefined) { cancelListener(); } scopeButtons.forEach((b) => b.remove()); scopeButtons = []; }, }); const addImportButton = (sdk: FrontendSDK) => { const topbarLeft = document.querySelector( ".c-topbar .c-topbar__left", ) as HTMLElement; if ( topbarLeft === null || document.querySelector("#scope-presents-import") !== null ) return; const importButton = sdk.ui.button({ label: "Import", leadingIcon: "fas fa-file-upload", variant: "tertiary", size: "small", }); importButton.id = "scope-presents-import"; importButton.addEventListener("click", () => { importFile(".json", (content: string) => { const data = JSON.parse(content); sdk.scopes.createScope({ name: data.name, allowlist: data.allowlist, denylist: data.denylist, }); }); }); scopeButtons.push(importButton); setTimeout(() => { topbarLeft.appendChild(importButton); }, 0); }; const observeScopeTab = (sdk: FrontendSDK) => { const presetForm = document.querySelector( ".c-preset-form-create", )?.parentElement; if (presetForm === null || presetForm === undefined) return; if (scopeTabObserver !== undefined) { scopeTabObserver.disconnect(); scopeTabObserver = undefined; } scopeTabObserver = new MutationObserver((m) => { if ( m.some( (m) => m.attributeName === "style" || (m.target as HTMLElement).classList.contains( "c-preset-form-create__header", ), ) ) return; attachDownloadButton(sdk); }); scopeTabObserver.observe(presetForm, { childList: true, attributes: true, subtree: true, }); }; const attachDownloadButton = (sdk: FrontendSDK) => { document.querySelector("#scope-presents-download")?.remove(); const presetCreateHeader = document.querySelector( ".c-preset-form-create__header", ); const downloadButton = sdk.ui.button({ label: "Download", leadingIcon: "fas fa-file-arrow-down", variant: "tertiary", size: "small", }); scopeButtons.push(downloadButton); downloadButton.id = "scope-presents-download"; const button = downloadButton.querySelector("button"); if (button === null) return; button.addEventListener("click", () => { const id = getActiveScopePreset(); if (id === undefined) return; const scopes = sdk.scopes.getScopes(); const scope = scopes.find((s) => s.id === id); if (scope === undefined) return; downloadFile("scope-" + scope.name + ".json", JSON.stringify(scope)); sdk.window.showToast("Scope preset downloaded successfully", { duration: 3000, variant: "success", }); }); presetCreateHeader?.appendChild(downloadButton); }; const getActiveScopePreset = () => { return document .querySelector(`.c-preset[data-is-selected="true"]`) ?.getAttribute("data-preset-id"); }; ================================================ FILE: packages/frontend/src/fonts/index.ts ================================================ import { getFontUrl } from "shared"; import { type FrontendSDK } from "@/types"; function loadFont(font: string, fontUrl: string) { const customFontElement = document.getElementById("eb-custom-font"); if (customFontElement) { document.head.removeChild(customFontElement); } const customFontStyleElement = document.getElementById( "eb-custom-font-style", ); if (customFontStyleElement) { document.head.removeChild(customFontStyleElement); } if (font === "Default") return; const link = document.createElement("link"); link.href = fontUrl; link.rel = "stylesheet"; link.id = "eb-custom-font"; const style = document.createElement("style"); style.id = "eb-custom-font-style"; style.textContent = `body { font-family: ${font} !important; }`; document.head.appendChild(link); document.head.appendChild(style); } export async function initFontLoader(sdk: FrontendSDK) { sdk.backend.onEvent("font:load", loadFont); const settings = await sdk.backend.getSettings(); if (settings.kind === "Error") return; const customFont = settings.value.customFont; if (customFont) { loadFont(customFont, getFontUrl(customFont)); } } ================================================ FILE: packages/frontend/src/index.ts ================================================ import { Classic } from "@caido/primevue"; import { createPinia } from "pinia"; import PrimeVue from "primevue/config"; import Tooltip from "primevue/tooltip"; import { createApp } from "vue"; import "./features"; import { SDKPlugin } from "./plugins/sdk"; import "./styles/index.css"; import type { FrontendSDK } from "./types"; import App from "./views/App.vue"; import { initDOMManager } from "@/dom"; import { PiniaColada } from "@pinia/colada"; import { initialize } from "@/features/manager"; import { initFontLoader } from "@/fonts"; export const init = (sdk: FrontendSDK) => { initDOMManager(); initialize(sdk); initFontLoader(sdk); const app = createApp(App); const pinia = createPinia(); app.use(pinia); app.use(PiniaColada); app.use(PrimeVue, { unstyled: true, pt: Classic, }); app.directive("tooltip", Tooltip); app.use(SDKPlugin, sdk); const root = document.createElement("div"); Object.assign(root.style, { height: "100%", width: "100%", }); root.id = `plugin--evenbetter`; app.mount(root); sdk.navigation.addPage("/evenbetter", { body: root, }); sdk.sidebar.registerItem("EvenBetter", "/evenbetter", { icon: "fas fa-rocket", }); }; ================================================ FILE: packages/frontend/src/plugins/sdk.ts ================================================ import { inject, type InjectionKey, type Plugin } from "vue"; import { type FrontendSDK } from "@/types"; const KEY: InjectionKey = Symbol("FrontendSDK"); // This is the plugin that will provide the FrontendSDK to VueJS // To access the frontend SDK from within a component, use the `useSDK` function. export const SDKPlugin: Plugin = (app, sdk: FrontendSDK) => { app.provide(KEY, sdk); }; // This is the function that will be used to access the FrontendSDK from within a component. export const useSDK = () => { return inject(KEY) as FrontendSDK; }; ================================================ FILE: packages/frontend/src/queries/flags.ts ================================================ import { useQuery } from "@pinia/colada"; import { type FeatureFlag, type FeatureFlagTag } from "shared"; import { computed, ref } from "vue"; import { useSDK } from "@/plugins/sdk"; const FLAGS_KEY = ["flags"] as const; export const useFlagsQuery = () => { const sdk = useSDK(); return useQuery({ key: FLAGS_KEY, query: async () => { const res = await sdk.backend.getFlags(); if (res.kind === "Error") { throw new Error(res.error); } return res.value; }, }); }; export const useSetFlag = () => { const sdk = useSDK(); const { refetch } = useFlagsQuery(); const isPending = ref(false); const setFlag = async (payload: { flag: FeatureFlagTag; value: boolean }) => { isPending.value = true; const res = await sdk.backend.setFlag(payload.flag, payload.value); if (res.kind === "Error") { isPending.value = false; throw new Error(res.error); } await refetch(); isPending.value = false; }; return { setFlag, isPending: computed(() => isPending.value), }; }; ================================================ FILE: packages/frontend/src/queries/settings.ts ================================================ import { useQuery } from "@pinia/colada"; import { type Result, type SettingKey, type Settings, type SettingValue, } from "shared"; import { computed, ref } from "vue"; import { useSDK } from "@/plugins/sdk"; const SETTINGS_KEY = ["settings"] as const; export const useSettingsQuery = () => { const sdk = useSDK(); return useQuery({ key: SETTINGS_KEY, query: async () => { const res = await sdk.backend.getSettings(); if (res.kind === "Error") { throw new Error(res.error); } return res.value; }, }); }; export const useUpdateSetting = () => { const sdk = useSDK(); const { refetch } = useSettingsQuery(); const isPending = ref(false); const updateSetting = async (payload: { key: K; value: SettingValue; }) => { isPending.value = true; const res = await sdk.backend.updateSetting(payload.key, payload.value); if (res.kind === "Error") { isPending.value = false; throw new Error(res.error); } await refetch(); isPending.value = false; }; return { updateSetting, isPending: computed(() => isPending.value), }; }; ================================================ FILE: packages/frontend/src/styles/index.css ================================================ @import "tailwindcss/base"; @import "tailwindcss/components"; @import "tailwindcss/utilities"; * { margin: 0; padding: 0; border: 0; box-sizing: border-box; } ================================================ FILE: packages/frontend/src/types.ts ================================================ import { type Caido } from "@caido/sdk-frontend"; import { type API, type BackendEvents } from "backend"; export type FrontendSDK = Caido; ================================================ FILE: packages/frontend/src/utils/file-utils.ts ================================================ export const downloadFile = (name: string, content: string) => { const a = document.createElement("a"); a.href = URL.createObjectURL( new Blob([content], { type: "application/json" }), ); a.download = name; document.body.appendChild(a); a.click(); document.body.removeChild(a); }; export const importFile = ( accept: string, onFileRead: (content: string) => void, ) => { const input = document.createElement("input"); input.type = "file"; input.accept = accept; input.style.display = "none"; input.addEventListener("change", (event) => { const target = event.target as HTMLInputElement; if (!target.files || !target.files.length) return; const file = target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { const target = e.target as FileReader; const content = target.result as string; onFileRead(content); }; reader.readAsText(file); }); document.body.prepend(input); input.click(); input.remove(); }; ================================================ FILE: packages/frontend/src/views/App.vue ================================================ ================================================ FILE: packages/frontend/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "lib": ["DOM", "ESNext"], "types": ["@caido/sdk-backend"], "baseUrl": ".", "paths": { "@/*": ["src/*"] } }, "include": ["./src/**/*.ts", "./src/**/*.vue"] } ================================================ FILE: packages/shared/package.json ================================================ { "name": "shared", "version": "0.0.0", "description": "Shared types", "author": "bebiks", "license": "CC0-1.0", "type": "module", "types": "src/index.ts", "main": "src/index.ts", "scripts": { "typecheck": "tsc --noEmit" }, "dependencies": { } } ================================================ FILE: packages/shared/src/flags.ts ================================================ export type FeatureFlagTag = | "exclude-host-path" | "backend-test" | "quick-decode" | "clear-all-findings" | "share-scope" | "share-replay-collections" | "colorize-by-method" | "share-filters" | "quick-mar" | "common-filters" | "command-palette-workflows"; export type FeatureFlagKind = "backend" | "frontend"; export type FeatureFlag = { tag: FeatureFlagTag; description: string; enabled: boolean; kind: FeatureFlagKind; knownIssues?: string[]; requiresReload?: boolean; }; ================================================ FILE: packages/shared/src/fonts.ts ================================================ const fonts = [ { name: "Default", url: "" }, { name: "JetBrains Mono", url: "https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap", }, { name: "Fira Code", url: "https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700&display=swap", }, { name: "Roboto Mono", url: "https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap", }, { name: "Inconsolata", url: "https://fonts.googleapis.com/css2?family=Inconsolata:wght@200..700&display=swap", }, ]; export function getFontUrl(font: string) { const fontOption = fonts.find((f) => f.name === font); return fontOption ? fontOption.url : ""; } ================================================ FILE: packages/shared/src/index.ts ================================================ export * from "./result"; export * from "./flags"; export * from "./settings"; export * from "./fonts"; ================================================ FILE: packages/shared/src/result.ts ================================================ export type Result = | { kind: "Error"; error: string } | { kind: "Success"; value: T }; export function ok(value: T): Result { return { kind: "Success", value }; } export function error(error: string): Result { return { kind: "Error", error }; } ================================================ FILE: packages/shared/src/settings.ts ================================================ export type Settings = { customFont: string; }; export type SettingKey = keyof Settings; export type SettingValue = Settings[K]; export const DEFAULT_SETTINGS: Settings = { customFont: "Default", }; ================================================ FILE: packages/shared/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["./src/**/*.ts"] } ================================================ FILE: pnpm-workspace.yaml ================================================ packages: - packages/* onlyBuiltDependencies: - esbuild ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "esnext", "module": "esnext", "lib": ["ESNext"], "jsx": "preserve", "noImplicitAny": true, "noUncheckedIndexedAccess": true, "strict": true, "skipLibCheck": true, "resolveJsonModule": true, "moduleResolution": "bundler", "esModuleInterop": true, "sourceMap": true, "noUnusedLocals": true, "useDefineForClassFields": true, "isolatedModules": true, "baseUrl": "." } }