/**
* In-memory session store with sliding renewal.
*
* Sessions are bearer tokens (random 32-byte hex). Each session is mapped
* to a publicKey and an expiresAt. Lookup extends expiresAt — sliding
* window renewal — so active users stay logged in indefinitely.
*
* Sessions are NOT persisted across daemon restarts. The user re-authenticates
* after a restart, which is fine because the browser still has its keypair
* in localStorage and the auth flow is silent.
*
* This is intentionally simpler than v1's Hyperbee-persisted token registry
* (packages/hyper-identity/lib/auth/identity-registry.js, ~800 LOC). v3 only
* needs in-memory bearer tokens — re-auth on restart is cheap because the
* browser keypair survives in localStorage.
*/
import crypto from 'hypercore-crypto'
import b4a from 'b4a'
const DEFAULT_TTL = 24 * 60 * 60 * 1000 // 24h
const DEFAULT_CLEANUP = 60 * 1000 // 1 min
export class SessionStore {
constructor (opts = {}) {
this._ttl = opts.ttl || DEFAULT_TTL
this._sessions = new Map() // token → { publicKey, expiresAt }
this._cleanupInterval = setInterval(() => this._cleanup(), opts.cleanupInterval || DEFAULT_CLEANUP)
if (this._cleanupInterval.unref) this._cleanupInterval.unref()
}
/**
* Mint a fresh session for a public key. Returns the bearer token.
* @param {string} publicKey - hex string
* @returns {string} 64-char hex token
*/
create (publicKey) {
const token = b4a.toString(crypto.randomBytes(32), 'hex')
this._sessions.set(token, {
publicKey,
expiresAt: Date.now() + this._ttl
})
return token
}
/**
* Look up a session by token. Slides the expiry forward on every hit.
* @param {string} token
* @returns {{ publicKey: string, 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
}
// Sliding renewal — extend on each access
entry.expiresAt = Date.now() + this._ttl
return entry
}
/**
* Revoke a session. Returns true if it existed.
*/
revoke (token) {
if (!token) return false
return this._sessions.delete(token)
}
/**
* Number of active (unexpired) sessions.
*/
get size () {
return this._sessions.size
}
/**
* List active sessions (for diagnostics — never expose secrets).
*/
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),
publicKey: entry.publicKey,
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 cleanup timer and drop all sessions. */
close () {
if (this._cleanupInterval) {
clearInterval(this._cleanupInterval)
this._cleanupInterval = null
}
this._sessions.clear()
}
}