@rebasepro/server-postgres 0.13.0 → 0.13.1-canary.g1822133
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/PostgresBootstrapper.d.ts +26 -0
- package/dist/auth/services.d.ts +21 -0
- package/dist/{src-DlPBctw_.js → auth-users-columns-BfQHf9JE.js} +1222 -86
- package/dist/auth-users-columns-BfQHf9JE.js.map +1 -0
- package/dist/{backup-service-CD8o_1Sl.js → backup-service-BH0Dzo_h.js} +2 -3
- package/dist/{backup-service-CD8o_1Sl.js.map → backup-service-BH0Dzo_h.js.map} +1 -1
- package/dist/cli-helpers.d.ts +57 -1
- package/dist/cli-output.d.ts +34 -0
- 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-8vuu-n4r.js +124 -0
- package/dist/ensure-collection-policies-8vuu-n4r.js.map +1 -0
- package/dist/{ensure-collection-tables-CBQdOETu.js → ensure-collection-tables-CbvaGuVn.js} +170 -20
- package/dist/ensure-collection-tables-CbvaGuVn.js.map +1 -0
- package/dist/index.es.js +1815 -1023
- package/dist/index.es.js.map +1 -1
- package/dist/rls-bootstrap-sql-69hYT8nr.js +244 -0
- package/dist/rls-bootstrap-sql-69hYT8nr.js.map +1 -0
- package/dist/rls-enforcement-BJ_3wxwg.js +425 -0
- package/dist/rls-enforcement-BJ_3wxwg.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 +41 -25
- package/dist/schema/ensure-collection-policies.d.ts +33 -9
- 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/introspect-db-inference.d.ts +8 -1
- package/dist/schema/introspect-db-logic.d.ts +49 -0
- package/dist/schema/introspect-db-project.d.ts +21 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
- package/dist/schema/search-column.d.ts +248 -0
- package/dist/security/policy-drift.d.ts +34 -0
- package/dist/security/rls-enforcement.d.ts +61 -5
- package/dist/services/FetchService.d.ts +34 -7
- package/dist/services/PersistService.d.ts +21 -17
- package/dist/services/RelationService.d.ts +9 -57
- package/dist/services/RelationWriteService.d.ts +82 -0
- package/dist/services/collection-helpers.d.ts +42 -0
- package/dist/services/dataService.d.ts +5 -0
- package/dist/services/junction-writes.d.ts +82 -0
- package/dist/services/realtimeService.d.ts +164 -23
- package/dist/services/write-denial.d.ts +36 -0
- package/dist/{src-DoU9yPqq.js → src-DCdn3Val.js} +124 -3
- package/dist/src-DCdn3Val.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +124 -2
- package/dist/{websocket-B2LsrINK.js → websocket-C8ZqVBiV.js} +75 -18
- package/dist/websocket-C8ZqVBiV.js.map +1 -0
- package/package.json +9 -8
- package/src/PostgresBackendDriver.ts +172 -6
- package/src/PostgresBootstrapper.ts +136 -11
- package/src/auth/ensure-tables.ts +212 -91
- package/src/auth/services.ts +82 -5
- package/src/backup/backup-cli.ts +59 -57
- package/src/cli-errors.ts +6 -6
- package/src/cli-helpers.ts +132 -13
- package/src/cli-output.ts +43 -0
- package/src/cli.ts +371 -161
- package/src/collections/buildRegistry.ts +3 -1
- package/src/collections/validate-relations.ts +124 -17
- package/src/data-transformer.ts +142 -28
- package/src/history/ensure-history-table.ts +9 -2
- package/src/schema/auth-schema.ts +17 -1
- package/src/schema/auth-users-columns.ts +131 -0
- package/src/schema/doctor-cli.ts +14 -65
- package/src/schema/doctor-policy-checks.ts +105 -0
- package/src/schema/doctor.ts +156 -77
- package/src/schema/ensure-collection-policies.ts +99 -6
- package/src/schema/ensure-collection-tables.ts +374 -32
- package/src/schema/generate-drizzle-schema-logic.ts +152 -66
- package/src/schema/generate-drizzle-schema.ts +11 -10
- package/src/schema/generate-postgres-ddl-logic.ts +294 -16
- package/src/schema/generate-postgres-ddl.ts +38 -14
- package/src/schema/generated-schema-staleness.ts +171 -0
- package/src/schema/introspect-db-inference.ts +9 -2
- package/src/schema/introspect-db-logic.ts +251 -75
- package/src/schema/introspect-db-project.ts +78 -0
- package/src/schema/introspect-db.ts +42 -25
- package/src/schema/introspect-runtime.ts +14 -2
- 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/policy-drift.test.ts +104 -3
- package/src/security/policy-drift.ts +129 -7
- package/src/security/rls-enforcement.ts +150 -7
- package/src/services/BranchService.ts +5 -0
- package/src/services/FetchService.ts +253 -115
- package/src/services/PersistService.ts +82 -43
- package/src/services/RelationService.ts +37 -696
- package/src/services/RelationWriteService.ts +653 -0
- package/src/services/cdc/trigger-cdc.ts +5 -1
- package/src/services/channel-history.ts +14 -0
- package/src/services/channel-presence.ts +13 -0
- package/src/services/collection-helpers.ts +89 -4
- package/src/services/dataService.ts +5 -0
- package/src/services/junction-writes.ts +295 -0
- package/src/services/pg-notify-listener.ts +1 -1
- package/src/services/realtimeService.ts +382 -118
- package/src/services/write-denial.ts +55 -0
- package/src/utils/drizzle-conditions.ts +433 -35
- package/src/utils/pg-error-utils.ts +8 -3
- package/src/websocket.ts +113 -16
- package/dist/ensure-collection-policies-ViG8XiPn.js +0 -57
- package/dist/ensure-collection-policies-ViG8XiPn.js.map +0 -1
- 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
|
@@ -37,7 +37,7 @@ import { ensureHistoryTableExists } from "./history/ensure-history-table";
|
|
|
37
37
|
import { patchPgArrayNullSafety } from "./utils/pg-array-null-patch";
|
|
38
38
|
import { buildCollectionsFromSchema, introspectSchema, readRlsStatus } from "./schema/introspect-runtime";
|
|
39
39
|
import { buildDrizzleTablesFromSchema, buildDrizzleRelationsFromSchema } from "./schema/dynamic-tables";
|
|
40
|
-
import { detectConnectionPosture, ensureAppRole, validatePolicyPgRoles, warnOnAnonymousGrants, REBASE_USER_ROLE, type RawSqlRunner } from "./security/rls-enforcement";
|
|
40
|
+
import { detectConnectionPosture, ensureAppRole, validatePolicyPgRoles, warnOnAnonymousGrants, warnOnLegacyRlsFunctions, warnOnRoleSchemaCollision, REBASE_USER_ROLE, type RawSqlRunner } from "./security/rls-enforcement";
|
|
41
41
|
import { provisionTriggerCdc, type CdcTableRef } from "./services/cdc/trigger-cdc";
|
|
42
42
|
import { collectJunctionLinks } from "./services/cdc/junction-tables";
|
|
43
43
|
import { createChannelBus, resolveChannelBusSetting } from "./services/channel-bus";
|
|
@@ -137,6 +137,50 @@ export function resolveDriftCheckName(
|
|
|
137
137
|
?? col.slug;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Is this the local database `rebase init` scaffolds — i.e. the one case where
|
|
142
|
+
* "you are connected as a superuser" is not news?
|
|
143
|
+
*
|
|
144
|
+
* The scaffold's own `docker-compose.yml` sets `POSTGRES_USER: rebase_app`,
|
|
145
|
+
* which makes that role the cluster superuser, so the superuser advisory below
|
|
146
|
+
* was the only WARN a brand-new project ever saw and it was about a decision
|
|
147
|
+
* the tool had made for the developer.
|
|
148
|
+
*
|
|
149
|
+
* Of the two available fixes — provision a non-superuser table-owner role in
|
|
150
|
+
* the scaffold, or recognise the local shape and stay quiet — this is the
|
|
151
|
+
* second, because the first breaks the scaffold it is meant to improve: a
|
|
152
|
+
* non-superuser owner cannot `CREATE EXTENSION` (search collections need
|
|
153
|
+
* `pg_trgm`/`unaccent`, applied by `rebase db push` and again by the boot
|
|
154
|
+
* schema-ensure), so the very first `pnpm run db:push` on a scaffolded project
|
|
155
|
+
* with a search block would fail. Trading a working first run for a quieter log
|
|
156
|
+
* line is the wrong trade.
|
|
157
|
+
*
|
|
158
|
+
* The condition is deliberately narrow — a *non-production* process talking to
|
|
159
|
+
* a database on the loopback interface. A genuine production superuser
|
|
160
|
+
* connection still warns, and so does a non-production process pointed at a
|
|
161
|
+
* remote database (the usual "my dev machine writes to staging" mistake, where
|
|
162
|
+
* the advisory is exactly right). NODE_ENV alone would not do: the scaffold
|
|
163
|
+
* ships `NODE_ENV=development` and some deployments inherit it.
|
|
164
|
+
*/
|
|
165
|
+
export function isScaffoldedLocalDatabase(connectionString: string | undefined): boolean {
|
|
166
|
+
if (process.env.NODE_ENV === "production") return false;
|
|
167
|
+
if (!connectionString) return false;
|
|
168
|
+
let host: string;
|
|
169
|
+
try {
|
|
170
|
+
host = new URL(connectionString).hostname;
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
// `new URL` keeps IPv6 literals in brackets.
|
|
175
|
+
const bare = host.replace(/^\[|\]$/g, "").toLowerCase();
|
|
176
|
+
return bare === "localhost"
|
|
177
|
+
|| bare === "::1"
|
|
178
|
+
|| bare === "0.0.0.0"
|
|
179
|
+
|| bare === ""
|
|
180
|
+
|| /^127\./.test(bare)
|
|
181
|
+
|| bare.endsWith(".localhost");
|
|
182
|
+
}
|
|
183
|
+
|
|
140
184
|
/**
|
|
141
185
|
* Default PostgreSQL bootstrapper.
|
|
142
186
|
*
|
|
@@ -364,29 +408,46 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
|
|
|
364
408
|
// bypasses RLS. Detect the posture and, when privileged, provision
|
|
365
409
|
// the restricted `rebase_user` role and route authenticated
|
|
366
410
|
// requests (reads AND writes) through it. The server context (base
|
|
367
|
-
// driver /
|
|
368
|
-
// and bypasses.
|
|
411
|
+
// driver / auth flows / `rebase.sql`) stays on the owner connection
|
|
412
|
+
// and bypasses; `rebase.dataAsAdmin` does NOT — it is scoped with
|
|
413
|
+
// `withAuth({ uid: "service", roles: ["admin"] })` at boot, so it
|
|
414
|
+
// runs as `rebase_user` and its policies are evaluated.
|
|
415
|
+
// Default-on: a privileged connection that cannot be
|
|
369
416
|
// isolated fails the boot rather than serving unenforced requests.
|
|
370
417
|
{
|
|
371
418
|
const runSql: RawSqlRunner = async (text) => {
|
|
372
419
|
const res = await schemaAwareDb.execute(sql.raw(text));
|
|
373
420
|
return (res.rows ?? []) as Record<string, unknown>[];
|
|
374
421
|
};
|
|
422
|
+
// Said before anything else touches the schema: if the role and
|
|
423
|
+
// a schema share a name, unqualified SQL from any tool that does
|
|
424
|
+
// not pin `search_path` has been landing in the wrong place, and
|
|
425
|
+
// that is worth knowing before reading the drift report below.
|
|
426
|
+
await warnOnRoleSchemaCollision(runSql);
|
|
427
|
+
|
|
375
428
|
const posture = await detectConnectionPosture(runSql);
|
|
376
429
|
if (posture.privileged) {
|
|
377
430
|
const collectionSchemas = registry.getCollections()
|
|
378
431
|
.map((c) => (c as { schema?: string }).schema)
|
|
379
432
|
.filter((s): s is string => typeof s === "string");
|
|
380
|
-
|
|
433
|
+
// `auth` is deliberately absent: the RLS helpers moved into
|
|
434
|
+
// `rebase`, and granting USAGE on a schema Rebase does not
|
|
435
|
+
// own would, on a Supabase database, hand the end-user role
|
|
436
|
+
// access to theirs.
|
|
437
|
+
await ensureAppRole(runSql, ["public", "rebase", ...collectionSchemas]);
|
|
381
438
|
driver.rlsUserRole = REBASE_USER_ROLE;
|
|
382
439
|
realtimeService.rlsUserRole = REBASE_USER_ROLE;
|
|
383
440
|
logger.info(`🔐 RLS enforcement active: authenticated requests run as "${REBASE_USER_ROLE}" (connection "${posture.role}" bypasses RLS: ${posture.superuser ? "superuser" : posture.bypassRLS ? "BYPASSRLS" : "table owner"})`);
|
|
384
441
|
if (posture.superuser || posture.bypassRLS) {
|
|
385
|
-
|
|
386
|
-
|
|
442
|
+
const message =
|
|
443
|
+
`The database connection runs as ${posture.superuser ? "a superuser" : "a BYPASSRLS role"} ("${posture.role}"). ` +
|
|
387
444
|
`User requests are isolated via SET LOCAL ROLE, but connect as a non-superuser ` +
|
|
388
|
-
`table-owner role in production so the server/owner context is least-privilege
|
|
389
|
-
)
|
|
445
|
+
`table-owner role in production so the server/owner context is least-privilege.`;
|
|
446
|
+
if (isScaffoldedLocalDatabase(pgConfig.connectionString)) {
|
|
447
|
+
logger.debug(`🔐 ${message}`);
|
|
448
|
+
} else {
|
|
449
|
+
logger.warn(`⚠️ ${message}`);
|
|
450
|
+
}
|
|
390
451
|
}
|
|
391
452
|
} else {
|
|
392
453
|
logger.info(`🔐 RLS enforcement: connection role "${posture.role}" is subject to RLS natively; no role switch needed.`);
|
|
@@ -406,6 +467,11 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
|
|
|
406
467
|
// a rule that reads as "signed in only" but is true for every
|
|
407
468
|
// caller grants the data away rather than hiding it.
|
|
408
469
|
warnOnAnonymousGrants(registry.getCollections() as never);
|
|
470
|
+
|
|
471
|
+
// Raw policy SQL written against the pre-1.0 helper schema. It
|
|
472
|
+
// is rewritten on compile, so this is the only place the project
|
|
473
|
+
// is ever told the spelling moved.
|
|
474
|
+
warnOnLegacyRlsFunctions(registry.getCollections() as never);
|
|
409
475
|
}
|
|
410
476
|
|
|
411
477
|
// Ensure branch metadata table exists when branching is available
|
|
@@ -820,8 +886,15 @@ schemaHealthCheck: () => probeAuthSchema(db, resolveAuthSchema(authCollection))
|
|
|
820
886
|
);
|
|
821
887
|
for (const failure of plan.failures) {
|
|
822
888
|
logger.warn(
|
|
823
|
-
|
|
824
|
-
|
|
889
|
+
failure.kind === "comment-column"
|
|
890
|
+
// The stamp records which `search` block the generated
|
|
891
|
+
// column was built from. Without it the next boot cannot
|
|
892
|
+
// tell a changed block from an unchanged one and adopts
|
|
893
|
+
// the column again instead of refusing.
|
|
894
|
+
? `🔍 [schema] Could not record the search fingerprint on "${failure.target}" — search works, ` +
|
|
895
|
+
`but a later change to the \`search\` block will not be detected: ${failure.error}`
|
|
896
|
+
: `🔗 [schema] Could not add foreign key "${failure.target}" — the column exists and the ` +
|
|
897
|
+
`collection still serves, but rows are not policed by this constraint: ${failure.error}`
|
|
825
898
|
);
|
|
826
899
|
}
|
|
827
900
|
return { applied: plan.actions.length - plan.failures.length };
|
|
@@ -867,9 +940,61 @@ schemaHealthCheck: () => probeAuthSchema(db, resolveAuthSchema(authCollection))
|
|
|
867
940
|
}
|
|
868
941
|
for (const failure of outcome.failures) {
|
|
869
942
|
logger.warn(
|
|
870
|
-
`🔐 [rls] Could not fully apply policies to "${failure.table}" —
|
|
943
|
+
`🔐 [rls] Could not fully apply policies to "${failure.table}" — RLS is on, so it denies until this is resolved: ${failure.error}`
|
|
871
944
|
);
|
|
872
945
|
}
|
|
946
|
+
|
|
947
|
+
// RLS could not be turned on at all. The schema-wide grant to the
|
|
948
|
+
// user role has already run by this point, so without the revoke
|
|
949
|
+
// below the table is readable and writable by every authenticated
|
|
950
|
+
// request with no row filtering — the one state this boot path must
|
|
951
|
+
// never leave behind, and the one it used to describe as "locked".
|
|
952
|
+
const unrevoked = outcome.unsecured.filter(u => !u.grantWithdrawn);
|
|
953
|
+
for (const u of outcome.unsecured.filter(u => u.grantWithdrawn)) {
|
|
954
|
+
logger.error(
|
|
955
|
+
`🔐 [rls] Could not enable row-level security on "${u.table}": ${u.error}. ` +
|
|
956
|
+
`Its privileges have been revoked from ${REBASE_USER_ROLE}, so the table is ` +
|
|
957
|
+
"unreachable rather than unprotected. Reads and writes to that collection will " +
|
|
958
|
+
"fail until RLS can be enabled."
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
if (unrevoked.length > 0) {
|
|
962
|
+
// Neither securable nor closable. Serving here would mean
|
|
963
|
+
// handing every authenticated caller unfiltered access, so the
|
|
964
|
+
// boot fails instead — this is the one failure that is not
|
|
965
|
+
// survivable per-table.
|
|
966
|
+
throw new Error(
|
|
967
|
+
"Refusing to start: row-level security could not be enabled on " +
|
|
968
|
+
unrevoked.map(u => `"${u.table}" (${u.error})`).join(", ") +
|
|
969
|
+
`, and the privileges granted to ${REBASE_USER_ROLE} could not be revoked either. ` +
|
|
970
|
+
"The table would be served with no row filtering. Fix the database permissions " +
|
|
971
|
+
"(the connection role must own these tables, or be able to ALTER them) and boot again."
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Retire the pre-1.0 `auth` schema now that the policies above no
|
|
976
|
+
// longer call into it. Deliberately after, and deliberately quiet:
|
|
977
|
+
// Postgres refuses to drop a function an RLS policy still
|
|
978
|
+
// references, so on a database where some table has not been
|
|
979
|
+
// recompiled yet this is expected to fail and succeed on a later
|
|
980
|
+
// boot. See DROP_LEGACY_AUTH_SCHEMA_SQL for the guards that keep it
|
|
981
|
+
// off a Supabase `auth` schema.
|
|
982
|
+
try {
|
|
983
|
+
const { dropLegacyAuthSchema } = await import("./schema/rls-bootstrap-sql");
|
|
984
|
+
await dropLegacyAuthSchema(
|
|
985
|
+
async (text) => {
|
|
986
|
+
const res = await internals.db.execute(sql.raw(text));
|
|
987
|
+
return (res.rows ?? []) as Record<string, unknown>[];
|
|
988
|
+
},
|
|
989
|
+
{ info: (m) => logger.info(m), warn: (m) => logger.warn(m) }
|
|
990
|
+
);
|
|
991
|
+
} catch (err) {
|
|
992
|
+
logger.info(
|
|
993
|
+
"Left the legacy `auth` schema in place: " +
|
|
994
|
+
(err instanceof Error ? err.message : String(err))
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
|
|
873
998
|
return { applied: outcome.policiesApplied };
|
|
874
999
|
},
|
|
875
1000
|
|