feat(session): add pairing_approved table and getPairingStore()
This commit is contained in:
@@ -62,4 +62,42 @@ describe('SessionStore', () => {
|
||||
expect(sessions).toContain('session-a');
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Database from 'better-sqlite3';
|
||||
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'. */
|
||||
export function parseDuration(s: string): number | null {
|
||||
@@ -28,6 +29,13 @@ export class SessionStore {
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
this.db.close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user