chore: address Plan 4 deferred-minors (stale-cookie cleanup pass)
CI / php (push) Failing after 1m11s

- Rename $secret to $csrfSecret in public/index.php to make
  intent clear at every call site; the value is the CSRF/turn-token
  HMAC key, not just 'the secret'.
- Drop the stale 'Configure the router with the eight routes' comment
  in public/index.php; the router now has sixteen routes.
- Drop the duplicate .bf-toast rule in public/assets/styles.css; the
  second declaration (the new yellow box) is the active one, the
  first (legacy green pill) is dead code from the editor era.
- Add a trailing newline to src/Views/home.php.
- Tighten the matchId regex in two tests from {16,} to {16} since
  bin2hex(random_bytes(8)) always produces exactly 16 hex chars; the
  spec's {16,} stays in production TurnToken.

No behavior changes; addresses the deferred-minors parked during
per-task review.
This commit is contained in:
Keith Solomon
2026-07-26 16:38:05 -05:00
parent 3185bc3602
commit 44199ec0a8
5 changed files with 23 additions and 21 deletions
-1
View File
@@ -18,7 +18,6 @@ table.bf-grid button[data-objective="1"] { outline: 3px solid #c00; }
table.bf-grid button[data-zone="alpha"] { outline: 3px solid #00c; }
table.bf-grid button[data-zone="bravo"] { outline: 3px solid #0c0; }
.bf-errors { color: #c00; }
.bf-toast { background: #dfd; padding: 0.5rem 1rem; margin: 0.5rem 0; }
.bf-match { display: flex; gap: 1rem; }
.bf-grid { display: grid; gap: 1px; background: #ddd; padding: 1px; width: max-content; grid-template-columns: repeat(var(--bf-cols, 8), 2.5rem); }
.bf-tile { width: 2.5rem; height: 2.5rem; border: 0; background: #fff; padding: 0; position: relative; }
+20 -17
View File
@@ -25,8 +25,11 @@ use BattleForge\Http\Router;
require __DIR__ . '/../vendor/autoload.php';
// 1. Read the app secret.
$secret = getenv('BATTLEFORGE_SECRET');
if ($secret === false || $secret === '') {
// The app secret reads as the CSRF/turn-token HMAC key. The name below
// (kept for historical reasons) is just the variable label; the same
// value is used for both the CSRF cookie HMAC and the turn-token HMAC.
$csrfSecret = getenv('BATTLEFORGE_SECRET');
if ($csrfSecret === false || $csrfSecret === '') {
$secretFile = __DIR__ . '/../var/secret.key';
if (!is_file($secretFile)) {
if (!is_dir(dirname($secretFile))) {
@@ -35,8 +38,8 @@ if ($secret === false || $secret === '') {
file_put_contents($secretFile, random_bytes(32));
chmod($secretFile, 0600);
}
$secret = file_get_contents($secretFile);
if ($secret === false) {
$csrfSecret = file_get_contents($secretFile);
if ($csrfSecret === false) {
http_response_code(500);
echo "Failed to read app secret.\n";
return;
@@ -61,11 +64,11 @@ $existingCookie = (string) ($cookies['__csrf'] ?? '');
$existingRawToken = (string) ($cookies['__csrf_token'] ?? '');
$pairIsFresh = false;
if ($existingCookie !== '' && $existingRawToken !== '') {
$expectedHmac = hash_hmac('sha256', $existingRawToken, $secret);
$expectedHmac = hash_hmac('sha256', $existingRawToken, $csrfSecret);
$pairIsFresh = hash_equals($expectedHmac, $existingCookie);
}
if ($existingCookie === '' || $existingRawToken === '' || !$pairIsFresh) {
[$token, $cookie] = CsrfToken::issue($secret);
[$token, $cookie] = CsrfToken::issue($csrfSecret);
setcookie('__csrf', $cookie, [
'expires' => time() + 86400,
'path' => '/',
@@ -93,7 +96,7 @@ if ($existingCookie === '' || $existingRawToken === '' || !$pairIsFresh) {
}
// 3. Compute the upload-endpoint user token (derived from the same secret).
$uploadsToken = hash_hmac('sha256', $secret, 'bf-uploads');
$uploadsToken = hash_hmac('sha256', $csrfSecret, 'bf-uploads');
// 4. Determine the request method, path, and content type.
$method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'));
@@ -115,7 +118,7 @@ $rawBody = (string) ($_SERVER['__raw_body'] ?? file_get_contents('php://input'))
// 6. Build a Request with the request data, the CSRF secret, and the upload token.
$server = $_SERVER;
if (!isset($server['__csrf_secret'])) {
$server['__csrf_secret'] = $secret;
$server['__csrf_secret'] = $csrfSecret;
}
$server['__uploads_token'] = $uploadsToken;
@@ -132,7 +135,7 @@ $request = new Request(
rawBody: $rawBody,
);
// 7. Configure the router with the eight routes.
// 7. Configure the router.
$uploadsRoot = __DIR__ . '/../var/uploads';
$placeholderDir = __DIR__ . '/assets/placeholders';
@@ -164,17 +167,17 @@ $getBundledOne = static function (Request $r, array $p) use ($scenariosDir): Res
return (new GetBundledScenario($scenariosDir))->handle($r, $p);
};
$getMatchView = static fn (Request $r, array $p): Response => (new GetMatchView())->handle($r, $p);
$postMatchMove = static function (Request $r, array $p) use ($secret): Response {
return (new PostMatchMove($secret))->handle($r, $p);
$postMatchMove = static function (Request $r, array $p) use ($csrfSecret): Response {
return (new PostMatchMove($csrfSecret))->handle($r, $p);
};
$postMatchAttack = static function (Request $r, array $p) use ($secret): Response {
return (new PostMatchAttack($secret))->handle($r, $p);
$postMatchAttack = static function (Request $r, array $p) use ($csrfSecret): Response {
return (new PostMatchAttack($csrfSecret))->handle($r, $p);
};
$postMatchAbility = static function (Request $r, array $p) use ($secret): Response {
return (new PostMatchAbility($secret))->handle($r, $p);
$postMatchAbility = static function (Request $r, array $p) use ($csrfSecret): Response {
return (new PostMatchAbility($csrfSecret))->handle($r, $p);
};
$postMatchEndTurn = static function (Request $r, array $p) use ($secret): Response {
return (new PostMatchEndTurn($secret))->handle($r, $p);
$postMatchEndTurn = static function (Request $r, array $p) use ($csrfSecret): Response {
return (new PostMatchEndTurn($csrfSecret))->handle($r, $p);
};
$router = new Router();
+1 -1
View File
@@ -27,4 +27,4 @@ render_layout(static function () use ($csrf, $bundled): void {
<div id="recent"><p class="bf-empty">No saved scenarios yet.</p></div>
<script type="module" src="<?= $a('/js/storage.js') ?>"></script>
<?php
}, $csrf, 'BattleForge');
}, $csrf, 'BattleForge');
@@ -60,7 +60,7 @@ final class PostMatchActionSmokeTest extends TestCase
$turnToken = $startResponse['body']['turnToken'] ?? null;
self::assertIsArray($match);
self::assertIsString($matchId);
self::assertMatchesRegularExpression('/^[a-f0-9]{16,}$/', $matchId);
self::assertMatchesRegularExpression('/^[a-f0-9]{16}$/', $matchId);
self::assertMatchesRegularExpression('/^[a-f0-9]{32}$/', $turnToken);
for ($i = 0; $i < 200; $i += 1) {
+1 -1
View File
@@ -39,7 +39,7 @@ final class PostStartMatchTest extends TestCase
self::assertSame(1, $body['match']['round'] ?? null);
self::assertCount(6, $body['match']['units'] ?? []);
self::assertIsString($body['matchId'] ?? null);
self::assertMatchesRegularExpression('/^[a-f0-9]{16,}$/', $body['matchId']);
self::assertMatchesRegularExpression('/^[a-f0-9]{16}$/', $body['matchId']);
self::assertIsString($body['turnToken'] ?? null);
self::assertMatchesRegularExpression('/^[a-f0-9]{32}$/', $body['turnToken']);
}