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
42 changes: 32 additions & 10 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,42 @@
## Project Structure & Module Organization

- `src/app/index.ts` is the worker entrypoint and route wiring for Hono.
- Feature logic lives in `src/services/` (for example `central-alerts/v1`, `versions/v1`, `stats/v1`) and should stay runtime-agnostic.
- Feature logic lives in `src/services/` and should stay runtime-agnostic.
- Platform interfaces and adapters are in `src/lib/` with Cloudflare and Node implementations under `src/lib/adapters/`.
- Tests mirror the source layout under `test/`, with shared helpers in `test/utils/` and mocks in `test/mocks/`.
- Runtime/config files include `wrangler.jsonc`, `worker-configuration.d.ts`, `tsconfig.json`, `eslint.config.ts`, and `prettier.config.ts`.

### Service layout

Small services are flat: `index.ts`, `interfaces.ts`, optionally `database.ts`
and `db/` (see `central-alerts/v1`, `versions/v1`, `stats/v1`).

`extensions/v2` is the reference layout for anything larger, and new services
should grow into it rather than inventing a third shape:

- `index.ts` — app assembly only: middleware, route registration, OpenAPI
document. Route registration order is load-bearing where static paths must
beat parameter paths; those cases carry comments.
- `middleware.ts` — service-specific Hono middleware.
- `routes/` — one module per route group, each exporting `register*Routes(app)`.
`routes/errors.ts` maps domain error codes to HTTP status; `routes/app.ts`
holds the typed app alias.
- `db/` — `schema.ts`, `migrations/`, one `*Database` class per workflow, plus
`errors.ts` (D1 constraint classification) and `batch.ts`.
- `schemas/` — zod/OpenAPI contract split by domain. There is deliberately **no
barrel**: import from `schemas/<domain>` directly so a module's dependencies
are visible. This is why `extensions/v2` has no `interfaces.ts`.
- `github/` — outbound GitHub calls, kept out of the persistence modules.

Route modules import `getExtensionsDb`/`getAuth`/`getPlatform` and middleware
directly. There is no dependency-injection container; tests drive the real app
through `app.request`.

Each service documents its own contract and operational detail in its own
`README.md` (`src/services/<name>/<version>/README.md`). Keep API behaviour
there rather than here or in the root README: this file is for conventions that
apply when modifying the code.

## Build, Test, and Development Commands

- `npm install`: install dependencies.
Expand Down Expand Up @@ -49,12 +80,3 @@
`ASSERTION_SIGNING_SECRET="..."` to `.dev.vars` for local dev; set via
`wrangler secret put ASSERTION_SIGNING_SECRET` in production, matching the value
configured in the extensions site's Worker.

## Stats API v1

- Provides release statistics visualization for FOSSBilling versions.
- HTML endpoint: `GET /stats/v1/` - Returns a client-side rendered page with Chart.js visualizations.
- Data endpoint: `GET /stats/v1/data` - Returns aggregated statistics data for the charts.
- Charts include: Release Size Graph (line), PHP Version Requirements (line), Patches Per Release (bar), and Releases Per Year (bar).
- Stats data is cached with a TTL of 24 hours and reuses release data from the versions service.
- Service follows the same caching patterns as versions API, including graceful handling of GitHub API errors.
87 changes: 21 additions & 66 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,49 +15,34 @@ The worker exposes three main services:
Allows the project to push critical notifications to all FOSSBilling installations—useful for security hotfixes or major announcements.

- **Extensions** (`/extensions/v1`, `/extensions/v2`)
Owns the complete Extensions domain and its `DB_EXTENSIONS` schema, including
users, developers, submissions, claims, transfers, history, and catalogue data.
The separate Extensions site keeps OIDC/session state but accesses this domain
through the generated HTTPS API client; it must not bind or migrate `DB_EXTENSIONS`.
Owns the complete Extensions domain and its `DB_EXTENSIONS` schema. The separate Extensions site keeps OIDC/session state but accesses this domain through the
generated HTTPS API client; it must not bind or migrate `DB_EXTENSIONS`.
See [`src/services/extensions/v2/README.md`](src/services/extensions/v2/README.md).

## Architecture

We've structured the app to separate the core logic from the specific runtime environment (Cloudflare, Node, etc.).

- **Application Logic**: Found in `src/services/versions/v1`, `src/services/central-alerts/v1`, etc. These feature modules don't know they are running on Cloudflare.
Smaller services are a flat `index.ts` + `interfaces.ts`; `src/services/extensions/v2` is the reference layout for larger ones, splitting into `routes/`, `db/`, `schemas/`, and `github/`.
See `AGENTS.md` for what belongs in each.
- **Platform Layer**: Located in `src/lib`. This defines interfaces for things like Cache, Database, and Environment variables.
- **Adapters**:
- `src/lib/adapters/cloudflare`: Real implementations using KV and D1.
- `src/lib/adapters/node`: Reference implementations (useful for testing or alternative deployments).
- `src/lib/adapters/cloudflare`: Real implementations using KV and D1.
- `src/lib/adapters/node`: Reference implementations (useful for testing or alternative deployments).

## APIs

### Versions (`/versions/v1`)
Each service documents its own endpoints and behaviour:

