@plutocms/supabase 0.11.0 → 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,26 @@
|
|
|
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
|
+
|
|
11
|
+
## [0.11.1](https://github.com/plutocms/supabase/compare/v0.11.0...v0.11.1) (2026-10-21)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* **deps:** use @plutocms/pluto 0.17 ([#91](https://github.com/plutocms/supabase/issues/91)) ([7deeac5](https://github.com/plutocms/supabase/commit/7deeac5379ddaac0317fa457897190c0850e8ffc))
|
|
17
|
+
* **typegen:** keep the PostgREST version block and the app's format ([#90](https://github.com/plutocms/supabase/issues/90)) ([2518c33](https://github.com/plutocms/supabase/commit/2518c33f69fe188e69d93695819d7d3ca19932d7))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Performance Improvements
|
|
21
|
+
|
|
22
|
+
* **admin:** load the admin components on demand and start the setup check early ([#89](https://github.com/plutocms/supabase/issues/89)) ([f0d36bc](https://github.com/plutocms/supabase/commit/f0d36bc7d46f911522990653cd93ebb523c5a774))
|
|
23
|
+
|
|
3
24
|
## [0.11.0](https://github.com/plutocms/supabase/compare/v0.10.0...v0.11.0) (2026-10-20)
|
|
4
25
|
|
|
5
26
|
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { defineAsyncComponent } from 'vue'
|
|
2
|
+
|
|
3
|
+
// The three components load on demand. They stay out of the entry chunk. Core
|
|
4
|
+
// shows a skeleton while each chunk loads.
|
|
5
|
+
const AuthSignIn = defineAsyncComponent(
|
|
6
|
+
() => import('../components/auth/AuthSignIn.vue')
|
|
7
|
+
)
|
|
8
|
+
const MigrationsBanner = defineAsyncComponent(
|
|
9
|
+
() => import('../components/migrations/MigrationsBanner.vue')
|
|
10
|
+
)
|
|
11
|
+
const SettingsGeneralPanel = defineAsyncComponent(
|
|
12
|
+
() => import('../components/settings/SettingsGeneralPanel.vue')
|
|
13
|
+
)
|
|
4
14
|
|
|
5
15
|
export default defineNuxtPlugin(() => {
|
|
6
16
|
// Read at setup, while the Nuxt context exists. The driver below calls it
|
|
@@ -35,11 +45,7 @@ export default defineNuxtPlugin(() => {
|
|
|
35
45
|
// again each time.
|
|
36
46
|
const setupDone = useState<boolean>('supabase-setup-done', () => false)
|
|
37
47
|
|
|
38
|
-
async function
|
|
39
|
-
if (setupDone.value) {
|
|
40
|
-
return 'done'
|
|
41
|
-
}
|
|
42
|
-
|
|
48
|
+
async function fetchSetupState(): Promise<SetupState> {
|
|
43
49
|
let state: SetupState = 'unknown'
|
|
44
50
|
try {
|
|
45
51
|
state = setupStateFromResponse(
|
|
@@ -56,6 +62,27 @@ export default defineNuxtPlugin(() => {
|
|
|
56
62
|
return state
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
// On a cold load of an admin path, the request starts here, at plugin setup.
|
|
66
|
+
// The guard then awaits a request that is already in flight. The guard takes
|
|
67
|
+
// this promise once, so a later navigation reads the state again.
|
|
68
|
+
let earlyState: Promise<SetupState> | null =
|
|
69
|
+
import.meta.client &&
|
|
70
|
+
!setupDone.value &&
|
|
71
|
+
/^\/admin(?:\/|$)/.test(window.location.pathname)
|
|
72
|
+
? fetchSetupState()
|
|
73
|
+
: null
|
|
74
|
+
|
|
75
|
+
async function readSetupState(): Promise<SetupState> {
|
|
76
|
+
if (setupDone.value) {
|
|
77
|
+
return 'done'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const early = earlyState
|
|
81
|
+
earlyState = null
|
|
82
|
+
|
|
83
|
+
return early ?? fetchSetupState()
|
|
84
|
+
}
|
|
85
|
+
|
|
59
86
|
if (import.meta.client) {
|
|
60
87
|
watch(
|
|
61
88
|
[() => Boolean(session.value), hasSettingsModified],
|
|
@@ -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.
|
|
4
|
+
"version": "0.11.2",
|
|
5
5
|
"trustedDependencies": [
|
|
6
6
|
"@parcel/watcher",
|
|
7
7
|
"@plutocms/pluto",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@nuxt/eslint": "^1.17.0",
|
|
40
40
|
"@nuxt/ui": "^4.12.0",
|
|
41
41
|
"@nuxtjs/supabase": "^2.1.0",
|
|
42
|
-
"@plutocms/pluto": "^0.
|
|
42
|
+
"@plutocms/pluto": "^0.17.0",
|
|
43
43
|
"@plutocms/utils": "^0.2.1",
|
|
44
44
|
"postgres": "^3.4.9",
|
|
45
45
|
"supabase": "^2.121.0",
|
package/server/utils/typegen.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process'
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
3
4
|
import { join } from 'node:path'
|
|
5
|
+
import { pathToFileURL } from 'node:url'
|
|
4
6
|
import { promisify } from 'node:util'
|
|
5
7
|
|
|
6
8
|
const execFileAsync = promisify(execFile)
|
|
@@ -12,9 +14,103 @@ const execFileAsync = promisify(execFile)
|
|
|
12
14
|
* directly via --db-url).
|
|
13
15
|
*/
|
|
14
16
|
function resolveSupabaseBin(rootDir: string): string {
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
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'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The block, with its leading comment lines. The block has no nested braces.
|
|
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
|
|
32
|
+
const DATABASE_OPEN = /^export type Database = \{[ \t]*\n/m
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Keeps the `__InternalSupabase` block of the existing types file.
|
|
36
|
+
*
|
|
37
|
+
* The `--db-url` mode of the Supabase CLI does not write this block. It
|
|
38
|
+
* holds the PostgREST version that supabase-js reads for its typings. When
|
|
39
|
+
* the new output has no block and the existing file has one, this function
|
|
40
|
+
* copies the existing block into the new output. In every other case it
|
|
41
|
+
* returns the new output as it is.
|
|
42
|
+
*/
|
|
43
|
+
export function keepInternalSupabase(
|
|
44
|
+
next: string,
|
|
45
|
+
existing: string | null
|
|
46
|
+
): string {
|
|
47
|
+
if (!existing || HAS_BLOCK.test(next)) {
|
|
48
|
+
return next
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const block = existing.match(INTERNAL_BLOCK)?.[0]
|
|
52
|
+
const open = next.match(DATABASE_OPEN)
|
|
53
|
+
|
|
54
|
+
if (!block || !open || open.index === undefined) {
|
|
55
|
+
return next
|
|
56
|
+
}
|
|
57
|
+
|
|
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)+/, '')
|
|
62
|
+
|
|
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
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Formats the text with the Prettier of the app. It resolves Prettier from
|
|
98
|
+
* `rootDir`. It returns the text unchanged when the app has no Prettier or
|
|
99
|
+
* when the format fails.
|
|
100
|
+
*/
|
|
101
|
+
async function formatWithAppPrettier(
|
|
102
|
+
rootDir: string,
|
|
103
|
+
filePath: string,
|
|
104
|
+
text: string
|
|
105
|
+
): Promise<string> {
|
|
106
|
+
try {
|
|
107
|
+
const api = await importFromApp(rootDir, 'prettier')
|
|
108
|
+
const options = (await api.resolveConfig(filePath)) ?? {}
|
|
109
|
+
|
|
110
|
+
return await api.format(text, { ...options, filepath: filePath })
|
|
111
|
+
} catch {
|
|
112
|
+
return text
|
|
113
|
+
}
|
|
18
114
|
}
|
|
19
115
|
|
|
20
116
|
/**
|
|
@@ -56,11 +152,20 @@ export async function regenerateSupabaseTypes(
|
|
|
56
152
|
? readFileSync(targetPath, 'utf-8')
|
|
57
153
|
: null
|
|
58
154
|
|
|
59
|
-
|
|
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(
|
|
158
|
+
rootDir,
|
|
159
|
+
targetPath,
|
|
160
|
+
keepInternalSupabase(stdout, existing)
|
|
161
|
+
)
|
|
162
|
+
const processed = await formatWithAppPrettier(rootDir, targetPath, fixed)
|
|
163
|
+
|
|
164
|
+
if (existing === processed) {
|
|
60
165
|
return
|
|
61
166
|
}
|
|
62
167
|
|
|
63
|
-
writeFileSync(targetPath,
|
|
168
|
+
writeFileSync(targetPath, processed, 'utf-8')
|
|
64
169
|
console.warn(`[typegen] Regenerated Supabase types at ${targetPath}`)
|
|
65
170
|
} catch (error) {
|
|
66
171
|
console.warn('[typegen] Failed to regenerate Supabase types:', error)
|