@rebasepro/common 0.17.3 → 0.18.1

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 (58) hide show
  1. package/README.md +4 -0
  2. package/dist/collections/CollectionRegistry.d.ts +1 -1
  3. package/dist/collections/default-collections.d.ts +15 -84
  4. package/dist/data/buildRebaseData.d.ts +1 -1
  5. package/dist/data/filter-dialect.d.ts +11 -0
  6. package/dist/data/sort-dialect.d.ts +15 -3
  7. package/dist/index.es.js +375 -63
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/util/builders.d.ts +69 -24
  10. package/dist/util/callback-errors.d.ts +77 -0
  11. package/dist/util/callback-errors.test.d.ts +1 -0
  12. package/dist/util/index.d.ts +1 -0
  13. package/dist/util/policy/evaluatePolicy.d.ts +6 -0
  14. package/dist/util/relations.d.ts +41 -0
  15. package/dist/util/table-name.test.d.ts +1 -0
  16. package/package.json +26 -22
  17. package/src/collections/CollectionRegistry.ts +0 -485
  18. package/src/collections/default-collections.ts +0 -109
  19. package/src/collections/index.ts +0 -2
  20. package/src/data/buildRebaseData.ts +0 -816
  21. package/src/data/buildRoutedRebaseData.ts +0 -103
  22. package/src/data/filter-conditions.ts +0 -46
  23. package/src/data/filter-dialect.ts +0 -737
  24. package/src/data/paginate.ts +0 -334
  25. package/src/data/query_builder.ts +0 -176
  26. package/src/data/resolveDataSource.ts +0 -135
  27. package/src/data/sort-dialect.ts +0 -237
  28. package/src/index.ts +0 -11
  29. package/src/table-classification.ts +0 -109
  30. package/src/types/json-logic-js.d.ts +0 -8
  31. package/src/util/auth-default-policies.ts +0 -215
  32. package/src/util/builders.ts +0 -82
  33. package/src/util/callbacks.ts +0 -122
  34. package/src/util/collections.ts +0 -117
  35. package/src/util/common.ts +0 -2
  36. package/src/util/conditions.ts +0 -168
  37. package/src/util/email.ts +0 -32
  38. package/src/util/entities.ts +0 -282
  39. package/src/util/enums.ts +0 -26
  40. package/src/util/identity.ts +0 -202
  41. package/src/util/index.ts +0 -21
  42. package/src/util/internal-tables.test.ts +0 -188
  43. package/src/util/internal-tables.ts +0 -197
  44. package/src/util/junction-policies.ts +0 -355
  45. package/src/util/paths.ts +0 -27
  46. package/src/util/permissions.test.ts +0 -866
  47. package/src/util/permissions.ts +0 -206
  48. package/src/util/pg-column-to-property.ts +0 -377
  49. package/src/util/policy/evaluatePolicy.ts +0 -194
  50. package/src/util/policy/index.ts +0 -4
  51. package/src/util/policy/policyToPostgres.ts +0 -263
  52. package/src/util/policy/securityRuleToConditions.ts +0 -67
  53. package/src/util/policy/sqlToPolicy.ts +0 -422
  54. package/src/util/relations.ts +0 -236
  55. package/src/util/resolutions.ts +0 -534
  56. package/src/util/resolve-relation.ts +0 -243
  57. package/src/util/storage.ts +0 -177
  58. package/src/util/string-column-length.ts +0 -31
