Skip to main content

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

MethodDescription
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

MethodDescription
addFinding(finding)Write a finding (category, severity, title, description, evidence) to the VAAST results pane

Requires: scanner.write

api.logger

MethodDescription
getLogs(filter?)Read log entries, optionally filtered by level or tool name

Requires: logger.read

api.ui

MethodDescription
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

MethodDescription
getMeta()Get current session metadata: workspace ID/name, target URL, plan, VAAST version

Requires: session.read

Permissions

Each permission string unlocks specific API calls:

PermissionUnlocks
proxy.readapi.proxy.getCaptures, api.proxy.onCapture
proxy.writeModify proxy behavior (future)
scanner.readRead existing findings (future)
scanner.writeapi.scanner.addFinding
logger.readapi.logger.getLogs
ui.tabapi.ui.registerTab
ui.panelapi.ui.registerPanel
session.readapi.session.getMeta
http.fetchMake outbound HTTP requests from the Xtension

uiType

ValueWhen to use
tabYour Xtension needs its own full-width view in the sidebar
panelYour Xtension overlays existing views (e.g. a passive monitor badge)
noneYour 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:

PermissionInstall-time behavior
proxy.read, scanner.write, etc.Installed silently
http.fetchTOS 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

  1. Build your bundle: npm run build
  2. 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
  3. Restart VAAST or reload Xtensions from Settings
  4. 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 id exactly
  • 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

ValueWhen 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.

Next steps