# OpenWA Plugin Standard The conventions every plugin in this repository follows, so the catalog stays consistent and management/automation stays easy. Verified against OpenWA's plugin runtime (sandboxed worker model) at the same baseline as [`types/openwa.d.ts`](./types/openwa.d.ts) — re-check both together, since this document describes host behaviour that has moved repeatedly (per-session config in 0.7, boot re-enable in 0.10.5, the storage quota and `message:deleted` in 0.11.0, the `storage:use` gate in 0.17.0, hook result checks and sandboxed hook ordering in 0.23.6). The current baseline is OpenWA v0.30.0, including host-version floor enforcement and HTTPS origin grants for configured outbound endpoints (both 0.24.0), the secret rule for host overrides, `null` secret removal and the capped boot restore (0.26.0), outbound-only `message:ack`, edit-triggered `message:persisted`, query-aware empty-body ingress dedup and config push after `onEnable` (0.27.0), the 184 UTF-8 byte storage key limit, queued ingress dispatch timeout budget, `dedupBodyMismatchAsNew`, query-borne ingress `verified: false`, Baileys group `message:ack` progression and whatsapp-web.js system notice filtering (0.28.0/0.29.0), `isForwarded` / `forwardingScore` on `IncomingMessage` (0.29.1), and the Baileys inbound media stream fix (0.30.0). ## Principles | Concern | Lives in | Notes | | ------- | -------- | ----- | | Machine-readable metadata | `manifest.json` | Single source of truth. OpenWA reads required fields and ignores unknown ones. | | Human-readable docs | `README.md` | Standard section order (below). | | Dated version history | `CHANGELOG.md` | Keep a Changelog + SemVer. The release date lives here. | | Computed artifacts (size, sha256) | release time | GitHub Release notes + assets, plus the `#sha256=` pin on each catalog `download` URL; never hand-written. | | Cross-plugin index | `plugins.json` (repo root) | **Generated** from manifests + changelogs. Don't edit by hand. | ## Repository layout ``` OpenWA-plugins/ ├─ plugins.json # generated catalog (one entry per plugin) ├─ PLUGIN-STANDARD.md # this file ├─ scripts/catalog.mjs # catalog generator + drift check ├─ package.mjs # per-plugin build + validate + package (zip) └─ / ├─ manifest.json ├─ README.md # Details (generated block) + Features + … ├─ CHANGELOG.md ├─ index.ts # default-exports an IPlugin class ├─ *.ts, *.test.ts └─ dist/index.js # built artifact (gitignored) ``` ## `manifest.json` ```jsonc { // ── Required by OpenWA (install fails without these) ── "id": "my-plugin", // /^[a-z0-9][a-z0-9._-]*$/i, unique, not reserved "name": "My Plugin", // shown in the dashboard "version": "1.0.0", // SemVer; MUST equal the top released CHANGELOG heading "type": "extension", // only "extension" is user-installable "main": "dist/index.js", // require()-able entry, present in the package // ── Surfaced by OpenWA ── "description": "…", // rendered in the dashboard card + returned by the API "provides": ["feature-tag"], // rendered as tags in the dashboard card "permissions": [], // see the seven values below — enforced at the capability boundary "sessions": ["*"], // capability session scope (static; editing config can't widen it) "hooks": ["message:received"],// declared interest "configSchema": { … }, // declarative config form (see vocabulary below); mark secrets "secret": true // ── v0.7 contract ── "sessionScoped": true, // default true: only runs for sessions an operator activates it for // (ctx.config is the resolved per-session slice). false = global, always on. "net": { "allow": ["api.example.com:443"] }, // outbound-HTTP allowlist for ctx.net.fetch (needs "net:fetch") "configUi": { "entry": "config/index.html", "height": 600 }, // sandboxed-iframe config editor (see below) // ── Returned by the API; `author` shows on the catalog row and is searchable there ── "author": "Name ", // string form (npm-style) "license": "MIT", // stored and returned, not rendered // ── Standard catalog metadata (OpenWA stores but ignores; used by our tooling) ── "homepage": "https://github.com/rmyndharis/OpenWA-plugins/tree/main/my-plugin", "repository": "https://github.com/rmyndharis/OpenWA-plugins", "keywords": ["…"], "status": "stable", // "stable" | "beta" | "development" "minOpenWAVersion": "0.7.0", // enforced at install and boot load since OpenWA v0.24.0 "testedOpenWAVersion": "0.30.0" // newest host checked; describe the verification scope in the README } ``` Repo gates — all hard failures in CI. `scripts/catalog.mjs` refuses to build the catalog unless every plugin: - declares `sessionScoped` explicitly (`true` or `false`); - declares `testedOpenWAVersion` when `status` is `"stable"`; - ships a `CHANGELOG.md` carrying a released `## [x.y.z] - YYYY-MM-DD` heading; - has a `manifest.json` `version` equal to that heading's version. `catalog.mjs` only *warns* about i18n — a missing block, or a missing locale — but the test suite does not: `npm test` fails when a `stable` plugin is missing any supported locale, and when any plugin ships a locale that translates its name and description without translating every `configSchema` field. Treat i18n as gated; the warning is where you notice a gap, not where it is stopped. The shared locale list in `scripts/locales.mjs` matches OpenWA v0.30.0, including `id`; stable plugins must cover every non-English locale. ### `configSchema` field vocabulary (v0.7) A JSON-Schema-ish object the host renders into an authenticated form (writes go through `PUT /plugins/:id/config`). `{ "type": "object", "properties": { : } }`, where each field is: | Key | Applies to | Effect | | --- | ---------- | ------ | | `type` | all | `string` · `number` · `boolean` · `textarea` (multi-line string) · `object` · `array` | | `title` / `description` | all | label + helper text | | `default` | all | seeded into the form; also the blank value for a new array row | | `required` | all | marks the label (advisory — not hard-enforced by the host) | | `secret` | scalar | masked on read (shown as `***`); an unchanged `***` write, or an empty string, keeps the stored value, and from 0.26.0 an explicit `null` removes it. Works at **any depth** (nested object / array row). | | `enum` | scalar | renders as a `