mbkauthe 5.1.1 → 5.2.1

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.
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Best-effort translator for the small, consistent subset of Postgres SQL
3
+ * used throughout mbkauthe's raw queries (see AuthRepository.js). This is
4
+ * NOT a general-purpose SQL translator — it only handles the specific
5
+ * constructs this codebase actually emits:
6
+ *
7
+ * - `$1`, `$2`, ... positional params -> `?`
8
+ * - `col = ANY($1)` array matching -> `col IN (?, ?, ...)`
9
+ * - `NOW()` -> `CURRENT_TIMESTAMP`
10
+ * - bare `TRUE` / `FALSE` literals -> `1` / `0`
11
+ * - `::type` casts -> stripped
12
+ *
13
+ * Queries with more exotic Postgres-only syntax (INTERVAL arithmetic,
14
+ * pg_advisory_xact_lock, LOCK TABLE, etc.) are handled by dialect-specific
15
+ * branches in the repositories/pool instead of trying to regex them here.
16
+ */
17
+ export function translatePgToSqlite(text, values = []) {
18
+ let sql = String(text ?? "")
19
+ .replace(/::\w+(\[\])?/g, "")
20
+ .replace(/\bNOW\(\)/gi, "CURRENT_TIMESTAMP")
21
+ .replace(/\bTRUE\b/g, "1")
22
+ .replace(/\bFALSE\b/g, "0");
23
+
24
+ // Queries already written in native SQLite style with `?` placeholders
25
+ // (e.g. SqliteSessionStore) have no $N tokens to rewrite — pass their
26
+ // values through untouched instead of rebuilding an empty array.
27
+ if (!/\$\d/.test(sql)) {
28
+ return { text: sql, values };
29
+ }
30
+
31
+ const tokenRegex = /(=\s*ANY\(\$(\d+)\))|(\$(\d+))/g;
32
+ let out = "";
33
+ let lastIndex = 0;
34
+ let match;
35
+ const newValues = [];
36
+
37
+ while ((match = tokenRegex.exec(sql)) !== null) {
38
+ out += sql.slice(lastIndex, match.index);
39
+
40
+ if (match[1]) {
41
+ // `col = ANY($N)` -> `col IN (?, ?, ...)`
42
+ const paramIndex = Number(match[2]) - 1;
43
+ const rawValue = values[paramIndex];
44
+ const arr = Array.isArray(rawValue) ? rawValue : [rawValue];
45
+ if (arr.length === 0) {
46
+ out += "IN (NULL)"; // never matches, mirrors ANY('{}') semantics
47
+ } else {
48
+ out += `IN (${arr.map(() => "?").join(", ")})`;
49
+ newValues.push(...arr);
50
+ }
51
+ } else if (match[3]) {
52
+ const paramIndex = Number(match[4]) - 1;
53
+ out += "?";
54
+ newValues.push(values[paramIndex]);
55
+ }
56
+
57
+ lastIndex = tokenRegex.lastIndex;
58
+ }
59
+
60
+ out += sql.slice(lastIndex);
61
+ return { text: out, values: newValues };
62
+ }
@@ -0,0 +1,210 @@
1
+ import Database from "better-sqlite3";
2
+ import { translatePgToSqlite } from "./sqlSqliteTranslate.js";
3
+
4
+ /**
5
+ * FIFO promise mutex. All statements run on ONE shared better-sqlite3
6
+ * handle, and BEGIN/COMMIT are connection state — so a transaction must
7
+ * hold exclusive access for its whole (async) lifetime, or queries from
8
+ * other requests interleave between its awaits and silently join it
9
+ * (or a second BEGIN throws "cannot start a transaction within a
10
+ * transaction"). Individual statements are synchronous, so waiters queue
11
+ * for microseconds, not I/O time.
12
+ */
13
+ class Mutex {
14
+ constructor() {
15
+ this._tail = Promise.resolve();
16
+ }
17
+
18
+ /** Resolves to a release function once the lock is held. */
19
+ acquire() {
20
+ const prev = this._tail;
21
+ let release;
22
+ this._tail = new Promise((resolve) => {
23
+ let released = false;
24
+ release = () => {
25
+ if (released) return;
26
+ released = true;
27
+ resolve();
28
+ };
29
+ });
30
+ return prev.then(() => release);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Exclusive handle returned by SqlitePool.connect(). Holds the pool mutex
36
+ * from connect() until release(), so a BEGIN..COMMIT sequence issued on it
37
+ * cannot interleave with queries from other requests. release() is
38
+ * idempotent. Queries on the client itself do NOT re-acquire the mutex —
39
+ * the client is the holder. Do not call pool.query() from code running
40
+ * inside a transaction (use the client / txRepo): it would wait for the
41
+ * lock the transaction is holding and deadlock.
42
+ */
43
+ class SqliteClient {
44
+ constructor(db, releaseLock) {
45
+ this.db = db;
46
+ this._releaseLock = releaseLock;
47
+ }
48
+
49
+ async query(queryOrText, maybeValues) {
50
+ const { text, values, name } = normalizeQueryArgs(queryOrText, maybeValues);
51
+ return runQuery(this.db, text, values, name);
52
+ }
53
+
54
+ release() {
55
+ this._releaseLock();
56
+ }
57
+ }
58
+
59
+ function normalizeQueryArgs(queryOrText, maybeValues) {
60
+ if (typeof queryOrText === "string") {
61
+ return { text: queryOrText, values: maybeValues || [], name: undefined };
62
+ }
63
+ return {
64
+ text: queryOrText?.text ?? "",
65
+ values: queryOrText?.values ?? [],
66
+ name: queryOrText?.name
67
+ };
68
+ }
69
+
70
+ /**
71
+ * better-sqlite3 only binds numbers, strings, bigints, buffers, and null.
72
+ * Coerce the extra types the pg driver would serialize for us: Date ->
73
+ * SQLite's CURRENT_TIMESTAMP text format ('YYYY-MM-DD HH:MM:SS', UTC) so
74
+ * comparisons against CURRENT_TIMESTAMP stay correct, boolean -> 1/0,
75
+ * undefined -> null.
76
+ */
77
+ function coerceBindValue(value) {
78
+ if (value === undefined) return null;
79
+ if (typeof value === "boolean") return value ? 1 : 0;
80
+ if (value instanceof Date) {
81
+ return value.toISOString().slice(0, 19).replace("T", " ");
82
+ }
83
+ // pg serializes objects/arrays bound to json/jsonb params; mirror that.
84
+ if (typeof value === "object" && value !== null && !Buffer.isBuffer(value)) {
85
+ return JSON.stringify(value);
86
+ }
87
+ return value;
88
+ }
89
+
90
+ /**
91
+ * Read-side row normalization so results match what the pg driver returns.
92
+ *
93
+ * Postgres jsonb columns arrive as parsed objects/arrays and timestamp
94
+ * columns as Date objects; better-sqlite3 returns raw TEXT for both. All
95
+ * consumers (login app-authorization checks, API-token permissions, expiry
96
+ * comparisons) are written against the pg shapes, so convert by column name
97
+ * — the schema is fixed and known. Timestamp text is UTC (it comes from
98
+ * CURRENT_TIMESTAMP or coerceBindValue), but `new Date('YYYY-MM-DD
99
+ * HH:MM:SS')` would parse it as LOCAL time, skewing every expiry check by
100
+ * the machine's UTC offset — hence the explicit 'Z'.
101
+ *
102
+ * "sess" and "expire" (express-session store) are deliberately NOT listed:
103
+ * SqliteSessionStore owns those columns and does its own JSON/UTC handling.
104
+ */
105
+ const JSON_COLUMNS = new Set([
106
+ "AllowedApps",
107
+ "user_allowed_apps", // alias of Users.AllowedApps in getApiTokenByHash
108
+ "Permissions",
109
+ "Positions",
110
+ "SocialAccounts",
111
+ "meta"
112
+ ]);
113
+
114
+ const TIMESTAMP_COLUMNS = new Set([
115
+ "expires_at",
116
+ "created_at",
117
+ "updated_at",
118
+ "last_login",
119
+ "last_activity",
120
+ "connect_expire", // alias of session.expire in getSessionValidity
121
+ "CreatedAt",
122
+ "ExpiresAt",
123
+ "LastUsed"
124
+ ]);
125
+
126
+ const SQLITE_TIMESTAMP_RE = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2}(?:\.\d+)?)(Z)?$/;
127
+
128
+ function normalizeRow(row) {
129
+ for (const key of Object.keys(row)) {
130
+ const value = row[key];
131
+ if (typeof value !== "string") continue;
132
+
133
+ if (JSON_COLUMNS.has(key)) {
134
+ try {
135
+ row[key] = JSON.parse(value);
136
+ } catch {
137
+ // Not valid JSON — leave the raw string rather than lose data.
138
+ }
139
+ } else if (TIMESTAMP_COLUMNS.has(key)) {
140
+ const m = SQLITE_TIMESTAMP_RE.exec(value);
141
+ if (m) {
142
+ row[key] = new Date(`${m[1]}T${m[2]}Z`);
143
+ }
144
+ }
145
+ }
146
+ return row;
147
+ }
148
+
149
+ function runQuery(db, rawText, rawValues) {
150
+ const trimmed = rawText.trim();
151
+ const upper = trimmed.toUpperCase();
152
+
153
+ // Transaction control statements pass straight through.
154
+ if (upper === "BEGIN" || upper === "COMMIT" || upper === "ROLLBACK") {
155
+ db.exec(upper);
156
+ return { rows: [], rowCount: 0, command: upper };
157
+ }
158
+
159
+ const { text, values } = translatePgToSqlite(trimmed, rawValues);
160
+ const stmt = db.prepare(text);
161
+ const bindValues = values.map(coerceBindValue);
162
+
163
+ if (stmt.reader) {
164
+ const rows = stmt.all(...bindValues).map(normalizeRow);
165
+ return { rows, rowCount: rows.length, command: "SELECT" };
166
+ }
167
+
168
+ const info = stmt.run(...bindValues);
169
+ return {
170
+ rows: [],
171
+ rowCount: info.changes,
172
+ command: "EXECUTE",
173
+ lastInsertRowid: info.lastInsertRowid
174
+ };
175
+ }
176
+
177
+ export class SqlitePool {
178
+ constructor(filePath) {
179
+ this.db = new Database(filePath);
180
+ this.db.pragma("journal_mode = WAL");
181
+ this.db.pragma("foreign_keys = ON");
182
+ this.mutex = new Mutex();
183
+ }
184
+
185
+ async query(queryOrText, maybeValues) {
186
+ const { text, values } = normalizeQueryArgs(queryOrText, maybeValues);
187
+ // Take the mutex per statement so a plain query can never land inside
188
+ // another request's open transaction.
189
+ const release = await this.mutex.acquire();
190
+ try {
191
+ return runQuery(this.db, text, values);
192
+ } finally {
193
+ release();
194
+ }
195
+ }
196
+
197
+ async connect() {
198
+ const release = await this.mutex.acquire();
199
+ return new SqliteClient(this.db, release);
200
+ }
201
+
202
+ /** Run a raw multi-statement SQL script (schema files, migrations). */
203
+ execScript(sql) {
204
+ this.db.exec(sql);
205
+ }
206
+
207
+ async end() {
208
+ this.db.close();
209
+ }
210
+ }
@@ -1,4 +1,4 @@
1
- import { dblogin } from "#pool.js";
1
+ import { dblogin, dialect } from "#pool.js";
2
2
  import { mbkautheVar } from "#config.js";
