@plutocms/supabase 0.11.1 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.11.2](https://github.com/plutocms/supabase/compare/v0.11.1...v0.11.2) (2026-10-22)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **db:** grant Data API access explicitly and restrict staff reads ([#93](https://github.com/plutocms/supabase/issues/93)) ([0ed2d56](https://github.com/plutocms/supabase/commit/0ed2d56c593ed6550312692e125ba75525f5fd69))
9
+ * **typegen:** keep the PostgREST version block and match the app's lint output ([#94](https://github.com/plutocms/supabase/issues/94)) ([38ec3ea](https://github.com/plutocms/supabase/commit/38ec3ea5e49ef559cfc8142bbbfff401f934cd22))
10
+
3
11
  ## [0.11.1](https://github.com/plutocms/supabase/compare/v0.11.0...v0.11.1) (2026-10-21)
4
12
 
5
13
 
@@ -0,0 +1,48 @@
1
+ -- Data API grants
2
+ --
3
+ -- From 2026-10-30, Supabase stops granting Data API access to new tables
4
+ -- in the public schema on its own. A table that a migration creates needs
5
+ -- an explicit grant, or the API answers "permission denied" (42501).
6
+ --
7
+ -- This file grants the minimal privileges that the RLS policies of this
8
+ -- layer already allow. It changes nothing on an older database: those
9
+ -- tables already hold broader privileges, and grant is additive. Every
10
+ -- statement is safe to run twice.
11
+ --
12
+ -- Rules: anon gets select only, and only on a table with a public read
13
+ -- policy. service_role gets select, insert, update and delete. No file
14
+ -- grants truncate, references, trigger or all.
15
+
16
+ -- healthcheck: the setup check reads it without a login.
17
+ grant select on public.healthcheck to anon;
18
+ grant select, update on public.healthcheck to authenticated;
19
+
20
+ -- settings: the policies allow select, insert and update.
21
+ grant select, insert, update on public.settings to authenticated;
22
+
23
+ -- profiles: select only. A write goes through a security definer function.
24
+ grant select on public.profiles to authenticated;
25
+
26
+ -- pluto_migrations: the admin reads the migration status.
27
+ grant select on public.pluto_migrations to authenticated;
28
+
29
+ -- Role tables: today's behavior. Every write policy needs public.is_admin().
30
+ grant select, insert, update, delete on public.roles to authenticated;
31
+ grant select, insert, update, delete on public.role_capabilities to authenticated;
32
+ grant select, insert, update, delete on public.user_roles to authenticated;
33
+
34
+ -- service_role: select, insert, update and delete on every table.
35
+ grant select, insert, update, delete on public.healthcheck to service_role;
36
+ grant select, insert, update, delete on public.settings to service_role;
37
+ grant select, insert, update, delete on public.profiles to service_role;
38
+ grant select, insert, update, delete on public.pluto_migrations to service_role;
39
+ grant select, insert, update, delete on public.roles to service_role;
40
+ grant select, insert, update, delete on public.role_capabilities to service_role;
41
+ grant select, insert, update, delete on public.user_roles to service_role;
42
+
43
+ -- Functions. Postgres already gives execute to public. These grants are
44
+ -- explicit, so the access does not depend on that default.
45
+ grant execute on function public.is_admin() to authenticated, service_role;
46
+ grant execute on function public.has_capability(text) to authenticated, service_role;
47
+ grant execute on function public.my_capabilities() to authenticated, service_role;
48
+ grant execute on function public.set_my_locale(text) to authenticated;
@@ -0,0 +1,58 @@
1
+ -- Restrict staff reads
2
+ --
3
+ -- Sign-up is open. Before this file, any signed-in user could read every
4
+ -- row of settings, pluto_migrations, roles and role_capabilities, because
5
+ -- the select policies used "using (true)". This file narrows them.
6
+ --
7
+ -- settings: a user who holds at least one capability (staff) reads them.
8
+ -- The admin bar loads the site name and URL for every staff user.
9
+ -- pluto_migrations: only an admin reads it. The migrations page is
10
+ -- admin only (requireAdmin -> public.is_admin()).
11
+ -- roles, role_capabilities: only an admin reads them. Only security
12
+ -- definer functions read them for other users.
13
+ --
14
+ -- dashboard_user keeps its access where it had access before. Every
15
+ -- statement is safe to run twice.
16
+
17
+ -- settings
18
+ drop policy if exists "Enable read access for authenticated users on settings" on public.settings;
19
+ drop policy if exists "Enable read access for staff on settings" on public.settings;
20
+
21
+ create policy "Enable read access for staff on settings"
22
+ on public.settings
23
+ for select
24
+ to authenticated, dashboard_user
25
+ using (
26
+ current_user = 'dashboard_user'
27
+ or exists (select 1 from public.my_capabilities())
28
+ );
29
+
30
+ -- pluto_migrations
31
+ drop policy if exists "Enable read access for authenticated users on pluto_migrations" on public.pluto_migrations;
32
+ drop policy if exists "Enable read access for admins on pluto_migrations" on public.pluto_migrations;
33
+
34
+ create policy "Enable read access for admins on pluto_migrations"
35
+ on public.pluto_migrations
36
+ for select
37
+ to authenticated, dashboard_user
38
+ using (current_user = 'dashboard_user' or public.is_admin());
39
+
40
+ -- roles
41
+ drop policy if exists "Enable read access for authenticated users on roles" on public.roles;
42
+ drop policy if exists "Enable read access for admins on roles" on public.roles;
43
+
44
+ create policy "Enable read access for admins on roles"
45
+ on public.roles
46
+ for select
47
+ to authenticated
48
+ using (public.is_admin());
49
+
50
+ -- role_capabilities
51
+ drop policy if exists "Enable read access for authenticated users on role_capabilities" on public.role_capabilities;
52
+ drop policy if exists "Enable read access for admins on role_capabilities" on public.role_capabilities;
53
+
54
+ create policy "Enable read access for admins on role_capabilities"
55
+ on public.role_capabilities
56
+ for select
57
+ to authenticated
58
+ using (public.is_admin());
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@plutocms/supabase",
3
3
  "type": "module",
4
- "version": "0.11.1",
4
+ "version": "0.11.2",
5
5
  "trustedDependencies": [
6
6
  "@parcel/watcher",
7
7
  "@plutocms/pluto",
@@ -14,13 +14,21 @@ const execFileAsync = promisify(execFile)
14
14
  * directly via --db-url).
15
15
  */
16
16
  function resolveSupabaseBin(rootDir: string): string {
17
- const binName = process.platform === 'win32' ? 'supabase.cmd' : 'supabase'
18
- const localBin = join(rootDir, 'node_modules/.bin', binName)
19
- return existsSync(localBin) ? localBin : 'supabase'
17
+ // On Windows, npm installs `supabase.cmd` and Bun installs `supabase.exe`.
18
+ const binNames = process.platform === 'win32'
19
+ ? ['supabase.cmd', 'supabase.exe']
20
+ : ['supabase']
21
+ const localBin = binNames
22
+ .map((name) => join(rootDir, 'node_modules/.bin', name))
23
+ .find((path) => existsSync(path))
24
+ return localBin ?? 'supabase'
20
25
  }
21
26
 
22
27
  // The block, with its leading comment lines. The block has no nested braces.
23
28
  const INTERNAL_BLOCK = /(?:^[ \t]*\/\/[^\n]*\n)*^[ \t]*__InternalSupabase:\s*\{[^{}]*\}[ \t]*\n/m
29
+ // The block itself. The name alone is not enough: the output also has it in
30
+ // `Omit<Database, '__InternalSupabase'>`.
31
+ const HAS_BLOCK = /^[ \t]*__InternalSupabase:\s*\{/m
24
32
  const DATABASE_OPEN = /^export type Database = \{[ \t]*\n/m
25
33
 
26
34
  /**
@@ -36,7 +44,7 @@ export function keepInternalSupabase(
36
44
  next: string,
37
45
  existing: string | null
38
46
  ): string {
39
- if (!existing || next.includes('__InternalSupabase')) {
47
+ if (!existing || HAS_BLOCK.test(next)) {
40
48
  return next
41
49
  }
42
50
 
@@ -48,8 +56,41 @@ export function keepInternalSupabase(
48
56
  }
49
57
 
50
58
  const at = open.index + open[0].length
59
+ // The CLI writes a whitespace-only line after the opening brace. After the
60
+ // block it would stay as a blank line, so remove it.
61
+ const rest = next.slice(at).replace(/^(?:[ \t]*\n)+/, '')
51
62
 
52
- return next.slice(0, at) + block + next.slice(at)
63
+ return next.slice(0, at) + block + rest
64
+ }
65
+
66
+ /**
67
+ * Imports a package from the app's node_modules (resolved from `rootDir`).
68
+ */
69
+ async function importFromApp(rootDir: string, name: string) {
70
+ const entry = createRequire(join(rootDir, 'package.json')).resolve(name)
71
+ const mod = await import(pathToFileURL(entry).href)
72
+ return mod.default ?? mod
73
+ }
74
+
75
+ /**
76
+ * Fixes the text with the ESLint of the app. It resolves ESLint and the
77
+ * app's config from `rootDir`. It returns the text unchanged when the app
78
+ * has no ESLint, when ESLint ignores the file, or when the fix fails.
79
+ */
80
+ async function fixWithAppEslint(
81
+ rootDir: string,
82
+ filePath: string,
83
+ text: string
84
+ ): Promise<string> {
85
+ try {
86
+ const { ESLint } = await importFromApp(rootDir, 'eslint')
87
+ const eslint = new ESLint({ cwd: rootDir, fix: true })
88
+ const [result] = await eslint.lintText(text, { filePath })
89
+
90
+ return result?.output ?? text
91
+ } catch {
92
+ return text
93
+ }
53
94
  }
54
95
 
55
96
  /**
@@ -63,11 +104,7 @@ async function formatWithAppPrettier(
63
104
  text: string
64
105
  ): Promise<string> {
65
106
  try {
66
- const entry = createRequire(join(rootDir, 'package.json')).resolve(
67
- 'prettier'
68
- )
69
- const prettier = await import(pathToFileURL(entry).href)
70
- const api = prettier.default ?? prettier
107
+ const api = await importFromApp(rootDir, 'prettier')
71
108
  const options = (await api.resolveConfig(filePath)) ?? {}
72
109
 
73
110
  return await api.format(text, { ...options, filepath: filePath })
@@ -115,11 +152,14 @@ export async function regenerateSupabaseTypes(
115
152
  ? readFileSync(targetPath, 'utf-8')
116
153
  : null
117
154
 
118
- const processed = await formatWithAppPrettier(
155
+ // The same steps as a usual `lint:fix`: ESLint first, then Prettier. So
156
+ // an unchanged schema gives the same text as the committed file.
157
+ const fixed = await fixWithAppEslint(
119
158
  rootDir,
120
159
  targetPath,
121
160
  keepInternalSupabase(stdout, existing)
122
161
  )
162
+ const processed = await formatWithAppPrettier(rootDir, targetPath, fixed)
123
163
 
124
164
  if (existing === processed) {
125
165
  return