@lastshotlabs/bunshot 0.0.8 → 0.0.10

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 (44) hide show
  1. package/README.md +97 -7
  2. package/dist/adapters/memoryAuth.d.ts +13 -3
  3. package/dist/adapters/memoryAuth.js +116 -8
  4. package/dist/adapters/sqliteAuth.d.ts +13 -3
  5. package/dist/adapters/sqliteAuth.js +93 -15
  6. package/dist/app.d.ts +39 -2
  7. package/dist/app.js +23 -5
  8. package/dist/cli.js +0 -0
  9. package/dist/entrypoints/mongo.d.ts +3 -0
  10. package/dist/entrypoints/mongo.js +3 -0
  11. package/dist/entrypoints/queue.d.ts +2 -0
  12. package/dist/entrypoints/queue.js +1 -0
  13. package/dist/entrypoints/redis.d.ts +1 -0
  14. package/dist/entrypoints/redis.js +1 -0
  15. package/dist/index.d.ts +6 -8
  16. package/dist/index.js +5 -6
  17. package/dist/lib/appConfig.d.ts +17 -0
  18. package/dist/lib/appConfig.js +20 -0
  19. package/dist/lib/context.d.ts +1 -0
  20. package/dist/lib/emailVerification.js +11 -10
  21. package/dist/lib/jwt.d.ts +1 -1
  22. package/dist/lib/jwt.js +1 -1
  23. package/dist/lib/mongo.d.ts +9 -4
  24. package/dist/lib/mongo.js +61 -10
  25. package/dist/lib/oauth.js +11 -10
  26. package/dist/lib/queue.d.ts +3 -4
  27. package/dist/lib/queue.js +18 -3
  28. package/dist/lib/redis.d.ts +3 -8
  29. package/dist/lib/redis.js +19 -8
  30. package/dist/lib/resetPassword.d.ts +12 -0
  31. package/dist/lib/resetPassword.js +95 -0
  32. package/dist/lib/session.d.ts +20 -3
  33. package/dist/lib/session.js +288 -35
  34. package/dist/middleware/cacheResponse.js +10 -9
  35. package/dist/middleware/identify.js +21 -7
  36. package/dist/models/AuthUser.d.ts +14 -106
  37. package/dist/models/AuthUser.js +31 -14
  38. package/dist/routes/auth.d.ts +3 -2
  39. package/dist/routes/auth.js +139 -4
  40. package/dist/routes/oauth.js +13 -4
  41. package/dist/services/auth.d.ts +3 -2
  42. package/dist/services/auth.js +20 -11
  43. package/dist/ws/index.js +6 -3
  44. package/package.json +39 -9
