snipe-auth-rbac 0.4.0 → 0.5.0
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 +53 -76
- package/dist/admin/index.cjs.map +1 -1
- package/dist/admin/index.d.cts +13 -4
- package/dist/admin/index.d.ts +13 -4
- package/dist/admin/index.js +53 -76
- package/dist/admin/index.js.map +1 -1
- package/dist/{chunk-5UAIIOKT.js → chunk-WL4QZ7HO.js} +9 -4
- package/dist/chunk-WL4QZ7HO.js.map +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/react/index.cjs +8 -3
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.js +1 -1
- package/package.json +1 -1
- package/sql/0001_initial.sql +162 -42
- package/dist/chunk-5UAIIOKT.js.map +0 -1
package/sql/0001_initial.sql
CHANGED
|
@@ -232,6 +232,18 @@ CREATE INDEX IF NOT EXISTS user_company_roles_company_idx
|
|
|
232
232
|
-- 6. Resolver function — the single source of truth for "can?"
|
|
233
233
|
-- ─────────────────────────────────────────────────────────────────
|
|
234
234
|
|
|
235
|
+
-- 0.5.0+: resolver expansion. user_can returns true if the user has a
|
|
236
|
+
-- DIRECT grant on the resource OR a direct grant on any PARENT of
|
|
237
|
+
-- the resource (per rbac.resource_dependencies). Implied access is
|
|
238
|
+
-- computed at lookup time — there are no materialised "implied"
|
|
239
|
+
-- rows on role_permissions any more, so the admin can't accidentally
|
|
240
|
+
-- override a dependent grant and the matrix can't drift.
|
|
241
|
+
--
|
|
242
|
+
-- Parent expansion requires SAME-SCOPE grants: a system-scope grant
|
|
243
|
+
-- on properties implies a system-scope grant on units; a company-
|
|
244
|
+
-- scope grant on properties in company X implies a company-scope
|
|
245
|
+
-- grant on units in company X. Cross-scope expansion is never
|
|
246
|
+
-- silent.
|
|
235
247
|
CREATE OR REPLACE FUNCTION rbac.user_can(
|
|
236
248
|
p_user_id uuid,
|
|
237
249
|
p_resource text,
|
|
@@ -245,9 +257,9 @@ SET search_path = rbac, public
|
|
|
245
257
|
AS $$
|
|
246
258
|
DECLARE
|
|
247
259
|
v_resource_scope text;
|
|
260
|
+
v_action_col text;
|
|
248
261
|
BEGIN
|
|
249
|
-
-- Super-admin bypass
|
|
250
|
-
-- every permission everywhere.
|
|
262
|
+
-- Super-admin bypass.
|
|
251
263
|
IF EXISTS (
|
|
252
264
|
SELECT 1
|
|
253
265
|
FROM rbac.user_system_roles usr
|
|
@@ -272,6 +284,7 @@ BEGIN
|
|
|
272
284
|
|
|
273
285
|
IF v_resource_scope = 'system' THEN
|
|
274
286
|
RETURN EXISTS (
|
|
287
|
+
-- Direct grant on the resource at system scope.
|
|
275
288
|
SELECT 1
|
|
276
289
|
FROM rbac.user_system_roles usr
|
|
277
290
|
JOIN rbac.role_permissions rp ON rp.role_id = usr.role_id
|
|
@@ -283,14 +296,36 @@ BEGIN
|
|
|
283
296
|
WHEN 'update' THEN rp.can_update
|
|
284
297
|
WHEN 'delete' THEN rp.can_delete
|
|
285
298
|
END
|
|
299
|
+
) OR EXISTS (
|
|
300
|
+
-- Implied via a direct grant on any parent at system
|
|
301
|
+
-- scope. resource_dependencies edges are scope-agnostic
|
|
302
|
+
-- (declared per-(parent,child,action)); the SCOPE of the
|
|
303
|
+
-- inheritance is governed by where the parent grant
|
|
304
|
+
-- lives.
|
|
305
|
+
SELECT 1
|
|
306
|
+
FROM rbac.resource_dependencies d
|
|
307
|
+
JOIN rbac.user_system_roles usr ON true
|
|
308
|
+
JOIN rbac.role_permissions rp ON rp.role_id = usr.role_id
|
|
309
|
+
WHERE d.child_resource = p_resource
|
|
310
|
+
AND d.action = p_action
|
|
311
|
+
AND usr.user_id = p_user_id
|
|
312
|
+
AND rp.resource = d.parent_resource
|
|
313
|
+
AND CASE p_action
|
|
314
|
+
WHEN 'read' THEN rp.can_read
|
|
315
|
+
WHEN 'write' THEN rp.can_write
|
|
316
|
+
WHEN 'update' THEN rp.can_update
|
|
317
|
+
WHEN 'delete' THEN rp.can_delete
|
|
318
|
+
END
|
|
286
319
|
);
|
|
287
320
|
END IF;
|
|
288
321
|
|
|
322
|
+
-- Company scope.
|
|
289
323
|
IF p_company_id IS NULL THEN
|
|
290
324
|
RETURN false;
|
|
291
325
|
END IF;
|
|
292
326
|
|
|
293
327
|
RETURN EXISTS (
|
|
328
|
+
-- Direct grant on the resource in this company.
|
|
294
329
|
SELECT 1
|
|
295
330
|
FROM rbac.user_company_roles ucr
|
|
296
331
|
JOIN rbac.role_permissions rp ON rp.role_id = ucr.role_id
|
|
@@ -303,6 +338,24 @@ BEGIN
|
|
|
303
338
|
WHEN 'update' THEN rp.can_update
|
|
304
339
|
WHEN 'delete' THEN rp.can_delete
|
|
305
340
|
END
|
|
341
|
+
) OR EXISTS (
|
|
342
|
+
-- Implied via a direct grant on any parent in the same
|
|
343
|
+
-- company.
|
|
344
|
+
SELECT 1
|
|
345
|
+
FROM rbac.resource_dependencies d
|
|
346
|
+
JOIN rbac.user_company_roles ucr ON true
|
|
347
|
+
JOIN rbac.role_permissions rp ON rp.role_id = ucr.role_id
|
|
348
|
+
WHERE d.child_resource = p_resource
|
|
349
|
+
AND d.action = p_action
|
|
350
|
+
AND ucr.user_id = p_user_id
|
|
351
|
+
AND ucr.company_id = p_company_id
|
|
352
|
+
AND rp.resource = d.parent_resource
|
|
353
|
+
AND CASE p_action
|
|
354
|
+
WHEN 'read' THEN rp.can_read
|
|
355
|
+
WHEN 'write' THEN rp.can_write
|
|
356
|
+
WHEN 'update' THEN rp.can_update
|
|
357
|
+
WHEN 'delete' THEN rp.can_delete
|
|
358
|
+
END
|
|
306
359
|
);
|
|
307
360
|
END $$;
|
|
308
361
|
|
|
@@ -310,6 +363,20 @@ END $$;
|
|
|
310
363
|
-- 7. Convenience: hydrate a user's full profile in one call
|
|
311
364
|
-- ─────────────────────────────────────────────────────────────────
|
|
312
365
|
|
|
366
|
+
-- 0.5.0+: `system_permissions` and `memberships[*].permissions`
|
|
367
|
+
-- now reflect EXPANDED grants — direct grants on the resource OR
|
|
368
|
+
-- direct grants on any parent listed in rbac.resource_dependencies.
|
|
369
|
+
-- This is what useCan() reads, so detail-page renders and RLS see
|
|
370
|
+
-- a consistent answer.
|
|
371
|
+
--
|
|
372
|
+
-- The direct-only maps (`system_direct_*`, `memberships[*].direct_*`)
|
|
373
|
+
-- continue to reflect ONLY direct grants, used by canAccessSection()
|
|
374
|
+
-- to gate top-level navigation and list pages.
|
|
375
|
+
--
|
|
376
|
+
-- No more `<action>_granted_via` lookups — every row on
|
|
377
|
+
-- rbac.role_permissions is direct now (the matrix transport
|
|
378
|
+
-- stopped cascading in 0.5.0). Implied access is computed by
|
|
379
|
+
-- joining role_permissions to resource_dependencies at query time.
|
|
313
380
|
CREATE OR REPLACE FUNCTION rbac.user_profile(p_user_id uuid)
|
|
314
381
|
RETURNS jsonb
|
|
315
382
|
LANGUAGE sql
|
|
@@ -324,25 +391,45 @@ AS $$
|
|
|
324
391
|
JOIN rbac.roles r ON r.id = usr.role_id
|
|
325
392
|
WHERE usr.user_id = p_user_id
|
|
326
393
|
),
|
|
327
|
-
--
|
|
328
|
-
|
|
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".
|
|
331
|
-
system_perms AS (
|
|
394
|
+
-- Direct grants at system scope, aggregated per resource.
|
|
395
|
+
system_direct AS (
|
|
332
396
|
SELECT rp.resource,
|
|
333
397
|
bool_or(rp.can_read) AS can_read,
|
|
334
398
|
bool_or(rp.can_write) AS can_write,
|
|
335
399
|
bool_or(rp.can_update) AS can_update,
|
|
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
|
|
400
|
+
bool_or(rp.can_delete) AS can_delete
|
|
341
401
|
FROM rbac.user_system_roles usr
|
|
342
402
|
JOIN rbac.role_permissions rp ON rp.role_id = usr.role_id
|
|
343
403
|
WHERE usr.user_id = p_user_id
|
|
344
404
|
GROUP BY rp.resource
|
|
345
405
|
),
|
|
406
|
+
-- For every direct grant on a parent, fan out to children via
|
|
407
|
+
-- rbac.resource_dependencies and AND-mask by the cascading action.
|
|
408
|
+
system_implied AS (
|
|
409
|
+
SELECT d.child_resource AS resource,
|
|
410
|
+
bool_or(d.action = 'read' AND sd.can_read) AS can_read,
|
|
411
|
+
bool_or(d.action = 'write' AND sd.can_write) AS can_write,
|
|
412
|
+
bool_or(d.action = 'update' AND sd.can_update) AS can_update,
|
|
413
|
+
bool_or(d.action = 'delete' AND sd.can_delete) AS can_delete
|
|
414
|
+
FROM system_direct sd
|
|
415
|
+
JOIN rbac.resource_dependencies d ON d.parent_resource = sd.resource
|
|
416
|
+
GROUP BY d.child_resource
|
|
417
|
+
),
|
|
418
|
+
-- Expanded = direct UNION implied. bool_or merges duplicate
|
|
419
|
+
-- (resource, action) hits across the two sources.
|
|
420
|
+
system_expanded AS (
|
|
421
|
+
SELECT resource,
|
|
422
|
+
bool_or(can_read) AS can_read,
|
|
423
|
+
bool_or(can_write) AS can_write,
|
|
424
|
+
bool_or(can_update) AS can_update,
|
|
425
|
+
bool_or(can_delete) AS can_delete
|
|
426
|
+
FROM (
|
|
427
|
+
SELECT * FROM system_direct
|
|
428
|
+
UNION ALL
|
|
429
|
+
SELECT * FROM system_implied
|
|
430
|
+
) u
|
|
431
|
+
GROUP BY resource
|
|
432
|
+
),
|
|
346
433
|
memberships AS (
|
|
347
434
|
SELECT c.id AS company_id,
|
|
348
435
|
c.name AS company_name,
|
|
@@ -357,21 +444,59 @@ AS $$
|
|
|
357
444
|
WHERE ucr.user_id = p_user_id
|
|
358
445
|
GROUP BY c.id, c.name, c.slug
|
|
359
446
|
),
|
|
360
|
-
|
|
447
|
+
-- Direct grants at company scope.
|
|
448
|
+
company_direct AS (
|
|
361
449
|
SELECT ucr.company_id,
|
|
362
450
|
rp.resource,
|
|
363
451
|
bool_or(rp.can_read) AS can_read,
|
|
364
452
|
bool_or(rp.can_write) AS can_write,
|
|
365
453
|
bool_or(rp.can_update) AS can_update,
|
|
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
|
|
454
|
+
bool_or(rp.can_delete) AS can_delete
|
|
371
455
|
FROM rbac.user_company_roles ucr
|
|
372
456
|
JOIN rbac.role_permissions rp ON rp.role_id = ucr.role_id
|
|
373
457
|
WHERE ucr.user_id = p_user_id
|
|
374
458
|
GROUP BY ucr.company_id, rp.resource
|
|
459
|
+
),
|
|
460
|
+
company_implied AS (
|
|
461
|
+
SELECT cd.company_id,
|
|
462
|
+
d.child_resource AS resource,
|
|
463
|
+
bool_or(d.action = 'read' AND cd.can_read) AS can_read,
|
|
464
|
+
bool_or(d.action = 'write' AND cd.can_write) AS can_write,
|
|
465
|
+
bool_or(d.action = 'update' AND cd.can_update) AS can_update,
|
|
466
|
+
bool_or(d.action = 'delete' AND cd.can_delete) AS can_delete
|
|
467
|
+
FROM company_direct cd
|
|
468
|
+
JOIN rbac.resource_dependencies d ON d.parent_resource = cd.resource
|
|
469
|
+
GROUP BY cd.company_id, d.child_resource
|
|
470
|
+
),
|
|
471
|
+
company_expanded AS (
|
|
472
|
+
SELECT company_id, resource,
|
|
473
|
+
bool_or(can_read) AS can_read,
|
|
474
|
+
bool_or(can_write) AS can_write,
|
|
475
|
+
bool_or(can_update) AS can_update,
|
|
476
|
+
bool_or(can_delete) AS can_delete
|
|
477
|
+
FROM (
|
|
478
|
+
SELECT * FROM company_direct
|
|
479
|
+
UNION ALL
|
|
480
|
+
SELECT * FROM company_implied
|
|
481
|
+
) u
|
|
482
|
+
GROUP BY company_id, resource
|
|
483
|
+
),
|
|
484
|
+
-- Direct-only stays the source of truth for canAccessSection().
|
|
485
|
+
system_perms AS (
|
|
486
|
+
SELECT resource,
|
|
487
|
+
can_read AS direct_read,
|
|
488
|
+
can_write AS direct_write,
|
|
489
|
+
can_update AS direct_update,
|
|
490
|
+
can_delete AS direct_delete
|
|
491
|
+
FROM system_direct
|
|
492
|
+
),
|
|
493
|
+
company_perms AS (
|
|
494
|
+
SELECT company_id, resource,
|
|
495
|
+
can_read AS direct_read,
|
|
496
|
+
can_write AS direct_write,
|
|
497
|
+
can_update AS direct_update,
|
|
498
|
+
can_delete AS direct_delete
|
|
499
|
+
FROM company_direct
|
|
375
500
|
)
|
|
376
501
|
SELECT jsonb_build_object(
|
|
377
502
|
'user_id', p_user_id,
|
|
@@ -380,15 +505,17 @@ AS $$
|
|
|
380
505
|
(SELECT jsonb_agg(jsonb_build_object('id', id, 'name', name)) FROM system_roles),
|
|
381
506
|
'[]'::jsonb
|
|
382
507
|
),
|
|
508
|
+
-- 0.5.0+: system_permissions is EXPANDED — what useCan() reads.
|
|
509
|
+
-- Includes direct grants + implied grants from any parent in
|
|
510
|
+
-- rbac.resource_dependencies. canAccessSection() reads the
|
|
511
|
+
-- direct_* maps below for the list-page distinction.
|
|
383
512
|
'system_permissions', coalesce(
|
|
384
513
|
(SELECT jsonb_object_agg(resource, jsonb_build_object(
|
|
385
514
|
'read', can_read, 'write', can_write,
|
|
386
515
|
'update', can_update, 'delete', can_delete
|
|
387
|
-
)) FROM
|
|
516
|
+
)) FROM system_expanded),
|
|
388
517
|
'{}'::jsonb
|
|
389
518
|
),
|
|
390
|
-
-- 0.4.0+. Per-action direct-grant maps. Drives canAccessSection()
|
|
391
|
-
-- on the client side for top-level nav / list-page gating.
|
|
392
519
|
'system_direct_reads', coalesce(
|
|
393
520
|
(SELECT jsonb_object_agg(resource, true)
|
|
394
521
|
FROM system_perms WHERE direct_read), '{}'::jsonb
|
|
@@ -411,13 +538,14 @@ AS $$
|
|
|
411
538
|
'company_name', m.company_name,
|
|
412
539
|
'company_slug', m.company_slug,
|
|
413
540
|
'roles', m.roles,
|
|
541
|
+
-- 0.5.0+: expanded grants for this company.
|
|
414
542
|
'permissions', coalesce(
|
|
415
|
-
(SELECT jsonb_object_agg(
|
|
416
|
-
'read',
|
|
417
|
-
'update',
|
|
543
|
+
(SELECT jsonb_object_agg(ce.resource, jsonb_build_object(
|
|
544
|
+
'read', ce.can_read, 'write', ce.can_write,
|
|
545
|
+
'update', ce.can_update, 'delete', ce.can_delete
|
|
418
546
|
))
|
|
419
|
-
FROM
|
|
420
|
-
WHERE
|
|
547
|
+
FROM company_expanded ce
|
|
548
|
+
WHERE ce.company_id = m.company_id),
|
|
421
549
|
'{}'::jsonb
|
|
422
550
|
),
|
|
423
551
|
'direct_reads', coalesce(
|
|
@@ -450,22 +578,14 @@ AS $$
|
|
|
450
578
|
);
|
|
451
579
|
$$;
|
|
452
580
|
|
|
453
|
-
-- 0.
|
|
454
|
-
--
|
|
455
|
-
--
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
$$;
|
|
581
|
+
-- 0.5.0: rbac.list_implied_grants removed. The matrix UI no longer
|
|
582
|
+
-- materialises implied rows on toggle (the resolver expands at
|
|
583
|
+
-- query time instead). DROP for clean upgrade from 0.4.x.
|
|
584
|
+
DROP FUNCTION IF EXISTS rbac.list_implied_grants(text, text);
|
|
585
|
+
-- 0.5.0: rbac.backfill_implied_grants removed. There are no implied
|
|
586
|
+
-- rows to backfill any more — the resolver computes implied access
|
|
587
|
+
-- on every lookup against rbac.resource_dependencies.
|
|
588
|
+
DROP FUNCTION IF EXISTS rbac.backfill_implied_grants();
|
|
469
589
|
|
|
470
590
|
-- 0.4.0+. Atomic replace-all for the dependency graph. The admin
|
|
471
591
|
-- transport calls this on every `syncResources()` so removed
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/define.ts","../src/fetchers.ts"],"sourcesContent":["/**\n * Typed factory — turns a const-asserted resource registry into a\n * set of hooks/components whose `resource` arg is constrained to\n * the registered names. Typos become TypeScript errors instead of\n * silent runtime `false`.\n *\n * @example\n * // src/auth/resources.ts\n * import { defineAuthRbac } from \"snipe-auth-rbac/react\";\n *\n * export const RESOURCES = [\n * { resource: \"properties\", scope: \"company\", label: \"Liegenschaften\", group: \"Stammdaten\" },\n * { resource: \"payments\", scope: \"company\", label: \"Zahlungen\", group: \"Finanzen\" },\n * { resource: \"system_audit\", scope: \"system\", label: \"Audit-Log\", group: \"Plattform\" },\n * ] as const;\n *\n * export const { useCan, Can, RequirePermission } = defineAuthRbac(RESOURCES);\n *\n * // ----- elsewhere -----\n * useCan(\"properties\", \"update\"); // ✓\n * useCan(\"paymetns\", \"update\"); // ✗ TS error: not assignable to type \"properties\" | \"payments\" | \"system_audit\"\n */\n\nimport type { ComponentType, ReactNode } from \"react\";\n\nimport type {\n Action,\n DependencyEdge,\n ResourceDescriptor,\n ResourceRegistry,\n} from \"./types.js\";\n\n/**\n * Thrown by `defineAuthRbac` when the registry contains a cycle in\n * its `dependsOn` graph. The cycle path is included in the message,\n * named in registry order.\n */\nexport class RbacRegistryError extends Error {\n constructor(message: string) {\n super(`auth-rbac: ${message}`);\n this.name = \"RbacRegistryError\";\n }\n}\n\n/**\n * Walks the `dependsOn` graph with three-colour DFS and throws on\n * the first back-edge. Runs once at module-init time, so misconfigured\n * registries fail loud at app boot rather than corrupting the matrix\n * at first toggle.\n */\nfunction detectDependencyCycles(registry: ResourceRegistry): void {\n const WHITE = 0;\n const GREY = 1;\n const BLACK = 2;\n const colour = new Map<string, number>();\n const byName = new Map<string, ResourceDescriptor>();\n for (const r of registry) {\n byName.set(r.resource, r);\n colour.set(r.resource, WHITE);\n }\n const stack: string[] = [];\n\n const edgeTarget = (edge: string | DependencyEdge): string =>\n typeof edge === \"string\" ? edge : edge.resource;\n\n function visit(name: string): void {\n const state = colour.get(name);\n if (state === BLACK) {\n return;\n }\n if (state === GREY) {\n const cycleStart = stack.indexOf(name);\n const cycle = [...stack.slice(cycleStart), name].join(\" → \");\n throw new RbacRegistryError(\n `dependency cycle detected in resource registry: ${cycle}`,\n );\n }\n if (state === undefined) {\n // Dangling edge — referenced resource not in registry. Treat\n // as a registration error so adopters fix the typo at boot.\n throw new RbacRegistryError(\n `dependsOn target '${name}' is not a registered resource ` +\n `(referenced by '${stack[stack.length - 1] ?? \"<root>\"}')`,\n );\n }\n colour.set(name, GREY);\n stack.push(name);\n const descriptor = byName.get(name);\n const edges = descriptor?.dependsOn ?? [];\n for (const edge of edges) {\n visit(edgeTarget(edge));\n }\n stack.pop();\n colour.set(name, BLACK);\n }\n\n for (const r of registry) {\n visit(r.resource);\n }\n}\n\n/**\n * Drop-in replacement signatures for the three guards, with a\n * narrowed `resource` arg.\n */\nexport interface TypedGuards<R extends string> {\n useCan: (\n resource: R,\n action: Action,\n options?: { companyId?: string | null },\n ) => boolean;\n /**\n * `useCan` for sidebar / list-page access: returns true only when\n * the user holds the action on the resource as a **direct** grant\n * (no `<action>_granted_via`). Implied rows answer false here.\n *\n * `action` defaults to `'read'` because the canonical use is\n * top-level-nav gating. Available since 0.4.0.\n */\n useCanAccessSection: (\n resource: R,\n action?: Action,\n options?: { companyId?: string | null },\n ) => boolean;\n Can: ComponentType<{\n resource: R;\n action: Action;\n companyId?: string | null;\n children: ReactNode;\n fallback?: ReactNode;\n }>;\n RequirePermission: ComponentType<{\n resource: R;\n action: Action;\n companyId?: string | null;\n loadingFallback?: ReactNode;\n deniedFallback?: ReactNode;\n children?: ReactNode;\n }>;\n /** The const-asserted registry, re-exported so call-sites can iterate. */\n resources: ResourceRegistry;\n /** All registered resource names as a union — handy for typing\n * application-side data structures. */\n resourceNames: ReadonlyArray<R>;\n}\n\n/**\n * Factory. Run once at module-init time in your host project; the\n * returned hooks/components are referentially stable.\n */\nexport function defineAuthRbac<\n const Reg extends ReadonlyArray<ResourceDescriptor>,\n>(\n resources: Reg,\n // The runtime guards live in the React entry; we accept them\n // here as plain refs so this module stays React-free at the type\n // level. The `react` entry calls this factory passing its own\n // exports so adopters never see the wiring.\n runtime: {\n useCan: TypedGuards<string>[\"useCan\"];\n useCanAccessSection: TypedGuards<string>[\"useCanAccessSection\"];\n Can: TypedGuards<string>[\"Can\"];\n RequirePermission: TypedGuards<string>[\"RequirePermission\"];\n },\n): TypedGuards<Reg[number][\"resource\"]> {\n // Cycle detection runs once at module-init time so misconfigured\n // registries fail at app boot, not on the first matrix toggle.\n detectDependencyCycles(resources);\n\n type R = Reg[number][\"resource\"];\n return {\n useCan: runtime.useCan as TypedGuards<R>[\"useCan\"],\n useCanAccessSection: runtime.useCanAccessSection as TypedGuards<R>[\"useCanAccessSection\"],\n Can: runtime.Can as TypedGuards<R>[\"Can\"],\n RequirePermission: runtime.RequirePermission as TypedGuards<R>[\"RequirePermission\"],\n resources,\n resourceNames: resources.map((r) => r.resource) as ReadonlyArray<R>,\n };\n}\n","/**\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":"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","names":[]}
|