@nextblock-cms/db 0.22.1 → 0.22.3

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.
Files changed (43) hide show
  1. package/README.md +47 -47
  2. package/lib/media-actions.cjs.js +1 -1
  3. package/lib/media-actions.d.ts +13 -35
  4. package/lib/media-actions.es.js +25 -49
  5. package/lib/media-actions.test.d.ts +1 -0
  6. package/lib/media-record.cjs.js +1 -0
  7. package/lib/media-record.d.ts +46 -0
  8. package/lib/media-record.es.js +47 -0
  9. package/lib/package-validation.cjs.js +1 -1
  10. package/lib/package-validation.es.js +1 -1
  11. package/lib/secret-crypto.cjs.js +1 -1
  12. package/lib/secret-crypto.es.js +1 -0
  13. package/lib/supabase/server.cjs.js +1 -1
  14. package/lib/supabase/server.es.js +1 -0
  15. package/package.json +1 -1
  16. package/server.cjs.js +1 -1
  17. package/server.d.ts +1 -0
  18. package/server.es.js +6 -5
  19. package/supabase/migrations/02000_catchup_gen1.sql +5880 -5880
  20. package/supabase/migrations/02001_baseline_schema.sql +2734 -2734
  21. package/supabase/migrations/02002_baseline_constraints_and_indexes.sql +1262 -1262
  22. package/supabase/migrations/02003_baseline_security_and_grants.sql +1577 -1577
  23. package/supabase/migrations/02004_baseline_seed.sql +565 -565
  24. package/supabase/migrations/02005_home_live_demo_promo_cadence_15min.sql +53 -53
  25. package/supabase/migrations/02006_home_marketing_contrast.sql +56 -56
  26. package/supabase/migrations/02007_home_pillar_caption_contrast.sql +44 -44
  27. package/supabase/migrations/02008_home_fr_parity.sql +89 -89
  28. package/supabase/migrations/02009_fr_cortex_posts.sql +67 -67
  29. package/supabase/migrations/02010_shop_hero_redesign.sql +72 -72
  30. package/supabase/migrations/02011_drop_backdrop_blur_on_pages.sql +26 -26
  31. package/supabase/migrations/02012_drop_variant_backdrop_blur.sql +17 -17
  32. package/supabase/migrations/02013_advisor_rls_and_trigger_grants.sql +102 -102
  33. package/supabase/migrations/02014_package_activations_staff_read.sql +18 -18
  34. package/supabase/migrations/02018_install_paths_and_seed_copy_accuracy.sql +1820 -1820
  35. package/supabase/migrations/02019_open_handed_licensing_and_mcp_clients.sql +448 -448
  36. package/supabase/migrations/02020_commerce_product_accuracy.sql +93 -93
  37. package/supabase/migrations/02021_profiles_staff_read_and_role_guard.sql +362 -362
  38. package/supabase/migrations/02022_security_hardening_writer_boundary_and_aal.sql +504 -504
  39. package/supabase/migrations/02024_advisor_trigger_grants_and_pg_net_schema.sql +113 -0
  40. package/supabase/migrations/02025_api_role_grant_hardening.sql +66 -0
  41. package/supabase/migrations/02026_authenticated_write_grant_hardening.sql +91 -0
  42. package/supabase/migrations/02027_api_role_select_grant_hardening.sql +150 -0
  43. package/supabase/migrations/02028_writer_settings_allowlist_email_factor_aal_and_media_keys.sql +284 -0
