@bobfrankston/mailx-store 0.1.11 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/db.d.ts +17 -2
  2. package/db.js +73 -9
  3. package/package.json +1 -1
package/db.d.ts CHANGED
@@ -148,6 +148,20 @@ export declare class MailxDB {
148
148
  updateLastSync(accountId: string, timestamp: number): void;
149
149
  upsertFolder(accountId: string, folderPath: string, name: string, specialUse: string, delimiter: string): number;
150
150
  getFolders(accountId: string): Folder[];
151
+ /** Append a row to the audit_log table. Every destructive operation on
152
+ * the messages table writes here so "where did the rows go?" has an
153
+ * authoritative answer. Cheap insert, indexed by ts and (account, folder). */
154
+ audit(entry: {
155
+ kind: "delete-msg" | "delete-bulk" | "migration" | "manual" | string;
156
+ accountId?: string;
157
+ folderId?: number;
158
+ uid?: number;
159
+ count?: number;
160
+ messageId?: string;
161
+ subject?: string;
162
+ reason?: string;
163
+ source?: string;
164
+ }): void;
151
165
  deleteFolder(folderId: number): void;
152
166
  markFolderRead(folderId: number): void;
153
167
  deleteAllMessages(accountId: string, folderId: number): void;
@@ -204,8 +218,9 @@ export declare class MailxDB {
204
218
  getMessageCount(accountId: string, folderId: number): number;
205
219
  /** Get all UIDs for a folder */
206
220
  getUidsForFolder(accountId: string, folderId: number): number[];
207
- /** Delete a message by account + UID */
208
- deleteMessage(accountId: string, uid: number): void;
221
+ /** Delete a message by account + UID. Reason is propagated into the
222
+ * audit_log so the DB carries an authoritative trail of every removal. */
223
+ deleteMessage(accountId: string, uid: number, reason?: string, source?: string): void;
209
224
  /** Recalculate folder total/unread counts from actual messages */
210
225
  recalcFolderCounts(folderId: number): void;
211
226
  /** Bulk insert within a transaction for sync performance */
package/db.js CHANGED
@@ -285,6 +285,35 @@ const SCHEMA = `
285
285
  updated_at INTEGER NOT NULL,
286
286
  PRIMARY KEY(scope, key)
287
287
  );
288
+
289
+ -- Audit trail of every destructive DB operation. Writes ONLY — the row
290
+ -- inserted here records the deletion the caller is about to (or just
291
+ -- did) commit on the messages table. Lets the user retroactively answer
292
+ -- "where did my Sent rows go?" — every deletion has a row with
293
+ -- kind/reason/folder/uid + caller stack hint. Trivial cost (rows are
294
+ -- tiny), no retention policy yet — kept indefinitely.
295
+ --
296
+ -- kind values:
297
+ -- delete-msg single message removed (sync_actions drain, body
298
+ -- fetch reported isNotFound, reconcile)
299
+ -- delete-bulk bulk delete (reconcile pruning, emptyFolder, etc.)
300
+ -- migration schema migrations that touch messages
301
+ -- manual direct user "rebuild" / "wipe folder"
302
+ CREATE TABLE IF NOT EXISTS audit_log (
303
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
304
+ ts INTEGER NOT NULL,
305
+ kind TEXT NOT NULL,
306
+ account_id TEXT,
307
+ folder_id INTEGER,
308
+ uid INTEGER,
309
+ count INTEGER,
310
+ message_id TEXT,
311
+ subject TEXT,
312
+ reason TEXT,
313
+ source TEXT
314
+ );
315
+ CREATE INDEX IF NOT EXISTS idx_audit_log_ts ON audit_log(ts);
316
+ CREATE INDEX IF NOT EXISTS idx_audit_log_folder ON audit_log(account_id, folder_id);
288
317
  `;
289
318
  export class MailxDB {
290
319
  db;
@@ -344,8 +373,10 @@ export class MailxDB {
344
373
  const purgedFlag = this.getKv("schema", "drop_synthetic_uids_v1");
345
374
  if (!purgedFlag) {
346
375
  const r = this.db.prepare("DELETE FROM messages WHERE uid < 0").run();
347
- if (r.changes) {
348
- console.log(` [migration] dropped ${r.changes} synthetic-UID rows from messages`);
376
+ const count = r.changes || 0;
377
+ if (count > 0) {
378
+ console.log(` [migration] dropped ${count} synthetic-UID rows from messages`);
379
+ this.audit({ kind: "migration", count, reason: "drop_synthetic_uids_v1 — purge negative-UID optimistic-Sent rows", source: "MailxDB constructor" });
349
380
  }
350
381
  this.setKv("schema", "drop_synthetic_uids_v1", "1");
351
382
  }
@@ -930,8 +961,24 @@ export class MailxDB {
930
961
  }
931
962
  return folders;
932
963
  }
964
+ /** Append a row to the audit_log table. Every destructive operation on
965
+ * the messages table writes here so "where did the rows go?" has an
966
+ * authoritative answer. Cheap insert, indexed by ts and (account, folder). */
967
+ audit(entry) {
968
+ try {
969
+ this.db.prepare(`INSERT INTO audit_log (ts, kind, account_id, folder_id, uid, count, message_id, subject, reason, source)
970
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(Date.now(), entry.kind, entry.accountId ?? null, entry.folderId ?? null, entry.uid ?? null, entry.count ?? null, entry.messageId ?? null, entry.subject ?? null, entry.reason ?? null, entry.source ?? null);
971
+ }
972
+ catch (e) {
973
+ // Audit must never break the operation it's auditing.
974
+ console.error(` [audit] failed: ${e?.message || e}`);
975
+ }
976
+ }
933
977
  deleteFolder(folderId) {
934
- this.db.prepare("DELETE FROM messages WHERE folder_id = ?").run(folderId);
978
+ const r = this.db.prepare("DELETE FROM messages WHERE folder_id = ?").run(folderId);
979
+ const count = r.changes || 0;
980
+ if (count > 0)
981
+ this.audit({ kind: "delete-bulk", folderId, count, reason: "folder deleted", source: "deleteFolder" });
935
982
  this.db.prepare("DELETE FROM folders WHERE id = ?").run(folderId);
936
983
  }
937
984
  markFolderRead(folderId) {
@@ -939,7 +986,10 @@ export class MailxDB {
939
986
  this.recalcFolderCounts(folderId);
940
987
  }
941
988
  deleteAllMessages(accountId, folderId) {
942
- this.db.prepare("DELETE FROM messages WHERE account_id = ? AND folder_id = ?").run(accountId, folderId);
989
+ const r = this.db.prepare("DELETE FROM messages WHERE account_id = ? AND folder_id = ?").run(accountId, folderId);
990
+ const count = r.changes || 0;
991
+ if (count > 0)
992
+ this.audit({ kind: "delete-bulk", accountId, folderId, count, reason: "deleteAllMessages (emptyFolder)", source: "deleteAllMessages" });
943
993
  this.recalcFolderCounts(folderId);
944
994
  }
945
995
  updateFolderCounts(folderId, total, unread) {
@@ -1231,11 +1281,25 @@ export class MailxDB {
1231
1281
  const rows = this.db.prepare("SELECT uid FROM messages WHERE account_id = ? AND folder_id = ?").all(accountId, folderId);
1232
1282
  return rows.map(r => r.uid);
1233
1283
  }
1234
- /** Delete a message by account + UID */
1235
- deleteMessage(accountId, uid) {
1236
- // Get folderId before deleting so we can update counts
1237
- const msg = this.db.prepare("SELECT folder_id FROM messages WHERE account_id = ? AND uid = ?").get(accountId, uid);
1238
- this.db.prepare("DELETE FROM messages WHERE account_id = ? AND uid = ?").run(accountId, uid);
1284
+ /** Delete a message by account + UID. Reason is propagated into the
1285
+ * audit_log so the DB carries an authoritative trail of every removal. */
1286
+ deleteMessage(accountId, uid, reason, source) {
1287
+ // Get folderId + message_id + subject before deleting so the audit
1288
+ // row carries enough context to identify what was removed.
1289
+ const msg = this.db.prepare("SELECT folder_id, message_id, subject FROM messages WHERE account_id = ? AND uid = ?").get(accountId, uid);
1290
+ const r = this.db.prepare("DELETE FROM messages WHERE account_id = ? AND uid = ?").run(accountId, uid);
1291
+ if (r.changes && msg) {
1292
+ this.audit({
1293
+ kind: "delete-msg",
1294
+ accountId,
1295
+ folderId: msg.folder_id,
1296
+ uid,
1297
+ messageId: msg.message_id || undefined,
1298
+ subject: msg.subject || undefined,
1299
+ reason: reason || "deleteMessage (no reason)",
1300
+ source: source || "db.deleteMessage",
1301
+ });
1302
+ }
1239
1303
  // Refresh folder counts
1240
1304
  if (msg)
1241
1305
  this.recalcFolderCounts(msg.folder_id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-store",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",