---
url: https://docs.ogmabox.com/id/plugins/README.md
description: >-
  Buat, paketkan, instal, aktifkan, dan distribusikan plugin Ogma dengan logika
  backend, panel frontend, perintah, izin, dan metadata marketplace.
---

# Sistem Plugin Ogma {#ogma-plugin-system}

Plugin Ogma memperluas alat dengan logika backend kustom, panel UI frontend, dan langkah alur kerja. Plugin diinstal secara lokal dari direktori pada disk, diaktifkan per proyek, dan berjalan dalam lingkungan sandbox.

Dokumen ini adalah referensi utama bagi pembuat plugin.

Jika Anda menginginkan cara paling cepat untuk memulai, gunakan [Panduan Awal Plugin](/id/plugins/quickstart).

***

## Mulai cepat {#quick-start}

### Plugin backend minimal {#minimal-backend-plugin}

```
my-plugin/
  manifest.json
  backend/script.js
```

`manifest.json`:

```json
{
  "id": "my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "plugins": [
    {
      "kind": "backend",
      "id": "my-plugin-backend",
      "entrypoint": "backend/script.js"
    }
  ]
}
```

`backend/script.js` (ES2020; impor dapat digunakan dengan bundler):

```js
async function init(sdk) {
  sdk.console.log("my-plugin started");

  sdk.events.onInterceptResponse(function(req, res) {
    if (res.getCode() === 403) {
      sdk.console.warn("403 on " + req.getUrl());
    }
  });
}
```

Ini sudah cukup untuk plugin yang hanya memiliki backend dan berfungsi. Simpan sebagai satu file di `backend/script.js` dan arahkan `manifest.json` langsung ke file tersebut.

### Alur build satu menit (sumber TypeScript) {#one-minute-build-flow-typescript-source}

Jika Anda menulis TypeScript, gunakan struktur ini:

```text
my-plugin/
  manifest.json
  backend/
    src/index.ts
```

Build:

```bash
pnpm add -D @ogmabox/ogma-sdk esbuild typescript
pnpm exec esbuild backend/src/index.ts --bundle --format=iife --platform=neutral --external:@ogma/sdk --external:@ogmabox/ogma-sdk --outfile=backend/script.js
```

Instal: **Plugin > Pasang**, pilih direktori `my-plugin/`. Lalu aktifkan.

***

## Referensi manifes {#manifest-reference}

`manifest.json` berada di direktori root paket. Semua kolom membedakan huruf besar/kecil.

### Kolom tingkat teratas {#top-level-fields}

