@rebasepro/server-mongo 0.10.0 → 0.10.1-canary.14e53ae

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/server-mongo",
3
3
  "type": "module",
4
- "version": "0.10.0",
4
+ "version": "0.10.1-canary.14e53ae",
5
5
  "description": "MongoDB backend for Rebase",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -40,9 +40,9 @@
40
40
  "dependencies": {
41
41
  "mongodb": "^7.3.0",
42
42
  "ws": "^8.21.0",
43
- "@rebasepro/common": "0.10.0",
44
- "@rebasepro/types": "0.10.0",
45
- "@rebasepro/utils": "0.10.0"
43
+ "@rebasepro/utils": "0.10.1-canary.14e53ae",
44
+ "@rebasepro/common": "0.10.1-canary.14e53ae",
45
+ "@rebasepro/types": "0.10.1-canary.14e53ae"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/jest": "^30.0.0",
@@ -55,7 +55,7 @@
55
55
  "vite": "^8.0.16"
56
56
  },
57
57
  "peerDependencies": {
58
- "@rebasepro/server": "0.10.0"
58
+ "@rebasepro/server": "0.10.1-canary.14e53ae"
59
59
  },
60
60
  "peerDependenciesMeta": {
61
61
  "@rebasepro/server": {
@@ -12,6 +12,7 @@ import {
12
12
  RoleData,
13
13
  CreateRoleData,
14
14
  RefreshTokenInfo,
15
+ RefreshTokenSession,
15
16
  PasswordResetTokenInfo,
16
17
  MagicLinkTokenInfo,
17
18
  UserIdentityData,
@@ -336,40 +337,89 @@ export class MongoRefreshTokenService {
336
337
  return this.db.collection<MongoDoc>("rebase_refresh_tokens");
337
338
  }
338
339
 
339
- async createToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void> {
340
+ private toInfo(doc: MongoDoc): RefreshTokenInfo {
341
+ return {
342
+ id: doc.id,
343
+ uid: doc.uid,
344
+ tokenHash: doc.tokenHash,
345
+ expiresAt: new Date(doc.expiresAt),
346
+ createdAt: new Date(doc.createdAt),
347
+ userAgent: doc.userAgent,
348
+ ipAddress: doc.ipAddress,
349
+ sessionId: doc.sessionId,
350
+ rotatedAt: doc.rotatedAt ? new Date(doc.rotatedAt) : null,
351
+ revoked: Boolean(doc.revoked),
352
+ sessionStartedAt: new Date(doc.sessionStartedAt || doc.createdAt)
353
+ };
354
+ }
355
+
356
+ async createToken(
357
+ uid: string,
358
+ tokenHash: string,
359
+ expiresAt: Date,
360
+ userAgent?: string,
361
+ ipAddress?: string,
362
+ session?: RefreshTokenSession
363
+ ): Promise<void> {
340
364
  const safeUserAgent = userAgent || "";
341
365
  const safeIpAddress = ipAddress || "";
342
366
 
343
- await this.collection.deleteMany({
344
- uid,
345
- userAgent: safeUserAgent,
346
- ipAddress: safeIpAddress
347
- });
348
-
367
+ // No deleteMany first. Tokens of one sign-in accumulate under a shared
368
+ // sessionId and are pruned once nobody can still be holding them —
369
+ // evicting by (uid, userAgent, ipAddress) is what used to sign out a
370
+ // second browser profile behind the same address.
371
+ const now = new Date();
349
372
  await this.collection.insertOne({
350
373
  _id: new ObjectId().toString(),
351
374
  id: new ObjectId().toString(),
352
375
  uid,
353
376
  tokenHash,
354
377
  expiresAt,
355
- createdAt: new Date(),
378
+ createdAt: now,
356
379
  userAgent: safeUserAgent,
357
- ipAddress: safeIpAddress
380
+ ipAddress: safeIpAddress,
381
+ sessionId: session?.id ?? new ObjectId().toString(),
382
+ sessionStartedAt: session?.startedAt ?? now,
383
+ rotatedAt: null,
384
+ revoked: false
358
385
  });
359
386
  }
360
387
 
361
388
  async findByHash(tokenHash: string): Promise<RefreshTokenInfo | null> {
362
389
  const doc = await this.collection.findOne({ tokenHash });
363
- if (!doc) return null;
364
- return {
365
- id: doc.id,
366
- uid: doc.uid,
367
- tokenHash: doc.tokenHash,
368
- expiresAt: new Date(doc.expiresAt),
369
- createdAt: new Date(doc.createdAt),
370
- userAgent: doc.userAgent,
371
- ipAddress: doc.ipAddress
372
- };
390
+ return doc ? this.toInfo(doc) : null;
391
+ }
392
+
393
+ /** Superseded, not gone — see the Postgres service for why that matters. */
394
+ async markRotated(tokenHash: string): Promise<void> {
395
+ await this.collection.updateOne({ tokenHash }, { $set: { rotatedAt: new Date() } });
396
+ }
397
+
398
+ async revokeSession(sessionId: string): Promise<void> {
399
+ await this.collection.updateMany(
400
+ { sessionId },
401
+ { $set: { revoked: true, rotatedAt: new Date() } }
402
+ );
403
+ }
404
+
405
+ async prune(uid: string, sessionId: string, supersededBefore: Date): Promise<void> {
406
+ await this.collection.deleteMany({
407
+ uid,
408
+ $or: [
409
+ { expiresAt: { $lt: new Date() } },
410
+ { sessionId, rotatedAt: { $ne: null, $lt: supersededBefore } }
411
+ ]
412
+ });
413
+ }
414
+
415
+ async getTokensValidAfter(uid: string): Promise<Date | null> {
416
+ const user = await this.db.collection<MongoDoc>("rebase_users").findOne({ id: uid });
417
+ return user?.tokensValidAfter ? new Date(user.tokensValidAfter) : null;
418
+ }
419
+
420
+ async setTokensValidAfter(uid: string, at: Date): Promise<void> {
421
+ await this.db.collection<MongoDoc>("rebase_users")
422
+ .updateOne({ id: uid }, { $set: { tokensValidAfter: at } });
373
423
  }
374
424
 
375
425
  async deleteByHash(tokenHash: string): Promise<void> {
@@ -382,15 +432,7 @@ export class MongoRefreshTokenService {
382
432
 
383
433
  async listForUser(uid: string): Promise<RefreshTokenInfo[]> {
384
434
  const docs = await this.collection.find({ uid }).sort({ createdAt: 1 }).toArray();
385
- return docs.map(doc => ({
386
- id: doc.id,
387
- uid: doc.uid,
388
- tokenHash: doc.tokenHash,
389
- expiresAt: new Date(doc.expiresAt),
390
- createdAt: new Date(doc.createdAt),
391
- userAgent: doc.userAgent,
392
- ipAddress: doc.ipAddress
393
- }));
435
+ return docs.map(doc => this.toInfo(doc));
394
436
  }
395
437
 
396
438
  async deleteById(id: string, uid: string): Promise<void> {
@@ -459,8 +501,28 @@ export class MongoTokenRepository implements TokenRepository {
459
501
  this.passwordResetTokenService = new MongoPasswordResetTokenService(db);
460
502
  }
461
503
 
462
- async createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void> {
463
- await this.refreshTokenService.createToken(uid, tokenHash, expiresAt, userAgent, ipAddress);
504
+ async createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<void> {
505
+ await this.refreshTokenService.createToken(uid, tokenHash, expiresAt, userAgent, ipAddress, session);
506
+ }
507
+
508
+ async markRefreshTokenRotated(tokenHash: string): Promise<void> {
509
+ await this.refreshTokenService.markRotated(tokenHash);
510
+ }
511
+
512
+ async revokeRefreshTokenSession(sessionId: string): Promise<void> {
513
+ await this.refreshTokenService.revokeSession(sessionId);
514
+ }
515
+
516
+ async pruneRefreshTokens(uid: string, sessionId: string, supersededBefore: Date): Promise<void> {
517
+ await this.refreshTokenService.prune(uid, sessionId, supersededBefore);
518
+ }
519
+
520
+ async getTokensValidAfter(uid: string): Promise<Date | null> {
521
+ return this.refreshTokenService.getTokensValidAfter(uid);
522
+ }
523
+
524
+ async setTokensValidAfter(uid: string, at: Date): Promise<void> {
525
+ await this.refreshTokenService.setTokensValidAfter(uid, at);
464
526
  }
465
527
 
466
528
  async findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null> {
@@ -631,8 +693,28 @@ export class MongoAuthRepository implements AuthRepository {
631
693
  await this.roleService.deleteRole(id);
632
694
  }
633
695
 
634
- async createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void> {
635
- await this.tokenRepository.createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress);
696
+ async createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<void> {
697
+ await this.tokenRepository.createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress, session);
698
+ }
699
+
700
+ async markRefreshTokenRotated(tokenHash: string): Promise<void> {
701
+ await this.tokenRepository.markRefreshTokenRotated(tokenHash);
702
+ }
703
+
704
+ async revokeRefreshTokenSession(sessionId: string): Promise<void> {
705
+ await this.tokenRepository.revokeRefreshTokenSession(sessionId);
706
+ }
707
+
708
+ async pruneRefreshTokens(uid: string, sessionId: string, supersededBefore: Date): Promise<void> {
709
+ await this.tokenRepository.pruneRefreshTokens(uid, sessionId, supersededBefore);
710
+ }
711
+
712
+ async getTokensValidAfter(uid: string): Promise<Date | null> {
713
+ return this.tokenRepository.getTokensValidAfter(uid);
714
+ }
715
+
716
+ async setTokensValidAfter(uid: string, at: Date): Promise<void> {
717
+ await this.tokenRepository.setTokensValidAfter(uid, at);
636
718
  }
637
719
 
638
720
  async findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null> {