docs: update documentation to OKF v0.1 format

This commit is contained in:
OpenVelo Agent
2026-07-22 20:26:26 +00:00
parent 1a40e5708a
commit 6c18603a7d
8 changed files with 292 additions and 40 deletions
+46 -36
View File
@@ -3,7 +3,7 @@ type: database
title: Challenge Tables
description: category, challenge, challenge_file, and solve tables — how CTF challenges and scoring are stored.
tags: [database, challenge, category, solve]
timestamp: 2026-07-22T16:44:54Z
timestamp: 2026-07-22T20:30:00Z
---
# Tables
@@ -18,68 +18,80 @@ timestamp: 2026-07-22T16:44:54Z
| `abbreviation` | TEXT | Short label (e.g. `CRY`), 26 chars, server-uppercased. Globally unique — enforced by `uq_category_abbreviation`. |
| `description` | TEXT | Optional description. |
| `icon_path` | TEXT | Default icon URL (e.g. `/uploads/icons/CRY.png`). |
| `created_at` | TEXT | ISO 8601 timestamp the row was created. Declared in the initial `InitSchema1700000000000` migration (defaulting to `strftime('%Y-%m-%dT%H:%M:%fZ','now')`); the `AddCategoryTimestampsAndUniqueAbbrev1700000000200` migration adds it as a no-op for older pre-existing databases. |
| `updated_at` | TEXT | ISO 8601 timestamp the row was last updated (bumped on `PUT /api/v1/admin/categories/:id`). Same provenance as `created_at`. |
| `created_at` | TEXT | ISO 8601 timestamp the row was created. |
| `updated_at` | TEXT | ISO 8601 timestamp the row was last updated. |
System rows are seeded by the
`UpdateSystemCategoryKeys1700000000300` migration so the canonical
keys are `CRY` (Cryptography), `MSC` (Misc), `PWN` (Binary
exploitation), `REV` (Reverse Engineering), `WEB` (Web), and `HW`
(Hardware). A forward-only repair migration,
`RepairCategorySchemaAndSystemCategories1700000000400`, runs after
that one on existing databases that pre-date the canonical six keys
(it adds `created_at`/`updated_at` if missing, deletes obsolete
`FOR`/`OSI` system rows, rewrites name/abbreviation/description/icon
metadata on canonical rows, inserts any missing canonical row, and
guarantees unique indexes on `system_key` and `abbreviation`).
User-created categories leave `system_key` as `NULL`.
The uniqueness on `abbreviation` is enforced both by application
validation (uppercased on save, duplicates rejected) and by the
`uq_category_abbreviation` index created in migration
`1700000000200` (re-asserted by `1700000000400`).
(Hardware). The forward-only
`RepairCategorySchemaAndSystemCategories1700000000400` migration
repairs legacy schemas on existing databases.
## `challenge`
| Column | Type | Description |
|-------------------|----------|-----------------------------------------------------------------------------------|
| `id` | TEXT PK | UUID v4. |
| `name` | TEXT | Display name. |
| `name` | TEXT | Display name. **Case-insensitive uniqueness** enforced by `uq_challenge_name_lower`. |
| `description_md` | TEXT | Markdown description. |
| `category_id` | TEXT | FK → `category.id`. |
| `difficulty` | TEXT | `'low'` / `'med'` / `'high'`. |
| `initial_points` | INTEGER | Starting point value. |
| `minimum_points` | INTEGER | Floor after decay. |
| `decay_solves` | INTEGER | Number of solves at which decay reaches the minimum. |
| `flag` | TEXT | Submission string expected from players. |
| `protocol` | TEXT | `'nc'` (netcat-style connection) or `'web'` (browser-based). |
| `port` | INTEGER | TCP port when relevant (nullable). |
| `ip_address` | TEXT | IP the challenge listens on (defaulted from `setting.defaultChallengeIp`). |
| `created_at` | TEXT | ISO 8601 timestamp. |
| `category_id` | TEXT | FK → `category.id` (`ON DELETE RESTRICT`). |
| `difficulty` | TEXT | `'LOW'` / `'MEDIUM'` / `'HIGH'`. |
| `initial_points` | INTEGER | Starting point value (1100000). |
| `minimum_points` | INTEGER | Floor after decay (1100000, must be ≤ `initial_points`). |
| `decay_solves` | INTEGER | Solves at which decay reaches the minimum (110000). |
| `flag` | TEXT | Submission string expected from players. **Never returned in non-admin DTOs.** |
| `protocol` | TEXT | `'NC'` (netcat-style connection) or `'WEB'` (browser-based). |
| `port` | INTEGER | TCP port (nullable for WEB, required 165535 for NC). |
| `ip_address` | TEXT | IP the challenge listens on (defaults to `setting.defaultChallengeIp`). |
| `enabled` | INTEGER | Boolean — 1 (default) or 0. |
| `created_at` | TEXT | ISO 8601 timestamp the row was created. |
| `updated_at` | TEXT | ISO 8601 timestamp the row was last updated (bumped on every PUT). |
The `UpgradeChallengeAdminSchema1700000000500` migration rebuilt
the `challenge` table atomically (preserving legacy data), introduced
canonical enums, the `enabled` flag, `updated_at`, the
`LOWER(name)` unique index, and tighter scoring defaults. The
category FK stays at `ON DELETE RESTRICT` so the
`CATEGORY_HAS_CHALLENGES` business rule is preserved.
## `challenge_file`
| Column | Type | Description |
|---------------------|---------|--------------------------------------|
| `id` | TEXT PK | UUID v4. |
| `challenge_id` | TEXT | FK → `challenge.id`. |
| `challenge_id` | TEXT | FK → `challenge.id` (`ON DELETE CASCADE`). |
| `original_filename` | TEXT | Filename from upload. |
| `stored_path` | TEXT | Path on disk under `UPLOAD_DIR/challenges/`. |
| `stored_filename` | TEXT | UUID-based filename on disk under `UPLOAD_DIR/challenges/`; unique. |
| `mime_type` | TEXT | Source MIME type (defaults to `application/octet-stream`). |
| `size_bytes` | INTEGER | Decoded file size in bytes. |
| `created_at` | TEXT | ISO 8601 timestamp. |
The upgrade migration added `stored_filename`, `mime_type`,
`size_bytes`, and `created_at`. The challenge → file FK was changed
from `RESTRICT` to `CASCADE` so deleting a challenge in one DB
transaction removes all its `ChallengeFile` rows in lockstep.
## `solve`
| Column | Type | Description |
|------------------|---------|----------------------------------------------------------------------|
| `id` | TEXT PK | UUID v4. |
| `challenge_id` | TEXT | FK → `challenge.id`. |
| `user_id` | TEXT | FK → `user.id`. |
| `challenge_id` | TEXT | FK → `challenge.id` (`ON DELETE CASCADE`). |
| `user_id` | TEXT | FK → `user.id` (`ON DELETE RESTRICT`). |
| `solved_at` | TEXT | ISO 8601 timestamp the solve was recorded. |
| `points_awarded` | INTEGER | Points actually awarded (after decay). |
| `base_points` | INTEGER | Snapshot of `challenge.initial_points` at solve time. |
| `rank_bonus` | INTEGER | Extra points from rank (e.g. first-blood). |
| `is_first` | INTEGER | Boolean — first-blood flag. |
| `is_second` | INTEGER | Boolean — second-blood flag. |
| `is_third` | INTEGER | Boolean — third-blood flag. |
Uniqueness is enforced by `uq_solve_challenge_user` on
`(challenge_id, user_id)` — a user can solve a given challenge only once.
`uq_solve_challenge_user` on `(challenge_id, user_id)` is retained;
deleting a challenge now cascades to its solve rows, while deleting
a user still requires explicit handling (`user_id` remains
`RESTRICT`).
# Scoring formula
@@ -87,11 +99,9 @@ Uniqueness is enforced by `uq_solve_challenge_user` on
where `decay = max(0, base_points - minimum_points) * solvesBefore / decay_solves`,
and additional `rank_bonus` may be added for early solves.
(The exact scoring algorithm lives in the future solve-recording service
and is consumed by `SseHubService` whenever a new solve is published.)
# See also
- [Database Schema Overview](/database/schema.md)
- [System Endpoints](/api/system.md) (SSE scoreboard stream)
- [Scoreboard Stream](/guides/scoreboard-stream.md)
- [Admin — Challenges](/guides/admin-challenges.md)