snipe-auth-rbac 0.3.1 → 0.4.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.
- package/dist/admin/index.cjs +201 -11
- package/dist/admin/index.cjs.map +1 -1
- package/dist/admin/index.d.cts +112 -3
- package/dist/admin/index.d.ts +112 -3
- package/dist/admin/index.js +200 -12
- package/dist/admin/index.js.map +1 -1
- package/dist/chunk-WL4QZ7HO.js +138 -0
- package/dist/chunk-WL4QZ7HO.js.map +1 -0
- package/dist/{chunk-C76JHCKM.js → chunk-XHPBUCFN.js} +33 -1
- package/dist/chunk-XHPBUCFN.js.map +1 -0
- package/dist/index-CJqb5nY5.d.cts +191 -0
- package/dist/index-nfrns9Ye.d.ts +191 -0
- package/dist/index.cjs +42 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -118
- package/dist/index.d.ts +3 -118
- package/dist/index.js +4 -2
- package/dist/react/index.cjs +111 -11
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +34 -59
- package/dist/react/index.d.ts +34 -59
- package/dist/react/index.js +14 -13
- package/dist/react/index.js.map +1 -1
- package/dist/types-Oj9yfWvz.d.cts +132 -0
- package/dist/types-Oj9yfWvz.d.ts +132 -0
- package/package.json +1 -1
- package/sql/0001_initial.sql +137 -2
- package/dist/chunk-C76JHCKM.js.map +0 -1
- package/dist/chunk-NRDW233A.js +0 -69
- package/dist/chunk-NRDW233A.js.map +0 -1
- package/dist/types-DxvFudPF.d.cts +0 -69
- package/dist/types-DxvFudPF.d.ts +0 -69
package/sql/0001_initial.sql
CHANGED
|
@@ -138,9 +138,44 @@ CREATE TABLE IF NOT EXISTS rbac.role_permissions (
|
|
|
138
138
|
can_write boolean NOT NULL DEFAULT false,
|
|
139
139
|
can_update boolean NOT NULL DEFAULT false,
|
|
140
140
|
can_delete boolean NOT NULL DEFAULT false,
|
|
141
|
+
-- 0.4.0+. Per-action origin tracking. NULL = direct admin grant;
|
|
142
|
+
-- non-NULL = name of the parent resource whose `dependsOn` edge
|
|
143
|
+
-- caused this cell to be materialised. The matrix UI reads this
|
|
144
|
+
-- to render the "Implied by …" badge; the resolver and
|
|
145
|
+
-- `rbac.user_can(...)` SQL function ignore the columns entirely
|
|
146
|
+
-- (flat matrix lookups are unchanged so RLS stays performant).
|
|
147
|
+
read_granted_via text,
|
|
148
|
+
write_granted_via text,
|
|
149
|
+
update_granted_via text,
|
|
150
|
+
delete_granted_via text,
|
|
141
151
|
PRIMARY KEY (role_id, resource)
|
|
142
152
|
);
|
|
143
153
|
|
|
154
|
+
-- Backfill for adopters upgrading from 0.3.x.
|
|
155
|
+
ALTER TABLE rbac.role_permissions
|
|
156
|
+
ADD COLUMN IF NOT EXISTS read_granted_via text,
|
|
157
|
+
ADD COLUMN IF NOT EXISTS write_granted_via text,
|
|
158
|
+
ADD COLUMN IF NOT EXISTS update_granted_via text,
|
|
159
|
+
ADD COLUMN IF NOT EXISTS delete_granted_via text;
|
|
160
|
+
|
|
161
|
+
-- 0.4.0+. Dependent-read graph. One row per (parent, child, action)
|
|
162
|
+
-- cascade edge. The host registry's `dependsOn` arrays get
|
|
163
|
+
-- materialised here by `service.syncResources()` on app boot. The
|
|
164
|
+
-- admin matrix UI consults this on every toggle-on to decide which
|
|
165
|
+
-- implied rows to write alongside the parent.
|
|
166
|
+
--
|
|
167
|
+
-- The resolver does NOT read this table at runtime — implied rows
|
|
168
|
+
-- live on `rbac.role_permissions` and look identical to direct
|
|
169
|
+
-- grants at the policy layer. This keeps RLS flat and fast.
|
|
170
|
+
CREATE TABLE IF NOT EXISTS rbac.resource_dependencies (
|
|
171
|
+
parent_resource text NOT NULL REFERENCES rbac.resources(resource) ON DELETE CASCADE,
|
|
172
|
+
child_resource text NOT NULL REFERENCES rbac.resources(resource) ON DELETE CASCADE,
|
|
173
|
+
action text NOT NULL CHECK (action IN ('read','write','update','delete')),
|
|
174
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
175
|
+
PRIMARY KEY (parent_resource, child_resource, action),
|
|
176
|
+
CONSTRAINT resource_dependencies_no_self CHECK (parent_resource <> child_resource)
|
|
177
|
+
);
|
|
178
|
+
|
|
144
179
|
CREATE OR REPLACE FUNCTION rbac.check_role_resource_scope()
|
|
145
180
|
RETURNS trigger
|
|
146
181
|
LANGUAGE plpgsql
|
|
@@ -289,12 +324,20 @@ AS $$
|
|
|
289
324
|
JOIN rbac.roles r ON r.id = usr.role_id
|
|
290
325
|
WHERE usr.user_id = p_user_id
|
|
291
326
|
),
|
|
327
|
+
-- 0.4.0+. Per-action direct grants are aggregated alongside the
|
|
328
|
+
-- regular permission booleans. Direct = action true AND its
|
|
329
|
+
-- granted_via is NULL on at least one of the user's roles. bool_or
|
|
330
|
+
-- across roles gives "any role grants this directly".
|
|
292
331
|
system_perms AS (
|
|
293
332
|
SELECT rp.resource,
|
|
294
333
|
bool_or(rp.can_read) AS can_read,
|
|
295
334
|
bool_or(rp.can_write) AS can_write,
|
|
296
335
|
bool_or(rp.can_update) AS can_update,
|
|
297
|
-
bool_or(rp.can_delete) AS can_delete
|
|
336
|
+
bool_or(rp.can_delete) AS can_delete,
|
|
337
|
+
bool_or(rp.can_read AND rp.read_granted_via IS NULL) AS direct_read,
|
|
338
|
+
bool_or(rp.can_write AND rp.write_granted_via IS NULL) AS direct_write,
|
|
339
|
+
bool_or(rp.can_update AND rp.update_granted_via IS NULL) AS direct_update,
|
|
340
|
+
bool_or(rp.can_delete AND rp.delete_granted_via IS NULL) AS direct_delete
|
|
298
341
|
FROM rbac.user_system_roles usr
|
|
299
342
|
JOIN rbac.role_permissions rp ON rp.role_id = usr.role_id
|
|
300
343
|
WHERE usr.user_id = p_user_id
|
|
@@ -320,7 +363,11 @@ AS $$
|
|
|
320
363
|
bool_or(rp.can_read) AS can_read,
|
|
321
364
|
bool_or(rp.can_write) AS can_write,
|
|
322
365
|
bool_or(rp.can_update) AS can_update,
|
|
323
|
-
bool_or(rp.can_delete) AS can_delete
|
|
366
|
+
bool_or(rp.can_delete) AS can_delete,
|
|
367
|
+
bool_or(rp.can_read AND rp.read_granted_via IS NULL) AS direct_read,
|
|
368
|
+
bool_or(rp.can_write AND rp.write_granted_via IS NULL) AS direct_write,
|
|
369
|
+
bool_or(rp.can_update AND rp.update_granted_via IS NULL) AS direct_update,
|
|
370
|
+
bool_or(rp.can_delete AND rp.delete_granted_via IS NULL) AS direct_delete
|
|
324
371
|
FROM rbac.user_company_roles ucr
|
|
325
372
|
JOIN rbac.role_permissions rp ON rp.role_id = ucr.role_id
|
|
326
373
|
WHERE ucr.user_id = p_user_id
|
|
@@ -340,6 +387,24 @@ AS $$
|
|
|
340
387
|
)) FROM system_perms),
|
|
341
388
|
'{}'::jsonb
|
|
342
389
|
),
|
|
390
|
+
-- 0.4.0+. Per-action direct-grant maps. Drives canAccessSection()
|
|
391
|
+
-- on the client side for top-level nav / list-page gating.
|
|
392
|
+
'system_direct_reads', coalesce(
|
|
393
|
+
(SELECT jsonb_object_agg(resource, true)
|
|
394
|
+
FROM system_perms WHERE direct_read), '{}'::jsonb
|
|
395
|
+
),
|
|
396
|
+
'system_direct_writes', coalesce(
|
|
397
|
+
(SELECT jsonb_object_agg(resource, true)
|
|
398
|
+
FROM system_perms WHERE direct_write), '{}'::jsonb
|
|
399
|
+
),
|
|
400
|
+
'system_direct_updates', coalesce(
|
|
401
|
+
(SELECT jsonb_object_agg(resource, true)
|
|
402
|
+
FROM system_perms WHERE direct_update), '{}'::jsonb
|
|
403
|
+
),
|
|
404
|
+
'system_direct_deletes', coalesce(
|
|
405
|
+
(SELECT jsonb_object_agg(resource, true)
|
|
406
|
+
FROM system_perms WHERE direct_delete), '{}'::jsonb
|
|
407
|
+
),
|
|
343
408
|
'memberships', coalesce(
|
|
344
409
|
(SELECT jsonb_agg(jsonb_build_object(
|
|
345
410
|
'company_id', m.company_id,
|
|
@@ -354,6 +419,30 @@ AS $$
|
|
|
354
419
|
FROM company_perms cp
|
|
355
420
|
WHERE cp.company_id = m.company_id),
|
|
356
421
|
'{}'::jsonb
|
|
422
|
+
),
|
|
423
|
+
'direct_reads', coalesce(
|
|
424
|
+
(SELECT jsonb_object_agg(cp.resource, true)
|
|
425
|
+
FROM company_perms cp
|
|
426
|
+
WHERE cp.company_id = m.company_id AND cp.direct_read),
|
|
427
|
+
'{}'::jsonb
|
|
428
|
+
),
|
|
429
|
+
'direct_writes', coalesce(
|
|
430
|
+
(SELECT jsonb_object_agg(cp.resource, true)
|
|
431
|
+
FROM company_perms cp
|
|
432
|
+
WHERE cp.company_id = m.company_id AND cp.direct_write),
|
|
433
|
+
'{}'::jsonb
|
|
434
|
+
),
|
|
435
|
+
'direct_updates', coalesce(
|
|
436
|
+
(SELECT jsonb_object_agg(cp.resource, true)
|
|
437
|
+
FROM company_perms cp
|
|
438
|
+
WHERE cp.company_id = m.company_id AND cp.direct_update),
|
|
439
|
+
'{}'::jsonb
|
|
440
|
+
),
|
|
441
|
+
'direct_deletes', coalesce(
|
|
442
|
+
(SELECT jsonb_object_agg(cp.resource, true)
|
|
443
|
+
FROM company_perms cp
|
|
444
|
+
WHERE cp.company_id = m.company_id AND cp.direct_delete),
|
|
445
|
+
'{}'::jsonb
|
|
357
446
|
)
|
|
358
447
|
)) FROM memberships m),
|
|
359
448
|
'[]'::jsonb
|
|
@@ -361,6 +450,52 @@ AS $$
|
|
|
361
450
|
);
|
|
362
451
|
$$;
|
|
363
452
|
|
|
453
|
+
-- 0.4.0+. Matrix-UI cascade helper. Returns the (child, action) pairs
|
|
454
|
+
-- that should be materialised when an admin toggles
|
|
455
|
+
-- (parent_resource, p_action) on for a role.
|
|
456
|
+
CREATE OR REPLACE FUNCTION rbac.list_implied_grants(
|
|
457
|
+
p_parent_resource text,
|
|
458
|
+
p_action text
|
|
459
|
+
) RETURNS TABLE (child_resource text, action text)
|
|
460
|
+
LANGUAGE sql
|
|
461
|
+
STABLE
|
|
462
|
+
SET search_path = rbac, public
|
|
463
|
+
AS $$
|
|
464
|
+
SELECT d.child_resource, d.action
|
|
465
|
+
FROM rbac.resource_dependencies d
|
|
466
|
+
WHERE d.parent_resource = p_parent_resource
|
|
467
|
+
AND d.action = p_action;
|
|
468
|
+
$$;
|
|
469
|
+
|
|
470
|
+
-- 0.4.0+. Atomic replace-all for the dependency graph. The admin
|
|
471
|
+
-- transport calls this on every `syncResources()` so removed
|
|
472
|
+
-- `dependsOn` edges actually disappear from the table. Argument is
|
|
473
|
+
-- a JSONB array of `{ parent_resource, child_resource, action }`
|
|
474
|
+
-- objects.
|
|
475
|
+
CREATE OR REPLACE FUNCTION rbac.replace_resource_dependencies(
|
|
476
|
+
p_edges jsonb
|
|
477
|
+
) RETURNS int
|
|
478
|
+
LANGUAGE plpgsql
|
|
479
|
+
SECURITY DEFINER
|
|
480
|
+
SET search_path = rbac, public
|
|
481
|
+
AS $$
|
|
482
|
+
DECLARE
|
|
483
|
+
v_count int;
|
|
484
|
+
BEGIN
|
|
485
|
+
DELETE FROM rbac.resource_dependencies;
|
|
486
|
+
INSERT INTO rbac.resource_dependencies (
|
|
487
|
+
parent_resource, child_resource, action
|
|
488
|
+
)
|
|
489
|
+
SELECT e->>'parent_resource',
|
|
490
|
+
e->>'child_resource',
|
|
491
|
+
e->>'action'
|
|
492
|
+
FROM jsonb_array_elements(coalesce(p_edges, '[]'::jsonb)) AS e
|
|
493
|
+
ON CONFLICT (parent_resource, child_resource, action) DO NOTHING;
|
|
494
|
+
GET DIAGNOSTICS v_count = ROW_COUNT;
|
|
495
|
+
RETURN v_count;
|
|
496
|
+
END;
|
|
497
|
+
$$;
|
|
498
|
+
|
|
364
499
|
-- ─────────────────────────────────────────────────────────────────
|
|
365
500
|
-- 7b. Template defaults — pre-fill role_permissions from a JSONB
|
|
366
501
|
-- pattern carried on the role row. Run after sync_resources() so
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["/**\n * Transport-agnostic client: turns an adopter-supplied\n * `AuthRbacFetcher` into a permission resolver. The React provider\n * wraps this; non-React consumers (Node scripts, edge functions)\n * can use it directly.\n */\n\nimport type {\n Action,\n AuthRbacFetcher,\n PermissionMap,\n ResourceDescriptor,\n ResourceRegistry,\n ResourceScope,\n UserProfile,\n} from \"./types.js\";\n\nexport interface AuthRbacClientOptions {\n fetcher: AuthRbacFetcher;\n /**\n * The host project's full resource list. Required so the resolver\n * can look up a resource's scope without a DB round-trip per call.\n * Re-using the same array the host syncs into the\n * `rbac.resources` table at boot keeps everything in lockstep.\n */\n resources: ResourceRegistry;\n}\n\nexport interface CanOptions {\n /**\n * Override the active company. Omit to use the company the\n * caller has currently activated (the React provider tracks\n * this; for direct client use you must pass it).\n */\n companyId?: string | null;\n}\n\n/**\n * Pure resolver. Given a hydrated profile it answers boolean\n * questions instantly — no I/O. The `resourceMap` is built once at\n * construction so per-call work is two map lookups.\n */\nexport function buildPermissionResolver(\n resources: ResourceRegistry,\n profile: UserProfile,\n defaultCompanyId: string | null,\n) {\n const scopeByResource = new Map<string, ResourceScope>(\n resources.map((r) => [r.resource, r.scope]),\n );\n\n const can = (\n resource: string,\n action: Action,\n options?: CanOptions,\n ): boolean => {\n if (profile.is_super_admin) {\n return true;\n }\n const scope = scopeByResource.get(resource);\n if (!scope) {\n // Unknown resource — fail closed.\n return false;\n }\n if (scope === \"system\") {\n return readGrid(profile.system_permissions, resource, action);\n }\n const companyId = options?.companyId ?? defaultCompanyId;\n if (!companyId) {\n return false;\n }\n const membership = profile.memberships.find(\n (m) => m.company_id === companyId,\n );\n if (!membership) {\n return false;\n }\n return readGrid(membership.permissions, resource, action);\n };\n\n return {\n can,\n /** Permission map for the active (or specified) company. */\n activePermissions: (companyId?: string | null): PermissionMap => {\n const id = companyId ?? defaultCompanyId;\n if (!id) {\n return {};\n }\n return (\n profile.memberships.find((m) => m.company_id === id)?.permissions ?? {}\n );\n },\n systemPermissions: (): PermissionMap => profile.system_permissions,\n };\n}\n\nfunction readGrid(\n map: PermissionMap,\n resource: string,\n action: Action,\n): boolean {\n const grid = map[resource];\n if (!grid) {\n return false;\n }\n return grid[action];\n}\n\n/**\n * Helper: groups a resource registry by `group` for the matrix UI.\n * Returns groups in insertion order with their resources.\n */\nexport function groupResources(\n registry: ResourceRegistry,\n): Array<{ group: string; resources: ResourceDescriptor[] }> {\n const order: string[] = [];\n const buckets = new Map<string, ResourceDescriptor[]>();\n for (const r of registry) {\n const key = r.group ?? \"Sonstige\";\n if (!buckets.has(key)) {\n buckets.set(key, []);\n order.push(key);\n }\n buckets.get(key)!.push(r);\n }\n return order.map((g) => ({ group: g, resources: buckets.get(g)! }));\n}\n\nexport type AuthRbacClient = ReturnType<typeof buildPermissionResolver>;\nexport type { AuthRbacClientOptions as ClientOptions };\n"],"mappings":";AA0CO,SAAS,wBACd,WACA,SACA,kBACA;AACA,QAAM,kBAAkB,IAAI;AAAA,IAC1B,UAAU,IAAI,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC;AAAA,EAC5C;AAEA,QAAM,MAAM,CACV,UACA,QACA,YACY;AACZ,QAAI,QAAQ,gBAAgB;AAC1B,aAAO;AAAA,IACT;AACA,UAAM,QAAQ,gBAAgB,IAAI,QAAQ;AAC1C,QAAI,CAAC,OAAO;AAEV,aAAO;AAAA,IACT;AACA,QAAI,UAAU,UAAU;AACtB,aAAO,SAAS,QAAQ,oBAAoB,UAAU,MAAM;AAAA,IAC9D;AACA,UAAM,YAAY,SAAS,aAAa;AACxC,QAAI,CAAC,WAAW;AACd,aAAO;AAAA,IACT;AACA,UAAM,aAAa,QAAQ,YAAY;AAAA,MACrC,CAAC,MAAM,EAAE,eAAe;AAAA,IAC1B;AACA,QAAI,CAAC,YAAY;AACf,aAAO;AAAA,IACT;AACA,WAAO,SAAS,WAAW,aAAa,UAAU,MAAM;AAAA,EAC1D;AAEA,SAAO;AAAA,IACL;AAAA;AAAA,IAEA,mBAAmB,CAAC,cAA6C;AAC/D,YAAM,KAAK,aAAa;AACxB,UAAI,CAAC,IAAI;AACP,eAAO,CAAC;AAAA,MACV;AACA,aACE,QAAQ,YAAY,KAAK,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,eAAe,CAAC;AAAA,IAE1E;AAAA,IACA,mBAAmB,MAAqB,QAAQ;AAAA,EAClD;AACF;AAEA,SAAS,SACP,KACA,UACA,QACS;AACT,QAAM,OAAO,IAAI,QAAQ;AACzB,MAAI,CAAC,MAAM;AACT,WAAO;AAAA,EACT;AACA,SAAO,KAAK,MAAM;AACpB;AAMO,SAAS,eACd,UAC2D;AAC3D,QAAM,QAAkB,CAAC;AACzB,QAAM,UAAU,oBAAI,IAAkC;AACtD,aAAW,KAAK,UAAU;AACxB,UAAM,MAAM,EAAE,SAAS;AACvB,QAAI,CAAC,QAAQ,IAAI,GAAG,GAAG;AACrB,cAAQ,IAAI,KAAK,CAAC,CAAC;AACnB,YAAM,KAAK,GAAG;AAAA,IAChB;AACA,YAAQ,IAAI,GAAG,EAAG,KAAK,CAAC;AAAA,EAC1B;AACA,SAAO,MAAM,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,WAAW,QAAQ,IAAI,CAAC,EAAG,EAAE;AACpE;","names":[]}
|
package/dist/chunk-NRDW233A.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
// src/fetchers.ts
|
|
2
|
-
function createSupabaseFetcher(opts) {
|
|
3
|
-
return {
|
|
4
|
-
async fetchProfile() {
|
|
5
|
-
const { data, error } = await opts.supabase.schema("rbac").rpc(
|
|
6
|
-
"user_profile",
|
|
7
|
-
{ p_user_id: opts.userId }
|
|
8
|
-
);
|
|
9
|
-
if (error) {
|
|
10
|
-
throw new Error(
|
|
11
|
-
`auth-rbac: failed to load user profile via Supabase RPC: ${error.message}`
|
|
12
|
-
);
|
|
13
|
-
}
|
|
14
|
-
return normalizeProfile(data);
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
async function detectRbacSchema(supabase) {
|
|
19
|
-
try {
|
|
20
|
-
const { error } = await supabase.schema("rbac").rpc("user_can", {
|
|
21
|
-
p_user_id: "00000000-0000-0000-0000-000000000000",
|
|
22
|
-
p_resource: "__rbac_self_check__",
|
|
23
|
-
p_action: "read",
|
|
24
|
-
p_company_id: null
|
|
25
|
-
});
|
|
26
|
-
return error === null;
|
|
27
|
-
} catch {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
function createHttpFetcher(opts) {
|
|
32
|
-
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
33
|
-
return {
|
|
34
|
-
async fetchProfile() {
|
|
35
|
-
const res = await fetchImpl(opts.url, opts.init);
|
|
36
|
-
if (!res.ok) {
|
|
37
|
-
throw new Error(
|
|
38
|
-
`auth-rbac: profile endpoint ${opts.url} returned ${res.status}`
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
const json = await res.json();
|
|
42
|
-
return normalizeProfile(json);
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
function normalizeProfile(raw) {
|
|
47
|
-
if (!raw || typeof raw !== "object") {
|
|
48
|
-
throw new Error("auth-rbac: profile payload is not an object");
|
|
49
|
-
}
|
|
50
|
-
const p = raw;
|
|
51
|
-
if (typeof p.user_id !== "string") {
|
|
52
|
-
throw new Error("auth-rbac: profile payload missing user_id");
|
|
53
|
-
}
|
|
54
|
-
return {
|
|
55
|
-
user_id: p.user_id,
|
|
56
|
-
is_super_admin: !!p.is_super_admin,
|
|
57
|
-
system_roles: Array.isArray(p.system_roles) ? p.system_roles : [],
|
|
58
|
-
system_permissions: p.system_permissions && typeof p.system_permissions === "object" ? p.system_permissions : {},
|
|
59
|
-
system_frontend_config: p.system_frontend_config && typeof p.system_frontend_config === "object" ? p.system_frontend_config : {},
|
|
60
|
-
memberships: Array.isArray(p.memberships) ? p.memberships : []
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export {
|
|
65
|
-
createSupabaseFetcher,
|
|
66
|
-
detectRbacSchema,
|
|
67
|
-
createHttpFetcher
|
|
68
|
-
};
|
|
69
|
-
//# sourceMappingURL=chunk-NRDW233A.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/fetchers.ts"],"sourcesContent":["/**\n * Built-in fetchers — adopters can use these or pass their own\n * implementation of `AuthRbacFetcher`.\n */\n\nimport type { AuthRbacFetcher, UserProfile } from \"./types.js\";\n\n/**\n * Calls the package's SQL function `rbac.user_profile(uuid)` via\n * a Supabase JS client. Easiest path when the host project already\n * uses Supabase.\n *\n * The function lives in the dedicated `rbac` Postgres schema, so the\n * adopter must add `rbac` to their PostgREST exposed-schemas list\n * (Supabase Studio → Settings → API → Exposed schemas) for the\n * `.schema('rbac')` call below to reach it.\n *\n * @example\n * createSupabaseFetcher({ supabase, userId: session.user.id })\n */\nexport function createSupabaseFetcher(opts: {\n supabase: {\n schema: (name: string) => {\n rpc: (\n fn: string,\n args: Record<string, unknown>,\n ) => Promise<{ data: unknown; error: { message: string } | null }>;\n };\n };\n userId: string;\n}): AuthRbacFetcher {\n return {\n async fetchProfile(): Promise<UserProfile> {\n const { data, error } = await opts.supabase.schema(\"rbac\").rpc(\n \"user_profile\",\n { p_user_id: opts.userId },\n );\n if (error) {\n throw new Error(\n `auth-rbac: failed to load user profile via Supabase RPC: ${error.message}`,\n );\n }\n return normalizeProfile(data);\n },\n };\n}\n\n/**\n * Cheap probe — returns true if the package's `rbac` schema looks\n * reachable. Useful at app start to fail loudly if the migration\n * hasn't been applied OR if `rbac` isn't in the project's PostgREST\n * exposed-schemas list.\n *\n * @example\n * if (!(await detectRbacSchema(supabase))) {\n * console.error(\"rbac schema not reachable — apply 0001_initial.sql\");\n * }\n */\nexport async function detectRbacSchema(supabase: {\n schema: (name: string) => {\n rpc: (\n fn: string,\n args: Record<string, unknown>,\n ) => Promise<{ data: unknown; error: { message: string } | null }>;\n };\n}): Promise<boolean> {\n try {\n const { error } = await supabase.schema(\"rbac\").rpc(\"user_can\", {\n p_user_id: \"00000000-0000-0000-0000-000000000000\",\n p_resource: \"__rbac_self_check__\",\n p_action: \"read\",\n p_company_id: null,\n });\n return error === null;\n } catch {\n return false;\n }\n}\n\n/**\n * Calls a regular HTTP endpoint that returns a `UserProfile` JSON\n * payload. Use this when the host project has its own backend that\n * wraps the package's Python helpers (or any equivalent).\n *\n * @example\n * createHttpFetcher({ url: \"/api/users/me/profile\" })\n */\nexport function createHttpFetcher(opts: {\n url: string;\n /** Forwarded as-is to `fetch`. Use this to attach auth headers. */\n init?: RequestInit;\n /** Override the global `fetch` if you're in a non-browser env. */\n fetch?: typeof fetch;\n}): AuthRbacFetcher {\n const fetchImpl = opts.fetch ?? globalThis.fetch;\n return {\n async fetchProfile(): Promise<UserProfile> {\n const res = await fetchImpl(opts.url, opts.init);\n if (!res.ok) {\n throw new Error(\n `auth-rbac: profile endpoint ${opts.url} returned ${res.status}`,\n );\n }\n const json = (await res.json()) as unknown;\n return normalizeProfile(json);\n },\n };\n}\n\n/**\n * Defensive normalisation: the Supabase RPC returns whatever the SQL\n * function emitted. We coerce missing fields into the empty defaults\n * so consumers can iterate without null checks. Throws if the shape\n * is unrecognisable.\n */\nfunction normalizeProfile(raw: unknown): UserProfile {\n if (!raw || typeof raw !== \"object\") {\n throw new Error(\"auth-rbac: profile payload is not an object\");\n }\n const p = raw as Partial<UserProfile> & Record<string, unknown>;\n if (typeof p.user_id !== \"string\") {\n throw new Error(\"auth-rbac: profile payload missing user_id\");\n }\n return {\n user_id: p.user_id,\n is_super_admin: !!p.is_super_admin,\n system_roles: Array.isArray(p.system_roles) ? p.system_roles : [],\n system_permissions:\n p.system_permissions && typeof p.system_permissions === \"object\"\n ? (p.system_permissions as UserProfile[\"system_permissions\"])\n : {},\n system_frontend_config:\n p.system_frontend_config && typeof p.system_frontend_config === \"object\"\n ? (p.system_frontend_config as UserProfile[\"system_frontend_config\"])\n : {},\n memberships: Array.isArray(p.memberships)\n ? (p.memberships as UserProfile[\"memberships\"])\n : [],\n };\n}\n"],"mappings":";AAoBO,SAAS,sBAAsB,MAUlB;AAClB,SAAO;AAAA,IACL,MAAM,eAAqC;AACzC,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,KAAK,SAAS,OAAO,MAAM,EAAE;AAAA,QACzD;AAAA,QACA,EAAE,WAAW,KAAK,OAAO;AAAA,MAC3B;AACA,UAAI,OAAO;AACT,cAAM,IAAI;AAAA,UACR,4DAA4D,MAAM,OAAO;AAAA,QAC3E;AAAA,MACF;AACA,aAAO,iBAAiB,IAAI;AAAA,IAC9B;AAAA,EACF;AACF;AAaA,eAAsB,iBAAiB,UAOlB;AACnB,MAAI;AACF,UAAM,EAAE,MAAM,IAAI,MAAM,SAAS,OAAO,MAAM,EAAE,IAAI,YAAY;AAAA,MAC9D,WAAW;AAAA,MACX,YAAY;AAAA,MACZ,UAAU;AAAA,MACV,cAAc;AAAA,IAChB,CAAC;AACD,WAAO,UAAU;AAAA,EACnB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAUO,SAAS,kBAAkB,MAMd;AAClB,QAAM,YAAY,KAAK,SAAS,WAAW;AAC3C,SAAO;AAAA,IACL,MAAM,eAAqC;AACzC,YAAM,MAAM,MAAM,UAAU,KAAK,KAAK,KAAK,IAAI;AAC/C,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,IAAI;AAAA,UACR,+BAA+B,KAAK,GAAG,aAAa,IAAI,MAAM;AAAA,QAChE;AAAA,MACF;AACA,YAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,aAAO,iBAAiB,IAAI;AAAA,IAC9B;AAAA,EACF;AACF;AAQA,SAAS,iBAAiB,KAA2B;AACnD,MAAI,CAAC,OAAO,OAAO,QAAQ,UAAU;AACnC,UAAM,IAAI,MAAM,6CAA6C;AAAA,EAC/D;AACA,QAAM,IAAI;AACV,MAAI,OAAO,EAAE,YAAY,UAAU;AACjC,UAAM,IAAI,MAAM,4CAA4C;AAAA,EAC9D;AACA,SAAO;AAAA,IACL,SAAS,EAAE;AAAA,IACX,gBAAgB,CAAC,CAAC,EAAE;AAAA,IACpB,cAAc,MAAM,QAAQ,EAAE,YAAY,IAAI,EAAE,eAAe,CAAC;AAAA,IAChE,oBACE,EAAE,sBAAsB,OAAO,EAAE,uBAAuB,WACnD,EAAE,qBACH,CAAC;AAAA,IACP,wBACE,EAAE,0BAA0B,OAAO,EAAE,2BAA2B,WAC3D,EAAE,yBACH,CAAC;AAAA,IACP,aAAa,MAAM,QAAQ,EAAE,WAAW,IACnC,EAAE,cACH,CAAC;AAAA,EACP;AACF;","names":[]}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core types — used by both the transport-agnostic client and the
|
|
3
|
-
* React layer. Adopters that don't use React only need to depend on
|
|
4
|
-
* the `index` entry; the `react` entry's types extend these.
|
|
5
|
-
*/
|
|
6
|
-
type Action = "read" | "write" | "update" | "delete";
|
|
7
|
-
type ResourceScope = "system" | "company";
|
|
8
|
-
interface ResourceDescriptor {
|
|
9
|
-
resource: string;
|
|
10
|
-
scope: ResourceScope;
|
|
11
|
-
label: string;
|
|
12
|
-
description?: string;
|
|
13
|
-
group?: string;
|
|
14
|
-
}
|
|
15
|
-
interface RoleSummary {
|
|
16
|
-
id: string;
|
|
17
|
-
name: string;
|
|
18
|
-
is_system?: boolean;
|
|
19
|
-
is_super?: boolean;
|
|
20
|
-
frontend_config?: FrontendConfig;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Free-form per-role config that the host project can interpret as
|
|
24
|
-
* it sees fit. A typical shape includes `sidebar` (list of section
|
|
25
|
-
* keys), `default_dashboard` (string), `home_route` (string).
|
|
26
|
-
*/
|
|
27
|
-
type FrontendConfig = Record<string, unknown>;
|
|
28
|
-
interface PermissionGrid {
|
|
29
|
-
read: boolean;
|
|
30
|
-
write: boolean;
|
|
31
|
-
update: boolean;
|
|
32
|
-
delete: boolean;
|
|
33
|
-
}
|
|
34
|
-
type PermissionMap = Record<string, PermissionGrid>;
|
|
35
|
-
interface CompanyMembership {
|
|
36
|
-
company_id: string;
|
|
37
|
-
company_name: string;
|
|
38
|
-
company_slug?: string | null;
|
|
39
|
-
roles: RoleSummary[];
|
|
40
|
-
permissions: PermissionMap;
|
|
41
|
-
/** Merged frontend_config across all roles the user holds in this company. */
|
|
42
|
-
frontend_config: FrontendConfig;
|
|
43
|
-
}
|
|
44
|
-
interface UserProfile {
|
|
45
|
-
user_id: string;
|
|
46
|
-
is_super_admin: boolean;
|
|
47
|
-
system_roles: RoleSummary[];
|
|
48
|
-
system_permissions: PermissionMap;
|
|
49
|
-
/** Merged frontend_config across all of the user's system roles. */
|
|
50
|
-
system_frontend_config: FrontendConfig;
|
|
51
|
-
memberships: CompanyMembership[];
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Adopter-supplied transport. Two flavours are supported out of the
|
|
55
|
-
* box:
|
|
56
|
-
*
|
|
57
|
-
* 1. Pass a Supabase client + a JWT-derived `user_id` and the
|
|
58
|
-
* library calls the package's SQL RPC `rbac.user_profile`.
|
|
59
|
-
* 2. Pass a plain `fetcher` (anything that resolves a `UserProfile`)
|
|
60
|
-
* and the library calls that. Use this when your backend serves
|
|
61
|
-
* a custom `/api/users/me/profile` endpoint or when you don't
|
|
62
|
-
* run Supabase at all.
|
|
63
|
-
*/
|
|
64
|
-
interface AuthRbacFetcher {
|
|
65
|
-
fetchProfile(): Promise<UserProfile>;
|
|
66
|
-
}
|
|
67
|
-
type ResourceRegistry = ReadonlyArray<ResourceDescriptor>;
|
|
68
|
-
|
|
69
|
-
export type { Action as A, CompanyMembership as C, FrontendConfig as F, PermissionGrid as P, ResourceScope as R, UserProfile as U, ResourceDescriptor as a, AuthRbacFetcher as b, ResourceRegistry as c, PermissionMap as d, RoleSummary as e };
|
package/dist/types-DxvFudPF.d.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core types — used by both the transport-agnostic client and the
|
|
3
|
-
* React layer. Adopters that don't use React only need to depend on
|
|
4
|
-
* the `index` entry; the `react` entry's types extend these.
|
|
5
|
-
*/
|
|
6
|
-
type Action = "read" | "write" | "update" | "delete";
|
|
7
|
-
type ResourceScope = "system" | "company";
|
|
8
|
-
interface ResourceDescriptor {
|
|
9
|
-
resource: string;
|
|
10
|
-
scope: ResourceScope;
|
|
11
|
-
label: string;
|
|
12
|
-
description?: string;
|
|
13
|
-
group?: string;
|
|
14
|
-
}
|
|
15
|
-
interface RoleSummary {
|
|
16
|
-
id: string;
|
|
17
|
-
name: string;
|
|
18
|
-
is_system?: boolean;
|
|
19
|
-
is_super?: boolean;
|
|
20
|
-
frontend_config?: FrontendConfig;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Free-form per-role config that the host project can interpret as
|
|
24
|
-
* it sees fit. A typical shape includes `sidebar` (list of section
|
|
25
|
-
* keys), `default_dashboard` (string), `home_route` (string).
|
|
26
|
-
*/
|
|
27
|
-
type FrontendConfig = Record<string, unknown>;
|
|
28
|
-
interface PermissionGrid {
|
|
29
|
-
read: boolean;
|
|
30
|
-
write: boolean;
|
|
31
|
-
update: boolean;
|
|
32
|
-
delete: boolean;
|
|
33
|
-
}
|
|
34
|
-
type PermissionMap = Record<string, PermissionGrid>;
|
|
35
|
-
interface CompanyMembership {
|
|
36
|
-
company_id: string;
|
|
37
|
-
company_name: string;
|
|
38
|
-
company_slug?: string | null;
|
|
39
|
-
roles: RoleSummary[];
|
|
40
|
-
permissions: PermissionMap;
|
|
41
|
-
/** Merged frontend_config across all roles the user holds in this company. */
|
|
42
|
-
frontend_config: FrontendConfig;
|
|
43
|
-
}
|
|
44
|
-
interface UserProfile {
|
|
45
|
-
user_id: string;
|
|
46
|
-
is_super_admin: boolean;
|
|
47
|
-
system_roles: RoleSummary[];
|
|
48
|
-
system_permissions: PermissionMap;
|
|
49
|
-
/** Merged frontend_config across all of the user's system roles. */
|
|
50
|
-
system_frontend_config: FrontendConfig;
|
|
51
|
-
memberships: CompanyMembership[];
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Adopter-supplied transport. Two flavours are supported out of the
|
|
55
|
-
* box:
|
|
56
|
-
*
|
|
57
|
-
* 1. Pass a Supabase client + a JWT-derived `user_id` and the
|
|
58
|
-
* library calls the package's SQL RPC `rbac.user_profile`.
|
|
59
|
-
* 2. Pass a plain `fetcher` (anything that resolves a `UserProfile`)
|
|
60
|
-
* and the library calls that. Use this when your backend serves
|
|
61
|
-
* a custom `/api/users/me/profile` endpoint or when you don't
|
|
62
|
-
* run Supabase at all.
|
|
63
|
-
*/
|
|
64
|
-
interface AuthRbacFetcher {
|
|
65
|
-
fetchProfile(): Promise<UserProfile>;
|
|
66
|
-
}
|
|
67
|
-
type ResourceRegistry = ReadonlyArray<ResourceDescriptor>;
|
|
68
|
-
|
|
69
|
-
export type { Action as A, CompanyMembership as C, FrontendConfig as F, PermissionGrid as P, ResourceScope as R, UserProfile as U, ResourceDescriptor as a, AuthRbacFetcher as b, ResourceRegistry as c, PermissionMap as d, RoleSummary as e };
|