@rebasepro/server-postgres 0.25.1 → 0.26.0
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/{BranchService-ClklxyAQ.js → BranchService-CWBnDhcv.js} +25 -9
- package/dist/BranchService-CWBnDhcv.js.map +1 -0
- package/dist/PostgresBackendDriver.d.ts +13 -0
- package/dist/PostgresBootstrapper.d.ts +7 -0
- package/dist/auth/services.d.ts +8 -1
- package/dist/auth/verify-auth-tables.d.ts +21 -0
- package/dist/{backup-cli-gijeXe0u.js → backup-cli-PoRG-zEq.js} +3 -3
- package/dist/{backup-cli-gijeXe0u.js.map → backup-cli-PoRG-zEq.js.map} +1 -1
- package/dist/{boot-ddl-DYH95Qeg.js → boot-ddl-EsgqDkx0.js} +2 -2
- package/dist/{boot-ddl-DYH95Qeg.js.map → boot-ddl-EsgqDkx0.js.map} +1 -1
- package/dist/branch-diff-CxE4VlxT.js +1396 -0
- package/dist/branch-diff-CxE4VlxT.js.map +1 -0
- package/dist/branch-diff-report-DpvVf7hR.js +48 -0
- package/dist/branch-diff-report-DpvVf7hR.js.map +1 -0
- package/dist/branching/branch-diff-report.d.ts +7 -0
- package/dist/branching/branch-diff.d.ts +115 -0
- package/dist/{cli-console-CQrQ0lAY.js → cli-console-f0k6_TsJ.js} +38 -5
- package/dist/cli-console-f0k6_TsJ.js.map +1 -0
- package/dist/cli-console.d.ts +17 -1
- package/dist/{cli-errors-DiA0XFIS.js → cli-errors-DsYyNYzd.js} +53 -2
- package/dist/cli-errors-DsYyNYzd.js.map +1 -0
- package/dist/cli.js +102 -122
- package/dist/cli.js.map +1 -1
- package/dist/{collections-schema-version-T6cGvrDi.js → collections-schema-version-D7bDI0PJ.js} +13 -9
- package/dist/collections-schema-version-D7bDI0PJ.js.map +1 -0
- package/dist/{deferred-index-build-BE0I6sYU.js → deferred-index-build-CqaOv1Xx.js} +3 -3
- package/dist/{deferred-index-build-BE0I6sYU.js.map → deferred-index-build-CqaOv1Xx.js.map} +1 -1
- package/dist/{doctor-BT--1kq3.js → doctor-DW_mlTzz.js} +81 -33
- package/dist/doctor-DW_mlTzz.js.map +1 -0
- package/dist/{ensure-collection-policies-BnGOURpG.js → ensure-collection-policies-DLWgabQ2.js} +5 -5
- package/dist/{ensure-collection-policies-BnGOURpG.js.map → ensure-collection-policies-DLWgabQ2.js.map} +1 -1
- package/dist/{ensure-collection-tables-Cet4650m.js → ensure-collection-tables-CFIRq8ni.js} +5 -5
- package/dist/{ensure-collection-tables-Cet4650m.js.map → ensure-collection-tables-CFIRq8ni.js.map} +1 -1
- package/dist/{ensure-tables-l_OgFQjN.js → ensure-tables-4nA-tfwi.js} +4 -4
- package/dist/{ensure-tables-l_OgFQjN.js.map → ensure-tables-4nA-tfwi.js.map} +1 -1
- package/dist/{enum-labels-kept-nheku1pB.js → enum-labels-kept-PcPW65xy.js} +2 -2
- package/dist/{enum-labels-kept-nheku1pB.js.map → enum-labels-kept-PcPW65xy.js.map} +1 -1
- package/dist/history/HistoryService.d.ts +7 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.es.js +226 -8
- package/dist/index.es.js.map +1 -1
- package/dist/{db-subcommands-BPoHCxJR.js → migrate-baseline-B-2Ef4Mv.js} +266 -41
- package/dist/migrate-baseline-B-2Ef4Mv.js.map +1 -0
- package/dist/{migrate-policy-stamps-3eqfhes6.js → migrate-policy-stamps-BQtFHApx.js} +5 -5
- package/dist/{migrate-policy-stamps-3eqfhes6.js.map → migrate-policy-stamps-BQtFHApx.js.map} +1 -1
- package/dist/{open-rebase-EmqVmS14.js → open-rebase-C8jQ6s5a.js} +292 -46
- package/dist/open-rebase-C8jQ6s5a.js.map +1 -0
- package/dist/open-rebase.d.ts +9 -1
- package/dist/{pg-error-utils-CFEnm2m9.js → pg-error-utils-DhiUTlVx.js} +2 -2
- package/dist/{pg-error-utils-CFEnm2m9.js.map → pg-error-utils-DhiUTlVx.js.map} +1 -1
- package/dist/{push-index-prebuild-COd7i6wm.js → push-index-prebuild-UTvB5pqH.js} +3 -3
- package/dist/{push-index-prebuild-COd7i6wm.js.map → push-index-prebuild-UTvB5pqH.js.map} +1 -1
- package/dist/push-kept-defaults-BGOlOqRQ.js +137 -0
- package/dist/push-kept-defaults-BGOlOqRQ.js.map +1 -0
- package/dist/{push-legacy-renames-CLgYYwPE.js → push-legacy-renames-BwaKi8xF.js} +3 -3
- package/dist/{push-legacy-renames-CLgYYwPE.js.map → push-legacy-renames-BwaKi8xF.js.map} +1 -1
- package/dist/{push-policy-plan-D2gpQsdv.js → push-policy-plan-DQ2tWceD.js} +2 -2
- package/dist/{push-policy-plan-D2gpQsdv.js.map → push-policy-plan-DQ2tWceD.js.map} +1 -1
- package/dist/{rls-enforcement-DDyNoOSy.js → rls-enforcement-BOHuHVlc.js} +42 -3
- package/dist/rls-enforcement-BOHuHVlc.js.map +1 -0
- package/dist/schema/destructive-sql.d.ts +16 -0
- package/dist/schema/doctor-cli.js +3 -3
- package/dist/schema/introspect-db.js +1 -1
- package/dist/schema/migration-freshness.d.ts +69 -0
- package/dist/schema/push-kept-defaults.d.ts +57 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +47 -10
- package/dist/security/rls-enforcement.d.ts +18 -0
- package/dist/services/FetchService.d.ts +12 -0
- package/dist/services/channel-presence.d.ts +3 -1
- package/dist/services/nested-path.d.ts +16 -0
- package/dist/services/realtimeService.d.ts +18 -1
- package/dist/{websocket-BKZwsjmf.js → websocket-CKYc3V4N.js} +41 -12
- package/dist/websocket-CKYc3V4N.js.map +1 -0
- package/package.json +7 -7
- package/dist/BranchService-ClklxyAQ.js.map +0 -1
- package/dist/cli-console-CQrQ0lAY.js.map +0 -1
- package/dist/cli-errors-DiA0XFIS.js.map +0 -1
- package/dist/collections-schema-version-T6cGvrDi.js.map +0 -1
- package/dist/db-subcommands-BPoHCxJR.js.map +0 -1
- package/dist/doctor-BT--1kq3.js.map +0 -1
- package/dist/open-rebase-EmqVmS14.js.map +0 -1
- package/dist/rls-enforcement-DDyNoOSy.js.map +0 -1
- package/dist/websocket-BKZwsjmf.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rls-enforcement-BOHuHVlc.js","names":[],"sources":["../src/security/rls-enforcement.ts"],"sourcesContent":["import { sql as drizzleSql, SQL } from \"drizzle-orm\";\nimport { ANONYMOUS_USER_ID, PolicyExpression, SecurityRule } from \"@rebasepro/types\";\nimport {\n AnonymousGrantRisk,\n findAnonymousGrants,\n REBASE_INTERNAL_TABLES,\n REBASE_USER_ROLE,\n revokeInternalTableAccess,\n securityRuleToConditions\n} from \"@rebasepro/common\";\nimport { REBASE_SCHEMA, usesLegacyRlsFunctions } from \"@rebasepro/types\";\nimport { logger } from \"@rebasepro/server\";\n\n/**\n * Unified RLS enforcement — the \"user context vs server context\" model.\n *\n * Every operation runs in one of two contexts:\n *\n * - **User context** — a request authenticated (or anonymous) via\n * `driver.withAuth(user)`. Runs as the restricted `rebase_user` role: a\n * non-owner, NOSUPERUSER, NOBYPASSRLS role, so Postgres RLS binds *every*\n * statement (SELECT, INSERT, UPDATE, DELETE). The collection's\n * `securityRules` are the whole authorization model; app-layer callbacks\n * are validation/side-effects, not a security boundary.\n *\n * - **Server context** — the base (owner) connection: auth flows, migrations,\n * and raw `rebase.sql`. As table owner it bypasses RLS. This is the trusted\n * plane, equivalent to Supabase's `service_role`.\n *\n * `rebase.dataAsAdmin` is **not** in it, despite the name. `init.ts` scopes\n * that driver with `withAuth(SERVICE_IDENTITY)`, so it arrives as user\n * context above — `rebase_user`, `app.uid = 'service'`, policies evaluated —\n * and clears the default policies through their admin arm rather than the\n * `rebase.uid() IS NULL` one.\n *\n * This module provides the three pieces:\n *\n * 1. {@link detectConnectionPosture} — is the connection subject to RLS at\n * all? (superuser / BYPASSRLS / table owner ⇒ no)\n * 2. {@link ensureAppRole} — idempotently provision `rebase_user` with\n * SELECT/INSERT/UPDATE/DELETE grants (+ default privileges so future\n * tables stay covered).\n * 3. {@link applyAuthContext} — per-transaction: set the `app.*` GUCs the\n * policies read (`rebase.uid()` etc.) and the transaction-local role\n * `rebase_user` so RLS binds, in one statement. Transaction-scoped, so it\n * composes with poolers.\n *\n * Provisioning runs from the framework's own bootstrap/migrate (which already\n * self-creates the `auth` schema and functions) — enforcement is default-on,\n * not an operator opt-in.\n */\n\n/**\n * The restricted role every authenticated (user-context) request runs as.\n *\n * Re-exported, not re-declared: the same name is needed by\n * `@rebasepro/common`'s internal-table revokes, and two spellings of a role name\n * fail as a silent no-op rather than an error.\n */\nexport { REBASE_USER_ROLE };\n\n/** Minimal SQL runner so callers can adapt drizzle or pg.Client. */\nexport type RawSqlRunner = (sqlText: string) => Promise<Record<string, unknown>[]>;\n\n/** Minimal transaction surface needed by {@link applyAuthContext}. */\nexport interface SqlTx {\n execute(query: SQL): Promise<unknown>;\n}\n\nexport interface ConnectionPosture {\n /** The connection's `current_user`. */\n role: string;\n superuser: boolean;\n bypassRLS: boolean;\n /**\n * Owns at least one user table — owners bypass non-FORCE RLS.\n *\n * \"Owns\" in Postgres's sense, not by name: a role that INHERITs the owner is\n * the owner for every ownership test the planner makes, including the one\n * that decides whether a policy applies at all.\n */\n ownsTables: boolean;\n /**\n * May create tables somewhere the server writes — and would then own them.\n *\n * The reason this is part of the posture rather than a separate question:\n * the posture is decided ONCE, at boot, and on a fresh database the\n * catalogue has nothing to report yet. The process that asks then goes on to\n * create every table in the schema and owns all of them for the rest of its\n * life. Asking only what is already there answers about the wrong instant.\n */\n canCreateTables: boolean;\n /** True when RLS would NOT constrain this connection. */\n privileged: boolean;\n}\n\nexport interface AuthContext {\n uid: string;\n /** Raw roles as carried on the user (strings or `{ id }` objects). */\n roles: unknown[];\n /**\n * Whether this session came from anonymous sign-in rather than an account.\n *\n * Anonymous sign-in mints a real user row and a real uid, so a guest was\n * indistinguishable from a registered account inside a policy: same\n * `rebase.uid()`, same default role. Every rule meaning \"a signed-in\n * person\" was therefore also a rule about anybody who had called\n * `POST /auth/anonymous`, which asks for no email, no password and no\n * agreement to anything.\n *\n * Optional, and absent reads as `false`: a caller that predates this — a\n * realtime subscription, a custom validator — keeps the behaviour it had\n * rather than having every one of its users reclassified as guests.\n */\n isAnonymous?: boolean;\n /**\n * The **custom** claims on the caller's token, as `rebase.jwt()` reports\n * them.\n *\n * This is how a policy reaches a fact about the session that is neither the\n * uid nor the roles — which, before this, it could not: `app.jwt` was built\n * from three fields the driver already had, so `rebase.jwt()`, documented\n * as \"full JWT claims as JSONB\", returned a three-key object that never had\n * a claim in it.\n *\n * Multi-tenancy's `claim` form is the first caller — `policy.authClaim(…)`\n * compiles to `rebase.jwt() ->> '<name>'`. Identity claims are stripped out\n * before they reach here (`AccessTokenPayload.claims`) and the identity\n * fields are written *over* them below, so nothing a hook put on a token\n * can assert a uid, roles or guest status.\n */\n claims?: Record<string, unknown>;\n}\n\nconst quoteIdent = (name: string): string => `\"${name.replace(/\"/g, \"\\\"\\\"\")}\"`;\n\n/** DML the user role holds on managed tables (RLS still filters per row). */\nconst USER_TABLE_PRIVILEGES = \"SELECT, INSERT, UPDATE, DELETE\";\n\n/**\n * Take every privilege the user role holds on one table back — what a table\n * gets when RLS cannot be switched on for it, so it is unreachable rather than\n * unprotected. Quoted exactly: a mixed-case adopted table (`\"User\"`) folded to\n * lower case names a table that does not exist.\n */\nexport const revokeUserRoleSql = (schema: string, table: string): string =>\n `REVOKE ALL PRIVILEGES ON ${quoteIdent(schema)}.${quoteIdent(table)} FROM ${quoteIdent(REBASE_USER_ROLE)}`;\n\n/**\n * Warn when the connection role shares its name with an existing schema.\n *\n * Postgres resolves unqualified names through `search_path`, which defaults to\n * `\"$user\", public` — and `$user` is the connection ROLE. When a schema of that\n * name exists it sits ahead of `public`, so every unqualified statement\n * silently operates on it instead:\n *\n * CREATE TABLE posts (...); -- you meant public.posts; you got <role>.posts\n *\n * Nothing errors. You get a second table of the same name in the wrong schema,\n * and reads that pin `public` cannot see it — which reads as \"missing table\" and\n * sends people to re-run a push that creates a *third* copy. The bootstrapper\n * has a whole branch dedicated to recognising the symptom after the fact.\n *\n * Rebase shipped straight into this: it creates a schema named `rebase` while\n * every template named the database role `rebase` too. The scaffold uses\n * `rebase_app` now, and every pool Rebase opens pins `search_path=public`\n * (`pinSearchPath`), which covers the paths the framework controls. This covers\n * the ones it does not — `psql`, `pg_dump`, drizzle-kit, a colleague's script,\n * a hand-written migration — because the hazard is a property of the two NAMES,\n * not of any one connection.\n *\n * A warning rather than a boot failure: the database works, the framework's own\n * traffic is pinned, and refusing to start over a naming choice a user may have\n * inherited would be worse than the risk.\n */\nexport async function warnOnRoleSchemaCollision(run: RawSqlRunner): Promise<void> {\n try {\n const rows = await run(`\n SELECT current_user AS role,\n EXISTS (\n SELECT 1 FROM pg_namespace n WHERE n.nspname = current_user\n ) AS collides\n `);\n if (rows[0]?.collides !== true) return;\n const role = String(rows[0]?.role ?? \"the connection role\");\n logger.warn(\n `⚠️ The database role \"${role}\" has the same name as a schema. Postgres resolves unqualified ` +\n `names through \\`search_path\\`, which defaults to \\`\"$user\", public\\` — so \"${role}\" is searched ` +\n `BEFORE public, and any unqualified \\`CREATE TABLE\\`/\\`SELECT\\` from a tool that does not pin the ` +\n `path (psql, pg_dump, drizzle-kit, a hand-written migration) silently lands in \"${role}\" instead. ` +\n `Rebase's own connections pin \\`search_path=public\\`, so the server is unaffected. To remove the ` +\n `hazard entirely, connect as a role whose name is not also a schema — the scaffold uses ` +\n `\"rebase_app\".`\n );\n } catch {\n // A diagnostic must never be the reason a boot fails.\n }\n}\n\n/**\n * Would RLS actually constrain this connection?\n *\n * Four ways the answer is no, and only two of them are obvious. Superuser and\n * BYPASSRLS announce themselves in `pg_roles`. The other two are about\n * ownership, and both used to read as \"no bypass here\":\n *\n * - **Ownership through membership.** The old query compared\n * `pg_tables.tableowner` to `current_user` as a string. Postgres does not: a\n * role that INHERITs the owner passes every ownership test the planner makes,\n * including the one that decides whether a policy applies. So a perfectly\n * ordinary \"app role that is a member of the owner role\" setup reported\n * unprivileged and bypassed every policy — permanently, since no restart\n * changes a string comparison's answer.\n *\n * - **Ownership that has not happened yet.** The posture is decided once, at\n * boot, and on a fresh database there is nothing in the catalogue to find. The\n * same process then creates every table and owns all of them. The first boot\n * of a new deployment therefore served every request unconstrained, the log\n * said RLS was fine, and the SECOND boot silently fixed it — which is the\n * worst possible shape for a security check, because the state that reproduces\n * it is gone by the time anyone looks. Asking \"may this role create tables\n * where the server provisions?\" answers about the process's whole life rather\n * than about one instant of it.\n *\n * `schemas` narrows the CREATE question to where the server actually writes.\n * Omitted, it asks about every schema the role can see, which is the safe\n * direction: over-reporting costs a role switch that was not strictly needed,\n * under-reporting costs the entire authorization model.\n */\nexport async function detectConnectionPosture(\n run: RawSqlRunner,\n schemas?: string[]\n): Promise<ConnectionPosture> {\n // Identifiers, and they reach SQL as literals in a catalogue comparison —\n // but they arrive from collection configs, so they are quoted as string\n // literals rather than interpolated as names, and a quote inside one is\n // escaped rather than trusted.\n const schemaFilter = schemas?.length\n ? `AND n.nspname IN (${Array.from(new Set(schemas))\n .map((s) => `'${s.replace(/'/g, \"''\")}'`)\n .join(\", \")})`\n : \"\";\n\n const rows = await run(`\n SELECT current_user AS role,\n r.rolsuper AS superuser,\n r.rolbypassrls AS bypassrls,\n EXISTS (\n SELECT 1 FROM pg_tables t\n WHERE pg_has_role(current_user, t.tableowner, 'USAGE')\n AND t.schemaname NOT IN ('pg_catalog', 'information_schema')\n ) AS owns_tables,\n EXISTS (\n SELECT 1 FROM pg_namespace n\n WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')\n AND n.nspname NOT LIKE 'pg\\\\_toast%'\n AND n.nspname NOT LIKE 'pg\\\\_temp%'\n ${schemaFilter}\n AND has_schema_privilege(current_user, n.oid, 'CREATE')\n ) AS can_create\n FROM pg_roles r\n WHERE r.rolname = current_user\n `);\n const row = rows[0] ?? {};\n const superuser = row.superuser === true;\n const bypassRLS = row.bypassrls === true;\n const ownsTables = row.owns_tables === true;\n const canCreateTables = row.can_create === true;\n return {\n role: String(row.role ?? \"unknown\"),\n superuser,\n bypassRLS,\n ownsTables,\n canCreateTables,\n privileged: superuser || bypassRLS || ownsTables || canCreateTables\n };\n}\n\n/**\n * Human-actionable instructions for when the connection cannot provision the\n * user role itself (no CREATEROLE and role not pre-created by the platform).\n */\nexport function appRoleSetupInstructions(connectionRole: string, schemas: string[]): string {\n const grants = schemas.map((s) =>\n `GRANT USAGE ON SCHEMA ${quoteIdent(s)} TO ${REBASE_USER_ROLE};\\n` +\n `GRANT ${USER_TABLE_PRIVILEGES} ON ALL TABLES IN SCHEMA ${quoteIdent(s)} TO ${REBASE_USER_ROLE};\\n` +\n `GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA ${quoteIdent(s)} TO ${REBASE_USER_ROLE};`\n ).join(\"\\n\");\n return (\n `Rebase enforces row-level security by running authenticated requests as ` +\n `the restricted role \"${REBASE_USER_ROLE}\", but the connection role ` +\n `\"${connectionRole}\" bypasses RLS and cannot create that role itself.\\n` +\n `Run the following as a database administrator, then restart:\\n\\n` +\n `CREATE ROLE ${REBASE_USER_ROLE} NOLOGIN NOSUPERUSER NOBYPASSRLS NOINHERIT;\\n` +\n `GRANT ${REBASE_USER_ROLE} TO ${quoteIdent(connectionRole)};\\n` +\n grants\n );\n}\n\n/**\n * Idempotently provision the `rebase_user` role, membership for the current\n * connection role, and DML grants (+ default privileges for future tables)\n * on every existing schema in `schemas`.\n *\n * Split into privilege tiers so it works both when the connection is a\n * superuser (creates everything) and when the platform pre-created the role\n * and membership (e.g. CNPG `postInitApplicationSQL`) and the connection is\n * merely the table owner — owners can always run the grant tier themselves.\n *\n * RLS still filters every row: these grants only make the tables *reachable*\n * by the role; the policies decide which rows/commands actually pass.\n *\n * Throws with precise setup instructions when the role is missing and the\n * connection cannot create it.\n */\nexport async function ensureAppRole(run: RawSqlRunner, schemas: string[]): Promise<void> {\n const uniqueSchemas = Array.from(new Set(schemas.filter(Boolean)));\n\n // Tier 1 — role existence.\n const roleRows = await run(`SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}'`);\n if (roleRows.length === 0) {\n try {\n // Guarded in the statement as well as by the probe above. A role\n // belongs to the cluster, not the database, so a recorded\n // provisioning run (REBASE_PROVISION_SQL_OUT) replayed into another\n // database on a cluster that already has the role must find this a\n // no-op rather than its first error.\n await run(`\n DO $rebase_role$\n BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}') THEN\n CREATE ROLE ${REBASE_USER_ROLE} NOLOGIN NOSUPERUSER NOBYPASSRLS NOINHERIT;\n END IF;\n END\n $rebase_role$\n `);\n } catch (err) {\n throw new Error(\n `Failed to create the \"${REBASE_USER_ROLE}\" role: ${err instanceof Error ? err.message : String(err)}\\n\\n` +\n appRoleSetupInstructions(\"current connection role\", uniqueSchemas)\n );\n }\n }\n\n // Tier 2 — membership, so a non-superuser connection may SET ROLE to it.\n const memberRows = await run(`\n SELECT (pg_has_role(current_user, '${REBASE_USER_ROLE}', 'MEMBER')\n OR (SELECT rolsuper FROM pg_roles WHERE rolname = current_user)) AS can_set,\n current_user AS role\n `);\n if (memberRows[0]?.can_set !== true) {\n try {\n await run(`GRANT ${REBASE_USER_ROLE} TO CURRENT_USER`);\n } catch (err) {\n throw new Error(\n `The connection role is not a member of \"${REBASE_USER_ROLE}\" and cannot grant itself membership: ` +\n `${err instanceof Error ? err.message : String(err)}\\n\\n` +\n appRoleSetupInstructions(String(memberRows[0]?.role ?? \"current connection role\"), uniqueSchemas)\n );\n }\n }\n\n // Tier 3 — grants.\n await ensureAppRoleGrants(run, uniqueSchemas);\n\n logger.debug(`🔐 [rls] User role \"${REBASE_USER_ROLE}\" provisioned (schemas: ${uniqueSchemas.join(\", \")})`);\n}\n\n/** A string literal for a catalogue read. Names come from config and constants, never from a request. */\nconst sqlLiteral = (value: string): string => `'${value.replace(/'/g, \"''\")}'`;\nconst textArray = (values: readonly string[]): string =>\n values.length === 0 ? \"ARRAY[]::text[]\" : `ARRAY[${values.map(sqlLiteral).join(\", \")}]::text[]`;\n\n/**\n * The framework's internal tables in `schemas` that the request role can\n * reach — `schema.table`, sorted — when nothing is left to take it back off.\n *\n * Every provisioning boot repairs this: each store revokes on its own table\n * and {@link ensureAppRole}'s sweep revokes on the rest, because the grant\n * tier hands the request role DML on whole schemas. A backend that changes\n * nothing in its database runs neither, so a database whose `rebase_user` can\n * read `refresh_tokens` or `api_keys` would stay that way for as long as it\n * served. This is the question the sweep would have answered, asked instead.\n *\n * \"Can reach\" is what the role can actually do — through a grant to it, to a\n * role it inherits, or to PUBLIC, on the table or on any of its columns —\n * because that, not how the ACL is spelled, is what a request running as it\n * gets. A table with RLS on is somebody's collection by that same rule as the\n * sweep's, and is left out.\n */\nexport async function findExposedInternalTables(run: RawSqlRunner, schemas: string[]): Promise<string[]> {\n const rows = await run(`\n SELECT n.nspname || '.' || c.relname AS name\n FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE n.nspname = ANY(${textArray(Array.from(new Set(schemas)))})\n AND c.relname = ANY(${textArray(REBASE_INTERNAL_TABLES)})\n AND c.relkind IN ('r', 'p')\n AND NOT c.relrowsecurity\n AND (has_table_privilege(${sqlLiteral(REBASE_USER_ROLE)}, c.oid, 'SELECT, INSERT, UPDATE, DELETE')\n OR has_any_column_privilege(${sqlLiteral(REBASE_USER_ROLE)}, c.oid, 'SELECT, INSERT, UPDATE'))\n ORDER BY 1\n `);\n return rows.map(row => String(row.name));\n}\n\n/** What the grant tier would find already in place in one schema. */\ninterface SchemaGrants {\n /** `USAGE` on the schema, granted by its owner. */\n usage: boolean;\n /**\n * Every relation `GRANT … ON ALL TABLES` reaches already has all four DML\n * privileges from its owner — Rebase's own RLS-off tables aside, which the\n * sweep takes back right after and so never keep them.\n */\n tables: boolean;\n /** Every sequence already has `USAGE, SELECT` from its owner. */\n sequences: boolean;\n /** This role's default privileges in the schema already grant the table DML. */\n defaultTables: boolean;\n /** …and the sequence `USAGE, SELECT`. */\n defaultSequences: boolean;\n /**\n * Rebase's own RLS-off tables a `REVOKE` would still change: the user role\n * holds a privilege on them, or the table has never had an ACL written —\n * `NULL`, which a `REVOKE` (like the grant-and-revoke it replaces) turns\n * into the owner's explicit default. Same access either way; the same\n * catalogue only this way.\n */\n internalGranted: string[];\n}\n\n/**\n * The grant tier's state for every existing schema in `schemas`, in one read.\n *\n * A grant counts as present only as the statement this tier runs would record\n * it — grantor the object's owner, which is what a GRANT by the owner, a member\n * of the owning role or a superuser records — so skipping it leaves the ACL\n * exactly as running it would. Anything else reads as missing, and the\n * statement runs as it always did.\n */\nasync function readSchemaGrants(run: RawSqlRunner, schemas: string[]): Promise<Map<string, SchemaGrants>> {\n const internal = textArray(REBASE_INTERNAL_TABLES);\n const target = `(SELECT oid FROM pg_roles WHERE rolname = ${sqlLiteral(REBASE_USER_ROLE)})`;\n /** Distinct privileges `acl` gives the user role (from `grantor`, when given) out of `privileges`. */\n const held = (acl: string, privileges: string[], grantor?: string): string =>\n `(SELECT count(DISTINCT a.privilege_type) FROM aclexplode(${acl}) a WHERE a.grantee = ${target}` +\n `${grantor ? ` AND a.grantor = ${grantor}` : \"\"} AND a.privilege_type IN (${privileges.map(sqlLiteral).join(\", \")}))`;\n const DML = [\"SELECT\", \"INSERT\", \"UPDATE\", \"DELETE\"];\n const SEQ = [\"USAGE\", \"SELECT\"];\n const rows = await run(`\n SELECT n.nspname AS schema,\n ${held(\"n.nspacl\", [\"USAGE\"], \"n.nspowner\")} = 1 AS usage,\n NOT EXISTS (\n SELECT 1 FROM pg_class c\n WHERE c.relnamespace = n.oid AND c.relkind IN ('r', 'v', 'm', 'f', 'p')\n AND NOT (c.relname = ANY(${internal}) AND NOT c.relrowsecurity)\n AND ${held(\"c.relacl\", DML, \"c.relowner\")} < ${DML.length}\n ) AS tables,\n NOT EXISTS (\n SELECT 1 FROM pg_class c\n WHERE c.relnamespace = n.oid AND c.relkind = 'S'\n AND ${held(\"c.relacl\", SEQ, \"c.relowner\")} < ${SEQ.length}\n ) AS sequences,\n EXISTS (\n SELECT 1 FROM pg_default_acl d\n WHERE d.defaclrole = (SELECT oid FROM pg_roles WHERE rolname = current_user)\n AND d.defaclnamespace = n.oid AND d.defaclobjtype = 'r'\n AND ${held(\"d.defaclacl\", DML)} = ${DML.length}\n ) AS default_tables,\n EXISTS (\n SELECT 1 FROM pg_default_acl d\n WHERE d.defaclrole = (SELECT oid FROM pg_roles WHERE rolname = current_user)\n AND d.defaclnamespace = n.oid AND d.defaclobjtype = 'S'\n AND ${held(\"d.defaclacl\", SEQ)} = ${SEQ.length}\n ) AS default_sequences,\n ARRAY(\n SELECT c.relname::text FROM pg_class c\n WHERE c.relnamespace = n.oid AND c.relname = ANY(${internal}) AND NOT c.relrowsecurity\n AND (c.relacl IS NULL OR EXISTS (SELECT 1 FROM aclexplode(c.relacl) a WHERE a.grantee = ${target}))\n ORDER BY 1\n ) AS internal_granted\n FROM pg_namespace n\n WHERE n.nspname = ANY(${textArray(schemas)})\n `);\n const grants = new Map<string, SchemaGrants>();\n for (const row of rows) {\n const internalGranted = row.internal_granted;\n grants.set(String(row.schema), {\n usage: row.usage === true,\n tables: row.tables === true,\n sequences: row.sequences === true,\n defaultTables: row.default_tables === true,\n defaultSequences: row.default_sequences === true,\n internalGranted: Array.isArray(internalGranted)\n ? internalGranted.map(String)\n // A driver that hands arrays back as text: `{a,b}`.\n : String(internalGranted ?? \"\").replace(/^\\{|\\}$/g, \"\").split(\",\").filter(Boolean)\n });\n }\n return grants;\n}\n\n/**\n * The grant tier of {@link ensureAppRole} on its own: `USAGE` on each existing\n * schema in `schemas`, DML on its tables and `USAGE, SELECT` on its\n * sequences, the default privileges that cover what this role creates later,\n * and Rebase's own tables taken back — for a role and membership already\n * provisioned.\n *\n * Every statement is issued only when the catalogue says it would change\n * something. They used to run unconditionally, twice per boot (at connect, and\n * again in the final posture check): a `GRANT … ON ALL TABLES` rewrites every\n * table's `pg_class` row whether or not the ACL changes, which invalidates\n * every table's cached plans on every connection, and the internal-table sweep\n * ran 23 `REVOKE`s per schema. A boot against a database whose grants already\n * hold now reads one catalogue query and writes nothing; one that is missing\n * any of them runs exactly the statements it always ran.\n *\n * Table owners (the expected non-superuser posture) can always grant on their\n * own objects, so this needs no extra privilege.\n */\nexport async function ensureAppRoleGrants(run: RawSqlRunner, schemas: string[]): Promise<void> {\n const uniqueSchemas = Array.from(new Set(schemas.filter(Boolean)));\n const grants = await readSchemaGrants(run, uniqueSchemas);\n for (const schema of uniqueSchemas) {\n const state = grants.get(schema);\n // Not there (yet): a later call, once provisioning has created it, grants it.\n if (!state) continue;\n const s = quoteIdent(schema);\n if (!state.usage) await run(`GRANT USAGE ON SCHEMA ${s} TO ${REBASE_USER_ROLE}`);\n if (!state.tables) await run(`GRANT ${USER_TABLE_PRIVILEGES} ON ALL TABLES IN SCHEMA ${s} TO ${REBASE_USER_ROLE}`);\n if (!state.sequences) await run(`GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA ${s} TO ${REBASE_USER_ROLE}`);\n // Cover objects created later by the CURRENT role (the role that runs\n // migrations), so a migrate can never strand the user role.\n if (!state.defaultTables) {\n await run(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${s} GRANT ${USER_TABLE_PRIVILEGES} ON TABLES TO ${REBASE_USER_ROLE}`);\n }\n if (!state.defaultSequences) {\n await run(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${s} GRANT USAGE, SELECT ON SEQUENCES TO ${REBASE_USER_ROLE}`);\n }\n\n // The grants above are deliberately schema-wide — a project's own\n // collections may live in `rebase`, and future tables must be reachable\n // or a migration strands the role. Rebase's OWN tables are the exception:\n // refresh tokens, MFA secrets, API keys and the rest carry no RLS and no\n // row an end user should ever address. Taking the privilege back here\n // covers every table that already exists; each creator revokes on the\n // table it just made, for the boot that creates them for the first time.\n //\n // All of them right after the schema-wide grant, which has just handed\n // them out; otherwise only the ones the catalogue says still hold one.\n const tables = state.tables ? state.internalGranted : undefined;\n if (tables && tables.length === 0) continue;\n await revokeInternalTableAccess(async (text) => { await run(text); }, schema, {\n tables,\n onError: (table, error) => logger.warn(\n `🔐 [rls] Could not revoke \"${REBASE_USER_ROLE}\" access to \"${schema}\".\"${table}\" — ` +\n \"it stays reachable by authenticated requests: \" +\n (error instanceof Error ? error.message : String(error))\n )\n });\n }\n}\n\n/**\n * Rebase's own RLS-off tables in `schema` that a `revokeInternalTableAccess`\n * sweep would change — the user role holds a privilege on them, or their ACL\n * has never been written. One read; the sweep it narrows is one `DO` per\n * table name.\n */\nexport async function internalTablesGrantedToUserRole(run: RawSqlRunner, schema: string): Promise<string[]> {\n return (await readSchemaGrants(run, [schema])).get(schema)?.internalGranted ?? [];\n}\n\n/**\n * Apply the authenticated context to a transaction: the `app.*` GUCs that RLS\n * policies read via `rebase.uid()` / `rebase.roles()` / `rebase.jwt()`, and — when\n * `userRole` is set — the `LOCAL` role switch, in the same statement, so RLS\n * binds every statement in this transaction (reads *and* writes).\n *\n * GUCs are set with `is_local = true` and the role switch is `LOCAL`: both\n * reset at commit/rollback, so pooled connections are never polluted.\n *\n * Fails closed by construction: if the role switch errors, the transaction\n * aborts instead of proceeding privileged.\n *\n * SECURITY: this function is only ever called on the **user** path (the server\n * context uses the base/owner driver and never calls it). The default policies\n * treat `rebase.uid() IS NULL` as the trusted server context, and `rebase.uid()`\n * is `NULLIF(current_setting('app.uid'), '')` — so an EMPTY user id would\n * be read as NULL and silently escalate a user request to server privileges.\n * Coerce empty/blank ids to `ANONYMOUS_USER_ID` here, at the single chokepoint,\n * rather than trusting every caller (e.g. realtime subscription auth) to do it.\n * That sentinel is exported from `@rebasepro/types` because it leaks into rule\n * semantics: it is why `rebase.uid() IS NOT NULL` is true for anonymous requests.\n */\nexport async function applyAuthContext(tx: SqlTx, auth: AuthContext, userRole?: string): Promise<void> {\n const uid = typeof auth.uid === \"string\" && auth.uid.trim() !== \"\" ? auth.uid : ANONYMOUS_USER_ID;\n const normalizedRoles = auth.roles.map((r: unknown) =>\n typeof r === \"string\" ? r : (r as Record<string, unknown>)?.id ?? String(r)\n );\n // `app.user_id` is the pre-rename spelling, still written because policies\n // are data: a database provisioned before the rename holds rules compiled\n // to `current_setting('app.user_id')`, and those predicates would evaluate\n // to NULL — failing open or locking out — if we stopped setting it. Drop\n // the alias only once no live database carries a legacy policy.\n // A guest is a signed-in caller with no account behind them, and until this\n // the database could not tell the difference — see `rebase.is_anonymous()`.\n // Written as a string because that is what `set_config` takes.\n const isAnonymous = auth.isAnonymous === true ? \"true\" : \"false\";\n\n await tx.execute(drizzleSql`\n SELECT\n set_config('app.uid', ${uid}, true),\n set_config('app.user_id', ${uid}, true),\n set_config('app.user_roles', ${normalizedRoles.join(\",\")}, true),\n set_config('app.is_anonymous', ${isAnonymous}, true),\n set_config('app.jwt', ${JSON.stringify({\n // The caller's custom claims, FIRST, so the identity keys below\n // overwrite them rather than the other way round. Same ordering, and\n // the same reason, as `generateAccessToken`: a claims hook is a place\n // to add facts about a session, not the place its subject is decided.\n // `verifyAccessToken` already strips the identity claims out of this\n // object; the ordering is the second lock on the same door.\n ...(auth.claims ?? {}),\n sub: uid,\n roles: auth.roles,\n // In the claims too, so a policy reading `rebase.jwt()` — which is\n // how a rule reaches anything that is not uid or roles — sees the\n // same fact as `rebase.is_anonymous()`. Two sources that could\n // disagree would be worse than one.\n is_anonymous: auth.isAnonymous === true\n })}, true)${userRole\n // The role switch, in the same statement rather than a `SET LOCAL ROLE`\n // round trip of its own on every transaction. Postgres implements\n // `SET ROLE` as the `role` setting: `set_config('role', name, true)`\n // runs the same membership check, fails with the same SQLSTATE and\n // message (42501 \"permission denied to set role\", 22023 for an unknown\n // role — `isRoleSwitchingPermissionError` still reads it), takes the\n // name verbatim as the quoted identifier did, and resets at COMMIT and\n // ROLLBACK like `LOCAL`. Last in the list, after the GUCs; a refusal\n // fails the whole statement, so the transaction aborts rather than\n // running on as the connection owner. auth-context-one-statement.test.ts.\n ? drizzleSql`,\n set_config('role', ${userRole}, true)`\n : drizzleSql``}\n `);\n}\n\n/** Role names from other BaaS platforms that people reach for out of habit. */\nconst FOREIGN_CONVENTION_ROLES: Record<string, string> = {\n authenticated: \"Supabase\",\n anon: \"Supabase\",\n service_role: \"Supabase\"\n};\n\n/**\n * Warn about rules that read as \"signed-in users only\" but admit anonymous\n * callers — `rebase.uid() IS NOT NULL`, or a comparison against another\n * platform's magic user id such as `'anon'`.\n *\n * The sibling of {@link validatePolicyPgRoles}, for the more dangerous spelling\n * of the same habit. A foreign `pgRoles` value makes a policy unreachable and\n * the table reads empty — loud, and that guard throws. These do the opposite:\n * the rule compiles to a grant, and nothing looks wrong until the data is\n * already public.\n *\n * Warns rather than throws. Unlike an unreachable `pgRoles`, these rules are\n * serving traffic today: refusing to boot would take an app offline to report a\n * problem it already has, and on the read path it would take it offline\n * *because* its data was exposed. Rewriting the author's SQL is not an option\n * either — this is the escape hatch whose whole promise is that it means what it\n * says. So: say so, loudly, and leave the rule alone.\n */\nexport function warnOnAnonymousGrants(\n collections: { slug?: string; securityRules?: readonly SecurityRule[] }[]\n): void {\n // Grouped by the mistake, not by the rule: one habit typically repeats\n // across every collection an author wrote, and a per-rule list would repeat\n // the same paragraph dozens of times and get skimmed.\n const byRisk = new Map<string, { risk: AnonymousGrantRisk; sites: string[] }>();\n\n for (const collection of collections) {\n for (const rule of collection.securityRules ?? []) {\n const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);\n const risks = [usingExpr, withCheckExpr]\n .filter((e): e is PolicyExpression => e !== null)\n .flatMap(findAnonymousGrants);\n\n for (const risk of risks) {\n const key = `${risk.pattern}:${risk.detail}`;\n const site = `${collection.slug ?? \"(unnamed)\"} → \"${rule.name ?? \"(unnamed rule)\"}\"`;\n const entry = byRisk.get(key) ?? { risk, sites: [] };\n if (!entry.sites.includes(site)) entry.sites.push(site);\n byRisk.set(key, entry);\n }\n }\n }\n\n if (byRisk.size === 0) return;\n\n const problems = [...byRisk.values()].map(({ risk, sites }) =>\n ` • ${risk.explanation}\\n ${sites.length} rule(s): ${sites.join(\", \")}`\n );\n\n logger.warn(\n `Security rules that read as a lockdown but grant access to anonymous requests. Every caller from a ` +\n `client carries a user id ('${ANONYMOUS_USER_ID}' when nobody is signed in), so these clauses are ` +\n `true for everyone:\\n\\n` +\n problems.join(\"\\n\\n\") + \"\\n\"\n );\n}\n\n/**\n * Name the collections whose raw policy SQL still calls the pre-1.0 helpers.\n *\n * The compiler rewrites `auth.uid()` to `rebase.uid()` on the way into the\n * database, so nothing is broken and no policy is wrong — which is exactly why\n * this has to be said out loud. A silent rewrite that works forever is not a\n * migration, it is a second supported spelling nobody wrote down, and the next\n * person to read those rules will copy the old one.\n *\n * Only `raw` expressions can carry it. Structured rules (`policy.authUid()`,\n * `policy.rolesOverlap(...)`) compile from the model and were never affected.\n */\nexport function warnOnLegacyRlsFunctions(\n collections: { slug?: string; securityRules?: readonly SecurityRule[] }[]\n): void {\n const sites: string[] = [];\n\n for (const collection of collections) {\n for (const rule of collection.securityRules ?? []) {\n const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);\n const carriesLegacy = [usingExpr, withCheckExpr]\n .filter((e): e is PolicyExpression => e !== null)\n .some(containsLegacyRlsCall);\n if (!carriesLegacy) continue;\n\n const site = `${collection.slug ?? \"(unnamed)\"} → \"${rule.name ?? \"(unnamed rule)\"}\"`;\n if (!sites.includes(site)) sites.push(site);\n }\n }\n\n if (sites.length === 0) return;\n\n logger.warn(\n `These security rules call the pre-1.0 RLS helpers (\\`auth.uid()\\`, \\`auth.roles()\\`, \\`auth.jwt()\\`). ` +\n `They still work — the compiler rewrites them — but the functions now live in the \\`rebase\\` schema, ` +\n `and the \\`auth\\` one is Supabase's. Update the raw SQL in these rules to \\`${REBASE_SCHEMA}.uid()\\` ` +\n `and friends, or switch them to the structured helpers (\\`policy.authUid()\\`, \\`policy.rolesOverlap()\\`), ` +\n `which never had to be spelled by hand:\\n\\n` +\n sites.map(s => ` • ${s}`).join(\"\\n\") + \"\\n\"\n );\n}\n\n/** Whether any `raw` expression in the tree calls a pre-1.0 helper. */\nfunction containsLegacyRlsCall(expr: PolicyExpression): boolean {\n switch (expr.kind) {\n case \"raw\":\n return usesLegacyRlsFunctions(expr.sql);\n case \"and\":\n case \"or\":\n return expr.operands.some(containsLegacyRlsCall);\n case \"not\":\n return containsLegacyRlsCall(expr.operand);\n case \"existsIn\":\n return containsLegacyRlsCall(expr.where);\n default:\n return false;\n }\n}\n\n/**\n * Reject `pgRoles` that this server can never satisfy.\n *\n * `pgRoles` sets the `TO` clause of a generated policy, so a policy naming a\n * role the request never runs as simply never applies — and RLS then filters\n * every row. The table reads as empty, which is indistinguishable from having\n * no data, so the mistake survives review and ships.\n *\n * Requests run as `rebase_user`, so a policy is only reachable if it targets\n * `public` or a role `rebase_user` holds. Anything else is a configuration\n * error worth failing the boot for.\n */\nexport async function validatePolicyPgRoles(\n run: RawSqlRunner,\n collections: { slug?: string; securityRules?: readonly { name?: string; pgRoles?: readonly string[] }[] }[],\n /** The role requests actually run as: `rebase_user` when the connection is\n * privileged enough to switch, otherwise the connection role itself. */\n requestRole: string = REBASE_USER_ROLE\n): Promise<void> {\n const wanted = new Map<string, string[]>();\n for (const collection of collections) {\n for (const rule of collection.securityRules ?? []) {\n for (const role of rule.pgRoles ?? []) {\n if (role === \"public\") continue;\n wanted.set(role, [...(wanted.get(role) ?? []), collection.slug ?? \"(unnamed)\"]);\n }\n }\n }\n if (wanted.size === 0) return;\n\n const names = [...wanted.keys()].map((r) => `'${r.replace(/'/g, \"''\")}'`).join(\",\");\n const escapedRequestRole = requestRole.replace(/'/g, \"''\");\n const rows = await run(`\n SELECT r.rolname AS role,\n COALESCE(pg_has_role(to_regrole('${escapedRequestRole}'), r.oid, 'MEMBER'), false) AS reachable\n FROM pg_roles r\n WHERE r.rolname IN (${names})\n `);\n\n const reachable = new Map(rows.map((row) => [String(row.role), row.reachable === true]));\n const problems: string[] = [];\n\n for (const [role, slugs] of wanted) {\n if (reachable.get(role) === true) continue;\n\n const why = reachable.has(role)\n ? `\"${requestRole}\" is not a member of it`\n : \"no such role exists in this database\";\n const platform = FOREIGN_CONVENTION_ROLES[role];\n const hint = platform\n ? `\"${role}\" is a ${platform} convention, not a PostgreSQL role. Application roles belong in \\`roles: [\"${role === \"service_role\" ? \"admin\" : role}\"]\\`, which is checked inside the policy via rebase.roles().`\n : `Either grant it (GRANT ${role} TO ${requestRole}) or drop \\`pgRoles\\` so the policy targets \\`public\\`.`;\n\n problems.push(\n ` • pgRoles: [\"${role}\"] on ${slugs.join(\", \")} — ${why}.\\n ${hint}`\n );\n }\n\n if (problems.length > 0) {\n throw new Error(\n `Security rules target PostgreSQL roles this server cannot use. Requests run as ` +\n `\"${requestRole}\", so these policies would never apply and every row would be ` +\n `filtered out — the collections would look empty rather than error.\\n\\n` +\n problems.join(\"\\n\\n\") + \"\\n\"\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -131,6 +131,22 @@ export declare function destructiveOptInFlag(args: {
|
|
|
131
131
|
"--allow-destructive"?: boolean;
|
|
132
132
|
"--yes"?: boolean;
|
|
133
133
|
}): "--allow-destructive" | "--yes" | undefined;
|
|
134
|
+
/**
|
|
135
|
+
* What a destructive push approved by `--yes` says, once per process.
|
|
136
|
+
*
|
|
137
|
+
* `--yes` is an alias of `--allow-destructive` on `db push`, and nothing else
|
|
138
|
+
* spells it that way: on `rebase init` it means "take the defaults", and an
|
|
139
|
+
* agent passes it by reflex to get past prompts — so one habitual flag
|
|
140
|
+
* dropped a column ("Proceeding because --yes was passed" → DROP COLUMN). It
|
|
141
|
+
* keeps working for one release, as the deprecate-before-removing rule asks,
|
|
142
|
+
* and says what changes next.
|
|
143
|
+
*/
|
|
144
|
+
export declare const YES_APPROVES_DESTRUCTIVE_DEPRECATION: string;
|
|
145
|
+
/**
|
|
146
|
+
* Warn — once per process, through `warnDeprecated` — when it was `--yes`, not
|
|
147
|
+
* `--allow-destructive`, that let destructive statements through.
|
|
148
|
+
*/
|
|
149
|
+
export declare function warnIfYesApprovedDestructive(optIn: "--allow-destructive" | "--yes" | undefined, destructiveCount: number): void;
|
|
134
150
|
/**
|
|
135
151
|
* Decide how `db push` should proceed given the plan's destructiveness and
|
|
136
152
|
* the invocation context.
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import { createRequire as __createRequire } from "module";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
4
|
import { a as buildSearchColumnSpec } from "../search-column-BM-GV6vH.js";
|
|
5
|
-
import { a as diagnoseDbError } from "../cli-errors-
|
|
6
|
-
import {
|
|
5
|
+
import { a as diagnoseDbError } from "../cli-errors-DsYyNYzd.js";
|
|
6
|
+
import { d as warnOnAnonymousGrants, u as validatePolicyPgRoles } from "../rls-enforcement-BOHuHVlc.js";
|
|
7
7
|
import { n as describeFailure, t as chalk } from "../source-fZb92pN8.js";
|
|
8
|
-
import { n as loadCollections, r as runDoctor } from "../doctor-BT--1kq3.js";
|
|
9
8
|
import { n as formatPolicyDrift, r as hasDrift, t as checkPolicyDrift } from "../policy-drift-X4A-xAzv.js";
|
|
9
|
+
import { n as loadCollections, r as runDoctor } from "../doctor-DW_mlTzz.js";
|
|
10
10
|
import { isPostgresCollectionConfig } from "@rebasepro/types";
|
|
11
11
|
import { getTableName, relationalCollections } from "@rebasepro/common";
|
|
12
12
|
import path from "path";
|
|
@@ -2,7 +2,7 @@ import { createRequire as __createRequire } from "module";
|
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
3
|
import { n as require_arg, o as __toESM, t as promptConfirm } from "../cli-prompt-DXG0yk-h.js";
|
|
4
4
|
import { c as pinSearchPath } from "../connection-czvI7TZL.js";
|
|
5
|
-
import { a as diagnoseDbError } from "../cli-errors-
|
|
5
|
+
import { a as diagnoseDbError } from "../cli-errors-DsYyNYzd.js";
|
|
6
6
|
import { a as outWarn, i as outError, r as out, t as chalk } from "../source-fZb92pN8.js";
|
|
7
7
|
import { i as readSchemaMetadata, n as isUsableFileName, r as quoteRelation, t as countRowsUpTo } from "../introspect-db-queries-DevUrzll.js";
|
|
8
8
|
import { a as compositeKeySkipReason, c as generateIndexContent, d as mergeIndexContent, f as safeHostFromUrl, h as lookupCandidates, i as buildTablesMap, m as classifyTables, o as describeRlsOffTables, r as buildEnumMap, s as generateCollectionFile } from "../introspect-db-logic-DgQVzccW.js";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Do the migration files still describe the collections?
|
|
3
|
+
*
|
|
4
|
+
* `rebase db generate` asks Atlas, which needs a dev database to replay the
|
|
5
|
+
* migrations into — seconds of Docker, or the managed PGlite, before it can say
|
|
6
|
+
* a word. `rebase verify` runs after every edit an agent makes to a collection
|
|
7
|
+
* file and in CI, where neither is acceptable, so this answers the narrower
|
|
8
|
+
* question offline: replay the `CREATE TABLE` / `ALTER TABLE` / `DROP TABLE`
|
|
9
|
+
* statements the migrations contain, and compare the tables and columns that
|
|
10
|
+
* leaves with the ones the schema plan derives from the collections.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately tables and columns only. A changed type or default is a real
|
|
13
|
+
* drift too, but reading it back out of Atlas's SQL means parsing every type
|
|
14
|
+
* spelling Postgres accepts, and a check that misreads `character varying(255)`
|
|
15
|
+
* as a change cries wolf on every run. A missing table or column is the drift
|
|
16
|
+
* that breaks a deploy — the runtime queries a column the migrated database
|
|
17
|
+
* does not have — and it is unambiguous in the text.
|
|
18
|
+
*
|
|
19
|
+
* Policies are not compared: the runtime applies a collection's policies at
|
|
20
|
+
* boot whether or not a migration carries them.
|
|
21
|
+
*/
|
|
22
|
+
import type { CollectionConfig } from "@rebasepro/types";
|
|
23
|
+
/** One difference between the collections and what the migrations build. */
|
|
24
|
+
export interface UnmigratedChange {
|
|
25
|
+
/**
|
|
26
|
+
* - `missing-table` / `missing-column` — the collections declare it and no
|
|
27
|
+
* migration creates it.
|
|
28
|
+
* - `removed-column` — a migration creates it, the collection no longer
|
|
29
|
+
* declares it, and no later migration drops it.
|
|
30
|
+
*/
|
|
31
|
+
kind: "missing-table" | "missing-column" | "removed-column";
|
|
32
|
+
/** `schema.table`. */
|
|
33
|
+
table: string;
|
|
34
|
+
column?: string;
|
|
35
|
+
/** The collection that declares the table, when one does (a junction has none). */
|
|
36
|
+
slug?: string;
|
|
37
|
+
}
|
|
38
|
+
/** A table as the migrations leave it. */
|
|
39
|
+
interface MigratedTable {
|
|
40
|
+
/** Every column, however it was added. */
|
|
41
|
+
columns: Set<string>;
|
|
42
|
+
/**
|
|
43
|
+
* The columns Atlas wrote: from a `CREATE TABLE` or an `ADD COLUMN` without
|
|
44
|
+
* `IF NOT EXISTS`. The search, vector and trigger DDL `db generate` appends
|
|
45
|
+
* after Atlas's half always says `IF NOT EXISTS`, and the planner keeps
|
|
46
|
+
* those columns out of `TablePlan.columns` — so only these are candidates
|
|
47
|
+
* for "removed from the collection".
|
|
48
|
+
*/
|
|
49
|
+
managed: Set<string>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The tables and columns the migrations leave behind, replayed in order.
|
|
53
|
+
*
|
|
54
|
+
* Exported for its tests.
|
|
55
|
+
*/
|
|
56
|
+
export declare function replayMigrationTables(migrations: readonly string[]): Map<string, MigratedTable>;
|
|
57
|
+
/**
|
|
58
|
+
* What the collections declare that the migrations do not build, and what the
|
|
59
|
+
* migrations still build that the collections dropped.
|
|
60
|
+
*
|
|
61
|
+
* `migrations` are the contents of every `.sql` file in the migrations
|
|
62
|
+
* directory, in file-name order — Atlas's order. An empty list is a project
|
|
63
|
+
* with no migrations at all, for which everything is missing; whether that is a
|
|
64
|
+
* problem (a project on `db push` has none by design) is the caller's call.
|
|
65
|
+
*
|
|
66
|
+
* Throws what {@link planSchema} throws for a configuration it cannot plan.
|
|
67
|
+
*/
|
|
68
|
+
export declare function findUnmigratedSchema(collections: CollectionConfig[], migrations: readonly string[]): UnmigratedChange[];
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DEFAULTs `db push` keeps because a property says the database owns them.
|
|
3
|
+
*
|
|
4
|
+
* `databaseDefault: true` declares a DEFAULT somebody else wrote: a DBA's
|
|
5
|
+
* `now()`, a migration's `'X-' || gen_random_uuid()`. The planner writes no
|
|
6
|
+
* DEFAULT for it, since it has no expression to write, so `schema.sql` holds
|
|
7
|
+
* the column without one. Atlas reads that file as the whole desired state and
|
|
8
|
+
* planned `ALTER COLUMN … DROP DEFAULT`. The API kept letting creates through
|
|
9
|
+
* without the field, and the database then refused them on NOT NULL.
|
|
10
|
+
*
|
|
11
|
+
* Atlas has no way to leave one column's DEFAULT alone: `--exclude` names
|
|
12
|
+
* objects, and a DEFAULT is not one. So the desired state is made to agree with
|
|
13
|
+
* the database instead. Before Atlas plans, the live DEFAULT of each such
|
|
14
|
+
* column is read and appended to `schema.sql` as `ALTER TABLE … SET DEFAULT`,
|
|
15
|
+
* and Atlas finds nothing to change.
|
|
16
|
+
*
|
|
17
|
+
* Two kinds are left out, because Atlas already does something else with them:
|
|
18
|
+
*
|
|
19
|
+
* - a DEFAULT reading a sequence (`nextval(…)`). Atlas does not manage
|
|
20
|
+
* sequences and leaves those DEFAULTs alone, and the sequence does not exist
|
|
21
|
+
* in the scratch database Atlas loads `schema.sql` into.
|
|
22
|
+
* - a DEFAULT calling a function or operator the database defines (an
|
|
23
|
+
* extension's `uuid_generate_v4()`, a project's own function). The scratch
|
|
24
|
+
* database has none of them, and Atlas refuses such a column whether or not
|
|
25
|
+
* it is in `schema.sql`; adding it would only change the error.
|
|
26
|
+
*/
|
|
27
|
+
import { type CollectionConfig } from "@rebasepro/types";
|
|
28
|
+
import type { Queryable } from "./ensure-collection-tables.js";
|
|
29
|
+
/** A column whose DEFAULT the collections leave to the database. */
|
|
30
|
+
export interface DatabaseOwnedDefaultColumn {
|
|
31
|
+
schema: string;
|
|
32
|
+
table: string;
|
|
33
|
+
column: string;
|
|
34
|
+
}
|
|
35
|
+
/** A DEFAULT the database holds for one of those columns, as SQL. */
|
|
36
|
+
export interface KeptDefault extends DatabaseOwnedDefaultColumn {
|
|
37
|
+
expression: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Every column declared `databaseDefault: true` that the planner writes no
|
|
41
|
+
* DEFAULT for. An id strategy or `autoValue` writes one of its own, and that
|
|
42
|
+
* one is the planner's to keep.
|
|
43
|
+
*/
|
|
44
|
+
export declare function databaseOwnedDefaultColumns(collections: CollectionConfig[]): DatabaseOwnedDefaultColumn[];
|
|
45
|
+
/**
|
|
46
|
+
* The live DEFAULTs of `columns` that the desired state must carry so Atlas
|
|
47
|
+
* plans no `DROP DEFAULT`. A column with no DEFAULT, or one Atlas cannot carry,
|
|
48
|
+
* is not listed.
|
|
49
|
+
*
|
|
50
|
+
* Read in a transaction of its own, so the search path it narrows goes back
|
|
51
|
+
* when it ends.
|
|
52
|
+
*/
|
|
53
|
+
export declare function readKeptDefaults(client: Queryable, columns: DatabaseOwnedDefaultColumn[]): Promise<KeptDefault[]>;
|
|
54
|
+
/** What is appended to `schema.sql`: one `SET DEFAULT` per kept DEFAULT. */
|
|
55
|
+
export declare function keptDefaultStatements(kept: KeptDefault[]): string[];
|
|
56
|
+
/** `schema.sql` with the kept DEFAULTs appended, under a comment saying why. */
|
|
57
|
+
export declare function withKeptDefaults(desiredSql: string, kept: KeptDefault[]): string;
|
|
@@ -70,11 +70,12 @@ export declare const RLS_BOOTSTRAP_SQL: string;
|
|
|
70
70
|
* Two independent guards, and both have to pass:
|
|
71
71
|
*
|
|
72
72
|
* 1. **Each function is identified before it is dropped.** Ours returns `text`
|
|
73
|
-
* and reads the `app.uid` GUC; Supabase's returns `uuid`
|
|
74
|
-
* `request.jwt.claims`. Nothing is dropped on a signature we did
|
|
75
|
-
* so a Supabase database — where our `CREATE OR REPLACE` could
|
|
76
|
-
* succeeded in the first place, Postgres refusing to change a
|
|
77
|
-
* matches nothing and this is a no-op.
|
|
73
|
+
* and reads the `app.uid` or `app.user_id` GUC; Supabase's returns `uuid`
|
|
74
|
+
* and reads `request.jwt.claims`. Nothing is dropped on a signature we did
|
|
75
|
+
* not write, so a Supabase database — where our `CREATE OR REPLACE` could
|
|
76
|
+
* never have succeeded in the first place, Postgres refusing to change a
|
|
77
|
+
* return type — matches nothing and this is a no-op. See
|
|
78
|
+
* {@link LEGACY_AUTH_HELPERS} for exactly what is matched.
|
|
78
79
|
* 2. **`DROP SCHEMA … RESTRICT`**, never CASCADE. If anything else at all still
|
|
79
80
|
* lives in `auth` (Supabase's `users` table, its other helpers), the drop
|
|
80
81
|
* fails and the schema stays. CASCADE here would be unrecoverable.
|
|
@@ -88,7 +89,31 @@ export declare const RLS_BOOTSTRAP_SQL: string;
|
|
|
88
89
|
* Callers therefore run this *after* applying policies, and treat a failure as
|
|
89
90
|
* "not yet — try again next boot" rather than as an error.
|
|
90
91
|
*/
|
|
91
|
-
export declare const DROP_LEGACY_AUTH_SCHEMA_SQL
|
|
92
|
+
export declare const DROP_LEGACY_AUTH_SCHEMA_SQL: string;
|
|
93
|
+
/** One object in the `auth` schema that `DROP SCHEMA … RESTRICT` refuses on. */
|
|
94
|
+
export interface LegacyAuthSchemaObject {
|
|
95
|
+
/**
|
|
96
|
+
* As Postgres names it: `function auth.uid()`, `table auth.sessions`.
|
|
97
|
+
* `null` on the one row an empty schema answers with.
|
|
98
|
+
*/
|
|
99
|
+
object: string | null;
|
|
100
|
+
/**
|
|
101
|
+
* `ours` is a helper Rebase wrote, which the drop removes. `lookalike` has a
|
|
102
|
+
* helper's name and result type but a body no release wrote, so the drop
|
|
103
|
+
* leaves it alone. `null` for everything else.
|
|
104
|
+
*/
|
|
105
|
+
helper: "ours" | "lookalike" | null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* What the `auth` schema holds, one row per object keeping it alive.
|
|
109
|
+
*
|
|
110
|
+
* No rows: the schema does not exist. A single row with a `null` object: it
|
|
111
|
+
* exists and is empty. Read from the namespace entries in `pg_depend`, which
|
|
112
|
+
* are what `DROP SCHEMA … RESTRICT` itself consults — minus whatever goes with
|
|
113
|
+
* its owner (a serial's sequence, an identity column's), which Postgres does not
|
|
114
|
+
* name either.
|
|
115
|
+
*/
|
|
116
|
+
export declare const LEGACY_AUTH_SCHEMA_CONTENTS_SQL: string;
|
|
92
117
|
/** Somebody's policy that still calls a pre-1.0 helper. */
|
|
93
118
|
export interface LegacyRlsDependent {
|
|
94
119
|
schema: string;
|
|
@@ -123,11 +148,12 @@ export interface LegacyRlsFunctionDependent {
|
|
|
123
148
|
* exists, which fails at *query* time rather than at boot.
|
|
124
149
|
*
|
|
125
150
|
* A downstream project building on these helpers is not hypothetical: the Rebase
|
|
126
|
-
* control plane
|
|
151
|
+
* control plane defined `auth.is_org_member(uuid)` and `auth.is_org_admin(uuid)`
|
|
127
152
|
* in this very schema, each calling `auth.uid()` in a string body, and eleven of
|
|
128
|
-
* its row-level-security policies
|
|
129
|
-
* started failing the first time a recompile left no policy referencing
|
|
130
|
-
* `auth.uid()` directly — the drop's own precondition.
|
|
153
|
+
* its row-level-security policies went through them. Every one of those would
|
|
154
|
+
* have started failing the first time a recompile left no policy referencing
|
|
155
|
+
* `auth.uid()` directly — the drop's own precondition. (Its migration 0065 has
|
|
156
|
+
* since moved both to `public`, calling `rebase.uid()`.)
|
|
131
157
|
*
|
|
132
158
|
* Matching on the body text is the only option available, and it is deliberately
|
|
133
159
|
* broad: a false positive costs a schema that stays one release longer and says
|
|
@@ -143,6 +169,17 @@ export declare const LEGACY_RLS_FUNCTION_DEPENDENTS_SQL = "\n SELECT n.nspnam
|
|
|
143
169
|
* table not recompiled *yet* — and wrong for the one that never resolves: a
|
|
144
170
|
* hand-written policy nothing will ever rewrite. Then the schema stays forever
|
|
145
171
|
* and nothing ever says why.
|
|
172
|
+
*
|
|
173
|
+
* The drop itself had the same blind spot. A helper it did not recognise as
|
|
174
|
+
* Rebase's stayed, and so did anything else in the schema, behind a
|
|
175
|
+
* `DROP SCHEMA … RESTRICT` whose failure is swallowed so the helpers it did
|
|
176
|
+
* drop stay dropped. The control plane's `auth.uid()` was one such: it read
|
|
177
|
+
* `app.user_id` alone, the drop looked for `app.uid`, and the schema stayed
|
|
178
|
+
* with no word said. So the schema is read twice. Before, so that one with no
|
|
179
|
+
* Rebase helper in it is left alone: one already gone, as on every boot after
|
|
180
|
+
* the drop, or one that was never Rebase's, like Supabase's — whose own
|
|
181
|
+
* policies the checks below would otherwise report as blockers. After, to name
|
|
182
|
+
* whatever is still there.
|
|
146
183
|
*/
|
|
147
184
|
export declare function dropLegacyAuthSchema(run: (sql: string) => Promise<Record<string, unknown>[]>, report: {
|
|
148
185
|
info: (m: string) => void;
|
|
@@ -204,6 +204,24 @@ export declare function appRoleSetupInstructions(connectionRole: string, schemas
|
|
|
204
204
|
* connection cannot create it.
|
|
205
205
|
*/
|
|
206
206
|
export declare function ensureAppRole(run: RawSqlRunner, schemas: string[]): Promise<void>;
|
|
207
|
+
/**
|
|
208
|
+
* The framework's internal tables in `schemas` that the request role can
|
|
209
|
+
* reach — `schema.table`, sorted — when nothing is left to take it back off.
|
|
210
|
+
*
|
|
211
|
+
* Every provisioning boot repairs this: each store revokes on its own table
|
|
212
|
+
* and {@link ensureAppRole}'s sweep revokes on the rest, because the grant
|
|
213
|
+
* tier hands the request role DML on whole schemas. A backend that changes
|
|
214
|
+
* nothing in its database runs neither, so a database whose `rebase_user` can
|
|
215
|
+
* read `refresh_tokens` or `api_keys` would stay that way for as long as it
|
|
216
|
+
* served. This is the question the sweep would have answered, asked instead.
|
|
217
|
+
*
|
|
218
|
+
* "Can reach" is what the role can actually do — through a grant to it, to a
|
|
219
|
+
* role it inherits, or to PUBLIC, on the table or on any of its columns —
|
|
220
|
+
* because that, not how the ACL is spelled, is what a request running as it
|
|
221
|
+
* gets. A table with RLS on is somebody's collection by that same rule as the
|
|
222
|
+
* sweep's, and is left out.
|
|
223
|
+
*/
|
|
224
|
+
export declare function findExposedInternalTables(run: RawSqlRunner, schemas: string[]): Promise<string[]>;
|
|
207
225
|
/**
|
|
208
226
|
* The grant tier of {@link ensureAppRole} on its own: `USAGE` on each existing
|
|
209
227
|
* schema in `schemas`, DML on its tables and `USAGE, SELECT` on its
|
|
@@ -461,6 +461,18 @@ export declare class FetchService {
|
|
|
461
461
|
* Process raw database results into flat rows with relations.
|
|
462
462
|
*/
|
|
463
463
|
private processRowResults;
|
|
464
|
+
/**
|
|
465
|
+
* Every row of a collection's table, as stored, a page at a time, in key
|
|
466
|
+
* order — the driver's `scanStoredRows`.
|
|
467
|
+
*
|
|
468
|
+
* Deliberately none of the read pipeline: no `beforeQuery` (a sweep has no
|
|
469
|
+
* caller to scope to, and the documented hook narrows a caller-less read to
|
|
470
|
+
* the rows owned by nobody), no soft-delete filter, no `afterRead`, no
|
|
471
|
+
* field stripping. The columns come back under their table names; a sweep
|
|
472
|
+
* reads values, not keys. Paged by the key rather than an offset, so a row
|
|
473
|
+
* deleted during the scan never shifts another past the next page.
|
|
474
|
+
*/
|
|
475
|
+
scanStoredRows(collectionPath: string, pageSize?: number): AsyncGenerator<Record<string, unknown>[]>;
|
|
464
476
|
/**
|
|
465
477
|
* Fetch a collection of rows
|
|
466
478
|
*/
|
|
@@ -53,7 +53,9 @@ export declare class ChannelPresenceStore {
|
|
|
53
53
|
* `tablesReady` is now set from a probe of what exists, not from having been
|
|
54
54
|
* the instance that created it.
|
|
55
55
|
*/
|
|
56
|
-
ensureTables(
|
|
56
|
+
ensureTables(options?: {
|
|
57
|
+
provision?: boolean;
|
|
58
|
+
}): Promise<boolean>;
|
|
57
59
|
/** Record (or refresh) a client's presence. */
|
|
58
60
|
track(channel: string, clientId: string, state: Record<string, unknown>): Promise<void>;
|
|
59
61
|
/** Drop one client's presence in one channel. */
|
|
@@ -46,6 +46,22 @@ export declare function resolveNestedPath(path: string, registry: PostgresCollec
|
|
|
46
46
|
* such a path addresses the *link*, not the row.
|
|
47
47
|
*/
|
|
48
48
|
export declare function isJunctionBackedRelation(relation: ResolvedRelation): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Reject a create or update whose body names a relation Rebase does not write
|
|
51
|
+
* — a multi-hop `via` (`writable: false`) — before anything is read or written.
|
|
52
|
+
*
|
|
53
|
+
* A single-hop `via` is written through this door as it shipped (to-one: the
|
|
54
|
+
* target's key; to-many: one junction's rows). A longer chain used to get as
|
|
55
|
+
* far as the writer: a to-many one failed inside the transaction as a 500
|
|
56
|
+
* RELATION_MISCONFIGURED telling the caller to regenerate a schema that was
|
|
57
|
+
* fine, and a to-one one re-pointed the target at the first step's column.
|
|
58
|
+
*
|
|
59
|
+
* Matched the way the write path matches: a key that is a relation's own name
|
|
60
|
+
* (the to-many split), or a `relation` property that resolves to one (the
|
|
61
|
+
* to-one transformer). An `undefined` value is no value — the transformer skips
|
|
62
|
+
* it — so it is not a write; `null` is, since it would unlink.
|
|
63
|
+
*/
|
|
64
|
+
export declare function assertNoReadOnlyRelationValues(values: Record<string, unknown>, collection: CollectionConfig): void;
|
|
49
65
|
/**
|
|
50
66
|
* Reject a nested write whose final segment is a to-one relation.
|
|
51
67
|
*
|
|
@@ -367,6 +367,14 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
367
367
|
private refetchQueue;
|
|
368
368
|
/** Group refetches running now — at most {@link maxConcurrentRefetches}. */
|
|
369
369
|
private refetchesRunning;
|
|
370
|
+
/**
|
|
371
|
+
* What a group refetch runs inside — see {@link setScopeRunner}. A refetch
|
|
372
|
+
* runs on a timer armed by whatever announced the change; one armed by a
|
|
373
|
+
* change-capture notification inherits the LISTEN connection's context,
|
|
374
|
+
* which is no backend's, so a hook the read reached saw the process
|
|
375
|
+
* default as `rebase`.
|
|
376
|
+
*/
|
|
377
|
+
private runInScope;
|
|
370
378
|
/** See {@link refetchConcurrencyFor}. */
|
|
371
379
|
maxConcurrentRefetches: number;
|
|
372
380
|
/**
|
|
@@ -758,6 +766,11 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
758
766
|
* transaction of its own — see {@link collectionMeta}.
|
|
759
767
|
*/
|
|
760
768
|
private fetchCollectionWithAuth;
|
|
769
|
+
/**
|
|
770
|
+
* Refetch `group` — and so count it again — at `at`, when the total its
|
|
771
|
+
* last frame reused expires. One pending at a time per group.
|
|
772
|
+
*/
|
|
773
|
+
private scheduleRecount;
|
|
761
774
|
/**
|
|
762
775
|
* The `meta` a `collection_update` frame carries beside its rows, counted
|
|
763
776
|
* through `service` — the subscriber's own transaction, read only, with
|
|
@@ -988,7 +1001,9 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
988
1001
|
* what it was before this existed, whereas refusing to boot takes the whole
|
|
989
1002
|
* backend down for it.
|
|
990
1003
|
*/
|
|
991
|
-
configureChannelBus(bus: ChannelBus
|
|
1004
|
+
configureChannelBus(bus: ChannelBus, options?: {
|
|
1005
|
+
provision?: boolean;
|
|
1006
|
+
}): Promise<void>;
|
|
992
1007
|
/** Which transport is in use — `"memory"` means per-instance only. */
|
|
993
1008
|
getChannelBusKind(): ChannelBus["kind"];
|
|
994
1009
|
/**
|
|
@@ -1116,6 +1131,8 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
1116
1131
|
* down and being replaced.
|
|
1117
1132
|
*/
|
|
1118
1133
|
isCdcActive(): boolean;
|
|
1134
|
+
/** See `RealtimeProvider.setScopeRunner`. */
|
|
1135
|
+
setScopeRunner(run: <T>(fn: () => T) => T): void;
|
|
1119
1136
|
/** The LISTEN connections this service depends on — see `RealtimeProvider.health`. */
|
|
1120
1137
|
health(): RealtimeListenerHealth[];
|
|
1121
1138
|
/**
|