@nextblock-cms/db 0.22.0 → 0.22.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.
Files changed (26) hide show
  1. package/README.md +47 -47
  2. package/lib/supabase/server.d.ts +151 -2
  3. package/lib/supabase/types.d.ts +160 -2
  4. package/package.json +1 -1
  5. package/supabase/functions/nextblock-migration-ingest/index.ts +64 -1
  6. package/supabase/migrations/02000_catchup_gen1.sql +5880 -5880
  7. package/supabase/migrations/02001_baseline_schema.sql +2734 -2734
  8. package/supabase/migrations/02002_baseline_constraints_and_indexes.sql +1262 -1262
  9. package/supabase/migrations/02003_baseline_security_and_grants.sql +1577 -1577
  10. package/supabase/migrations/02004_baseline_seed.sql +565 -565
  11. package/supabase/migrations/02005_home_live_demo_promo_cadence_15min.sql +53 -53
  12. package/supabase/migrations/02006_home_marketing_contrast.sql +56 -56
  13. package/supabase/migrations/02007_home_pillar_caption_contrast.sql +44 -44
  14. package/supabase/migrations/02008_home_fr_parity.sql +89 -89
  15. package/supabase/migrations/02009_fr_cortex_posts.sql +67 -67
  16. package/supabase/migrations/02010_shop_hero_redesign.sql +72 -72
  17. package/supabase/migrations/02011_drop_backdrop_blur_on_pages.sql +26 -26
  18. package/supabase/migrations/02012_drop_variant_backdrop_blur.sql +17 -17
  19. package/supabase/migrations/02013_advisor_rls_and_trigger_grants.sql +102 -102
  20. package/supabase/migrations/02014_package_activations_staff_read.sql +18 -18
  21. package/supabase/migrations/02018_install_paths_and_seed_copy_accuracy.sql +1820 -1820
  22. package/supabase/migrations/02019_open_handed_licensing_and_mcp_clients.sql +448 -448
  23. package/supabase/migrations/02020_commerce_product_accuracy.sql +93 -93
  24. package/supabase/migrations/02021_profiles_staff_read_and_role_guard.sql +362 -362
  25. package/supabase/migrations/02022_security_hardening_writer_boundary_and_aal.sql +504 -0
  26. package/supabase/migrations/02023_aal2_for_orders_profiles_site_settings.sql +171 -0
