@rebasepro/server-postgres 0.18.0 → 0.19.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.
Files changed (70) hide show
  1. package/dist/BranchService-ByLfM_Tt.js +360 -0
  2. package/dist/BranchService-ByLfM_Tt.js.map +1 -0
  3. package/dist/__vite-browser-external-BnuHet1e.js +46 -0
  4. package/dist/__vite-browser-external-BnuHet1e.js.map +1 -0
  5. package/dist/arg-Dni7MzLB.js +87 -0
  6. package/dist/arg-Dni7MzLB.js.map +1 -0
  7. package/dist/auth-users-columns-D2LBFrMH.js +116 -0
  8. package/dist/auth-users-columns-D2LBFrMH.js.map +1 -0
  9. package/dist/backup-cli-oM9WQVlV.js +408 -0
  10. package/dist/backup-cli-oM9WQVlV.js.map +1 -0
  11. package/dist/backup-service-Br3tqwsy.js +715 -0
  12. package/dist/backup-service-Br3tqwsy.js.map +1 -0
  13. package/dist/cli-errors-DxN4yzdH.js +657 -0
  14. package/dist/cli-errors-DxN4yzdH.js.map +1 -0
  15. package/dist/cli-output-CNdMql-L.js +47 -0
  16. package/dist/cli-output-CNdMql-L.js.map +1 -0
  17. package/dist/cli.js +2020 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/{collection-index-C7LopMyX.js → collection-index-BHgT_TKd.js} +32 -3
  20. package/dist/{collection-index-C7LopMyX.js.map → collection-index-BHgT_TKd.js.map} +1 -1
  21. package/dist/{collections-schema-version-BMeu3cgv.js → collections-schema-version-T6cGvrDi.js} +1 -2
  22. package/dist/{collections-schema-version-BMeu3cgv.js.map → collections-schema-version-T6cGvrDi.js.map} +1 -1
  23. package/dist/databasePoolManager-Bj5FbeAs.js +349 -0
  24. package/dist/databasePoolManager-Bj5FbeAs.js.map +1 -0
  25. package/dist/doctor-D7HWmGzs.js +974 -0
  26. package/dist/doctor-D7HWmGzs.js.map +1 -0
  27. package/dist/{ensure-collection-policies-CagOzEPS.js → ensure-collection-policies-2c2NV8zX.js} +5 -25
  28. package/dist/ensure-collection-policies-2c2NV8zX.js.map +1 -0
  29. package/dist/ensure-collection-tables-D4wJzM4y.js +820 -0
  30. package/dist/ensure-collection-tables-D4wJzM4y.js.map +1 -0
  31. package/dist/ensure-tables-CIojK-Rx.js +650 -0
  32. package/dist/ensure-tables-CIojK-Rx.js.map +1 -0
  33. package/dist/generate-drizzle-schema-CwEgVyjO.js +123 -0
  34. package/dist/generate-drizzle-schema-CwEgVyjO.js.map +1 -0
  35. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +528 -0
  36. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +1 -0
  37. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js +1312 -0
  38. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +1 -0
  39. package/dist/generated-schema-staleness-DQdfJl2A.js +157 -0
  40. package/dist/generated-schema-staleness-DQdfJl2A.js.map +1 -0
  41. package/dist/index.es.js +849 -10807
  42. package/dist/index.es.js.map +1 -1
  43. package/dist/introspect-db-logic-C6LQdTxj.js +1378 -0
  44. package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -0
  45. package/dist/policy-drift-kNJ6X2Uj.js +313 -0
  46. package/dist/policy-drift-kNJ6X2Uj.js.map +1 -0
  47. package/dist/rls-bootstrap-sql-DLDnUjtx.js +7092 -0
  48. package/dist/rls-bootstrap-sql-DLDnUjtx.js.map +1 -0
  49. package/dist/{rls-enforcement-ClJoTboT.js → rls-enforcement-DeYEjYLt.js} +14 -3
  50. package/dist/{rls-enforcement-ClJoTboT.js.map → rls-enforcement-DeYEjYLt.js.map} +1 -1
  51. package/dist/schema/bin/generate-drizzle-schema.d.ts +1 -0
  52. package/dist/schema/bin/generate-postgres-ddl.d.ts +1 -0
  53. package/dist/schema/doctor-cli.js +129 -0
  54. package/dist/schema/doctor-cli.js.map +1 -0
  55. package/dist/schema/generate-drizzle-schema.d.ts +1 -1
  56. package/dist/schema/generate-drizzle-schema.js +24 -0
  57. package/dist/schema/generate-drizzle-schema.js.map +1 -0
  58. package/dist/schema/generate-postgres-ddl.d.ts +1 -1
  59. package/dist/schema/generate-postgres-ddl.js +138 -0
  60. package/dist/schema/generate-postgres-ddl.js.map +1 -0
  61. package/dist/schema/introspect-db.js +866 -0
  62. package/dist/schema/introspect-db.js.map +1 -0
  63. package/dist/source-Br7L7GOI.js +410 -0
  64. package/dist/source-Br7L7GOI.js.map +1 -0
  65. package/dist/vector-index-DkWxm2de.js +309 -0
  66. package/dist/vector-index-DkWxm2de.js.map +1 -0
  67. package/package.json +7 -7
  68. package/dist/ensure-collection-policies-CagOzEPS.js.map +0 -1
  69. package/dist/ensure-collection-tables-DbAhrjnG.js +0 -2732
  70. package/dist/ensure-collection-tables-DbAhrjnG.js.map +0 -1
@@ -1,11 +1,22 @@
1
1
  import { createRequire as __createRequire } from "module";
2
- import "process";
3
2
  __createRequire(import.meta.url);
3
+ import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
4
4
  import { logger } from "@rebasepro/server";
5
5
  import { sql } from "drizzle-orm";
6
6
  import { ANONYMOUS_USER_ID, REBASE_SCHEMA, usesLegacyRlsFunctions } from "@rebasepro/types";
