@orion-studios/cms 0.5.6 → 0.5.8
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/README.md +21 -4
- package/dist/analytics/react.d.ts +3 -1
- package/dist/analytics/react.js +5 -0
- package/dist/chunk-DPKXKG2S.js +8 -0
- package/dist/{chunk-AYV6KYDP.js → chunk-NSAZCP4I.js} +20 -1
- package/dist/{chunk-ULE565KD.js → chunk-Z52R6XR7.js} +227 -72
- package/dist/content/index.js +1 -1
- package/dist/forms/react.d.ts +12 -2
- package/dist/forms/react.js +6 -3
- package/dist/server/index.d.ts +93 -21
- package/dist/server/index.js +803 -143
- package/dist/studio/index.js +29 -7
- package/package.json +1 -1
- package/sql/bootstrap.sql +313 -6
- package/sql/migrations/20260829032027_cms_security_and_sync_contracts.sql +390 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
-- Purpose-bound preview grants, conflict-safe non-page sync, and target identity.
|
|
2
|
+
|
|
3
|
+
alter table cms_globals add column if not exists builder_owned boolean;
|
|
4
|
+
update cms_globals set builder_owned = true where builder_owned is null;
|
|
5
|
+
alter table cms_globals alter column builder_owned set not null;
|
|
6
|
+
alter table cms_globals alter column builder_owned set default false;
|
|
7
|
+
|
|
8
|
+
alter table cms_forms add column if not exists builder_owned boolean;
|
|
9
|
+
update cms_forms set builder_owned = true where builder_owned is null;
|
|
10
|
+
alter table cms_forms alter column builder_owned set not null;
|
|
11
|
+
alter table cms_forms alter column builder_owned set default false;
|
|
12
|
+
|
|
13
|
+
alter table cms_media add column if not exists builder_owned boolean;
|
|
14
|
+
update cms_media set builder_owned = true where builder_owned is null;
|
|
15
|
+
alter table cms_media alter column builder_owned set not null;
|
|
16
|
+
alter table cms_media alter column builder_owned set default false;
|
|
17
|
+
|
|
18
|
+
alter table cms_redirects add column if not exists builder_owned boolean;
|
|
19
|
+
update cms_redirects set builder_owned = true where builder_owned is null;
|
|
20
|
+
alter table cms_redirects alter column builder_owned set not null;
|
|
21
|
+
alter table cms_redirects alter column builder_owned set default false;
|
|
22
|
+
|
|
23
|
+
create table if not exists cms_preview_grants (
|
|
24
|
+
id uuid primary key,
|
|
25
|
+
page_id uuid not null references cms_pages (id) on delete cascade,
|
|
26
|
+
user_id uuid not null,
|
|
27
|
+
project_ref text not null check (project_ref ~ '^[a-z0-9][a-z0-9-]{0,63}$'),
|
|
28
|
+
key_id text not null check (key_id ~ '^[A-Za-z0-9._-]{1,64}$'),
|
|
29
|
+
expires_at timestamptz not null,
|
|
30
|
+
revoked_at timestamptz,
|
|
31
|
+
last_used_at timestamptz,
|
|
32
|
+
use_count integer not null default 0 check (use_count >= 0),
|
|
33
|
+
max_uses integer not null check (max_uses between 1 and 32),
|
|
34
|
+
created_at timestamptz not null default now()
|
|
35
|
+
);
|
|
36
|
+
create index if not exists cms_preview_grants_page_active_idx
|
|
37
|
+
on cms_preview_grants (page_id, expires_at)
|
|
38
|
+
where revoked_at is null;
|
|
39
|
+
create index if not exists cms_preview_grants_user_active_idx
|
|
40
|
+
on cms_preview_grants (user_id, expires_at)
|
|
41
|
+
where revoked_at is null;
|
|
42
|
+
|
|
43
|
+
create table if not exists cms_target_identity (
|
|
44
|
+
singleton boolean primary key default true check (singleton),
|
|
45
|
+
project_ref text not null check (project_ref ~ '^[a-z0-9][a-z0-9-]{0,63}$'),
|
|
46
|
+
installed_at timestamptz not null default now(),
|
|
47
|
+
updated_at timestamptz not null default now()
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
create table if not exists cms_sync_runs (
|
|
51
|
+
manifest_hash text primary key check (manifest_hash ~ '^[a-f0-9]{64}$'),
|
|
52
|
+
target_receipt_hash text not null check (target_receipt_hash ~ '^[a-f0-9]{64}$'),
|
|
53
|
+
status text not null check (status in ('active', 'complete', 'failed')),
|
|
54
|
+
result jsonb,
|
|
55
|
+
error text,
|
|
56
|
+
started_at timestamptz not null default now(),
|
|
57
|
+
updated_at timestamptz not null default now()
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
create or replace function cms_consume_preview_grant(
|
|
61
|
+
p_grant_id uuid,
|
|
62
|
+
p_page_id uuid,
|
|
63
|
+
p_user_id uuid,
|
|
64
|
+
p_project_ref text,
|
|
65
|
+
p_key_id text
|
|
66
|
+
) returns boolean
|
|
67
|
+
language plpgsql
|
|
68
|
+
security definer
|
|
69
|
+
set search_path = public, pg_catalog
|
|
70
|
+
as $$
|
|
71
|
+
declare
|
|
72
|
+
consumed_id uuid;
|
|
73
|
+
begin
|
|
74
|
+
update cms_preview_grants
|
|
75
|
+
set use_count = use_count + 1,
|
|
76
|
+
last_used_at = now()
|
|
77
|
+
where id = p_grant_id
|
|
78
|
+
and page_id = p_page_id
|
|
79
|
+
and user_id = p_user_id
|
|
80
|
+
and project_ref = p_project_ref
|
|
81
|
+
and key_id = p_key_id
|
|
82
|
+
and revoked_at is null
|
|
83
|
+
and expires_at > now()
|
|
84
|
+
and use_count < max_uses
|
|
85
|
+
returning id into consumed_id;
|
|
86
|
+
|
|
87
|
+
return consumed_id is not null;
|
|
88
|
+
end;
|
|
89
|
+
$$;
|
|
90
|
+
|
|
91
|
+
create or replace function cms_sync_global(
|
|
92
|
+
p_key text,
|
|
93
|
+
p_data jsonb
|
|
94
|
+
) returns boolean
|
|
95
|
+
language plpgsql
|
|
96
|
+
security definer
|
|
97
|
+
set search_path = public, pg_catalog
|
|
98
|
+
as $$
|
|
99
|
+
declare
|
|
100
|
+
changed_key text;
|
|
101
|
+
begin
|
|
102
|
+
insert into cms_globals (key, data, builder_owned, updated_at)
|
|
103
|
+
values (p_key, p_data, false, now())
|
|
104
|
+
on conflict (key) do update
|
|
105
|
+
set data = excluded.data,
|
|
106
|
+
updated_at = now()
|
|
107
|
+
where not cms_globals.builder_owned
|
|
108
|
+
returning key into changed_key;
|
|
109
|
+
|
|
110
|
+
return changed_key is not null;
|
|
111
|
+
end;
|
|
112
|
+
$$;
|
|
113
|
+
|
|
114
|
+
create or replace function cms_sync_form(
|
|
115
|
+
p_slug text,
|
|
116
|
+
p_title text,
|
|
117
|
+
p_config jsonb,
|
|
118
|
+
p_success_message text
|
|
119
|
+
) returns boolean
|
|
120
|
+
language plpgsql
|
|
121
|
+
security definer
|
|
122
|
+
set search_path = public, pg_catalog
|
|
123
|
+
as $$
|
|
124
|
+
declare
|
|
125
|
+
changed_slug text;
|
|
126
|
+
begin
|
|
127
|
+
insert into cms_forms (
|
|
128
|
+
slug, title, config, success_message, builder_owned, updated_at
|
|
129
|
+
) values (
|
|
130
|
+
p_slug, p_title, p_config - 'notify', p_success_message, false, now()
|
|
131
|
+
)
|
|
132
|
+
on conflict (slug) do update
|
|
133
|
+
set title = excluded.title,
|
|
134
|
+
config = excluded.config,
|
|
135
|
+
success_message = excluded.success_message,
|
|
136
|
+
updated_at = now()
|
|
137
|
+
where not cms_forms.builder_owned
|
|
138
|
+
returning slug into changed_slug;
|
|
139
|
+
|
|
140
|
+
return changed_slug is not null;
|
|
141
|
+
end;
|
|
142
|
+
$$;
|
|
143
|
+
|
|
144
|
+
create or replace function cms_sync_media(
|
|
145
|
+
p_storage_path text,
|
|
146
|
+
p_filename text,
|
|
147
|
+
p_alt text,
|
|
148
|
+
p_caption text,
|
|
149
|
+
p_mime_type text,
|
|
150
|
+
p_width integer,
|
|
151
|
+
p_height integer,
|
|
152
|
+
p_filesize bigint
|
|
153
|
+
) returns boolean
|
|
154
|
+
language plpgsql
|
|
155
|
+
security definer
|
|
156
|
+
set search_path = public, pg_catalog
|
|
157
|
+
as $$
|
|
158
|
+
declare
|
|
159
|
+
changed_path text;
|
|
160
|
+
begin
|
|
161
|
+
insert into cms_media (
|
|
162
|
+
storage_path, filename, alt, caption, mime_type, width, height, filesize, builder_owned, updated_at
|
|
163
|
+
) values (
|
|
164
|
+
p_storage_path, p_filename, p_alt, p_caption, p_mime_type,
|
|
165
|
+
p_width, p_height, p_filesize, false, now()
|
|
166
|
+
)
|
|
167
|
+
on conflict (storage_path) do update
|
|
168
|
+
set filename = excluded.filename,
|
|
169
|
+
alt = excluded.alt,
|
|
170
|
+
caption = excluded.caption,
|
|
171
|
+
mime_type = excluded.mime_type,
|
|
172
|
+
width = excluded.width,
|
|
173
|
+
height = excluded.height,
|
|
174
|
+
filesize = excluded.filesize,
|
|
175
|
+
updated_at = now()
|
|
176
|
+
where not cms_media.builder_owned
|
|
177
|
+
returning storage_path into changed_path;
|
|
178
|
+
return changed_path is not null;
|
|
179
|
+
end;
|
|
180
|
+
$$;
|
|
181
|
+
|
|
182
|
+
create or replace function cms_sync_redirect(
|
|
183
|
+
p_from_path text,
|
|
184
|
+
p_to_path text,
|
|
185
|
+
p_permanent boolean
|
|
186
|
+
) returns boolean
|
|
187
|
+
language plpgsql
|
|
188
|
+
security definer
|
|
189
|
+
set search_path = public, pg_catalog
|
|
190
|
+
as $$
|
|
191
|
+
declare
|
|
192
|
+
changed_path text;
|
|
193
|
+
begin
|
|
194
|
+
insert into cms_redirects (from_path, to_path, permanent, builder_owned)
|
|
195
|
+
values (p_from_path, p_to_path, p_permanent, false)
|
|
196
|
+
on conflict (from_path) do update
|
|
197
|
+
set to_path = excluded.to_path,
|
|
198
|
+
permanent = excluded.permanent
|
|
199
|
+
where not cms_redirects.builder_owned
|
|
200
|
+
returning from_path into changed_path;
|
|
201
|
+
return changed_path is not null;
|
|
202
|
+
end;
|
|
203
|
+
$$;
|
|
204
|
+
|
|
205
|
+
create or replace function cms_begin_sync(
|
|
206
|
+
p_manifest_hash text,
|
|
207
|
+
p_target_receipt_hash text
|
|
208
|
+
) returns jsonb
|
|
209
|
+
language plpgsql
|
|
210
|
+
security definer
|
|
211
|
+
set search_path = public, pg_catalog
|
|
212
|
+
as $$
|
|
213
|
+
declare
|
|
214
|
+
active_run cms_sync_runs;
|
|
215
|
+
requested_run cms_sync_runs;
|
|
216
|
+
begin
|
|
217
|
+
if p_manifest_hash !~ '^[a-f0-9]{64}$' or p_target_receipt_hash !~ '^[a-f0-9]{64}$' then
|
|
218
|
+
raise exception 'Invalid sync receipt.';
|
|
219
|
+
end if;
|
|
220
|
+
|
|
221
|
+
-- Serialize lease decisions across every manifest. Row locks cannot protect
|
|
222
|
+
-- the empty set when two new manifests start at the same time.
|
|
223
|
+
perform pg_advisory_xact_lock(hashtextextended('orion_cms_content_sync', 0));
|
|
224
|
+
|
|
225
|
+
select * into requested_run from cms_sync_runs where manifest_hash = p_manifest_hash for update;
|
|
226
|
+
if requested_run.status = 'complete' then
|
|
227
|
+
return jsonb_build_object('status', 'complete', 'result', requested_run.result);
|
|
228
|
+
end if;
|
|
229
|
+
if requested_run.status = 'active' and requested_run.updated_at > now() - interval '15 minutes' then
|
|
230
|
+
return jsonb_build_object('status', 'busy');
|
|
231
|
+
end if;
|
|
232
|
+
|
|
233
|
+
select * into active_run
|
|
234
|
+
from cms_sync_runs
|
|
235
|
+
where status = 'active' and manifest_hash <> p_manifest_hash
|
|
236
|
+
order by updated_at desc
|
|
237
|
+
limit 1
|
|
238
|
+
for update;
|
|
239
|
+
if active_run.manifest_hash is not null and active_run.updated_at > now() - interval '15 minutes' then
|
|
240
|
+
return jsonb_build_object('status', 'busy');
|
|
241
|
+
end if;
|
|
242
|
+
if active_run.manifest_hash is not null then
|
|
243
|
+
update cms_sync_runs
|
|
244
|
+
set status = 'failed', error = 'Sync lease expired.', updated_at = now()
|
|
245
|
+
where manifest_hash = active_run.manifest_hash;
|
|
246
|
+
end if;
|
|
247
|
+
|
|
248
|
+
insert into cms_sync_runs (manifest_hash, target_receipt_hash, status)
|
|
249
|
+
values (p_manifest_hash, p_target_receipt_hash, 'active')
|
|
250
|
+
on conflict (manifest_hash) do update
|
|
251
|
+
set target_receipt_hash = excluded.target_receipt_hash,
|
|
252
|
+
status = 'active',
|
|
253
|
+
result = null,
|
|
254
|
+
error = null,
|
|
255
|
+
updated_at = now();
|
|
256
|
+
return jsonb_build_object('status', case when requested_run.manifest_hash is null then 'started' else 'resume' end);
|
|
257
|
+
end;
|
|
258
|
+
$$;
|
|
259
|
+
|
|
260
|
+
create or replace function cms_finish_sync(
|
|
261
|
+
p_manifest_hash text,
|
|
262
|
+
p_status text,
|
|
263
|
+
p_result jsonb default null,
|
|
264
|
+
p_error text default null
|
|
265
|
+
) returns boolean
|
|
266
|
+
language plpgsql
|
|
267
|
+
security definer
|
|
268
|
+
set search_path = public, pg_catalog
|
|
269
|
+
as $$
|
|
270
|
+
begin
|
|
271
|
+
if p_status not in ('complete', 'failed') then
|
|
272
|
+
raise exception 'Invalid sync completion status.';
|
|
273
|
+
end if;
|
|
274
|
+
update cms_sync_runs
|
|
275
|
+
set status = p_status,
|
|
276
|
+
result = case when p_status = 'complete' then p_result else null end,
|
|
277
|
+
error = case when p_status = 'failed' then left(coalesce(p_error, 'Sync failed.'), 500) else null end,
|
|
278
|
+
updated_at = now()
|
|
279
|
+
where manifest_hash = p_manifest_hash and status = 'active';
|
|
280
|
+
return found;
|
|
281
|
+
end;
|
|
282
|
+
$$;
|
|
283
|
+
|
|
284
|
+
create or replace function cms_update_global(
|
|
285
|
+
p_key text,
|
|
286
|
+
p_data jsonb,
|
|
287
|
+
p_actor uuid default null
|
|
288
|
+
) returns cms_globals
|
|
289
|
+
language plpgsql
|
|
290
|
+
security definer
|
|
291
|
+
set search_path = public, pg_catalog
|
|
292
|
+
as $$
|
|
293
|
+
declare
|
|
294
|
+
result cms_globals;
|
|
295
|
+
begin
|
|
296
|
+
insert into cms_globals (key, data, builder_owned, updated_at)
|
|
297
|
+
values (p_key, p_data, true, now())
|
|
298
|
+
on conflict (key) do update
|
|
299
|
+
set data = excluded.data,
|
|
300
|
+
builder_owned = true,
|
|
301
|
+
updated_at = now()
|
|
302
|
+
returning * into result;
|
|
303
|
+
|
|
304
|
+
insert into cms_global_versions (key, data, created_by)
|
|
305
|
+
values (p_key, p_data, p_actor);
|
|
306
|
+
|
|
307
|
+
return result;
|
|
308
|
+
end;
|
|
309
|
+
$$;
|
|
310
|
+
|
|
311
|
+
alter table cms_preview_grants enable row level security;
|
|
312
|
+
alter table cms_target_identity enable row level security;
|
|
313
|
+
alter table cms_sync_runs enable row level security;
|
|
314
|
+
|
|
315
|
+
alter default privileges revoke execute on functions from public;
|
|
316
|
+
|
|
317
|
+
do $cms_safe_function_paths$
|
|
318
|
+
declare
|
|
319
|
+
fn regprocedure;
|
|
320
|
+
begin
|
|
321
|
+
for fn in
|
|
322
|
+
select p.oid::regprocedure
|
|
323
|
+
from pg_proc p
|
|
324
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
325
|
+
where n.nspname = 'public'
|
|
326
|
+
and p.proname like 'cms_%'
|
|
327
|
+
and p.prosecdef
|
|
328
|
+
loop
|
|
329
|
+
execute format('alter function %s set search_path to public, pg_catalog', fn);
|
|
330
|
+
end loop;
|
|
331
|
+
end
|
|
332
|
+
$cms_safe_function_paths$;
|
|
333
|
+
|
|
334
|
+
revoke all on cms_preview_grants from public;
|
|
335
|
+
revoke all on cms_target_identity from public;
|
|
336
|
+
revoke all on cms_sync_runs from public;
|
|
337
|
+
revoke execute on function cms_consume_preview_grant(uuid, uuid, uuid, text, text) from public;
|
|
338
|
+
revoke execute on function cms_sync_global(text, jsonb) from public;
|
|
339
|
+
revoke execute on function cms_sync_form(text, text, jsonb, text) from public;
|
|
340
|
+
revoke execute on function cms_sync_media(text, text, text, text, text, integer, integer, bigint) from public;
|
|
341
|
+
revoke execute on function cms_sync_redirect(text, text, boolean) from public;
|
|
342
|
+
revoke execute on function cms_begin_sync(text, text) from public;
|
|
343
|
+
revoke execute on function cms_finish_sync(text, text, jsonb, text) from public;
|
|
344
|
+
|
|
345
|
+
do $cms_security_contracts$
|
|
346
|
+
begin
|
|
347
|
+
if exists (select 1 from pg_roles where rolname = 'anon') then
|
|
348
|
+
execute 'alter default privileges in schema public revoke all on tables from anon';
|
|
349
|
+
execute 'alter default privileges in schema public revoke all on sequences from anon';
|
|
350
|
+
execute 'alter default privileges in schema public revoke execute on functions from anon';
|
|
351
|
+
revoke all on cms_preview_grants from anon;
|
|
352
|
+
revoke all on cms_target_identity from anon;
|
|
353
|
+
revoke all on cms_sync_runs from anon;
|
|
354
|
+
revoke execute on function cms_consume_preview_grant(uuid, uuid, uuid, text, text) from anon;
|
|
355
|
+
revoke execute on function cms_sync_global(text, jsonb) from anon;
|
|
356
|
+
revoke execute on function cms_sync_form(text, text, jsonb, text) from anon;
|
|
357
|
+
revoke execute on function cms_sync_media(text, text, text, text, text, integer, integer, bigint) from anon;
|
|
358
|
+
revoke execute on function cms_sync_redirect(text, text, boolean) from anon;
|
|
359
|
+
revoke execute on function cms_begin_sync(text, text) from anon;
|
|
360
|
+
revoke execute on function cms_finish_sync(text, text, jsonb, text) from anon;
|
|
361
|
+
end if;
|
|
362
|
+
if exists (select 1 from pg_roles where rolname = 'authenticated') then
|
|
363
|
+
execute 'alter default privileges in schema public revoke all on tables from authenticated';
|
|
364
|
+
execute 'alter default privileges in schema public revoke all on sequences from authenticated';
|
|
365
|
+
execute 'alter default privileges in schema public revoke execute on functions from authenticated';
|
|
366
|
+
revoke all on cms_preview_grants from authenticated;
|
|
367
|
+
revoke all on cms_target_identity from authenticated;
|
|
368
|
+
revoke all on cms_sync_runs from authenticated;
|
|
369
|
+
revoke execute on function cms_consume_preview_grant(uuid, uuid, uuid, text, text) from authenticated;
|
|
370
|
+
revoke execute on function cms_sync_global(text, jsonb) from authenticated;
|
|
371
|
+
revoke execute on function cms_sync_form(text, text, jsonb, text) from authenticated;
|
|
372
|
+
revoke execute on function cms_sync_media(text, text, text, text, text, integer, integer, bigint) from authenticated;
|
|
373
|
+
revoke execute on function cms_sync_redirect(text, text, boolean) from authenticated;
|
|
374
|
+
revoke execute on function cms_begin_sync(text, text) from authenticated;
|
|
375
|
+
revoke execute on function cms_finish_sync(text, text, jsonb, text) from authenticated;
|
|
376
|
+
end if;
|
|
377
|
+
if exists (select 1 from pg_roles where rolname = 'service_role') then
|
|
378
|
+
grant select, insert, update, delete on cms_preview_grants to service_role;
|
|
379
|
+
grant select on cms_target_identity to service_role;
|
|
380
|
+
grant select, insert, update on cms_sync_runs to service_role;
|
|
381
|
+
grant execute on function cms_consume_preview_grant(uuid, uuid, uuid, text, text) to service_role;
|
|
382
|
+
grant execute on function cms_sync_global(text, jsonb) to service_role;
|
|
383
|
+
grant execute on function cms_sync_form(text, text, jsonb, text) to service_role;
|
|
384
|
+
grant execute on function cms_sync_media(text, text, text, text, text, integer, integer, bigint) to service_role;
|
|
385
|
+
grant execute on function cms_sync_redirect(text, text, boolean) to service_role;
|
|
386
|
+
grant execute on function cms_begin_sync(text, text) to service_role;
|
|
387
|
+
grant execute on function cms_finish_sync(text, text, jsonb, text) to service_role;
|
|
388
|
+
end if;
|
|
389
|
+
end
|
|
390
|
+
$cms_security_contracts$;
|