@@ -0,0 +1,95 @@
1
+ import { createHash } from "crypto";
2
+ import { getRedis } from "./redis";
3
+ import { appConnection } from "./mongo";
4
+ import { getAppName, getResetTokenExpiry } from "./appConfig";
5
+ import { Schema } from "mongoose";
6
+ import { sqliteCreateResetToken, sqliteConsumeResetToken, } from "../adapters/sqliteAuth";
7
+ import { memoryCreateResetToken, memoryConsumeResetToken, } from "../adapters/memoryAuth";
8
+ // ---------------------------------------------------------------------------
9
+ // Token hashing — store SHA-256(token); raw token is only in the email link.
10
+ // If the store is ever leaked, outstanding tokens cannot be replayed directly.
11
+ // ---------------------------------------------------------------------------
12
+ const hashToken = (token) => createHash("sha256").update(token).digest("hex");
13
+ const resetSchema = new Schema({
14
+ token: { type: String, required: true, unique: true },
15
+ userId: { type: String, required: true },
16
+ email: { type: String, required: true },
17
+ expiresAt: { type: Date, required: true, index: { expireAfterSeconds: 0 } },
18
+ }, { collection: "password_resets" });
19
+ function getResetModel() {
20
+ return appConnection.models["PasswordReset"] ??
21
+ appConnection.model("PasswordReset", resetSchema);
22
+ }
23
+ // ---------------------------------------------------------------------------
24
+ // Redis helpers
25
+ // ---------------------------------------------------------------------------
26
+ /** Atomically GET+DEL a key. Uses native GETDEL (Redis >= 6.2) with a Lua fallback. */
27
+ async function redisGetDel(key) {
28
+ const redis = getRedis();
29
+ if (typeof redis.getdel === "function") {
30
+ try {
31
+ return await redis.getdel(key);
32
+ }
33
+ catch (err) {
34
+ const msg = err?.message ?? "";
35
+ if (!/unknown command|ERR unknown command/i.test(msg))
36
+ throw err;
37
+ // Fall through to Lua on "unknown command"
38
+ }
39
+ }
40
+ const result = await redis.eval("local v = redis.call('GET', KEYS[1])\nif v then redis.call('DEL', KEYS[1]) end\nreturn v", 1, key);
41
+ return result ?? null;
42
+ }
43
+ let _store = "redis";
44
+ export const setPasswordResetStore = (store) => { _store = store; };
45
+ // ---------------------------------------------------------------------------
46
+ // Public API
47
+ // ---------------------------------------------------------------------------
48
+ /** Create a reset token. Returns the raw token (to embed in the email link).
49
+ * Only the SHA-256 hash is persisted in the store. */
50
+ export const createResetToken = async (userId, email) => {
51
+ const token = crypto.randomUUID();
52
+ const hash = hashToken(token);
53
+ const ttl = getResetTokenExpiry();
54
+ if (_store === "memory") {
55
+ memoryCreateResetToken(hash, userId, email, ttl);
56
+ return token;
57
+ }
58
+ if (_store === "sqlite") {
59
+ sqliteCreateResetToken(hash, userId, email, ttl);
60
+ return token;
61
+ }
62
+ if (_store === "mongo") {
63
+ await getResetModel().create({
64
+ token: hash,
65
+ userId,
66
+ email,
67
+ expiresAt: new Date(Date.now() + ttl * 1000),
68
+ });
69
+ return token;
70
+ }
71
+ await getRedis().set(`reset:${getAppName()}:${hash}`, JSON.stringify({ userId, email }), "EX", ttl);
72
+ return token;
73
+ };
74
+ /** Atomically consume a reset token — returns its payload and deletes it in one operation.
75
+ * Returns null if the token is invalid, expired, or already used. */
76
+ export const consumeResetToken = async (token) => {
77
+ const hash = hashToken(token);
78
+ if (_store === "memory")
79
+ return memoryConsumeResetToken(hash);
80
+ if (_store === "sqlite")
81
+ return sqliteConsumeResetToken(hash);
82
+ if (_store === "mongo") {
83
+ const doc = await getResetModel()
84
+ .findOneAndDelete({ token: hash, expiresAt: { $gt: new Date() } })
85
+ .lean();
86
+ if (!doc)
87
+ return null;
88
+ return { userId: doc.userId, email: doc.email };
89
+ }
90
+ // Redis: atomically return and remove the key (GETDEL or Lua fallback)
91
+ const raw = await redisGetDel(`reset:${getAppName()}:${hash}`);
92
+ if (!raw)
93
+ return null;
94
+ return JSON.parse(raw);
95
+ };
@@ -1,6 +1,23 @@
1
+ export interface SessionMetadata {
2
+ ipAddress?: string;
3
+ userAgent?: string;
4
+ }
5
+ export interface SessionInfo {
6
+ sessionId: string;
7
+ createdAt: number;
8
+ lastActiveAt: number;
9
+ expiresAt: number;
10
+ ipAddress?: string;
11
+ userAgent?: string;
12
+ isActive: boolean;
13
+ }
1
14
  type SessionStore = "redis" | "mongo" | "sqlite" | "memory";
2
15
  export declare const setSessionStore: (store: SessionStore) => void;
3
- export declare const createSession: (userId: string, token: string) => Promise<void>;
4
- export declare const getSession: (userId: string) => Promise<any>;
5
- export declare const deleteSession: (userId: string) => Promise<void>;
16
+ export declare const createSession: (userId: string, token: string, sessionId: string, metadata?: SessionMetadata) => Promise<void>;
17
+ export declare const getSession: (sessionId: string) => Promise<string | null>;
18
+ export declare const deleteSession: (sessionId: string) => Promise<void>;
19
+ export declare const getUserSessions: (userId: string) => Promise<SessionInfo[]>;
20
+ export declare const getActiveSessionCount: (userId: string) => Promise<number>;
21
+ export declare const evictOldestSession: (userId: string) => Promise<void>;
22
+ export declare const updateSessionLastActive: (sessionId: string) => Promise<void>;
6
23
  export {};
