Files
feuerwerk/tools/lib/auth.mjs
T
adminandClaude Opus 5 8adef83f23 Pixel art e2e checks run through in a full CI run (#130)
CI has been red since 66d6aaf (tasks 221 and 223, two failures each).
Both failures come from the tests added with #130, not from the
editor.

- Module 04 still expected 19 asset types in the + menu. Pixel art
  and Palette make it 21.
- Module 48 died with "reading 'assets'". The password change in
  module 45 ends every other session of the e2e account, including
  the one the tools had cached, so api() got a 401. api() now drops
  the cached session and signs in once more on a 401.

Tests:
- GM_STEPS=4,45,48 (fresh seed): ALL PASSED, 102 checks.
- Counter-check: without the retry module 48 dies with exactly the
  CI error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 18:48:47 +02:00

74 lines
2.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Signing the check tools in. Since #177 every API route but `auth/*` needs a session, so every
* seed, smoke and e2e run logs in first – as `e2e`, the account that owns the four test projects.
*
* The password is never in the repo: it comes from `GAME_MAKER_E2E_PASSWORD` or from
* `data/e2e-password.txt`, which is git-ignored and written once per server (CI writes its own).
*/
import { readFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
export const E2E_USER = 'e2e';
const PASSWORD_FILE = resolve(dirname(fileURLToPath(import.meta.url)), '../../data/e2e-password.txt');
const COOKIE = 'game_maker_session';
/** One session per server for the whole process; logging in per request would be pointless load. */
const cookies = new Map();
/** The password of the `e2e` account; the step that drives the sign-in screen types it in. */
export async function e2ePassword() {
const fromEnv = process.env.GAME_MAKER_E2E_PASSWORD;
if (fromEnv) return fromEnv;
try {
return (await readFile(PASSWORD_FILE, 'utf8')).trim();
} catch {
throw new Error(
`no password for the "${E2E_USER}" account: set GAME_MAKER_E2E_PASSWORD or put it in ${PASSWORD_FILE}`,
);
}
}
/** `game_maker_session=…` for `Cookie` headers, logging in on first use. */
export async function sessionCookie(baseUrl) {
const cached = cookies.get(baseUrl);
if (cached) return cached;
const res = await fetch(`${baseUrl}api/index.php?r=auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: E2E_USER, password: await e2ePassword() }),
signal: AbortSignal.timeout(20_000),
});
if (!res.ok) {
throw new Error(`could not sign in as "${E2E_USER}" at ${baseUrl}: HTTP ${res.status}`);
}
// `getSetCookie` keeps the headers apart; a joined string cannot be split on commas, because a
// cookie's own Expires date carries one.
const set = res.headers.getSetCookie().find((c) => c.startsWith(`${COOKIE}=`));
if (!set) throw new Error(`signing in at ${baseUrl} returned no ${COOKIE} cookie`);
const cookie = set.split(';')[0];
cookies.set(baseUrl, cookie);
return cookie;
}
/**
* Drops the cached session, so the next request signs in again. Changing the password ends every
* other session of the account (#182) – the step that does it leaves the tools' one dead.
*/
export function forgetSession(baseUrl) {
cookies.delete(baseUrl);
}
/** The headers a tool's `fetch` needs on top of its own. */
export async function authHeaders(baseUrl) {
return { Cookie: await sessionCookie(baseUrl) };
}
/** The session token alone – what the browser gets handed over CDP. */
export async function sessionToken(baseUrl) {
return (await sessionCookie(baseUrl)).slice(COOKIE.length + 1);
}
export const SESSION_COOKIE = COOKIE;