@rebasepro/server-postgres 0.13.0 → 0.13.1-canary.g06dbe5b
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/PostgresBackendDriver.d.ts +48 -1
- package/dist/auth/services.d.ts +19 -0
- package/dist/{src-DlPBctw_.js → auth-users-columns-CBEOeYqa.js} +835 -63
- package/dist/auth-users-columns-CBEOeYqa.js.map +1 -0
- package/dist/{backup-service-CD8o_1Sl.js → backup-service-Bww-Lg0s.js} +2 -2
- package/dist/{backup-service-CD8o_1Sl.js.map → backup-service-Bww-Lg0s.js.map} +1 -1
- package/dist/cli-helpers.d.ts +57 -1
- package/dist/data-transformer.d.ts +7 -2
- package/dist/data_driver-ULAyJEi9.js +193 -0
- package/dist/data_driver-ULAyJEi9.js.map +1 -0
- package/dist/{ensure-collection-policies-ViG8XiPn.js → ensure-collection-policies-B_JMGa5K.js} +2 -2
- package/dist/{ensure-collection-policies-ViG8XiPn.js.map → ensure-collection-policies-B_JMGa5K.js.map} +1 -1
- package/dist/{ensure-collection-tables-CBQdOETu.js → ensure-collection-tables-DzeTEvMv.js} +170 -20
- package/dist/ensure-collection-tables-DzeTEvMv.js.map +1 -0
- package/dist/index.es.js +1365 -338
- package/dist/index.es.js.map +1 -1
- package/dist/rls-bootstrap-sql-Bpv3nUZo.js +244 -0
- package/dist/rls-bootstrap-sql-Bpv3nUZo.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +102 -0
- package/dist/schema/auth-users-columns.d.ts +97 -0
- package/dist/schema/doctor-policy-checks.d.ts +28 -0
- package/dist/schema/doctor.d.ts +23 -25
- package/dist/schema/ensure-collection-tables.d.ts +61 -7
- package/dist/schema/generate-drizzle-schema-logic.d.ts +10 -2
- package/dist/schema/generate-postgres-ddl-logic.d.ts +53 -5
- package/dist/schema/generated-schema-staleness.d.ts +39 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
- package/dist/schema/search-column.d.ts +248 -0
- package/dist/security/rls-enforcement.d.ts +61 -5
- package/dist/services/FetchService.d.ts +34 -7
- package/dist/services/RelationService.d.ts +30 -0
- package/dist/services/collection-helpers.d.ts +26 -0
- package/dist/services/dataService.d.ts +5 -0
- package/dist/services/realtimeService.d.ts +131 -21
- package/dist/{src-DoU9yPqq.js → src-C_wvdMnl.js} +91 -2
- package/dist/src-C_wvdMnl.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +124 -2
- package/dist/{websocket-B2LsrINK.js → websocket-D1qbmLZ2.js} +75 -18
- package/dist/websocket-D1qbmLZ2.js.map +1 -0
- package/package.json +9 -8
- package/src/PostgresBackendDriver.ts +172 -6
- package/src/PostgresBootstrapper.ts +55 -6
- package/src/auth/ensure-tables.ts +207 -86
- package/src/auth/services.ts +69 -5
- package/src/cli-helpers.ts +129 -10
- package/src/cli.ts +232 -30
- package/src/collections/validate-relations.ts +124 -17
- package/src/data-transformer.ts +120 -17
- package/src/history/ensure-history-table.ts +7 -0
- package/src/schema/auth-schema.ts +17 -1
- package/src/schema/auth-users-columns.ts +131 -0
- package/src/schema/doctor-cli.ts +12 -63
- package/src/schema/doctor-policy-checks.ts +105 -0
- package/src/schema/doctor.ts +135 -76
- package/src/schema/ensure-collection-tables.ts +374 -32
- package/src/schema/generate-drizzle-schema-logic.ts +132 -42
- package/src/schema/generate-postgres-ddl-logic.ts +294 -16
- package/src/schema/generate-postgres-ddl.ts +25 -2
- package/src/schema/generated-schema-staleness.ts +169 -0
- package/src/schema/introspect-db-logic.ts +66 -34
- package/src/schema/non-sql-collections.test.ts +131 -0
- package/src/schema/rls-bootstrap-sql.ts +288 -0
- package/src/schema/search-column.ts +643 -0
- package/src/security/anonymous-grants.test.ts +4 -2
- package/src/security/rls-enforcement.ts +149 -6
- package/src/services/BranchService.ts +5 -0
- package/src/services/FetchService.ts +175 -108
- package/src/services/PersistService.ts +38 -2
- package/src/services/RelationService.ts +110 -67
- package/src/services/channel-history.ts +14 -0
- package/src/services/channel-presence.ts +13 -0
- package/src/services/collection-helpers.ts +54 -1
- package/src/services/dataService.ts +5 -0
- package/src/services/realtimeService.ts +344 -79
- package/src/utils/drizzle-conditions.ts +365 -23
- package/src/utils/pg-error-utils.ts +8 -3
- package/src/websocket.ts +113 -16
- package/dist/ensure-collection-tables-CBQdOETu.js.map +0 -1
- package/dist/policy-CeA1JcxP.js +0 -105
- package/dist/policy-CeA1JcxP.js.map +0 -1
- package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
- package/dist/src-DlPBctw_.js.map +0 -1
- package/dist/src-DoU9yPqq.js.map +0 -1
- package/dist/websocket-B2LsrINK.js.map +0 -1
- package/src/schema/auth-bootstrap-sql.ts +0 -47
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SQL } from "drizzle-orm";
|
|
2
2
|
import { SecurityRule } from "@rebasepro/types";
|
|
3
|
+
import { REBASE_USER_ROLE } from "@rebasepro/common";
|
|
3
4
|
/**
|
|
4
5
|
* Unified RLS enforcement — the "user context vs server context" model.
|
|
5
6
|
*
|
|
@@ -13,9 +14,14 @@ import { SecurityRule } from "@rebasepro/types";
|
|
|
13
14
|
* are validation/side-effects, not a security boundary.
|
|
14
15
|
*
|
|
15
16
|
* - **Server context** — the base (owner) connection: auth flows, migrations,
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* and raw `rebase.sql`. As table owner it bypasses RLS. This is the trusted
|
|
18
|
+
* plane, equivalent to Supabase's `service_role`.
|
|
19
|
+
*
|
|
20
|
+
* `rebase.dataAsAdmin` is **not** in it, despite the name. `init.ts` scopes
|
|
21
|
+
* that driver with `withAuth(SERVICE_IDENTITY)`, so it arrives as user
|
|
22
|
+
* context above — `rebase_user`, `app.uid = 'service'`, policies evaluated —
|
|
23
|
+
* and clears the default policies through their admin arm rather than the
|
|
24
|
+
* `auth.uid() IS NULL` one.
|
|
19
25
|
*
|
|
20
26
|
* This module provides the three pieces:
|
|
21
27
|
*
|
|
@@ -32,8 +38,14 @@ import { SecurityRule } from "@rebasepro/types";
|
|
|
32
38
|
* self-creates the `auth` schema and functions) — enforcement is default-on,
|
|
33
39
|
* not an operator opt-in.
|
|
34
40
|
*/
|
|
35
|
-
/**
|
|
36
|
-
|
|
41
|
+
/**
|
|
42
|
+
* The restricted role every authenticated (user-context) request runs as.
|
|
43
|
+
*
|
|
44
|
+
* Re-exported, not re-declared: the same name is needed by
|
|
45
|
+
* `@rebasepro/common`'s internal-table revokes, and two spellings of a role name
|
|
46
|
+
* fail as a silent no-op rather than an error.
|
|
47
|
+
*/
|
|
48
|
+
export { REBASE_USER_ROLE };
|
|
37
49
|
/** Minimal SQL runner so callers can adapt drizzle or pg.Client. */
|
|
38
50
|
export type RawSqlRunner = (sqlText: string) => Promise<Record<string, unknown>[]>;
|
|
39
51
|
/** Minimal transaction surface needed by {@link applyAuthContext}. */
|
|
@@ -55,6 +67,34 @@ export interface AuthContext {
|
|
|
55
67
|
/** Raw roles as carried on the user (strings or `{ id }` objects). */
|
|
56
68
|
roles: unknown[];
|
|
57
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Warn when the connection role shares its name with an existing schema.
|
|
72
|
+
*
|
|
73
|
+
* Postgres resolves unqualified names through `search_path`, which defaults to
|
|
74
|
+
* `"$user", public` — and `$user` is the connection ROLE. When a schema of that
|
|
75
|
+
* name exists it sits ahead of `public`, so every unqualified statement
|
|
76
|
+
* silently operates on it instead:
|
|
77
|
+
*
|
|
78
|
+
* CREATE TABLE posts (...); -- you meant public.posts; you got <role>.posts
|
|
79
|
+
*
|
|
80
|
+
* Nothing errors. You get a second table of the same name in the wrong schema,
|
|
81
|
+
* and reads that pin `public` cannot see it — which reads as "missing table" and
|
|
82
|
+
* sends people to re-run a push that creates a *third* copy. The bootstrapper
|
|
83
|
+
* has a whole branch dedicated to recognising the symptom after the fact.
|
|
84
|
+
*
|
|
85
|
+
* Rebase shipped straight into this: it creates a schema named `rebase` while
|
|
86
|
+
* every template named the database role `rebase` too. The scaffold uses
|
|
87
|
+
* `rebase_app` now, and every pool Rebase opens pins `search_path=public`
|
|
88
|
+
* (`pinSearchPath`), which covers the paths the framework controls. This covers
|
|
89
|
+
* the ones it does not — `psql`, `pg_dump`, drizzle-kit, a colleague's script,
|
|
90
|
+
* a hand-written migration — because the hazard is a property of the two NAMES,
|
|
91
|
+
* not of any one connection.
|
|
92
|
+
*
|
|
93
|
+
* A warning rather than a boot failure: the database works, the framework's own
|
|
94
|
+
* traffic is pinned, and refusing to start over a naming choice a user may have
|
|
95
|
+
* inherited would be worse than the risk.
|
|
96
|
+
*/
|
|
97
|
+
export declare function warnOnRoleSchemaCollision(run: RawSqlRunner): Promise<void>;
|
|
58
98
|
export declare function detectConnectionPosture(run: RawSqlRunner): Promise<ConnectionPosture>;
|
|
59
99
|
/**
|
|
60
100
|
* Human-actionable instructions for when the connection cannot provision the
|
|
@@ -123,6 +163,22 @@ export declare function warnOnAnonymousGrants(collections: {
|
|
|
123
163
|
slug?: string;
|
|
124
164
|
securityRules?: readonly SecurityRule[];
|
|
125
165
|
}[]): void;
|
|
166
|
+
/**
|
|
167
|
+
* Name the collections whose raw policy SQL still calls the pre-1.0 helpers.
|
|
168
|
+
*
|
|
169
|
+
* The compiler rewrites `auth.uid()` to `rebase.uid()` on the way into the
|
|
170
|
+
* database, so nothing is broken and no policy is wrong — which is exactly why
|
|
171
|
+
* this has to be said out loud. A silent rewrite that works forever is not a
|
|
172
|
+
* migration, it is a second supported spelling nobody wrote down, and the next
|
|
173
|
+
* person to read those rules will copy the old one.
|
|
174
|
+
*
|
|
175
|
+
* Only `raw` expressions can carry it. Structured rules (`policy.authUid()`,
|
|
176
|
+
* `policy.rolesOverlap(...)`) compile from the model and were never affected.
|
|
177
|
+
*/
|
|
178
|
+
export declare function warnOnLegacyRlsFunctions(collections: {
|
|
179
|
+
slug?: string;
|
|
180
|
+
securityRules?: readonly SecurityRule[];
|
|
181
|
+
}[]): void;
|
|
126
182
|
/**
|
|
127
183
|
* Reject `pgRoles` that this server can never satisfy.
|
|
128
184
|
*
|
|
@@ -64,6 +64,18 @@ export declare class FetchService {
|
|
|
64
64
|
* and skips rows rather than erroring. The guesses stay, last, for a
|
|
65
65
|
* caller that hands over no collection to resolve against.
|
|
66
66
|
*/
|
|
67
|
+
/**
|
|
68
|
+
* The ORDER BY target, which may be relevance rather than a column.
|
|
69
|
+
*
|
|
70
|
+
* `_score` is only meaningful for a collection that declared a `search`
|
|
71
|
+
* block *and* for a request that carried a search string — ranking rows
|
|
72
|
+
* against no query ranks them all at zero. Outside those two conditions it
|
|
73
|
+
* is an unknown field and gets the same 400 as any other typo, which is the
|
|
74
|
+
* behaviour that matters: a sort that is silently dropped returns 200 with
|
|
75
|
+
* rows in arbitrary order, and paging over that repeats and skips rows.
|
|
76
|
+
*/
|
|
77
|
+
static readonly SCORE_FIELD = "_score";
|
|
78
|
+
private resolveOrderTarget;
|
|
67
79
|
private resolveOrderByField;
|
|
68
80
|
/**
|
|
69
81
|
* Build the `with` config for Drizzle's relational query API.
|
|
@@ -132,6 +144,7 @@ export declare class FetchService {
|
|
|
132
144
|
offset?: number;
|
|
133
145
|
startAfter?: Record<string, unknown>;
|
|
134
146
|
searchString?: string;
|
|
147
|
+
searchExplain?: boolean;
|
|
135
148
|
databaseId?: string;
|
|
136
149
|
vectorSearch?: VectorSearchParams;
|
|
137
150
|
logical?: LogicalCondition;
|
|
@@ -153,6 +166,16 @@ export declare class FetchService {
|
|
|
153
166
|
*/
|
|
154
167
|
fetchCollection<M extends Record<string, unknown>>(collectionPath: string, options?: {
|
|
155
168
|
filter?: FilterValues<Extract<keyof M, string>>;
|
|
169
|
+
/**
|
|
170
|
+
* An `or(...)`/`and(...)` group, applied alongside `filter`.
|
|
171
|
+
*
|
|
172
|
+
* `fetchRowsWithConditions` below has always applied this; it was
|
|
173
|
+
* simply absent from this signature, so the only callers that could
|
|
174
|
+
* pass one were the ones that went around this method. Realtime
|
|
175
|
+
* came through here, which is why a subscription filtered by a
|
|
176
|
+
* logical group was pushed every row in the table.
|
|
177
|
+
*/
|
|
178
|
+
logical?: LogicalCondition;
|
|
156
179
|
orderBy?: string;
|
|
157
180
|
order?: "desc" | "asc";
|
|
158
181
|
limit?: number;
|
|
@@ -167,10 +190,21 @@ export declare class FetchService {
|
|
|
167
190
|
*/
|
|
168
191
|
searchRows<M extends Record<string, unknown>>(collectionPath: string, searchString: string, options?: {
|
|
169
192
|
filter?: FilterValues<Extract<keyof M, string>>;
|
|
193
|
+
/**
|
|
194
|
+
* An `or(...)`/`and(...)` group, applied alongside `filter`.
|
|
195
|
+
*
|
|
196
|
+
* `fetchRowsWithConditions` has always applied one; it was missing
|
|
197
|
+
* from this signature, so a realtime search subscription carrying a
|
|
198
|
+
* group could not pass it on and served every row matching the text
|
|
199
|
+
* that RLS allowed.
|
|
200
|
+
*/
|
|
201
|
+
logical?: LogicalCondition;
|
|
170
202
|
orderBy?: string;
|
|
171
203
|
order?: "desc" | "asc";
|
|
172
204
|
limit?: number;
|
|
173
205
|
databaseId?: string;
|
|
206
|
+
/** Ask each row which declared search field matched. */
|
|
207
|
+
searchExplain?: boolean;
|
|
174
208
|
}): Promise<Record<string, unknown>[]>;
|
|
175
209
|
/**
|
|
176
210
|
* Count rows in a collection
|
|
@@ -226,13 +260,6 @@ export declare class FetchService {
|
|
|
226
260
|
* Note: Primary path now uses inline `getQueryBuilder()` checks.
|
|
227
261
|
*/
|
|
228
262
|
private hasDrizzleQueryAPI;
|
|
229
|
-
/**
|
|
230
|
-
* Attempt to use Drizzle's relational query API (db.query.<table>.findMany)
|
|
231
|
-
* for efficient JOIN-based relation loading.
|
|
232
|
-
* Returns null if the API is not available or the query fails.
|
|
233
|
-
* Note: Primary path now uses `buildWithConfig` + `buildDrizzleQueryOptions`.
|
|
234
|
-
*/
|
|
235
|
-
private fetchWithDrizzleQuery;
|
|
236
263
|
/**
|
|
237
264
|
* Fallback path used when db.query is unavailable.
|
|
238
265
|
* The primary path uses db.query.findMany with `with` config, which
|
|
@@ -149,6 +149,36 @@ export declare class RelationService {
|
|
|
149
149
|
* Uses a single SQL query with IN clause to avoid N+1.
|
|
150
150
|
*/
|
|
151
151
|
batchFetchRelatedEntitiesMany(parentCollectionPath: string, parentIds: (string | number)[], _relationKey: string, relation: ResolvedRelation): Promise<Map<string, RelatedRow<Record<string, unknown>>[]>>;
|
|
152
|
+
/**
|
|
153
|
+
* Bring one row's junction links in line with the ids a save carried,
|
|
154
|
+
* by diffing against what is stored rather than replacing the lot.
|
|
155
|
+
*
|
|
156
|
+
* The old shape was `DELETE every link for this parent` followed by
|
|
157
|
+
* `INSERT what the client sent`, which makes a save of the parent a full
|
|
158
|
+
* replacement of the membership set from a list the browser assembled out
|
|
159
|
+
* of a read it did earlier. Three things follow from that, and the diff
|
|
160
|
+
* closes all three:
|
|
161
|
+
*
|
|
162
|
+
* - **Lost update.** Two editors with post 7 open: A adds tag X and saves;
|
|
163
|
+
* B saves any field and the whole set is rewritten from B's older list,
|
|
164
|
+
* dropping X with nothing to show for it. A diff only names the ids that
|
|
165
|
+
* actually changed, so edits to disjoint tags no longer collide.
|
|
166
|
+
* - **Partial read, partial delete.** The read that fills the form runs
|
|
167
|
+
* under RLS, so a user who may edit the parent but cannot *see* some of
|
|
168
|
+
* the linked rows gets a shorter list — and writing it back deleted the
|
|
169
|
+
* links they were never shown. The DELETE now names ids instead of
|
|
170
|
+
* "everything for this parent", and the select that produces them runs
|
|
171
|
+
* in this same transaction under the same policies, so a link the caller
|
|
172
|
+
* cannot read is in neither list and survives the save.
|
|
173
|
+
* - **Junction payload columns.** A junction carrying its own columns
|
|
174
|
+
* (`position`, `role`, `created_at`) lost them on every save, because
|
|
175
|
+
* every row was re-inserted with only the two keys. Untouched links are
|
|
176
|
+
* now left alone.
|
|
177
|
+
*
|
|
178
|
+
* The insert is `ON CONFLICT DO NOTHING` so that two sessions adding the
|
|
179
|
+
* same link concurrently is a no-op rather than a unique violation.
|
|
180
|
+
*/
|
|
181
|
+
private syncJunctionLinks;
|
|
152
182
|
/**
|
|
153
183
|
* Update many-to-many and junction relations
|
|
154
184
|
*/
|
|
@@ -34,6 +34,32 @@ export declare function getColumnMeta(col: AnyPgColumn): DrizzleColumnMeta;
|
|
|
34
34
|
*/
|
|
35
35
|
export declare function idCanAddressTable(id: string | number, table: PgTable, idInfoArray: PrimaryKeyInfo[]): boolean;
|
|
36
36
|
export declare function getCollectionByPath(collectionPath: string, registry: PostgresCollectionRegistry): CollectionConfig;
|
|
37
|
+
/**
|
|
38
|
+
* Reject a write naming something that is not a column of the table.
|
|
39
|
+
*
|
|
40
|
+
* Drizzle builds INSERT from `Object.entries(table[Symbol.Columns])` and UPDATE
|
|
41
|
+
* from `Object.keys(tableColumns)`, so a key the table does not carry is not
|
|
42
|
+
* rejected by anything — it is *left out of the statement*. The insert answers
|
|
43
|
+
* 201 having stored nothing under that name; the update, if the key was the
|
|
44
|
+
* only one, builds `update "posts" set where …` and Postgres raises a syntax
|
|
45
|
+
* error (SQLSTATE 42601), which is neither class 22 nor 23 and so surfaces as a
|
|
46
|
+
* 500 for what is a caller's typo.
|
|
47
|
+
*
|
|
48
|
+
* That makes this the last honest place to check, and the only one every write
|
|
49
|
+
* passes through. `assertKnownWriteFields` in the REST layer checks the same
|
|
50
|
+
* thing against the *config* and is skipped on four paths — `strictWrites:
|
|
51
|
+
* false`, a collection declaring no properties, an auth adapter that owns the
|
|
52
|
+
* body's shape, and a nested route whose target cannot be walked — and it never
|
|
53
|
+
* sees an in-process `rebase.data` write at all.
|
|
54
|
+
*
|
|
55
|
+
* It also gives `strictWrites: false` a truthful implementation. The flag is
|
|
56
|
+
* documented for "a column that really does exist which the config never
|
|
57
|
+
* declared", and skipping the config check alone could not deliver that: the
|
|
58
|
+
* value was dropped a layer later regardless. Skipping the config check and
|
|
59
|
+
* keeping this one does exactly what the flag says — the column must exist,
|
|
60
|
+
* the property need not.
|
|
61
|
+
*/
|
|
62
|
+
export declare function assertWritableColumns(values: Record<string, unknown>, table: PgTable, collectionPath: string): void;
|
|
37
63
|
export declare function getTableForCollection(collection: CollectionConfig, registry: PostgresCollectionRegistry): PgTable<any>;
|
|
38
64
|
/**
|
|
39
65
|
* The key columns a collection's rows are addressed by.
|
|
@@ -36,6 +36,8 @@ export declare class DataService implements DataRepository {
|
|
|
36
36
|
*/
|
|
37
37
|
fetchCollection<M extends Record<string, unknown>>(collectionPath: string, options?: {
|
|
38
38
|
filter?: FilterValues<Extract<keyof M, string>>;
|
|
39
|
+
/** An `or(...)`/`and(...)` group, applied alongside `filter`. */
|
|
40
|
+
logical?: LogicalCondition;
|
|
39
41
|
orderBy?: string;
|
|
40
42
|
order?: "desc" | "asc";
|
|
41
43
|
limit?: number;
|
|
@@ -50,10 +52,13 @@ export declare class DataService implements DataRepository {
|
|
|
50
52
|
*/
|
|
51
53
|
searchRows<M extends Record<string, unknown>>(collectionPath: string, searchString: string, options?: {
|
|
52
54
|
filter?: FilterValues<Extract<keyof M, string>>;
|
|
55
|
+
/** An `or(...)`/`and(...)` group, applied alongside `filter`. */
|
|
56
|
+
logical?: LogicalCondition;
|
|
53
57
|
orderBy?: string;
|
|
54
58
|
order?: "desc" | "asc";
|
|
55
59
|
limit?: number;
|
|
56
60
|
databaseId?: string;
|
|
61
|
+
searchExplain?: boolean;
|
|
57
62
|
}): Promise<Record<string, unknown>[]>;
|
|
58
63
|
/**
|
|
59
64
|
* Count rows in a collection
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { WebSocket } from "ws";
|
|
2
2
|
import { EventEmitter } from "events";
|
|
3
|
-
import { DataDriver, WebSocketMessage } from "@rebasepro/types";
|
|
3
|
+
import { DataDriver, WebSocketMessage, LogicalCondition } from "@rebasepro/types";
|
|
4
4
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
5
5
|
import { RealtimeProvider, CollectionSubscriptionConfig, SingleSubscriptionConfig } from "../interfaces";
|
|
6
6
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
@@ -14,6 +14,57 @@ export interface SubscriptionAuthContext {
|
|
|
14
14
|
uid: string;
|
|
15
15
|
roles: string[];
|
|
16
16
|
}
|
|
17
|
+
/** What a channel frame is asking to do. */
|
|
18
|
+
export type ChannelAction = "join" | "broadcast" | "presence" | "history";
|
|
19
|
+
/** Everything an authorizer is told about the frame it is asked to allow. */
|
|
20
|
+
export interface ChannelAuthorizationRequest {
|
|
21
|
+
/** The channel the frame names, exactly as the client wrote it. */
|
|
22
|
+
channel: string;
|
|
23
|
+
action: ChannelAction;
|
|
24
|
+
/** The socket, not the principal — one user may hold several. */
|
|
25
|
+
clientId: string;
|
|
26
|
+
/** The socket's authenticated principal, or the anonymous one. */
|
|
27
|
+
user?: SubscriptionAuthContext;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The extension point for channel access rules.
|
|
31
|
+
*
|
|
32
|
+
* **This is deliberately not a product API yet.** The rule *language* — a
|
|
33
|
+
* config key, a per-pattern DSL, how it composes with `securityRules` — is an
|
|
34
|
+
* open design question (see `docs/channel-authorization.md`), and
|
|
35
|
+
* inventing one here would be inventing the answer. What exists is the single
|
|
36
|
+
* place every channel frame passes through, so that whatever shape the rules
|
|
37
|
+
* eventually take has exactly one seam to plug into and no arm of the switch
|
|
38
|
+
* can be forgotten.
|
|
39
|
+
*
|
|
40
|
+
* Returning `false` — or throwing — refuses the frame. It is consulted *after*
|
|
41
|
+
* the membership floor below, so an authorizer can only ever narrow access,
|
|
42
|
+
* never widen it.
|
|
43
|
+
*/
|
|
44
|
+
export type ChannelAuthorizer = (request: ChannelAuthorizationRequest) => boolean | Promise<boolean>;
|
|
45
|
+
/**
|
|
46
|
+
* The narrowing a collection subscription was created with, kept so that every
|
|
47
|
+
* refetch answers the same query the initial fetch did.
|
|
48
|
+
*
|
|
49
|
+
* Named once because it used to be written out inline in five places, and a
|
|
50
|
+
* field missing from one of them is accepted over the wire and then silently
|
|
51
|
+
* ignored: `offset` was declared on the incoming props and never stored, so a
|
|
52
|
+
* live list on page three served page one, and `logical` was never stored
|
|
53
|
+
* either, so an `or(...)` subscription was pushed every row in the table.
|
|
54
|
+
*/
|
|
55
|
+
type StoredCollectionRequest = {
|
|
56
|
+
filter?: Record<string, unknown>;
|
|
57
|
+
logical?: LogicalCondition;
|
|
58
|
+
orderBy?: string;
|
|
59
|
+
order?: "desc" | "asc";
|
|
60
|
+
limit?: number;
|
|
61
|
+
offset?: number;
|
|
62
|
+
startAfter?: Record<string, unknown>;
|
|
63
|
+
databaseId?: string;
|
|
64
|
+
searchString?: string;
|
|
65
|
+
/** Ask each row which declared search field matched — populates `_matches`. */
|
|
66
|
+
searchExplain?: boolean;
|
|
67
|
+
};
|
|
17
68
|
/**
|
|
18
69
|
* PostgreSQL-specific realtime service.
|
|
19
70
|
* Handles WebSocket connections and subscriptions for real-time row updates.
|
|
@@ -23,6 +74,12 @@ export interface SubscriptionAuthContext {
|
|
|
23
74
|
export declare class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
24
75
|
private db;
|
|
25
76
|
private registry;
|
|
77
|
+
/**
|
|
78
|
+
* Declares to the multi-engine router that channel frames can be handled
|
|
79
|
+
* here. Read by `createRoutedRealtimeService`, which otherwise would have to
|
|
80
|
+
* guess — and guessed "the default provider", whichever engine that is.
|
|
81
|
+
*/
|
|
82
|
+
readonly supportsChannels = true;
|
|
26
83
|
private clients;
|
|
27
84
|
private channels;
|
|
28
85
|
private presence;
|
|
@@ -69,6 +126,23 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
69
126
|
* so a hot channel logs the problem once rather than once per message.
|
|
70
127
|
*/
|
|
71
128
|
private oversizedBroadcastWarned;
|
|
129
|
+
/**
|
|
130
|
+
* Optional narrowing on top of the membership floor — see
|
|
131
|
+
* {@link ChannelAuthorizer}. Unset by default, which leaves membership as
|
|
132
|
+
* the whole of the rule.
|
|
133
|
+
*/
|
|
134
|
+
private channelAuthorizer?;
|
|
135
|
+
/**
|
|
136
|
+
* Whether a notification from another instance has ever arrived.
|
|
137
|
+
*
|
|
138
|
+
* The entity LISTEN handler sees a foreign `sid` on every cross-instance
|
|
139
|
+
* change, which is proof that this deployment runs more than one pod — the
|
|
140
|
+
* one fact needed to tell "the memory bus is fine here" from "broadcast and
|
|
141
|
+
* presence silently reach a fraction of your users".
|
|
142
|
+
*/
|
|
143
|
+
private foreignInstanceSeen;
|
|
144
|
+
/** So the multi-pod memory-bus warning is emitted once, not once per join. */
|
|
145
|
+
private memoryBusWarned;
|
|
72
146
|
private presenceInterval?;
|
|
73
147
|
private static readonly PRESENCE_TIMEOUT_MS;
|
|
74
148
|
/** How often stale roster rows from other instances are reaped. */
|
|
@@ -128,16 +202,7 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
128
202
|
type: "collection" | "single";
|
|
129
203
|
path: string;
|
|
130
204
|
id?: string | number;
|
|
131
|
-
collectionRequest?:
|
|
132
|
-
filter?: Record<string, unknown>;
|
|
133
|
-
orderBy?: string;
|
|
134
|
-
order?: "desc" | "asc";
|
|
135
|
-
limit?: number;
|
|
136
|
-
offset?: number;
|
|
137
|
-
startAfter?: Record<string, unknown>;
|
|
138
|
-
databaseId?: string;
|
|
139
|
-
searchString?: string;
|
|
140
|
-
};
|
|
205
|
+
collectionRequest?: StoredCollectionRequest;
|
|
141
206
|
authContext?: SubscriptionAuthContext;
|
|
142
207
|
}>;
|
|
143
208
|
registerDataDriverSubscription(subscriptionId: string, subscription: {
|
|
@@ -145,16 +210,7 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
145
210
|
type: "collection" | "single";
|
|
146
211
|
path: string;
|
|
147
212
|
id?: string | number;
|
|
148
|
-
collectionRequest?:
|
|
149
|
-
filter?: Record<string, unknown>;
|
|
150
|
-
orderBy?: string;
|
|
151
|
-
order?: "desc" | "asc";
|
|
152
|
-
limit?: number;
|
|
153
|
-
offset?: number;
|
|
154
|
-
startAfter?: Record<string, unknown>;
|
|
155
|
-
databaseId?: string;
|
|
156
|
-
searchString?: string;
|
|
157
|
-
};
|
|
213
|
+
collectionRequest?: StoredCollectionRequest;
|
|
158
214
|
authContext?: SubscriptionAuthContext;
|
|
159
215
|
}): void;
|
|
160
216
|
addSubscriptionCallback(subscriptionId: string, callback: (data: Record<string, unknown>[] | Record<string, unknown> | null) => void): void;
|
|
@@ -242,8 +298,61 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
242
298
|
* Returns ["posts", "posts/70"] for the example above
|
|
243
299
|
*/
|
|
244
300
|
private getParentPaths;
|
|
301
|
+
/**
|
|
302
|
+
* Install a channel authorizer — see {@link ChannelAuthorizer}.
|
|
303
|
+
*
|
|
304
|
+
* Nothing in the framework calls this yet: it is the seam a rules API will
|
|
305
|
+
* be built on, kept deliberately separate from the membership floor so the
|
|
306
|
+
* floor holds whether or not anyone uses it.
|
|
307
|
+
*/
|
|
308
|
+
setChannelAuthorizer(authorizer: ChannelAuthorizer | undefined): void;
|
|
309
|
+
/** Which action each channel frame is asking to perform. */
|
|
310
|
+
private static readonly CHANNEL_ACTIONS;
|
|
311
|
+
/**
|
|
312
|
+
* The one door every channel frame comes through.
|
|
313
|
+
*
|
|
314
|
+
* Returns synchronously — and so dispatches synchronously — unless an
|
|
315
|
+
* authorizer is installed. That matters: a client sends `join_channel`,
|
|
316
|
+
* `presence_state` and `channel_history` back to back on connect, and the
|
|
317
|
+
* socket's message handler processes each frame up to its first `await`,
|
|
318
|
+
* so a gate that always yielded would let the reads overtake the join that
|
|
319
|
+
* is about to authorize them.
|
|
320
|
+
*/
|
|
321
|
+
private handleChannelMessage;
|
|
322
|
+
/** Perform an already-authorized channel frame. */
|
|
323
|
+
private dispatchChannelMessage;
|
|
324
|
+
/**
|
|
325
|
+
* Decide whether a client may perform an action on a channel.
|
|
326
|
+
*
|
|
327
|
+
* **Membership is the floor.** Reading a channel's presence roster, replaying
|
|
328
|
+
* its retained history and broadcasting into it all require that this client
|
|
329
|
+
* has joined it. That is a low bar — joining is open to anyone who can name
|
|
330
|
+
* the channel — but it is not the bar that was there before, which was none
|
|
331
|
+
* at all: `channel_history` and `presence_state` answered any socket about
|
|
332
|
+
* any channel, and a broadcast fanned out to members the sender had never
|
|
333
|
+
* joined. Two internal tables (`rebase.channel_presence`,
|
|
334
|
+
* `rebase.channel_messages`) are held outside RLS on the strength of this
|
|
335
|
+
* check, so it fails closed: an authorizer that throws refuses the frame.
|
|
336
|
+
*
|
|
337
|
+
* Anything richer than membership belongs in a {@link ChannelAuthorizer};
|
|
338
|
+
* this method is where it is consulted, and the only place.
|
|
339
|
+
*/
|
|
340
|
+
private authorizeChannelAction;
|
|
341
|
+
/** Tell the client why its channel frame went nowhere, and say so in the log. */
|
|
342
|
+
private denyChannelAction;
|
|
245
343
|
/** Join a broadcast channel */
|
|
246
344
|
joinChannel(clientId: string, channel: string): void;
|
|
345
|
+
/**
|
|
346
|
+
* Say something the first time channels are used on a deployment that is
|
|
347
|
+
* demonstrably multi-pod while the bus is still the in-memory default.
|
|
348
|
+
*
|
|
349
|
+
* Every other warning in this subsystem covers a *configured* bus failing —
|
|
350
|
+
* the case where the operator already knew a bus mattered. The common
|
|
351
|
+
* misconfiguration is the opposite one: scaled to two replicas, never
|
|
352
|
+
* touched `realtime.bus`, and broadcast and presence quietly serve a
|
|
353
|
+
* fraction of the room. The evidence is already in the process, so use it.
|
|
354
|
+
*/
|
|
355
|
+
private warnIfMemoryBusOnMultiplePods;
|
|
247
356
|
/** Leave a broadcast channel */
|
|
248
357
|
leaveChannel(clientId: string, channel: string): void;
|
|
249
358
|
/**
|
|
@@ -497,3 +606,4 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
497
606
|
* This allows code to use PostgresRealtimeProvider alongside future MongoRealtimeProvider, etc.
|
|
498
607
|
*/
|
|
499
608
|
export declare const PostgresRealtimeProvider: typeof RealtimeService;
|
|
609
|
+
export {};
|
|
@@ -231,6 +231,95 @@ function getDeclaredSubcollections(collection) {
|
|
|
231
231
|
return collection.subcollections;
|
|
232
232
|
}
|
|
233
233
|
//#endregion
|
|
234
|
-
|
|
234
|
+
//#region ../types/src/types/rls-functions.ts
|
|
235
|
+
/**
|
|
236
|
+
* The SQL helper functions RLS policies call, and the schema they live in.
|
|
237
|
+
*
|
|
238
|
+
* ## One schema, and it is ours
|
|
239
|
+
*
|
|
240
|
+
* Rebase creates exactly one schema in a project's database: `rebase`. These
|
|
241
|
+
* three functions live in it alongside the framework's own tables, and that is
|
|
242
|
+
* the whole contract — a reader can look at a database and know precisely which
|
|
243
|
+
* namespace belongs to the framework and that nothing else was touched.
|
|
244
|
+
*
|
|
245
|
+
* It used to be two. `uid()`, `jwt()` and `roles()` sat in a schema called
|
|
246
|
+
* `auth`, which is Supabase's name, chosen so that a developer who had written
|
|
247
|
+
* Supabase RLS would recognise `auth.uid()`. The familiarity was real but the
|
|
248
|
+
* name was not Rebase's to take, and taking it had a concrete cost: pointing
|
|
249
|
+
* Rebase at a database that already had a Supabase `auth` schema meant
|
|
250
|
+
* `CREATE OR REPLACE FUNCTION auth.uid() RETURNS text` against Supabase's
|
|
251
|
+
* `RETURNS uuid`, which Postgres rejects outright —
|
|
252
|
+
*
|
|
253
|
+
* ERROR: cannot change return type of existing function
|
|
254
|
+
* HINT: Use DROP FUNCTION auth.uid() first.
|
|
255
|
+
*
|
|
256
|
+
* — and the failure landed inside a catch-all that logged a warning and carried
|
|
257
|
+
* on, leaving a database with auth tables, no helper functions, and policies
|
|
258
|
+
* calling functions that did not exist. Under `rebase db migrate` the same
|
|
259
|
+
* statements aborted the migration instead.
|
|
260
|
+
*
|
|
261
|
+
* `rebase.uid()` collides with nobody. A Supabase database keeps its `auth`
|
|
262
|
+
* schema untouched and gains a `rebase` one, which is what a gradual migration
|
|
263
|
+
* needs.
|
|
264
|
+
*
|
|
265
|
+
* ## Why functions at all, rather than inlining `current_setting`
|
|
266
|
+
*
|
|
267
|
+
* Because the indirection has already been spent once. `uid()` resolves
|
|
268
|
+
* `app.uid` and falls back to the pre-rename `app.user_id`, so that during a
|
|
269
|
+
* rolling deploy — old and new pods serving one database — both eras resolve
|
|
270
|
+
* the principal. That was a single `CREATE OR REPLACE`. Inlined into policy
|
|
271
|
+
* bodies it would have been a rewrite of every policy on every table.
|
|
272
|
+
*
|
|
273
|
+
* ## Why the name is not configurable
|
|
274
|
+
*
|
|
275
|
+
* A policy body is stored SQL: Postgres parses `USING (…)` once and keeps it, so
|
|
276
|
+
* these strings are written into every policy in every database Rebase has
|
|
277
|
+
* provisioned. Everything that reads policies back — the SQL-to-policy parser
|
|
278
|
+
* behind the admin UI, the drift checker, `rls-check` — would have to know the
|
|
279
|
+
* configured value to recognise its own output. One frozen name is the feature.
|
|
280
|
+
*/
|
|
281
|
+
/** The schema Rebase owns. The only schema Rebase creates. */
|
|
282
|
+
var REBASE_SCHEMA = "rebase";
|
|
283
|
+
/**
|
|
284
|
+
* The principal of the current request, as text, or NULL in the server context.
|
|
285
|
+
*
|
|
286
|
+
* Never NULL for a user request — an anonymous one carries
|
|
287
|
+
* {@link ANONYMOUS_USER_ID} — which is what makes `IS NULL` a reliable test for
|
|
288
|
+
* the trusted server plane and `IS NOT NULL` a tautology.
|
|
289
|
+
*/
|
|
290
|
+
var RLS_UID_SQL = `${REBASE_SCHEMA}.uid()`;
|
|
291
|
+
/** The request's roles as a comma-separated string, for `string_to_array`. */
|
|
292
|
+
var RLS_ROLES_SQL = `${REBASE_SCHEMA}.roles()`;
|
|
293
|
+
`${REBASE_SCHEMA}`;
|
|
294
|
+
/**
|
|
295
|
+
* The pre-1.0 spellings, for recognising policies and hand-written SQL that
|
|
296
|
+
* predate the move.
|
|
297
|
+
*
|
|
298
|
+
* Kept because policies outlive the server that wrote them: a database migrated
|
|
299
|
+
* by an older release still holds `auth.uid()` in its policy bodies until the
|
|
300
|
+
* next push or boot recompiles them, and anything that reads policies back has
|
|
301
|
+
* to recognise both eras or report the framework's own output as foreign drift.
|
|
302
|
+
* Also used to give a project whose `securityRules` contain raw `auth.uid()` a
|
|
303
|
+
* message naming the replacement, instead of a parse failure.
|
|
304
|
+
*/
|
|
305
|
+
var LEGACY_RLS_SCHEMA = "auth";
|
|
306
|
+
`${LEGACY_RLS_SCHEMA}`;
|
|
307
|
+
`${LEGACY_RLS_SCHEMA}`;
|
|
308
|
+
`${LEGACY_RLS_SCHEMA}`;
|
|
309
|
+
/**
|
|
310
|
+
* Rewrites the pre-1.0 function calls in a fragment of policy SQL.
|
|
311
|
+
*
|
|
312
|
+
* Deliberately anchored on a word boundary and the schema qualifier, so a column
|
|
313
|
+
* called `auth_uid` or a table named `auth` is left alone.
|
|
314
|
+
*/
|
|
315
|
+
function rewriteLegacyRlsFunctions(sql) {
|
|
316
|
+
return sql.replace(/\bauth\.(uid|jwt|roles)\s*\(\s*\)/gi, (_match, fn) => `${REBASE_SCHEMA}.${fn.toLowerCase()}()`);
|
|
317
|
+
}
|
|
318
|
+
/** Whether a fragment of SQL still calls the pre-1.0 functions. */
|
|
319
|
+
function usesLegacyRlsFunctions(sql) {
|
|
320
|
+
return /\bauth\.(uid|jwt|roles)\s*\(\s*\)/i.test(sql);
|
|
321
|
+
}
|
|
322
|
+
//#endregion
|
|
323
|
+
export { rewriteLegacyRlsFunctions as a, isPostgresCollectionConfig as c, getDataSourceCapabilities as d, NULL_OPS as f, RLS_UID_SQL as i, isRelationalCollectionConfig as l, toCanonicalOp as m, REBASE_SCHEMA as n, usesLegacyRlsFunctions as o, REST_TO_CANONICAL as p, RLS_ROLES_SQL as r, getDeclaredSubcollections as s, LEGACY_RLS_SCHEMA as t, DEFAULT_DATA_SOURCE_KEY as u };
|
|
235
324
|
|
|
236
|
-
//# sourceMappingURL=src-
|
|
325
|
+
//# sourceMappingURL=src-C_wvdMnl.js.map
|