Building an Xtension
Prerequisites
- Node.js 20+
- TypeScript
- esbuild (for bundling)
Manifest
Every Xtension needs a manifest.json at its root. See Manifest Reference for the full schema.
Minimal example:
{
"id": "my-xtension",
"name": "My Xtension",
"version": "1.0.0",
"description": "Finds something interesting in proxy captures.",
"author": "your-github-username",
"entrypoint": "dist/index.js",
"permissions": ["proxy.read", "scanner.write", "ui.tab"],
"tier": "free"
}
The VAAST Plugin API
Your Xtension's entrypoint must export a register(api) function. The api object exposes these namespaces:
api.proxy
| Method | Description |
|---|---|
getCaptures(filter?) | Returns an array of captured proxy requests matching the optional filter (method, host, statusCode) |
onCapture(callback) | Subscribe to live proxy captures. Returns an unsubscribe function. |
Requires: proxy.read
api.scanner
| Method | Description |
|---|---|
addFinding(finding) | Write a finding (category, severity, title, description, evidence) to the VAAST results pane |
Requires: scanner.write
api.logger
| Method | Description |
|---|---|
getLogs(filter?) | Read log entries, optionally filtered by level or tool name |
Requires: logger.read
api.ui
| Method | Description |
|---|---|
registerTab(id, label) | Add a new tab to the VAAST sidebar |
registerPanel(id) | Add a floating panel overlay |
Requires: ui.tab or ui.panel
api.session
| Method | Description |
|---|---|
getMeta() | Get current session metadata: workspace ID/name, target URL, plan, VAAST version |
Requires: session.read
Permissions
Each permission string unlocks specific API calls:
| Permission | Unlocks |
|---|---|
proxy.read | api.proxy.getCaptures, api.proxy.onCapture |
proxy.write | Modify proxy behavior (future) |
scanner.read | Read existing findings (future) |
scanner.write | api.scanner.addFinding |
logger.read | api.logger.getLogs |
ui.tab | api.ui.registerTab |
ui.panel | api.ui.registerPanel |
session.read | api.session.getMeta |
http.fetch | Make outbound HTTP requests from the Xtension |
uiType
| Value | When to use |
|---|---|
tab | Your Xtension needs its own full-width view in the sidebar |
panel | Your Xtension overlays existing views (e.g. a passive monitor badge) |
none | Your Xtension runs silently with no UI (e.g. a background analyzer) |
Minimal working example
manifest.json:
{
"id": "example-finder",
"name": "Example Finder",
"version": "1.0.0",
"description": "Flags responses containing the word 'example'.",
"author": "demo",
"entrypoint": "dist/index.js",
"permissions": ["proxy.read", "scanner.write", "ui.tab"],
"tier": "free",
"uiType": "tab"
}
src/index.ts:
import type { VaastXtensionApi } from '../types/vaast-api';
export function register(api: VaastXtensionApi): void {
api.ui.registerTab('example-finder', 'Example Finder');
api.proxy.onCapture(async (capture) => {
if (capture.responseBody?.includes('example')) {
await api.scanner.addFinding({
category: 'Information Disclosure',
severity: 'low',
title: `Response contains "example" — ${capture.url}`,
description: 'The word "example" was found in the response body.',
evidence: capture.responseBody.slice(0, 200),
});
}
});
}
Permissions and Responsible Use
Each permission your Xtension declares is shown to the user at install time. Requesting http.fetch triggers an additional Terms of Service acknowledgment dialog warning that the Xtension makes outbound HTTP requests.
Developer responsibilities:
- Only make outbound requests (
http.fetch) to URLs provided by the user or derived from the active VAAST session target - Never make requests to hardcoded third-party URLs without disclosing them in your manifest description
- VAAST's verified Targets system is available via
api.session.getVerifiedDomains()— use it to check if a domain is in the user's authorized target list before making requests - You are responsible for ensuring your Xtension is only used against authorized targets
What the user sees at install time:
| Permission | Install-time behavior |
|---|---|
proxy.read, scanner.write, etc. | Installed silently |
http.fetch | TOS confirmation dialog: "This Xtension makes outbound HTTP requests..." |
Links:
Bundling with esbuild
// esbuild.config.js
const esbuild = require('esbuild');
esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/index.js',
format: 'esm',
external: ['react', 'react-dom'],
target: 'es2020',
}).catch(() => process.exit(1));
Run: node esbuild.config.js
Testing locally
- Build your bundle:
npm run build - Copy files to the VAAST extensions directory:
cp dist/index.js ~/.vaast/extensions/your-id/index.js
cp manifest.json ~/.vaast/extensions/your-id/manifest.json - Restart VAAST or reload Xtensions from Settings
- Your Xtension appears in the installed list under COMMUNITY XTENSIONS
Local installs bypass the registry entirely. You do not need to submit to the registry to test.
If your changes don't appear after copying:
- Confirm the folder name matches your manifest
idexactly - Quit VAAST fully (Cmd+Q / Alt+F4) and reopen — a window close is not sufficient on some platforms
- Check the VAAST console log for
[your-id]prefixed errors
Common errors
api.scanner.addFinding() findings not appearing
Findings written via scanner.write only surface in the Findings pane when there is an active scan session. If your Xtension analyzes proxy captures passively (without a scan running), use a uiType: "tab" UI instead and render results directly into your tab's DOM. See the Mezo community Xtension for a reference implementation.
api.proxy.getCaptures() returns empty array
The proxy must be running and have captured at least one request. Subscribe to api.proxy.onCapture() for live updates and call getCaptures() again on each new capture (debounced) to re-analyze the full session history.
Tab UI not rendering
VAAST creates the tab's mount container when api.ui.registerTab() is called. Pass a render function as the third argument: registerTab('id', 'Label', (container) => { container.innerHTML = '...' }). The container is available immediately inside the callback.
Bundle too large
esbuild bundles all dependencies by default. Mark external packages using the external array in esbuild.config.js. react and react-dom are already external by convention. Keep your bundle under 500KB for fast install times.
http.fetch permission rejected
You must declare http.fetch in your manifest permissions. Users see a confirmation dialog at install time. Only make requests to URLs derived from the user's active VAAST session target — never to hardcoded third-party URLs without disclosure.
uiType guidance
| Value | When to use |
|---|---|
"tab" | Your Xtension has results or state to display — analysis views, history tables, configuration panels. Gets a sidebar tab. |
"panel" | Your Xtension overlays existing views passively — a badge counter, a live alert indicator. |
"none" | Your Xtension runs silently with no UI — writes findings during an active scan session only. |
Important: if your Xtension primarily analyzes proxy captures outside of an active scan session, "none" is the wrong choice. Use "tab" and render your own results view so users can see the output regardless of scan state.