fix: harden residual route and restore checks

This commit is contained in:
jb
2026-09-19 10:01:23 -04:00
parent cb86497724
commit 01b8b4642b
7 changed files with 45 additions and 11 deletions
+12 -7
View File
@@ -4,6 +4,7 @@ import { hashSecret } from '../src/lib/server/security';
import {
parseRestoreMarkerPhase,
validateRestoreMarkerTarget,
SYNTHETIC_CHARACTER_ID_MIN,
validateRestoreMarkerWrite,
} from '../src/lib/server/restore-marker';
@@ -15,9 +16,13 @@ validateRestoreMarkerWrite({
allowWrite: process.env.SMOKE_ALLOW_WRITE,
confirmation: process.env.RESTORE_MARKER_CONFIRMATION,
});
const characterId = Number(process.env.RESTORE_MARKER_CHARACTER_ID ?? '998000001');
const characterId = Number(process.env.RESTORE_MARKER_CHARACTER_ID ?? String(SYNTHETIC_CHARACTER_ID_MIN + 1));
const expiryRaw = process.env.RESTORE_MARKER_SESSION_EXPIRES_AT;
const sessionExpiresAt = expiryRaw === undefined ? Date.now() + 7 * 24 * 60 * 60_000 : Number(expiryRaw);
if (!marker || !/^[A-Za-z0-9._-]{8,100}$/.test(marker)) throw new Error('RESTORE_MARKER must be a synthetic 8–100 character identifier');
if (!Number.isSafeInteger(characterId) || characterId < 90_000_000) throw new Error('RESTORE_MARKER_CHARACTER_ID must be a synthetic safe integer');
if (!Number.isSafeInteger(characterId) || characterId < SYNTHETIC_CHARACTER_ID_MIN) throw new Error(`RESTORE_MARKER_CHARACTER_ID must be a reserved synthetic ID >= ${SYNTHETIC_CHARACTER_ID_MIN}`);
if (!Number.isSafeInteger(sessionExpiresAt) || sessionExpiresAt <= Date.now()) throw new Error('RESTORE_MARKER_SESSION_EXPIRES_AT must be a future millisecond timestamp');
if (phase !== 'write' && expiryRaw === undefined) throw new Error('RESTORE_MARKER_SESSION_EXPIRES_AT is required when verifying or invalidating a marker');
const store = new ConvexStore(target.toString(), process.env.CONVEX_BRIDGE_SECRET!);
const sessionHash = hashSecret(`restore-marker-session:${marker}`);
const ownerHash = hashSecret(`restore-marker-owner:${marker}`);
@@ -30,20 +35,20 @@ if (phase === 'write') {
ownerHash,
scopes: ['esi-assets.read_assets.v1'],
encryptedTokens: `restore-marker-placeholder:${marker}`,
tokenExpiresAt: now + 60_000,
tokenExpiresAt: sessionExpiresAt,
sessionHash,
sessionExpiresAt: now + 60 * 60_000,
sessionExpiresAt,
});
console.log(`Restore marker written: character=${characterId}, user=${user.userId}`);
console.log(`Restore marker written: character=${characterId}, user=${user.userId}, expiresAt=${sessionExpiresAt}`);
} else if (phase === 'verify') {
const session = await store.session(sessionHash);
assert(session, 'restore marker session is missing');
const characters = await store.characters(session.userId);
assert(characters.some((character) => character.id === characterId && character.name === `Restore marker ${marker}`), 'restore marker character is missing or changed');
console.log(`Restore marker verified: character=${characterId}, user=${session.userId}`);
console.log(`Restore marker verified: character=${characterId}, user=${session.userId}, expiresAt=${sessionExpiresAt}`);
} else if (phase === 'expect-invalidated') {
assert.equal(await store.session(sessionHash), null, 'restore marker session remains usable; invalidation is incomplete');
console.log(`Restore marker invalidation verified: character=${characterId}`);
console.log(`Restore marker invalidation verified: character=${characterId}, beforeExpiresAt=${sessionExpiresAt}`);
} else {
throw new Error('RESTORE_MARKER_PHASE must be write, verify, or expect-invalidated');
}