7
7
  import { REBASE_USER_ROLE, findAnonymousGrants, revokeInternalTableAccess, securityRuleToConditions } from "@rebasepro/common";
8
8
  //#region src/security/rls-enforcement.ts
9
+ var rls_enforcement_exports = /* @__PURE__ */ __exportAll({
10
+ REBASE_USER_ROLE: () => REBASE_USER_ROLE,
11
+ appRoleSetupInstructions: () => appRoleSetupInstructions,
12
+ applyAuthContext: () => applyAuthContext,
13
+ detectConnectionPosture: () => detectConnectionPosture,
14
+ ensureAppRole: () => ensureAppRole,
15
+ validatePolicyPgRoles: () => validatePolicyPgRoles,
16
+ warnOnAnonymousGrants: () => warnOnAnonymousGrants,
17
+ warnOnLegacyRlsFunctions: () => warnOnLegacyRlsFunctions,
18
+ warnOnRoleSchemaCollision: () => warnOnRoleSchemaCollision
19
+ });
9
20
  var quoteIdent = (name) => `"${name.replace(/"/g, "\"\"")}"`;
10
21
  /** DML the user role holds on managed tables (RLS still filters per row). */
11
22
  var USER_TABLE_PRIVILEGES = "SELECT, INSERT, UPDATE, DELETE";
@@ -326,6 +337,6 @@ async function validatePolicyPgRoles(run, collections, requestRole = REBASE_USER
326
337
  if (problems.length > 0) throw new Error(`Security rules target PostgreSQL roles this server cannot use. Requests run as "${requestRole}", so these policies would never apply and every row would be filtered out — the collections would look empty rather than error.\n\n` + problems.join("\n\n") + "\n");
327
338
  }
328
339
  //#endregion
329
- export { validatePolicyPgRoles as a, warnOnRoleSchemaCollision as c, ensureAppRole as i, applyAuthContext as n, warnOnAnonymousGrants as o, detectConnectionPosture as r, warnOnLegacyRlsFunctions as s, REBASE_USER_ROLE as t };
340
+ export { rls_enforcement_exports as a, warnOnLegacyRlsFunctions as c, ensureAppRole as i, warnOnRoleSchemaCollision as l, applyAuthContext as n, validatePolicyPgRoles as o, detectConnectionPosture as r, warnOnAnonymousGrants as s, REBASE_USER_ROLE as t };
330
341
 
