@rebasepro/server-postgres 0.9.1-canary.742f831 → 0.9.1-canary.78ab3dc
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/README.md +21 -0
- package/dist/PostgresBackendDriver.d.ts +18 -0
- package/dist/PostgresBootstrapper.d.ts +7 -1
- package/dist/auth/services.d.ts +53 -53
- package/dist/index.es.js +708 -191
- package/dist/index.es.js.map +1 -1
- package/dist/schema/auth-bootstrap-sql.d.ts +1 -1
- package/dist/schema/auth-schema.d.ts +24 -24
- package/dist/schema/introspect-db-logic.d.ts +0 -5
- package/dist/schema/introspect-db-naming.d.ts +10 -0
- package/dist/security/rls-enforcement.d.ts +2 -2
- package/dist/services/channel-history.d.ts +118 -0
- package/dist/services/realtimeService.d.ts +69 -2
- package/package.json +8 -31
- package/src/PostgresBackendDriver.ts +56 -5
- package/src/PostgresBootstrapper.ts +18 -1
- package/src/auth/ensure-tables.ts +97 -17
- package/src/auth/services.ts +134 -133
- package/src/schema/auth-bootstrap-sql.ts +7 -1
- package/src/schema/auth-schema.ts +13 -13
- package/src/schema/introspect-db-inference.ts +1 -1
- package/src/schema/introspect-db-logic.ts +1 -10
- package/src/schema/introspect-db-naming.ts +15 -0
- package/src/schema/introspect-runtime.ts +1 -1
- package/src/security/rls-enforcement.ts +11 -5
- package/src/services/channel-history.ts +343 -0
- package/src/services/realtimeService.ts +198 -10
- package/src/websocket.ts +30 -11
package/README.md
CHANGED
|
@@ -76,6 +76,27 @@ process.on("SIGTERM", async () => {
|
|
|
76
76
|
| `statementTimeout` | 30,000 |
|
|
77
77
|
| `keepAlive` | true |
|
|
78
78
|
|
|
79
|
+
## Testing
|
|
80
|
+
|
|
81
|
+
This package runs **two test runners**, split by directory. This is deliberate — check which half you are in before running anything.
|
|
82
|
+
|
|
83
|
+
| Tests | Runner | Config | Command |
|
|
84
|
+
|-------|--------|--------|---------|
|
|
85
|
+
| `test/*.ts` (unit) | jest | [`jest.config.cjs`](./jest.config.cjs) | `pnpm test` |
|
|
86
|
+
| `test/e2e/**` (integration) | vitest | [`vitest.e2e.config.ts`](./vitest.e2e.config.ts) | `pnpm test:e2e` |
|
|
87
|
+
|
|
88
|
+
The unit tests use jest's injected globals (`describe`/`it`/`expect`) and `jest.mock`. The e2e tests import explicitly from `vitest` and need Docker (testcontainers spins up a real Postgres).
|
|
89
|
+
|
|
90
|
+
**Running a single unit test:**
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npx jest test/auth-services.test.ts
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Pointing `vitest` at a unit test is the easy mistake here — it produces a bare `ReferenceError: jest is not defined` and a "no tests" result, which looks exactly like a dead or broken test file rather than the wrong runner. [`vitest.config.ts`](./vitest.config.ts) exists solely to intercept that and print the command you actually wanted.
|
|
97
|
+
|
|
98
|
+
`pnpm test` runs jest **without** `--passWithNoTests`: if the unit suite ever stops being collected, CI fails instead of going quietly green.
|
|
99
|
+
|
|
79
100
|
## Related Packages
|
|
80
101
|
|
|
81
102
|
| Package | Role |
|
|
@@ -114,6 +114,24 @@ export declare class PostgresBackendDriver implements DataDriver {
|
|
|
114
114
|
}): Promise<Record<string, unknown>[]>;
|
|
115
115
|
fetchAvailableDatabases(): Promise<string[]>;
|
|
116
116
|
fetchAvailableRoles(): Promise<string[]>;
|
|
117
|
+
/**
|
|
118
|
+
* Application-level roles actually in use in this project.
|
|
119
|
+
*
|
|
120
|
+
* Distinct from {@link fetchAvailableRoles}, which returns native
|
|
121
|
+
* PostgreSQL roles from `pg_roles` (`postgres`, `rebase_user`, …). Those
|
|
122
|
+
* are the roles the SQL editor can `SET ROLE` to. *These* are the strings
|
|
123
|
+
* held in the users table's `roles` column, injected per-transaction as
|
|
124
|
+
* `auth.roles()` and matched by `SecurityRule.roles`. Feeding the pg roles
|
|
125
|
+
* into a `SecurityRule.roles` field produces a condition no user can ever
|
|
126
|
+
* satisfy, so the two must not be conflated.
|
|
127
|
+
*
|
|
128
|
+
* Roles have no registry table — they were migrated out of
|
|
129
|
+
* `rebase.user_roles` onto an inline `roles TEXT[]` column — so the live
|
|
130
|
+
* set is derived from what is assigned. A role that is declared in a policy
|
|
131
|
+
* but held by nobody yet cannot be discovered here; callers that need it
|
|
132
|
+
* should union in the roles they already know about.
|
|
133
|
+
*/
|
|
134
|
+
fetchApplicationRoles(): Promise<string[]>;
|
|
117
135
|
fetchCurrentDatabase(): Promise<string | undefined>;
|
|
118
136
|
/**
|
|
119
137
|
* Fetch public tables that are not yet mapped to a collection.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Implements the `BackendBootstrapper` interface for PostgreSQL.
|
|
5
5
|
*/
|
|
6
6
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
7
|
-
import { BackendBootstrapper } from "@rebasepro/types";
|
|
7
|
+
import { BackendBootstrapper, type RealtimeChannelsConfig } from "@rebasepro/types";
|
|
8
8
|
import { PostgresBackendDriver } from "./PostgresBackendDriver";
|
|
9
9
|
import { RealtimeService } from "./services/realtimeService";
|
|
10
10
|
import { DatabasePoolManager } from "./databasePoolManager";
|
|
@@ -25,6 +25,12 @@ export interface PostgresDriverConfig {
|
|
|
25
25
|
* (BaaS mode). Defaults to `public`.
|
|
26
26
|
*/
|
|
27
27
|
introspectionSchema?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Realtime options. Currently only channel retention, which is opt-in:
|
|
30
|
+
* without rules here no channel keeps any history and broadcast stays
|
|
31
|
+
* fire-and-forget. See {@link ChannelRetentionRule}.
|
|
32
|
+
*/
|
|
33
|
+
realtime?: RealtimeChannelsConfig;
|
|
28
34
|
}
|
|
29
35
|
/**
|
|
30
36
|
* Opaque internals bag that PostgresBootstrapper stores during `initializeDriver()`
|
package/dist/auth/services.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export declare class UserService implements UserRepository {
|
|
|
23
23
|
* Run a privileged auth write with an explicitly cleared RLS context.
|
|
24
24
|
*
|
|
25
25
|
* The auth services run on the base/owner connection, which by design
|
|
26
|
-
* carries a NULL `app.
|
|
26
|
+
* carries a NULL `app.uid` so the `auth.uid() IS NULL` server-escape
|
|
27
27
|
* in the default policies applies. That NULL is normally guaranteed by
|
|
28
28
|
* `set_config(..., is_local = true)` resetting at transaction end — but a
|
|
29
29
|
* GUC that survives on a pooled connection (or a connection role that
|
|
@@ -41,8 +41,8 @@ export declare class UserService implements UserRepository {
|
|
|
41
41
|
getUserById(id: string): Promise<UserData | null>;
|
|
42
42
|
getUserByEmail(email: string): Promise<UserData | null>;
|
|
43
43
|
getUserByIdentity(provider: string, providerId: string): Promise<UserData | null>;
|
|
44
|
-
getUserIdentities(
|
|
45
|
-
linkUserIdentity(
|
|
44
|
+
getUserIdentities(uid: string): Promise<UserIdentityData[]>;
|
|
45
|
+
linkUserIdentity(uid: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void>;
|
|
46
46
|
updateUser(id: string, data: Partial<Omit<CreateUserData, "id">>): Promise<UserData | null>;
|
|
47
47
|
deleteUser(id: string): Promise<void>;
|
|
48
48
|
listUsers(): Promise<UserData[]>;
|
|
@@ -66,23 +66,23 @@ export declare class UserService implements UserRepository {
|
|
|
66
66
|
/**
|
|
67
67
|
* Get roles for a user from database (inline TEXT[] column)
|
|
68
68
|
*/
|
|
69
|
-
getUserRoles(
|
|
69
|
+
getUserRoles(uid: string): Promise<Role[]>;
|
|
70
70
|
/**
|
|
71
71
|
* Get role IDs for a user
|
|
72
72
|
*/
|
|
73
|
-
getUserRoleIds(
|
|
73
|
+
getUserRoleIds(uid: string): Promise<string[]>;
|
|
74
74
|
/**
|
|
75
75
|
* Set roles for a user (replaces existing roles)
|
|
76
76
|
*/
|
|
77
|
-
setUserRoles(
|
|
77
|
+
setUserRoles(uid: string, roleIds: string[]): Promise<void>;
|
|
78
78
|
/**
|
|
79
79
|
* Assign a specific role to new user (appends if not present)
|
|
80
80
|
*/
|
|
81
|
-
assignDefaultRole(
|
|
81
|
+
assignDefaultRole(uid: string, roleId: string): Promise<void>;
|
|
82
82
|
/**
|
|
83
83
|
* Get user with their roles
|
|
84
84
|
*/
|
|
85
|
-
getUserWithRoles(
|
|
85
|
+
getUserWithRoles(uid: string): Promise<{
|
|
86
86
|
user: UserData;
|
|
87
87
|
roles: Role[];
|
|
88
88
|
} | null>;
|
|
@@ -91,12 +91,12 @@ export declare class RefreshTokenService {
|
|
|
91
91
|
private db;
|
|
92
92
|
private refreshTokensTable;
|
|
93
93
|
constructor(db: NodePgDatabase, tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>);
|
|
94
|
-
createToken(
|
|
94
|
+
createToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void>;
|
|
95
95
|
findByHash(tokenHash: string): Promise<RefreshTokenInfo | null>;
|
|
96
96
|
deleteByHash(tokenHash: string): Promise<void>;
|
|
97
|
-
deleteAllForUser(
|
|
98
|
-
listForUser(
|
|
99
|
-
deleteById(id: string,
|
|
97
|
+
deleteAllForUser(uid: string): Promise<void>;
|
|
98
|
+
listForUser(uid: string): Promise<RefreshTokenInfo[]>;
|
|
99
|
+
deleteById(id: string, uid: string): Promise<void>;
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
102
102
|
* Password reset token service
|
|
@@ -109,12 +109,12 @@ export declare class PasswordResetTokenService {
|
|
|
109
109
|
/**
|
|
110
110
|
* Create a password reset token
|
|
111
111
|
*/
|
|
112
|
-
createToken(
|
|
112
|
+
createToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
|
|
113
113
|
/**
|
|
114
114
|
* Find a valid (not expired, not used) token by hash
|
|
115
115
|
*/
|
|
116
116
|
findValidByHash(tokenHash: string): Promise<{
|
|
117
|
-
|
|
117
|
+
uid: string;
|
|
118
118
|
expiresAt: Date;
|
|
119
119
|
} | null>;
|
|
120
120
|
/**
|
|
@@ -124,7 +124,7 @@ export declare class PasswordResetTokenService {
|
|
|
124
124
|
/**
|
|
125
125
|
* Delete all tokens for a user
|
|
126
126
|
*/
|
|
127
|
-
deleteAllForUser(
|
|
127
|
+
deleteAllForUser(uid: string): Promise<void>;
|
|
128
128
|
/**
|
|
129
129
|
* Clean up expired tokens
|
|
130
130
|
*/
|
|
@@ -139,7 +139,7 @@ export declare class MagicLinkTokenService {
|
|
|
139
139
|
private magicLinkTokensTable;
|
|
140
140
|
constructor(db: NodePgDatabase, tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>);
|
|
141
141
|
private getQualifiedTableName;
|
|
142
|
-
createToken(
|
|
142
|
+
createToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
|
|
143
143
|
findValidByHash(tokenHash: string): Promise<MagicLinkTokenInfo | null>;
|
|
144
144
|
markAsUsed(tokenHash: string): Promise<void>;
|
|
145
145
|
}
|
|
@@ -153,18 +153,18 @@ export declare class PostgresTokenRepository implements TokenRepository {
|
|
|
153
153
|
private passwordResetTokenService;
|
|
154
154
|
private magicLinkTokenService;
|
|
155
155
|
constructor(db: NodePgDatabase, tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>);
|
|
156
|
-
createRefreshToken(
|
|
156
|
+
createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void>;
|
|
157
157
|
findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null>;
|
|
158
158
|
deleteRefreshToken(tokenHash: string): Promise<void>;
|
|
159
|
-
deleteAllRefreshTokensForUser(
|
|
160
|
-
listRefreshTokensForUser(
|
|
161
|
-
deleteRefreshTokenById(id: string,
|
|
162
|
-
createPasswordResetToken(
|
|
159
|
+
deleteAllRefreshTokensForUser(uid: string): Promise<void>;
|
|
160
|
+
listRefreshTokensForUser(uid: string): Promise<RefreshTokenInfo[]>;
|
|
161
|
+
deleteRefreshTokenById(id: string, uid: string): Promise<void>;
|
|
162
|
+
createPasswordResetToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
|
|
163
163
|
findValidPasswordResetToken(tokenHash: string): Promise<PasswordResetTokenInfo | null>;
|
|
164
164
|
markPasswordResetTokenUsed(tokenHash: string): Promise<void>;
|
|
165
|
-
deleteAllPasswordResetTokensForUser(
|
|
165
|
+
deleteAllPasswordResetTokensForUser(uid: string): Promise<void>;
|
|
166
166
|
deleteExpiredTokens(): Promise<void>;
|
|
167
|
-
createMagicLinkToken(
|
|
167
|
+
createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
|
|
168
168
|
findValidMagicLinkToken(tokenHash: string): Promise<MagicLinkTokenInfo | null>;
|
|
169
169
|
markMagicLinkTokenUsed(tokenHash: string): Promise<void>;
|
|
170
170
|
}
|
|
@@ -182,8 +182,8 @@ export declare class PostgresAuthRepository implements AuthRepository {
|
|
|
182
182
|
getUserById(id: string): Promise<UserData | null>;
|
|
183
183
|
getUserByEmail(email: string): Promise<UserData | null>;
|
|
184
184
|
getUserByIdentity(provider: string, providerId: string): Promise<UserData | null>;
|
|
185
|
-
getUserIdentities(
|
|
186
|
-
linkUserIdentity(
|
|
185
|
+
getUserIdentities(uid: string): Promise<UserIdentityData[]>;
|
|
186
|
+
linkUserIdentity(uid: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void>;
|
|
187
187
|
updateUser(id: string, data: Partial<Omit<CreateUserData, "id">>): Promise<UserData | null>;
|
|
188
188
|
deleteUser(id: string): Promise<void>;
|
|
189
189
|
listUsers(): Promise<UserData[]>;
|
|
@@ -192,11 +192,11 @@ export declare class PostgresAuthRepository implements AuthRepository {
|
|
|
192
192
|
setEmailVerified(id: string, verified: boolean): Promise<void>;
|
|
193
193
|
setVerificationToken(id: string, token: string | null): Promise<void>;
|
|
194
194
|
getUserByVerificationToken(token: string): Promise<UserData | null>;
|
|
195
|
-
getUserRoles(
|
|
196
|
-
getUserRoleIds(
|
|
197
|
-
setUserRoles(
|
|
198
|
-
assignDefaultRole(
|
|
199
|
-
getUserWithRoles(
|
|
195
|
+
getUserRoles(uid: string): Promise<RoleData[]>;
|
|
196
|
+
getUserRoleIds(uid: string): Promise<string[]>;
|
|
197
|
+
setUserRoles(uid: string, roleIds: string[]): Promise<void>;
|
|
198
|
+
assignDefaultRole(uid: string, roleId: string): Promise<void>;
|
|
199
|
+
getUserWithRoles(uid: string): Promise<{
|
|
200
200
|
user: UserData;
|
|
201
201
|
roles: RoleData[];
|
|
202
202
|
} | null>;
|
|
@@ -205,37 +205,37 @@ export declare class PostgresAuthRepository implements AuthRepository {
|
|
|
205
205
|
createRole(_data: CreateRoleData): Promise<RoleData>;
|
|
206
206
|
updateRole(id: string, data: Partial<Omit<RoleData, "id">>): Promise<RoleData | null>;
|
|
207
207
|
deleteRole(_id: string): Promise<void>;
|
|
208
|
-
createRefreshToken(
|
|
208
|
+
createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void>;
|
|
209
209
|
findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null>;
|
|
210
210
|
deleteRefreshToken(tokenHash: string): Promise<void>;
|
|
211
|
-
deleteAllRefreshTokensForUser(
|
|
212
|
-
listRefreshTokensForUser(
|
|
213
|
-
deleteRefreshTokenById(id: string,
|
|
214
|
-
createPasswordResetToken(
|
|
211
|
+
deleteAllRefreshTokensForUser(uid: string): Promise<void>;
|
|
212
|
+
listRefreshTokensForUser(uid: string): Promise<RefreshTokenInfo[]>;
|
|
213
|
+
deleteRefreshTokenById(id: string, uid: string): Promise<void>;
|
|
214
|
+
createPasswordResetToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
|
|
215
215
|
findValidPasswordResetToken(tokenHash: string): Promise<PasswordResetTokenInfo | null>;
|
|
216
216
|
markPasswordResetTokenUsed(tokenHash: string): Promise<void>;
|
|
217
|
-
deleteAllPasswordResetTokensForUser(
|
|
217
|
+
deleteAllPasswordResetTokensForUser(uid: string): Promise<void>;
|
|
218
218
|
deleteExpiredTokens(): Promise<void>;
|
|
219
|
-
createMagicLinkToken(
|
|
219
|
+
createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
|
|
220
220
|
findValidMagicLinkToken(tokenHash: string): Promise<MagicLinkTokenInfo | null>;
|
|
221
221
|
markMagicLinkTokenUsed(tokenHash: string): Promise<void>;
|
|
222
222
|
private _mfaService;
|
|
223
223
|
private getMfaService;
|
|
224
|
-
createMfaFactor(
|
|
225
|
-
getMfaFactors(
|
|
224
|
+
createMfaFactor(uid: string, factorType: "totp", secretEncrypted: string, friendlyName?: string): Promise<MfaFactor>;
|
|
225
|
+
getMfaFactors(uid: string): Promise<MfaFactor[]>;
|
|
226
226
|
getMfaFactorById(factorId: string): Promise<(MfaFactor & {
|
|
227
227
|
secretEncrypted: string;
|
|
228
228
|
}) | null>;
|
|
229
229
|
verifyMfaFactor(factorId: string): Promise<void>;
|
|
230
|
-
deleteMfaFactor(factorId: string,
|
|
230
|
+
deleteMfaFactor(factorId: string, uid: string): Promise<void>;
|
|
231
231
|
createMfaChallenge(factorId: string, ipAddress?: string): Promise<MfaChallengeInfo>;
|
|
232
232
|
getMfaChallengeById(challengeId: string): Promise<MfaChallengeInfo | null>;
|
|
233
233
|
verifyMfaChallenge(challengeId: string): Promise<void>;
|
|
234
|
-
createRecoveryCodes(
|
|
235
|
-
useRecoveryCode(
|
|
236
|
-
getUnusedRecoveryCodeCount(
|
|
237
|
-
deleteAllRecoveryCodes(
|
|
238
|
-
hasVerifiedMfaFactors(
|
|
234
|
+
createRecoveryCodes(uid: string, codeHashes: string[]): Promise<void>;
|
|
235
|
+
useRecoveryCode(uid: string, codeHash: string): Promise<boolean>;
|
|
236
|
+
getUnusedRecoveryCodeCount(uid: string): Promise<number>;
|
|
237
|
+
deleteAllRecoveryCodes(uid: string): Promise<void>;
|
|
238
|
+
hasVerifiedMfaFactors(uid: string): Promise<boolean>;
|
|
239
239
|
}
|
|
240
240
|
/**
|
|
241
241
|
* PostgreSQL implementation of MfaRepository.
|
|
@@ -246,21 +246,21 @@ export declare class MfaService implements MfaRepository {
|
|
|
246
246
|
private schemaName;
|
|
247
247
|
constructor(db: NodePgDatabase, schemaName?: string);
|
|
248
248
|
private qualify;
|
|
249
|
-
createMfaFactor(
|
|
250
|
-
getMfaFactors(
|
|
249
|
+
createMfaFactor(uid: string, factorType: "totp", secretEncrypted: string, friendlyName?: string): Promise<MfaFactor>;
|
|
250
|
+
getMfaFactors(uid: string): Promise<MfaFactor[]>;
|
|
251
251
|
getMfaFactorById(factorId: string): Promise<(MfaFactor & {
|
|
252
252
|
secretEncrypted: string;
|
|
253
253
|
}) | null>;
|
|
254
254
|
verifyMfaFactor(factorId: string): Promise<void>;
|
|
255
|
-
deleteMfaFactor(factorId: string,
|
|
255
|
+
deleteMfaFactor(factorId: string, uid: string): Promise<void>;
|
|
256
256
|
createMfaChallenge(factorId: string, ipAddress?: string): Promise<MfaChallengeInfo>;
|
|
257
257
|
getMfaChallengeById(challengeId: string): Promise<MfaChallengeInfo | null>;
|
|
258
258
|
verifyMfaChallenge(challengeId: string): Promise<void>;
|
|
259
|
-
createRecoveryCodes(
|
|
260
|
-
useRecoveryCode(
|
|
261
|
-
getUnusedRecoveryCodeCount(
|
|
262
|
-
deleteAllRecoveryCodes(
|
|
263
|
-
hasVerifiedMfaFactors(
|
|
259
|
+
createRecoveryCodes(uid: string, codeHashes: string[]): Promise<void>;
|
|
260
|
+
useRecoveryCode(uid: string, codeHash: string): Promise<boolean>;
|
|
261
|
+
getUnusedRecoveryCodeCount(uid: string): Promise<number>;
|
|
262
|
+
deleteAllRecoveryCodes(uid: string): Promise<void>;
|
|
263
|
+
hasVerifiedMfaFactors(uid: string): Promise<boolean>;
|
|
264
264
|
}
|
|
265
265
|
/** PostgreSQL user repository implementation */
|
|
266
266
|
export type PostgresUserRepository = UserService;
|