@@ -0,0 +1,171 @@
1
+ -- 02023: the second factor inside RLS reaches the last ADMIN-only rows: orders, order_items,
2
+ -- profiles (other accounts' rows, admin writes) and the ADMIN tier of site_settings.
3
+ --
4
+ -- 02022 (sections 5 and 14) introduced public.is_admin_mfa() -- ADMIN, and an aal2 session
5
+ -- whenever the account has a verified MFA factor; an admin who never enrolled one always
6
+ -- passes -- and applied it where the app never reads with the cookie client (mcp_access_tokens,
7
+ -- package_activations, coupons, coupon_redemptions). It deliberately left orders, order_items,
8
+ -- profiles and the site_settings secret tier on the plain public.is_admin() check: the CMS read
9
+ -- them with the cookie-scoped client, and an ADMIN who signed in with "remember this device"
10
+ -- passes the app's own 2FA gate (proxy.ts / app/cms/layout.tsx) while the Supabase session
11
+ -- stays aal1, so gating those rows would have emptied the admin's own screens.
12
+ --
13
+ -- Those reads and writes now go through the service-role client after an in-code role check
14
+ -- (apps/nextblock/lib/auth/cms-actor.ts; libs/ecommerce .../server-actions/require-ecom-role.ts;
15
+ -- docs/04 "Row Level Security Patterns"), so this migration can put is_admin_mfa() in every
16
+ -- ADMIN branch of those tables. DB-side effect: an ADMIN session JWT carrying only a first
17
+ -- factor, used directly against PostgREST, reaches no order, no other account's profile row and
18
+ -- no setting above the staff tier, and cannot write any of them. Nothing else moves:
19
+ --
20
+ -- * owner semantics are unchanged: a customer still reads their own orders and order items,
21
+ -- every signed-in account still reads and edits its own profile row (role pinned, exactly
22
+ -- as 02021 left it);
23
+ -- * WRITER is unchanged: still reads every profiles row (02021), still nothing on orders,
24
+ -- still the public + staff tiers of site_settings and nothing above them;
25
+ -- * the anon read policy on site_settings is untouched, and the staff-tier keys stay
26
+ -- readable and the non-sensitive keys writable by an aal1 ADMIN (the ADMIN branch only
27
+ -- governs what lies above the staff tier);
28
+ -- * user_addresses has no ADMIN policy at all (owner + service role), so there is nothing
29
+ -- to gate there;
30
+ -- * the service-role policies are untouched.
31
+ --
32
+ -- Every policy is dropped by name and recreated, so the file is idempotent and re-runnable.
33
+ -- tools/scripts/security-verify.sql (sections 13-17) checks every assertion above.
34
+
35
+ -- ---------------------------------------------------------------------------
36
+ -- 1. orders: ADMIN branch requires the second factor; owner branch unchanged
37
+ -- ---------------------------------------------------------------------------
38
+ DROP POLICY IF EXISTS "Users can view own orders" ON public.orders;
39
+ CREATE POLICY "Users can view own orders" ON public.orders
40
+ FOR SELECT TO authenticated
41
+ USING (((SELECT public.is_admin_mfa()) IS TRUE) OR (user_id = (SELECT auth.uid())));
42
+
43
+ DROP POLICY IF EXISTS orders_insert_policy ON public.orders;
44
+ CREATE POLICY orders_insert_policy ON public.orders
45
+ FOR INSERT TO authenticated
46
+ WITH CHECK (((SELECT public.is_admin_mfa()) IS TRUE));
47
+
48
+ DROP POLICY IF EXISTS orders_update_policy ON public.orders;
49
+ CREATE POLICY orders_update_policy ON public.orders
50
+ FOR UPDATE TO authenticated
51
+ USING (((SELECT public.is_admin_mfa()) IS TRUE))
52
+ WITH CHECK (((SELECT public.is_admin_mfa()) IS TRUE));
53
+
54
+ DROP POLICY IF EXISTS orders_delete_policy ON public.orders;
55
+ CREATE POLICY orders_delete_policy ON public.orders
56
+ FOR DELETE TO authenticated
57
+ USING (((SELECT public.is_admin_mfa()) IS TRUE));
58
+
59
+ -- ---------------------------------------------------------------------------
60
+ -- 2. order_items: same shape; the owner branch still walks through the parent order
61
+ -- ---------------------------------------------------------------------------
62
+ DROP POLICY IF EXISTS "Users can view own order items" ON public.order_items;
63
+ CREATE POLICY "Users can view own order items" ON public.order_items
64
+ FOR SELECT TO authenticated
65
+ USING (
66
+ ((SELECT public.is_admin_mfa()) IS TRUE)
67
+ OR EXISTS (
68
+ SELECT 1 FROM public.orders
69
+ WHERE orders.id = order_items.order_id
70
+ AND orders.user_id = (SELECT auth.uid())
71
+ )
72
+ );
73
+
74
+ DROP POLICY IF EXISTS order_items_insert_policy ON public.order_items;
75
+ CREATE POLICY order_items_insert_policy ON public.order_items
76
+ FOR INSERT TO authenticated
77
+ WITH CHECK (((SELECT public.is_admin_mfa()) IS TRUE));
78
+
79
+ DROP POLICY IF EXISTS order_items_update_policy ON public.order_items;
80
+ CREATE POLICY order_items_update_policy ON public.order_items
81
+ FOR UPDATE TO authenticated
82
+ USING (((SELECT public.is_admin_mfa()) IS TRUE))
83
+ WITH CHECK (((SELECT public.is_admin_mfa()) IS TRUE));
84
+
85
+ DROP POLICY IF EXISTS order_items_delete_policy ON public.order_items;
86
+ CREATE POLICY order_items_delete_policy ON public.order_items
87
+ FOR DELETE TO authenticated
88
+ USING (((SELECT public.is_admin_mfa()) IS TRUE));
89
+
90
+ -- ---------------------------------------------------------------------------
91
+ -- 3. profiles: own row as before, WRITER as before (02021), ADMIN needs the second factor
92
+ -- ---------------------------------------------------------------------------
93
+ -- 02021 wrote the staff branch as "role IN (ADMIN, WRITER)". It is split here so that the
94
+ -- WRITER half keeps its plain role check (unchanged visibility for a content role) while the
95
+ -- ADMIN half goes through is_admin_mfa(). The helpers run as the table owner (private schema,
96
+ -- SECURITY DEFINER), so a policy on profiles itself may call them without recursion.
97
+ DROP POLICY IF EXISTS profiles_read_policy ON public.profiles;
98
+ CREATE POLICY profiles_read_policy ON public.profiles
99
+ FOR SELECT TO authenticated
100
+ USING (
101
+ (id = (SELECT auth.uid()))
102
+ OR ((SELECT public.get_current_user_role()) = 'WRITER'::public.user_role)
103
+ OR ((SELECT public.is_admin_mfa()) IS TRUE)
104
+ );
105
+
106
+ DROP POLICY IF EXISTS profiles_insert_policy ON public.profiles;
107
+ CREATE POLICY profiles_insert_policy ON public.profiles
108
+ FOR INSERT TO authenticated
109
+ WITH CHECK (((SELECT public.is_admin_mfa()) IS TRUE));
110
+
111
+ -- An account edits its own row with `role` pinned to its current value (02021); an ADMIN on a
112
+ -- satisfied second factor edits any row. The protect_profile_role trigger stays behind both.
113
+ DROP POLICY IF EXISTS profiles_update_policy ON public.profiles;
114
+ CREATE POLICY profiles_update_policy ON public.profiles
115
+ FOR UPDATE TO authenticated
116
+ USING ((id = (SELECT auth.uid())) OR ((SELECT public.is_admin_mfa()) IS TRUE))
117
+ WITH CHECK (
118
+ ((SELECT public.is_admin_mfa()) IS TRUE)
119
+ OR ((id = (SELECT auth.uid())) AND (role = (SELECT public.get_current_user_role())))
120
+ );
121
+
122
+ -- ---------------------------------------------------------------------------
123
+ -- 4. site_settings: the ADMIN tier (reads above staff, writes of the sensitive keys)
124
+ -- ---------------------------------------------------------------------------
125
+ -- The key arrays are verbatim from 02021 (read tiers) and 02022 (write tiers); only the ADMIN
126
+ -- branch changes. The anon read policy is not touched.
127
+ DROP POLICY IF EXISTS site_settings_read_policy ON public.site_settings;
128
+ CREATE POLICY site_settings_read_policy ON public.site_settings
129
+ FOR SELECT TO authenticated
130
+ USING (
131
+ (key = ANY (ARRAY['site_title','site_description','site_keywords','site_social_image','footer_copyright','footer_show_attribution','global_css','language_detection_settings','seo_robots_settings','bot_protection_public','active_logo_id','is_admin_created','privacy_settings','enabled_payment_providers','ecommerce_inventory_settings','migration_baseline_generation','invoice_settings']::text[]))
132
+ OR ((key = ANY (ARRAY['security_settings','onboarding_state','email_public','cortex_ai_site_brief']::text[]))
133
+ AND (SELECT public.get_current_user_role()) = ANY (ARRAY['ADMIN'::public.user_role,'WRITER'::public.user_role]))
134
+ OR ((SELECT public.is_admin_mfa()) IS TRUE)
135
+ );
136
+
137
+ DROP POLICY IF EXISTS site_settings_insert_policy ON public.site_settings;
138
+ CREATE POLICY site_settings_insert_policy ON public.site_settings
139
+ FOR INSERT TO authenticated
140
+ WITH CHECK (
141
+ (key <> ALL (ARRAY['cortex_ai_openrouter_api_key','bot_protection_secret','email_secret','payment_secret','language_detection_settings','cortex_ai_pexels_api_key','cortex_ai_unsplash_access_key','cortex_ai_mcp_settings','seo_robots_settings','privacy_settings','is_admin_created','global_css','security_settings']::text[])
142
+ AND (SELECT public.get_current_user_role()) = ANY (ARRAY['ADMIN'::public.user_role,'WRITER'::public.user_role]))
143
+ OR (key = ANY (ARRAY['cortex_ai_openrouter_api_key','bot_protection_secret','email_secret','payment_secret','language_detection_settings','cortex_ai_pexels_api_key','cortex_ai_unsplash_access_key','cortex_ai_mcp_settings','seo_robots_settings','privacy_settings','is_admin_created','global_css','security_settings']::text[])
144
+ AND ((SELECT public.is_admin_mfa()) IS TRUE))
145
+ );
146
+
147
+ DROP POLICY IF EXISTS site_settings_update_policy ON public.site_settings;
148
+ CREATE POLICY site_settings_update_policy ON public.site_settings
149
+ FOR UPDATE TO authenticated
150
+ USING (
151
+ (key <> ALL (ARRAY['cortex_ai_openrouter_api_key','bot_protection_secret','email_secret','payment_secret','language_detection_settings','cortex_ai_pexels_api_key','cortex_ai_unsplash_access_key','cortex_ai_mcp_settings','seo_robots_settings','privacy_settings','is_admin_created','global_css','security_settings']::text[])
152
+ AND (SELECT public.get_current_user_role()) = ANY (ARRAY['ADMIN'::public.user_role,'WRITER'::public.user_role]))
153
+ OR (key = ANY (ARRAY['cortex_ai_openrouter_api_key','bot_protection_secret','email_secret','payment_secret','language_detection_settings','cortex_ai_pexels_api_key','cortex_ai_unsplash_access_key','cortex_ai_mcp_settings','seo_robots_settings','privacy_settings','is_admin_created','global_css','security_settings']::text[])
154
+ AND ((SELECT public.is_admin_mfa()) IS TRUE))
155
+ )
156
+ WITH CHECK (
157
+ (key <> ALL (ARRAY['cortex_ai_openrouter_api_key','bot_protection_secret','email_secret','payment_secret','language_detection_settings','cortex_ai_pexels_api_key','cortex_ai_unsplash_access_key','cortex_ai_mcp_settings','seo_robots_settings','privacy_settings','is_admin_created','global_css','security_settings']::text[])
158
+ AND (SELECT public.get_current_user_role()) = ANY (ARRAY['ADMIN'::public.user_role,'WRITER'::public.user_role]))
159
+ OR (key = ANY (ARRAY['cortex_ai_openrouter_api_key','bot_protection_secret','email_secret','payment_secret','language_detection_settings','cortex_ai_pexels_api_key','cortex_ai_unsplash_access_key','cortex_ai_mcp_settings','seo_robots_settings','privacy_settings','is_admin_created','global_css','security_settings']::text[])
160
+ AND ((SELECT public.is_admin_mfa()) IS TRUE))
161
+ );
162
+
163
+ DROP POLICY IF EXISTS site_settings_delete_policy ON public.site_settings;
164
+ CREATE POLICY site_settings_delete_policy ON public.site_settings
165
+ FOR DELETE TO authenticated
166
+ USING (
167
+ (key <> ALL (ARRAY['cortex_ai_openrouter_api_key','bot_protection_secret','email_secret','payment_secret','language_detection_settings','cortex_ai_pexels_api_key','cortex_ai_unsplash_access_key','cortex_ai_mcp_settings','seo_robots_settings','privacy_settings','is_admin_created','global_css','security_settings']::text[])
168
+ AND (SELECT public.get_current_user_role()) = ANY (ARRAY['ADMIN'::public.user_role,'WRITER'::public.user_role]))
169
+ OR (key = ANY (ARRAY['cortex_ai_openrouter_api_key','bot_protection_secret','email_secret','payment_secret','language_detection_settings','cortex_ai_pexels_api_key','cortex_ai_unsplash_access_key','cortex_ai_mcp_settings','seo_robots_settings','privacy_settings','is_admin_created','global_css','security_settings']::text[])
170
+ AND ((SELECT public.is_admin_mfa()) IS TRUE))
171
+ );