@nextblock-cms/db 0.14.6 → 0.15.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.
package/lib/media-actions.d.ts
CHANGED
|
@@ -16,6 +16,16 @@ export declare function recordMediaUpload(payload: {
|
|
|
16
16
|
r2Variants: ImageVariant[];
|
|
17
17
|
originalImageDetails: ImageVariant;
|
|
18
18
|
blurDataUrl?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Uploader for callers that have no cookie session — the MCP server, which
|
|
21
|
+
* authenticates by bearer token instead. When set, the row is written with the
|
|
22
|
+
* service-role client and attributed to this user rather than to `auth.getUser()`.
|
|
23
|
+
*
|
|
24
|
+
* This grants no new authority: the role check below still runs against the
|
|
25
|
+
* supplied id, so a non-ADMIN/WRITER actor is rejected exactly as a cookie
|
|
26
|
+
* session would be. The caller is responsible for having authenticated them.
|
|
27
|
+
*/
|
|
28
|
+
actorUserId?: string;
|
|
19
29
|
}, returnJustData?: boolean): Promise<{
|
|
20
30
|
success: true;
|
|
21
31
|
data: Media;
|
package/lib/supabase/server.d.ts
CHANGED
|
@@ -1946,6 +1946,84 @@ export declare const getServiceRoleSupabaseClient: () => import('@supabase/supab
|
|
|
1946
1946
|
};
|
|
1947
1947
|
Relationships: [];
|
|
1948
1948
|
};
|
|
1949
|
+
site_script_revisions: {
|
|
1950
|
+
Row: {
|
|
1951
|
+
actor_user_id: string | null;
|
|
1952
|
+
created_at: string;
|
|
1953
|
+
id: string;
|
|
1954
|
+
revision_type: string;
|
|
1955
|
+
script_id: string | null;
|
|
1956
|
+
script_name: string;
|
|
1957
|
+
snapshot: import('./types').Json;
|
|
1958
|
+
source: string;
|
|
1959
|
+
summary: string | null;
|
|
1960
|
+
};
|
|
1961
|
+
Insert: {
|
|
1962
|
+
actor_user_id?: string | null;
|
|
1963
|
+
created_at?: string;
|
|
1964
|
+
id?: string;
|
|
1965
|
+
revision_type: string;
|
|
1966
|
+
script_id?: string | null;
|
|
1967
|
+
script_name: string;
|
|
1968
|
+
snapshot: import('./types').Json;
|
|
1969
|
+
source?: string;
|
|
1970
|
+
summary?: string | null;
|
|
1971
|
+
};
|
|
1972
|
+
Update: {
|
|
1973
|
+
actor_user_id?: string | null;
|
|
1974
|
+
created_at?: string;
|
|
1975
|
+
id?: string;
|
|
1976
|
+
revision_type?: string;
|
|
1977
|
+
script_id?: string | null;
|
|
1978
|
+
script_name?: string;
|
|
1979
|
+
snapshot?: import('./types').Json;
|
|
1980
|
+
source?: string;
|
|
1981
|
+
summary?: string | null;
|
|
1982
|
+
};
|
|
1983
|
+
Relationships: [];
|
|
1984
|
+
};
|
|
1985
|
+
site_scripts: {
|
|
1986
|
+
Row: {
|
|
1987
|
+
code: string;
|
|
1988
|
+
created_at: string;
|
|
1989
|
+
description: string | null;
|
|
1990
|
+
id: string;
|
|
1991
|
+
is_active: boolean;
|
|
1992
|
+
load_strategy: string;
|
|
1993
|
+
name: string;
|
|
1994
|
+
placement: string;
|
|
1995
|
+
sort_order: number;
|
|
1996
|
+
src: string | null;
|
|
1997
|
+
updated_at: string;
|
|
1998
|
+
};
|
|
1999
|
+
Insert: {
|
|
2000
|
+
code?: string;
|
|
2001
|
+
created_at?: string;
|
|
2002
|
+
description?: string | null;
|
|
2003
|
+
id?: string;
|
|
2004
|
+
is_active?: boolean;
|
|
2005
|
+
load_strategy?: string;
|
|
2006
|
+
name: string;
|
|
2007
|
+
placement?: string;
|
|
2008
|
+
sort_order?: number;
|
|
2009
|
+
src?: string | null;
|
|
2010
|
+
updated_at?: string;
|
|
2011
|
+
};
|
|
2012
|
+
Update: {
|
|
2013
|
+
code?: string;
|
|
2014
|
+
created_at?: string;
|
|
2015
|
+
description?: string | null;
|
|
2016
|
+
id?: string;
|
|
2017
|
+
is_active?: boolean;
|
|
2018
|
+
load_strategy?: string;
|
|
2019
|
+
name?: string;
|
|
2020
|
+
placement?: string;
|
|
2021
|
+
sort_order?: number;
|
|
2022
|
+
src?: string | null;
|
|
2023
|
+
updated_at?: string;
|
|
2024
|
+
};
|
|
2025
|
+
Relationships: [];
|
|
2026
|
+
};
|
|
1949
2027
|
site_settings: {
|
|
1950
2028
|
Row: {
|
|
1951
2029
|
key: string;
|
package/lib/supabase/types.d.ts
CHANGED
|
@@ -2021,6 +2021,84 @@ export type Database = {
|
|
|
2021
2021
|
};
|
|
2022
2022
|
Relationships: [];
|
|
2023
2023
|
};
|
|
2024
|
+
site_script_revisions: {
|
|
2025
|
+
Row: {
|
|
2026
|
+
actor_user_id: string | null;
|
|
2027
|
+
created_at: string;
|
|
2028
|
+
id: string;
|
|
2029
|
+
revision_type: string;
|
|
2030
|
+
script_id: string | null;
|
|
2031
|
+
script_name: string;
|
|
2032
|
+
snapshot: Json;
|
|
2033
|
+
source: string;
|
|
2034
|
+
summary: string | null;
|
|
2035
|
+
};
|
|
2036
|
+
Insert: {
|
|
2037
|
+
actor_user_id?: string | null;
|
|
2038
|
+
created_at?: string;
|
|
2039
|
+
id?: string;
|
|
2040
|
+
revision_type: string;
|
|
2041
|
+
script_id?: string | null;
|
|
2042
|
+
script_name: string;
|
|
2043
|
+
snapshot: Json;
|
|
2044
|
+
source?: string;
|
|
2045
|
+
summary?: string | null;
|
|
2046
|
+
};
|
|
2047
|
+
Update: {
|
|
2048
|
+
actor_user_id?: string | null;
|
|
2049
|
+
created_at?: string;
|
|
2050
|
+
id?: string;
|
|
2051
|
+
revision_type?: string;
|
|
2052
|
+
script_id?: string | null;
|
|
2053
|
+
script_name?: string;
|
|
2054
|
+
snapshot?: Json;
|
|
2055
|
+
source?: string;
|
|
2056
|
+
summary?: string | null;
|
|
2057
|
+
};
|
|
2058
|
+
Relationships: [];
|
|
2059
|
+
};
|
|
2060
|
+
site_scripts: {
|
|
2061
|
+
Row: {
|
|
2062
|
+
code: string;
|
|
2063
|
+
created_at: string;
|
|
2064
|
+
description: string | null;
|
|
2065
|
+
id: string;
|
|
2066
|
+
is_active: boolean;
|
|
2067
|
+
load_strategy: string;
|
|
2068
|
+
name: string;
|
|
2069
|
+
placement: string;
|
|
2070
|
+
sort_order: number;
|
|
2071
|
+
src: string | null;
|
|
2072
|
+
updated_at: string;
|
|
2073
|
+
};
|
|
2074
|
+
Insert: {
|
|
2075
|
+
code?: string;
|
|
2076
|
+
created_at?: string;
|
|
2077
|
+
description?: string | null;
|
|
2078
|
+
id?: string;
|
|
2079
|
+
is_active?: boolean;
|
|
2080
|
+
load_strategy?: string;
|
|
2081
|
+
name: string;
|
|
2082
|
+
placement?: string;
|
|
2083
|
+
sort_order?: number;
|
|
2084
|
+
src?: string | null;
|
|
2085
|
+
updated_at?: string;
|
|
2086
|
+
};
|
|
2087
|
+
Update: {
|
|
2088
|
+
code?: string;
|
|
2089
|
+
created_at?: string;
|
|
2090
|
+
description?: string | null;
|
|
2091
|
+
id?: string;
|
|
2092
|
+
is_active?: boolean;
|
|
2093
|
+
load_strategy?: string;
|
|
2094
|
+
name?: string;
|
|
2095
|
+
placement?: string;
|
|
2096
|
+
sort_order?: number;
|
|
2097
|
+
src?: string | null;
|
|
2098
|
+
updated_at?: string;
|
|
2099
|
+
};
|
|
2100
|
+
Relationships: [];
|
|
2101
|
+
};
|
|
2024
2102
|
site_settings: {
|
|
2025
2103
|
Row: {
|
|
2026
2104
|
key: string;
|
package/package.json
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
-- Site scripts: admin-authored JavaScript injected into every page of the public site.
|
|
2
|
+
--
|
|
3
|
+
-- Rich-text blocks can already carry an inline <script>, but that script belongs to
|
|
4
|
+
-- one block on one page. This table is for behaviour that spans the site: chat
|
|
5
|
+
-- widgets, third-party embeds, and the scroll/animation helpers that page classes
|
|
6
|
+
-- rely on. Each row gets a name, an on/off switch, and a defined injection point.
|
|
7
|
+
--
|
|
8
|
+
-- NOT the same thing as `site_settings.privacy_settings -> custom_scripts`, which is
|
|
9
|
+
-- a single consent-gated blob for marketing tags and only fires once a visitor
|
|
10
|
+
-- accepts cookies. Rows here are functional site code and run unconditionally, so
|
|
11
|
+
-- anything requiring consent belongs in that setting instead, not this table.
|
|
12
|
+
--
|
|
13
|
+
-- Scripts are emitted with the request's CSP nonce by the root layout, so they run
|
|
14
|
+
-- under the site's existing Content-Security-Policy rather than forcing it open.
|
|
15
|
+
--
|
|
16
|
+
-- Security posture: this is arbitrary JavaScript on every page, so writes are
|
|
17
|
+
-- ADMIN-only (WRITER is deliberately excluded, unlike most content tables) and the
|
|
18
|
+
-- public may read only rows that are switched on, so a half-written draft is never
|
|
19
|
+
-- served to a visitor.
|
|
20
|
+
|
|
21
|
+
CREATE TABLE IF NOT EXISTS public.site_scripts (
|
|
22
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
23
|
+
name text NOT NULL,
|
|
24
|
+
description text,
|
|
25
|
+
-- Raw JavaScript, stored WITHOUT the surrounding <script> tag. The layout adds
|
|
26
|
+
-- the tag so the nonce and attributes are always applied by us, never by the
|
|
27
|
+
-- author. Ignored when `src` is set.
|
|
28
|
+
code text DEFAULT ''::text NOT NULL,
|
|
29
|
+
-- When set, an external script is loaded from this URL and `code` is ignored.
|
|
30
|
+
src text,
|
|
31
|
+
-- Where the tag is emitted. 'head' runs before first paint (blocking, use
|
|
32
|
+
-- sparingly); 'body_end' runs once the markup exists and is the right default
|
|
33
|
+
-- for anything that queries the DOM.
|
|
34
|
+
placement text DEFAULT 'body_end'::text NOT NULL,
|
|
35
|
+
-- Applies to external `src` scripts; inline code ignores it.
|
|
36
|
+
load_strategy text DEFAULT 'default'::text NOT NULL,
|
|
37
|
+
is_active boolean DEFAULT false NOT NULL,
|
|
38
|
+
sort_order integer DEFAULT 0 NOT NULL,
|
|
39
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
40
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
41
|
+
CONSTRAINT site_scripts_pkey PRIMARY KEY (id),
|
|
42
|
+
CONSTRAINT site_scripts_placement_check
|
|
43
|
+
CHECK ((placement = ANY (ARRAY['head'::text, 'body_start'::text, 'body_end'::text]))),
|
|
44
|
+
CONSTRAINT site_scripts_load_strategy_check
|
|
45
|
+
CHECK ((load_strategy = ANY (ARRAY['default'::text, 'defer'::text, 'async'::text]))),
|
|
46
|
+
-- An external script must be https so it cannot be downgraded in transit.
|
|
47
|
+
CONSTRAINT site_scripts_src_scheme_check
|
|
48
|
+
CHECK ((src IS NULL OR src ~ '^https://')),
|
|
49
|
+
-- A row has to actually do something: inline code or an external src.
|
|
50
|
+
CONSTRAINT site_scripts_has_payload_check
|
|
51
|
+
CHECK ((src IS NOT NULL OR length(btrim(code)) > 0))
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
COMMENT ON TABLE public.site_scripts IS
|
|
55
|
+
'Admin-authored JavaScript injected into the public site by the root layout, with the request CSP nonce applied. Only is_active rows are publicly readable; only ADMIN may write. Distinct from privacy_settings.custom_scripts, which is consent-gated marketing tags.';
|
|
56
|
+
|
|
57
|
+
CREATE INDEX IF NOT EXISTS site_scripts_active_placement_sort_idx
|
|
58
|
+
ON public.site_scripts USING btree (is_active, placement, sort_order);
|
|
59
|
+
|
|
60
|
+
DROP TRIGGER IF EXISTS set_site_scripts_updated_at ON public.site_scripts;
|
|
61
|
+
CREATE TRIGGER set_site_scripts_updated_at
|
|
62
|
+
BEFORE UPDATE ON public.site_scripts
|
|
63
|
+
FOR EACH ROW EXECUTE FUNCTION public.set_current_timestamp_updated_at();
|
|
64
|
+
|
|
65
|
+
ALTER TABLE public.site_scripts ENABLE ROW LEVEL SECURITY;
|
|
66
|
+
|
|
67
|
+
GRANT ALL ON TABLE public.site_scripts TO anon;
|
|
68
|
+
GRANT ALL ON TABLE public.site_scripts TO authenticated;
|
|
69
|
+
GRANT ALL ON TABLE public.site_scripts TO service_role;
|
|
70
|
+
|
|
71
|
+
-- Anonymous visitors need the active scripts to render the page. Inactive rows stay
|
|
72
|
+
-- private so a half-written script is never exposed before it is switched on.
|
|
73
|
+
DROP POLICY IF EXISTS "Public read active site scripts" ON public.site_scripts;
|
|
74
|
+
CREATE POLICY "Public read active site scripts" ON public.site_scripts
|
|
75
|
+
FOR SELECT TO authenticated, anon USING (is_active);
|
|
76
|
+
|
|
77
|
+
DROP POLICY IF EXISTS "Admins read all site scripts" ON public.site_scripts;
|
|
78
|
+
CREATE POLICY "Admins read all site scripts" ON public.site_scripts
|
|
79
|
+
FOR SELECT TO authenticated
|
|
80
|
+
USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));
|
|
81
|
+
|
|
82
|
+
DROP POLICY IF EXISTS "Admins insert site scripts" ON public.site_scripts;
|
|
83
|
+
CREATE POLICY "Admins insert site scripts" ON public.site_scripts
|
|
84
|
+
FOR INSERT TO authenticated
|
|
85
|
+
WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));
|
|
86
|
+
|
|
87
|
+
DROP POLICY IF EXISTS "Admins update site scripts" ON public.site_scripts;
|
|
88
|
+
CREATE POLICY "Admins update site scripts" ON public.site_scripts
|
|
89
|
+
FOR UPDATE TO authenticated
|
|
90
|
+
USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role))
|
|
91
|
+
WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));
|
|
92
|
+
|
|
93
|
+
DROP POLICY IF EXISTS "Admins delete site scripts" ON public.site_scripts;
|
|
94
|
+
CREATE POLICY "Admins delete site scripts" ON public.site_scripts
|
|
95
|
+
FOR DELETE TO authenticated
|
|
96
|
+
USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
-- Audit trail and undo for site scripts.
|
|
2
|
+
--
|
|
3
|
+
-- `site_scripts` ships arbitrary JavaScript to every visitor, which makes it the
|
|
4
|
+
-- highest-privilege write in the CMS: a bad or malicious snippet can read cookies,
|
|
5
|
+
-- watch checkout forms, or phone home. Content has Revision History for exactly this
|
|
6
|
+
-- reason; code needs it more, not less. Every create/update/delete writes one row
|
|
7
|
+
-- here, and every row is a complete, restorable snapshot — so this table is both the
|
|
8
|
+
-- log ("who shipped what, when, from where") and the undo.
|
|
9
|
+
--
|
|
10
|
+
-- APPEND-ONLY BY CONSTRUCTION. There are no UPDATE or DELETE policies, and the
|
|
11
|
+
-- trigger below rejects both even for the service role, which otherwise bypasses
|
|
12
|
+
-- RLS. An audit trail that the compromised credential can rewrite is not an audit
|
|
13
|
+
-- trail. Reverting therefore writes a NEW 'revert' row rather than removing history.
|
|
14
|
+
--
|
|
15
|
+
-- `script_id` and `actor_user_id` are deliberately PLAIN uuids with no foreign keys:
|
|
16
|
+
-- an FK with ON DELETE SET NULL would have to UPDATE this table when a script or a
|
|
17
|
+
-- profile is deleted, which the append-only trigger forbids. `script_name` is
|
|
18
|
+
-- denormalised so a deleted script is still identifiable in the log.
|
|
19
|
+
|
|
20
|
+
CREATE TABLE IF NOT EXISTS public.site_script_revisions (
|
|
21
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
22
|
+
script_id uuid,
|
|
23
|
+
script_name text NOT NULL,
|
|
24
|
+
revision_type text NOT NULL,
|
|
25
|
+
-- Null when the actor could not be resolved (e.g. a localhost dev connection).
|
|
26
|
+
actor_user_id uuid,
|
|
27
|
+
-- Which surface made the change, so an unexpected edit can be traced back to
|
|
28
|
+
-- the dashboard or to an MCP token.
|
|
29
|
+
source text DEFAULT 'cms'::text NOT NULL,
|
|
30
|
+
summary text,
|
|
31
|
+
-- Full restorable state of the script at this revision. For 'delete' it is the
|
|
32
|
+
-- state immediately BEFORE removal, so restoring it brings the script back.
|
|
33
|
+
snapshot jsonb NOT NULL,
|
|
34
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
35
|
+
CONSTRAINT site_script_revisions_pkey PRIMARY KEY (id),
|
|
36
|
+
CONSTRAINT site_script_revisions_type_check
|
|
37
|
+
CHECK ((revision_type = ANY (ARRAY['create'::text, 'update'::text, 'delete'::text, 'revert'::text]))),
|
|
38
|
+
CONSTRAINT site_script_revisions_source_check
|
|
39
|
+
CHECK ((source = ANY (ARRAY['cms'::text, 'mcp'::text]))),
|
|
40
|
+
CONSTRAINT site_script_revisions_snapshot_is_object_check
|
|
41
|
+
CHECK ((jsonb_typeof(snapshot) = 'object'))
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
COMMENT ON TABLE public.site_script_revisions IS
|
|
45
|
+
'Append-only audit log and undo history for site_scripts. Each row is a restorable snapshot. UPDATE and DELETE are blocked by trigger, including for the service role.';
|
|
46
|
+
|
|
47
|
+
CREATE INDEX IF NOT EXISTS site_script_revisions_script_created_idx
|
|
48
|
+
ON public.site_script_revisions USING btree (script_id, created_at DESC);
|
|
49
|
+
|
|
50
|
+
CREATE INDEX IF NOT EXISTS site_script_revisions_created_idx
|
|
51
|
+
ON public.site_script_revisions USING btree (created_at DESC);
|
|
52
|
+
|
|
53
|
+
-- Enforced in the database rather than the application so it holds for every
|
|
54
|
+
-- caller, including the service-role client the MCP server uses.
|
|
55
|
+
CREATE OR REPLACE FUNCTION public.prevent_site_script_revision_rewrite() RETURNS trigger
|
|
56
|
+
LANGUAGE plpgsql
|
|
57
|
+
SET search_path = ''
|
|
58
|
+
AS $$
|
|
59
|
+
BEGIN
|
|
60
|
+
RAISE EXCEPTION 'site_script_revisions is append-only; % is not permitted', TG_OP
|
|
61
|
+
USING ERRCODE = 'restrict_violation';
|
|
62
|
+
END;
|
|
63
|
+
$$;
|
|
64
|
+
|
|
65
|
+
DROP TRIGGER IF EXISTS trg_site_script_revisions_append_only ON public.site_script_revisions;
|
|
66
|
+
CREATE TRIGGER trg_site_script_revisions_append_only
|
|
67
|
+
BEFORE UPDATE OR DELETE ON public.site_script_revisions
|
|
68
|
+
FOR EACH ROW EXECUTE FUNCTION public.prevent_site_script_revision_rewrite();
|
|
69
|
+
|
|
70
|
+
ALTER TABLE public.site_script_revisions ENABLE ROW LEVEL SECURITY;
|
|
71
|
+
|
|
72
|
+
GRANT SELECT, INSERT ON TABLE public.site_script_revisions TO authenticated;
|
|
73
|
+
GRANT ALL ON TABLE public.site_script_revisions TO service_role;
|
|
74
|
+
|
|
75
|
+
-- Read is ADMIN-only: snapshots contain the full source of scripts that may not be
|
|
76
|
+
-- active yet, and the log itself reveals operational history.
|
|
77
|
+
DROP POLICY IF EXISTS "Admins read site script revisions" ON public.site_script_revisions;
|
|
78
|
+
CREATE POLICY "Admins read site script revisions" ON public.site_script_revisions
|
|
79
|
+
FOR SELECT TO authenticated
|
|
80
|
+
USING (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));
|
|
81
|
+
|
|
82
|
+
DROP POLICY IF EXISTS "Admins insert site script revisions" ON public.site_script_revisions;
|
|
83
|
+
CREATE POLICY "Admins insert site script revisions" ON public.site_script_revisions
|
|
84
|
+
FOR INSERT TO authenticated
|
|
85
|
+
WITH CHECK (((SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role));
|