@@ -1,17 +1,28 @@
1
1
  import { getRedis } from "./redis";
2
- import { appConnection } from "./mongo";
3
- import { getAppName } from "./appConfig";
4
- import { Schema } from "mongoose";
5
- import { sqliteCreateSession, sqliteGetSession, sqliteDeleteSession } from "../adapters/sqliteAuth";
6
- import { memoryCreateSession, memoryGetSession, memoryDeleteSession } from "../adapters/memoryAuth";
7
- const sessionSchema = new Schema({
8
- userId: { type: String, required: true, unique: true },
9
- token: { type: String, required: true },
10
- expiresAt: { type: Date, required: true, index: { expireAfterSeconds: 0 } },
11
- }, { collection: "sessions" });
2
+ import { appConnection, mongoose } from "./mongo";
3
+ import { getAppName, getPersistSessionMetadata, getIncludeInactiveSessions } from "./appConfig";
4
+ import { sqliteCreateSession, sqliteGetSession, sqliteDeleteSession, sqliteGetUserSessions, sqliteGetActiveSessionCount, sqliteEvictOldestSession, sqliteUpdateSessionLastActive, } from "../adapters/sqliteAuth";
5
+ import { memoryCreateSession, memoryGetSession, memoryDeleteSession, memoryGetUserSessions, memoryGetActiveSessionCount, memoryEvictOldestSession, memoryUpdateSessionLastActive, } from "../adapters/memoryAuth";
12
6
  function getSessionModel() {
13
- return appConnection.models["Session"] ??
14
- appConnection.model("Session", sessionSchema);
7
+ if (appConnection.models["Session"])
8
+ return appConnection.models["Session"];
9
+ const { Schema } = mongoose;
10
+ const sessionSchema = new Schema({
11
+ sessionId: { type: String, required: true, unique: true },
12
+ userId: { type: String, required: true, index: true },
13
+ token: { type: String, default: null },
14
+ createdAt: { type: Date, required: true },
15
+ lastActiveAt: { type: Date, required: true },
16
+ expiresAt: { type: Date, required: true },
17
+ ipAddress: { type: String },
18
+ userAgent: { type: String },
19
+ }, { collection: "sessions", timestamps: false });
20
+ // Add TTL index only when metadata is not persisted — docs auto-delete at expiresAt.
21
+ // When persisting, token is nulled (soft-delete) but the row is kept indefinitely.
22
+ if (!getPersistSessionMetadata()) {
23
+ sessionSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
24
+ }
25
+ return appConnection.model("Session", sessionSchema);
15
26
  }
16
27
  let _store = "redis";
17
28
  export const setSessionStore = (store) => { _store = store; };
@@ -19,50 +30,292 @@ export const setSessionStore = (store) => { _store = store; };
19
30
  // TTL
20
31
  // ---------------------------------------------------------------------------
21
32
  const TTL_SECONDS = 60 * 60 * 24 * 7; // 7 days
