نظام إضافات Ogma
توسّع إضافات Ogma الأداة بمنطق خلفي مخصّص ولوحات لواجهة المستخدم الأمامية وخطوات لسير العمل. تُثبّت الإضافات محليًا من مجلد على القرص، وتُفعّل لكل مشروع، وتعمل في بيئة معزولة.
هذا المستند هو المرجع الرئيسي لمطوّري الإضافات.
للبدء بأسرع طريقة ممكنة، استخدم دليل البدء السريع للإضافات.
البدء السريع
إضافة خلفية مبسّطة
my-plugin/
manifest.json
backend/script.jsmanifest.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؛ يمكن استخدام عبارات الاستيراد عند استخدام أداة تجميع):
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());
}
});
}هذا يكفي لإنشاء إضافة تعمل بالواجهة الخلفية فقط. أبقِها في ملف واحد في backend/script.js، ووجّه manifest.json إليه مباشرة.
خطوات بناء خلال دقيقة واحدة (مصدر TypeScript)
إذا كنت تكتب الشيفرة باستخدام TypeScript، فاستخدم هذه البنية:
text
my-plugin/
manifest.json
backend/
src/index.tsالبناء:
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التثبيت: الإضافات > تثبيت، ثم اختر المجلد my-plugin/. وبعد ذلك فعّل الإضافة.
مرجع ملف التعريف
يوجد manifest.json في المجلد الجذري للحزمة. جميع الحقول حسّاسة لحالة الأحرف.
الحقول ذات المستوى الأعلى
| الحقل | مطلوب | النوع | ملاحظات |
|---|---|---|---|
id | نعم | سلسلة نصية | أحرف لاتينية صغيرة وأرقام وواصلات فقط. الحد الأقصى 64 محرفًا. فريد بين الإضافات المثبّتة. |
version | نعم | سلسلة نصية | الإصدار الدلالي: MAJOR.MINOR.PATCH |
name | لا | سلسلة نصية | اسم العرض الظاهر في واجهة المستخدم. قيمته الافتراضية id. |
description | لا | سلسلة نصية | ملخّص من سطر واحد. |
author | لا | كائن | { "name": "...", "email": "...", "url": "..." } |
homepage | لا | سلسلة نصية | عنوان URL لمستودع المصدر أو التوثيق. |
plugins | نعم | مصفوفة | مدخل واحد أو أكثر لمكوّنات الإضافة (انظر أدناه). |
permissions | لا | مصفوفة | قائمة أسماء الصلاحيات المطلوبة (راجع الصلاحيات). |
مدخل مكوّن الإضافة
يصف كل كائن في مصفوفة plugins مكوّنًا واحدًا.
المكوّن الخلفي:
json
{
"kind": "backend",
"id": "my-plugin-backend",
"entrypoint": "backend/script.js",
"runtime": "javascript",
"assets": "backend/assets"
}المكوّن الأمامي:
json
{
"kind": "frontend",
"id": "my-plugin-frontend",
"entrypoint": "frontend/script.js",
"style": "frontend/style.css",
"assets": "frontend/assets",
"backend": { "id": "my-plugin-backend" }
}| الحقل | مطلوب | ملاحظات |
|---|---|---|
kind | نعم | "backend" أو "frontend" |
id | نعم | فريد داخل ملف التعريف. أحرف صغيرة وواصلات. |
entrypoint | نعم | مسار نسبي لملف الدخول بلغة JS. |
style | لا | ملف CSS يُحمّل في iframe الخاص بالإضافة. |
assets | لا | مجلد للأصول الثابتة المقدّمة تحت /plugins/{id}/assets/. |
backend.id | لا | يربط المكوّن الأمامي بمكوّنه الخلفي لاستدعاءات RPC عبر sdk.backend.*. |
runtime | لا (للواجهة الخلفية فقط) | "javascript" (القيمة الافتراضية والوحيدة المدعومة). |
API الإضافات الخلفية (sdk)
يُمرّر كائن sdk الخلفي إلى دالتك init(sdk). جميع الطرق متزامنة ما لم تُعلّم بـasync.
sdk.console
js
sdk.console.log("message")
sdk.console.warn("message")
sdk.console.error("message")تكتب في مخزن سجلات الإضافة (المعروض تحت علامة تبويب السجلات). يُحتفظ بحد أقصى 500 سجل. تُقتطع كل رسالة عند 1 KB.
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() إلى مجلد البيانات الخاص بالإضافة والقابل للكتابة، مثل ~/.local/share/ogma/plugins/my-plugin/data. يُنشأ المجلد تلقائيًا ويمكن استخدامه للاحتفاظ ببيانات الإضافة عبر عمليات إعادة التشغيل.
تتوفّر أيضًا sdk.storage وsdk.path وsdk.fs لأدوات مساعدة للحالة والملفات.
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 على معرّف الإضافة، وتُحفظ بياناته عبر عمليات إعادة تشغيل الإضافة.
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 على الملفات الموجودة تحت sdk.meta.path().
يبقى read وwrite اسمين بديلين للتوافق مع readFile وwriteFile. استخدم exists أو existsSync قبل إنشاء ملف لا تريد استبداله. تعمل هذه الواجهات بصورة متزامنة في بيئة تشغيل الإضافة؛ وهي ليست وحدة fs الكاملة في Node.js. يتطلّب وصول الإضافة إلى الملفات الصلاحية plugin_storage، ويظل داخل مجلد بيانات الإضافة الخاص. يستخدم JavaScript في سير العمل سياقًا مختلفًا لنظام الملفات؛ راجع الوصول إلى الملفات في سير العمل.
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.sepsdk.events
سجّل دوال استدعاء راجع لأحداث Ogma. تُستدعى جميع هذه الدوال بصورة متزامنة داخل بيئة 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
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 إعلان الصلاحية send_requests في ملف التعريف ومنحها من المستخدم. راجع الصلاحيات.
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");حدود معدل sdk.findings.create: 10 في الدقيقة، و500 لكل جلسة إضافة، و3 لكل دالة استدعاء راجع لحدث.
sdk.api
سجّل دوال RPC خلفية تستطيع الواجهة الأمامية استدعاءها عبر 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" });يتلقّى المعالج الوسائط الممرّرة من الواجهة الأمامية (لا يُحقن وسيط sdk إضافي). تُسلسَل القيم المُرجعة بصيغة JSON وتُرسل إلى المستدعي.
تستدعي الواجهة الأمامية هذه الدوال عبر sdk.backend.getScans(scanId) - راجع API الإضافات الأمامية.
يدفع sdk.api.send الأحداث إلى قائمة انتظار خاصة بكل إضافة (بحد أقصى 200 مدخل). تستطلع الواجهات الأمامية هذه القائمة عبر sdk.backend.onEvent.
sdk.replay, sdk.projects, sdk.scope, sdk.workflows, sdk.matchReplace
هذه مساحات أسماء للاستعلام للقراءة فقط. راجع مرجع SDK الواجهة الخلفية للاطّلاع على التواقيع الكاملة للطرق.
أصناف الطلبات
RequestSpecRaw - يمثّل طلبًا معترضًا. تتلقّاه في 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 - طلب ملتقط للقراءة فقط (من 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 - استجابة ملتقطة للقراءة فقط.
js
res.getCode() // > number (HTTP status)
res.getHeaders() // > Record<string, string>
res.getHeader("name")
res.getBody() // > Body | null
res.getRoundtripTime() // > number (ms)
res.getCreatedAt() // > DateBody:
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 الإضافات الأمامية (sdk)
تعمل شيفرة الإضافة الأمامية في إطار iframe معزول يُحمّل من /plugins/{id}/ui. يستخدم iframe الدالة postMessage للتواصل مع مضيف Ogma، الذي يتوسّط في الاستدعاءات إلى الواجهة الخلفية.
يتوفّر SDK عبر window.ogmaSDK. استدعِ ogmaSDK.ready(cb) لتلقّي SDK الجاهز للعمل بعد إنشاء جسر المضيف:
js
ogmaSDK.ready(function(sdk) {
// sdk is the live SDK - safe to call any method here
sdk.log.info("frontend ready");
});تُرجع جميع طرق SDK كائنات Promise.
sdk.log
js
sdk.log.info("message")
sdk.log.warn("message")
sdk.log.error("message")sdk.meta
js
var meta = await sdk.meta.get();
// { pluginId, packageId, name, version, ogmaVersion }sdk.requests
js
var entry = await sdk.requests.get({ id: "entry-id" });
var result = await sdk.requests.search({ limit: 20, offset: 0, query: "host:example.com" });تتطلّب الصلاحية read_http_history (يُمنح تلقائيًا؛ لا يحتاج إلى موافقة المستخدم).
sdk.findings
js
var page = await sdk.findings.list({ limit: 20, offset: 0 });تتطلّب الصلاحية read_findings (يُمنح تلقائيًا).
sdk.scope
js
var scope = await sdk.scope.getActive();sdk.projects
js
var project = await sdk.projects.getCurrent();sdk.backend - استدعاءات RPC الخلفية
استدعِ الدوال المسجّلة باستخدام sdk.api.register في الواجهة الخلفية:
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 داخليًا حلقة استطلاع بفاصل 2 ثانية. أوقف الاستماع باستدعاء دالة إلغاء الاشتراك المُرجعة:
js
var unsub = sdk.backend.onEvent("scan:complete", handler);
// later:
unsub();sdk.navigation
js
await sdk.navigation.addPage("/my-plugin", { title: "My Plugin" });تسجّل صفحة تنقّل. يقرّ بها المضيف حاليًا. يجري العمل على التكامل الكامل مع موجّه المسارات.
sdk.sidebar
js
await sdk.sidebar.registerItem("My Plugin", "/my-plugin", { icon: "puzzle" });تسجّل عنصرًا في الشريط الجانبي. يقتصر حاليًا على لوحة واجهة الإضافة - يجري العمل على ربطه بموضع الشريط الجانبي العام.
sdk.commands
js
await sdk.commands.register("my-plugin:scan", {
name: "Scan with My Plugin",
handler: function(context) { /* ... */ }
});يقرّ بها المضيف. يجري العمل على التكامل مع لوحة الأوامر.
sdk.menu
js
await sdk.menu.registerItem({
type: "Request",
commandId: "my-plugin:scan",
leadingIcon: "shield"
});يقرّ بها المضيف. يجري العمل على إدراجها في القائمة السياقية.
sdk.window
js
sdk.window.showToast("Scan complete", { variant: "success", duration: 3000 });تعرض إشعارًا عابرًا في لوحة الإضافة. الأنواع: info وsuccess وwarning وerror.
sdk.ui
js
sdk.ui.resize(600); // request iframe height change
sdk.ui.sidebar.registerItem("name", "/path"); // alias for sdk.sidebar.registerItemالصلاحيات
أعلن الصلاحيات في manifest.json:
json
{
"permissions": ["send_requests", "write_findings"]
}الصلاحيات الممنوحة تلقائيًا (لا تحتاج إلى موافقة المستخدم)
تُمنح هذه الصلاحيات دائمًا لأي إضافة مثبّتة:
| الصلاحية | ما يسمح به |
|---|---|
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 |
الصلاحيات المحمية (تتطلّب موافقة المستخدم)
يجب إعلان هذه الصلاحيات في ملف التعريف ومنحها صراحةً من المستخدم عبر علامة تبويب الصلاحيات:
| الصلاحية | ما يسمح به |
|---|---|
send_requests | sdk.requests.send - إرسال طلبات HTTP صادرة |
write_findings | sdk.findings.create, sdk.findings.update |
يرى المستخدم مطالبة عند تفعيل إضافة تعلن صلاحيات محمية. ويمكنه أيضًا منح الصلاحيات أو سحبها في أي وقت من علامة تبويب الصلاحيات.
بنية حزمة الإضافة
يمكن تثبيت حزمة إضافة من مجلد محلي، وكذلك من تصديرات .zip عند استخدام المتصفح.
my-plugin/
manifest.json - required
backend/
script.js - bundled backend JS (ES2020)
frontend/
script.js - bundled frontend JS
style.css - optional CSS
assets/ - static assets (images, fonts, etc.)متطلّبات الملف البرمجي الخلفي
- يجب أن يكون ملف JS واحدًا مستقلًا بذاته.
- لا يُدعم
require()ولاimport()الديناميكي. - يجب تصدير دالة
init(sdk)(أو تعريفها كدالة عامة). - تدعم QuickJS مجموعة فرعية من ES2020:
async/awaitوPromiseوMapوSetوSymbolوProxyوDateوRegExpوJSON. لا يتوفّرfetchولاBuffer. - تُحل عبارات الاستيراد الثابتة المدعومة أثناء المعالجة المسبقة للإضافة:
@ogma/sdkوcryptoوfsوpath. - الحد الأقصى لحجم الملف: 256 KB.
متطلّبات الملف البرمجي الأمامي
- يعمل داخل iframe معزول. يُسمح بـ
connect-src: 'self'كي تتمكّن الإضافة من إرسال POST إلى/plugins/{id}/api/*واستطلاع/plugins/{id}/events/poll. - سياسة CSP:
default-src 'none'; script-src 'nonce-...'; style-src 'self'; img-src data: blob: 'self'; connect-src 'self'. - لا يوجد
allow-same-originفي عزل iframe - لا تستطيع الإضافة الوصول إلى DOM الخاص بصفحة Ogma الأم أو ملفات تعريف الارتباط. - استخدم
ogmaSDK.ready(cb)للوصول إلى SDK؛ ولا تستدعِ طرق SDK قبل تنفيذ دالة الاستدعاء الراجع.
بناء إضافة لـOgma
نظرًا إلى أنّ الواجهة الخلفية يجب أن تكون ملف JS مجمّعًا واحدًا، يجب تجميع مصدر TypeScript أو وحدات ES قبل التثبيت.
سلسلة الأدوات الموصى بها:
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إذا كنت تستخدم سلسلة أدوات تطوير Caido (@caido-community/dev)، فنفّذ caido-dev build ثم انسخ الناتج إلى بنية متوافقة مع Ogma، مع وضع manifest.json في الجذر.
تثبيت إضافة
- افتح الإضافات في الشريط الجانبي الأيسر.
- انقر على تثبيت (أعلى علامة تبويب المثبتة).
- في تطبيق سطح المكتب، انقر على استعراض لفتح منتقي مجلدات أصلي. في المتصفح، اكتب المسار الكامل لمجلد الإضافة على جانب الخادم.
- انقر على تحقق لفحص ملف التعريف وقائمة الملفات.
- انقر على تثبيت إذا نجح التحقّق.
- اختر الإضافة من القائمة وانقر على تفعيل.
- إذا كانت الإضافة تعلن صلاحيات محمية، فراجعها وامنحها من علامة تبويب الصلاحيات قبل التفعيل.
استكشاف المشكلات وإصلاحها
فشل تهيئة الإضافة دون ظهور رسالة: راجع علامة تبويب السجلات. الأسباب الأكثر شيوعًا:
- استُدعي
sdk.meta.path()لكن تعذّر إنشاء مجلد البيانات. - استثناء غير معالج في
init(). - استدعاء
sdk.*مفقود أو مكتوب بصورة غير صحيحة.
ظهور الواجهة الأمامية فارغة: راجع وحدة تحكّم المتصفح بحثًا عن انتهاكات CSP. تأكّد من أنّ الملف البرمجي الأمامي يستدعي ogmaSDK.ready(cb) قبل الوصول إلى أي طريقة في SDK.
رمي sdk.requests.send الخطأ Permission denied: يجب إعلان الصلاحية send_requests في ملف التعريف، ويجب كذلك أن يمنحها المستخدم في علامة تبويب الصلاحيات.
تعذّر استدعاء دوال sdk.api.register من الواجهة الأمامية: يجب تفعيل الواجهة الخلفية (لا تثبيتها فقط). يجب أن يطابق اسم الدالة تمامًا ما تمرّره الواجهة الأمامية إلى sdk.backend.call، مع مراعاة حالة الأحرف.
ظهور تحذيرات التوافق كأخطاء: لا تمنع هذه التحذيرات التشغيل، لكنها تشير إلى فجوات في تغطية واجهة API. راجع جداول مطابقة SDK أعلاه لمعرفة تغطية API.