@rebasepro/server-postgres 0.10.1-canary.31c773c → 0.10.1-canary.811b3da
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/dist/auth/services.d.ts +43 -4
- package/dist/index.es.js +212 -71
- package/dist/index.es.js.map +1 -1
- package/dist/schema/auth-schema.d.ts +170 -0
- package/package.json +6 -6
- package/src/auth/ensure-tables.ts +70 -56
- package/src/auth/services.ts +186 -48
- package/src/schema/auth-schema.ts +41 -3
package/dist/auth/services.d.ts
CHANGED
|
@@ -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
|
-
|
|
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) => ({
|
|
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,6 +21370,7 @@ 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`
|
|
@@ -21344,42 +21387,36 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
21344
21387
|
for (const { table_schema } of found) {
|
|
21345
21388
|
const qualified = `"${table_schema}"."refresh_tokens"`;
|
|
21346
21389
|
try {
|
|
21347
|
-
await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS
|
|
21348
|
-
await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS
|
|
21349
|
-
await db.execute(sql`
|
|
21350
|
-
await db.execute(sql`
|
|
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
|
+
`);
|
|
21351
21399
|
await db.execute(sql`
|
|
21352
|
-
|
|
21353
|
-
|
|
21354
|
-
WHERE
|
|
21355
|
-
AND a.uid = b.uid
|
|
21356
|
-
AND a.user_agent = b.user_agent
|
|
21357
|
-
AND a.ip_address = b.ip_address
|
|
21400
|
+
UPDATE ${sql.raw(qualified)}
|
|
21401
|
+
SET session_started_at = COALESCE(created_at, NOW())
|
|
21402
|
+
WHERE session_started_at IS NULL
|
|
21358
21403
|
`);
|
|
21359
|
-
|
|
21360
|
-
|
|
21361
|
-
|
|
21362
|
-
|
|
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)
|
|
21363
21411
|
`);
|
|
21364
|
-
|
|
21365
|
-
|
|
21366
|
-
const cols = (u.def.match(/\(([^)]*)\)/)?.[1] || "").split(",").map((c) => c.trim().replace(/"/g, ""));
|
|
21367
|
-
return cols.length === 3 && cols.includes("uid") && cols.includes("user_agent") && cols.includes("ip_address");
|
|
21368
|
-
})) logger.info(`✓ correct device-session unique already present on ${qualified}`);
|
|
21369
|
-
else {
|
|
21370
|
-
await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} DROP CONSTRAINT IF EXISTS unique_device_session`);
|
|
21371
|
-
await db.execute(sql`
|
|
21372
|
-
ALTER TABLE ${sql.raw(qualified)}
|
|
21373
|
-
ADD CONSTRAINT unique_device_session UNIQUE (uid, user_agent, ip_address)
|
|
21374
|
-
`);
|
|
21375
|
-
logger.info(`✅ Added correct unique_device_session constraint to ${qualified}`);
|
|
21376
|
-
}
|
|
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}`);
|
|
21377
21414
|
} catch (perTableError) {
|
|
21378
21415
|
logger.warn(`⚠️ refresh_tokens reconcile failed for ${qualified}: ${perTableError instanceof Error ? perTableError.message : String(perTableError)}`);
|
|
21379
21416
|
}
|
|
21380
21417
|
}
|
|
21381
21418
|
} catch (migrationError) {
|
|
21382
|
-
logger.warn(`⚠️ refresh_tokens
|
|
21419
|
+
logger.warn(`⚠️ refresh_tokens session migration skipped: ${migrationError instanceof Error ? migrationError.message : String(migrationError)}`);
|
|
21383
21420
|
}
|
|
21384
21421
|
try {
|
|
21385
21422
|
if ((await db.execute(sql`
|
|
@@ -21871,34 +21908,31 @@ var UserService = class {
|
|
|
21871
21908
|
var RefreshTokenService = class {
|
|
21872
21909
|
db;
|
|
21873
21910
|
refreshTokensTable;
|
|
21911
|
+
usersTable;
|
|
21874
21912
|
constructor(db, tableOrTables) {
|
|
21875
21913
|
this.db = db;
|
|
21876
|
-
if (tableOrTables && (tableOrTables.refreshTokens || tableOrTables.users))
|
|
21877
|
-
|
|
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
|
+
}
|
|
21878
21921
|
}
|
|
21879
|
-
|
|
21880
|
-
|
|
21881
|
-
|
|
21882
|
-
|
|
21883
|
-
|
|
21884
|
-
|
|
21885
|
-
|
|
21886
|
-
userAgent: safeUserAgent,
|
|
21887
|
-
ipAddress: safeIpAddress
|
|
21888
|
-
}).onConflictDoUpdate({
|
|
21889
|
-
target: [
|
|
21890
|
-
this.refreshTokensTable.uid,
|
|
21891
|
-
this.refreshTokensTable.userAgent,
|
|
21892
|
-
this.refreshTokensTable.ipAddress
|
|
21893
|
-
],
|
|
21894
|
-
set: {
|
|
21895
|
-
tokenHash,
|
|
21896
|
-
expiresAt
|
|
21897
|
-
}
|
|
21898
|
-
});
|
|
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]);
|
|
21899
21929
|
}
|
|
21900
|
-
|
|
21901
|
-
|
|
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 = {
|
|
21902
21936
|
id: this.refreshTokensTable.id,
|
|
21903
21937
|
uid: this.refreshTokensTable.uid,
|
|
21904
21938
|
tokenHash: this.refreshTokensTable.tokenHash,
|
|
@@ -21906,9 +21940,94 @@ var RefreshTokenService = class {
|
|
|
21906
21940
|
createdAt: this.refreshTokensTable.createdAt,
|
|
21907
21941
|
userAgent: this.refreshTokensTable.userAgent,
|
|
21908
21942
|
ipAddress: this.refreshTokensTable.ipAddress
|
|
21909
|
-
}
|
|
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));
|
|
21910
21966
|
return token || null;
|
|
21911
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
|
+
}
|
|
21912
22031
|
async deleteByHash(tokenHash) {
|
|
21913
22032
|
await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.tokenHash, tokenHash));
|
|
21914
22033
|
}
|
|
@@ -21916,15 +22035,7 @@ var RefreshTokenService = class {
|
|
|
21916
22035
|
await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.uid, uid));
|
|
21917
22036
|
}
|
|
21918
22037
|
async listForUser(uid) {
|
|
21919
|
-
return await this.db.select(
|
|
21920
|
-
id: this.refreshTokensTable.id,
|
|
21921
|
-
uid: this.refreshTokensTable.uid,
|
|
21922
|
-
tokenHash: this.refreshTokensTable.tokenHash,
|
|
21923
|
-
expiresAt: this.refreshTokensTable.expiresAt,
|
|
21924
|
-
createdAt: this.refreshTokensTable.createdAt,
|
|
21925
|
-
userAgent: this.refreshTokensTable.userAgent,
|
|
21926
|
-
ipAddress: this.refreshTokensTable.ipAddress
|
|
21927
|
-
}).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);
|
|
21928
22039
|
}
|
|
21929
22040
|
async deleteById(id, uid) {
|
|
21930
22041
|
await this.db.delete(this.refreshTokensTable).where(sql`${this.refreshTokensTable.id} = ${id} AND ${this.refreshTokensTable.uid} = ${uid}`);
|
|
@@ -22069,8 +22180,23 @@ var PostgresTokenRepository = class {
|
|
|
22069
22180
|
this.passwordResetTokenService = new PasswordResetTokenService(db, tableOrTables);
|
|
22070
22181
|
this.magicLinkTokenService = new MagicLinkTokenService(db, tableOrTables);
|
|
22071
22182
|
}
|
|
22072
|
-
async createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress) {
|
|
22073
|
-
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);
|
|
22074
22200
|
}
|
|
22075
22201
|
async findRefreshTokenByHash(tokenHash) {
|
|
22076
22202
|
return this.refreshTokenService.findByHash(tokenHash);
|
|
@@ -22236,8 +22362,23 @@ var PostgresAuthRepository = class {
|
|
|
22236
22362
|
};
|
|
22237
22363
|
}
|
|
22238
22364
|
async deleteRole(_id) {}
|
|
22239
|
-
async createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress) {
|
|
22240
|
-
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);
|
|
22241
22382
|
}
|
|
22242
22383
|
async findRefreshTokenByHash(tokenHash) {
|
|
22243
22384
|
return this.tokenRepository.findRefreshTokenByHash(tokenHash);
|