Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
__pycache__/

# Plugin binaries are never committed to this metadata-only repository.
# Packaged extensions are hosted on SourceForge.
*.orplug
154 changes: 154 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Contributing an Extension to the OpenROM Marketplace

This repository is the **metadata-only** catalog behind the OpenROM Marketplace
(`openrom-store.pages.dev`). It holds publisher records, extension manifests, and
the static storefront.

**Binary `.orplug` bundles are never committed here.** They are hosted on
SourceForge and referenced from your manifest by download URL and SHA-256
checksum. `*.orplug` is git-ignored.

Submitting an extension takes five steps.

---

## 1. Publish your `.orplug` bundle to SourceForge

1. Build your extension and package it as `<plugin-id>-<version>.orplug`.
2. Upload it to your SourceForge project's release files.
3. Copy the **direct download URL** for the uploaded file, for example:

```
https://downloads.sourceforge.net/project/<project>/plugins/<plugin-id>-<version>.orplug
```

Use the `downloads.sourceforge.net` form so mirrors and redirects work.

## 2. Compute the SHA-256 checksum

Every extension is verified against the checksum in its manifest before it is
listed. Generate it from the exact file you uploaded:

```bash
sha256sum <plugin-id>-<version>.orplug # Linux
shasum -a 256 <plugin-id>-<version>.orplug # macOS
certutil -hashfile <plugin-id>-<version>.orplug SHA256 # Windows
```

Paste the 64-character lowercase hex digest into the manifest's `sha256` field.

## 3. Register as a publisher

Create `publishers/<publisher-id>.json` (use a short, lowercase, hyphenated id —
this is the same id you will use in your manifest):

```json
{
"id": "your-publisher-id",
"name": "Your Display Name",
"github_username": "your-github-handle",
"github": "https://github.com/your-org",
"avatar_url": "https://github.com/your-github-handle.png",
"bio": "One sentence about you or your team.",
"is_verified": false,
"badge": null
}
```

> **Verification is granted by maintainers, never self-declared.** The
> authoritative list is `verified_publishers.json`, which only maintainers edit.
> Any `is_verified` / `verified` value in your publisher file or manifest is
> ignored, and claiming verification in a manifest fails validation.

## 4. Add your extension manifest

Manifests live at `manifests/m/<publisher-id>/<plugin-id>/<version>/manifest.json`
— note the `m/` shard directory. The `<publisher-id>` path segment **must** match
the manifest's `publisher_id`, or validation fails.

```json
{
"id": "your-plugin-id",
"name": "Your Plugin Name",
"version": "1.0.0",
"author": "Your Display Name",
"publisher_id": "your-publisher-id",
"min_core_version": "3.9.0",
"license": "MIT",
"description": "One or two sentences shown on the storefront card.",
"homepage": "https://github.com/your-org/your-plugin",
"icon": "gamepad",
"download_url": "https://downloads.sourceforge.net/project/<project>/plugins/<plugin-id>-<version>.orplug",
"sha256": "<64-char lowercase hex digest from step 2>",
"tags": ["nintendo", "wua"],
"extensions": [".iso", ".wud", ".wua"],
"binaries": {
"win32": "your-tool.exe",
"linux": "your-tool",
"darwin": "your-tool"
},
"conversions": [
{ "from": "ISO", "to": "WUA", "command": "{bin} convert --in {input} --out {output}" }
]
}
```

### Field reference

