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<API>) {
// 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<never, BackendEvents>;
```
### 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<T> =
| { kind: "Error"; error: string }
| { kind: "Ok"; value: T };
// Backend API function returning Result
function processData(sdk: SDK, input: string): Result<ProcessedData> {
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<Data> {
// Implementation
}
function saveData(sdk: SDK, data: Data): Result<void> {
// Implementation
}
function deleteData(sdk: SDK, id: string): Result<boolean> {
// 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<API>) {
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<API, BackendEvents>;
// 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<string, never>, Record<string, never>>;
```
#### For plugins WITH backend:
```typescript
import { Caido } from "@caido/sdk-frontend";
import { API, BackendEvents } from "backend";
export type FrontendSDK = Caido<API, BackendEvents>;
```
### 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<string, Array<string>>;
getHeader(name: string): Array<string> | undefined;
getBody(): Body | undefined;
getRaw(): RequestRaw;
getCreatedAt(): Date;
toSpec(): RequestSpec;
toSpecRaw(): RequestSpecRaw;
};
export type Response = {
getId(): ID;
getCode(): number;
getHeaders(): Record<string, Array<string>>;
getHeader(name: string): Array<string> | 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:
```
<Card
class="h-full"
:pt="{
body: { class: 'h-full p-0' },
content: { class: 'h-full flex flex-col' },
}"
>
```
### 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 [](https://twitter.com/bebiksior) [](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:
<!-- sponsors --><a href="https://github.com/CRITSoftware"><img src="https://github.com/CRITSoftware.png" width="60px" alt="User avatar: CRIT Software" /></a><!-- sponsors -->
## 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 <html> 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<FeatureFlag>,
): Result<FeatureFlag[]> => {
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<Result<void>> => {
const flagsStore = FeatureFlagsStore.get();
await flagsStore.setFlags(flags);
return ok(undefined);
};
export const getFlag = (
_: BackendSDK,
tag: FeatureFlagTag,
): Result<boolean> => {
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<Result<void>> => {
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<Settings> => {
const settingsStore = SettingsStore.get();
return ok(settingsStore.getSettings());
};
export const updateSetting = <K extends SettingKey>(
_: BackendSDK,
key: K,
value: SettingValue<K>,
): Result<void> => {
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<FeatureFlagTag, FeatureHandlers>();
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<API, BackendEvents>) {
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<FeatureFlagsStore> {
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<string> {
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<void> {
this.flags = flags;
await this.saveFlagsToFile(flags);
}
async setFlag(tag: FeatureFlagTag, enabled: boolean): Promise<void> {
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<string> {
const settingsPath = getSettingsPath(this.sdk);
await writeFile(settingsPath, JSON.stringify(settings, null, 2));
return settingsPath;
}
static async initialize(sdk: BackendSDK): Promise<SettingsStore> {
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<K extends SettingKey>(key: K, value: SettingValue<K>): 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<never, BackendEvents>;
================================================
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<boolean> {
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<boolean> {
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
================================================
<script setup lang="ts">
import Button from "primevue/button";
import Card from "primevue/card";
import Column from "primevue/column";
import DataTable from "primevue/datatable";
import Dialog from "primevue/dialog";
import ToggleSwitch from "primevue/toggleswitch";
import { useForm } from "./useForm";
const {
flags,
isLoading,
dialogOpen,
handleDialogClose,
handleFlagChange,
confirmFlagChange,
hasKnownIssues,
} = useForm();
</script>
<template>
<Card
class="h-full"
:pt="{
body: { class: 'h-full p-0' },
content: { class: 'h-full flex flex-col p-0' },
}"
>
<template #content>
<div class="p-4 text-base font-bold">Features</div>
<div class="overflow-y-auto">
<DataTable
:value="flags || []"
:loading="isLoading"
striped-rows
size="small"
>
<Column field="tag" header="Name">
<template #body="{ data }">
<span>{{ data.tag }}</span>
<span
v-if="hasKnownIssues(data.knownIssues)"
v-tooltip.top="
'Known issues: \n' + data.knownIssues?.join('\n')
"
class="ml-2"
>
<i class="fas fa-info-circle text-surface-300"></i>
</span>
</template>
</Column>
<Column field="description" header="Description">
<template #body="{ data }">
<span class="text-surface-200">{{ data.description }}</span>
</template>
</Column>
<Column field="kind" header="Kind" />
<Column header="Requires Refresh?">
<template #body="{ data }">
{{ data.requiresReload ? "Yes" : "No" }}
</template>
</Column>
<Column header="Enabled">
<template #body="{ data }">
<ToggleSwitch
:model-value="data.enabled"
@update:model-value="() => handleFlagChange(data)"
/>
</template>
</Column>
</DataTable>
<Dialog
v-model:visible="dialogOpen"
header="Confirm Flag Change"
modal
:closable="false"
>
<p>
Disabling this flag will require a page reload. Are you sure you
want to change it?
</p>
<template #footer>
<Button label="Cancel" @click="handleDialogClose" />
<Button label="Confirm" @click="confirmFlagChange" />
</template>
</Dialog>
</div>
</template>
</Card>
</template>
================================================
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<FeatureFlagTag | undefined>(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
================================================
<script setup lang="ts">
import Avatar from "primevue/avatar";
import Button from "primevue/button";
import Card from "primevue/card";
import Divider from "primevue/divider";
import Select from "primevue/select";
import { useForm } from "./useForm";
const {
isLoading,
error,
isPending,
fontOptions,
localSettings,
hasChanges,
handleSave,
openLink,
} = useForm();
</script>
<template>
<div class="h-full flex flex-col gap-1">
<Card
:pt="{
body: { class: 'p-0' },
content: { class: 'flex flex-col' },
}"
>
<template #content>
<div class="p-4">
<div class="text-base font-bold">Settings</div>
<div v-if="isLoading" class="text-sm">Loading...</div>
<div v-else-if="error" class="text-sm text-red-400">Error</div>
<div v-else class="flex flex-col gap-3">
<div class="flex flex-col gap-2 mt-2">
<label class="text-sm text-surface-200" for="font-select"
>Custom Font</label
>
<Select
v-model="localSettings.customFont"
input-id="font-select"
:options="fontOptions"
:disabled="isPending"
class="w-full"
/>
</div>
<Button
label="Save Changes"
:disabled="!hasChanges || isPending"
@click="handleSave"
/>
</div>
</div>
</template>
</Card>
<Card
class="flex-1"
:pt="{
body: { class: 'h-full p-0' },
content: { class: 'h-full flex flex-col' },
}"
>
<template #content>
<div class="p-4 overflow-y-auto">
<div class="flex items-center justify-between mb-3">
<div class="text-base font-bold text-primary">About EvenBetter</div>
<Button
label="Star on GitHub"
icon="fas fa-star"
size="small"
@click="openLink('https://github.com/bebiksior/evenbetter')"
/>
</div>
<div class="flex flex-col gap-2">
<div class="text-sm">
<b>EvenBetter</b> is a collection of tweaks to make Caido even
better. You can find the source code on
<a
class="underline"
href="https://github.com/bebiksior/evenbetter"
target="_blank"
rel="noopener noreferrer"
>GitHub</a
>.
</div>
<div class="text-sm">
Feel free to contribute to the project :D You can also submit
feature requests and bugs via the GitHub issues page. I'm always
looking for new ideas and improvements!
</div>
<div class="text-sm">
Thanks for using this plugin. I hope it makes your Caido
experience better and more efficient.
</div>
</div>
<Divider />
<div class="flex flex-col gap-2">
<div class="text-xs text-surface-400">
Your feedback and suggestions are always welcome. My X profile is
<a
class="underline"
href="https://x.com/bebiksior"
target="_blank"
rel="noopener noreferrer"
>bebiksior</a
>
and my discord handle is <b>bebiks</b>
</div>
<div class="flex items-center gap-2">
<Avatar
image="https://avatars.githubusercontent.com/u/71410238?v=4&size=30"
size="normal"
shape="circle"
/>
<div class="text-xs">
Made with ❤️ by
<a
class="underline"
href="https://x.com/bebiksior"
target="_blank"
rel="noopener noreferrer"
>bebiks</a
>
</div>
</div>
</div>
</div>
</template>
</Card>
</div>
</template>
================================================
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<Callback>();
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<typeof original>) {
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<HTTPMethod> {
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<FeatureFlagTag, FeatureHandlers>();
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<void>) | 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<FrontendSDK> = 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<FeatureFlag[]>({
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<Settings>({
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 <K extends SettingKey>(payload: {
key: K;
value: SettingValue<K>;
}) => {
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<API, BackendEvents>;
================================================
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
================================================
<script setup lang="ts">
import Splitter from "primevue/splitter";
import SplitterPanel from "primevue/splitterpanel";
import { FlagsList } from "@/components/FlagsList";
import { Settings } from "@/components/Settings";
</script>
<template>
<div class="h-full flex flex-col">
<Splitter class="h-full">
<SplitterPanel :size="40" :min-size="20">
<Settings />
</SplitterPanel>
<SplitterPanel :size="60" :min-size="20">
<FlagsList />
</SplitterPanel>
</Splitter>
</div>
</template>
================================================
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<T> =
| { kind: "Error"; error: string }
| { kind: "Success"; value: T };
export function ok<T>(value: T): Result<T> {
return { kind: "Success", value };
}
export function error<T>(error: string): Result<T> {
return { kind: "Error", error };
}
================================================
FILE: packages/shared/src/settings.ts
================================================
export type Settings = {
customFont: string;
};
export type SettingKey = keyof Settings;
export type SettingValue<K extends SettingKey> = 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": "."
}
}
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
SYMBOL INDEX (110 symbols across 22 files)
FILE: packages/backend/src/features/manager.ts
type FeatureHandlers (line 22) | type FeatureHandlers = {
function createFeature (line 29) | function createFeature(tag: FeatureFlagTag, handlers: FeatureHandlers) {
function backendHandleFlagToggle (line 34) | function backendHandleFlagToggle(
function initializeFeatures (line 51) | function initializeFeatures(flags: FeatureFlag[], sdk: BackendSDK) {
FILE: packages/backend/src/index.ts
type API (line 11) | type API = DefineAPI<{
function init (line 20) | async function init(sdk: SDK<API, BackendEvents>) {
FILE: packages/backend/src/stores/flags.ts
type StoredFlag (line 13) | interface StoredFlag {
class FeatureFlagsStore (line 18) | class FeatureFlagsStore {
method constructor (line 23) | private constructor(sdk: BackendSDK) {
method initialize (line 108) | static async initialize(sdk: BackendSDK): Promise<FeatureFlagsStore> {
method readFlags (line 115) | public async readFlags() {
method saveFlagsToFile (line 142) | private async saveFlagsToFile(flags: FeatureFlag[]): Promise<string> {
method get (line 152) | static get(): FeatureFlagsStore {
method getFlags (line 160) | getFlags(): FeatureFlag[] {
method setFlags (line 164) | async setFlags(flags: FeatureFlag[]): Promise<void> {
method setFlag (line 169) | async setFlag(tag: FeatureFlagTag, enabled: boolean): Promise<void> {
method handleFlagToggle (line 182) | private handleFlagToggle(flag: FeatureFlag, enabled: boolean): void {
FILE: packages/backend/src/stores/settings.ts
class SettingsStore (line 15) | class SettingsStore {
method constructor (line 20) | private constructor(sdk: BackendSDK) {
method readSettings (line 25) | public async readSettings() {
method saveSettingsToFile (line 50) | private async saveSettingsToFile(settings: Settings): Promise<string> {
method initialize (line 56) | static async initialize(sdk: BackendSDK): Promise<SettingsStore> {
method get (line 67) | static get(): SettingsStore {
method getSettings (line 75) | getSettings(): Settings {
method updateSetting (line 79) | updateSetting<K extends SettingKey>(key: K, value: SettingValue<K>): v...
FILE: packages/backend/src/types.ts
type BackendEvents (line 4) | type BackendEvents = DefineEvents<{
type BackendSDK (line 9) | type BackendSDK = SDK<never, BackendEvents>;
FILE: packages/backend/src/utils/files.ts
function ensureDir (line 6) | async function ensureDir(
function getSettingsPath (line 19) | function getSettingsPath(sdk: BackendSDK): string {
function getFlagsPath (line 23) | function getFlagsPath(sdk: BackendSDK): string {
function exists (line 27) | async function exists(f: string): Promise<boolean> {
FILE: packages/frontend/src/dom/index.ts
type LocationChange (line 36) | type LocationChange = {
type Callback (line 41) | type Callback = (change: LocationChange) => void;
function notify (line 46) | function notify(): void {
function onLocationChange (line 72) | function onLocationChange(cb: Callback): () => void {
FILE: packages/frontend/src/features/clear-all-findings/index.ts
function cleanup (line 89) | function cleanup() {
FILE: packages/frontend/src/features/colorize-by-method/index.ts
function setTabMethodAttributes (line 11) | function setTabMethodAttributes(sdk: FrontendSDK) {
function updateCurrentTabHTTPMethod (line 31) | function updateCurrentTabHTTPMethod(newMethod: string) {
function updateSelectedTabColor (line 40) | function updateSelectedTabColor() {
function liveUpdateHTTPMethod (line 49) | function liveUpdateHTTPMethod() {
type HTTPMethod (line 62) | type HTTPMethod =
function getHTTPMethod (line 74) | async function getHTTPMethod(
function handleTabListChanges (line 85) | function handleTabListChanges(sdk: FrontendSDK) {
function setup (line 114) | function setup(sdk: FrontendSDK) {
FILE: packages/frontend/src/features/command-palette-workflows/index.ts
type Workflow (line 4) | interface Workflow {
FILE: packages/frontend/src/features/common-filters/index.ts
type TimeFilter (line 6) | interface TimeFilter {
constant TIME_FILTERS (line 11) | const TIME_FILTERS: TimeFilter[] = [
FILE: packages/frontend/src/features/manager.ts
type FeatureHandlers (line 9) | type FeatureHandlers = {
function createFeature (line 16) | function createFeature(tag: FeatureFlagTag, handlers: FeatureHandlers) {
function handleFlagToggle (line 21) | function handleFlagToggle(
function initializeFeatures (line 38) | function initializeFeatures(flags: FeatureFlag[], sdk: FrontendSDK) {
FILE: packages/frontend/src/features/quick-decode/index.ts
type CodeMirrorEditor (line 8) | interface CodeMirrorEditor {
type Selection (line 31) | interface Selection {
function unicodeEncode (line 37) | function unicodeEncode(str: string): string {
type HistoryEntry (line 47) | interface HistoryEntry {
class QuickDecode (line 53) | class QuickDecode {
method constructor (line 67) | constructor() {
method initializeHTMLElement (line 79) | private initializeHTMLElement(): void {
method initializeResizer (line 90) | private initializeResizer(): void {
method initializeSelectedTextDiv (line 122) | private initializeSelectedTextDiv(): void {
method initializeTextArea (line 133) | private initializeTextArea(): void {
method initializeEncodingMethodSelect (line 152) | private initializeEncodingMethodSelect(): void {
method initializeCopyIcon (line 188) | private initializeCopyIcon(): void {
method copyToClipboard (line 204) | private copyToClipboard(): void {
method handleKeyDown (line 211) | private handleKeyDown(e: KeyboardEvent): void {
method saveHistory (line 228) | private saveHistory(): void {
method undo (line 248) | private undo(): void {
method redo (line 272) | private redo(): void {
method handleInput (line 296) | private handleInput(): void {
method encodeContent (line 322) | private encodeContent(content: string): string {
method updateText (line 339) | public updateText(text: string): void {
method updateEncodeMethod (line 346) | public updateEncodeMethod(encodeMethod?: string): void {
method show (line 351) | public show(): void {
method hide (line 355) | public hide(): void {
method getElement (line 359) | public getElement(): HTMLDivElement {
method getActiveEditor (line 363) | private getActiveEditor(): CodeMirrorEditor | undefined {
method getCurrentSelection (line 373) | private getCurrentSelection(): Selection {
method startMonitoringSelection (line 387) | private startMonitoringSelection(): void {
method stopMonitoringSelection (line 404) | public stopMonitoringSelection(): void {
method isMouseOver (line 410) | private isMouseOver(element: HTMLElement): boolean {
method onSelectionChange (line 415) | private onSelectionChange(selection: Selection): void {
method showQuickDecode (line 432) | private showQuickDecode(text: string): void {
method setReadOnly (line 439) | private setReadOnly(readOnly: boolean): void {
method isUrlEncoded (line 447) | private isUrlEncoded(str: string): boolean {
method base64Decode (line 452) | private base64Decode(input: string): {
method tryToDecode (line 472) | private tryToDecode(input: string): {
method cleanup (line 520) | public cleanup(): void {
class QuickDecodeManager (line 532) | class QuickDecodeManager {
method constructor (line 540) | constructor(sdk: FrontendSDK) {
method removeExistingQuickDecode (line 544) | private removeExistingQuickDecode(): void {
method attachQuickDecode (line 553) | private attachQuickDecode(): void {
method init (line 563) | public init(): void {
method cleanup (line 620) | public cleanup(fullCleanup: boolean = true): void {
FILE: packages/frontend/src/fonts/index.ts
function loadFont (line 5) | function loadFont(font: string, fontUrl: string) {
function initFontLoader (line 33) | async function initFontLoader(sdk: FrontendSDK) {
FILE: packages/frontend/src/plugins/sdk.ts
constant KEY (line 5) | const KEY: InjectionKey<FrontendSDK> = Symbol("FrontendSDK");
FILE: packages/frontend/src/queries/flags.ts
constant FLAGS_KEY (line 7) | const FLAGS_KEY = ["flags"] as const;
FILE: packages/frontend/src/queries/settings.ts
constant SETTINGS_KEY (line 12) | const SETTINGS_KEY = ["settings"] as const;
FILE: packages/frontend/src/types.ts
type FrontendSDK (line 4) | type FrontendSDK = Caido<API, BackendEvents>;
FILE: packages/shared/src/flags.ts
type FeatureFlagTag (line 1) | type FeatureFlagTag =
type FeatureFlagKind (line 14) | type FeatureFlagKind = "backend" | "frontend";
type FeatureFlag (line 16) | type FeatureFlag = {
FILE: packages/shared/src/fonts.ts
function getFontUrl (line 21) | function getFontUrl(font: string) {
FILE: packages/shared/src/result.ts
type Result (line 1) | type Result<T> =
function ok (line 5) | function ok<T>(value: T): Result<T> {
function error (line 9) | function error<T>(error: string): Result<T> {
FILE: packages/shared/src/settings.ts
type Settings (line 1) | type Settings = {
type SettingKey (line 5) | type SettingKey = keyof Settings;
type SettingValue (line 6) | type SettingValue<K extends SettingKey> = Settings[K];
constant DEFAULT_SETTINGS (line 8) | const DEFAULT_SETTINGS: Settings = {
Condensed preview — 67 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (128K chars).
[
{
"path": ".cursor/rules/caido-backend.mdc",
"chars": 2894,
"preview": "---\nglobs:\n - \"**/packages/backend/**\"\nalwaysApply: true\ndescription: Caido Backend SDK Rules and Patterns\n---\n\n## Caid"
},
{
"path": ".cursor/rules/caido-frontend.mdc",
"chars": 3935,
"preview": "---\nglobs:\n - \"**/packages/frontend/**\"\nalwaysApply: true\ndescription: Caido Frontend SDK Rules and Patterns\n---\n\n## Ca"
},
{
"path": ".cursor/rules/caido.mdc",
"chars": 3276,
"preview": "---\nglobs:\nalwaysApply: true\ndescription: Caido HTTP Proxy Overview\n---\n\n## What is Caido\n\nCaido is a lightweight web ap"
},
{
"path": ".cursor/rules/linter.mdc",
"chars": 539,
"preview": "---\nglobs:\nalwaysApply: true\ndescription: Linter Guidelines\n---\n\n# Linter\n\nWe have a built-in ESLint linter configured a"
},
{
"path": ".cursor/rules/style.mdc",
"chars": 1743,
"preview": "---\nglobs: **/**.vue\nalwaysApply: false\n---\n## UI Style Guidelines\n\n### PrimeVue\n\n- Prefer to use PrimeVue compontents w"
},
{
"path": ".cursor/rules/typescript.mdc",
"chars": 475,
"preview": "---\nglobs:\nalwaysApply: true\ndescription: TypeScript Guidelines\n---\n# TypeScript Guidelines\n\n- Use TypeScript for all fi"
},
{
"path": ".github/workflows/release.yml",
"chars": 1607,
"preview": "name: 🚀 Release\n\non:\n workflow_dispatch:\n\nenv:\n NODE_VERSION: 20\n PNPM_VERSION: 9\n\njobs:\n release:\n name: Release"
},
{
"path": ".github/workflows/validate.yml",
"chars": 1293,
"preview": "name: Validate\n\non:\n push:\n branches:\n - 'main'\n workflow_call:\n\nconcurrency:\n group: validate-${{ github.ref"
},
{
"path": ".gitignore",
"chars": 28,
"preview": "node_modules\ndist\n.DS_Store\n"
},
{
"path": "LICENSE",
"chars": 1065,
"preview": "MIT License\n\nCopyright (c) 2024 [Author]\n\nPermission is hereby granted, free of charge, to any person obtaining a copy o"
},
{
"path": "README.md",
"chars": 2211,
"preview": "# EvenBetter [.Linter.Config } */\nexport default [\n "
},
{
"path": "package.json",
"chars": 536,
"preview": "{\n \"name\": \"evenbetter\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"scripts\": {\n \"typecheck\": \"pnpm -r typecheck\",\n"
},
{
"path": "packages/backend/package.json",
"chars": 238,
"preview": "{\n \"name\": \"backend\",\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"types\": \"src/index.ts\",\n \"scripts\": {\n \"typechec"
},
{
"path": "packages/backend/src/api/flags.ts",
"chars": 1318,
"preview": "import {\n error,\n type FeatureFlag,\n type FeatureFlagTag,\n ok,\n type Result,\n} from \"shared\";\n\nimport { FeatureFlag"
},
{
"path": "packages/backend/src/api/settings.ts",
"chars": 588,
"preview": "import {\n ok,\n type Result,\n type SettingKey,\n type Settings,\n type SettingValue,\n} from \"shared\";\n\nimport { Settin"
},
{
"path": "packages/backend/src/features/backend-test/index.ts",
"chars": 347,
"preview": "import { type BackendSDK } from \"../../types\";\nimport { createFeature } from \"../manager\";\n\nexport const backendTest = c"
},
{
"path": "packages/backend/src/features/index.ts",
"chars": 83,
"preview": "// This file is used to import all the backend features.\n\nimport \"./backend-test\";\n"
},
{
"path": "packages/backend/src/features/manager.ts",
"chars": 1741,
"preview": "import { type FeatureFlag, type FeatureFlagTag } from \"shared\";\n\nimport { type BackendSDK } from \"../types\";\n\n/**\n * Exa"
},
{
"path": "packages/backend/src/index.ts",
"chars": 1134,
"preview": "import { type DefineAPI, type SDK } from \"caido:plugin\";\n\nimport { getFlag, getFlags, setFlag, updateFlags } from \"./api"
},
{
"path": "packages/backend/src/stores/flags.ts",
"chars": 5588,
"preview": "import { readFile, writeFile } from \"fs/promises\";\nimport * as path from \"path\";\n\nimport { type FeatureFlag, type Featur"
},
{
"path": "packages/backend/src/stores/settings.ts",
"chars": 2334,
"preview": "import { readFile, writeFile } from \"fs/promises\";\nimport * as path from \"path\";\n\nimport {\n DEFAULT_SETTINGS,\n getFont"
},
{
"path": "packages/backend/src/types.ts",
"chars": 371,
"preview": "import { type DefineEvents, type SDK } from \"caido:plugin\";\nimport { type FeatureFlagTag } from \"shared\";\n\nexport type B"
},
{
"path": "packages/backend/src/utils/files.ts",
"chars": 735,
"preview": "import { mkdir, stat } from \"fs/promises\";\nimport path from \"path\";\n\nimport { type BackendSDK } from \"../types\";\n\nexport"
},
{
"path": "packages/backend/tsconfig.json",
"chars": 135,
"preview": "{\n \"extends\": \"../../tsconfig.json\",\n \"compilerOptions\": {\n \"types\": [\"@caido/sdk-backend\"]\n },\n \"include\": [\"./s"
},
{
"path": "packages/frontend/package.json",
"chars": 498,
"preview": "{\n \"name\": \"frontend\",\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"typecheck\": \"vue-tsc --noEmit\"\n }"
},
{
"path": "packages/frontend/src/components/FlagsList/Container.vue",
"chars": 2649,
"preview": "<script setup lang=\"ts\">\nimport Button from \"primevue/button\";\nimport Card from \"primevue/card\";\nimport Column from \"pri"
},
{
"path": "packages/frontend/src/components/FlagsList/index.ts",
"chars": 56,
"preview": "export { default as FlagsList } from \"./Container.vue\";\n"
},
{
"path": "packages/frontend/src/components/FlagsList/useForm.ts",
"chars": 1596,
"preview": "import { type FeatureFlag, type FeatureFlagTag } from \"shared\";\nimport { ref } from \"vue\";\n\nimport { useSDK } from \"@/pl"
},
{
"path": "packages/frontend/src/components/Settings/Container.vue",
"chars": 4159,
"preview": "<script setup lang=\"ts\">\nimport Avatar from \"primevue/avatar\";\nimport Button from \"primevue/button\";\nimport Card from \"p"
},
{
"path": "packages/frontend/src/components/Settings/index.ts",
"chars": 55,
"preview": "export { default as Settings } from \"./Container.vue\";\n"
},
{
"path": "packages/frontend/src/components/Settings/useForm.ts",
"chars": 1247,
"preview": "import { computed, ref, watch } from \"vue\";\n\nimport { useSettingsQuery, useUpdateSetting } from \"@/queries/settings\";\n\ne"
},
{
"path": "packages/frontend/src/dom/index.ts",
"chars": 1984,
"preview": "export const initDOMManager = () => {\n patchHistoryMethod(\"pushState\");\n patchHistoryMethod(\"replaceState\");\n\n window"
},
{
"path": "packages/frontend/src/features/clear-all-findings/index.ts",
"chars": 3006,
"preview": "import { onLocationChange } from \"@/dom\";\nimport { createFeature } from \"@/features/manager\";\nimport { type FrontendSDK "
},
{
"path": "packages/frontend/src/features/colorize-by-method/index.ts",
"chars": 3601,
"preview": "import { createFeature } from \"@/features/manager\";\nimport { type FrontendSDK } from \"@/types\";\n\nimport \"./style.css\";\n\n"
},
{
"path": "packages/frontend/src/features/colorize-by-method/style.css",
"chars": 511,
"preview": "[http-method=\"GET\"] {\n border-bottom: 2px solid #2196f3 !important;\n}\n\n[http-method=\"POST\"] {\n border-bottom: 2px soli"
},
{
"path": "packages/frontend/src/features/command-palette-workflows/index.ts",
"chars": 4618,
"preview": "import { createFeature } from \"@/features/manager\";\nimport { type FrontendSDK } from \"@/types\";\n\ninterface Workflow {\n "
},
{
"path": "packages/frontend/src/features/common-filters/index.ts",
"chars": 3203,
"preview": "import { createFeature } from \"@/features/manager\";\nimport { type FrontendSDK } from \"@/types\";\n\nlet intervalId: Timeout"
},
{
"path": "packages/frontend/src/features/exclude-host-path/index.ts",
"chars": 1982,
"preview": "import { createFeature } from \"@/features/manager\";\nimport { type FrontendSDK } from \"@/types\";\n\nconst excludeHostPathFu"
},
{
"path": "packages/frontend/src/features/index.ts",
"chars": 350,
"preview": "// This file is used to import all the frontend features.\n\nimport \"./quick-decode\";\nimport \"./clear-all-findings\";\nimpor"
},
{
"path": "packages/frontend/src/features/manager.ts",
"chars": 1843,
"preview": "// This is the features manager for the frontend.\n// It creates a map of feature tags and their functions for enable and"
},
{
"path": "packages/frontend/src/features/quick-decode/index.ts",
"chars": 18573,
"preview": "import { type FrontendSDK } from \"@/types\";\nimport { createFeature } from \"@/features/manager\";\n\nimport \"./quick-decode."
},
{
"path": "packages/frontend/src/features/quick-decode/quick-decode.css",
"chars": 1012,
"preview": ".evenbetter__qd-body {\n max-height: 40vh;\n height: 160px;\n\n background-color: var(--c-bg-default);\n word-wrap: break"
},
{
"path": "packages/frontend/src/features/quick-mar/index.ts",
"chars": 2499,
"preview": "import { createFeature } from \"@/features/manager\";\nimport { type FrontendSDK } from \"@/types\";\n\nconst init = (sdk: Fron"
},
{
"path": "packages/frontend/src/features/share-filters/index.ts",
"chars": 5507,
"preview": "import { onLocationChange } from \"@/dom\";\nimport { createFeature } from \"@/features/manager\";\nimport { type FrontendSDK "
},
{
"path": "packages/frontend/src/features/share-replay-collections/index.ts",
"chars": 7171,
"preview": "import { type RequestRawInput } from \"@caido/sdk-frontend/src/types/__generated__/graphql-sdk\";\n\nimport { onLocationChan"
},
{
"path": "packages/frontend/src/features/share-scope/index.ts",
"chars": 3890,
"preview": "import { onLocationChange } from \"@/dom\";\nimport { createFeature } from \"@/features/manager\";\nimport { type FrontendSDK "
},
{
"path": "packages/frontend/src/fonts/index.ts",
"chars": 1183,
"preview": "import { getFontUrl } from \"shared\";\n\nimport { type FrontendSDK } from \"@/types\";\n\nfunction loadFont(font: string, fontU"
},
{
"path": "packages/frontend/src/index.ts",
"chars": 1225,
"preview": "import { Classic } from \"@caido/primevue\";\nimport { createPinia } from \"pinia\";\nimport PrimeVue from \"primevue/config\";\n"
},
{
"path": "packages/frontend/src/plugins/sdk.ts",
"chars": 572,
"preview": "import { inject, type InjectionKey, type Plugin } from \"vue\";\n\nimport { type FrontendSDK } from \"@/types\";\n\nconst KEY: I"
},
{
"path": "packages/frontend/src/queries/flags.ts",
"chars": 1083,
"preview": "import { useQuery } from \"@pinia/colada\";\nimport { type FeatureFlag, type FeatureFlagTag } from \"shared\";\nimport { compu"
},
{
"path": "packages/frontend/src/queries/settings.ts",
"chars": 1180,
"preview": "import { useQuery } from \"@pinia/colada\";\nimport {\n type Result,\n type SettingKey,\n type Settings,\n type SettingValu"
},
{
"path": "packages/frontend/src/styles/index.css",
"chars": 168,
"preview": "@import \"tailwindcss/base\";\n@import \"tailwindcss/components\";\n@import \"tailwindcss/utilities\";\n\n* {\n margin: 0;\n paddi"
},
{
"path": "packages/frontend/src/types.ts",
"chars": 160,
"preview": "import { type Caido } from \"@caido/sdk-frontend\";\nimport { type API, type BackendEvents } from \"backend\";\n\nexport type F"
},
{
"path": "packages/frontend/src/utils/file-utils.ts",
"chars": 1037,
"preview": "export const downloadFile = (name: string, content: string) => {\n const a = document.createElement(\"a\");\n a.href = URL"
},
{
"path": "packages/frontend/src/views/App.vue",
"chars": 533,
"preview": "<script setup lang=\"ts\">\nimport Splitter from \"primevue/splitter\";\nimport SplitterPanel from \"primevue/splitterpanel\";\n\n"
},
{
"path": "packages/frontend/tsconfig.json",
"chars": 248,
"preview": "{\n \"extends\": \"../../tsconfig.json\",\n \"compilerOptions\": {\n \"lib\": [\"DOM\", \"ESNext\"],\n \"types\": [\"@caido/sdk-bac"
},
{
"path": "packages/shared/package.json",
"chars": 274,
"preview": "{\n \"name\": \"shared\",\n \"version\": \"0.0.0\",\n \"description\": \"Shared types\",\n \"author\": \"bebiks\",\n \"license\": \"CC0-1.0"
},
{
"path": "packages/shared/src/flags.ts",
"chars": 512,
"preview": "export type FeatureFlagTag =\n | \"exclude-host-path\"\n | \"backend-test\"\n | \"quick-decode\"\n | \"clear-all-findings\"\n | "
},
{
"path": "packages/shared/src/fonts.ts",
"chars": 739,
"preview": "const fonts = [\n { name: \"Default\", url: \"\" },\n {\n name: \"JetBrains Mono\",\n url: \"https://fonts.googleapis.com/c"
},
{
"path": "packages/shared/src/index.ts",
"chars": 104,
"preview": "export * from \"./result\";\nexport * from \"./flags\";\nexport * from \"./settings\";\nexport * from \"./fonts\";\n"
},
{
"path": "packages/shared/src/result.ts",
"chars": 272,
"preview": "export type Result<T> =\n | { kind: \"Error\"; error: string }\n | { kind: \"Success\"; value: T };\n\nexport function ok<T>(v"
},
{
"path": "packages/shared/src/settings.ts",
"chars": 227,
"preview": "export type Settings = {\n customFont: string;\n};\n\nexport type SettingKey = keyof Settings;\nexport type SettingValue<K e"
},
{
"path": "packages/shared/tsconfig.json",
"chars": 71,
"preview": "{\n \"extends\": \"../../tsconfig.json\",\n \"include\": [\"./src/**/*.ts\"]\n}\n"
},
{
"path": "pnpm-workspace.yaml",
"chars": 61,
"preview": "packages:\n - packages/*\n\nonlyBuiltDependencies:\n - esbuild\n"
},
{
"path": "tsconfig.json",
"chars": 471,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"esnext\",\n \"module\": \"esnext\",\n \"lib\": [\"ESNext\"],\n\n \"jsx\": \"preserve\",\n"
}
]
About this extraction
This page contains the full source code of the bebiksior/EvenBetter GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 67 files (114.5 KB), approximately 30.2k tokens, and a symbol index with 110 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.