// SessionStore — ported from pear-git-v3 lib/auth/sessions.js.
//
// favela's sessions lived as a bare Map inside FavelaAuth: fixed expiry, no sliding renewal, no
// cleanup timer, so an abandoned session sat in memory until the process died and an ACTIVE user was
// logged out mid-use at the 30-day mark. pgv3 solved this once; this is that store, with the identity
// payload generalised ({ did, subKey } here vs a bare publicKey there).
//
// Run: node test/sessions.test.mjs
import { SessionStore, DEFAULT_TTL } from '../lib/sessions.js'
let pass = 0
const ok = (c, m) => { if (!c) { console.error('FAIL:', m); process.exit(1) } console.log(' ok -', m); pass++ }
const eq = (a, b, m) => ok(a === b, m + ` (got ${JSON.stringify(a)})`)
const ID = { did: 'did:plc:sessiontest1111111111111', subKey: 'aabb' }
// ── minting + lookup ────────────────────────────────────────────────
{
const s = new SessionStore()
const token = s.create(ID)
ok(/^[0-9a-f]{64}$/.test(token), 'mints a 32-byte hex bearer token')
const got = s.get(token)
eq(got.identity.did, ID.did, 'lookup returns the identity payload')
eq(s.get('nope'), null, 'unknown token → null')
eq(s.get(null), null, 'null token → null (no throw)')
eq(s.size, 1, 'size counts the live session')
s.close()
}
// ── two sessions never collide ──────────────────────────────────────
{
const s = new SessionStore()
ok(s.create(ID) !== s.create(ID), 'each create() mints a distinct token')
s.close()
}
// ── expiry ──────────────────────────────────────────────────────────
{
const s = new SessionStore({ ttl: 20 })
const token = s.create(ID)
await new Promise((r) => setTimeout(r, 40))
eq(s.get(token), null, 'an expired session is refused')
s.close()
}
// ── sliding renewal: the point of the port ──────────────────────────
{
const s = new SessionStore({ ttl: 60 })
const token = s.create(ID)
// Three lookups spaced past HALF the TTL each: under a FIXED expiry the session dies at 60ms
// regardless of activity — which is precisely the "logged out mid-use" behaviour being replaced.
for (let i = 0; i < 3; i++) {
await new Promise((r) => setTimeout(r, 40))
ok(!!s.get(token), `still valid after ${(i + 1) * 40}ms of CONTINUOUS use (slid forward)`)
}
await new Promise((r) => setTimeout(r, 80)) // now go idle past the TTL
eq(s.get(token), null, 'but an IDLE session still expires — sliding is not immortal')
s.close()
}
// ── revoke ──────────────────────────────────────────────────────────
{
const s = new SessionStore()
const token = s.create(ID)
eq(s.revoke(token), true, 'revoke returns true for a live session')
eq(s.get(token), null, 'revoked session is gone')
eq(s.revoke(token), false, 'revoking twice returns false')
eq(s.revoke(null), false, 'revoke(null) is false, not a throw')
s.close()
}
// ── cleanup timer reclaims abandoned sessions ───────────────────────
{
const s = new SessionStore({ ttl: 20, cleanupInterval: 10 })
s.create(ID); s.create(ID); s.create(ID)
eq(s.size, 3, 'three sessions parked')
await new Promise((r) => setTimeout(r, 80))
eq(s.size, 0, 'the sweeper reclaims them WITHOUT anyone calling get()')
s.close()
}
// ── list() is a diagnostic, and must not leak the token ─────────────
{
const s = new SessionStore()
const token = s.create(ID)
const rows = s.list()
eq(rows.length, 1, 'list reports the live session')
ok(!JSON.stringify(rows).includes(token), 'the full token is NOT in the listing — a bearer token IS the credential')
eq(rows[0].tokenPrefix, token.slice(0, 8), 'only a short prefix, enough to correlate with a log line')
eq(rows[0].did, ID.did, 'and the identity, which is not a secret')
s.close()
}
// ── close() ─────────────────────────────────────────────────────────
{
const s = new SessionStore()
const token = s.create(ID)
s.close()
eq(s.get(token), null, 'close() drops every session')
eq(s.size, 0, 'and empties the store')
}
// ── the default TTL is favela's existing 30 days, not pgv3's 24h ────
{
eq(DEFAULT_TTL, 30 * 24 * 60 * 60 * 1000, 'default TTL preserves favela\'s current session lifetime')
}
console.log(`\n${pass} assertions passed`)