fix(ios-qa): /auth/sessions no longer hands raw bearer tokens to any local process

The loopback sessions list echoed live tokens — a harvest-and-replay
primitive for anything on the machine (same class as the /health token leak
fixed in v1.63). The list now returns a device-salted 16-hex token_id plus
metadata; the salt is shared with the attempts log so identifiers correlate.
/auth/revoke keeps the list→revoke workflow alive by accepting token_id
alongside the caller's own raw token and identity. saltedHash() is exported
from audit.ts and writeAttempt now reuses it (was inlined).

Integration tests pin raw-token absence, the id shape/metadata, and the
token_id revoke round-trip (verified RED against the leaking handler).

List fix ported from time-attack/gstack (GStack 2); token_id revoke is ours.

Co-authored-by: Sina Matian <sina@time-attack.dev>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-14 12:35:47 -07:00
co-authored by Sina Matian Claude Fable 5
parent d2257abedd
commit f31aff1bc6
3 changed files with 82 additions and 7 deletions
+8 -2
View File
@@ -60,14 +60,20 @@ export async function writeAudit(row: AuditRow, path: string = defaultAuditPath(
await appendFile(path, JSON.stringify(row) + '\n', { mode: 0o600 });
}
// Non-reversible identifier for tokens/identities in logs and API responses.
// Same device salt as the attempts log, so ids correlate across both.
export async function saltedHash(raw: string): Promise<string> {
const salt = await loadDeviceSalt();
return createHash('sha256').update(salt + ':' + raw).digest('hex').slice(0, 16);
}
export async function writeAttempt(opts: {
rawIdentity: string;
endpoint: string;
reason: AttemptRow['reason'];
path?: string;
}): Promise<void> {
const salt = await loadDeviceSalt();
const hash = createHash('sha256').update(salt + ':' + opts.rawIdentity).digest('hex').slice(0, 16);
const hash = await saltedHash(opts.rawIdentity);
const row: AttemptRow = {
ts: new Date().toISOString(),
identity_canon: hash,