331
- //# sourceMappingURL=rls-enforcement-ClJoTboT.js.map
342
+ //# sourceMappingURL=rls-enforcement-DeYEjYLt.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rls-enforcement-ClJoTboT.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_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 `SET LOCAL ROLE rebase_user` so\n * RLS binds. Transaction-scoped, so it 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\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 * 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 await run(`CREATE ROLE ${REBASE_USER_ROLE} NOLOGIN NOSUPERUSER NOBYPASSRLS NOINHERIT`);\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. Table owners (the expected non-superuser posture) can\n // always grant on their own objects, so this tier needs no extra privilege.\n const nspRows = await run(\"SELECT nspname FROM pg_namespace\");\n const existing = new Set(nspRows.map((r) => String(r.nspname)));\n for (const schema of uniqueSchemas) {\n if (!existing.has(schema)) continue;\n const s = quoteIdent(schema);\n await run(`GRANT USAGE ON SCHEMA ${s} TO ${REBASE_USER_ROLE}`);\n await run(`GRANT ${USER_TABLE_PRIVILEGES} ON ALL TABLES IN SCHEMA ${s} TO ${REBASE_USER_ROLE}`);\n 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 await run(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${s} GRANT ${USER_TABLE_PRIVILEGES} ON TABLES TO ${REBASE_USER_ROLE}`);\n await run(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${s} GRANT USAGE, SELECT ON SEQUENCES TO ${REBASE_USER_ROLE}`);\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 await revokeInternalTableAccess(async (text) => { await run(text); }, schema, {\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 logger.debug(`🔐 [rls] User role \"${REBASE_USER_ROLE}\" provisioned (schemas: ${uniqueSchemas.join(\", \")})`);\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 — `SET LOCAL ROLE` so RLS binds every statement in this\n * 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 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)\n `);\n if (userRole) {\n await tx.execute(drizzleSql.raw(`SET LOCAL ROLE ${quoteIdent(userRole)}`));\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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"}
1
+ {"version":3,"file":"rls-enforcement-DeYEjYLt.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_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 `SET LOCAL ROLE rebase_user` so\n * RLS binds. Transaction-scoped, so it 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\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 * 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 await run(`CREATE ROLE ${REBASE_USER_ROLE} NOLOGIN NOSUPERUSER NOBYPASSRLS NOINHERIT`);\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. Table owners (the expected non-superuser posture) can\n // always grant on their own objects, so this tier needs no extra privilege.\n const nspRows = await run(\"SELECT nspname FROM pg_namespace\");\n const existing = new Set(nspRows.map((r) => String(r.nspname)));\n for (const schema of uniqueSchemas) {\n if (!existing.has(schema)) continue;\n const s = quoteIdent(schema);\n await run(`GRANT USAGE ON SCHEMA ${s} TO ${REBASE_USER_ROLE}`);\n await run(`GRANT ${USER_TABLE_PRIVILEGES} ON ALL TABLES IN SCHEMA ${s} TO ${REBASE_USER_ROLE}`);\n 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 await run(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${s} GRANT ${USER_TABLE_PRIVILEGES} ON TABLES TO ${REBASE_USER_ROLE}`);\n await run(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${s} GRANT USAGE, SELECT ON SEQUENCES TO ${REBASE_USER_ROLE}`);\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 await revokeInternalTableAccess(async (text) => { await run(text); }, schema, {\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 logger.debug(`🔐 [rls] User role \"${REBASE_USER_ROLE}\" provisioned (schemas: ${uniqueSchemas.join(\", \")})`);\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 — `SET LOCAL ROLE` so RLS binds every statement in this\n * 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 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)\n `);\n if (userRole) {\n await tx.execute(drizzleSql.raw(`SET LOCAL ROLE ${quoteIdent(userRole)}`));\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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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __createRequire } from "module";
3
+ __createRequire(import.meta.url);
4
+ import { i as diagnoseDbError } from "../cli-errors-DxN4yzdH.js";
5
+ import { o as validatePolicyPgRoles, s as warnOnAnonymousGrants } from "../rls-enforcement-DeYEjYLt.js";
6
+ import { t as chalk } from "../source-Br7L7GOI.js";
7
+ import { n as loadCollections, r as runDoctor } from "../doctor-D7HWmGzs.js";
8
+ import { n as formatPolicyDrift, r as hasDrift, t as checkPolicyDrift } from "../policy-drift-kNJ6X2Uj.js";
9
+ import path from "path";
10
+ //#region src/schema/doctor-policy-checks.ts
11
+ /**
12
+ * The RLS half of `rebase doctor`, and the exit code `--policies` gates CI on.
13
+ *
14
+ * Lives beside `doctor-cli.ts` rather than inside it because that file runs
15
+ * `main()` on import: a gate whose failure modes cannot be unit-tested is how
16
+ * this one shipped reporting success for work it never did.
17
+ */
18
+ /**
19
+ * The exit code for `rebase doctor --policies`.
20
+ *
21
+ * A gate that could not run has not passed. Only a completed, clean check
22
+ * exits 0 — anything else, including "we never opened a connection", is a
23
+ * failure, or the flag certifies a database nobody looked at.
24
+ */
25
+ function exitCodeForPolicyGate(status) {
26
+ return status === "ok" ? 0 : 1;
27
+ }
28
+ /**
29
+ * Policies actually deployed vs the ones the collections describe, plus policy
30
+ * roles this server could never satisfy.
31
+ *
32
+ * Never reports `ok` for work it did not do — see {@link PolicyCheckStatus}.
33
+ */
34
+ async function runPolicyChecks(collectionsPath, databaseUrl) {
35
+ if (!databaseUrl) {
36
+ console.error(chalk.yellow(" ⚠ No DATABASE_URL — RLS policies were NOT checked"));
37
+ return "unchecked";
38
+ }
39
+ const resolvedPath = path.resolve(process.cwd(), collectionsPath);
40
+ let problems = false;
41
+ const { Pool } = await import("pg");
42
+ const pool = new Pool({ connectionString: databaseUrl });
43
+ try {
44
+ const collections = await loadCollections(resolvedPath);
45
+ if (collections.length === 0) {
46
+ console.error(chalk.red(` ✗ No collections found in ${resolvedPath}`));
47
+ console.error(chalk.gray(" RLS policies were NOT checked. Pass --collections=<dir> if your collections live elsewhere."));
48
+ return "unchecked";
49
+ }
50
+ const runSql = async (text) => (await pool.query(text)).rows;
51
+ try {
52
+ await runSql("SELECT 1");
53
+ } catch (err) {
54
+ console.error(chalk.gray(" ⏭ Policy roles: not checked"));
55
+ console.error(chalk.red(" ✗ Could not check RLS policies:"), err instanceof Error ? err.message : String(err));
56
+ return "unchecked";
57
+ }
58
+ try {
59
+ await validatePolicyPgRoles(runSql, collections);
60
+ console.log(chalk.green(" ✓ Policy roles are usable by this server"));
61
+ } catch (err) {
62
+ problems = true;
63
+ console.log("");
64
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
65
+ }
66
+ warnOnAnonymousGrants(collections);
67
+ const drift = await checkPolicyDrift(pool, collections);
68
+ console.log("");
69
+ if (hasDrift(drift)) {
70
+ problems = true;
71
+ console.log(chalk.yellow(" RLS policies: database does not match your collections"));
72
+ console.log(formatPolicyDrift(drift));
73
+ } else console.log(chalk.green(` ✓ RLS policies match your collections (${collections.length} collection(s) checked)`));
74
+ } catch (err) {
75
+ console.error(chalk.red(" ✗ Could not check RLS policies:"), err instanceof Error ? err.message : String(err));
76
+ return "unchecked";
77
+ } finally {
78
+ await pool.end();
79
+ }
80
+ return problems ? "problems" : "ok";
81
+ }
82
+ //#endregion
83
+ //#region src/schema/doctor-cli.ts
84
+ /**
85
+ * CLI entry point for `rebase doctor`.
86
+ * Invoked via tsx by the server-postgres CLI plugin.
87
+ */
88
+ async function main() {
89
+ const collectionsArg = process.argv.find((a) => a.startsWith("--collections="));
90
+ const schemaArg = process.argv.find((a) => a.startsWith("--schema="));
91
+ const sdkArg = process.argv.find((a) => a.startsWith("--sdk="));
92
+ const policiesOnly = process.argv.includes("--policies");
93
+ const collectionsPath = collectionsArg?.split("=")[1] ?? path.join("..", "config", "collections");
94
+ const schemaPath = schemaArg?.split("=")[1] ?? path.join("src", "schema.generated.ts");
95
+ const sdkPath = sdkArg?.split("=")[1] ?? path.join("..", "generated", "sdk", "database.types.ts");
96
+ try {
97
+ const dotenv = await import("dotenv");
98
+ const envPath = process.env.DOTENV_CONFIG_PATH;
99
+ if (envPath) dotenv.config({
100
+ path: envPath,
101
+ quiet: true
102
+ });
103
+ else dotenv.config({ quiet: true });
104
+ } catch {}
105
+ const databaseUrl = process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING;
106
+ if (policiesOnly) process.exit(exitCodeForPolicyGate(await runPolicyChecks(collectionsPath, databaseUrl)));
107
+ const report = await runDoctor({
108
+ collectionsPath: path.resolve(process.cwd(), collectionsPath),
109
+ schemaPath: path.resolve(process.cwd(), schemaPath),
110
+ sdkPath: path.resolve(process.cwd(), sdkPath),
111
+ databaseUrl: databaseUrl ?? void 0
112
+ });
113
+ const policyStatus = await runPolicyChecks(collectionsPath, databaseUrl);
114
+ const blocked = [
115
+ report.collectionsToSchema,
116
+ report.schemaToDatabase,
117
+ report.collectionsToSdk
118
+ ].some((phase) => phase.blocked);
119
+ if (report.summary.errors > 0 || policyStatus === "problems" || blocked) process.exit(1);
120
+ }
121
+ main().catch((err) => {
122
+ const banner = diagnoseDbError(err, process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING);
123
+ if (banner) console.error(banner);
124
+ else console.error(chalk.red(" ✗ Doctor failed"), err instanceof Error ? err.stack ?? err.message : String(err));
125
+ process.exit(1);
126
+ });
127
+ //#endregion
128
+
129
+ //# sourceMappingURL=doctor-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor-cli.js","names":[],"sources":["../../src/schema/doctor-policy-checks.ts","../../src/schema/doctor-cli.ts"],"sourcesContent":["/**\n * The RLS half of `rebase doctor`, and the exit code `--policies` gates CI on.\n *\n * Lives beside `doctor-cli.ts` rather than inside it because that file runs\n * `main()` on import: a gate whose failure modes cannot be unit-tested is how\n * this one shipped reporting success for work it never did.\n */\nimport path from \"path\";\nimport chalk from \"chalk\";\nimport { CollectionConfig } from \"@rebasepro/types\";\nimport { loadCollections } from \"./doctor\";\nimport { checkPolicyDrift, formatPolicyDrift, hasDrift } from \"../security/policy-drift\";\nimport { validatePolicyPgRoles, warnOnAnonymousGrants } from \"../security/rls-enforcement\";\n\n/**\n * What the RLS checks concluded.\n *\n * `unchecked` exists because \"we could not look\" and \"we looked and it is fine\"\n * used to be reported identically: a collections path that did not resolve made\n * the loader return `[]` (it warns, it does not throw), `checkPolicyDrift`\n * early-returned an empty diff, and the gate printed\n * `✓ RLS policies match your collections` having compared zero policies against\n * zero collections. Any exception at all — a collection file that throws on\n * import, a `pg_policies` read the CI role is not granted, a connection reset —\n * did the same thing through a `warn`, and exited 0.\n */\nexport type PolicyCheckStatus = \"ok\" | \"problems\" | \"unchecked\";\n\n/**\n * The exit code for `rebase doctor --policies`.\n *\n * A gate that could not run has not passed. Only a completed, clean check\n * exits 0 — anything else, including \"we never opened a connection\", is a\n * failure, or the flag certifies a database nobody looked at.\n */\nexport function exitCodeForPolicyGate(status: PolicyCheckStatus): 0 | 1 {\n return status === \"ok\" ? 0 : 1;\n}\n\n/**\n * Policies actually deployed vs the ones the collections describe, plus policy\n * roles this server could never satisfy.\n *\n * Never reports `ok` for work it did not do — see {@link PolicyCheckStatus}.\n */\nexport async function runPolicyChecks(collectionsPath: string, databaseUrl?: string): Promise<PolicyCheckStatus> {\n if (!databaseUrl) {\n console.error(chalk.yellow(\" ⚠ No DATABASE_URL — RLS policies were NOT checked\"));\n return \"unchecked\";\n }\n\n const resolvedPath = path.resolve(process.cwd(), collectionsPath);\n let problems = false;\n const { Pool } = await import(\"pg\");\n const pool = new Pool({ connectionString: databaseUrl });\n try {\n const collections: CollectionConfig[] = await loadCollections(resolvedPath);\n\n // Zero collections is not \"no drift\", it is nothing to compare. The\n // loader returns `[]` for a path that does not exist, so the commonest\n // way to get here is a `--collections` path resolved against the wrong\n // directory — which used to render as a green tick.\n if (collections.length === 0) {\n console.error(chalk.red(` ✗ No collections found in ${resolvedPath}`));\n console.error(chalk.gray(\" RLS policies were NOT checked. Pass --collections=<dir> if your collections live elsewhere.\"));\n return \"unchecked\";\n }\n\n const runSql = async (text: string) => (await pool.query(text)).rows as Record<string, unknown>[];\n\n // Prove the connection before anything is ticked.\n //\n // `validatePolicyPgRoles` returns without a query when no collection\n // names a `pgRoles` — which is most projects — so it \"passed\" against a\n // database that was never reached, and the tick printed above an\n // ECONNREFUSED that only surfaced two checks later. Ten lines up, this\n // file says that reporting success for work it never did is the reason\n // it exists. This is the same shape, one function along.\n try {\n await runSql(\"SELECT 1\");\n } catch (err) {\n console.error(chalk.gray(\" ⏭ Policy roles: not checked\"));\n console.error(chalk.red(\" ✗ Could not check RLS policies:\"), err instanceof Error ? err.message : String(err));\n return \"unchecked\";\n }\n\n // A policy naming a role the server never runs as filters every row, so\n // the collection reads as empty. Report it without booting a server.\n try {\n await validatePolicyPgRoles(runSql, collections as never);\n console.log(chalk.green(\" ✓ Policy roles are usable by this server\"));\n } catch (err) {\n problems = true;\n console.log(\"\");\n console.error(chalk.red(err instanceof Error ? err.message : String(err)));\n }\n\n // A rule that reads as a lockdown but is true for every caller compiles\n // to a grant. Report it without booting a server.\n warnOnAnonymousGrants(collections as never);\n\n const drift = await checkPolicyDrift(pool as never, collections);\n console.log(\"\");\n if (hasDrift(drift)) {\n problems = true;\n console.log(chalk.yellow(\" RLS policies: database does not match your collections\"));\n console.log(formatPolicyDrift(drift));\n } else {\n console.log(chalk.green(` ✓ RLS policies match your collections (${collections.length} collection(s) checked)`));\n }\n } catch (err) {\n // Fail closed. This catch covers every query and every collection import\n // above, so returning the pre-catch verdict made the documented CI gate\n // green on exactly the runs it exists to catch.\n console.error(chalk.red(\" ✗ Could not check RLS policies:\"), err instanceof Error ? err.message : String(err));\n return \"unchecked\";\n } finally {\n await pool.end();\n }\n return problems ? \"problems\" : \"ok\";\n}\n","#!/usr/bin/env node\n/**\n * CLI entry point for `rebase doctor`.\n * Invoked via tsx by the server-postgres CLI plugin.\n */\nimport path from \"path\";\nimport chalk from \"chalk\";\nimport { runDoctor } from \"./doctor\";\nimport { exitCodeForPolicyGate, runPolicyChecks } from \"./doctor-policy-checks\";\nimport { diagnoseDbError } from \"../cli-errors\";\n\nasync function main() {\n const collectionsArg = process.argv.find((a) => a.startsWith(\"--collections=\"));\n const schemaArg = process.argv.find((a) => a.startsWith(\"--schema=\"));\n const sdkArg = process.argv.find((a) => a.startsWith(\"--sdk=\"));\n // --policies runs only the RLS checks: no schema diff, no SDK types. Useful\n // as a CI gate against a deployed database, where the collection files and\n // generated schema are not what you are asking about.\n const policiesOnly = process.argv.includes(\"--policies\");\n\n const collectionsPath = collectionsArg?.split(\"=\")[1] ?? path.join(\"..\", \"config\", \"collections\");\n const schemaPath = schemaArg?.split(\"=\")[1] ?? path.join(\"src\", \"schema.generated.ts\");\n const sdkPath = sdkArg?.split(\"=\")[1] ?? path.join(\"..\", \"generated\", \"sdk\", \"database.types.ts\");\n\n // Load .env\n try {\n const dotenv = await import(\"dotenv\");\n const envPath = process.env.DOTENV_CONFIG_PATH;\n if (envPath) {\n dotenv.config({ path: envPath, quiet: true });\n } else {\n dotenv.config({ quiet: true });\n }\n } catch {\n // dotenv may not be installed\n }\n\n const databaseUrl = process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING;\n\n if (policiesOnly) {\n // Non-zero so this can gate CI — the whole point of the flag, and a\n // check that could not run has not passed. See exitCodeForPolicyGate.\n process.exit(exitCodeForPolicyGate(await runPolicyChecks(collectionsPath, databaseUrl)));\n }\n\n const report = await runDoctor({\n collectionsPath: path.resolve(process.cwd(), collectionsPath),\n schemaPath: path.resolve(process.cwd(), schemaPath),\n sdkPath: path.resolve(process.cwd(), sdkPath),\n databaseUrl: databaseUrl ?? undefined\n });\n\n const policyStatus = await runPolicyChecks(collectionsPath, databaseUrl);\n\n // Exit non-zero if there are errors. A policy run that could not happen is\n // reported loudly above but does not fail the interactive command — the\n // same treatment the *chosen* skip of the database phase gets. `--policies`\n // is the gate, and that one fails closed.\n //\n // A phase that was skipped by a fault is different, and `blocked` is what\n // separates the two: \"no DATABASE_URL\" is a local situation somebody may be\n // content with, while \"the DATABASE_URL you set refuses connections\" would\n // otherwise let `rebase doctor` go green in CI against a database it never\n // reached.\n const blocked = [report.collectionsToSchema, report.schemaToDatabase, report.collectionsToSdk]\n .some(phase => phase.blocked);\n if (report.summary.errors > 0 || policyStatus === \"problems\" || blocked) {\n process.exit(1);\n }\n}\n\nmain().catch((err) => {\n // A known database failure has a diagnosis; a stack trace through pg and\n // drizzle internals is not it. This is the last resort — `runDoctor`\n // already catches the connection failure it can name — so it exists for\n // everything reached after the report, the policy checks in particular.\n const banner = diagnoseDbError(err, process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING);\n if (banner) {\n console.error(banner);\n } else {\n console.error(chalk.red(\" ✗ Doctor failed\"), err instanceof Error ? (err.stack ?? err.message) : String(err));\n }\n process.exit(1);\n});\n"],"mappings":"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"}
@@ -1 +1 @@
1
- export {};
1
+ export declare const main: () => Promise<void>;
@@ -0,0 +1,24 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ __createRequire(import.meta.url);
3
+ import { t as main } from "../generate-drizzle-schema-CwEgVyjO.js";
4
+ //#region src/schema/bin/generate-drizzle-schema.ts
5
+ /**
6
+ * The built entry point for `generate-drizzle-schema`.
7
+ *
8
+ * `generate-drizzle-schema.ts` guards its own `main()` with
9
+ * `import.meta.url.endsWith(process.argv[1])`, because `src/index.ts` re-exports
10
+ * the module and importing the driver must not run the generator. That guard is
11
+ * correct when the file IS the process entry — which is how it runs from source
12
+ * under tsx — and silently false once the module is bundled: the executed file
13
+ * is a stub, the code lives in a shared chunk, and the chunk's `import.meta.url`
14
+ * is not the stub's path. The generator then did nothing, exited 0, and the
15
+ * caller reported success over an unwritten schema.
16
+ *
17
+ * So the built path gets an entry that says what it means.
18
+ */
19
+ main().catch(() => {
20
+ process.exitCode = 1;
21
+ });
22
+ //#endregion
23
+
24
+ //# sourceMappingURL=generate-drizzle-schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-drizzle-schema.js","names":[],"sources":["../../src/schema/bin/generate-drizzle-schema.ts"],"sourcesContent":["/**\n * The built entry point for `generate-drizzle-schema`.\n *\n * `generate-drizzle-schema.ts` guards its own `main()` with\n * `import.meta.url.endsWith(process.argv[1])`, because `src/index.ts` re-exports\n * the module and importing the driver must not run the generator. That guard is\n * correct when the file IS the process entry — which is how it runs from source\n * under tsx — and silently false once the module is bundled: the executed file\n * is a stub, the code lives in a shared chunk, and the chunk's `import.meta.url`\n * is not the stub's path. The generator then did nothing, exited 0, and the\n * caller reported success over an unwritten schema.\n *\n * So the built path gets an entry that says what it means.\n */\nimport { main } from \"../generate-drizzle-schema\";\n\nmain().catch(() => {\n process.exitCode = 1;\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;AAgBA,KAAK,CAAC,CAAC,YAAY;CACf,QAAQ,WAAW;AACvB,CAAC"}
@@ -1 +1 @@
1
- export {};
1
+ export declare const main: () => Promise<void>;
@@ -0,0 +1,138 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ __createRequire(import.meta.url);
3
+ import { a as generatePostgresVectorDdl, i as generatePostgresSearchDdl, n as generatePostgresDdl, r as generatePostgresPoliciesDdl } from "../generate-postgres-ddl-logic-CtnpIBVv.js";
4
+ import { n as outError, t as out } from "../cli-output-CNdMql-L.js";
5
+ import { loadCollectionsFromDirectory } from "@rebasepro/server";
6
+ import { declaredDatabaseExtensions } from "@rebasepro/types";
7
+ import * as fs$1 from "fs";
8
+ import { promises } from "fs";
9
+ import path from "path";
10
+ import { pathToFileURL } from "url";
11
+ //#region src/schema/generate-postgres-ddl.ts
12
+ /** Where a project declares its resources, relative to the config directory. */
13
+ var RESOURCE_ENTRIES = [
14
+ "resources.ts",
15
+ "resources.js",
16
+ "resources/index.ts",
17
+ "resources/index.js"
18
+ ];
19
+ /**
20
+ * Evaluate the project's `resources.ts`, so `declaredDatabaseExtensions()` can
21
+ * answer.
22
+ *
23
+ * This generator runs as its own tsx subprocess and loads only the collections
24
+ * directory, so nothing else in it would ever import the resources module — and
25
+ * a permission that reads as absent because nobody loaded it is the worst shape
26
+ * this could take: the push succeeds, `vector.sql` quietly omits the
27
+ * `CREATE EXTENSION`, and the failure lands one statement later looking like a
28
+ * database problem.
29
+ *
30
+ * Imported directly rather than through the config index, for the reason
31
+ * `boot/resource-loading.ts` gives: re-exporting from the index would work only
32
+ * for projects that remember to.
33
+ *
34
+ * A project with no resources module is the normal case and declares nothing.
35
+ * An import that *throws* is not swallowed — a resources module that does not
36
+ * evaluate is a configuration error, and treating it as "declared nothing"
37
+ * would turn it into a silently missing extension.
38
+ */
39
+ async function loadDeclaredResources(configDir) {
40
+ const entry = RESOURCE_ENTRIES.map((name) => path.join(configDir, name)).find((candidate) => fs$1.existsSync(candidate));
41
+ if (!entry) return;
42
+ await import(pathToFileURL(entry).href);
43
+ }
44
+ var runGeneration = async (collectionsFilePath, outputPath) => {
45
+ try {
46
+ if (!collectionsFilePath) throw new Error("No collections file path provided.");
47
+ const resolvedPath = path.resolve(collectionsFilePath);
48
+ await loadDeclaredResources(path.dirname(resolvedPath));
49
+ let collections = await loadCollectionsFromDirectory(resolvedPath);
50
+ if (!collections || !Array.isArray(collections)) collections = [];
51
+ collections.sort((a, b) => a.slug.localeCompare(b.slug));
52
+ const ddlContent = await generatePostgresDdl(collections, {
53
+ includePolicies: false,
54
+ includeSearch: false,
55
+ includeVector: false
56
+ });
57
+ const policiesContent = generatePostgresPoliciesDdl(collections);
58
+ const searchContent = generatePostgresSearchDdl(collections);
59
+ const vectorContent = generatePostgresVectorDdl(collections, { extensions: declaredDatabaseExtensions() });
60
+ if (outputPath) {
61
+ const outputDir = path.dirname(outputPath);
62
+ await promises.mkdir(outputDir, { recursive: true });
63
+ await promises.writeFile(outputPath, ddlContent);
64
+ out(`✅ PostgreSQL DDL generated successfully at ${outputPath}`);
65
+ const policiesPath = path.join(outputDir, "policies.sql");
66
+ await promises.writeFile(policiesPath, policiesContent);
67
+ out(`✅ PostgreSQL Policies DDL generated successfully at ${policiesPath}`);
68
+ const searchPath = path.join(outputDir, "search.sql");
69
+ if (searchContent) {
70
+ await promises.writeFile(searchPath, searchContent);
71
+ out(`✅ PostgreSQL search DDL generated successfully at ${searchPath}`);
72
+ } else if (fs$1.existsSync(searchPath)) await promises.rm(searchPath);
73
+ const vectorPath = path.join(outputDir, "vector.sql");
74
+ if (vectorContent) {
75
+ await promises.writeFile(vectorPath, vectorContent);
76
+ out(`✅ PostgreSQL vector DDL generated successfully at ${vectorPath}`);
77
+ } else if (fs$1.existsSync(vectorPath)) await promises.rm(vectorPath);
78
+ } else {
79
+ out("✅ PostgreSQL DDL generated successfully.");
80
+ out(String(ddlContent));
81
+ out("\n✅ PostgreSQL Policies DDL generated successfully.");
82
+ out(String(policiesContent));
83
+ }
84
+ } catch (error) {
85
+ outError(`Error generating DDL schema: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
86
+ process.exitCode = 1;
87
+ throw error;
88
+ }
89
+ };
90
+ var main = async () => {
91
+ const collectionsFilePathArg = process.argv.find((arg) => arg.startsWith("--collections="));
92
+ const collectionsFilePath = collectionsFilePathArg ? collectionsFilePathArg.split("=")[1] : process.argv[2];
93
+ const outputPathArg = process.argv.find((arg) => arg.startsWith("--output="));
94
+ const outputPath = outputPathArg ? outputPathArg.split("=")[1] : void 0;
95
+ const watch = process.argv.includes("--watch");
96
+ if (!collectionsFilePath) {
97
+ out("Usage: ts-node generate-postgres-ddl.ts <path-to-collections-file> [--output <path-to-output-file>] [--watch]");
98
+ return;
99
+ }
100
+ const resolvedPath = path.resolve(process.cwd(), collectionsFilePath);
101
+ const resolvedOutputPath = outputPath ? path.resolve(process.cwd(), outputPath) : void 0;
102
+ if (watch) {
103
+ out(`Watching for changes in ${resolvedPath}...`);
104
+ const { default: chokidar } = await import("chokidar");
105
+ chokidar.watch(resolvedPath, {
106
+ persistent: true,
107
+ ignoreInitial: false
108
+ }).on("all", (event, filePath) => {
109
+ out(`[${event}] ${filePath}. Regenerating DDL schema...`);
110
+ runGeneration(resolvedPath, resolvedOutputPath).catch(() => void 0);
111
+ });
112
+ } else await runGeneration(resolvedPath, resolvedOutputPath);
113
+ };
114
+ if (import.meta.url.endsWith(process.argv[1])) main().catch(() => {
115
+ process.exitCode = 1;
116
+ });
117
+ //#endregion
118
+ //#region src/schema/bin/generate-postgres-ddl.ts
119
+ /**
120
+ * The built entry point for `generate-postgres-ddl`.
121
+ *
122
+ * `generate-postgres-ddl.ts` guards its own `main()` with
123
+ * `import.meta.url.endsWith(process.argv[1])`, because `src/index.ts` re-exports
124
+ * the module and importing the driver must not run the generator. That guard is
125
+ * correct when the file IS the process entry — which is how it runs from source
126
+ * under tsx — and silently false once the module is bundled: the executed file
127
+ * is a stub, the code lives in a shared chunk, and the chunk's `import.meta.url`
128
+ * is not the stub's path. The generator then did nothing, exited 0, and the
129
+ * caller reported success over an unwritten schema.
130
+ *
131
+ * So the built path gets an entry that says what it means.
132
+ */
133
+ main().catch(() => {
134
+ process.exitCode = 1;
135
+ });
136
+ //#endregion
137
+
138
+ //# sourceMappingURL=generate-postgres-ddl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-postgres-ddl.js","names":[],"sources":["../../src/schema/generate-postgres-ddl.ts","../../src/schema/bin/generate-postgres-ddl.ts"],"sourcesContent":["import { promises as fsPromises } from \"fs\";\nimport * as fs from \"fs\";\nimport path from \"path\";\nimport { pathToFileURL } from \"url\";\nimport {\n generatePostgresDdl,\n generatePostgresPoliciesDdl,\n generatePostgresSearchDdl,\n generatePostgresVectorDdl\n} from \"./generate-postgres-ddl-logic\";\nimport { CollectionConfig, declaredDatabaseExtensions } from \"@rebasepro/types\";\nimport { loadCollectionsFromDirectory } from \"@rebasepro/server\";\nimport { out, outError } from \"../cli-output\";\n\n/** Where a project declares its resources, relative to the config directory. */\nconst RESOURCE_ENTRIES = [\"resources.ts\", \"resources.js\", \"resources/index.ts\", \"resources/index.js\"];\n\n/**\n * Evaluate the project's `resources.ts`, so `declaredDatabaseExtensions()` can\n * answer.\n *\n * This generator runs as its own tsx subprocess and loads only the collections\n * directory, so nothing else in it would ever import the resources module — and\n * a permission that reads as absent because nobody loaded it is the worst shape\n * this could take: the push succeeds, `vector.sql` quietly omits the\n * `CREATE EXTENSION`, and the failure lands one statement later looking like a\n * database problem.\n *\n * Imported directly rather than through the config index, for the reason\n * `boot/resource-loading.ts` gives: re-exporting from the index would work only\n * for projects that remember to.\n *\n * A project with no resources module is the normal case and declares nothing.\n * An import that *throws* is not swallowed — a resources module that does not\n * evaluate is a configuration error, and treating it as \"declared nothing\"\n * would turn it into a silently missing extension.\n */\nasync function loadDeclaredResources(configDir: string): Promise<void> {\n const entry = RESOURCE_ENTRIES\n .map(name => path.join(configDir, name))\n .find(candidate => fs.existsSync(candidate));\n if (!entry) return;\n await import(pathToFileURL(entry).href);\n}\n\nconst runGeneration = async (collectionsFilePath?: string, outputPath?: string) => {\n try {\n if (!collectionsFilePath) {\n throw new Error(\"No collections file path provided.\");\n }\n\n const resolvedPath = path.resolve(collectionsFilePath);\n\n // `<config>/collections` is the path this is given, so the config\n // directory is its parent — the same relationship `rebase resources`\n // and the boot loader assume.\n await loadDeclaredResources(path.dirname(resolvedPath));\n\n // Shared with the runtime and the doctor: what gets generated here must\n // be exactly what the server serves, including directory-level defaults.\n let collections: CollectionConfig[] = await loadCollectionsFromDirectory(resolvedPath);\n\n\n if (!collections || !Array.isArray(collections)) {\n collections = [];\n }\n\n // Sort collections by slug alphabetically to ensure deterministic DDL generation\n collections.sort((a, b) => a.slug.localeCompare(b.slug));\n\n // `schema.sql` is Atlas's desired state, and it carries neither the RLS\n // policies, nor the search apparatus, nor the vector columns: Atlas\n // manages none of them, and in the search and vector cases cannot. All\n // three are written beside it and applied by the CLI in their own right.\n const ddlContent = await generatePostgresDdl(collections, {\n includePolicies: false,\n includeSearch: false,\n includeVector: false\n });\n const policiesContent = generatePostgresPoliciesDdl(collections);\n const searchContent = generatePostgresSearchDdl(collections);\n const vectorContent = generatePostgresVectorDdl(collections, {\n extensions: declaredDatabaseExtensions()\n });\n\n if (outputPath) {\n const outputDir = path.dirname(outputPath);\n await fsPromises.mkdir(outputDir, { recursive: true });\n await fsPromises.writeFile(outputPath, ddlContent);\n out(`✅ PostgreSQL DDL generated successfully at ${outputPath}`);\n\n const policiesPath = path.join(outputDir, \"policies.sql\");\n await fsPromises.writeFile(policiesPath, policiesContent);\n out(`✅ PostgreSQL Policies DDL generated successfully at ${policiesPath}`);\n\n // Removed when the last `search` block goes: the CLI applies this\n // file whenever it exists, and a stale one would keep re-creating\n // functions for collections that no longer want them.\n const searchPath = path.join(outputDir, \"search.sql\");\n if (searchContent) {\n await fsPromises.writeFile(searchPath, searchContent);\n out(`✅ PostgreSQL search DDL generated successfully at ${searchPath}`);\n } else if (fs.existsSync(searchPath)) {\n await fsPromises.rm(searchPath);\n }\n\n // Removed when the last `vector` property goes, for the same reason\n // `search.sql` is: the CLI applies whichever of these files exists,\n // and a stale one would keep re-adding a column the collections no\n // longer declare — which Atlas, no longer told to exclude it, would\n // then plan a DROP for on the very next push.\n const vectorPath = path.join(outputDir, \"vector.sql\");\n if (vectorContent) {\n await fsPromises.writeFile(vectorPath, vectorContent);\n out(`✅ PostgreSQL vector DDL generated successfully at ${vectorPath}`);\n } else if (fs.existsSync(vectorPath)) {\n await fsPromises.rm(vectorPath);\n }\n } else {\n out(\"✅ PostgreSQL DDL generated successfully.\");\n out(String(ddlContent));\n out(\"\\n✅ PostgreSQL Policies DDL generated successfully.\");\n out(String(policiesContent));\n }\n\n } catch (error) {\n outError(`Error generating DDL schema: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);\n // Step 1 of `rebase db push` is this process, and its exit code is the\n // only thing the push reads. Printing and exiting 0 made a resources\n // module that does not evaluate — the single most common cause here —\n // report `✓ rebase db push completed successfully` while Atlas applied\n // whatever `drizzle/schema.sql` happened to be on disk, sometimes\n // minutes old. With no prior file the same run died inside Atlas on\n // `stat drizzle/schema.sql: no such file or directory`, which hides the\n // real cause two screens up.\n process.exitCode = 1;\n throw error;\n }\n};\n\nexport const main = async () => {\n const collectionsFilePathArg = process.argv.find(arg => arg.startsWith(\"--collections=\"));\n const collectionsFilePath = collectionsFilePathArg ? collectionsFilePathArg.split(\"=\")[1] : process.argv[2];\n\n const outputPathArg = process.argv.find(arg => arg.startsWith(\"--output=\"));\n const outputPath = outputPathArg ? outputPathArg.split(\"=\")[1] : undefined;\n\n const watch = process.argv.includes(\"--watch\");\n\n if (!collectionsFilePath) {\n out(\"Usage: ts-node generate-postgres-ddl.ts <path-to-collections-file> [--output <path-to-output-file>] [--watch]\");\n return;\n }\n\n const resolvedPath = path.resolve(process.cwd(), collectionsFilePath);\n const resolvedOutputPath = outputPath ? path.resolve(process.cwd(), outputPath) : undefined;\n\n if (watch) {\n out(`Watching for changes in ${resolvedPath}...`);\n // Imported here rather than at module scope, and this is not a style\n // choice: chokidar is needed only by `--watch`, which is a\n // schema-authoring path that never runs inside the runtime image. A\n // top-level import puts it on the boot path of the published driver\n // bundle, and the image installs a hand-listed set of runtime\n // dependencies that does not include it — so the whole driver failed to\n // load with \"Cannot find package 'chokidar'\", and every self-hosted\n // container answered 500 with a stack trace about a file watcher.\n //\n // Same reasoning the image already applies to @ariga/atlas: an\n // authoring-only dependency does not belong on a boot path.\n const { default: chokidar } = await import(\"chokidar\");\n const watcher = chokidar.watch(resolvedPath, {\n persistent: true,\n ignoreInitial: false\n });\n\n watcher.on(\"all\", (event, filePath) => {\n out(`[${event}] ${filePath}. Regenerating DDL schema...`);\n // A watch session outlives a bad edit: the cause is already on\n // stderr, and the next save is the retry. Only the one-shot run\n // below turns a failure into an exit code.\n void runGeneration(resolvedPath, resolvedOutputPath).catch(() => undefined);\n });\n } else {\n await runGeneration(resolvedPath, resolvedOutputPath);\n }\n};\n\n// This check ensures the script only runs when executed directly\nif (import.meta.url.endsWith(process.argv[1])) {\n // `runGeneration` has already printed the cause and set the exit code; the\n // handler is here so the last thing on the terminal is that message rather\n // than Node's unhandled-rejection dump of the same stack.\n main().catch(() => {\n process.exitCode = 1;\n });\n}\n","/**\n * The built entry point for `generate-postgres-ddl`.\n *\n * `generate-postgres-ddl.ts` guards its own `main()` with\n * `import.meta.url.endsWith(process.argv[1])`, because `src/index.ts` re-exports\n * the module and importing the driver must not run the generator. That guard is\n * correct when the file IS the process entry — which is how it runs from source\n * under tsx — and silently false once the module is bundled: the executed file\n * is a stub, the code lives in a shared chunk, and the chunk's `import.meta.url`\n * is not the stub's path. The generator then did nothing, exited 0, and the\n * caller reported success over an unwritten schema.\n *\n * So the built path gets an entry that says what it means.\n */\nimport { main } from \"../generate-postgres-ddl\";\n\nmain().catch(() => {\n process.exitCode = 1;\n});\n"],"mappings":"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"}