@sunboyoo/supabase-rbac 1.0.3 → 1.0.4

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,132 @@
1
+ /*
2
+ Fix: ensure GUC keys used in rbac.has_perm always start with a letter.
3
+ Postgres requires each dot-separated GUC segment to start with [A-Za-z_].
4
+ Prefixing the md5 with 'h' avoids invalid configuration parameter name errors.
5
+ */
6
+
7
+ create or replace function rbac.has_perm(target_app_id text, required_perm_name text)
8
+ returns boolean
9
+ language plpgsql
10
+ stable
11
+ security definer
12
+ set search_path = ''
13
+ as $$
14
+ declare
15
+ _res_key text := 'rbac.res.h' || md5(coalesce(target_app_id,'') || '|' || coalesce(required_perm_name,''));
16
+ _t_roles_key text := 'rbac.rids.h' || md5('t|' || coalesce(target_app_id,''));
17
+ _g_roles_key text := 'rbac.rids.h' || md5('g|global');
18
+ _t_pid_key text := 'rbac.pid.h' || md5('t|' || coalesce(target_app_id,'') || '|' || coalesce(required_perm_name,''));
19
+ _g_pid_key text := 'rbac.pid.h' || md5('g|global|' || coalesce(required_perm_name,''));
20
+
21
+ _target_role_ids uuid[];
22
+ _global_role_ids uuid[];
23
+
24
+ _target_pid uuid;
25
+ _global_pid uuid;
26
+
27
+ _sentinel constant uuid := '00000000-0000-0000-0000-000000000000'::uuid;
28
+ _granted boolean := false;
29
+
30
+ _jwt jsonb;
31
+ begin
32
+ -- Level 1: final decision cache
33
+ if current_setting(_res_key, true) is not null then
34
+ return current_setting(_res_key) = 'true';
35
+ end if;
36
+
37
+ _jwt := auth.jwt();
38
+
39
+ -- Level 2: cache role_ids(uuid[]) for target app (never 500; fail-closed)
40
+ if current_setting(_t_roles_key, true) is null then
41
+ begin
42
+ select coalesce(array_agg((x.value)::uuid), '{}'::uuid[])
43
+ into _target_role_ids
44
+ from jsonb_array_elements_text(
45
+ coalesce(_jwt #> array['app_metadata','role_ids', target_app_id], '[]'::jsonb)
46
+ ) as x(value);
47
+ exception when others then
48
+ _target_role_ids := '{}'::uuid[];
49
+ end;
50
+ perform set_config(_t_roles_key, _target_role_ids::text, true);
51
+ else
52
+ begin
53
+ _target_role_ids := current_setting(_t_roles_key)::uuid[];
54
+ exception when others then
55
+ _target_role_ids := '{}'::uuid[];
56
+ end;
57
+ end if;
58
+
59
+ -- Level 2b: cache role_ids(uuid[]) for global
60
+ if current_setting(_g_roles_key, true) is null then
61
+ begin
62
+ select coalesce(array_agg((x.value)::uuid), '{}'::uuid[])
63
+ into _global_role_ids
64
+ from jsonb_array_elements_text(
65
+ coalesce(_jwt #> array['app_metadata','role_ids','global'], '[]'::jsonb)
66
+ ) as x(value);
67
+ exception when others then
68
+ _global_role_ids := '{}'::uuid[];
69
+ end;
70
+ perform set_config(_g_roles_key, _global_role_ids::text, true);
71
+ else
72
+ begin
73
+ _global_role_ids := current_setting(_g_roles_key)::uuid[];
74
+ exception when others then
75
+ _global_role_ids := '{}'::uuid[];
76
+ end;
77
+ end if;
78
+
79
+ -- Level 3: permission_id cache for target app (sentinel if not found)
80
+ if current_setting(_t_pid_key, true) is null then
81
+ select p.id
82
+ into _target_pid
83
+ from rbac.permissions p
84
+ where p.app_id = target_app_id
85
+ and p.name = required_perm_name;
86
+
87
+ _target_pid := coalesce(_target_pid, _sentinel);
88
+ perform set_config(_t_pid_key, _target_pid::text, true);
89
+ else
90
+ _target_pid := current_setting(_t_pid_key)::uuid;
91
+ end if;
92
+
93
+ -- Level 3b: permission_id cache for global mirror (sentinel if not found)
94
+ if current_setting(_g_pid_key, true) is null then
95
+ select p.id
96
+ into _global_pid
97
+ from rbac.permissions p
98
+ where p.app_id = 'global'
99
+ and p.name = required_perm_name;
100
+
101
+ _global_pid := coalesce(_global_pid, _sentinel);
102
+ perform set_config(_g_pid_key, _global_pid::text, true);
103
+ else
104
+ _global_pid := current_setting(_g_pid_key)::uuid;
105
+ end if;
106
+
107
+ -- Path A: target roles ↔ target permission
108
+ if _target_pid <> _sentinel and cardinality(_target_role_ids) > 0 then
109
+ select exists (
110
+ select 1
111
+ from rbac.role_permissions rp
112
+ where rp.app_id = target_app_id
113
+ and rp.permission_id = _target_pid
114
+ and rp.role_id = any(_target_role_ids)
115
+ ) into _granted;
116
+ end if;
117
+
118
+ -- Path B: global roles ↔ global mirrored permission
119
+ if not _granted and _global_pid <> _sentinel and cardinality(_global_role_ids) > 0 then
120
+ select exists (
121
+ select 1
122
+ from rbac.role_permissions rp
123
+ where rp.app_id = 'global'
124
+ and rp.permission_id = _global_pid
125
+ and rp.role_id = any(_global_role_ids)
126
+ ) into _granted;
127
+ end if;
128
+
129
+ perform set_config(_res_key, _granted::text, true);
130
+ return _granted;
131
+ end;
132
+ $$;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sunboyoo/supabase-rbac",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Industrial-grade RBAC helper for Supabase multi-app projects (JWT role_ids + optional permission RPC + React hooks + pg manager + CLI).",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -106,4 +106,4 @@
106
106
  "access": "public",
107
107
  "registry": "https://registry.npmjs.org/"
108
108
  }
109
- }
109
+ }