@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.
Files changed (85) hide show
  1. package/dist/PostgresBackendDriver.d.ts +48 -1
  2. package/dist/auth/services.d.ts +19 -0
  3. package/dist/{src-DlPBctw_.js → auth-users-columns-CBEOeYqa.js} +835 -63
  4. package/dist/auth-users-columns-CBEOeYqa.js.map +1 -0
  5. package/dist/{backup-service-CD8o_1Sl.js → backup-service-Bww-Lg0s.js} +2 -2
  6. package/dist/{backup-service-CD8o_1Sl.js.map → backup-service-Bww-Lg0s.js.map} +1 -1
  7. package/dist/cli-helpers.d.ts +57 -1
  8. package/dist/data-transformer.d.ts +7 -2
  9. package/dist/data_driver-ULAyJEi9.js +193 -0
  10. package/dist/data_driver-ULAyJEi9.js.map +1 -0
  11. package/dist/{ensure-collection-policies-ViG8XiPn.js → ensure-collection-policies-B_JMGa5K.js} +2 -2
  12. package/dist/{ensure-collection-policies-ViG8XiPn.js.map → ensure-collection-policies-B_JMGa5K.js.map} +1 -1
  13. package/dist/{ensure-collection-tables-CBQdOETu.js → ensure-collection-tables-DzeTEvMv.js} +170 -20
  14. package/dist/ensure-collection-tables-DzeTEvMv.js.map +1 -0
  15. package/dist/index.es.js +1365 -338
  16. package/dist/index.es.js.map +1 -1
  17. package/dist/rls-bootstrap-sql-Bpv3nUZo.js +244 -0
  18. package/dist/rls-bootstrap-sql-Bpv3nUZo.js.map +1 -0
  19. package/dist/schema/auth-schema.d.ts +102 -0
  20. package/dist/schema/auth-users-columns.d.ts +97 -0
  21. package/dist/schema/doctor-policy-checks.d.ts +28 -0
  22. package/dist/schema/doctor.d.ts +23 -25
  23. package/dist/schema/ensure-collection-tables.d.ts +61 -7
  24. package/dist/schema/generate-drizzle-schema-logic.d.ts +10 -2
  25. package/dist/schema/generate-postgres-ddl-logic.d.ts +53 -5
  26. package/dist/schema/generated-schema-staleness.d.ts +39 -0
  27. package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
  28. package/dist/schema/search-column.d.ts +248 -0
  29. package/dist/security/rls-enforcement.d.ts +61 -5
  30. package/dist/services/FetchService.d.ts +34 -7
  31. package/dist/services/RelationService.d.ts +30 -0
  32. package/dist/services/collection-helpers.d.ts +26 -0
  33. package/dist/services/dataService.d.ts +5 -0
  34. package/dist/services/realtimeService.d.ts +131 -21
  35. package/dist/{src-DoU9yPqq.js → src-C_wvdMnl.js} +91 -2
  36. package/dist/src-C_wvdMnl.js.map +1 -0
  37. package/dist/utils/drizzle-conditions.d.ts +124 -2
  38. package/dist/{websocket-B2LsrINK.js → websocket-D1qbmLZ2.js} +75 -18
  39. package/dist/websocket-D1qbmLZ2.js.map +1 -0
  40. package/package.json +9 -8
  41. package/src/PostgresBackendDriver.ts +172 -6
  42. package/src/PostgresBootstrapper.ts +55 -6
  43. package/src/auth/ensure-tables.ts +207 -86
  44. package/src/auth/services.ts +69 -5
  45. package/src/cli-helpers.ts +129 -10
  46. package/src/cli.ts +232 -30
  47. package/src/collections/validate-relations.ts +124 -17
  48. package/src/data-transformer.ts +120 -17
  49. package/src/history/ensure-history-table.ts +7 -0
  50. package/src/schema/auth-schema.ts +17 -1
  51. package/src/schema/auth-users-columns.ts +131 -0
  52. package/src/schema/doctor-cli.ts +12 -63
  53. package/src/schema/doctor-policy-checks.ts +105 -0
  54. package/src/schema/doctor.ts +135 -76
  55. package/src/schema/ensure-collection-tables.ts +374 -32
  56. package/src/schema/generate-drizzle-schema-logic.ts +132 -42
  57. package/src/schema/generate-postgres-ddl-logic.ts +294 -16
  58. package/src/schema/generate-postgres-ddl.ts +25 -2
  59. package/src/schema/generated-schema-staleness.ts +169 -0
  60. package/src/schema/introspect-db-logic.ts +66 -34
  61. package/src/schema/non-sql-collections.test.ts +131 -0
  62. package/src/schema/rls-bootstrap-sql.ts +288 -0
  63. package/src/schema/search-column.ts +643 -0
  64. package/src/security/anonymous-grants.test.ts +4 -2
  65. package/src/security/rls-enforcement.ts +149 -6
  66. package/src/services/BranchService.ts +5 -0
  67. package/src/services/FetchService.ts +175 -108
  68. package/src/services/PersistService.ts +38 -2
  69. package/src/services/RelationService.ts +110 -67
  70. package/src/services/channel-history.ts +14 -0
  71. package/src/services/channel-presence.ts +13 -0
  72. package/src/services/collection-helpers.ts +54 -1
  73. package/src/services/dataService.ts +5 -0
  74. package/src/services/realtimeService.ts +344 -79
  75. package/src/utils/drizzle-conditions.ts +365 -23
  76. package/src/utils/pg-error-utils.ts +8 -3
  77. package/src/websocket.ts +113 -16
  78. package/dist/ensure-collection-tables-CBQdOETu.js.map +0 -1
  79. package/dist/policy-CeA1JcxP.js +0 -105
  80. package/dist/policy-CeA1JcxP.js.map +0 -1
  81. package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
  82. package/dist/src-DlPBctw_.js.map +0 -1
  83. package/dist/src-DoU9yPqq.js.map +0 -1
  84. package/dist/websocket-B2LsrINK.js.map +0 -1
  85. 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
- * background jobs, and the explicit `rebase.dataAsAdmin` accessor. As table
17
- * owner it bypasses RLS. This is the trusted plane, equivalent to
18
- * Supabase's `service_role`.
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
- /** The restricted role every authenticated (user-context) request runs as. */
36
- export declare const REBASE_USER_ROLE = "rebase_user";
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
- export { getDataSourceCapabilities as a, toCanonicalOp as c, DEFAULT_DATA_SOURCE_KEY as i, isPostgresCollectionConfig as n, NULL_OPS as o, isRelationalCollectionConfig as r, REST_TO_CANONICAL as s, getDeclaredSubcollections as t };
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-DoU9yPqq.js.map
325
+ //# sourceMappingURL=src-C_wvdMnl.js.map