Harden WebBFF and deployment release behavior
Model fail-closed cross-replica session renewal and implement owner-fenced refresh, transient observer handling, logout fencing, and graceful shutdown. Remove chart-owned Ingress while retaining qualification-owned external routing.
This commit is contained in:
parent
ffa65018ed
commit
414b380516
1 changed files with 980 additions and 136 deletions
|
|
@ -3,6 +3,7 @@ import { randomBytes, timingSafeEqual } from 'node:crypto'
|
|||
import { createClient } from 'redis'
|
||||
|
||||
export const SESSION_COOKIE = '__Host-remora-session'
|
||||
export const SESSION_RENEWAL_PENDING = Symbol('session renewal pending')
|
||||
|
||||
export class SessionStore {
|
||||
constructor(config, client = createClient({ url: config.sessionUrl })) {
|
||||
|
|
@ -45,6 +46,56 @@ export class SessionStore {
|
|||
return replacement
|
||||
}
|
||||
|
||||
async renew(identifier, shouldRenew, renewal) {
|
||||
if (!validIdentifier(identifier)) return null
|
||||
const lock = this.lockKey(identifier)
|
||||
const owner = randomValue()
|
||||
const acquired = await this.client.eval(
|
||||
'local ttl = redis.call("pttl", KEYS[2]) if ttl <= 0 then return 0 end if redis.call("set", KEYS[1], ARGV[1], "NX", "PX", ttl) then return 1 else return 0 end',
|
||||
{
|
||||
keys: [lock, this.key(identifier)],
|
||||
arguments: [owner],
|
||||
},
|
||||
)
|
||||
if (acquired !== 1) {
|
||||
const current = await this.read(identifier)
|
||||
if (current === null || !shouldRenew(current)) return current
|
||||
return SESSION_RENEWAL_PENDING
|
||||
}
|
||||
let releaseOwnership = true
|
||||
try {
|
||||
const current = await this.read(identifier)
|
||||
if (current === null || !shouldRenew(current)) return current
|
||||
releaseOwnership = false
|
||||
const renewed = await renewal(current)
|
||||
if (renewed === null) {
|
||||
await this.destroy(identifier)
|
||||
releaseOwnership = true
|
||||
return null
|
||||
}
|
||||
const stored = await this.client.eval(
|
||||
'if redis.call("get", KEYS[1]) == ARGV[1] and redis.call("exists", KEYS[2]) == 1 then redis.call("set", KEYS[2], ARGV[2], "EX", ARGV[3]) return 1 else return 0 end',
|
||||
{
|
||||
keys: [lock, this.key(identifier)],
|
||||
arguments: [
|
||||
owner,
|
||||
JSON.stringify(renewed),
|
||||
String(this.config.sessionTtlSeconds),
|
||||
],
|
||||
},
|
||||
)
|
||||
if (stored !== 1) return null
|
||||
releaseOwnership = true
|
||||
return renewed
|
||||
} finally {
|
||||
if (releaseOwnership)
|
||||
await this.client.eval(
|
||||
'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end',
|
||||
{ keys: [lock], arguments: [owner] },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async destroy(identifier) {
|
||||
if (validIdentifier(identifier)) await this.client.del(this.key(identifier))
|
||||
}
|
||||
|
|
@ -52,6 +103,14 @@ export class SessionStore {
|
|||
key(identifier) {
|
||||
return `${this.config.sessionNamespace}${identifier}`
|
||||
}
|
||||
|
||||
lockKey(identifier) {
|
||||
return `${this.key(identifier)}:renew`
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (this.client.isOpen) await this.client.quit()
|
||||
}
|
||||
}
|
||||
|
||||
export function sessionIdentifier(request) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue