@rebasepro/server-postgres 0.10.0 → 0.10.1-canary.18115ba

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.
@@ -1,6 +1,6 @@
1
1
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
2
  import type { RebasePgTable } from "../types";
3
- import { UserRepository, TokenRepository, MfaRepository, AuthRepository, UserData, CreateUserData, RoleData, CreateRoleData, RefreshTokenInfo, PasswordResetTokenInfo, MagicLinkTokenInfo, UserIdentityData, ListUsersOptions, PaginatedUsersResult, MfaFactor, MfaChallengeInfo, RoleData as Role } from "@rebasepro/server";
3
+ import { UserRepository, TokenRepository, MfaRepository, AuthRepository, UserData, CreateUserData, RoleData, CreateRoleData, RefreshTokenInfo, RefreshTokenSession, PasswordResetTokenInfo, MagicLinkTokenInfo, UserIdentityData, ListUsersOptions, PaginatedUsersResult, MfaFactor, MfaChallengeInfo, RoleData as Role } from "@rebasepro/server";
4
4
  export type { Role };
5
5
  export interface AuthSchemaTables {
6
6
  users: RebasePgTable;
@@ -90,9 +90,38 @@ export declare class UserService implements UserRepository {
90
90
  export declare class RefreshTokenService {
91
91
  private db;
92
92
  private refreshTokensTable;
93
+ private usersTable;
93
94
  constructor(db: NodePgDatabase, tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>);
94
- createToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void>;
95
+ /**
96
+ * Whether the table actually carries a column, so a host application that
97
+ * supplied its own `refresh_tokens` table — one that predates session
98
+ * grouping — degrades instead of throwing on every sign-in.
99
+ */
100
+ private has;
101
+ private col;
102
+ /** The columns to read back, narrowed to the ones this table has. */
103
+ private selection;
104
+ createToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<void>;
95
105
  findByHash(tokenHash: string): Promise<RefreshTokenInfo | null>;
106
+ /**
107
+ * Record that a token was rotated away, keeping the row.
108
+ *
109
+ * The row is what lets `/auth/refresh` distinguish "you already used this,
110
+ * here is a fresh one" from "no idea what this is". Deleting it — which is
111
+ * what this used to do — collapsed both into a 401 and signed the user out
112
+ * for the crime of losing a response.
113
+ */
114
+ markRotated(tokenHash: string): Promise<void>;
115
+ /** Final kill of one sign-in: logout, or revoking a device remotely. */
116
+ revokeSession(sessionId: string): Promise<void>;
117
+ /**
118
+ * Housekeeping: rotation would otherwise leave a row per refresh forever.
119
+ * Superseded rows are only needed for as long as a straggler might still
120
+ * present them, and expired ones are dead weight everywhere.
121
+ */
122
+ prune(uid: string, sessionId: string, supersededBefore: Date): Promise<void>;
123
+ getTokensValidAfter(uid: string): Promise<Date | null>;
124
+ setTokensValidAfter(uid: string, at: Date): Promise<void>;
96
125
  deleteByHash(tokenHash: string): Promise<void>;
97
126
  deleteAllForUser(uid: string): Promise<void>;
98
127
  listForUser(uid: string): Promise<RefreshTokenInfo[]>;
@@ -153,7 +182,12 @@ export declare class PostgresTokenRepository implements TokenRepository {
153
182
  private passwordResetTokenService;
154
183
  private magicLinkTokenService;
155
184
  constructor(db: NodePgDatabase, tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>);
156
- createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void>;
185
+ createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<void>;
186
+ markRefreshTokenRotated(tokenHash: string): Promise<void>;
187
+ revokeRefreshTokenSession(sessionId: string): Promise<void>;
188
+ pruneRefreshTokens(uid: string, sessionId: string, supersededBefore: Date): Promise<void>;
189
+ getTokensValidAfter(uid: string): Promise<Date | null>;
190
+ setTokensValidAfter(uid: string, at: Date): Promise<void>;
157
191
  findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null>;
158
192
  deleteRefreshToken(tokenHash: string): Promise<void>;
159
193
  deleteAllRefreshTokensForUser(uid: string): Promise<void>;
@@ -205,7 +239,12 @@ export declare class PostgresAuthRepository implements AuthRepository {
205
239
  createRole(_data: CreateRoleData): Promise<RoleData>;
206
240
  updateRole(id: string, data: Partial<Omit<RoleData, "id">>): Promise<RoleData | null>;
207
241
  deleteRole(_id: string): Promise<void>;
208
- createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void>;
242
+ createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<void>;
243
+ markRefreshTokenRotated(tokenHash: string): Promise<void>;
244
+ revokeRefreshTokenSession(sessionId: string): Promise<void>;
245
+ pruneRefreshTokens(uid: string, sessionId: string, supersededBefore: Date): Promise<void>;
246
+ getTokensValidAfter(uid: string): Promise<Date | null>;
247
+ setTokensValidAfter(uid: string, at: Date): Promise<void>;
209
248
  findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null>;
210
249
  deleteRefreshToken(tokenHash: string): Promise<void>;
211
250
  deleteAllRefreshTokensForUser(uid: string): Promise<void>;
package/dist/index.es.js CHANGED
@@ -5,7 +5,7 @@ import { Client, Pool } from "pg";
5
5
  import { drizzle } from "drizzle-orm/node-postgres";
6
6
  import { ApiError, createEmailService, extractUserFromToken, loadCollectionsFromDirectory, logger, safeCompare } from "@rebasepro/server";
7
7
  import { and, asc, count, desc, eq, getTableColumns, getTableName, gt, ilike, inArray, isTable, lt, or, relations, sql } from "drizzle-orm";
8
- import { PgArray, PgChar, PgTable, PgText, PgVarchar, bigint, boolean, char, cidr, customType, date, doublePrecision, geometry, getTableConfig, inet, integer, interval, json, jsonb, line, macaddr, macaddr8, numeric, pgSchema, pgTable, point, primaryKey, real, smallint, text, time, timestamp, unique, uuid, varchar, vector } from "drizzle-orm/pg-core";
8
+ import { PgArray, PgChar, PgTable, PgText, PgVarchar, bigint, boolean, char, cidr, customType, date, doublePrecision, geometry, getTableConfig, index, inet, integer, interval, json, jsonb, line, macaddr, macaddr8, numeric, pgSchema, pgTable, point, primaryKey, real, smallint, text, time, timestamp, unique, uuid, varchar, vector } from "drizzle-orm/pg-core";
9
9
  import fs, { promises } from "fs";
10
10
  import path from "path";
11
11
  import chokidar from "chokidar";
@@ -9349,21 +9349,59 @@ function createAuthSchema(usersSchemaName = "rebase") {
9349
9349
  isAnonymous: boolean("is_anonymous").default(false).notNull(),
9350
9350
  roles: text("roles").array().default([]).notNull(),
9351
9351
  metadata: jsonb("metadata").$type().default({}).notNull(),
9352
+ /**
9353
+ * Sessions that began before this instant are dead, whatever tokens
9354
+ * they still hold. Password resets and admin revocations stamp it.
9355
+ *
9356
+ * Deleting the user's refresh-token rows (which we also do) is not
9357
+ * sufficient on its own: a request already in flight can insert a
9358
+ * freshly rotated row microseconds after the delete and survive it.
9359
+ * This timestamp cannot be outrun that way — it is checked against
9360
+ * `refresh_tokens.session_started_at`, which rotation carries forward.
9361
+ */
9362
+ tokensValidAfter: timestamp("tokens_valid_after"),
9352
9363
  createdAt: timestamp("created_at").defaultNow().notNull(),
9353
9364
  updatedAt: timestamp("updated_at").defaultNow().notNull()
9354
9365
  });
9355
9366
  /**
9356
- * Refresh tokens for long-lived sessions
9367
+ * Refresh tokens for long-lived sessions.
9368
+ *
9369
+ * A row is one token, not one device. Every token minted from the same
9370
+ * sign-in shares a `sessionId`, and rotation ADDS a row rather than
9371
+ * replacing one: the superseded token stays on file, flagged `revoked`
9372
+ * with a `rotatedAt` stamp. That record is what lets the refresh endpoint
9373
+ * tell a client replaying a token it never got an answer for (a response
9374
+ * lost to a redeploy, a second tab racing on boot) apart from a stranger
9375
+ * presenting a token that was never issued. Deleting the old row on sight
9376
+ * — the previous behaviour — made those two cases indistinguishable, and
9377
+ * the legitimate one is overwhelmingly the common one.
9378
+ *
9379
+ * There is deliberately NO unique constraint on (uid, user_agent,
9380
+ * ip_address). Keying a session on the IP meant one row per "device",
9381
+ * so a second browser profile behind the same NAT silently evicted the
9382
+ * first, and a phone changing networks orphaned a row on every hop.
9383
+ * User agent and IP are descriptive metadata for the sessions list;
9384
+ * `sessionId` is the identity.
9357
9385
  */
9358
9386
  const refreshTokens = tableCreator("refresh_tokens", {
9359
9387
  id: uuid("id").defaultRandom().primaryKey(),
9360
9388
  uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
9389
+ sessionId: uuid("session_id").defaultRandom().notNull(),
9361
9390
  tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
9362
9391
  expiresAt: timestamp("expires_at").notNull(),
9392
+ revoked: boolean("revoked").default(false).notNull(),
9393
+ rotatedAt: timestamp("rotated_at"),
9394
+ /**
9395
+ * When the sign-in this token descends from happened — carried across
9396
+ * every rotation, unlike `createdAt`. `users.tokensValidAfter` is
9397
+ * compared against this, so a revocation cannot be outrun by a token
9398
+ * that rotates immediately after it.
9399
+ */
9400
+ sessionStartedAt: timestamp("session_started_at").defaultNow().notNull(),
9363
9401
  userAgent: varchar("user_agent", { length: 500 }),
9364
9402
  ipAddress: varchar("ip_address", { length: 45 }),
9365
9403
  createdAt: timestamp("created_at").defaultNow().notNull()
9366
- }, (table) => ({ uniqueDeviceSession: unique("unique_device_session").on(table.uid, table.userAgent, table.ipAddress) }));
9404
+ }, (table) => ({ sessionIdx: index("idx_refresh_tokens_session").on(table.sessionId) }));
9367
9405
  /**
9368
9406
  * Password reset tokens for forgot password flow
9369
9407
  */
@@ -21153,6 +21191,7 @@ async function ensureAuthTablesExist(db, collection) {
21153
21191
  email_verification_sent_at TIMESTAMP WITH TIME ZONE,
21154
21192
  is_anonymous BOOLEAN DEFAULT FALSE NOT NULL,
21155
21193
  metadata JSONB DEFAULT '{}' NOT NULL,
21194
+ tokens_valid_after TIMESTAMP WITH TIME ZONE,
21156
21195
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
21157
21196
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
21158
21197
  )
@@ -21233,12 +21272,15 @@ async function ensureAuthTablesExist(db, collection) {
21233
21272
  CREATE TABLE IF NOT EXISTS ${sql.raw(refreshTokensTableName)} (
21234
21273
  id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
21235
21274
  uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
21275
+ session_id TEXT NOT NULL DEFAULT gen_random_uuid()::text,
21236
21276
  token_hash TEXT NOT NULL UNIQUE,
21237
21277
  expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
21278
+ revoked BOOLEAN DEFAULT FALSE NOT NULL,
21279
+ rotated_at TIMESTAMP WITH TIME ZONE,
21280
+ session_started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
21238
21281
  user_agent TEXT,
21239
21282
  ip_address TEXT,
21240
- created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
21241
- CONSTRAINT unique_device_session UNIQUE (uid, user_agent, ip_address)
21283
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
21242
21284
  )
21243
21285
  `);
21244
21286
  await db.execute(sql`
@@ -21328,12 +21370,54 @@ async function ensureAuthTablesExist(db, collection) {
21328
21370
  "email_verification_sent_at TIMESTAMP WITH TIME ZONE",
21329
21371
  "is_anonymous BOOLEAN DEFAULT FALSE NOT NULL",
21330
21372
  "metadata JSONB DEFAULT '{}' NOT NULL",
21373
+ "tokens_valid_after TIMESTAMP WITH TIME ZONE",
21331
21374
  "created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL",
21332
21375
  "updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL"
21333
21376
  ]) await db.execute(sql`
21334
21377
  ALTER TABLE ${sql.raw(usersTableName)}
21335
21378
  ADD COLUMN IF NOT EXISTS ${sql.raw(columnDef)}
21336
21379
  `);
21380
+ try {
21381
+ const found = (await db.execute(sql`
21382
+ SELECT table_schema, table_name
21383
+ FROM information_schema.tables
21384
+ WHERE table_name = 'refresh_tokens'
21385
+ `)).rows;
21386
+ logger.info(`🔍 refresh_tokens reconcile: found ${found.length} table(s): ${found.map((r) => `"${r.table_schema}"."${r.table_name}"`).join(", ") || "(none)"}`);
21387
+ for (const { table_schema } of found) {
21388
+ const qualified = `"${table_schema}"."refresh_tokens"`;
21389
+ try {
21390
+ await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS session_id TEXT`);
21391
+ await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS revoked BOOLEAN DEFAULT FALSE NOT NULL`);
21392
+ await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS rotated_at TIMESTAMP WITH TIME ZONE`);
21393
+ await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS session_started_at TIMESTAMP WITH TIME ZONE`);
21394
+ await db.execute(sql`
21395
+ UPDATE ${sql.raw(qualified)}
21396
+ SET session_id = gen_random_uuid()::text
21397
+ WHERE session_id IS NULL
21398
+ `);
21399
+ await db.execute(sql`
21400
+ UPDATE ${sql.raw(qualified)}
21401
+ SET session_started_at = COALESCE(created_at, NOW())
21402
+ WHERE session_started_at IS NULL
21403
+ `);
21404
+ await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ALTER COLUMN session_id SET DEFAULT gen_random_uuid()::text`);
21405
+ await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ALTER COLUMN session_started_at SET DEFAULT NOW()`);
21406
+ await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ALTER COLUMN session_id SET NOT NULL`);
21407
+ await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ALTER COLUMN session_started_at SET NOT NULL`);
21408
+ await db.execute(sql`
21409
+ CREATE INDEX IF NOT EXISTS idx_refresh_tokens_session
21410
+ ON ${sql.raw(qualified)}(session_id)
21411
+ `);
21412
+ await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} DROP CONSTRAINT IF EXISTS unique_device_session`);
21413
+ logger.info(`✅ refresh_tokens reconciled for session-scoped rotation: ${qualified}`);
21414
+ } catch (perTableError) {
21415
+ logger.warn(`⚠️ refresh_tokens reconcile failed for ${qualified}: ${perTableError instanceof Error ? perTableError.message : String(perTableError)}`);
21416
+ }
21417
+ }
21418
+ } catch (migrationError) {
21419
+ logger.warn(`⚠️ refresh_tokens session migration skipped: ${migrationError instanceof Error ? migrationError.message : String(migrationError)}`);
21420
+ }
21337
21421
  try {
21338
21422
  if ((await db.execute(sql`
21339
21423
  SELECT EXISTS (
@@ -21824,34 +21908,31 @@ var UserService = class {
21824
21908
  var RefreshTokenService = class {
21825
21909
  db;
21826
21910
  refreshTokensTable;
21911
+ usersTable;
21827
21912
  constructor(db, tableOrTables) {
21828
21913
  this.db = db;
21829
- if (tableOrTables && (tableOrTables.refreshTokens || tableOrTables.users)) this.refreshTokensTable = tableOrTables.refreshTokens || refreshTokens;
21830
- else this.refreshTokensTable = tableOrTables || refreshTokens;
21914
+ if (tableOrTables && (tableOrTables.refreshTokens || tableOrTables.users)) {
21915
+ this.refreshTokensTable = tableOrTables.refreshTokens || refreshTokens;
21916
+ this.usersTable = tableOrTables.users || users;
21917
+ } else {
21918
+ this.refreshTokensTable = tableOrTables || refreshTokens;
21919
+ this.usersTable = users;
21920
+ }
21831
21921
  }
21832
- async createToken(uid, tokenHash, expiresAt, userAgent, ipAddress) {
21833
- const safeUserAgent = userAgent || "";
21834
- const safeIpAddress = ipAddress || "";
21835
- await this.db.insert(this.refreshTokensTable).values({
21836
- uid,
21837
- tokenHash,
21838
- expiresAt,
21839
- userAgent: safeUserAgent,
21840
- ipAddress: safeIpAddress
21841
- }).onConflictDoUpdate({
21842
- target: [
21843
- this.refreshTokensTable.uid,
21844
- this.refreshTokensTable.userAgent,
21845
- this.refreshTokensTable.ipAddress
21846
- ],
21847
- set: {
21848
- tokenHash,
21849
- expiresAt
21850
- }
21851
- });
21922
+ /**
21923
+ * Whether the table actually carries a column, so a host application that
21924
+ * supplied its own `refresh_tokens` table — one that predates session
21925
+ * grouping — degrades instead of throwing on every sign-in.
21926
+ */
21927
+ has(column) {
21928
+ return Boolean(this.refreshTokensTable[column]);
21852
21929
  }
21853
- async findByHash(tokenHash) {
21854
- const [token] = await this.db.select({
21930
+ col(column) {
21931
+ return this.refreshTokensTable[column];
21932
+ }
21933
+ /** The columns to read back, narrowed to the ones this table has. */
21934
+ selection() {
21935
+ const selection = {
21855
21936
  id: this.refreshTokensTable.id,
21856
21937
  uid: this.refreshTokensTable.uid,
21857
21938
  tokenHash: this.refreshTokensTable.tokenHash,
@@ -21859,9 +21940,94 @@ var RefreshTokenService = class {
21859
21940
  createdAt: this.refreshTokensTable.createdAt,
21860
21941
  userAgent: this.refreshTokensTable.userAgent,
21861
21942
  ipAddress: this.refreshTokensTable.ipAddress
21862
- }).from(this.refreshTokensTable).where(eq(this.refreshTokensTable.tokenHash, tokenHash));
21943
+ };
21944
+ for (const optional of [
21945
+ "sessionId",
21946
+ "rotatedAt",
21947
+ "revoked",
21948
+ "sessionStartedAt"
21949
+ ]) if (this.has(optional)) selection[optional] = this.col(optional);
21950
+ return selection;
21951
+ }
21952
+ async createToken(uid, tokenHash, expiresAt, userAgent, ipAddress, session) {
21953
+ const values = {
21954
+ uid,
21955
+ tokenHash,
21956
+ expiresAt,
21957
+ userAgent: userAgent || "",
21958
+ ipAddress: ipAddress || ""
21959
+ };
21960
+ if (session && this.has("sessionId")) values.sessionId = session.id;
21961
+ if (session && this.has("sessionStartedAt")) values.sessionStartedAt = session.startedAt;
21962
+ await this.db.insert(this.refreshTokensTable).values(values);
21963
+ }
21964
+ async findByHash(tokenHash) {
21965
+ const [token] = await this.db.select(this.selection()).from(this.refreshTokensTable).where(eq(this.refreshTokensTable.tokenHash, tokenHash));
21863
21966
  return token || null;
21864
21967
  }
21968
+ /**
21969
+ * Record that a token was rotated away, keeping the row.
21970
+ *
21971
+ * The row is what lets `/auth/refresh` distinguish "you already used this,
21972
+ * here is a fresh one" from "no idea what this is". Deleting it — which is
21973
+ * what this used to do — collapsed both into a 401 and signed the user out
21974
+ * for the crime of losing a response.
21975
+ */
21976
+ async markRotated(tokenHash) {
21977
+ if (!this.has("rotatedAt")) {
21978
+ await this.deleteByHash(tokenHash);
21979
+ return;
21980
+ }
21981
+ await this.db.update(this.refreshTokensTable).set({ rotatedAt: /* @__PURE__ */ new Date() }).where(eq(this.refreshTokensTable.tokenHash, tokenHash));
21982
+ }
21983
+ /** Final kill of one sign-in: logout, or revoking a device remotely. */
21984
+ async revokeSession(sessionId) {
21985
+ if (!this.has("sessionId")) return;
21986
+ if (this.has("revoked")) {
21987
+ await this.db.update(this.refreshTokensTable).set({
21988
+ revoked: true,
21989
+ ...this.has("rotatedAt") ? { rotatedAt: /* @__PURE__ */ new Date() } : {}
21990
+ }).where(eq(this.col("sessionId"), sessionId));
21991
+ return;
21992
+ }
21993
+ await this.db.delete(this.refreshTokensTable).where(eq(this.col("sessionId"), sessionId));
21994
+ }
21995
+ /**
21996
+ * Housekeeping: rotation would otherwise leave a row per refresh forever.
21997
+ * Superseded rows are only needed for as long as a straggler might still
21998
+ * present them, and expired ones are dead weight everywhere.
21999
+ */
22000
+ async prune(uid, sessionId, supersededBefore) {
22001
+ const uidCol = this.refreshTokensTable.uid;
22002
+ const expiresCol = this.refreshTokensTable.expiresAt;
22003
+ if (!this.has("rotatedAt") || !this.has("sessionId")) {
22004
+ await this.db.delete(this.refreshTokensTable).where(sql`${uidCol} = ${uid} AND ${expiresCol} < NOW()`);
22005
+ return;
22006
+ }
22007
+ const rotatedCol = this.col("rotatedAt");
22008
+ const sessionCol = this.col("sessionId");
22009
+ await this.db.delete(this.refreshTokensTable).where(sql`
22010
+ ${uidCol} = ${uid}
22011
+ AND (
22012
+ ${expiresCol} < NOW()
22013
+ OR (
22014
+ ${sessionCol} = ${sessionId}
22015
+ AND ${rotatedCol} IS NOT NULL
22016
+ AND ${rotatedCol} < ${supersededBefore}
22017
+ )
22018
+ )
22019
+ `);
22020
+ }
22021
+ async getTokensValidAfter(uid) {
22022
+ if (!this.usersTable || !this.usersTable.tokensValidAfter) return null;
22023
+ const [row] = await this.db.select({ tokensValidAfter: this.usersTable.tokensValidAfter }).from(this.usersTable).where(eq(this.usersTable.id, uid));
22024
+ const value = row?.tokensValidAfter;
22025
+ return value ? new Date(value) : null;
22026
+ }
22027
+ async setTokensValidAfter(uid, at) {
22028
+ if (!this.usersTable || !this.usersTable.tokensValidAfter) return;
22029
+ await this.db.update(this.usersTable).set({ tokensValidAfter: at }).where(eq(this.usersTable.id, uid));
22030
+ }
21865
22031
  async deleteByHash(tokenHash) {
21866
22032
  await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.tokenHash, tokenHash));
21867
22033
  }
@@ -21869,15 +22035,7 @@ var RefreshTokenService = class {
21869
22035
  await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.uid, uid));
21870
22036
  }
21871
22037
  async listForUser(uid) {
21872
- return await this.db.select({
21873
- id: this.refreshTokensTable.id,
21874
- uid: this.refreshTokensTable.uid,
21875
- tokenHash: this.refreshTokensTable.tokenHash,
21876
- expiresAt: this.refreshTokensTable.expiresAt,
21877
- createdAt: this.refreshTokensTable.createdAt,
21878
- userAgent: this.refreshTokensTable.userAgent,
21879
- ipAddress: this.refreshTokensTable.ipAddress
21880
- }).from(this.refreshTokensTable).where(eq(this.refreshTokensTable.uid, uid)).orderBy(this.refreshTokensTable.createdAt);
22038
+ return await this.db.select(this.selection()).from(this.refreshTokensTable).where(eq(this.refreshTokensTable.uid, uid)).orderBy(this.refreshTokensTable.createdAt);
21881
22039
  }
21882
22040
  async deleteById(id, uid) {
21883
22041
  await this.db.delete(this.refreshTokensTable).where(sql`${this.refreshTokensTable.id} = ${id} AND ${this.refreshTokensTable.uid} = ${uid}`);
@@ -22022,8 +22180,23 @@ var PostgresTokenRepository = class {
22022
22180
  this.passwordResetTokenService = new PasswordResetTokenService(db, tableOrTables);
22023
22181
  this.magicLinkTokenService = new MagicLinkTokenService(db, tableOrTables);
22024
22182
  }
22025
- async createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress) {
22026
- await this.refreshTokenService.createToken(uid, tokenHash, expiresAt, userAgent, ipAddress);
22183
+ async createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress, session) {
22184
+ await this.refreshTokenService.createToken(uid, tokenHash, expiresAt, userAgent, ipAddress, session);
22185
+ }
22186
+ async markRefreshTokenRotated(tokenHash) {
22187
+ await this.refreshTokenService.markRotated(tokenHash);
22188
+ }
22189
+ async revokeRefreshTokenSession(sessionId) {
22190
+ await this.refreshTokenService.revokeSession(sessionId);
22191
+ }
22192
+ async pruneRefreshTokens(uid, sessionId, supersededBefore) {
22193
+ await this.refreshTokenService.prune(uid, sessionId, supersededBefore);
22194
+ }
22195
+ async getTokensValidAfter(uid) {
22196
+ return this.refreshTokenService.getTokensValidAfter(uid);
22197
+ }
22198
+ async setTokensValidAfter(uid, at) {
22199
+ await this.refreshTokenService.setTokensValidAfter(uid, at);
22027
22200
  }
22028
22201
  async findRefreshTokenByHash(tokenHash) {
22029
22202
  return this.refreshTokenService.findByHash(tokenHash);
@@ -22189,8 +22362,23 @@ var PostgresAuthRepository = class {
22189
22362
  };
22190
22363
  }
22191
22364
  async deleteRole(_id) {}
22192
- async createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress) {
22193
- await this.tokenRepository.createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress);
22365
+ async createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress, session) {
22366
+ await this.tokenRepository.createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress, session);
22367
+ }
22368
+ async markRefreshTokenRotated(tokenHash) {
22369
+ await this.tokenRepository.markRefreshTokenRotated(tokenHash);
22370
+ }
22371
+ async revokeRefreshTokenSession(sessionId) {
22372
+ await this.tokenRepository.revokeRefreshTokenSession(sessionId);
22373
+ }
22374
+ async pruneRefreshTokens(uid, sessionId, supersededBefore) {
22375
+ await this.tokenRepository.pruneRefreshTokens(uid, sessionId, supersededBefore);
22376
+ }
22377
+ async getTokensValidAfter(uid) {
22378
+ return this.tokenRepository.getTokensValidAfter(uid);
22379
+ }
22380
+ async setTokensValidAfter(uid, at) {
22381
+ await this.tokenRepository.setTokensValidAfter(uid, at);
22194
22382
  }
22195
22383
  async findRefreshTokenByHash(tokenHash) {
22196
22384
  return this.tokenRepository.findRefreshTokenByHash(tokenHash);