@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.
@@ -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$;