@rebasepro/server-postgres 0.10.1-canary.31c773c → 0.10.1-canary.6f89f77
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/backup/backup-logic.d.ts +23 -0
- package/dist/backup/backup-service.d.ts +44 -2
- package/dist/backup/pg-tools.d.ts +41 -1
- package/dist/cli-helpers.d.ts +33 -1
- package/dist/index.es.js +439 -85
- package/dist/index.es.js.map +1 -1
- package/dist/schema/auth-schema.d.ts +170 -0
- package/dist/schema/destructive-sql.d.ts +49 -0
- package/package.json +6 -6
- package/src/auth/ensure-tables.ts +70 -56
- package/src/auth/services.ts +186 -48
- package/src/backup/backup-cli.ts +60 -1
- package/src/backup/backup-cron.ts +24 -1
- package/src/backup/backup-logic.ts +62 -0
- package/src/backup/backup-service.ts +132 -13
- package/src/backup/pg-tools.ts +70 -2
- package/src/cli-helpers.ts +82 -27
- package/src/cli.ts +152 -6
- package/src/schema/auth-schema.ts +41 -3
- package/src/schema/destructive-sql.ts +94 -0
- package/src/services/realtimeService.ts +13 -3
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";
|
|
@@ -518,6 +518,25 @@ function getDataSourceCapabilities(engine) {
|
|
|
518
518
|
if (!engine) return POSTGRES_CAPABILITIES;
|
|
519
519
|
return CAPABILITIES_REGISTRY[engine] ?? DEFAULT_CAPABILITIES;
|
|
520
520
|
}
|
|
521
|
+
/**
|
|
522
|
+
* Resolve a client-supplied list `limit` into a safe, always-defined value.
|
|
523
|
+
*
|
|
524
|
+
* - A provided limit is coerced to an integer and clamped to `[1, maxLimit]`,
|
|
525
|
+
* so `0`, negatives, and absurd values can never bypass the cap.
|
|
526
|
+
* - An absent / blank / non-numeric limit falls back to the mode default:
|
|
527
|
+
* `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.
|
|
528
|
+
*
|
|
529
|
+
* The return is never `undefined` — no ingress that routes its client limit
|
|
530
|
+
* through this can produce an unbounded read.
|
|
531
|
+
*/
|
|
532
|
+
function resolveClientListLimit(rawLimit, opts = {}) {
|
|
533
|
+
const maxLimit = opts.maxLimit ?? 1e3;
|
|
534
|
+
if (rawLimit != null && String(rawLimit).trim() !== "") {
|
|
535
|
+
const parsed = typeof rawLimit === "number" ? rawLimit : parseInt(String(rawLimit), 10);
|
|
536
|
+
if (Number.isFinite(parsed)) return Math.min(Math.max(1, Math.floor(parsed)), maxLimit);
|
|
537
|
+
}
|
|
538
|
+
return opts.vectorSearch ? opts.vectorDefaultLimit ?? 10 : opts.defaultLimit ?? 50;
|
|
539
|
+
}
|
|
521
540
|
var snakeCaseRegex = /[A-Z]{2,}(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|[0-9]+(?:[a-z](?![a-z]))?|[A-Z]/g;
|
|
522
541
|
var toSnakeCase = (str) => {
|
|
523
542
|
if (!str || typeof str !== "string") return "";
|
|
@@ -9349,21 +9368,59 @@ function createAuthSchema(usersSchemaName = "rebase") {
|
|
|
9349
9368
|
isAnonymous: boolean("is_anonymous").default(false).notNull(),
|
|
9350
9369
|
roles: text("roles").array().default([]).notNull(),
|
|
9351
9370
|
metadata: jsonb("metadata").$type().default({}).notNull(),
|
|
9371
|
+
/**
|
|
9372
|
+
* Sessions that began before this instant are dead, whatever tokens
|
|
9373
|
+
* they still hold. Password resets and admin revocations stamp it.
|
|
9374
|
+
*
|
|
9375
|
+
* Deleting the user's refresh-token rows (which we also do) is not
|
|
9376
|
+
* sufficient on its own: a request already in flight can insert a
|
|
9377
|
+
* freshly rotated row microseconds after the delete and survive it.
|
|
9378
|
+
* This timestamp cannot be outrun that way — it is checked against
|
|
9379
|
+
* `refresh_tokens.session_started_at`, which rotation carries forward.
|
|
9380
|
+
*/
|
|
9381
|
+
tokensValidAfter: timestamp("tokens_valid_after"),
|
|
9352
9382
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
9353
9383
|
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
9354
9384
|
});
|
|
9355
9385
|
/**
|
|
9356
|
-
* Refresh tokens for long-lived sessions
|
|
9386
|
+
* Refresh tokens for long-lived sessions.
|
|
9387
|
+
*
|
|
9388
|
+
* A row is one token, not one device. Every token minted from the same
|
|
9389
|
+
* sign-in shares a `sessionId`, and rotation ADDS a row rather than
|
|
9390
|
+
* replacing one: the superseded token stays on file, flagged `revoked`
|
|
9391
|
+
* with a `rotatedAt` stamp. That record is what lets the refresh endpoint
|
|
9392
|
+
* tell a client replaying a token it never got an answer for (a response
|
|
9393
|
+
* lost to a redeploy, a second tab racing on boot) apart from a stranger
|
|
9394
|
+
* presenting a token that was never issued. Deleting the old row on sight
|
|
9395
|
+
* — the previous behaviour — made those two cases indistinguishable, and
|
|
9396
|
+
* the legitimate one is overwhelmingly the common one.
|
|
9397
|
+
*
|
|
9398
|
+
* There is deliberately NO unique constraint on (uid, user_agent,
|
|
9399
|
+
* ip_address). Keying a session on the IP meant one row per "device",
|
|
9400
|
+
* so a second browser profile behind the same NAT silently evicted the
|
|
9401
|
+
* first, and a phone changing networks orphaned a row on every hop.
|
|
9402
|
+
* User agent and IP are descriptive metadata for the sessions list;
|
|
9403
|
+
* `sessionId` is the identity.
|
|
9357
9404
|
*/
|
|
9358
9405
|
const refreshTokens = tableCreator("refresh_tokens", {
|
|
9359
9406
|
id: uuid("id").defaultRandom().primaryKey(),
|
|
9360
9407
|
uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
9408
|
+
sessionId: uuid("session_id").defaultRandom().notNull(),
|
|
9361
9409
|
tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
|
|
9362
9410
|
expiresAt: timestamp("expires_at").notNull(),
|
|
9411
|
+
revoked: boolean("revoked").default(false).notNull(),
|
|
9412
|
+
rotatedAt: timestamp("rotated_at"),
|
|
9413
|
+
/**
|
|
9414
|
+
* When the sign-in this token descends from happened — carried across
|
|
9415
|
+
* every rotation, unlike `createdAt`. `users.tokensValidAfter` is
|
|
9416
|
+
* compared against this, so a revocation cannot be outrun by a token
|
|
9417
|
+
* that rotates immediately after it.
|
|
9418
|
+
*/
|
|
9419
|
+
sessionStartedAt: timestamp("session_started_at").defaultNow().notNull(),
|
|
9363
9420
|
userAgent: varchar("user_agent", { length: 500 }),
|
|
9364
9421
|
ipAddress: varchar("ip_address", { length: 45 }),
|
|
9365
9422
|
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
9366
|
-
}, (table) => ({
|
|
9423
|
+
}, (table) => ({ sessionIdx: index("idx_refresh_tokens_session").on(table.sessionId) }));
|
|
9367
9424
|
/**
|
|
9368
9425
|
* Password reset tokens for forgot password flow
|
|
9369
9426
|
*/
|
|
@@ -10819,6 +10876,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10819
10876
|
this.sendError(clientId, msg, subscriptionId);
|
|
10820
10877
|
return;
|
|
10821
10878
|
}
|
|
10879
|
+
const boundedLimit = resolveClientListLimit(request.limit, { vectorSearch: !!request.vectorSearch });
|
|
10822
10880
|
this._subscriptions.set(subscriptionId, {
|
|
10823
10881
|
clientId,
|
|
10824
10882
|
type: "collection",
|
|
@@ -10827,7 +10885,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10827
10885
|
filter: request.filter,
|
|
10828
10886
|
orderBy: request.orderBy,
|
|
10829
10887
|
order: request.order,
|
|
10830
|
-
limit:
|
|
10888
|
+
limit: boundedLimit,
|
|
10831
10889
|
startAfter: request.startAfter,
|
|
10832
10890
|
databaseId: request.collection?.databaseId,
|
|
10833
10891
|
searchString: request.searchString
|
|
@@ -10838,7 +10896,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10838
10896
|
filter: request.filter,
|
|
10839
10897
|
orderBy: request.orderBy,
|
|
10840
10898
|
order: request.order,
|
|
10841
|
-
limit:
|
|
10899
|
+
limit: boundedLimit,
|
|
10842
10900
|
startAfter: request.startAfter,
|
|
10843
10901
|
searchString: request.searchString
|
|
10844
10902
|
}, authContext);
|
|
@@ -12463,11 +12521,57 @@ function buildPgRestoreArgs(opts) {
|
|
|
12463
12521
|
];
|
|
12464
12522
|
if (opts.clean) args.push("--clean", "--if-exists");
|
|
12465
12523
|
if (opts.noOwner) args.push("--no-owner");
|
|
12466
|
-
if (opts.exitOnError) args.push("--exit-on-error");
|
|
12524
|
+
if (opts.exitOnError !== false) args.push("--exit-on-error");
|
|
12467
12525
|
args.push(opts.inputFile);
|
|
12468
12526
|
return args;
|
|
12469
12527
|
}
|
|
12470
12528
|
/**
|
|
12529
|
+
* Assemble the `pg_restore --list` argument vector. Reading a dump's table
|
|
12530
|
+
* of contents parses the whole archive without touching a database, so it is
|
|
12531
|
+
* a cheap integrity check that the file isn't truncated or corrupt.
|
|
12532
|
+
*/
|
|
12533
|
+
function buildPgRestoreListArgs(inputFile) {
|
|
12534
|
+
return ["--list", inputFile];
|
|
12535
|
+
}
|
|
12536
|
+
/**
|
|
12537
|
+
* Assemble the `pg_dumpall --globals-only` argument vector. Roles (and other
|
|
12538
|
+
* cluster-wide objects) live outside any single database, so a per-database
|
|
12539
|
+
* `pg_dump` omits them. Without the `rebase_user` role the RLS GRANT
|
|
12540
|
+
* statements in the main dump fail on restore and RLS is silently lost — so
|
|
12541
|
+
* every backup captures the globals into a sidecar `.globals.sql`.
|
|
12542
|
+
*
|
|
12543
|
+
* `--no-role-passwords` keeps role secrets out of the artifact (backups may
|
|
12544
|
+
* be shipped off-box); roles are recreated password-less and re-secured by
|
|
12545
|
+
* the operator.
|
|
12546
|
+
*/
|
|
12547
|
+
function buildPgDumpallGlobalsArgs(opts) {
|
|
12548
|
+
return [
|
|
12549
|
+
"--globals-only",
|
|
12550
|
+
"--no-role-passwords",
|
|
12551
|
+
"--no-password",
|
|
12552
|
+
`--file=${opts.outFile}`,
|
|
12553
|
+
`--dbname=${opts.connectionString}`
|
|
12554
|
+
];
|
|
12555
|
+
}
|
|
12556
|
+
/**
|
|
12557
|
+
* Derive the globals sidecar path/key for a given `.dump` file. Keeps the
|
|
12558
|
+
* two artifacts adjacent so listing, uploading and pruning can find one from
|
|
12559
|
+
* the other. A name that doesn't end in `.dump` is returned unchanged with a
|
|
12560
|
+
* `.globals.sql` suffix appended.
|
|
12561
|
+
*/
|
|
12562
|
+
function globalsFileForDump(dumpPath) {
|
|
12563
|
+
return dumpPath.endsWith(".dump") ? dumpPath.slice(0, -5) + ".globals.sql" : dumpPath + ".globals.sql";
|
|
12564
|
+
}
|
|
12565
|
+
/**
|
|
12566
|
+
* Split a `pg_dumpall --globals-only` script into individual statements.
|
|
12567
|
+
* Used when replaying globals on restore so each `CREATE ROLE` / `GRANT`
|
|
12568
|
+
* can run independently and a benign "role already exists" on one doesn't
|
|
12569
|
+
* abort the rest. Drops `--` comment lines and blank statements.
|
|
12570
|
+
*/
|
|
12571
|
+
function splitGlobalsStatements(sql) {
|
|
12572
|
+
return sql.split("\n").filter((line) => !line.trim().startsWith("--")).join("\n").split(";").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
12573
|
+
}
|
|
12574
|
+
/**
|
|
12471
12575
|
* Resolve the Postgres connection string the backup commands should use,
|
|
12472
12576
|
* mirroring the precedence the branch command already relies on.
|
|
12473
12577
|
*/
|
|
@@ -20756,6 +20860,58 @@ function resolveLocalBin(binName) {
|
|
|
20756
20860
|
return null;
|
|
20757
20861
|
}
|
|
20758
20862
|
//#endregion
|
|
20863
|
+
//#region src/backup/backup-logic.ts
|
|
20864
|
+
/**
|
|
20865
|
+
* Pure-ish orchestration for the backup/restore paths, kept free of `execa`
|
|
20866
|
+
* and `pg` value imports so it can be unit-tested under jest (the impure
|
|
20867
|
+
* edges — spawning processes, opening connections — live in
|
|
20868
|
+
* `backup-service.ts`, which is vitest/runtime only).
|
|
20869
|
+
*
|
|
20870
|
+
* The functions here take their side-effecting dependency as an argument
|
|
20871
|
+
* (a statement runner, an object deleter) so tests can inject fakes.
|
|
20872
|
+
*/
|
|
20873
|
+
/**
|
|
20874
|
+
* Replay a `pg_dumpall --globals-only` script one statement at a time,
|
|
20875
|
+
* tolerating per-statement failures. On a same-cluster restore the roles
|
|
20876
|
+
* usually already exist (`CREATE ROLE` → "already exists") and on a managed
|
|
20877
|
+
* provider an `ALTER ROLE <superuser>` may be refused; neither should abort
|
|
20878
|
+
* recreation of the roles that *are* missing. Returns how many statements
|
|
20879
|
+
* applied vs were skipped.
|
|
20880
|
+
*
|
|
20881
|
+
* `runStatement` executes one SQL statement and rejects on error.
|
|
20882
|
+
*/
|
|
20883
|
+
async function applyGlobalsWith(runStatement, globalsSql, log = () => {}) {
|
|
20884
|
+
let applied = 0;
|
|
20885
|
+
let skipped = 0;
|
|
20886
|
+
for (const statement of splitGlobalsStatements(globalsSql)) try {
|
|
20887
|
+
await runStatement(statement);
|
|
20888
|
+
applied++;
|
|
20889
|
+
} catch (err) {
|
|
20890
|
+
skipped++;
|
|
20891
|
+
log(` • Skipped global: ${statement.split("\n")[0].slice(0, 80)} (${err instanceof Error ? err.message : String(err)})`);
|
|
20892
|
+
}
|
|
20893
|
+
return {
|
|
20894
|
+
applied,
|
|
20895
|
+
skipped
|
|
20896
|
+
};
|
|
20897
|
+
}
|
|
20898
|
+
/**
|
|
20899
|
+
* Delete each pruned dump together with its `.globals.sql` roles sidecar, so
|
|
20900
|
+
* pruning never orphans the roles file. The sidecar is best-effort — older
|
|
20901
|
+
* backups predate it, so a missing-sidecar failure is swallowed while a
|
|
20902
|
+
* failure deleting the dump itself propagates.
|
|
20903
|
+
*
|
|
20904
|
+
* `deleteObject` removes one key and rejects if it cannot (e.g. not found).
|
|
20905
|
+
*/
|
|
20906
|
+
async function pruneWith(keys, deleteObject) {
|
|
20907
|
+
for (const key of keys) {
|
|
20908
|
+
await deleteObject(key);
|
|
20909
|
+
try {
|
|
20910
|
+
await deleteObject(globalsFileForDump(key));
|
|
20911
|
+
} catch {}
|
|
20912
|
+
}
|
|
20913
|
+
}
|
|
20914
|
+
//#endregion
|
|
20759
20915
|
//#region src/backup/backup-service.ts
|
|
20760
20916
|
/**
|
|
20761
20917
|
* Backup / restore orchestration: thin, well-typed wrappers around
|
|
@@ -20768,6 +20924,7 @@ function resolveLocalBin(binName) {
|
|
|
20768
20924
|
*/
|
|
20769
20925
|
var backup_service_exports = /* @__PURE__ */ __exportAll({
|
|
20770
20926
|
BackupToolError: () => BackupToolError,
|
|
20927
|
+
applyGlobals: () => applyGlobals,
|
|
20771
20928
|
createDump: () => createDump,
|
|
20772
20929
|
detectToolMajor: () => detectToolMajor,
|
|
20773
20930
|
ensureDatabaseExists: () => ensureDatabaseExists,
|
|
@@ -20777,7 +20934,8 @@ var backup_service_exports = /* @__PURE__ */ __exportAll({
|
|
|
20777
20934
|
pruneBackups: () => pruneBackups,
|
|
20778
20935
|
resolvePgBinary: () => resolvePgBinary,
|
|
20779
20936
|
restoreDump: () => restoreDump,
|
|
20780
|
-
uploadBackup: () => uploadBackup
|
|
20937
|
+
uploadBackup: () => uploadBackup,
|
|
20938
|
+
validateDump: () => validateDump
|
|
20781
20939
|
});
|
|
20782
20940
|
var BackupToolError = class extends Error {
|
|
20783
20941
|
hint;
|
|
@@ -20787,10 +20945,10 @@ var BackupToolError = class extends Error {
|
|
|
20787
20945
|
this.name = "BackupToolError";
|
|
20788
20946
|
}
|
|
20789
20947
|
};
|
|
20790
|
-
/** Locate `pg_dump` / `pg_restore`, honouring an env override. */
|
|
20948
|
+
/** Locate `pg_dump` / `pg_restore` / `pg_dumpall`, honouring an env override. */
|
|
20791
20949
|
function resolvePgBinary(tool, env = process.env) {
|
|
20792
|
-
const
|
|
20793
|
-
if (
|
|
20950
|
+
const overrideVar = tool === "pg_dump" ? env.PG_DUMP_PATH : tool === "pg_restore" ? env.PG_RESTORE_PATH : env.PG_DUMPALL_PATH;
|
|
20951
|
+
if (overrideVar && fs.existsSync(overrideVar)) return overrideVar;
|
|
20794
20952
|
return resolveLocalBin(tool);
|
|
20795
20953
|
}
|
|
20796
20954
|
/** Run `<bin> --version` and extract the major version. */
|
|
@@ -20834,6 +20992,11 @@ async function preflight(tool, connectionString, env = process.env) {
|
|
|
20834
20992
|
* Produce a custom-format dump on local disk. When `outDir` is omitted the
|
|
20835
20993
|
* file is written to the OS temp directory (used by the upload path, which
|
|
20836
20994
|
* cleans it up afterwards).
|
|
20995
|
+
*
|
|
20996
|
+
* Alongside the `-Fc` dump it writes a `<name>.globals.sql` sidecar via
|
|
20997
|
+
* `pg_dumpall --globals-only` so the roles the dump's GRANT/RLS statements
|
|
20998
|
+
* depend on can be recreated on restore. Set `includeGlobals: false` to skip
|
|
20999
|
+
* it (e.g. when the caller has no privilege to read cluster globals).
|
|
20837
21000
|
*/
|
|
20838
21001
|
async function createDump(opts) {
|
|
20839
21002
|
const env = opts.env ?? process.env;
|
|
@@ -20852,17 +21015,92 @@ async function createDump(opts) {
|
|
|
20852
21015
|
stdio: opts.inheritStdio ? "inherit" : "pipe",
|
|
20853
21016
|
env: { ...env }
|
|
20854
21017
|
});
|
|
20855
|
-
|
|
21018
|
+
const result = {
|
|
20856
21019
|
localFile,
|
|
20857
21020
|
fileName,
|
|
20858
21021
|
sizeBytes: fs.existsSync(localFile) ? fs.statSync(localFile).size : 0
|
|
20859
21022
|
};
|
|
21023
|
+
if (opts.includeGlobals !== false) {
|
|
21024
|
+
const dumpallBin = resolvePgBinary("pg_dumpall", env);
|
|
21025
|
+
if (!dumpallBin) throw new BackupToolError("Could not find the 'pg_dumpall' binary needed to capture cluster roles.", "Install the PostgreSQL client tools or set PG_DUMPALL_PATH. To take a role-incomplete backup anyway, pass includeGlobals: false.");
|
|
21026
|
+
const globalsFile = globalsFileForDump(localFile);
|
|
21027
|
+
await execa(dumpallBin, buildPgDumpallGlobalsArgs({
|
|
21028
|
+
connectionString: opts.connectionString,
|
|
21029
|
+
outFile: globalsFile
|
|
21030
|
+
}), {
|
|
21031
|
+
stdio: opts.inheritStdio ? "inherit" : "pipe",
|
|
21032
|
+
env: { ...env }
|
|
21033
|
+
});
|
|
21034
|
+
result.globalsFile = globalsFile;
|
|
21035
|
+
result.globalsSizeBytes = fs.existsSync(globalsFile) ? fs.statSync(globalsFile).size : 0;
|
|
21036
|
+
}
|
|
21037
|
+
return result;
|
|
21038
|
+
}
|
|
21039
|
+
/**
|
|
21040
|
+
* Cheap integrity check on a freshly written dump: it must be non-empty and
|
|
21041
|
+
* `pg_restore --list` must parse its table of contents without error. Used
|
|
21042
|
+
* before pruning older backups so a corrupt-but-exit-0 dump never becomes
|
|
21043
|
+
* the reason the last good backup is deleted.
|
|
21044
|
+
*/
|
|
21045
|
+
async function validateDump(localFile, env = process.env) {
|
|
21046
|
+
if (!fs.existsSync(localFile)) return {
|
|
21047
|
+
ok: false,
|
|
21048
|
+
reason: `Dump file does not exist: ${localFile}`
|
|
21049
|
+
};
|
|
21050
|
+
if (fs.statSync(localFile).size === 0) return {
|
|
21051
|
+
ok: false,
|
|
21052
|
+
reason: "Dump file is empty (0 bytes)."
|
|
21053
|
+
};
|
|
21054
|
+
const bin = resolvePgBinary("pg_restore", env);
|
|
21055
|
+
if (!bin) return {
|
|
21056
|
+
ok: false,
|
|
21057
|
+
reason: "Could not find 'pg_restore' to verify the dump."
|
|
21058
|
+
};
|
|
21059
|
+
try {
|
|
21060
|
+
await execa(bin, buildPgRestoreListArgs(localFile), {
|
|
21061
|
+
stdio: "pipe",
|
|
21062
|
+
env: { ...env }
|
|
21063
|
+
});
|
|
21064
|
+
return { ok: true };
|
|
21065
|
+
} catch (err) {
|
|
21066
|
+
return {
|
|
21067
|
+
ok: false,
|
|
21068
|
+
reason: `pg_restore --list failed: ${err instanceof Error ? err.message : String(err)}`
|
|
21069
|
+
};
|
|
21070
|
+
}
|
|
21071
|
+
}
|
|
21072
|
+
/**
|
|
21073
|
+
* Replay a `pg_dumpall --globals-only` script to recreate cluster roles
|
|
21074
|
+
* before a restore, so the dump's GRANT/RLS statements (which reference
|
|
21075
|
+
* `rebase_user` and any owner roles) actually apply. Runs statement by
|
|
21076
|
+
* statement and tolerates per-statement failures — on a same-cluster restore
|
|
21077
|
+
* the roles usually already exist (`CREATE ROLE` → "already exists"), and on
|
|
21078
|
+
* a managed provider an `ALTER ROLE <superuser>` may be refused; neither
|
|
21079
|
+
* should abort role recreation. Returns how many statements applied vs were
|
|
21080
|
+
* skipped.
|
|
21081
|
+
*/
|
|
21082
|
+
async function applyGlobals(connectionString, globalsSql, log = () => {}) {
|
|
21083
|
+
const { Client } = await import("pg");
|
|
21084
|
+
const client = new Client({ connectionString });
|
|
21085
|
+
await client.connect();
|
|
21086
|
+
try {
|
|
21087
|
+
return await applyGlobalsWith(async (sql) => {
|
|
21088
|
+
await client.query(sql);
|
|
21089
|
+
}, globalsSql, log);
|
|
21090
|
+
} finally {
|
|
21091
|
+
await client.end();
|
|
21092
|
+
}
|
|
20860
21093
|
}
|
|
20861
21094
|
/**
|
|
20862
21095
|
* Restore a custom-format dump into the database named by
|
|
20863
21096
|
* `connectionString`. Destructive when `clean` is set (drops objects
|
|
20864
21097
|
* first). Never called automatically — the CLI gates it behind explicit
|
|
20865
21098
|
* confirmation.
|
|
21099
|
+
*
|
|
21100
|
+
* Runs with `--exit-on-error` by default: a restore that logs-and-continues
|
|
21101
|
+
* past a failed GRANT (because a role was missing) reports success with RLS
|
|
21102
|
+
* un-enforced. Callers should recreate roles first (see {@link applyGlobals})
|
|
21103
|
+
* and only set `exitOnError: false` deliberately.
|
|
20866
21104
|
*/
|
|
20867
21105
|
async function restoreDump(opts) {
|
|
20868
21106
|
const env = opts.env ?? process.env;
|
|
@@ -20872,7 +21110,8 @@ async function restoreDump(opts) {
|
|
|
20872
21110
|
connectionString: opts.connectionString,
|
|
20873
21111
|
inputFile: opts.inputFile,
|
|
20874
21112
|
clean: opts.clean,
|
|
20875
|
-
noOwner: opts.noOwner
|
|
21113
|
+
noOwner: opts.noOwner,
|
|
21114
|
+
exitOnError: opts.exitOnError
|
|
20876
21115
|
}), {
|
|
20877
21116
|
stdio: opts.inheritStdio ? "inherit" : "pipe",
|
|
20878
21117
|
env: { ...env }
|
|
@@ -20949,9 +21188,13 @@ async function listBackups(dest, storage) {
|
|
|
20949
21188
|
*/
|
|
20950
21189
|
async function pruneBackups(dest, options, storage) {
|
|
20951
21190
|
const toDelete = selectBackupsToPrune(await listBackups(dest, storage), options);
|
|
20952
|
-
|
|
20953
|
-
if (fs.existsSync(key))
|
|
20954
|
-
|
|
21191
|
+
await pruneWith(toDelete, dest.kind === "local" ? async (key) => {
|
|
21192
|
+
if (!fs.existsSync(key)) throw new Error(`not found: ${key}`);
|
|
21193
|
+
fs.unlinkSync(key);
|
|
21194
|
+
} : async (key) => {
|
|
21195
|
+
if (!storage) throw new BackupToolError("Storage backend required to prune object backups.");
|
|
21196
|
+
await storage.deleteObject(key, dest.bucket);
|
|
21197
|
+
});
|
|
20955
21198
|
return toDelete;
|
|
20956
21199
|
}
|
|
20957
21200
|
//#endregion
|
|
@@ -21016,7 +21259,7 @@ function createBackupCron(config) {
|
|
|
21016
21259
|
enabled: config.enabled ?? true,
|
|
21017
21260
|
timeoutSeconds: 3600,
|
|
21018
21261
|
async handler({ log }) {
|
|
21019
|
-
const { createDump, pruneBackups, uploadBackup } = await Promise.resolve().then(() => backup_service_exports);
|
|
21262
|
+
const { createDump, pruneBackups, uploadBackup, validateDump } = await Promise.resolve().then(() => backup_service_exports);
|
|
21020
21263
|
const { destination } = config;
|
|
21021
21264
|
if (destination.kind !== "local" && !config.storage) throw new Error(`Backup destination is ${destination.kind} but no storage controller was provided. Pass the backend's configured StorageController to createBackupCron({ storage }).`);
|
|
21022
21265
|
log(`Starting backup of "${dbName}"…`);
|
|
@@ -21028,15 +21271,23 @@ function createBackupCron(config) {
|
|
|
21028
21271
|
excludeSchemas
|
|
21029
21272
|
});
|
|
21030
21273
|
log(`Dump created: ${dump.fileName} (${formatBytes(dump.sizeBytes)})`);
|
|
21274
|
+
const check = await validateDump(dump.localFile);
|
|
21275
|
+
if (!check.ok) {
|
|
21276
|
+
if (destination.kind !== "local" && fs.existsSync(dump.localFile)) fs.unlinkSync(dump.localFile);
|
|
21277
|
+
if (dump.globalsFile && destination.kind !== "local" && fs.existsSync(dump.globalsFile)) fs.unlinkSync(dump.globalsFile);
|
|
21278
|
+
throw new Error(`New backup failed validation — skipping upload and pruning to protect existing backups. ${check.reason}`);
|
|
21279
|
+
}
|
|
21031
21280
|
let storedKey = dump.localFile;
|
|
21032
21281
|
try {
|
|
21033
21282
|
if (destination.kind !== "local") {
|
|
21034
21283
|
const uploaded = await uploadBackup(config.storage, dump.localFile, destination);
|
|
21035
21284
|
storedKey = uploaded.storageUrl;
|
|
21036
21285
|
log(`Uploaded to ${uploaded.storageUrl}`);
|
|
21286
|
+
if (dump.globalsFile && fs.existsSync(dump.globalsFile)) log(`Uploaded roles sidecar to ${(await uploadBackup(config.storage, dump.globalsFile, destination)).storageUrl}`);
|
|
21037
21287
|
}
|
|
21038
21288
|
} finally {
|
|
21039
21289
|
if (destination.kind !== "local" && fs.existsSync(dump.localFile)) fs.unlinkSync(dump.localFile);
|
|
21290
|
+
if (dump.globalsFile && destination.kind !== "local" && fs.existsSync(dump.globalsFile)) fs.unlinkSync(dump.globalsFile);
|
|
21040
21291
|
}
|
|
21041
21292
|
let pruned = [];
|
|
21042
21293
|
if (config.retentionDays && config.retentionDays > 0) {
|
|
@@ -21153,6 +21404,7 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
21153
21404
|
email_verification_sent_at TIMESTAMP WITH TIME ZONE,
|
|
21154
21405
|
is_anonymous BOOLEAN DEFAULT FALSE NOT NULL,
|
|
21155
21406
|
metadata JSONB DEFAULT '{}' NOT NULL,
|
|
21407
|
+
tokens_valid_after TIMESTAMP WITH TIME ZONE,
|
|
21156
21408
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
|
21157
21409
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
|
|
21158
21410
|
)
|
|
@@ -21233,12 +21485,15 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
21233
21485
|
CREATE TABLE IF NOT EXISTS ${sql.raw(refreshTokensTableName)} (
|
|
21234
21486
|
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
21235
21487
|
uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
21488
|
+
session_id TEXT NOT NULL DEFAULT gen_random_uuid()::text,
|
|
21236
21489
|
token_hash TEXT NOT NULL UNIQUE,
|
|
21237
21490
|
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
21491
|
+
revoked BOOLEAN DEFAULT FALSE NOT NULL,
|
|
21492
|
+
rotated_at TIMESTAMP WITH TIME ZONE,
|
|
21493
|
+
session_started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
|
21238
21494
|
user_agent TEXT,
|
|
21239
21495
|
ip_address TEXT,
|
|
21240
|
-
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
21241
|
-
CONSTRAINT unique_device_session UNIQUE (uid, user_agent, ip_address)
|
|
21496
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
21242
21497
|
)
|
|
21243
21498
|
`);
|
|
21244
21499
|
await db.execute(sql`
|
|
@@ -21328,6 +21583,7 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
21328
21583
|
"email_verification_sent_at TIMESTAMP WITH TIME ZONE",
|
|
21329
21584
|
"is_anonymous BOOLEAN DEFAULT FALSE NOT NULL",
|
|
21330
21585
|
"metadata JSONB DEFAULT '{}' NOT NULL",
|
|
21586
|
+
"tokens_valid_after TIMESTAMP WITH TIME ZONE",
|
|
21331
21587
|
"created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL",
|
|
21332
21588
|
"updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL"
|
|
21333
21589
|
]) await db.execute(sql`
|
|
@@ -21344,42 +21600,36 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
21344
21600
|
for (const { table_schema } of found) {
|
|
21345
21601
|
const qualified = `"${table_schema}"."refresh_tokens"`;
|
|
21346
21602
|
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`
|
|
21603
|
+
await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS session_id TEXT`);
|
|
21604
|
+
await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS revoked BOOLEAN DEFAULT FALSE NOT NULL`);
|
|
21605
|
+
await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS rotated_at TIMESTAMP WITH TIME ZONE`);
|
|
21606
|
+
await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS session_started_at TIMESTAMP WITH TIME ZONE`);
|
|
21351
21607
|
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
|
|
21608
|
+
UPDATE ${sql.raw(qualified)}
|
|
21609
|
+
SET session_id = gen_random_uuid()::text
|
|
21610
|
+
WHERE session_id IS NULL
|
|
21358
21611
|
`);
|
|
21359
|
-
|
|
21360
|
-
|
|
21361
|
-
|
|
21362
|
-
WHERE
|
|
21612
|
+
await db.execute(sql`
|
|
21613
|
+
UPDATE ${sql.raw(qualified)}
|
|
21614
|
+
SET session_started_at = COALESCE(created_at, NOW())
|
|
21615
|
+
WHERE session_started_at IS NULL
|
|
21363
21616
|
`);
|
|
21364
|
-
|
|
21365
|
-
|
|
21366
|
-
|
|
21367
|
-
|
|
21368
|
-
|
|
21369
|
-
|
|
21370
|
-
|
|
21371
|
-
|
|
21372
|
-
|
|
21373
|
-
|
|
21374
|
-
`);
|
|
21375
|
-
logger.info(`✅ Added correct unique_device_session constraint to ${qualified}`);
|
|
21376
|
-
}
|
|
21617
|
+
await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ALTER COLUMN session_id SET DEFAULT gen_random_uuid()::text`);
|
|
21618
|
+
await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ALTER COLUMN session_started_at SET DEFAULT NOW()`);
|
|
21619
|
+
await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ALTER COLUMN session_id SET NOT NULL`);
|
|
21620
|
+
await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ALTER COLUMN session_started_at SET NOT NULL`);
|
|
21621
|
+
await db.execute(sql`
|
|
21622
|
+
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_session
|
|
21623
|
+
ON ${sql.raw(qualified)}(session_id)
|
|
21624
|
+
`);
|
|
21625
|
+
await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} DROP CONSTRAINT IF EXISTS unique_device_session`);
|
|
21626
|
+
logger.info(`✅ refresh_tokens reconciled for session-scoped rotation: ${qualified}`);
|
|
21377
21627
|
} catch (perTableError) {
|
|
21378
21628
|
logger.warn(`⚠️ refresh_tokens reconcile failed for ${qualified}: ${perTableError instanceof Error ? perTableError.message : String(perTableError)}`);
|
|
21379
21629
|
}
|
|
21380
21630
|
}
|
|
21381
21631
|
} catch (migrationError) {
|
|
21382
|
-
logger.warn(`⚠️ refresh_tokens
|
|
21632
|
+
logger.warn(`⚠️ refresh_tokens session migration skipped: ${migrationError instanceof Error ? migrationError.message : String(migrationError)}`);
|
|
21383
21633
|
}
|
|
21384
21634
|
try {
|
|
21385
21635
|
if ((await db.execute(sql`
|
|
@@ -21871,34 +22121,31 @@ var UserService = class {
|
|
|
21871
22121
|
var RefreshTokenService = class {
|
|
21872
22122
|
db;
|
|
21873
22123
|
refreshTokensTable;
|
|
22124
|
+
usersTable;
|
|
21874
22125
|
constructor(db, tableOrTables) {
|
|
21875
22126
|
this.db = db;
|
|
21876
|
-
if (tableOrTables && (tableOrTables.refreshTokens || tableOrTables.users))
|
|
21877
|
-
|
|
22127
|
+
if (tableOrTables && (tableOrTables.refreshTokens || tableOrTables.users)) {
|
|
22128
|
+
this.refreshTokensTable = tableOrTables.refreshTokens || refreshTokens;
|
|
22129
|
+
this.usersTable = tableOrTables.users || users;
|
|
22130
|
+
} else {
|
|
22131
|
+
this.refreshTokensTable = tableOrTables || refreshTokens;
|
|
22132
|
+
this.usersTable = users;
|
|
22133
|
+
}
|
|
21878
22134
|
}
|
|
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
|
-
});
|
|
22135
|
+
/**
|
|
22136
|
+
* Whether the table actually carries a column, so a host application that
|
|
22137
|
+
* supplied its own `refresh_tokens` table — one that predates session
|
|
22138
|
+
* grouping — degrades instead of throwing on every sign-in.
|
|
22139
|
+
*/
|
|
22140
|
+
has(column) {
|
|
22141
|
+
return Boolean(this.refreshTokensTable[column]);
|
|
21899
22142
|
}
|
|
21900
|
-
|
|
21901
|
-
|
|
22143
|
+
col(column) {
|
|
22144
|
+
return this.refreshTokensTable[column];
|
|
22145
|
+
}
|
|
22146
|
+
/** The columns to read back, narrowed to the ones this table has. */
|
|
22147
|
+
selection() {
|
|
22148
|
+
const selection = {
|
|
21902
22149
|
id: this.refreshTokensTable.id,
|
|
21903
22150
|
uid: this.refreshTokensTable.uid,
|
|
21904
22151
|
tokenHash: this.refreshTokensTable.tokenHash,
|
|
@@ -21906,9 +22153,94 @@ var RefreshTokenService = class {
|
|
|
21906
22153
|
createdAt: this.refreshTokensTable.createdAt,
|
|
21907
22154
|
userAgent: this.refreshTokensTable.userAgent,
|
|
21908
22155
|
ipAddress: this.refreshTokensTable.ipAddress
|
|
21909
|
-
}
|
|
22156
|
+
};
|
|
22157
|
+
for (const optional of [
|
|
22158
|
+
"sessionId",
|
|
22159
|
+
"rotatedAt",
|
|
22160
|
+
"revoked",
|
|
22161
|
+
"sessionStartedAt"
|
|
22162
|
+
]) if (this.has(optional)) selection[optional] = this.col(optional);
|
|
22163
|
+
return selection;
|
|
22164
|
+
}
|
|
22165
|
+
async createToken(uid, tokenHash, expiresAt, userAgent, ipAddress, session) {
|
|
22166
|
+
const values = {
|
|
22167
|
+
uid,
|
|
22168
|
+
tokenHash,
|
|
22169
|
+
expiresAt,
|
|
22170
|
+
userAgent: userAgent || "",
|
|
22171
|
+
ipAddress: ipAddress || ""
|
|
22172
|
+
};
|
|
22173
|
+
if (session && this.has("sessionId")) values.sessionId = session.id;
|
|
22174
|
+
if (session && this.has("sessionStartedAt")) values.sessionStartedAt = session.startedAt;
|
|
22175
|
+
await this.db.insert(this.refreshTokensTable).values(values);
|
|
22176
|
+
}
|
|
22177
|
+
async findByHash(tokenHash) {
|
|
22178
|
+
const [token] = await this.db.select(this.selection()).from(this.refreshTokensTable).where(eq(this.refreshTokensTable.tokenHash, tokenHash));
|
|
21910
22179
|
return token || null;
|
|
21911
22180
|
}
|
|
22181
|
+
/**
|
|
22182
|
+
* Record that a token was rotated away, keeping the row.
|
|
22183
|
+
*
|
|
22184
|
+
* The row is what lets `/auth/refresh` distinguish "you already used this,
|
|
22185
|
+
* here is a fresh one" from "no idea what this is". Deleting it — which is
|
|
22186
|
+
* what this used to do — collapsed both into a 401 and signed the user out
|
|
22187
|
+
* for the crime of losing a response.
|
|
22188
|
+
*/
|
|
22189
|
+
async markRotated(tokenHash) {
|
|
22190
|
+
if (!this.has("rotatedAt")) {
|
|
22191
|
+
await this.deleteByHash(tokenHash);
|
|
22192
|
+
return;
|
|
22193
|
+
}
|
|
22194
|
+
await this.db.update(this.refreshTokensTable).set({ rotatedAt: /* @__PURE__ */ new Date() }).where(eq(this.refreshTokensTable.tokenHash, tokenHash));
|
|
22195
|
+
}
|
|
22196
|
+
/** Final kill of one sign-in: logout, or revoking a device remotely. */
|
|
22197
|
+
async revokeSession(sessionId) {
|
|
22198
|
+
if (!this.has("sessionId")) return;
|
|
22199
|
+
if (this.has("revoked")) {
|
|
22200
|
+
await this.db.update(this.refreshTokensTable).set({
|
|
22201
|
+
revoked: true,
|
|
22202
|
+
...this.has("rotatedAt") ? { rotatedAt: /* @__PURE__ */ new Date() } : {}
|
|
22203
|
+
}).where(eq(this.col("sessionId"), sessionId));
|
|
22204
|
+
return;
|
|
22205
|
+
}
|
|
22206
|
+
await this.db.delete(this.refreshTokensTable).where(eq(this.col("sessionId"), sessionId));
|
|
22207
|
+
}
|
|
22208
|
+
/**
|
|
22209
|
+
* Housekeeping: rotation would otherwise leave a row per refresh forever.
|
|
22210
|
+
* Superseded rows are only needed for as long as a straggler might still
|
|
22211
|
+
* present them, and expired ones are dead weight everywhere.
|
|
22212
|
+
*/
|
|
22213
|
+
async prune(uid, sessionId, supersededBefore) {
|
|
22214
|
+
const uidCol = this.refreshTokensTable.uid;
|
|
22215
|
+
const expiresCol = this.refreshTokensTable.expiresAt;
|
|
22216
|
+
if (!this.has("rotatedAt") || !this.has("sessionId")) {
|
|
22217
|
+
await this.db.delete(this.refreshTokensTable).where(sql`${uidCol} = ${uid} AND ${expiresCol} < NOW()`);
|
|
22218
|
+
return;
|
|
22219
|
+
}
|
|
22220
|
+
const rotatedCol = this.col("rotatedAt");
|
|
22221
|
+
const sessionCol = this.col("sessionId");
|
|
22222
|
+
await this.db.delete(this.refreshTokensTable).where(sql`
|
|
22223
|
+
${uidCol} = ${uid}
|
|
22224
|
+
AND (
|
|
22225
|
+
${expiresCol} < NOW()
|
|
22226
|
+
OR (
|
|
22227
|
+
${sessionCol} = ${sessionId}
|
|
22228
|
+
AND ${rotatedCol} IS NOT NULL
|
|
22229
|
+
AND ${rotatedCol} < ${supersededBefore}
|
|
22230
|
+
)
|
|
22231
|
+
)
|
|
22232
|
+
`);
|
|
22233
|
+
}
|
|
22234
|
+
async getTokensValidAfter(uid) {
|
|
22235
|
+
if (!this.usersTable || !this.usersTable.tokensValidAfter) return null;
|
|
22236
|
+
const [row] = await this.db.select({ tokensValidAfter: this.usersTable.tokensValidAfter }).from(this.usersTable).where(eq(this.usersTable.id, uid));
|
|
22237
|
+
const value = row?.tokensValidAfter;
|
|
22238
|
+
return value ? new Date(value) : null;
|
|
22239
|
+
}
|
|
22240
|
+
async setTokensValidAfter(uid, at) {
|
|
22241
|
+
if (!this.usersTable || !this.usersTable.tokensValidAfter) return;
|
|
22242
|
+
await this.db.update(this.usersTable).set({ tokensValidAfter: at }).where(eq(this.usersTable.id, uid));
|
|
22243
|
+
}
|
|
21912
22244
|
async deleteByHash(tokenHash) {
|
|
21913
22245
|
await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.tokenHash, tokenHash));
|
|
21914
22246
|
}
|
|
@@ -21916,15 +22248,7 @@ var RefreshTokenService = class {
|
|
|
21916
22248
|
await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.uid, uid));
|
|
21917
22249
|
}
|
|
21918
22250
|
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);
|
|
22251
|
+
return await this.db.select(this.selection()).from(this.refreshTokensTable).where(eq(this.refreshTokensTable.uid, uid)).orderBy(this.refreshTokensTable.createdAt);
|
|
21928
22252
|
}
|
|
21929
22253
|
async deleteById(id, uid) {
|
|
21930
22254
|
await this.db.delete(this.refreshTokensTable).where(sql`${this.refreshTokensTable.id} = ${id} AND ${this.refreshTokensTable.uid} = ${uid}`);
|
|
@@ -22069,8 +22393,23 @@ var PostgresTokenRepository = class {
|
|
|
22069
22393
|
this.passwordResetTokenService = new PasswordResetTokenService(db, tableOrTables);
|
|
22070
22394
|
this.magicLinkTokenService = new MagicLinkTokenService(db, tableOrTables);
|
|
22071
22395
|
}
|
|
22072
|
-
async createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress) {
|
|
22073
|
-
await this.refreshTokenService.createToken(uid, tokenHash, expiresAt, userAgent, ipAddress);
|
|
22396
|
+
async createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress, session) {
|
|
22397
|
+
await this.refreshTokenService.createToken(uid, tokenHash, expiresAt, userAgent, ipAddress, session);
|
|
22398
|
+
}
|
|
22399
|
+
async markRefreshTokenRotated(tokenHash) {
|
|
22400
|
+
await this.refreshTokenService.markRotated(tokenHash);
|
|
22401
|
+
}
|
|
22402
|
+
async revokeRefreshTokenSession(sessionId) {
|
|
22403
|
+
await this.refreshTokenService.revokeSession(sessionId);
|
|
22404
|
+
}
|
|
22405
|
+
async pruneRefreshTokens(uid, sessionId, supersededBefore) {
|
|
22406
|
+
await this.refreshTokenService.prune(uid, sessionId, supersededBefore);
|
|
22407
|
+
}
|
|
22408
|
+
async getTokensValidAfter(uid) {
|
|
22409
|
+
return this.refreshTokenService.getTokensValidAfter(uid);
|
|
22410
|
+
}
|
|
22411
|
+
async setTokensValidAfter(uid, at) {
|
|
22412
|
+
await this.refreshTokenService.setTokensValidAfter(uid, at);
|
|
22074
22413
|
}
|
|
22075
22414
|
async findRefreshTokenByHash(tokenHash) {
|
|
22076
22415
|
return this.refreshTokenService.findByHash(tokenHash);
|
|
@@ -22236,8 +22575,23 @@ var PostgresAuthRepository = class {
|
|
|
22236
22575
|
};
|
|
22237
22576
|
}
|
|
22238
22577
|
async deleteRole(_id) {}
|
|
22239
|
-
async createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress) {
|
|
22240
|
-
await this.tokenRepository.createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress);
|
|
22578
|
+
async createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress, session) {
|
|
22579
|
+
await this.tokenRepository.createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress, session);
|
|
22580
|
+
}
|
|
22581
|
+
async markRefreshTokenRotated(tokenHash) {
|
|
22582
|
+
await this.tokenRepository.markRefreshTokenRotated(tokenHash);
|
|
22583
|
+
}
|
|
22584
|
+
async revokeRefreshTokenSession(sessionId) {
|
|
22585
|
+
await this.tokenRepository.revokeRefreshTokenSession(sessionId);
|
|
22586
|
+
}
|
|
22587
|
+
async pruneRefreshTokens(uid, sessionId, supersededBefore) {
|
|
22588
|
+
await this.tokenRepository.pruneRefreshTokens(uid, sessionId, supersededBefore);
|
|
22589
|
+
}
|
|
22590
|
+
async getTokensValidAfter(uid) {
|
|
22591
|
+
return this.tokenRepository.getTokensValidAfter(uid);
|
|
22592
|
+
}
|
|
22593
|
+
async setTokensValidAfter(uid, at) {
|
|
22594
|
+
await this.tokenRepository.setTokensValidAfter(uid, at);
|
|
22241
22595
|
}
|
|
22242
22596
|
async findRefreshTokenByHash(tokenHash) {
|
|
22243
22597
|
return this.tokenRepository.findRefreshTokenByHash(tokenHash);
|
|
@@ -23516,6 +23870,6 @@ function createPostgresAdapter(pgConfig) {
|
|
|
23516
23870
|
};
|
|
23517
23871
|
}
|
|
23518
23872
|
//#endregion
|
|
23519
|
-
export { AuthenticatedPostgresBackendDriver, BackupToolError, BranchService, DatabasePoolManager, DrizzleConditionBuilder, PostgresBackendDriver, PostgresCollectionRegistry, PostgresConditionBuilder, PostgresRealtimeProvider, RealtimeService, appConfig, backupCronConfigFromEnv, buildBackupFilename, buildPgDumpArgs, buildPgRestoreArgs, checkToolServerCompatibility, createAuthSchema, createBackupCron, createDirectDatabaseConnection, createDump, createPostgresAdapter, createPostgresBootstrapper, createPostgresDatabaseConnection, createPostgresWebSocket, createReadReplicaConnection, detectToolMajor, ensureDatabaseExists, generateSchema, getServerVersionMajor, guardPoolAgainstDirtyRelease, joinStorageKey, listBackups, magicLinkTokens, magicLinkTokensRelations, mfaChallenges, mfaChallengesRelations, mfaFactors, mfaFactorsRelations, parseBackupDestination, parseBackupTimestamp, parseDbNameFromUrl, parsePgToolMajor, passwordResetTokens, passwordResetTokensRelations, preflight, pruneBackups, recoveryCodes, recoveryCodesRelations, refreshTokens, refreshTokensRelations, resolveConnectionString, resolvePgBinary, restoreDump, selectBackupsToPrune, serverVersionNumToMajor, uploadBackup, userIdentities, userIdentitiesRelations, users, usersRelations, usersSchema, withDatabaseName };
|
|
23873
|
+
export { AuthenticatedPostgresBackendDriver, BackupToolError, BranchService, DatabasePoolManager, DrizzleConditionBuilder, PostgresBackendDriver, PostgresCollectionRegistry, PostgresConditionBuilder, PostgresRealtimeProvider, RealtimeService, appConfig, applyGlobals, backupCronConfigFromEnv, buildBackupFilename, buildPgDumpArgs, buildPgDumpallGlobalsArgs, buildPgRestoreArgs, buildPgRestoreListArgs, checkToolServerCompatibility, createAuthSchema, createBackupCron, createDirectDatabaseConnection, createDump, createPostgresAdapter, createPostgresBootstrapper, createPostgresDatabaseConnection, createPostgresWebSocket, createReadReplicaConnection, detectToolMajor, ensureDatabaseExists, generateSchema, getServerVersionMajor, globalsFileForDump, guardPoolAgainstDirtyRelease, joinStorageKey, listBackups, magicLinkTokens, magicLinkTokensRelations, mfaChallenges, mfaChallengesRelations, mfaFactors, mfaFactorsRelations, parseBackupDestination, parseBackupTimestamp, parseDbNameFromUrl, parsePgToolMajor, passwordResetTokens, passwordResetTokensRelations, preflight, pruneBackups, recoveryCodes, recoveryCodesRelations, refreshTokens, refreshTokensRelations, resolveConnectionString, resolvePgBinary, restoreDump, selectBackupsToPrune, serverVersionNumToMajor, splitGlobalsStatements, uploadBackup, userIdentities, userIdentitiesRelations, users, usersRelations, usersSchema, validateDump, withDatabaseName };
|
|
23520
23874
|
|
|
23521
23875
|
//# sourceMappingURL=index.es.js.map
|