feat(auth): support schema-qualified SQL stores

This commit is contained in:
Clintchiz committed 2026-08-26 14:11:13 +05:30
1 parent a85cbf240d
commit 681679483a
5 files changed
+48 -4

No files matched your search

+1 -1
View File
@@ -235,7 +235,7 @@
},
"packages/auth": {
"name": "@wrnexus/auth",
"version": "0.8.19",
"version": "0.8.20",
"dependencies": {
"@wrnexus/authz": "workspace:*",
"@wrnexus/captcha": "workspace:*",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/auth",
"version": "0.8.19",
"version": "0.8.20",
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
"type": "module",
"sideEffects": false,
+1
View File
@@ -8,6 +8,7 @@ export {
export type { AuthStore } from "./store.ts";
export { MemoryAuthStore } from "./stores/memory.ts";
export { SqlAuthStore } from "./stores/sql.ts";
export type { SqlAuthStoreOptions } from "./stores/sql.ts";
export {
authSession,
requireAuth,
+23 -2
View File
@@ -34,10 +34,31 @@ function placeholders(sql: string, dialect: string): string {
return sql.replace(/\?/g, () => `$${++index}`);
}
export interface SqlAuthStoreOptions {
/** PostgreSQL schema owning authentication tables. */
schema?: string;
/** Defaults to wrn_auth_; use an empty prefix for e.g. sso.users. */
tablePrefix?: string;
}
export class SqlAuthStore implements AuthStore {
constructor(private readonly db: Db) {}
constructor(
private readonly db: Db,
private readonly options: SqlAuthStoreOptions = {},
) {
if (options.schema && !/^[a-z_][a-z0-9_]*$/i.test(options.schema))
throw new Error("Invalid auth schema identifier");
if (options.tablePrefix && !/^[a-z_][a-z0-9_]*$/i.test(options.tablePrefix))
throw new Error("Invalid auth table prefix");
if (options.schema && db.driver.dialect !== "postgres")
throw new Error("Auth schema requires PostgreSQL");
}
private sql(value: string): string {
return placeholders(value, this.db.driver.dialect);
const qualified = value.replace(/\bwrn_auth_([a-z_]+)\b/g, (_match, table: string) => {
const name = (this.options.tablePrefix ?? "wrn_auth_") + table;
return this.options.schema ? '"' + this.options.schema + '"."' + name + '"' : name;
});
return placeholders(qualified, this.db.driver.dialect);
}
private async one(sql: string, params: unknown[] = []): Promise<Row | undefined> {
return (await this.db.one(this.sql(sql), params)) ?? undefined;
+22
View File
@@ -0,0 +1,22 @@
import { expect, test } from "bun:test";
import type { Db } from "@wrnexus/db";
import { SqlAuthStore } from "../src/stores/sql.ts";
test("auth store qualifies tables without changing parameter values", async () => {
let query = "";
let values: unknown[] = [];
const db = {
driver: { dialect: "postgres" },
one: async (sql: string, params: unknown[]) => {
query = sql;
values = params;
return undefined;
},
} as unknown as Db;
const store = new SqlAuthStore(db, { schema: "sso", tablePrefix: "" });
await store.findUserById("wrn_auth_users");
expect(query).toContain('FROM "sso"."users"');
expect(query).toContain("$1");
expect(values).toEqual(["wrn_auth_users"]);
expect(() => new SqlAuthStore(db, { schema: 'sso";DROP TABLE users' })).toThrow();
});