marque / cennad
REST API for Marque tracker platform
Requires
- php: ^8.3
- illuminate/auth: ^13.0
- illuminate/http: ^13.0
- illuminate/routing: ^13.0
- illuminate/support: ^13.0
- marque/threepio: ^3.0
- marque/trove: ^4.5
Requires (Dev)
- laravel/pao: ^1.1
- laravel/sanctum: ^4.0
- mockery/mockery: ^1.6
- orchestra/testbench: ^11.0
- pestphp/pest: ^4.7
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
REST API for the Marque tracker platform. Provides JSON endpoints for torrent management.
Installation
Requires marque/trove.
composer require marque/cennad
Publish the config:
php artisan vendor:publish --tag=cennad-config
Endpoints
All endpoints require authentication by default and return JSON. Read endpoints can be opened to guests for a public tracker — see Authentication.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/torrents |
List torrents (paginated, searchable) |
| GET | /api/torrents/{id} |
Get torrent details |
| POST | /api/torrents |
Upload a torrent (Uploader+) |
| PUT | /api/torrents/{id} |
Update torrent (name, description) |
| DELETE | /api/torrents/{id} |
Delete torrent |
List Torrents
GET /api/torrents?search=ubuntu&page=2
Returns paginated results with standard Laravel pagination metadata.
Get Torrent
GET /api/torrents/1
Response:
{
"data": {
"id": 1,
"info_hash": "a1b2c3...",
"name": "Example Torrent",
"description": "Description text",
"size": 734003200,
"size_formatted": "700 MB",
"file_count": 12,
"seeders": 4,
"leechers": 1,
"has_torrent_file": true,
"created_at": "2026-01-15T10:30:00.000000Z",
"updated_at": "2026-01-15T10:30:00.000000Z",
"user": {
"id": 1,
"name": "uploader"
},
"links": {
"self": "https://example.com/api/torrents/1",
"download": "https://example.com/torrents/1/download"
}
}
}
Upload Torrent
POST /api/torrents
Content-Type: multipart/form-data
torrent_file=<file>, name=..., description=...
Returns 201 with the torrent, plus meta.warnings (any warnings from the tracker's rules,
e.g. a public torrent on a tracker that warns about them). The installed tracker's rules are
checked before anything is stored. A refused torrent is a 422 validation error on
torrent_file that says what to change: a public torrent on a private tracker, a v2-only
torrent, or a file that isn't a torrent at all.
Update Torrent
PUT /api/torrents/1
Content-Type: application/json
{
"name": "Updated Name",
"description": "Updated description"
}
Requires ownership or Moderator+ role.
Delete Torrent
DELETE /api/torrents/1
Requires Moderator+ role. Returns 204 No Content.
Authorization
Cennad uses Trove's TorrentPolicy for access control:
| Action | Who Can |
|---|---|
| List | Anyone read_middleware lets through. The list only holds torrents the viewer's role may see |
| View | The same, except a torrent with a min_role needs a viewer at that role or above (guests see only unrestricted torrents) |
| Update | Torrent owner or Moderator+ |
| Delete | Moderator+ |
Configuration
Published to config/cennad.php:
| Key | Default | Description |
|---|---|---|
prefix |
api |
URL prefix for all endpoints |
read_middleware |
['api', 'auth:api'] |
Middleware for index and show |
write_middleware |
['api', 'auth:api'] |
Middleware for store, update, destroy |
route_names.prefix |
cennad |
Route name prefix |
route_names.download |
torrents.download |
Download route name (for link generation) |
rate_limit |
60 |
Requests per minute, per user (per IP for guests). 0 or null turns it off |
public_middleware and protected_middleware are the pre-4.0 names for read_middleware
and write_middleware. They still work and still take precedence, but they emit a
deprecation notice and are removed in 5.0.
Rate limiting
Every cennad route carries throttle:cennad, a named limiter allowing rate_limit
requests a minute (60 by default). Reads and writes share one count. Signed-in users are
counted by their id, and everyone else by IP. On read routes opened to guests no guard
runs, so a client sending a token there is counted by IP too. Past the limit a request
gets 429 Too Many Requests with a Retry-After header. While the limit is on, every
response carries X-RateLimit-Limit and X-RateLimit-Remaining.
The limiter is appended after your read_middleware and write_middleware, so replacing
those lists can't drop it by accident. To turn it off, for example because your app
already throttles its API, set CENNAD_RATE_LIMIT=0. For a different shape (per token,
or one limit for reads and another for writes), redefine the cennad limiter in your own
service provider after cennad's has booted:
RateLimiter::for('cennad', fn (Request $request) => Limit::perMinute(300)->by($request->user()?->id ?: $request->ip()));
Authentication
The default middleware uses auth:api, which needs a guard named api in
config/auth.php. Stock Laravel defines only web, and neither passport:install nor
install:api adds one, so you add it yourself. With Passport, that's
'api' => ['driver' => 'passport', 'provider' => 'users']. With Sanctum, either define 'api' => ['driver' => 'sanctum', 'provider' => null],
or set read_middleware and write_middleware to ['api', 'auth:sanctum']. Any guard
works, as long as the middleware names it.
Reads and writes are configured separately so a public tracker can expose its catalogue without exposing its write endpoints. Both default to requiring authentication, because Cennad cannot tell whether it is serving a private tracker (bloodhound/guise) or a public one (hound/disguise), and the safe assumption is the private one.
To open the catalogue to unauthenticated visitors, drop the guard from read_middleware:
'read_middleware' => ['api'],
Leave write_middleware alone when you do — uploads, edits, and deletes are still
governed by Trove's TorrentPolicy on top of whatever middleware you set.
Requirements
- PHP 8.3+
- Laravel 13+
- marque/trove
License
MIT. See LICENSE.