@@ -1,188 +0,0 @@
1
- import {
2
- REBASE_INTERNAL_TABLES,
3
- REBASE_USER_ROLE,
4
- revokeInternalTableAccess,
5
- revokeInternalTableSql
6
- } from "./internal-tables";
7
-
8
- /**
9
- * The privilege boundary around Rebase's own tables.
10
- *
11
- * This module is the only thing keeping `rebase_user` away from refresh-token
12
- * hashes, TOTP secrets and API keys, and until now nothing tested it. Its
13
- * failure mode is the quiet kind: `revokeInternalTableAccess` swallows every
14
- * error into an `onError` callback, so a revoke that fails on all twenty tables
15
- * produces a boot that looks completely normal.
16
- *
17
- * The list membership assertions matter as much as the SQL ones. A table
18
- * revoked at creation but missing from `REBASE_INTERNAL_TABLES` is stranded on
19
- * every database provisioned before that revoke shipped — the boot-time sweep
20
- * in `ensureAppRole` iterates this list and nothing else can repair an
21
- * already-granted table.
22
- */
23
- describe("internal table access", () => {
24
-
25
- // ─── The list itself ─────────────────────────────────────────────────────
26
-
27
- describe("REBASE_INTERNAL_TABLES", () => {
28
-
29
- it("covers every table a creator revokes at creation time", () => {
30
- // Kept in sync by hand with the `revokeInternalTableSql` call sites.
31
- // A creation-time revoke fires once, on the boot that first makes
32
- // the table; only the sweep can fix a database that predates it.
33
- const revokedAtCreation = [
34
- "api_keys",
35
- "branches",
36
- "channel_cursors",
37
- "channel_messages",
38
- "channel_presence",
39
- "cron_claims",
40
- "cron_logs",
41
- "entity_history",
42
- "idempotency_keys",
43
- "jobs",
44
- "rate_limit_hits"
45
- ];
46
-
47
- const missing = revokedAtCreation.filter((table) => !REBASE_INTERNAL_TABLES.includes(table));
48
- expect(missing).toEqual([]);
49
- });
50
-
51
- it("holds the auth tables whose rows no end user may ever address", () => {
52
- for (const table of [
53
- "refresh_tokens",
54
- "password_reset_tokens",
55
- "magic_link_tokens",
56
- "mfa_factors",
57
- "mfa_challenges",
58
- "recovery_codes",
59
- "user_identities"
60
- ]) {
61
- expect(REBASE_INTERNAL_TABLES).toContain(table);
62
- }
63
- });
64
-
65
- it("leaves `users` out, because revoking there breaks sign-in", () => {
66
- // The auth user table is also a collection: RLS enabled, policies
67
- // applied, and users read their own row through it.
68
- expect(REBASE_INTERNAL_TABLES).not.toContain("users");
69
- });
70
-
71
- it("has no duplicates", () => {
72
- expect(new Set(REBASE_INTERNAL_TABLES).size).toBe(REBASE_INTERNAL_TABLES.length);
73
- });
74
- });
75
-
76
- // ─── The statement ───────────────────────────────────────────────────────
77
-
78
- describe("revokeInternalTableSql", () => {
79
-
80
- it("revokes every privilege from the end-user role", () => {
81
- const sql = revokeInternalTableSql("rebase", "refresh_tokens");
82
-
83
- expect(sql).toContain(`REVOKE ALL ON "rebase"."refresh_tokens" FROM ${REBASE_USER_ROLE}`);
84
- });
85
-
86
- it("guards on the role existing, so an unprivileged install is a no-op", () => {
87
- // A bare REVOKE naming a missing role is an error, not a no-op, and
88
- // it would abort the boot it runs in.
89
- const sql = revokeInternalTableSql("rebase", "api_keys");
90
-
91
- expect(sql).toContain(`SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}'`);
92
- });
93
-
94
- it("tolerates a table that does not exist yet", () => {
95
- // `cron_logs` never appears in a project with no cron jobs.
96
- expect(revokeInternalTableSql("rebase", "cron_logs")).toContain("to_regclass");
97
- });
98
-
99
- it("skips a table with RLS enabled, because that is somebody's collection", () => {
100
- // The names in the list are unqualified and the boot sweep applies
101
- // them to every project schema, so `public.jobs` — a job board's
102
- // vacancies — was being revoked on every boot by the entry meant for
103
- // `rebase.jobs`, the background queue. RLS is what tells them apart:
104
- // collections have it, framework tables do not.
105
- const sql = revokeInternalTableSql("public", "jobs");
106
-
107
- expect(sql).toContain("relrowsecurity");
108
- expect(sql).toMatch(/NOT \(SELECT relrowsecurity FROM pg_class WHERE oid = to_regclass\('"public"\."jobs"'\)\)/);
109
- });
110
-
111
- it("is a single statement, for handles that reject multi-statement strings", () => {
112
- // The semicolons inside the block are PL/pgSQL's, not the protocol's:
113
- // what matters is that the whole string is one dollar-quoted DO,
114
- // opened once and closed at the very end.
115
- const sql = revokeInternalTableSql("rebase", "jobs");
116
-
117
- expect(sql.startsWith("DO $rebase_revoke$")).toBe(true);
118
- expect(sql.endsWith("$rebase_revoke$;")).toBe(true);
119
- expect(sql.split("$rebase_revoke$")).toHaveLength(3);
120
- });
121
-
122
- it("refuses to interpolate an unsafe identifier", () => {
123
- expect(() => revokeInternalTableSql("rebase", 'x"; DROP TABLE users; --')).toThrow(/unsafe table name/);
124
- expect(() => revokeInternalTableSql('pub"lic', "api_keys")).toThrow(/unsafe schema name/);
125
- });
126
- });
127
-
128
- // ─── The sweep ───────────────────────────────────────────────────────────
129
-
130
- describe("revokeInternalTableAccess", () => {
131
-
132
- it("issues one statement per internal table", async () => {
133
- const executed: string[] = [];
134
-
135
- await revokeInternalTableAccess(async (sql) => { executed.push(sql); }, "rebase");
136
-
137
- expect(executed).toHaveLength(REBASE_INTERNAL_TABLES.length);
138
- for (const table of REBASE_INTERNAL_TABLES) {
139
- expect(executed.some((sql) => sql.includes(`"rebase"."${table}"`))).toBe(true);
140
- }
141
- });
142
-
143
- it("reports a failure rather than swallowing it", async () => {
144
- // The whole risk: an onError that no caller wires up turns twenty
145
- // failed revokes into a silent, apparently-clean boot.
146
- const seen: string[] = [];
147
-
148
- await revokeInternalTableAccess(
149
- async (sql) => {
150
- if (sql.includes("refresh_tokens")) throw new Error("permission denied");
151
- },
152
- "rebase",
153
- { onError: (table) => seen.push(table) }
154
- );
155
-
156
- expect(seen).toEqual(["refresh_tokens"]);
157
- });
158
-
159
- it("keeps going after one table fails", async () => {
160
- const executed: string[] = [];
161
-
162
- await revokeInternalTableAccess(
163
- async (sql) => {
164
- executed.push(sql);
165
- if (sql.includes("user_identities")) throw new Error("not the owner");
166
- },
167
- "rebase",
168
- { onError: () => undefined }
169
- );
170
-
171
- // A connection that does not own one table must not strand the rest.
172
- expect(executed).toHaveLength(REBASE_INTERNAL_TABLES.length);
173
- });
174
-
175
- it("honours an explicit table list", async () => {
176
- const executed: string[] = [];
177
-
178
- await revokeInternalTableAccess(
179
- async (sql) => { executed.push(sql); },
180
- "rebase",
181
- { tables: ["api_keys"] }
182
- );
183
-
184
- expect(executed).toHaveLength(1);
185
- expect(executed[0]).toContain('"rebase"."api_keys"');
186
- });
187
- });
188
- });
@@ -1,197 +0,0 @@
1
- /**
2
- * The tables Rebase creates for its own bookkeeping, and the SQL that keeps the
3
- * end-user role away from them.
4
- *
5
- * ## Why this exists
6
- *
7
- * Authenticated requests run as {@link REBASE_USER_ROLE}, and the boot-time role
8
- * provisioning grants that role `SELECT, INSERT, UPDATE, DELETE` on every table
9
- * in the schemas a project uses — including `rebase`, because a project's own
10
- * collections are allowed to live there (the scaffold puts `users` there). It
11
- * also sets `ALTER DEFAULT PRIVILEGES`, so a table created *later* by the
12
- * migrating role inherits the same grant.
13
- *
14
- * Every framework-internal table is created later: auth's tables come up during
15
- * `initializeAuth`, `api_keys` during route mounting, `cron_logs` when the first
16
- * job registers, `idempotency_keys` on the first request that carries a key. So
17
- * they all inherited full DML for the end-user role — and none of them enables
18
- * row-level security, because none of them is a collection with
19
- * `securityRules`. Measured on a freshly provisioned database, `SET ROLE
20
- * rebase_user` could read `rebase.refresh_tokens` (session token hashes),
21
- * `rebase.mfa_factors` (`secret_encrypted`), `rebase.recovery_codes`, and
22
- * `rebase.api_keys` (including its `admin` flag), and insert into
23
- * `rebase.app_config`.
24
- *
25
- * Nothing routes a user-context query at those tables today, so this was not
26
- * reachable over the API. That is the wrong thing to depend on: the documented
27
- * model is that RLS is the authorization boundary, and these tables sat outside
28
- * it. The boundary is now a privilege boundary instead — the role simply cannot
29
- * address them.
30
- *
31
- * ## Why REVOKE rather than ENABLE ROW LEVEL SECURITY
32
- *
33
- * RLS with no policy denies every row, which is the same outcome, but it is the
34
- * *weaker* statement: it leaves the grant in place, so a later policy — or a
35
- * `FORCE` flag cleared by some future migration — reopens the table. There is no
36
- * row of `refresh_tokens` any end user should ever reach, so the honest encoding
37
- * is "this role has no privilege here at all". It also keeps the owner
38
- * connection (which auth actually runs on) completely unaffected.
39
- *
40
- * ## Keeping it true
41
- *
42
- * `packages/rls-check` scans the `rebase` schema — it used to skip it as a
43
- * "platform" schema — and its `rls-disabled` check fires on exactly the
44
- * condition this module removes: RLS off *and* a DML grant to a reachable role.
45
- * So a table added here without a revoke is caught by `pnpm rls:check`, not by
46
- * someone re-reading this file.
47
- */
48
-
49
- /**
50
- * The Postgres role authenticated requests run as.
51
- *
52
- * Defined here rather than in the Postgres driver because both the driver (which
53
- * provisions the role) and this module (which revokes on its behalf) need it,
54
- * and a second spelling of a role name is a silent no-op waiting to happen.
55
- */
56
- export const REBASE_USER_ROLE = "rebase_user";
57
-
58
- /**
59
- * Framework-internal table names, unqualified.
60
- *
61
- * Deliberately NOT including `users`: the auth user table is also a collection,
62
- * with `securityRules`, RLS enabled and policies applied. Users read their own
63
- * row through it — revoking there would break sign-in. `revokeInternalTableSql`
64
- * now skips any table with RLS enabled, so that exception is enforced rather
65
- * than merely remembered — and so is the same hazard for every other name here,
66
- * any of which a project may legitimately use for a collection of its own.
67
- *
68
- * `atlas_schema_revisions` is Atlas's migration ledger, which lands in `rebase`
69
- * because `db migrate apply` passes `--revisions-schema rebase`.
70
- *
71
- * Every entry here must also be revoked by whatever creates it, and vice versa:
72
- * the creation-time revoke fires once, on the boot that first makes the table,
73
- * so it cannot help a database provisioned before that revoke existed. This
74
- * list is what the boot-time sweep in `ensureAppRole` iterates, and the sweep
75
- * is the only thing that can repair an already-granted table. A table revoked
76
- * at creation but missing here is therefore permanently stranded on any
77
- * database that predates its revoke.
78
- *
79
- * These names are unqualified, and the boot-time sweep applies them to every
80
- * schema a project uses — so an entry here is a claim on that name in `public`
81
- * as much as in `rebase`. `jobs` is Rebase's queue at `rebase.jobs` AND a
82
- * perfectly ordinary collection name, and revoking `public.jobs` from a project
83
- * that owns it leaves every read failing 42501 with correct policies applied
84
- * and nothing in the RLS logs to explain it. The `relrowsecurity` guard in
85
- * `revokeInternalTableSql` is what makes a common noun safe here — it is not a
86
- * licence to claim more of them.
87
- */
88
- export const REBASE_INTERNAL_TABLES: readonly string[] = [
89
- // auth
90
- "user_identities",
91
- "refresh_tokens",
92
- "password_reset_tokens",
93
- "magic_link_tokens",
94
- "mfa_factors",
95
- "mfa_challenges",
96
- "recovery_codes",
97
- "app_config",
98
- "schema_meta",
99
- // platform services
100
- "api_keys",
101
- "cron_logs",
102
- "cron_claims",
103
- "jobs",
104
- "rate_limit_hits",
105
- "idempotency_keys",
106
- "entity_history",
107
- "branches",
108
- "metric_samples",
109
- // realtime channels — authorization for these lives in the channel rules the
110
- // server evaluates before it reads or writes, never in a row policy
111
- "channel_messages",
112
- "channel_cursors",
113
- "channel_presence",
114
- // migration bookkeeping
115
- "atlas_schema_revisions"
116
- ];
117
-
118
- /** Postgres identifiers this module is willing to interpolate. */
119
- const SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;
120
-
121
- /**
122
- * A single statement that takes every privilege on `schema.table` away from the
123
- * end-user role.
124
- *
125
- * Wrapped in a `DO` block guarded on `pg_roles` for two reasons, both of which
126
- * happen in practice:
127
- *
128
- * - the role does not exist when the connection is unprivileged (Rebase then
129
- * relies on native RLS rather than a role switch), and a bare `REVOKE` on a
130
- * missing role is an error, not a no-op;
131
- * - the table may not exist yet — `cron_logs` never appears in a project with
132
- * no cron jobs — and `to_regclass` returning NULL has to be tolerated too.
133
- *
134
- * The third guard is the one that decides whether the *right* table is being
135
- * revoked. The names in {@link REBASE_INTERNAL_TABLES} are unqualified, and the
136
- * boot-time sweep in `ensureAppRole` applies all of them to every schema a
137
- * project uses — including the schema its own collections live in. A project is
138
- * free to call a collection `jobs`, `branches` or `api_keys`, and when it does,
139
- * the sweep was revoking `rebase_user`'s DML on the project's table on every
140
- * single boot. That is not a subtle degradation: the collection's whole API
141
- * answers 500 `permission denied for table …` from then on, which is what
142
- * happened to a public job board whose vacancies live in `public.jobs`.
143
- *
144
- * `relrowsecurity` separates the two cleanly, and it is the same fact this
145
- * module already relies on. Framework-internal tables carry no RLS — that is the
146
- * premise stated at the top of this file, and the reason a revoke is needed at
147
- * all. Every collection table has it enabled, because that is how Rebase
148
- * enforces `securityRules`. So "RLS is off" is exactly "this is not somebody's
149
- * collection", and the guard also subsumes the hand-carved `users` exception:
150
- * the auth user table is a collection, has RLS, and would now be skipped on its
151
- * own merits rather than by being kept off a list.
152
- *
153
- * One command, so it is safe on handles that speak the extended query protocol
154
- * and reject multi-statement strings.
155
- */
156
- export function revokeInternalTableSql(schema: string, table: string): string {
157
- if (!SAFE_IDENTIFIER.test(schema)) {
158
- throw new Error(`Refusing to build SQL with an unsafe schema name: ${JSON.stringify(schema)}`);
159
- }
160
- if (!SAFE_IDENTIFIER.test(table)) {
161
- throw new Error(`Refusing to build SQL with an unsafe table name: ${JSON.stringify(table)}`);
162
- }
163
- const qualified = `"${schema}"."${table}"`;
164
- return `
165
- DO $rebase_revoke$
166
- BEGIN
167
- IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}')
168
- AND to_regclass('${qualified}') IS NOT NULL
169
- AND NOT (SELECT relrowsecurity FROM pg_class WHERE oid = to_regclass('${qualified}')) THEN
170
- EXECUTE 'REVOKE ALL ON ${qualified} FROM ${REBASE_USER_ROLE}';
171
- END IF;
172
- END
173
- $rebase_revoke$;
174
- `.trim();
175
- }
176
-
177
- /**
178
- * Revoke on every internal table in `schema`, one statement at a time.
179
- *
180
- * Best-effort per table: a connection that does not own one of them (a
181
- * pre-provisioned database, a platform-managed ledger) cannot revoke on it, and
182
- * that must not take down a boot. The caller decides how loud to be — `onError`
183
- * exists so the driver can warn without this module importing a logger.
184
- */
185
- export async function revokeInternalTableAccess(
186
- execute: (sql: string) => Promise<unknown>,
187
- schema: string,
188
- options?: { tables?: readonly string[]; onError?: (table: string, error: unknown) => void }
189
- ): Promise<void> {
190
- for (const table of options?.tables ?? REBASE_INTERNAL_TABLES) {
191
- try {
192
- await execute(revokeInternalTableSql(schema, table));
193
- } catch (error) {
194
- options?.onError?.(table, error);
195
- }
196
- }
197
- }