- `GET /versions/v1` - List all releases.
- `GET /versions/v1/:version` - Get details for a specific version (e.g. `1.0.0`); use `latest` to get the newest release.
- `GET /versions/v1/build_changelog/:current` - Generates a consolidated changelog for all releases greater than `:current` (in semantic version order).
- `GET /versions/v1/update` - Refreshes the releases cache. Requires bearer token authentication using `Authorization: Bearer <UPDATE_TOKEN>`.
| Service | Base path | Docs |
| -------------- | ---------------------------------- | -------------------------------------------------------------------------------------- |
| Versions | `/versions/v1` | [`src/services/versions/v1/README.md`](src/services/versions/v1/README.md) |
| Central Alerts | `/central-alerts/v1` | [`src/services/central-alerts/v1/README.md`](src/services/central-alerts/v1/README.md) |
| Stats | `/stats/v1` | [`src/services/stats/v1/README.md`](src/services/stats/v1/README.md) |
| Extensions | `/extensions/v1`, `/extensions/v2` | [`src/services/extensions/v2/README.md`](src/services/extensions/v2/README.md) |

All version responses include a `stale` field that indicates whether the data was served from cache after a failed fetch.

### Central Alerts (`/central-alerts/v1`)

- `GET /central-alerts/v1/list` - Public endpoint for fetching active alerts.

### Extensions v2 ownership verification

For organization developer IDs, GitHub membership is used for automatic
verification only when the API has a valid, unexpired membership snapshot. A
fresh snapshot that does not contain the organization remains a confirmed
mismatch and is rejected. Missing, malformed, or expired evidence is
inconclusive instead: a new profile remains unapproved and a claim remains
pending for manual moderator review. Moderators must verify ownership through
their normal out-of-band process before approving either workflow.

`github_org_verified` being absent or `null` is a review signal, not proof of
ownership or an authorization grant. Consumers and moderation tooling must not
treat an inconclusive result as verified.
Extensions v2 also publishes a live OpenAPI document at `/extensions/v2/openapi.json` and a reference UI at `/extensions/v2/docs`.

## Configuration

Expand All @@ -68,40 +53,18 @@ If you're running this yourself, you'll need a few things set up.
We use [Cloudflare D1](https://developers.cloudflare.com/d1/) and [KV](https://developers.cloudflare.com/kv/).

- **D1 Database** (`DB_CENTRAL_ALERTS`): Stores the alert messages.
- **D1 Database** (`DB_EXTENSIONS`): Stores the complete Extensions domain. Apply
its migrations only from this repository, from
`src/services/extensions/v2/db/migrations`, with
`db:migrate:extensions-v2:*`. The Extensions site has no D1 migration source.
The `0000` users bootstrap mirrors the complete table created by the former
site migration, so it is safe to re-run against the existing split-owned
database without replacing rows; `0019` then adds the API-owned tombstone
column. Back up the database and inspect `PRAGMA table_info(users)` before
adoption, as with any schema ownership change.

- **D1 Database** (`DB_EXTENSIONS`): Stores the complete Extensions domain.
Migrations are owned by extensions v2 and applied only from this repository — see [its README](src/services/extensions/v2/README.md#database) for the migration and adoption procedure.
- **KV Namespace** (`CACHE_KV`): Caches GitHub API responses so we don't hit rate limits.
- **KV Namespace** (`AUTH_KV`): Stores the `UPDATE_TOKEN` value for `/versions/v1/update`.

### Environment Variables

- `GITHUB_TOKEN`: A GitHub Personal Access Token (classic) with public repo read access.
- `ASSERTION_SIGNING_SECRET`: Shared HMAC secret used to verify the short-lived
bearer assertions minted by the Extensions site. Configure the same value
in both Workers; it is never sent to clients.
- `ASSERTION_SIGNING_SECRET_PREVIOUS`: Optional previous HMAC secret accepted
during a signing-key rotation. Remove it after the new secret has been active
for at least 65 seconds and all in-flight assertions have expired.

Extensions assertions use HS256 and include the exact issuer
`fossbilling-extensions`, audience `fossbilling-api/extensions-v2`, purpose
`user-authentication`, and protocol version `1`. Assertions are valid for at
most 60 seconds; the previous secret is accepted only as a temporary rotation
window.

To rotate the shared secret without interrupting requests, first set the API's
`ASSERTION_SIGNING_SECRET_PREVIOUS` to the current value, then replace the API's
active `ASSERTION_SIGNING_SECRET`, and finally replace the Extensions site's
active secret. After at least 65 seconds, verify requests and remove the API
previous secret.
- `ASSERTION_SIGNING_SECRET`: Shared HMAC secret used to verify the short-lived bearer assertions minted by the Extensions site. Configure the same value in both Workers; it is never sent to clients.
- `ASSERTION_SIGNING_SECRET_PREVIOUS`: Optional previous HMAC secret accepted during a signing-key rotation.

Only extensions v2 consumes these. For the assertion format and the rotation procedure, see [its README](src/services/extensions/v2/README.md#authentication).

## Development

Expand Down Expand Up @@ -150,11 +113,3 @@ We use Vitest for testing. The suite includes unit tests for the endpoints and i
```bash
npm run test
```

### Extensions v2 list pagination

`GET /extensions/v2/extensions` returns bounded pages of lightweight catalogue
items. List items intentionally omit `readme` and `releases`; retrieve the full
object from `GET /extensions/v2/extensions/{id}` for detail views. Follow
`pagination.next_cursor` by passing it unchanged as `cursor`, and treat cursors
as opaque. The default page size is 50 and `limit` may be set from 1 through 100.
Loading