// In-memory session store with sliding renewal.
//
// PORTED from pear-git-v3 `lib/auth/sessions.js` (the flagship public-web p2p app). favela had the
// same need and a weaker answer: a bare `Map` inside FavelaAuth with a FIXED expiry, no renewal and
// no sweeper — so an abandoned session sat in memory until the process died, and an ACTIVE user was
// logged out the instant the fixed window elapsed.
//
// One deliberate difference from pgv3: it stores a bare `publicKey`; favela's identity is a PAIR —
// the DID (the account) and the browser sub-key delegated to it (the device) — so the payload is a
// whole identity object. Both are needed downstream: the DID for authorisation, the sub-key for the
// revocation check.
//
// NOT persisted across restarts, and that is correct rather than lazy: the browser keeps its sub-key
// AND its delegation in localStorage (`/api/auth/pair` returns the delegation for exactly this
// reason), so re-authentication after a restart is silent. Persisting bearer tokens would add a
// durable secret store to a process whose whole design point is that it holds no keys.
import crypto from 'hypercore-crypto'
import b4a from 'b4a'
// 30 days — favela's existing SESSION_TTL_MS. pgv3 uses 24h; keeping favela's value means this port
// changes session LIFETIME for nobody, only how it is renewed and reclaimed.
export const DEFAULT_TTL = 30 * 24 * 60 * 60 * 1000
export const DEFAULT_CLEANUP = 60 * 1000 // 1 min
export class SessionStore {
constructor (opts = {}) {
this._ttl = opts.ttl || DEFAULT_TTL
this._sessions = new Map() // token → { identity, expiresAt }
this._cleanupInterval = setInterval(() => this._cleanup(), opts.cleanupInterval || DEFAULT_CLEANUP)
// Unref'd: a session sweeper must never be the reason a process stays alive.
if (this._cleanupInterval.unref) this._cleanupInterval.unref()
}
/**
* Mint a fresh session. Returns the bearer token (64-char hex).
* @param {object} identity - e.g. { did, subKey }
*/
create (identity) {
const token = b4a.toString(crypto.randomBytes(32), 'hex')
this._sessions.set(token, { identity, expiresAt: Date.now() + this._ttl })
return token
}
/**
* Look up a session. Slides the expiry forward on every hit, so an active user stays signed in and
* only genuine IDLE time expires them.
* @returns {{ identity: object, expiresAt: number } | null}
*/
get (token) {
if (!token) return null
const entry = this._sessions.get(token)
if (!entry) return null
if (entry.expiresAt < Date.now()) {
this._sessions.delete(token)
return null
}
entry.expiresAt = Date.now() + this._ttl
return entry
}
/** Revoke a session. True if it existed. */
revoke (token) {
if (!token) return false
return this._sessions.delete(token)
}
/** Number of sessions held (including any not yet swept). */
get size () {
return this._sessions.size
}
/**
* Diagnostics only. The token IS the credential, so the listing carries a short PREFIX — enough to
* correlate with a log line, useless to replay — never the token itself.
*/
list () {
const now = Date.now()
const out = []
for (const [token, entry] of this._sessions) {
if (entry.expiresAt < now) continue
out.push({
tokenPrefix: token.slice(0, 8),
did: entry.identity && entry.identity.did ? entry.identity.did : null,
expiresAt: entry.expiresAt
})
}
return out
}
_cleanup () {
const now = Date.now()
for (const [token, entry] of this._sessions) {
if (entry.expiresAt < now) this._sessions.delete(token)
}
}
/** Stop the sweeper and drop every session. */
close () {
if (this._cleanupInterval) {
clearInterval(this._cleanupInterval)
this._cleanupInterval = null
}
this._sessions.clear()
}
}
export default SessionStore