@@ -0,0 +1,284 @@
1
+ -- 02028: close the database holes the 2026-10-21 audit confirmed on a local replay (each one
2
+ -- reproduced over PostgREST: SET LOCAL ROLE authenticated + request.jwt.claims, rolled back).
3
+ -- Append-only, forward-only, idempotent: every policy is dropped by name and recreated, every
4
+ -- function is CREATE OR REPLACE, every grant is safe to repeat. 02022/02023 are the model.
5
+ --
6
+ -- 1. site_settings: the WRITER write branch becomes an allow-list (HIGH).
7
+ -- 02022/02023 let ADMIN *or WRITER* write every key outside a 13-key deny-list. A WRITER
8
+ -- could therefore PATCH `email_public` to point SMTP at their own server, and
9
+ -- lib/config/email-settings.ts combines that host with the decrypted ADMIN-only
10
+ -- `email_secret` credentials: the SMTP password and every outgoing mail (ADMIN email-2FA
11
+ -- codes included) went to the attacker. The same branch let a WRITER turn the captcha off
12
+ -- (`bot_protection_public`), rewrite payment and invoice settings, redirect the store,
13
+ -- form and moderation notification addresses, change `migration_baseline_generation`
14
+ -- (which 02000 trusts) and the Cortex agent settings. An unfiltered PATCH updated 13 rows.
15
+ -- Writes now fall into three tiers, and a key in none of the first two is in the third:
16
+ -- * WRITER tier (ADMIN or WRITER, first factor): exactly the keys staff code writes
17
+ -- through the cookie client — site identity (site_title, site_description,
18
+ -- site_keywords, site_social_image, active_logo_id: app/cms/settings/logos/actions.ts),
19
+ -- the footer (footer_copyright, footer_show_attribution: settings/copyright/actions.ts)
20
+ -- and onboarding_state (lib/onboarding/actions.ts, the dashboard's dismiss button).
21
+ -- * ADMIN tier (ADMIN, first factor): the two keys an ADMIN still writes through the
22
+ -- cookie client, enabled_payment_providers and ecommerce_inventory_settings
23
+ -- (libs/ecommerce payments, shipping, taxes). Moving those writes to the service role
24
+ -- lets them drop into the tier below (docs/04 lists it as a follow-up).
25
+ -- * everything else: ADMIN with a satisfied second factor (public.is_admin_mfa()). That
26
+ -- is the old deny-list plus email_public, bot_protection_public, payment_public,
27
+ -- invoice_settings, store_contact, forms_contact, interactions_notification_emails,
28
+ -- migration_baseline_generation, the cortex_ai_* settings and any key added later.
29
+ -- The app writes all of them with the service role after requireCmsAdmin() /
30
+ -- requireEcomAdmin(), Cortex too (its context is the service client); the three that
31
+ -- still used the cookie client move in the same change (logos/actions.ts
32
+ -- saveInvoiceSettings, cms/inquiries saveStoreContactEmail, the interactions
33
+ -- notification-address actions). The last two were already broken for an aal1 admin:
34
+ -- an upsert's ON CONFLICT path checks the read policy, and both keys sit above the
35
+ -- staff read tier. Reads are untouched (02021/02023 tiers).
36
+ --
37
+ -- 2. The email second factor counts in RLS (HIGH).
38
+ -- private.meets_aal_requirement() passed whenever the caller had no verified row in
39
+ -- auth.mfa_factors. The app's email factor never creates one (only TOTP does), so for an
40
+ -- email-2FA ADMIN every is_admin_mfa() policy was a plain role check: a stolen password and
41
+ -- an aal1 JWT reached mcp_access_tokens (a persistent MCP bearer), package_activations,
42
+ -- the secret tier of site_settings and other accounts' profiles. The no-factor branch now
43
+ -- also requires that the caller has no enrolled factor in public.user_security_settings
44
+ -- (`mfa_enabled AND mfa_type IS NOT NULL`, written only by the service role since 02026).
45
+ -- An email factor never yields an aal2 Supabase session, so for those admins every
46
+ -- is_admin_mfa() row is now reachable only through the service role, after the app's own
47
+ -- check (lib/auth/cms-actor.ts getCmsActor() refuses a session whose factor is pending).
48
+ -- A TOTP admin keeps aal2, and an admin who enrolled nothing is never locked out.
49
+ --
50
+ -- 3. ADMIN branches that skipped the second factor (MEDIUM).
51
+ -- site_scripts / site_script_revisions writes (scripts render with the CSP nonce on every
52
+ -- route, /cms included, so this was script injection into a real admin's session),
53
+ -- cms_redirects writes (an open redirect on any path), system_configuration (sign-up
54
+ -- policy) and the PII reads and updates on product_inquiries, message_threads and
55
+ -- thread_messages now use is_admin_mfa(). The CMS actions for all of them run on the
56
+ -- service role after requireCmsAdmin() (site scripts, redirects, registration/security,
57
+ -- messages); the public thread page and the contact paths already did. The public read
58
+ -- policies (active scripts, active redirects) are unchanged.
59
+ --
60
+ -- 4. media: uploader_id is pinned on insert, and storage keys in use are visible (MEDIUM).
61
+ -- media_insert_policy only checked the role, so a WRITER could insert a row attributed to
62
+ -- anyone; nothing pins object_key or variants either, and app/cms/media/actions.ts deleted
63
+ -- every variants[].objectKey of a deleted row with server credentials. A WRITER could
64
+ -- insert a decoy row listing other uploaders' keys and delete their files. Insert now pins
65
+ -- uploader_id to the caller (ADMIN keeps any). The delete and move actions keep only the
66
+ -- keys derived from the row's own object_key (lib/storage/media-object-keys.ts) and ask
67
+ -- public.media_storage_keys_in_use() which keys another row still references, so a shared
68
+ -- key is never deleted. The function is SECURITY INVOKER and service-role only: the app
69
+ -- calls it after its own role check, and the API roles have no use for it.
70
+ --
71
+ -- 5. package_activations: license keys are not readable over the API (MEDIUM, sandbox).
72
+ -- The public demo signs every visitor in as an ADMIN with no factor, so is_admin_mfa()
73
+ -- passes and "Staff read package activations" handed out the plaintext license key.
74
+ -- authenticated now holds SELECT on every column except license_key and meta (meta keeps
75
+ -- the Freemius activation response, install API token included). Every app read of the
76
+ -- table already uses the service role (packages page, dashboard, verifyPackageOnline,
77
+ -- revalidation, activation). Both apply paths repeat the column grant after their blanket
78
+ -- grants (API_ROLE_REVOKE_SQL), the same shape as profiles in 02025.
79
+ --
80
+ -- 6. media_read_policy stays USING (true) for anon (LOW, decision recorded).
81
+ -- The bucket is public-read and every rendered image carries its object URL, so a media
82
+ -- row discloses nothing the object URL does not, beyond its file name, description and
83
+ -- uploader id. Scoping it to "media referenced by live content" would have to follow every
84
+ -- reference (image blocks, section backgrounds, posts, products, logos, site_settings) and
85
+ -- would break images on any path it missed. docs/04 records the decision.
86
+ --
87
+ -- tools/scripts/security-verify.sql sections 30-34 check every item above, before and after.
88
+
89
+ -- ---------------------------------------------------------------------------
90
+ -- 1. site_settings: three write tiers, WRITER on an allow-list
91
+ -- ---------------------------------------------------------------------------
92
+ DROP POLICY IF EXISTS site_settings_insert_policy ON public.site_settings;
93
+ CREATE POLICY site_settings_insert_policy ON public.site_settings
94
+ FOR INSERT TO authenticated
95
+ WITH CHECK (
96
+ ((key = ANY (ARRAY['site_title','site_description','site_keywords','site_social_image','active_logo_id','footer_copyright','footer_show_attribution','onboarding_state']::text[]))
97
+ AND (SELECT public.get_current_user_role()) = ANY (ARRAY['ADMIN'::public.user_role,'WRITER'::public.user_role]))
98
+ OR ((key = ANY (ARRAY['enabled_payment_providers','ecommerce_inventory_settings']::text[]))
99
+ AND (SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role)
100
+ OR ((SELECT public.is_admin_mfa()) IS TRUE)
101
+ );
102
+
103
+ DROP POLICY IF EXISTS site_settings_update_policy ON public.site_settings;
104
+ CREATE POLICY site_settings_update_policy ON public.site_settings
105
+ FOR UPDATE TO authenticated
106
+ USING (
107
+ ((key = ANY (ARRAY['site_title','site_description','site_keywords','site_social_image','active_logo_id','footer_copyright','footer_show_attribution','onboarding_state']::text[]))
108
+ AND (SELECT public.get_current_user_role()) = ANY (ARRAY['ADMIN'::public.user_role,'WRITER'::public.user_role]))
109
+ OR ((key = ANY (ARRAY['enabled_payment_providers','ecommerce_inventory_settings']::text[]))
110
+ AND (SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role)
111
+ OR ((SELECT public.is_admin_mfa()) IS TRUE)
112
+ )
113
+ WITH CHECK (
114
+ ((key = ANY (ARRAY['site_title','site_description','site_keywords','site_social_image','active_logo_id','footer_copyright','footer_show_attribution','onboarding_state']::text[]))
115
+ AND (SELECT public.get_current_user_role()) = ANY (ARRAY['ADMIN'::public.user_role,'WRITER'::public.user_role]))
116
+ OR ((key = ANY (ARRAY['enabled_payment_providers','ecommerce_inventory_settings']::text[]))
117
+ AND (SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role)
118
+ OR ((SELECT public.is_admin_mfa()) IS TRUE)
119
+ );
120
+
121
+ DROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;
122
+ CREATE POLICY site_settings_delete_policy ON public.site_settings
123
+ FOR DELETE TO authenticated
124
+ USING (
125
+ ((key = ANY (ARRAY['site_title','site_description','site_keywords','site_social_image','active_logo_id','footer_copyright','footer_show_attribution','onboarding_state']::text[]))
126
+ AND (SELECT public.get_current_user_role()) = ANY (ARRAY['ADMIN'::public.user_role,'WRITER'::public.user_role]))
127
+ OR ((key = ANY (ARRAY['enabled_payment_providers','ecommerce_inventory_settings']::text[]))
128
+ AND (SELECT public.get_current_user_role()) = 'ADMIN'::public.user_role)
129
+ OR ((SELECT public.is_admin_mfa()) IS TRUE)
130
+ );
131
+
132
+ -- ---------------------------------------------------------------------------
133
+ -- 2. meets_aal_requirement(): an enrolled email factor counts as a factor
134
+ -- ---------------------------------------------------------------------------
135
+ -- True when the session is aal2, or when the caller enrolled no second factor at all: no
136
+ -- verified Supabase factor (TOTP) AND no enabled factor in user_security_settings (TOTP or the
137
+ -- app's email codes). Same signature, owner and grants as 02022; CREATE OR REPLACE keeps the
138
+ -- is_admin_mfa() wrapper and every policy that calls it.
139
+ CREATE OR REPLACE FUNCTION private.meets_aal_requirement()
140
+ RETURNS boolean
141
+ LANGUAGE sql STABLE SECURITY DEFINER
142
+ SET search_path TO ''
143
+ AS $$
144
+ SELECT COALESCE((auth.jwt() ->> 'aal'), 'aal1') = 'aal2'
145
+ OR (
146
+ NOT EXISTS (
147
+ SELECT 1 FROM auth.mfa_factors f
148
+ WHERE f.user_id = auth.uid() AND f.status = 'verified'
149
+ )
150
+ AND NOT EXISTS (
151
+ SELECT 1 FROM public.user_security_settings s
152
+ WHERE s.user_id = auth.uid() AND s.mfa_enabled AND s.mfa_type IS NOT NULL
153
+ )
154
+ );
155
+ $$;
156
+ REVOKE ALL ON FUNCTION private.meets_aal_requirement() FROM PUBLIC;
157
+ GRANT EXECUTE ON FUNCTION private.meets_aal_requirement() TO anon, authenticated, service_role;
158
+
159
+ -- ---------------------------------------------------------------------------
160
+ -- 3. Second factor on the remaining ADMIN branches
161
+ -- ---------------------------------------------------------------------------
162
+ -- site_scripts: INSERT / UPDATE / DELETE (the two read policies of 02013 are unchanged)
163
+ DROP POLICY IF EXISTS "Admins insert site scripts" ON public.site_scripts;
164
+ CREATE POLICY "Admins insert site scripts" ON public.site_scripts
165
+ FOR INSERT TO authenticated WITH CHECK (((SELECT public.is_admin_mfa()) IS TRUE));
166
+ DROP POLICY IF EXISTS "Admins update site scripts" ON public.site_scripts;
167
+ CREATE POLICY "Admins update site scripts" ON public.site_scripts
168
+ FOR UPDATE TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE)) WITH CHECK (((SELECT public.is_admin_mfa()) IS TRUE));
169
+ DROP POLICY IF EXISTS "Admins delete site scripts" ON public.site_scripts;
170
+ CREATE POLICY "Admins delete site scripts" ON public.site_scripts
171
+ FOR DELETE TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE));
172
+
173
+ -- site_script_revisions: INSERT (UPDATE / DELETE have no policy and no grant since 02026)
174
+ DROP POLICY IF EXISTS "Admins insert site script revisions" ON public.site_script_revisions;
175
+ CREATE POLICY "Admins insert site script revisions" ON public.site_script_revisions
176
+ FOR INSERT TO authenticated WITH CHECK (((SELECT public.is_admin_mfa()) IS TRUE));
177
+
178
+ -- cms_redirects: INSERT / UPDATE / DELETE (the two read policies of 02013 are unchanged)
179
+ DROP POLICY IF EXISTS "Admins insert redirects" ON public.cms_redirects;
180
+ CREATE POLICY "Admins insert redirects" ON public.cms_redirects
181
+ FOR INSERT TO authenticated WITH CHECK (((SELECT public.is_admin_mfa()) IS TRUE));
182
+ DROP POLICY IF EXISTS "Admins update redirects" ON public.cms_redirects;
183
+ CREATE POLICY "Admins update redirects" ON public.cms_redirects
184
+ FOR UPDATE TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE)) WITH CHECK (((SELECT public.is_admin_mfa()) IS TRUE));
185
+ DROP POLICY IF EXISTS "Admins delete redirects" ON public.cms_redirects;
186
+ CREATE POLICY "Admins delete redirects" ON public.cms_redirects
187
+ FOR DELETE TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE));
188
+
189
+ -- system_configuration: every command (the sign-up path reads it with the service role)
190
+ DROP POLICY IF EXISTS system_configuration_admin_select ON public.system_configuration;
191
+ CREATE POLICY system_configuration_admin_select ON public.system_configuration
192
+ FOR SELECT TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE));
193
+ DROP POLICY IF EXISTS system_configuration_admin_insert ON public.system_configuration;
194
+ CREATE POLICY system_configuration_admin_insert ON public.system_configuration
195
+ FOR INSERT TO authenticated WITH CHECK (((SELECT public.is_admin_mfa()) IS TRUE));
196
+ DROP POLICY IF EXISTS system_configuration_admin_update ON public.system_configuration;
197
+ CREATE POLICY system_configuration_admin_update ON public.system_configuration
198
+ FOR UPDATE TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE)) WITH CHECK (((SELECT public.is_admin_mfa()) IS TRUE));
199
+ DROP POLICY IF EXISTS system_configuration_admin_delete ON public.system_configuration;
200
+ CREATE POLICY system_configuration_admin_delete ON public.system_configuration
201
+ FOR DELETE TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE));
202
+
203
+ -- product_inquiries / message_threads: ADMIN read and update (INSERT / DELETE revoked in 02026)
204
+ DROP POLICY IF EXISTS product_inquiries_admin_read_policy ON public.product_inquiries;
205
+ CREATE POLICY product_inquiries_admin_read_policy ON public.product_inquiries
206
+ FOR SELECT TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE));
207
+ DROP POLICY IF EXISTS product_inquiries_admin_update_policy ON public.product_inquiries;
208
+ CREATE POLICY product_inquiries_admin_update_policy ON public.product_inquiries
209
+ FOR UPDATE TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE)) WITH CHECK (((SELECT public.is_admin_mfa()) IS TRUE));
210
+
211
+ DROP POLICY IF EXISTS message_threads_admin_read_policy ON public.message_threads;
212
+ CREATE POLICY message_threads_admin_read_policy ON public.message_threads
213
+ FOR SELECT TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE));
214
+ DROP POLICY IF EXISTS message_threads_admin_update_policy ON public.message_threads;
215
+ CREATE POLICY message_threads_admin_update_policy ON public.message_threads
216
+ FOR UPDATE TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE)) WITH CHECK (((SELECT public.is_admin_mfa()) IS TRUE));
217
+
218
+ -- thread_messages: ADMIN read (every write is the service role's, 02026)
219
+ DROP POLICY IF EXISTS thread_messages_admin_read_policy ON public.thread_messages;
220
+ CREATE POLICY thread_messages_admin_read_policy ON public.thread_messages
221
+ FOR SELECT TO authenticated USING (((SELECT public.is_admin_mfa()) IS TRUE));
222
+
223
+ -- ---------------------------------------------------------------------------
224
+ -- 4. media: pin uploader_id on insert; which storage keys a row still references
225
+ -- ---------------------------------------------------------------------------
226
+ -- Same shape as 02022's update/delete policies: a WRITER inserts rows attributed to
227
+ -- themselves, an ADMIN may attribute a row to anyone (imports, restores).
228
+ DROP POLICY IF EXISTS media_insert_policy ON public.media;
229
+ CREATE POLICY media_insert_policy ON public.media
230
+ FOR INSERT TO authenticated
231
+ WITH CHECK (
232
+ (SELECT public.is_admin()) IS TRUE
233
+ OR (uploader_id = (SELECT auth.uid())
234
+ AND (SELECT public.get_current_user_role()) = ANY (ARRAY['ADMIN'::public.user_role,'WRITER'::public.user_role]))
235
+ );
236
+
237
+ -- The keys of p_keys that some media row other than p_exclude_id references as its object_key,
238
+ -- its file_path or a variants[].objectKey. The CMS delete and move actions call it (service
239
+ -- role) after removing or claiming their own rows, and keep every key it returns.
240
+ CREATE OR REPLACE FUNCTION public.media_storage_keys_in_use(p_keys text[], p_exclude_id uuid DEFAULT NULL)
241
+ RETURNS SETOF text
242
+ LANGUAGE sql STABLE SECURITY INVOKER
243
+ SET search_path TO ''
244
+ AS $$
245
+ SELECT DISTINCT k.key
246
+ FROM unnest(p_keys) AS k(key)
247
+ WHERE EXISTS (
248
+ SELECT 1
249
+ FROM public.media m
250
+ WHERE m.id IS DISTINCT FROM p_exclude_id
251
+ AND (m.object_key = k.key
252
+ OR m.file_path = k.key
253
+ OR m.variants @> jsonb_build_array(jsonb_build_object('objectKey', k.key)))
254
+ );
255
+ $$;
256
+ REVOKE ALL ON FUNCTION public.media_storage_keys_in_use(text[], uuid) FROM PUBLIC, anon, authenticated;
257
+ GRANT EXECUTE ON FUNCTION public.media_storage_keys_in_use(text[], uuid) TO service_role;
258
+
259
+ -- ---------------------------------------------------------------------------
260
+ -- 5. package_activations: no license_key / meta for authenticated
261
+ -- ---------------------------------------------------------------------------
262
+ -- Table-level SELECT goes (revoking it also drops any column grant), then SELECT comes back on
263
+ -- every column except the two secrets. The column list is read from the catalog so the
264
+ -- statement matches API_ROLE_REVOKE_SQL; a new column is readable by an aal2 ADMIN, add it to
265
+ -- the exclusions there and here if it is sensitive.
266
+ DO $nb_package_activations$
267
+ DECLARE
268
+ v_cols text;
269
+ BEGIN
270
+ SELECT string_agg(quote_ident(a.attname), ', ' ORDER BY a.attnum)
271
+ INTO v_cols
272
+ FROM pg_attribute a
273
+ WHERE a.attrelid = 'public.package_activations'::regclass AND a.attnum > 0 AND NOT a.attisdropped
274
+ AND a.attname NOT IN ('license_key', 'meta');
275
+ REVOKE SELECT ON TABLE public.package_activations FROM authenticated;
276
+ IF v_cols IS NOT NULL THEN
277
+ EXECUTE format('GRANT SELECT (%s) ON TABLE public.package_activations TO authenticated', v_cols);
278
+ END IF;
279
+ END
280
+ $nb_package_activations$;
281
+
282
+ -- ---------------------------------------------------------------------------
283
+ -- 6. media_read_policy: unchanged on purpose (see the header)
284
+ -- ---------------------------------------------------------------------------