Diagnostics Ready (Capsules)

A capsule is a small, signed, data-only .rphla file a plugin developer ships next to their DLL. It binds the exact release (assembly filenames and SHA-256 hashes) to the plugin's identity, declared dependencies, edition compatibility, and official links. RPH Log Analyzer reads capsules during analysis and shows a Developer support section in the report.

Everything happens locally: the analyzer never uploads DLLs, capsules, or configuration, never loads or executes plugin code, and works fully offline. The developer's draft is generated locally too — only the draft JSON (metadata and hashes) travels, by the developer's own hand, to the maintainer for approval.

This page is a developer reference. If you just want to describe your install layout, see the install-layout language.

What the badge means — and doesn't

A Verified capsule proves exactly one thing: the installed files match a release the developer registered through RPHLA's manual approval process. It is provenance of metadata.

It must never be presented as malware-free, endorsed by LSPDFR/RPH, guaranteed compatible, or "safe". UI wording is deliberately conservative for this reason.

Non-negotiable trust rules

  • A capsule is data. It is never loaded, executed, or used to run declarative logic.
  • A capsule failure never blocks analysis — every failure degrades to a status on the finding.
  • Component paths are bare .dll filenames; a capsule can only describe files in its own folder. Traversal is rejected structurally (regex) and re-proven by full-path containment at scan time.
  • Official/support links render only for validly signed capsules, and always pass the app's leaving-the-app confirmation. HTTPS only; Discord links must be discord.gg / discord.com.
  • Findings carry relative paths only (absolute paths leak usernames).
  • The signature covers the exact base64url payload bytes as they appear in the file — there is no JSON canonicalization step to attack.

File format

A .rphla file is UTF-8 JSON (optional BOM tolerated), hard-capped at 64 KB:

capsule.rphla
{
  "format": "rphla.capsule/1",
  "payload": "<base64url of the payload JSON bytes>",
  "signatures": [
    { "alg": "ES256", "keyId": "rphla-2026-a", "sig": "<base64url ECDSA P-1363 signature>" }
  ]
}
  • alg is pinned to ES256 (ECDSA P-256 + SHA-256). The verifier checks this constant rather than letting the file choose a mechanism.
  • Up to 4 signatures (key rotation overlap). The signed bytes are the ASCII bytes of the payload string.
  • Strict parsing everywhere: duplicate JSON keys rejected at every nesting level, unknown fields rejected, max depth 8, max string 4096, trailing commas/comments rejected.

Payload (decoded)

payload.json
{
  "schema": 1,
  "plugin": {
    "id": "developer.plugin-name",
    "name": "My Plugin",
    "developer": "Developer Name",
    "resource": "https://www.lcpdfr.com/downloads/..."
  },
  "release": { "version": "2.4.0", "sequence": 12 },
  "components": [
    { "file": "MyPlugin.dll", "role": "plugin", "sha256": "<64 hex, lowercase>" },
    { "file": "MyPlugin.Common.dll", "role": "owned-library", "sha256": "..." }
  ],
  "dependencies": [
    { "assembly": "RAGENativeUI.dll", "minimumVersion": "1.9.3", "optional": false }
  ],
  "install": [
    { "path": "plugins/LSPDFR/MyPlugin.ini", "rule": "require", "note": "your settings file" },
    { "path": "plugins/LSPDFR/MyPlugin/", "rule": "require" },
    { "path": "plugins/LSPDFR/MyPlugin/sounds/", "rule": "optional" },
    { "path": "scripts/MyPlugin.dll", "rule": "forbid", "note": "the old 1.x location" }
  ],
  "compatibility": { "editions": ["legacy", "enhanced"] },
  "support": { "discord": "https://discord.gg/...", "url": "https://..." }
}

Field rules

FieldRule
plugin.iddotted lowercase ([a-z0-9-] segments, 2–5 segments), permanent identity
plugin.name, developer≤ 200 chars, no control or bidi-override characters
release.versiondisplay string, ≤ 50 chars (no semver requirement)
release.sequenceregistry release number, 0–1,000,000,000, stamped at signing
components[]1–16; bare .dll filename (no separators, drive colons, ADS, reserved device names, leading/trailing dots or spaces); unique case-insensitively; role ∈ plugin, owned-library
components[].sha25664 lowercase hex — SHA-256 of the exact shipped file
dependencies[]0–32; same filename rules; must not duplicate an owned component; third-party files a developer does not attest to
install[]0–64 installation-layout entries: GTA-root-relative path (trailing / = folder) + rule ∈ require, optional, forbid. Relative only, ≤ 6 segments, ≤ 180 chars, no traversal/drive/UNC/ADS/reserved names, unique case-insensitively. Checked read-only (existence only)
install[].noteoptional; the developer's one-phrase explanation from the layout language's "note" syntax, ≤ 120 chars, display-text rules (no control or bidi-override characters). Shown beside the path in the report and the public install guide
compatibility.editionssubset of legacy, enhanced
support / resourceHTTPS, ≤ 300 chars, no embedded credentials; Discord host allowlisted