33
+ const TTL_MS = TTL_SECONDS * 1000;
34
+ // ---------------------------------------------------------------------------
35
+ // Redis helpers
36
+ // ---------------------------------------------------------------------------
37
+ function redisSessionKey(sessionId) {
38
+ return `session:${getAppName()}:${sessionId}`;
39
+ }
40
+ function redisUserSessionsKey(userId) {
41
+ return `usersessions:${getAppName()}:${userId}`;
42
+ }
43
+ async function redisCreateSession(userId, token, sessionId, metadata) {
44
+ const now = Date.now();
45
+ const expiresAt = now + TTL_MS;
46
+ const record = JSON.stringify({
47
+ sessionId, userId, token,
48
+ createdAt: now, lastActiveAt: now, expiresAt,
49
+ ipAddress: metadata?.ipAddress,
50
+ userAgent: metadata?.userAgent,
51
+ });
52
+ const redis = getRedis();
53
+ const persist = getPersistSessionMetadata();
54
+ if (persist) {
55
+ await redis.set(redisSessionKey(sessionId), record);
56
+ }
57
+ else {
58
+ await redis.set(redisSessionKey(sessionId), record, "EX", TTL_SECONDS);
59
+ }
60
+ // Sorted set: score = createdAt (oldest first)
61
+ await redis.zadd(redisUserSessionsKey(userId), now, sessionId);
62
+ }
63
+ async function redisGetSession(sessionId) {
64
+ const raw = await getRedis().get(redisSessionKey(sessionId));
65
+ if (!raw)
66
+ return null;
67
+ const rec = JSON.parse(raw);
68
+ if (!rec.token)
69
+ return null;
70
+ if (rec.expiresAt <= Date.now())
71
+ return null;
72
+ return rec.token;
73
+ }
74
+ async function redisDeleteSession(sessionId) {
75
+ const redis = getRedis();
76
+ const raw = await redis.get(redisSessionKey(sessionId));
77
+ if (!raw)
78
+ return;
79
+ const rec = JSON.parse(raw);
80
+ const persist = getPersistSessionMetadata();
81
+ if (persist) {
82
+ const updated = { ...JSON.parse(raw), token: null };
83
+ await redis.set(redisSessionKey(sessionId), JSON.stringify(updated));
84
+ }
85
+ else {
86
+ await redis.del(redisSessionKey(sessionId));
87
+ }
88
+ if (!persist) {
89
+ await redis.zrem(redisUserSessionsKey(rec.userId), sessionId);
90
+ }
91
+ }
92
+ async function redisGetUserSessions(userId) {
93
+ const redis = getRedis();
94
+ const sessionIds = await redis.zrange(redisUserSessionsKey(userId), 0, -1);
95
+ if (!sessionIds.length)
96
+ return [];
97
+ const now = Date.now();
98
+ const raws = await redis.mget(...sessionIds.map(redisSessionKey));
99
+ const results = [];
100
+ const toRemove = [];
101
+ for (let i = 0; i < sessionIds.length; i++) {
102
+ const raw = raws[i];
103
+ if (!raw) {
104
+ toRemove.push(sessionIds[i]);
105
+ continue;
106
+ }
107
+ const rec = JSON.parse(raw);
108
+ const isActive = !!rec.token && rec.expiresAt > now;
109
+ if (!isActive && !getPersistSessionMetadata()) {
110
+ toRemove.push(sessionIds[i]);
111
+ continue;
112
+ }
113
+ if (!isActive && !getIncludeInactiveSessions())
114
+ continue;
115
+ results.push({
116
+ sessionId: rec.sessionId,
117
+ createdAt: Number(rec.createdAt),
118
+ lastActiveAt: Number(rec.lastActiveAt),
119
+ expiresAt: Number(rec.expiresAt),
120
+ ipAddress: rec.ipAddress,
121
+ userAgent: rec.userAgent,
122
+ isActive,
123
+ });
124
+ }
125
+ if (toRemove.length) {
126
+ await redis.zrem(redisUserSessionsKey(userId), ...toRemove);
127
+ }
128
+ return results;
129
+ }
130
+ async function redisGetActiveSessionCount(userId) {
131
+ const sessions = await redisGetUserSessions(userId);
132
+ return sessions.filter((s) => s.isActive).length;
133
+ }
134
+ async function redisEvictOldestSession(userId) {
135
+ const redis = getRedis();
136
+ // Sorted set is ordered oldest-first (score = createdAt)
137
+ const sessionIds = await redis.zrange(redisUserSessionsKey(userId), 0, -1);
138
+ const now = Date.now();
139
+ for (const sessionId of sessionIds) {
140
+ const raw = await redis.get(redisSessionKey(sessionId));
141
+ if (!raw) {
142
+ await redis.zrem(redisUserSessionsKey(userId), sessionId);
143
+ continue;
144
+ }
145
+ const rec = JSON.parse(raw);
146
+ if (rec.token && rec.expiresAt > now) {
147
+ await redisDeleteSession(sessionId);
148
+ return;
149
+ }
150
+ }
151
+ }
152
+ async function redisUpdateSessionLastActive(sessionId) {
153
+ const redis = getRedis();
154
+ const raw = await redis.get(redisSessionKey(sessionId));
155
+ if (!raw)
156
+ return;
157
+ const rec = JSON.parse(raw);
158
+ rec.lastActiveAt = Date.now();
159
+ if (getPersistSessionMetadata()) {
160
+ await redis.set(redisSessionKey(sessionId), JSON.stringify(rec));
161
+ }
162
+ else {
163
+ const now = Date.now();
164
+ if (rec.expiresAt <= now) {
165
+ await redisDeleteSession(sessionId);
166
+ return;
167
+ }
168
+ const ttlRemaining = Math.max(1, Math.ceil((rec.expiresAt - now) / 1000));
169
+ await redis.set(redisSessionKey(sessionId), JSON.stringify(rec), "EX", ttlRemaining);
170
+ }
171
+ }
172
+ // ---------------------------------------------------------------------------
173
+ // Mongo helpers
174
+ // ---------------------------------------------------------------------------
175
+ async function mongoGetUserSessions(userId) {
176
+ const now = new Date();
177
+ const includeInactive = getIncludeInactiveSessions();
178
+ const persist = getPersistSessionMetadata();
179
+ const query = { userId };
180
+ if (!includeInactive) {
181
+ query.token = { $ne: null };
182
+ query.expiresAt = { $gt: now };
183
+ }
184
+ const docs = await getSessionModel().find(query).lean();
185
+ const results = [];
186
+ for (const doc of docs) {
187
+ const isActive = !!doc.token && doc.expiresAt > now;
188
+ if (!isActive && !persist)
189
+ continue;
190
+ if (!isActive && !includeInactive)
191
+ continue;
192
+ results.push({
193
+ sessionId: doc.sessionId,
194
+ createdAt: doc.createdAt.getTime(),
195
+ lastActiveAt: doc.lastActiveAt.getTime(),
196
+ expiresAt: doc.expiresAt.getTime(),
197
+ ipAddress: doc.ipAddress,
198
+ userAgent: doc.userAgent,
199
+ isActive,
200
+ });
201
+ }
202
+ return results;
203
+ }
22
204
  // ---------------------------------------------------------------------------
