feat(session): add pairing_approved table and getPairingStore()

This commit is contained in:
William Valentin
2026-02-09 21:46:51 -08:00
parent 1e1a68924e
commit ecd3aca7c1
2 changed files with 73 additions and 0 deletions
+38
View File
@@ -62,4 +62,42 @@ describe('SessionStore', () => {
expect(sessions).toContain('session-a'); expect(sessions).toContain('session-a');
expect(sessions).toContain('session-b'); expect(sessions).toContain('session-b');
}); });
describe('pairing persistence', () => {
it('getPairingStore returns a PairingStore', () => {
const pairingStore = store.getPairingStore();
expect(pairingStore).toBeDefined();
expect(pairingStore.loadApproved).toBeInstanceOf(Function);
expect(pairingStore.saveApproved).toBeInstanceOf(Function);
expect(pairingStore.removeApproved).toBeInstanceOf(Function);
});
it('saveApproved and loadApproved round-trip', () => {
const ps = store.getPairingStore();
ps.saveApproved({ channel: 'telegram', senderId: '123', approvedAt: 1000, codeUsed: 'AABB11' });
ps.saveApproved({ channel: 'discord', senderId: '456', approvedAt: 2000, codeUsed: 'CC33DD' });
const loaded = ps.loadApproved();
expect(loaded).toHaveLength(2);
expect(loaded).toEqual(expect.arrayContaining([
expect.objectContaining({ channel: 'telegram', senderId: '123', codeUsed: 'AABB11' }),
expect.objectContaining({ channel: 'discord', senderId: '456', codeUsed: 'CC33DD' }),
]));
});
it('removeApproved deletes a sender', () => {
const ps = store.getPairingStore();
ps.saveApproved({ channel: 'telegram', senderId: '123', approvedAt: 1000, codeUsed: 'AABB11' });
ps.removeApproved('telegram', '123');
expect(ps.loadApproved()).toHaveLength(0);
});
it('saveApproved upserts on duplicate channel+senderId', () => {
const ps = store.getPairingStore();
ps.saveApproved({ channel: 'telegram', senderId: '123', approvedAt: 1000, codeUsed: 'FIRST1' });
ps.saveApproved({ channel: 'telegram', senderId: '123', approvedAt: 2000, codeUsed: 'SECOND' });
const loaded = ps.loadApproved();
expect(loaded).toHaveLength(1);
expect(loaded[0].codeUsed).toBe('SECOND');
});
});
}); });
+35
View File
@@ -1,5 +1,6 @@
import Database from 'better-sqlite3'; import Database from 'better-sqlite3';
import type { Message } from '../models/types.js'; import type { Message } from '../models/types.js';
import type { PairingStore, ApprovedSender } from '../channels/pairing.js';
/** Parse a duration string like '30d', '7d', '12h' to milliseconds. Returns null if invalid or '0'. */ /** Parse a duration string like '30d', '7d', '12h' to milliseconds. Returns null if invalid or '0'. */
export function parseDuration(s: string): number | null { export function parseDuration(s: string): number | null {
@@ -28,6 +29,13 @@ export class SessionStore {
created_at INTEGER NOT NULL DEFAULT (unixepoch()) created_at INTEGER NOT NULL DEFAULT (unixepoch())
); );
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id); CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
CREATE TABLE IF NOT EXISTS pairing_approved (
channel TEXT NOT NULL,
sender_id TEXT NOT NULL,
approved_at INTEGER NOT NULL,
code_used TEXT NOT NULL,
PRIMARY KEY (channel, sender_id)
);
`); `);
} }
@@ -101,6 +109,33 @@ export class SessionStore {
return stale.map(r => r.session_id); return stale.map(r => r.session_id);
} }
getPairingStore(): PairingStore {
return {
loadApproved: (): ApprovedSender[] => {
const rows = this.db.prepare(
'SELECT channel, sender_id, approved_at, code_used FROM pairing_approved'
).all() as Array<{ channel: string; sender_id: string; approved_at: number; code_used: string }>;
return rows.map(r => ({
channel: r.channel,
senderId: r.sender_id,
approvedAt: r.approved_at,
codeUsed: r.code_used,
}));
},
saveApproved: (sender: ApprovedSender): void => {
this.db.prepare(`
INSERT OR REPLACE INTO pairing_approved (channel, sender_id, approved_at, code_used)
VALUES (?, ?, ?, ?)
`).run(sender.channel, sender.senderId, sender.approvedAt, sender.codeUsed);
},
removeApproved: (channel: string, senderId: string): void => {
this.db.prepare(
'DELETE FROM pairing_approved WHERE channel = ? AND sender_id = ?'
).run(channel, senderId);
},
};
}
close(): void { close(): void {
this.db.close(); this.db.close();
} }