3
3
  import { renderError } from "#response.js";
4
4
  import { clearSessionCookies, cachedCookieOptions, encryptSessionId } from "#cookies.js";
@@ -16,7 +16,7 @@ const MAX_API_TOKEN_LENGTH = 4096;
16
16
  const API_TOKEN_LAST_USED_INTERVAL_MS = 15 * 60 * 1000;
17
17
  const API_TOKEN_SESSION_RESTORE = Symbol('mbkauthe.apiTokenSessionRestore');
18
18
  const apiTokenLastUsedCache = new Map();
19
- const authRepo = new AuthRepository({ db: dblogin });
19
+ const authRepo = new AuthRepository({ db: dblogin, dialect });
20
20
  const logAuth = createLogger("auth");
21
21
 
22
22
  function pruneApiTokenLastUsedCache(now) {
@@ -1,15 +1,21 @@
1
1
  import session from "express-session";
2
2
  import pgSession from "connect-pg-simple";
3
3
  const PgSession = pgSession(session);
4
- import { dblogin, runWithRequestContext } from "#pool.js";
4
+ import { dblogin, dialect, dbType, runWithRequestContext } from "#pool.js";
5
5
  import { mbkautheVar } from "#config.js";
6
6
  import { cachedCookieOptions, decryptSessionId, encryptSessionId, cachedClearCookieOptions, getCookieDomain, getCookieSecure, isAllowedOriginHostname } from "#cookies.js";
7
7
  import { AuthRepository } from "../db/AuthRepository.js";
8
8
  import { isUserAuthorizedForApp } from "../utils/appAccess.js";
9
+ import { SqliteSessionStore } from "../session/SqliteSessionStore.js";
9
10
 
10
- // Session configuration
11
- export const sessionConfig = {
12
- store: new PgSession({
11
+ const sessionStore = dbType === "sqlite"
12
+ ? new SqliteSessionStore({
13
+ db: dblogin,
14
+ tableName: "session",
15
+ createTableIfMissing: true,
16
+ disableTouch: true
17
+ })
18
+ : new PgSession({
13
19
  pool: dblogin,
14
20
  tableName: "session",
15
21
  createTableIfMissing: true,
@@ -17,7 +23,11 @@ export const sessionConfig = {
17
23
  // This avoids an UPDATE per request, which can significantly reduce DB load under burst traffic.
18
24
  // The session will still expire based on the cookie maxAge and the TTL stored when the session is saved.
19
25
  disableTouch: true
20
- }),
26
+ });
27
+
28
+ // Session configuration
29
+ export const sessionConfig = {
30
+ store: sessionStore,
21
31
  secret: mbkautheVar.SESSION_SECRET_KEY,
22
32
  resave: false,
23
33
  saveUninitialized: false,
@@ -35,7 +45,7 @@ export const sessionConfig = {
35
45
  name: 'mbkauthe.sid'
36
46
  };
37
47
 
38
- const authRepo = new AuthRepository({ db: dblogin });
48
+ const authRepo = new AuthRepository({ db: dblogin, dialect });
39
49
  const hasAuthorizationHeader = (req) => typeof req.headers?.authorization === 'string' && req.headers.authorization.trim().length > 0;
40
50
 
41
51
  export function securityHeadersMiddleware(req, res, next) {
package/lib/pool.js CHANGED
@@ -2,24 +2,39 @@ import pkg from "pg";
2
2
  const { Pool } = pkg;
3
3
  import { mbkautheVar } from "#config.js";
4
4
  import { attachDevQueryLogger, runWithRequestContext, getRequestContext } from "./utils/dbQueryLogger.js";
5
+ import { postgresDialect } from "./db/dialects/postgres.js";
6
+ import { sqliteDialect } from "./db/dialects/sqlite.js";
7
+ import { SqlitePool } from "./db/sqlitePool.js";
5
8
  import dotenv from "dotenv";
6
9
  dotenv.config();
7
10
 
8
11
  export { runWithRequestContext, getRequestContext };
9
12
 
10
- const poolConfig = {
11
- connectionString: mbkautheVar.LOGIN_DB,
12
- ssl: { rejectUnauthorized: true },
13
- max: 10,
14
- idleTimeoutMillis: 30000,
15
- connectionTimeoutMillis: 5000,
16
- application_name: `${mbkautheVar.APP_NAME}-mbkauthe-app`,
17
- };
13
+ export const dbType = (mbkautheVar.DB_TYPE || "postgres").toLowerCase();
14
+ export const dialect = dbType === "sqlite" ? sqliteDialect : postgresDialect;
18
15
 
19
- export const dblogin = new Pool(poolConfig);
16
+ let dblogin;
20
17
 
21
- // Keep pool.js focused on pool setup; attach dev-only query logger from dedicated module.
22
- attachDevQueryLogger(dblogin);
18
+ if (dbType === "sqlite") {
19
+ dblogin = new SqlitePool(mbkautheVar.SQLITE_PATH);
20
+ } else {
21
+ const poolConfig = {
22
+ connectionString: mbkautheVar.LOGIN_DB,
23
+ ssl: { rejectUnauthorized: true },
24
+ max: 10,
25
+ idleTimeoutMillis: 30000,
26
+ connectionTimeoutMillis: 5000,
27
+ application_name: `${mbkautheVar.APP_NAME}-mbkauthe-app`,
28
+ };
29
+
30
+ dblogin = new Pool(poolConfig);
31
+
32
+ // Keep pool.js focused on pool setup; attach dev-only query logger from dedicated module.
33
+ // Only meaningful for the pg driver's event-emitter based pool.
34
+ attachDevQueryLogger(dblogin);
35
+ }
36
+
37
+ export { dblogin };
23
38
 
24
39
  /*
25
40
  attachDevQueryLogger([
@@ -38,4 +53,4 @@ attachDevQueryLogger(dblogin);
38
53
  console.error(`[mbkauthe] Database connection error (pool):`, err);
39
54
  }
40
55
  })();
41
- */
56
+ */
@@ -2,7 +2,7 @@ import express from "express";
2
2
  import csurf from "csurf";
3
3
  import speakeasy from "speakeasy";
4
4
  import rateLimit from 'express-rate-limit';
5
- import { dblogin } from "#pool.js";
5
+ import { dblogin, dialect } from "#pool.js";
6
6
  import { mbkautheVar } from "#config.js";
7
7
  import {
8
8
  cachedCookieOptions, cachedClearCookieOptions, clearSessionCookies,
@@ -17,7 +17,7 @@ import { AuthRepository } from "../db/AuthRepository.js";
17
17
  import { createLogger } from "../utils/logger.js";
18
18
 
19
19
  const router = express.Router();
20
- const authRepo = new AuthRepository({ db: dblogin });
20
+ const authRepo = new AuthRepository({ db: dblogin, dialect });
21
21
  const logAuth = createLogger("auth");
22
22
 
23
23
  // Helper function to clear profile picture cache
@@ -376,6 +376,7 @@ router.post("/api/login", LoginLimit, async (req, res) => {
376
376
  passwordMatches = await verifyPassword(password, user.UserName, user.PasswordEnc);
377
377
  }
378
378
 
379
+ // To Bypass Password check for testing, comment out this condition
379
380
  if (!passwordMatches) {
380
381
  logError('Login attempt', ErrorCodes.INCORRECT_PASSWORD, { username: trimmedUsername });
381
382
  return res.status(401).json(
@@ -5,7 +5,7 @@ import { mbkautheVar, packageJson, appVersion } from "#config.js";
5
5
  import { renderError, renderPage } from "#response.js";
6
6
  import { authenticate, sessVal, sessRole } from "../middleware/auth.js";
7
7
  import { ErrorCodes, ErrorMessages, createErrorResponse } from "../utils/errors.js";
8
- import { dblogin } from "#pool.js";
8
+ import { dblogin, dialect } from "#pool.js";
9
9
  import { clearSessionCookies, decryptSessionId, cachedCookieOptions, getCookieDomain } from "#cookies.js";
10
10
  import { AuthRepository } from "../db/AuthRepository.js";
11
11
  import { isSafeFetchUrl } from "../utils/urlSafety.js";
@@ -21,7 +21,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
21
 
22
22
 
23
23
  const router = express.Router();
24
- const authRepo = new AuthRepository({ db: dblogin });
24
+ const authRepo = new AuthRepository({ db: dblogin, dialect });
25
25
  const logMisc = createLogger("misc");
26
26
  const PROFILE_IMAGE_CACHE_SECONDS = 300;
27
27
  const PROFILE_IMAGE_CACHE_CONTROL = `private, max-age=${PROFILE_IMAGE_CACHE_SECONDS}, stale-while-revalidate=${PROFILE_IMAGE_CACHE_SECONDS}`;
@@ -4,7 +4,7 @@ import GitHubStrategy from 'passport-github2';
4
4
  import GoogleStrategy from 'passport-google-oauth20';
5
5
  import csurf from 'csurf';
6
6
  import rateLimit from 'express-rate-limit';
7
- import { dblogin } from "#pool.js";
7
+ import { dblogin, dialect } from "#pool.js";
8
8
  import { mbkautheVar } from "#config.js";
9
9
  import { renderError } from "../utils/response.js";
10
10
  import { checkTrustedDevice, completeLoginProcess } from "./auth.js";
@@ -12,7 +12,7 @@ import { AuthRepository } from "../db/AuthRepository.js";
12
12
  import { createLogger } from "../utils/logger.js";
13
13
 
14
14
  const router = express.Router();
15
- const authRepo = new AuthRepository({ db: dblogin });
15
+ const authRepo = new AuthRepository({ db: dblogin, dialect });
16
16
  const logOAuth = createLogger("oauth");
17
17
 
18
18
  // CSRF protection middleware
@@ -0,0 +1,127 @@
1
+ import session from "express-session";
2
+
3
+ const Store = session.Store;
4
+
5
+ // SQLite's CURRENT_TIMESTAMP format ('YYYY-MM-DD HH:MM:SS', UTC) — used so
6
+ // this store's `expire` column sorts/compares correctly against any other
7
+ // CURRENT_TIMESTAMP-derived timestamp text elsewhere in the SQLite schema.
8
+ function toSqliteTimestamp(date) {
9
+ return date.toISOString().slice(0, 19).replace("T", " ");
10
+ }
11
+
12
+ /**
13
+ * express-session store for the SQLite backend. Mirrors the table shape and
14
+ * public behavior of connect-pg-simple (same "session" table: sid, sess,
15
+ * expire, username, last_activity) so admin/cleanup queries elsewhere in
16
+ * mbkauthe that read the "session" table work unchanged across both
17
+ * database backends.
18
+ */
19
+ export class SqliteSessionStore extends Store {
20
+ constructor({ db, tableName = "session", createTableIfMissing = false, disableTouch = false } = {}) {
21
+ super();
22
+ if (!db) throw new Error("[mbkauthe] SqliteSessionStore requires a `db` (SqlitePool) instance");
23
+ this.db = db;
24
+ this.tableName = tableName;
25
+ this.disableTouch = disableTouch;
26
+
27
+ if (createTableIfMissing) {
28
+ this.db.execScript(`
29
+ CREATE TABLE IF NOT EXISTS "${this.tableName}" (
30
+ sid TEXT PRIMARY KEY,
31
+ sess TEXT NOT NULL,
32
+ expire TEXT NOT NULL,
33
+ username TEXT,
34
+ last_activity TEXT DEFAULT CURRENT_TIMESTAMP
35
+ );
36
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_expire ON "${this.tableName}" (expire);
37
+ `);
38
+ }
39
+ }
40
+
41
+ get(sid, callback) {
42
+ this.db.query(`SELECT sess, expire FROM "${this.tableName}" WHERE sid = ?`, [sid])
43
+ .then((result) => {
44
+ const row = result.rows?.[0];
45
+ if (!row) return callback(null, undefined);
46
+
47
+ if (row.expire && new Date(row.expire.replace(" ", "T") + "Z").getTime() <= Date.now()) {
48
+ return this.destroy(sid, (err) => callback(err, undefined));
49
+ }
50
+
51
+ try {
52
+ callback(null, JSON.parse(row.sess));
53
+ } catch (err) {
54
+ callback(err);
55
+ }
56
+ })
57
+ .catch(callback);
58
+ }
59
+
60
+ set(sid, sessionData, callback) {
61
+ const maxAge = sessionData.cookie?.maxAge;
62
+ const expireDate = typeof maxAge === "number"
63
+ ? new Date(Date.now() + maxAge)
64
+ : new Date(Date.now() + 24 * 60 * 60 * 1000);
65
+
66
+ const sess = JSON.stringify(sessionData);
67
+ const expire = toSqliteTimestamp(expireDate);
68
+ const username = sessionData.user?.UserName ?? sessionData.UserName ?? null;
69
+
70
+ this.db.query(
71
+ `INSERT INTO "${this.tableName}" (sid, sess, expire, username, last_activity)
72
+ VALUES (?, ?, ?, ?, ?)
73
+ ON CONFLICT(sid) DO UPDATE SET sess = excluded.sess, expire = excluded.expire,
74
+ username = excluded.username, last_activity = excluded.last_activity`,
75
+ [sid, sess, expire, username, toSqliteTimestamp(new Date())]
76
+ )
77
+ .then(() => callback?.(null))
78
+ .catch((err) => callback?.(err));
79
+ }
80
+
81
+ destroy(sid, callback) {
82
+ this.db.query(`DELETE FROM "${this.tableName}" WHERE sid = ?`, [sid])
83
+ .then(() => callback?.(null))
84
+ .catch((err) => callback?.(err));
85
+ }
86
+
87
+ touch(sid, sessionData, callback) {
88
+ if (this.disableTouch) return callback?.(null);
89
+
90
+ const maxAge = sessionData.cookie?.maxAge;
91
+ const expireDate = typeof maxAge === "number"
92
+ ? new Date(Date.now() + maxAge)
93
+ : new Date(Date.now() + 24 * 60 * 60 * 1000);
94
+
95
+ this.db.query(
96
+ `UPDATE "${this.tableName}" SET expire = ?, last_activity = ? WHERE sid = ?`,
97
+ [toSqliteTimestamp(expireDate), toSqliteTimestamp(new Date()), sid]
98
+ )
99
+ .then(() => callback?.(null))
100
+ .catch((err) => callback?.(err));
101
+ }
102
+
103
+ all(callback) {
104
+ this.db.query(`SELECT sid, sess FROM "${this.tableName}"`, [])
105
+ .then((result) => {
106
+ try {
107
+ const sessions = (result.rows || []).map((row) => ({ sid: row.sid, ...JSON.parse(row.sess) }));
108
+ callback(null, sessions);
109
+ } catch (err) {
110
+ callback(err);
111
+ }
112
+ })
113
+ .catch(callback);
114
+ }
115
+
116
+ length(callback) {
117
+ this.db.query(`SELECT COUNT(*) AS count FROM "${this.tableName}"`, [])
118
+ .then((result) => callback(null, Number(result.rows?.[0]?.count ?? 0)))
119
+ .catch(callback);
120
+ }
121
+
122
+ clear(callback) {
123
+ this.db.query(`DELETE FROM "${this.tableName}"`, [])
124
+ .then(() => callback?.(null))
125
+ .catch((err) => callback?.(err));
126
+ }
127
+ }