| Kolom | Wajib | Tipe | Catatan |
|-------|----------|------|-------|
| `id` | ya | string | Hanya huruf kecil, angka, dan tanda hubung. Maksimum 64 karakter. Unik di antara plugin terinstal. |
| `version` | ya | string | Semver: `MAJOR.MINOR.PATCH` |
| `name` | tidak | string | Nama tampilan di UI. Default-nya adalah `id`. |
| `description` | tidak | string | Ringkasan satu baris. |
| `author` | tidak | objek | `{ "name": "...", "email": "...", "url": "..." }` |
| `homepage` | tidak | string | URL repositori sumber atau dokumentasi. |
| `plugins` | ya | array | Satu atau beberapa entri komponen plugin (lihat di bawah). |
| `permissions` | tidak | array | Daftar nama izin yang diperlukan (lihat [Izin](#permissions)). |

### Entri komponen plugin {#plugin-component-entry}

Setiap objek dalam array `plugins` menjelaskan satu komponen.

**Komponen backend:**

```json
{
  "kind": "backend",
  "id": "my-plugin-backend",
  "entrypoint": "backend/script.js",
  "runtime": "javascript",
  "assets": "backend/assets"
}
```

**Komponen frontend:**

```json
{
  "kind": "frontend",
  "id": "my-plugin-frontend",
  "entrypoint": "frontend/script.js",
  "style": "frontend/style.css",
  "assets": "frontend/assets",
  "backend": { "id": "my-plugin-backend" }
}
```

| Kolom | Wajib | Catatan |
|-------|----------|-------|
| `kind` | ya | `"backend"` atau `"frontend"` |
| `id` | ya | Unik di dalam manifes. Huruf kecil, tanda hubung. |
| `entrypoint` | ya | Path relatif ke file titik masuk JS. |
| `style` | tidak | File CSS yang dimuat dalam iframe plugin. |
| `assets` | tidak | Direktori aset statis yang disajikan di bawah `/plugins/{id}/assets/`. |
| `backend.id` | tidak | Menautkan komponen frontend ke komponen backend-nya untuk RPC `sdk.backend.*`. |
| `runtime` | tidak (hanya backend) | `"javascript"` (nilai default dan satu-satunya yang didukung). |

***

## API plugin backend (sdk) {#backend-plugin-api-sdk}

Objek `sdk` backend diberikan ke fungsi `init(sdk)` Anda. Semua metode bersifat sinkron kecuali ditandai `async`.

### `sdk.console` {#sdk-console}

```js
sdk.console.log("message")
sdk.console.warn("message")
sdk.console.error("message")
```

Menulis ke buffer log plugin (terlihat di tab Log). Maksimum 500 entri dipertahankan. Setiap pesan dipotong pada 1 KB.

### `sdk.meta` {#sdk-meta}

```js
sdk.meta.id()          // > string: plugin id (e.g. "my-plugin")
sdk.meta.packageId()   // > string: same as id
sdk.meta.version()     // > string: semver (e.g. "1.0.0")
sdk.meta.path()        // > string: writable data directory for this plugin
```

`sdk.meta.path()` mengarah ke direktori data privat plugin yang dapat ditulis, misalnya `~/.local/share/ogma/plugins/my-plugin/data`. Direktori dibuat secara otomatis dan dapat digunakan untuk menyimpan data plugin secara persisten setelah mulai ulang.

`sdk.storage`, `sdk.path`, dan `sdk.fs` juga tersedia sebagai helper status dan file.

### `sdk.storage` {#sdk-storage}

```js
sdk.storage.get("key")      // > string | null
sdk.storage.set("key", "value")
sdk.storage.delete("key")
sdk.storage.clear()
sdk.storage.keys()          // > string[]
```

`sdk.storage` dibatasi pada ID plugin dan dipertahankan setelah plugin dimulai ulang.

### `sdk.fs` {#sdk-fs}

```js
sdk.fs.readFile("relative/file.txt")      // > string
sdk.fs.writeFile("relative/file.txt", "text")
sdk.fs.appendFile("relative/file.txt", "more")
sdk.fs.exists("relative/file.txt")        // > boolean
sdk.fs.existsSync("relative/file.txt")    // > boolean
sdk.fs.list("relative/dir")              // > string[]
sdk.fs.mkdir("relative/dir")
```

`sdk.fs` dibatasi pada file di bawah `sdk.meta.path()`.

`read` dan `write` tetap menjadi alias kompatibilitas untuk `readFile` dan `writeFile`. Gunakan `exists` atau `existsSync` sebelum membuat file yang tidak ingin Anda timpa. API ini bekerja secara sinkron dalam runtime plugin; API ini bukan modul `fs` Node.js lengkap. Akses file plugin memerlukan izin `plugin_storage` dan tetap berada di dalam direktori data privat plugin. JavaScript alur kerja memiliki konteks sistem file berbeda; lihat [Akses file alur kerja](../app/workflows.md#javascript-and-files).

### `sdk.path` {#sdk-path}

```js
sdk.path.join("a", "b", "c")
sdk.path.basename("/tmp/file.txt")
sdk.path.dirname("/tmp/file.txt")
sdk.path.extname("file.txt")
sdk.path.resolve("/a", "b")
sdk.path.isAbsolute("/tmp/file.txt")
sdk.path.sep
```

### `sdk.events` {#sdk-events}

Daftarkan callback untuk peristiwa Ogma. Semua callback dipanggil secara sinkron di dalam sandbox QuickJS.

```js
sdk.events.onInterceptRequest(function(req) {
  // req: RequestSpecRaw
  // Return a modified RequestSpecRaw to mutate the request.
  // Return null/undefined to pass through unchanged.
});

sdk.events.onInterceptResponse(function(req, res) {
  // req: Request (read-only), res: Response (read-only)
  // Return value is ignored.
});

sdk.events.onProjectChange(function() {
  // no callback args
});

sdk.events.onFindingCreated(function(finding) {
  // finding: { id, title, reporter }
});
```

### `sdk.requests` {#sdk-requests}

```js
// Get a single HTTP entry by id
var entry = sdk.requests.get("entry-id");
// entry: { id, method, host, path, query, tls, ... } or null

// Search HTTP history
var results = sdk.requests.search({ limit: 20, offset: 0 });
// results: { entries: [...], total: N }

// Send an HTTP request (requires send_requests permission)
var response = await sdk.requests.send(spec);
// spec: RequestSpecRaw (see below)
// response: Response
```

`sdk.requests.send` memerlukan izin `send_requests` yang dideklarasikan dalam manifes dan diberikan oleh pengguna. Lihat [Izin](#permissions).

### `sdk.findings` {#sdk-findings}

```js
// Create a finding (requires write_findings permission)
sdk.findings.create({
  title: "SSRF via redirect",
  reporter: "my-plugin",
  dedupeKey: "ssrf-" + request.getId(),
  request: { id: request.getId() }
});

// Check if a finding already exists (dedup check)
var exists = sdk.findings.exists({ dedupeKey: "ssrf-abc" });

// List findings
var page = sdk.findings.list({ limit: 20, offset: 0 });

// Get a single finding
var finding = sdk.findings.get("finding-id");
```

Batas laju pada `sdk.findings.create`: 10 per menit, 500 per sesi plugin, 3 per callback peristiwa.

### `sdk.api` {#sdk-api}

Daftarkan fungsi RPC backend yang dapat dipanggil frontend melalui `sdk.backend.*`:

```js
// In backend init:
sdk.api.register("getScans", function(scanId) {
  return { scans: [] };
});

// Emit an event to connected frontends:
sdk.api.send("scan:complete", { scanId: 1, status: "ok" });
```

Handler menerima argumen yang diberikan dari frontend (tidak ada argumen `sdk` tambahan yang disisipkan). Nilai kembalian diserialisasi sebagai JSON dan dikirim kembali kepada pemanggil.

Frontend memanggilnya melalui `sdk.backend.getScans(scanId)` - lihat [API plugin frontend](#frontend-plugin-api-sdk).

`sdk.api.send` mendorong peristiwa ke antrean per plugin (maksimum 200 entri). Frontend melakukan polling antrean ini melalui `sdk.backend.onEvent`.

### `sdk.replay`, `sdk.projects`, `sdk.scope`, `sdk.workflows`, `sdk.matchReplace` {#sdk-replay-sdk-projects-sdk-scope-sdk-workflows-sdk-matchreplace}

Ini adalah namespace kueri baca saja. Lihat [Referensi SDK backend](./backend-sdk.md) untuk signature metode lengkap.

### Kelas permintaan {#request-classes}

**`RequestSpecRaw`** - mewakili permintaan yang diintersepsi. Anda menerimanya dalam `onInterceptRequest`.

```js
spec.getMethod()          // > string
spec.setMethod("POST")
spec.getHost()            // > string
spec.setHost("example.com")
spec.getPort()            // > number
spec.getPath()            // > string
spec.setPath("/new/path")
spec.getQuery()           // > string
spec.getTls()             // > boolean
spec.getHeaders()         // > Record<string, string[]>
spec.setHeader("X-Foo", "bar")
spec.getBody()            // > Body | null
spec.setBody("new body")
spec.getRaw()             // > Uint8Array (raw bytes) or []
spec.setRaw(bytes)        // set raw bytes

// Create a new spec from a URL string:
var spec = new RequestSpecRaw("https://example.com/path?q=1");
```

**`Request`** - permintaan rekaman baca saja (dari `sdk.requests.get`).

```js
req.getId()
req.getMethod()
req.getHost()
req.getPort()
req.getTls()
req.getPath()
req.getQuery()
req.getUrl()           // > full URL string
req.getHeaders()       // > Record<string, string>
req.getHeader("name")
req.getBody()          // > Body | null
req.getCreatedAt()     // > Date
req.toSpec()           // > RequestSpec (mutable copy)
```

**`Response`** - respons rekaman baca saja.

```js
res.getCode()            // > number (HTTP status)
res.getHeaders()         // > Record<string, string>
res.getHeader("name")
res.getBody()            // > Body | null
res.getRoundtripTime()   // > number (ms)
res.getCreatedAt()       // > Date
```

**`Body`**:

```js
body.toText()    // > string
body.toJson()    // > parsed object or null
body.toRaw()     // > Uint8Array
body.length      // > number (original size, may differ from toText() if truncated)
```

***

## API plugin frontend (sdk) {#frontend-plugin-api-sdk}

Kode plugin frontend berjalan dalam iframe sandbox yang dimuat dari `/plugins/{id}/ui`. Iframe menggunakan `postMessage` untuk berkomunikasi dengan host Ogma, yang meneruskan pemanggilan ke backend.

SDK tersedia melalui `window.ogmaSDK`. Panggil `ogmaSDK.ready(cb)` untuk menerima SDK aktif setelah bridge host terbentuk:

```js
ogmaSDK.ready(function(sdk) {
  // sdk is the live SDK - safe to call any method here
  sdk.log.info("frontend ready");
});
```

Semua metode SDK mengembalikan Promise.

### `sdk.log` {#sdk-log}

```js
sdk.log.info("message")
sdk.log.warn("message")
sdk.log.error("message")
```

### `sdk.meta` {#sdk-meta-1}

```js
var meta = await sdk.meta.get();
// { pluginId, packageId, name, version, ogmaVersion }
```

### `sdk.requests` {#sdk-requests-1}

```js
var entry = await sdk.requests.get({ id: "entry-id" });
var result = await sdk.requests.search({ limit: 20, offset: 0, query: "host:example.com" });
```

Memerlukan izin `read_http_history` (diberikan otomatis; tidak memerlukan persetujuan pengguna).

### `sdk.findings` {#sdk-findings-1}

```js
var page = await sdk.findings.list({ limit: 20, offset: 0 });
```

Memerlukan izin `read_findings` (diberikan otomatis).

### `sdk.scope` {#sdk-scope}

```js
var scope = await sdk.scope.getActive();
```

### `sdk.projects` {#sdk-projects}

```js
var project = await sdk.projects.getCurrent();
```

### `sdk.backend` - RPC backend {#sdk-backend-backend-rpc}

Panggil fungsi yang didaftarkan dengan `sdk.api.register` pada backend:

```js
// Call a named backend function
var result = await sdk.backend.call("getScans", [scanId]);

// Poll for backend-emitted events (sdk.api.send on the backend side)
var { events, next_since } = await sdk.backend.poll(since);
// events: [{ event: "scan:complete", args: [...] }]

// Register an event listener (uses polling internally)
sdk.backend.onEvent("scan:complete", function(data) {
  console.log("scan done", data);
});
```

`sdk.backend.onEvent` menggunakan siklus polling 2 detik secara internal. Berhenti mendengarkan dengan memanggil fungsi berhenti berlangganan yang dikembalikan:

```js
var unsub = sdk.backend.onEvent("scan:complete", handler);
// later:
unsub();
```

### `sdk.navigation` {#sdk-navigation}

```js
await sdk.navigation.addPage("/my-plugin", { title: "My Plugin" });
```

Mendaftarkan halaman navigasi. Saat ini dikonfirmasi oleh host. Integrasi router penuh sedang dikembangkan.

### `sdk.sidebar` {#sdk-sidebar}

```js
await sdk.sidebar.registerItem("My Plugin", "/my-plugin", { icon: "puzzle" });
```

Mendaftarkan entri bilah sisi. Saat ini terbatas pada panel UI plugin - penghubungan slot bilah sisi global sedang dikembangkan.

### `sdk.commands` {#sdk-commands}

```js
await sdk.commands.register("my-plugin:scan", {
  name: "Scan with My Plugin",
  handler: function(context) { /* ... */ }
});
```

Dikonfirmasi oleh host. Integrasi palet perintah sedang dikembangkan.

### `sdk.menu` {#sdk-menu}

```js
await sdk.menu.registerItem({
  type: "Request",
  commandId: "my-plugin:scan",
  leadingIcon: "shield"
});
```

Dikonfirmasi oleh host. Penyisipan menu konteks sedang dikembangkan.

### `sdk.window` {#sdk-window}

```js
sdk.window.showToast("Scan complete", { variant: "success", duration: 3000 });
```

Menampilkan notifikasi toast dalam panel plugin. Varian: `info`, `success`, `warning`, `error`.

### `sdk.ui` {#sdk-ui}

```js
sdk.ui.resize(600);                              // request iframe height change
sdk.ui.sidebar.registerItem("name", "/path");    // alias for sdk.sidebar.registerItem
```

***

## Izin {#permissions}

Deklarasikan izin dalam `manifest.json`:

```json
{
  "permissions": ["send_requests", "write_findings"]
}
```

### Izin yang diberikan otomatis (tidak memerlukan persetujuan pengguna) {#auto-granted-permissions-no-user-approval-needed}

Izin berikut selalu diberikan kepada setiap plugin yang terinstal:

| Izin | Yang diizinkan |
|------------|----------------|
| `read_http_history` | `sdk.requests.get`, `sdk.requests.search` |
| `read_findings` | `sdk.findings.get`, `sdk.findings.list` |
| `read_scope` | `sdk.scope.getActive` |
| `read_projects` | `sdk.projects.getCurrent`, `sdk.projects.list` |
| `plugin_storage` | `sdk.storage`, `sdk.fs`, `sdk.path` |

### Izin yang dilindungi (memerlukan persetujuan pengguna) {#protected-permissions-require-user-approval}

Izin berikut harus dideklarasikan dalam manifes dan diberikan secara eksplisit oleh pengguna dari tab Izin:

| Izin | Yang diizinkan |
|------------|----------------|
| `send_requests` | `sdk.requests.send` - membuat permintaan HTTP keluar |
| `write_findings` | `sdk.findings.create`, `sdk.findings.update` |

Pengguna melihat prompt saat mengaktifkan plugin yang mendeklarasikan izin dilindungi. Pengguna juga dapat memberikan/mencabut izin kapan saja dari tab Izin.

***

## Struktur paket plugin {#plugin-package-structure}

Paket plugin dapat diinstal dari direktori lokal dan, dalam alur peramban, juga dari ekspor `.zip`.

```
my-plugin/
  manifest.json           - wajib
  backend/
    script.js             - JS backend yang dibundel (ES2020)
  frontend/
    script.js             - JS frontend yang dibundel
    style.css             - CSS opsional
    assets/               - aset statis (gambar, font, dan sebagainya)
```

### Persyaratan skrip backend {#backend-script-requirements}

* Harus berupa satu file JS mandiri.
* `require()` dan `import()` dinamis tidak didukung.
* Harus mengekspor fungsi `init(sdk)` (atau mendefinisikannya sebagai global).
* Subset ES2020 yang didukung QuickJS: `async/await`, `Promise`, `Map`, `Set`, `Symbol`, `Proxy`, `Date`, `RegExp`, `JSON`. Tanpa `fetch`, tanpa `Buffer`.
* Impor statis yang didukung diselesaikan oleh prapemrosesan plugin: `@ogma/sdk`, `crypto`, `fs`, `path`.
* Ukuran file maksimum: 256 KB.

### Persyaratan skrip frontend {#frontend-script-requirements}

* Berjalan di dalam iframe sandbox. `connect-src: 'self'` diizinkan agar plugin dapat melakukan POST ke `/plugins/{id}/api/*` dan polling `/plugins/{id}/events/poll`.
* CSP: `default-src 'none'; script-src 'nonce-...'; style-src 'self'; img-src data: blob: 'self'; connect-src 'self'`.
* Tanpa `allow-same-origin` dalam sandbox iframe - plugin tidak dapat mengakses DOM induk atau cookie Ogma.
* Gunakan `ogmaSDK.ready(cb)` untuk mengakses SDK; jangan panggil metode SDK sebelum callback terpicu.

***

## Melakukan build plugin untuk Ogma {#building-a-plugin-for-ogma}

Karena backend harus berupa satu file JS yang dibundel, Anda harus membundel sumber modul TypeScript/ES sebelum menginstal.

Rangkaian alat yang direkomendasikan:

```bash
# Install dependencies
pnpm install

# Bundle backend (outputs a single CJS/IIFE file):
esbuild packages/backend/src/index.ts \
  --bundle \
  --platform=neutral \
  --format=iife \
  --global-name=_plugin \
  --outfile=dist/backend/script.js \
  --external:@ogma/sdk --external:@ogmabox/ogma-sdk

# Bundle frontend:
vite build packages/frontend --outDir ../../dist/frontend
```

Jika Anda menggunakan rangkaian alat pengembangan Caido (`@caido-community/dev`), jalankan `caido-dev build`, lalu salin keluaran ke tata letak yang kompatibel dengan Ogma dengan `manifest.json` pada root.

***

## Menginstal plugin {#installing-a-plugin}

1. Buka **Plugin** di bilah sisi kiri.
2. Klik **Pasang** (di bagian atas tab Terpasang).
3. Dalam aplikasi desktop, klik **Telusuri** untuk membuka pemilih folder native. Dalam peramban, ketik path lengkap direktori plugin di sisi server.
4. Klik **Validasi** untuk memeriksa manifes dan inventaris file.
5. Klik **Pasang** jika validasi lulus.
6. Pilih plugin dalam daftar dan klik **Aktifkan**.
7. Jika plugin mendeklarasikan izin dilindungi, tinjau dan berikan izin dari tab **Izin** sebelum mengaktifkan.

***

## Pemecahan masalah {#troubleshooting}

**Inisialisasi plugin gagal tanpa pemberitahuan:** Periksa tab **Log**. Penyebab paling umum:

* `sdk.meta.path()` dipanggil, tetapi direktori data tidak dapat dibuat.
* Pengecualian yang tidak ditangani dalam `init()`.
* Pemanggilan `sdk.*` yang tidak ada atau salah eja.

**Frontend kosong:** Periksa konsol peramban untuk pelanggaran CSP. Pastikan skrip frontend memanggil `ogmaSDK.ready(cb)` sebelum mengakses metode SDK apa pun.

**`sdk.requests.send` melempar `Permission denied`:** Izin `send_requests` harus dideklarasikan dalam manifes DAN diberikan oleh pengguna di tab Izin.

**Fungsi `sdk.api.register` tidak dapat dipanggil dari frontend:** Backend harus diaktifkan (bukan hanya diinstal). Nama fungsi harus sama persis (membedakan huruf besar/kecil) dengan yang diberikan frontend ke `sdk.backend.call`.

**Peringatan kompatibilitas muncul sebagai kesalahan:** Peringatan ini tidak memblokir operasi, tetapi menunjukkan celah cakupan API. Lihat tabel pemetaan SDK di atas untuk cakupan API.