23
205
  // Public API
24
206
  // ---------------------------------------------------------------------------
25
- export const createSession = async (userId, token) => {
207
+ export const createSession = async (userId, token, sessionId, metadata) => {
26
208
  if (_store === "memory") {
27
- memoryCreateSession(userId, token);
209
+ memoryCreateSession(userId, token, sessionId, metadata);
28
210
  return;
29
211
  }
30
212
  if (_store === "sqlite") {
31
- sqliteCreateSession(userId, token);
213
+ sqliteCreateSession(userId, token, sessionId, metadata);
32
214
  return;
33
215
  }
34
- if (_store === "mongo") {
35
- const expiresAt = new Date(Date.now() + TTL_SECONDS * 1000);
36
- await getSessionModel().updateOne({ userId }, { $set: { token, expiresAt } }, { upsert: true });
216
+ if (_store === "redis") {
217
+ await redisCreateSession(userId, token, sessionId, metadata);
37
218
  return;
38
219
  }
39
- await getRedis().set(`session:${getAppName()}:${userId}`, token, "EX", TTL_SECONDS);
220
+ // mongo
221
+ const now = new Date();
222
+ const expiresAt = new Date(Date.now() + TTL_MS);
223
+ await getSessionModel().create({
224
+ sessionId, userId, token,
225
+ createdAt: now, lastActiveAt: now, expiresAt,
226
+ ipAddress: metadata?.ipAddress,
227
+ userAgent: metadata?.userAgent,
228
+ });
40
229
  };
41
- export const getSession = async (userId) => {
230
+ export const getSession = async (sessionId) => {
42
231
  if (_store === "memory")
43
- return memoryGetSession(userId);
232
+ return memoryGetSession(sessionId);
44
233
  if (_store === "sqlite")
45
- return sqliteGetSession(userId);
46
- if (_store === "mongo") {
47
- const doc = await getSessionModel()
48
- .findOne({ userId, expiresAt: { $gt: new Date() } }, "token")
49
- .lean();
50
- return doc ? doc.token : null;
51
- }
52
- return getRedis().get(`session:${getAppName()}:${userId}`);
234
+ return sqliteGetSession(sessionId);
235
+ if (_store === "redis")
236
+ return redisGetSession(sessionId);
237
+ // mongo
238
+ const doc = await getSessionModel()
239
+ .findOne({ sessionId, expiresAt: { $gt: new Date() } }, "token")
240
+ .lean();
241
+ return doc?.token ?? null;
242
+ };
243
+ export const deleteSession = async (sessionId) => {
244
+ if (_store === "memory") {
245
+ memoryDeleteSession(sessionId);
246
+ return;
247
+ }
248
+ if (_store === "sqlite") {
249
+ sqliteDeleteSession(sessionId);
250
+ return;
251
+ }
252
+ if (_store === "redis") {
253
+ await redisDeleteSession(sessionId);
254
+ return;
255
+ }
256
+ // mongo
257
+ if (getPersistSessionMetadata()) {
258
+ await getSessionModel().updateOne({ sessionId }, { $set: { token: null } });
259
+ }
260
+ else {
261
+ await getSessionModel().deleteOne({ sessionId });
262
+ }
263
+ };
264
+ export const getUserSessions = async (userId) => {
265
+ if (_store === "memory")
266
+ return memoryGetUserSessions(userId);
267
+ if (_store === "sqlite")
268
+ return sqliteGetUserSessions(userId);
269
+ if (_store === "redis")
270
+ return redisGetUserSessions(userId);
271
+ return mongoGetUserSessions(userId);
272
+ };
273
+ export const getActiveSessionCount = async (userId) => {
274
+ if (_store === "memory")
275
+ return memoryGetActiveSessionCount(userId);
276
+ if (_store === "sqlite")
277
+ return sqliteGetActiveSessionCount(userId);
278
+ if (_store === "redis")
279
+ return redisGetActiveSessionCount(userId);
280
+ // mongo
281
+ const now = new Date();
282
+ return getSessionModel().countDocuments({ userId, token: { $ne: null }, expiresAt: { $gt: now } });
283
+ };
284
+ export const evictOldestSession = async (userId) => {
285
+ if (_store === "memory") {
286
+ memoryEvictOldestSession(userId);
287
+ return;
288
+ }
289
+ if (_store === "sqlite") {
290
+ sqliteEvictOldestSession(userId);
291
+ return;
292
+ }
293
+ if (_store === "redis") {
294
+ await redisEvictOldestSession(userId);
295
+ return;
296
+ }
297
+ // mongo — oldest active session by createdAt
298
+ const now = new Date();
299
+ const oldest = await getSessionModel()
300
+ .findOne({ userId, token: { $ne: null }, expiresAt: { $gt: now } }, "sessionId")
301
+ .sort({ createdAt: 1 })
302
+ .lean();
303
+ if (oldest)
304
+ await deleteSession(oldest.sessionId);
53
305
  };
54
- export const deleteSession = async (userId) => {
306
+ export const updateSessionLastActive = async (sessionId) => {
55
307
  if (_store === "memory") {
56
- memoryDeleteSession(userId);
308
+ memoryUpdateSessionLastActive(sessionId);
57
309
  return;
58
310
  }
59
311
  if (_store === "sqlite") {
60
- sqliteDeleteSession(userId);
312
+ sqliteUpdateSessionLastActive(sessionId);
61
313
  return;
62
314
  }
63
- if (_store === "mongo") {
64
- await getSessionModel().deleteOne({ userId });
315
+ if (_store === "redis") {
316
+ await redisUpdateSessionLastActive(sessionId);
65
317
  return;
66
318
  }
67
- await getRedis().del(`session:${getAppName()}:${userId}`);
319
+ // mongo
320
+ await getSessionModel().updateOne({ sessionId }, { $set: { lastActiveAt: new Date() } });
68
321
  };
@@ -1,17 +1,18 @@
1
1
  import { getRedis } from "../lib/redis";
2
2
  import { getAppName } from "../lib/appConfig";
3
- import { appConnection } from "../lib/mongo";
4
- import { Schema } from "mongoose";
3
+ import { appConnection, mongoose } from "../lib/mongo";
5
4
  import { isSqliteReady, sqliteGetCache, sqliteSetCache, sqliteDelCache, sqliteDelCachePattern } from "../adapters/sqliteAuth";
6
5
  import { memoryGetCache, memorySetCache, memoryDelCache, memoryDelCachePattern } from "../adapters/memoryAuth";
7
- const cacheSchema = new Schema({
8
- key: { type: String, required: true, unique: true },
9
- value: { type: String, required: true },
10
- expiresAt: { type: Date, index: { expireAfterSeconds: 0 } },
11
- }, { collection: "cache_entries" });
12
6
  function getCacheModel() {
13
- return appConnection.models["CacheEntry"] ??
14
- appConnection.model("CacheEntry", cacheSchema);
7
+ if (appConnection.models["CacheEntry"])
8
+ return appConnection.models["CacheEntry"];
9
+ const { Schema } = mongoose;
10
+ const cacheSchema = new Schema({
11
+ key: { type: String, required: true, unique: true },
12
+ value: { type: String, required: true },
13
+ expiresAt: { type: Date, index: { expireAfterSeconds: 0 } },
14
+ }, { collection: "cache_entries" });
15
+ return appConnection.model("CacheEntry", cacheSchema);
15
16
  }
16
17
  function isMongoReady() {
17
18
  return appConnection.readyState === 1;
@@ -1,25 +1,39 @@
1
1
  import { getCookie } from "hono/cookie";
2
2
  import { verifyToken } from "../lib/jwt";
3
- import { getSession } from "../lib/session";
3
+ import { getSession, updateSessionLastActive } from "../lib/session";
4
4
  import { COOKIE_TOKEN, HEADER_USER_TOKEN } from "../lib/constants";
5
5
  import { log } from "../lib/logger";
6
+ import { getTrackLastActive } from "../lib/appConfig";
6
7
  export const identify = async (c, next) => {
7
8
  c.set("authUserId", null);
8
9
  c.set("roles", null);
10
+ c.set("sessionId", null);
9
11
  // cookie for browsers, x-user-token header for non-browser clients
10
12
  const token = getCookie(c, COOKIE_TOKEN) ?? c.req.header(HEADER_USER_TOKEN) ?? null;
11
13
  log(`[identify] token=${token ? "present" : "absent"}`);
12
14
  if (token) {
13
15
  try {
14
16
  const payload = await verifyToken(token);
15
- const stored = await getSession(payload.sub);
16
- log(`[identify] token for authUserId=${payload.sub} verified, checking session...`);
17
- if (stored === token) {
18
- c.set("authUserId", payload.sub);
19
- log(`[identify] authUserId=${payload.sub}`);
17
+ const sessionId = payload.sid;
18
+ if (!sessionId) {
19
+ log("[identify] token missing sid claim — unauthenticated");
20
20
  }
21
21
  else {
22
- log("[identify] token/session mismatch unauthenticated");
22
+ const stored = await getSession(sessionId);
23
+ log(`[identify] token for authUserId=${payload.sub} verified, checking session...`);
24
+ if (stored === token) {
25
+ c.set("authUserId", payload.sub);
26
+ c.set("sessionId", sessionId);
27
+ log(`[identify] authUserId=${payload.sub} sessionId=${sessionId}`);
28
+ if (getTrackLastActive()) {
29
+ updateSessionLastActive(sessionId).catch(() => {
30
+ log(`[identify] failed to update lastActiveAt for sessionId=${sessionId}`);
31
+ });
32
+ }
33
+ }
34
+ else {
35
+ log("[identify] token/session mismatch — unauthenticated");
36
+ }
23
37
  }
24
38
  }
25
39
  catch {
@@ -1,112 +1,20 @@
1
- import mongoose from "mongoose";
2
- export declare const AuthUser: mongoose.Model<{
1
+ import type { Document, Model } from "mongoose";
2
+ interface IAuthUser {
3
+ email?: string | null;
4
+ password?: string | null;
5
+ /** Compound provider keys: ["google:123456", "apple:000111"] */
3
6
  providerIds: string[];
4
- emailVerified: boolean;
7
+ /** App-defined roles assigned to this user: ["admin", "editor", ...] */
5
8
  roles: string[];
6
- email?: string | null | undefined;
7
- password?: string | null | undefined;
8
- } & mongoose.DefaultTimestampProps, {}, {}, {
9
- id: string;
10
- }, mongoose.Document<unknown, {}, {
11
- providerIds: string[];
9
+ /** Whether the user's email address has been verified. */
12
10
  emailVerified: boolean;
13
- roles: string[];
14
- email?: string | null | undefined;
15
- password?: string | null | undefined;
16
- } & mongoose.DefaultTimestampProps, {
17
- id: string;
18
- }, {
19
- timestamps: true;
20
- }> & Omit<{
21
- providerIds: string[];
22
- emailVerified: boolean;
23
- roles: string[];
24
- email?: string | null | undefined;
25
- password?: string | null | undefined;
26
- } & mongoose.DefaultTimestampProps & {
27
- _id: mongoose.Types.ObjectId;
28
- } & {
11
+ }
12
+ type AuthUserDocument = IAuthUser & Document;
13
+ export declare const AuthUser: Model<AuthUserDocument, {}, {}, {}, Document<unknown, {}, AuthUserDocument, {}, import("mongoose").DefaultSchemaOptions> & IAuthUser & Document<import("mongoose").Types.ObjectId, any, any, Record<string, any>, {}> & Required<{
14
+ _id: import("mongoose").Types.ObjectId;
15
+ }> & {
29
16
  __v: number;
30
- }, "id"> & {
31
- id: string;
32
- }, mongoose.Schema<any, mongoose.Model<any, any, any, any, any, any, any>, {}, {}, {}, {}, {
33
- timestamps: true;
34
- }, {
35
- providerIds: string[];
36
- emailVerified: boolean;
37
- roles: string[];
38
- email?: string | null | undefined;
39
- password?: string | null | undefined;
40
- } & mongoose.DefaultTimestampProps, mongoose.Document<unknown, {}, {
41
- providerIds: string[];
42
- emailVerified: boolean;
43
- roles: string[];
44
- email?: string | null | undefined;
45
- password?: string | null | undefined;
46
- } & mongoose.DefaultTimestampProps, {
47
- id: string;
48
- }, mongoose.MergeType<mongoose.DefaultSchemaOptions, {
49
- timestamps: true;
50
- }>> & Omit<{
51
- providerIds: string[];
52
- emailVerified: boolean;
53
- roles: string[];
54
- email?: string | null | undefined;
55
- password?: string | null | undefined;
56
- } & mongoose.DefaultTimestampProps & {
57
- _id: mongoose.Types.ObjectId;
58
17
  } & {
59
- __v: number;
60
- }, "id"> & {
61
18
  id: string;
62
- }, {
63
- [path: string]: mongoose.SchemaDefinitionProperty<undefined, any, any>;
64
- } | {
65
- [x: string]: mongoose.SchemaDefinitionProperty<any, any, mongoose.Document<unknown, {}, {
66
- providerIds: string[];
67
- emailVerified: boolean;
68
- roles: string[];
69
- email?: string | null | undefined;
70
- password?: string | null | undefined;
71
- } & mongoose.DefaultTimestampProps, {
72
- id: string;
73
- }, mongoose.MergeType<mongoose.DefaultSchemaOptions, {
74
- timestamps: true;
75
- }>> & Omit<{
76
- providerIds: string[];
77
- emailVerified: boolean;
78
- roles: string[];
79
- email?: string | null | undefined;
80
- password?: string | null | undefined;
81
- } & mongoose.DefaultTimestampProps & {
82
- _id: mongoose.Types.ObjectId;
83
- } & {
84
- __v: number;
85
- }, "id"> & {
86
- id: string;
87
- }> | undefined;
88
- }, {
89
- providerIds: string[];
90
- emailVerified: boolean;
91
- roles: string[];
92
- email?: string | null | undefined;
93
- password?: string | null | undefined;
94
- createdAt: NativeDate;
95
- updatedAt: NativeDate;
96
- } & {
97
- _id: mongoose.Types.ObjectId;
98
- } & {
99
- __v: number;
100
- }>, {
101
- providerIds: string[];
102
- emailVerified: boolean;
103
- roles: string[];
104
- email?: string | null | undefined;
105
- password?: string | null | undefined;
106
- createdAt: NativeDate;
107
- updatedAt: NativeDate;
108
- } & {
109
- _id: mongoose.Types.ObjectId;
110
- } & {
111
- __v: number;
112
- }>;
19
+ }, any, AuthUserDocument>;
20
+ export {};