| Field | Required | Notes |
| --- | --- | --- |
| `id` | yes | Lowercase, hyphenated, unique across the store |
| `name` | yes | Display name on the storefront |
| `version` | yes | Semantic version, e.g. `1.0.0` |
| `author` | yes | Human-readable author name |
| `publisher_id` | yes | Must match the `publishers/` id **and** the manifest path |
| `description` | yes | Short summary shown on the card |
| `download_url` | yes | Absolute `https://` URL to your SourceForge `.orplug` |
| `sha256` | yes | Lowercase hex digest of the uploaded bundle |
| `conversions` | yes | Non-empty list of `from` / `to` route mappings |
| `min_core_version` | no | Lowest compatible OpenROM Core version |
| `license` | no | SPDX identifier (e.g. `MIT`, `GPL-3.0`) |
| `homepage` | no | Project or source URL |
| `icon` | no | A [Lucide](https://lucide.dev) icon name, or an absolute image URL |
| `tags` | no | Search keywords |
| `extensions` | no | File extensions your tool accepts |
| `binaries` | no | Per-platform executable names inside the bundle |

## 5. Open a pull request

1. Fork this repository and create a branch, e.g. `add/your-plugin-id`.
2. Add your publisher file and manifest. **Do not** add any `.orplug` file —
the pull request will be rejected if binaries are included.
3. Run the checks locally:

```bash
python scripts/validate_plugin.py # validates every manifest
python scripts/build_catalog.py # regenerates public/api/catalog.json
python -m unittest discover tests # security tests
```

4. Commit, push, and open a pull request against `main`.

CI runs `scripts/validate_plugin.py` and `scripts/build_catalog.py` on every pull
request. On merge to `main` it regenerates `public/api/catalog.json` and Cloudflare
Pages redeploys the storefront.

## What reviewers check

- The `download_url` resolves to the uploaded bundle on SourceForge.
- The `sha256` matches that exact bundle.
- `publisher_id` matches both the publisher record and the manifest directory.
- `conversions` are accurate and the description is clear.
- No binaries, no secrets, no self-declared verification.

Once merged, your extension appears in the marketplace automatically and can be
installed with:

```bash
openrom-core --install-plugin <plugin-id>
```

## Questions

Open a [Discord](https://discord.gg/openrom) thread or a draft pull request and a
maintainer will help you get it over the line.
110 changes: 109 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,109 @@
# OpenROM-Plugins-Store
# OpenROM Marketplace

The metadata-only extension storefront for the **OpenROM** retro-gaming preservation
toolchain. This repository hosts the catalog, extension manifests, and the static
web storefront deployed to Cloudflare Pages at **openrom-store.pages.dev**.

> **This repository never stores plugin binaries.** Packaged `.orplug` bundles live
> exclusively on SourceForge and are referenced from the catalog by URL + SHA-256.
> `*.orplug` is git-ignored and has been purged from the entire Git history.

## Layout

| Path | Purpose |
| --- | --- |
| `public/` | Static storefront (Cloudflare Pages build output) |
| `public/index.html` | Single-page marketplace app (Tailwind + Alpine.js) |
| `public/plugins.json` | Showcase catalog consumed by the storefront |
| `public/api/catalog.json` | Generated catalog API (built by CI) |
| `manifests/m/<publisher>/<plugin>/<version>/manifest.json` | Per-extension manifests |
| `publishers/` | Publisher records |
| `scripts/` | Validation, catalog build, and packaging tooling |
| `schema.sql` | Cloudflare D1 schema (optional dynamic backend) |

## Extension data contract (`public/plugins.json`)

The storefront reads a flat JSON array of extension objects:

```json
[
{
"id": "nkit",
"name": "NKit",
"version": "3.1.0",
"minCoreVersion": "3.9.0",
"publisher": { "id": "nanook", "name": "Nanook", "verified": true },
"category": "Nintendo",
"description": "Lossless GameCube & Wii preservation, WUA conversion, and Redump restoration.",
"readme": "### Features\n- 1:1 Redump disc verification\n- WUA compression\n- Lossless GameCube scrubbing",
"routes": ["ISO -> WUA", "WUD -> Loadiine", "GCZ -> ISO"],
"platforms": ["Windows", "Linux", "macOS"],
"downloadUrl": "https://downloads.sourceforge.net/project/openrom-binaries/plugins/nkit-3.1.0.orplug",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"icon": "gamepad",
"downloads": 1420,
"lastUpdated": "2026-10-25",
"license": "MIT",
"tags": ["nintendo", "gamecube", "wii", "wua"],
"rating": 4.9
}
]
```

`icon` is a [Lucide](https://lucide.dev) icon name; `routes` are human-readable
`INPUT -> OUTPUT` mappings rendered as visual badges in the details drawer.

## Storefront features

- **Header** with OpenROM Marketplace branding, live search, GitHub / Discord links,
and a **Publish an Extension** call to action.
- **Discovery**: category sidebar (All, Nintendo, PlayStation, Sega, Xbox, Utility,
Patchers), platform chips (Windows / Linux / macOS), publisher filter, and instant
client-side search across name, description, publisher, category, tags and routes.
- **Extension cards** with icon, name, verified-publisher badge, download count,
rating, category, route preview, and a 1-click *Copy CLI install* button.
- **VS Code-style details drawer** with a tabbed interface:
*Overview* (rendered Markdown), *Supported Routes* (`ISO ➔ WUA` badges),
*Installation* (CLI + manual GUI drag-and-drop), and *Manifest* (raw JSON viewer).
- **Dark theme** by default (zinc-900/950 with neon cyan/purple accents).

## Local preview

```bash
python3 -m http.server 12000 --directory public
# open http://localhost:12000
```

## Deploy to Cloudflare Pages

The app is 100% static. Point a Pages project at this repository with:

- **Build output directory:** `public`
- **Build command:** *(none)*

Or deploy directly:

```bash
npx wrangler pages deploy public --project-name openrom-store
```

Security headers and legacy redirects are declared in `public/_headers` and
`public/_redirects`.

## Publishing an extension

1. Add `manifests/m/<publisher-id>/<plugin-id>/<version>/manifest.json`.
2. Ensure a matching record exists at `publishers/<publisher-id>.json`.
3. Open a pull request. CI runs `scripts/validate_plugin.py` and
`scripts/build_catalog.py`, then commits the regenerated `public/api/catalog.json`.

Manifests may not self-declare verification, and the manifest's `publisher_id`
must match its directory path — see `tests/test_security.py`.

## Tooling

```bash
python scripts/validate_plugin.py # validate manifests
python scripts/build_catalog.py # regenerate public/api/catalog.json
python -m unittest discover tests # run the security test suite
```
Binary file removed packages/nintendo-nkit.orplug
Binary file not shown.
Binary file removed packages/saturn-ssp.orplug
Binary file not shown.
Binary file removed packages/xbox360-tools.orplug
Binary file not shown.
19 changes: 19 additions & 0 deletions public/_headers
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Cloudflare Pages headers for the OpenROM Marketplace (openrom-store.pages.dev)

/*
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=(), interest-cohort=()
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.tailwindcss.com https://cdn.jsdelivr.net https://unpkg.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'

/index.html
Cache-Control: public, max-age=0, must-revalidate

/plugins.json
Content-Type: application/json; charset=utf-8
Cache-Control: public, max-age=300

/api/*
Content-Type: application/json; charset=utf-8
Cache-Control: public, max-age=300
5 changes: 5 additions & 0 deletions public/_redirects
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# The storefront is now a single-page app; legacy deep links resolve to it.
/plugin.html /index.html 301
/publisher.html /index.html 301
/plugins /index.html 301
/store /index.html 301
2 changes: 1 addition & 1 deletion public/api/catalog.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": "1.0",
"generated_at": "2026-10-17T15:35:08.536769+00:00",
"generated_at": "2026-11-02T01:26:28.684907+00:00",
"total_plugins": 3,
"plugins": [
{
Expand Down
Loading
Loading