@nextblock-cms/db 0.22.0 → 0.22.2
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/supabase/server.d.ts +151 -2
- package/lib/supabase/types.d.ts +160 -2
- package/package.json +1 -1
- package/supabase/functions/nextblock-migration-ingest/index.ts +64 -1
- package/supabase/migrations/02022_security_hardening_writer_boundary_and_aal.sql +504 -0
- package/supabase/migrations/02023_aal2_for_orders_profiles_site_settings.sql +171 -0
- package/supabase/migrations/02024_advisor_trigger_grants_and_pg_net_schema.sql +113 -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
|
+
);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
-- 02024: clear the Supabase Security Advisor warnings that came back after the 02022/02023
|
|
2
|
+
-- deploys, and move a `pg_net` that was installed in `public` into `extensions`.
|
|
3
|
+
--
|
|
4
|
+
-- Why the SECURITY DEFINER warnings (lints 0028/0029) returned
|
|
5
|
+
-- 02013 revoked anon/authenticated EXECUTE on the two SECURITY DEFINER trigger functions
|
|
6
|
+
-- (handle_new_user(), update_product_ratings()) and the advisor was clean. But every
|
|
7
|
+
-- production build runs apps/nextblock/tools/build-migrate.mjs, whose migrate-core.mjs
|
|
8
|
+
-- ends with a blanket `grant execute on all functions in schema public to anon,
|
|
9
|
+
-- authenticated, service_role` and, unlike lib/setup/schema-apply.ts (the /setup path),
|
|
10
|
+
-- had no pass taking that EXECUTE back off trigger functions or re-applying the API-role
|
|
11
|
+
-- revokes of 02021/02022. So each deploy silently handed the privilege back. The hook is
|
|
12
|
+
-- fixed in the same change (and a test now keeps the two files' SQL identical); this
|
|
13
|
+
-- migration repairs the databases that were already re-granted. Postgres checks EXECUTE
|
|
14
|
+
-- on a trigger function only at CREATE TRIGGER time, never when the trigger fires, and
|
|
15
|
+
-- PostgREST cannot call a `RETURNS trigger` function via /rpc, so nothing loses access.
|
|
16
|
+
--
|
|
17
|
+
-- Why `pg_net` (lint 0014, "Extension in Public")
|
|
18
|
+
-- A bare `create extension pg_net` lands in `public` (the first search_path entry), which
|
|
19
|
+
-- exposes its objects through the API schema. pg_net is NOT relocatable, so the lint's
|
|
20
|
+
-- `alter extension … set schema` is refused; the only way out is drop + re-create WITH
|
|
21
|
+
-- SCHEMA extensions. Its own objects live in the `net` schema either way; only the
|
|
22
|
+
-- extension's registration moves, and the request queue / response log are transient.
|
|
23
|
+
-- pg_cron job commands are plain text (no dependency), so a scheduled `net.http_get`
|
|
24
|
+
-- keeps working. Guarded: a database without pg_net, or with it already outside
|
|
25
|
+
-- `public`, is untouched. Same statement tools/scripts/schedule-sandbox-reset.js runs.
|
|
26
|
+
--
|
|
27
|
+
-- Everything here is idempotent and safe on a fresh install, the sandbox replay and a
|
|
28
|
+
-- /setup-installed database. The two Auth-dashboard warnings (leaked password protection,
|
|
29
|
+
-- MFA options) are project settings, not schema, and are not addressed here.
|
|
30
|
+
|
|
31
|
+
-- ---------------------------------------------------------------------------
|
|
32
|
+
-- 1. Trigger functions: the API roles never need EXECUTE
|
|
33
|
+
-- (identical to the post-apply pass in migrate-core.mjs / schema-apply.ts)
|
|
34
|
+
-- ---------------------------------------------------------------------------
|
|
35
|
+
do $nb_trg$
|
|
36
|
+
declare r record;
|
|
37
|
+
begin
|
|
38
|
+
for r in
|
|
39
|
+
select p.oid::regprocedure as sig
|
|
40
|
+
from pg_proc p
|
|
41
|
+
join pg_type t on t.oid = p.prorettype
|
|
42
|
+
where p.pronamespace = 'public'::regnamespace and t.typname = 'trigger'
|
|
43
|
+
loop
|
|
44
|
+
execute format('revoke execute on function %s from public, anon, authenticated', r.sig);
|
|
45
|
+
end loop;
|
|
46
|
+
end $nb_trg$;
|
|
47
|
+
|
|
48
|
+
-- The two the advisor names, stated explicitly so the intent survives a grep (02013 parity).
|
|
49
|
+
REVOKE EXECUTE ON FUNCTION public.handle_new_user() FROM PUBLIC, anon, authenticated;
|
|
50
|
+
GRANT EXECUTE ON FUNCTION public.handle_new_user() TO service_role;
|
|
51
|
+
|
|
52
|
+
REVOKE EXECUTE ON FUNCTION public.update_product_ratings() FROM PUBLIC, anon, authenticated;
|
|
53
|
+
GRANT EXECUTE ON FUNCTION public.update_product_ratings() TO service_role;
|
|
54
|
+
|
|
55
|
+
-- ---------------------------------------------------------------------------
|
|
56
|
+
-- 2. The API-role revokes of 02021 / 02022 that the same blanket grant handed back
|
|
57
|
+
-- (identical to API_ROLE_REVOKE_SQL in migrate-core.mjs / schema-apply.ts)
|
|
58
|
+
-- ---------------------------------------------------------------------------
|
|
59
|
+
do $nb_api$
|
|
60
|
+
begin
|
|
61
|
+
revoke truncate on all tables in schema public from anon, authenticated;
|
|
62
|
+
if to_regprocedure('public.generate_order_invoice_number()') is not null then
|
|
63
|
+
revoke execute on function public.generate_order_invoice_number() from public, anon, authenticated;
|
|
64
|
+
end if;
|
|
65
|
+
if to_regprocedure('public.assign_order_invoice_metadata(uuid, timestamp with time zone)') is not null then
|
|
66
|
+
revoke execute on function public.assign_order_invoice_metadata(uuid, timestamp with time zone) from public, anon, authenticated;
|
|
67
|
+
end if;
|
|
68
|
+
if to_regclass('public.order_invoice_number_seq') is not null then
|
|
69
|
+
revoke all on sequence public.order_invoice_number_seq from anon, authenticated;
|
|
70
|
+
end if;
|
|
71
|
+
if to_regprocedure('public.upsert_product_with_variants(jsonb)') is not null then
|
|
72
|
+
revoke execute on function public.upsert_product_with_variants(jsonb) from public, anon;
|
|
73
|
+
end if;
|
|
74
|
+
if to_regprocedure('public.bump_email_2fa_attempts(uuid)') is not null then
|
|
75
|
+
revoke execute on function public.bump_email_2fa_attempts(uuid) from public, anon, authenticated;
|
|
76
|
+
end if;
|
|
77
|
+
if to_regprocedure('public.redeem_coupon_atomically(uuid)') is not null then
|
|
78
|
+
revoke execute on function public.redeem_coupon_atomically(uuid) from public, anon, authenticated;
|
|
79
|
+
end if;
|
|
80
|
+
end $nb_api$;
|
|
81
|
+
|
|
82
|
+
-- ---------------------------------------------------------------------------
|
|
83
|
+
-- 3. pg_net out of `public` (not relocatable: drop + re-create WITH SCHEMA extensions)
|
|
84
|
+
-- ---------------------------------------------------------------------------
|
|
85
|
+
do $nb_pg_net$
|
|
86
|
+
declare
|
|
87
|
+
v_version text;
|
|
88
|
+
begin
|
|
89
|
+
select e.extversion
|
|
90
|
+
into v_version
|
|
91
|
+
from pg_extension e
|
|
92
|
+
where e.extname = 'pg_net'
|
|
93
|
+
and e.extnamespace = 'public'::regnamespace;
|
|
94
|
+
|
|
95
|
+
if v_version is null then
|
|
96
|
+
return; -- not installed, or already registered outside public
|
|
97
|
+
end if;
|
|
98
|
+
|
|
99
|
+
if to_regnamespace('extensions') is null then
|
|
100
|
+
raise notice '02024: pg_net % is registered in schema public and this database has no extensions schema; reinstall it by hand with `create extension pg_net with schema <schema>`', v_version;
|
|
101
|
+
return;
|
|
102
|
+
end if;
|
|
103
|
+
|
|
104
|
+
-- The pg_net worker holds net.http_request_queue while a request is in flight. Fail fast
|
|
105
|
+
-- rather than queue behind it; re-running the migration is cheap.
|
|
106
|
+
perform set_config('lock_timeout', '15s', true);
|
|
107
|
+
|
|
108
|
+
drop extension pg_net;
|
|
109
|
+
create extension pg_net with schema extensions;
|
|
110
|
+
|
|
111
|
+
raise notice '02024: pg_net re-registered in schema extensions (was % in public, now %)',
|
|
112
|
+
v_version, (select extversion from pg_extension where extname = 'pg_net');
|
|
113
|
+
end $nb_pg_net$;
|