65 lines
2.2 KiB
TypeScript
65 lines
2.2 KiB
TypeScript
import { raisePrincipalExpired } from "../error-codes/error-codes.js";
|
|
|
|
/** Clock abstraction used by the principal-expiry watcher. */
|
|
export interface PrincipalExpiryClock {
|
|
now(): number;
|
|
}
|
|
|
|
/** Listener signature invoked when the principal's token expires. */
|
|
export type PrincipalExpiryListener = (event: {
|
|
readonly connectionId: string;
|
|
readonly principalId: string;
|
|
}) => void;
|
|
|
|
/** Options for {@link PrincipalExpiryWatcher}. */
|
|
export interface PrincipalExpiryWatcherOptions {
|
|
readonly connectionId: string;
|
|
readonly principalId: string;
|
|
/**
|
|
* Wall-clock expiry timestamp (ms since epoch). The handshake
|
|
* pipeline reads this from the resolved token; once `now()` reaches
|
|
* this value the watcher fires.
|
|
*/
|
|
readonly expiresAt: number;
|
|
readonly clock: PrincipalExpiryClock;
|
|
readonly onExpired: PrincipalExpiryListener;
|
|
}
|
|
|
|
/**
|
|
* Tracks the frozen principal's underlying token expiry. The engine
|
|
* polls `check()` from a timer (production) or from a test loop driven
|
|
* by `FakeClock`. Once the expiry passes, the watcher fires the
|
|
* `onExpired` listener exactly once and throws
|
|
* `WS_BUN.PRINCIPAL_EXPIRED` so the engine can close the connection.
|
|
*
|
|
* The principal itself is NOT mutated by this watcher — the contract
|
|
* requires it to stay frozen for the connection's lifetime. The watcher
|
|
* is the engine's signal that it is time to drop the connection.
|
|
*/
|
|
export class PrincipalExpiryWatcher {
|
|
private fired = false;
|
|
|
|
public constructor(
|
|
private readonly options: PrincipalExpiryWatcherOptions
|
|
) {}
|
|
|
|
/**
|
|
* Run one expiry check. If `now() >= expiresAt` and the watcher has
|
|
* not yet fired, invoke `onExpired` and throw `WS_BUN.PRINCIPAL_EXPIRED`.
|
|
*/
|
|
public check(): void {
|
|
if (this.fired) return;
|
|
if (this.options.clock.now() < this.options.expiresAt) return;
|
|
this.fired = true;
|
|
this.options.onExpired({
|
|
connectionId: this.options.connectionId,
|
|
principalId: this.options.principalId,
|
|
});
|
|
raisePrincipalExpired(this.options.connectionId);
|
|
}
|
|
|
|
public get hasFired(): boolean {
|
|
return this.fired;
|
|
}
|
|
}
|