Draft format

<Plugin>.rphla-draft.json — the unsigned artifact a developer generates in the app (Settings → Developer) and sends for approval. Identical payload, human-readable, distinct marker:

draft.json
{ "format": "rphla.draft/1", "payload": { ... } }

Drafts validate under the same rules as capsules. A draft renamed to .rphla is rejected as malformed by the scanner (no capsule format marker, no signature).

Verification statuses

StatusMeaningReport tone
Verifiedsignature valid, all components present with matching hashesgreen “Diagnostics Ready”
ComponentMismatchsigned, but a file's bytes differ (updated separately / damaged)calm repair advice
ComponentMissingsigned, but a declared file is absentcalm repair advice
UnknownKeysigned with a key this app build doesn't know (likely newer)informational
SignatureInvalidpayload doesn't match its signature; claims unusedcalm, suggest reinstall
UnsupportedFormatrphla.capsule/N newer than this buildinformational, skipped
Malformed / Oversized / Unreadableinvalid or unreadable inputquiet, skipped

Per-component: Ok, Missing, HashMismatch, Unreadable, TooLarge (> 64 MB hashing budget), Reparse (symlink/junction — skipped, not judged). Per-dependency (best-effort, informational, never demotes the capsule): Found, Missing, TooOld, VersionUnknown.

Scanner limits: the three plugin load locations only (GTA root, Plugins, Plugins\LSPDFR), non-recursive, max 64 capsules per scan, deterministic alphabetical order, duplicate plugin ids flagged on the later capsule.

Workflow — developer

Five minutes, and the hashing stays local.

  1. Settings → Developer → fill in plugin identity → pick the release DLLs (main first).
  2. The app hashes them locally and submits only the draft metadata for approval. DLLs, ZIPs, and source never leave your PC.
  3. The submission appears under Submissions; once approved, Check status saves the signed <Plugin>.rphla next to the DLL automatically. Include it in the release ZIP.
  4. Each new release: repeat. New file bytes = new hashes = new submission.
  5. Offline fallback: when the backend is unreachable the app writes <Plugin>.rphla-draft.json locally instead, for the manual Discord path.

Workflow — maintainer

Manual approval is the trust step.

  1. Each submission arrives as a Discord message (private review channel) with the plugin identity, file hashes, contact, and Approve & sign / Reject buttons.
  2. Before approving: confirm the sender controls the LCPDFR resource named in the draft, and that the hashes match the official download (not a private rebuild).
  3. Approve & signassigns the plugin's next release sequence, signs server-side, and stores the capsule; the approved row is the v1 issuance record. Only the configured admin can press the buttons, verified through Discord's Ed25519 request signatures.
  4. Manual fallback (backend down / offline signing): the CLI still works — rphla-capsule sign <draft> --key <key>.

Keys, rotation, revocation (v1 policy)

  • Signing keys are P-256 and never committed. The production private key lives in the backend's function secrets so approval-in-Discord can sign automatically; keep an offline backup. The app ships only public halves in its trusted-keys table.
  • Rotation: add the new keyId + SPKI to the app while keeping the old, sign new capsules with the new key, retire the old id in a later release. Capsules carry up to 4 signatures for overlap.
  • Revocation v1: removing a keyId from the trust table (app update) demotes everything it signed to UnknownKey (informational, never alarming). Per-release revocation and developer-owned certificates are future work — the format's sequence and multi-signature fields exist so those can be added without a format break.
  • A compromised key is handled by rotation plus a note in the release notes. Because UnknownKey wording is calm by design, this degrades safely for offline users.

Threats explicitly covered by tests

Tampered DLLs, forged payloads, wrong signing keys, unknown keys, duplicate JSON keys, unknown extra fields, path traversal / ADS / reserved device names, bidi-spoofed display text, depth bombs, oversized files, junk and binary content, newer formats, duplicate capsules, drafts renamed to .rphla, and scan caps.

Still need help? Contact support.