Repository navigation
Expand file tree
/
Copy pathsession-metadata.ts
More file actions
85 lines (74 loc) · 2.46 KB
/
Copy pathsession-metadata.ts
File metadata and controls
85 lines (74 loc) · 2.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/**
* Session metadata store — UA + IP + timestamps per refresh-token jti.
*
* Stored as a single JSON blob in the private bucket so it survives restarts
* but is never in the public commit log (no PII in git per
* specs/behaviors/storage.md#pii-aware-redaction).
*
* In-memory map keyed by refreshJti; flushed to private store on each mutation.
*/
import type { PrivateStore } from '../store/private/index.js';
export interface SessionMeta {
readonly refreshJti: string;
readonly personId: string;
readonly userAgent: string;
readonly ipAddress: string;
readonly issuedAt: string;
readonly expiresAt: string;
}
const STORAGE_KEY = 'session-metadata.json';
export class SessionMetadataStore {
readonly #map = new Map<string, SessionMeta>();
async load(privateStore: PrivateStore): Promise<void> {
const raw = await privateStore.readBlob(STORAGE_KEY);
if (!raw) return;
try {
const parsed = JSON.parse(raw) as Record<string, SessionMeta>;
for (const [jti, meta] of Object.entries(parsed)) {
this.#map.set(jti, meta);
}
} catch {
// Corrupt metadata is non-fatal — start fresh
}
}
private async flush(privateStore: PrivateStore): Promise<void> {
const obj: Record<string, SessionMeta> = {};
for (const [jti, meta] of this.#map) {
obj[jti] = meta;
}
await privateStore.writeBlob(STORAGE_KEY, JSON.stringify(obj));
}
async add(meta: SessionMeta, privateStore: PrivateStore): Promise<void> {
this.#map.set(meta.refreshJti, meta);
await this.flush(privateStore);
}
async remove(refreshJti: string, privateStore: PrivateStore): Promise<void> {
this.#map.delete(refreshJti);
await this.flush(privateStore);
}
/** Forget every session of a purged person. */
async removeAllFor(personId: string, privateStore: PrivateStore): Promise<number> {
let removed = 0;
for (const [jti, meta] of this.#map) {
if (meta.personId === personId) {
this.#map.delete(jti);
removed++;
}
}
if (removed > 0) await this.flush(privateStore);
return removed;
}
getAll(personId: string): SessionMeta[] {
const result: SessionMeta[] = [];
for (const meta of this.#map.values()) {
if (meta.personId === personId) result.push(meta);
}
return result;
}
get(refreshJti: string): SessionMeta | null {
return this.#map.get(refreshJti) ?? null;
}
has(refreshJti: string): boolean {
return this.#map.has(refreshJti);
}
}