snipe-auth-rbac 0.4.0 → 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/{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/dist/chunk-5UAIIOKT.js.map +0 -1
|
@@ -16,6 +16,7 @@ function detectDependencyCycles(registry) {
|
|
|
16
16
|
colour.set(r.resource, WHITE);
|
|
17
17
|
}
|
|
18
18
|
const stack = [];
|
|
19
|
+
const cyclesFound = /* @__PURE__ */ new Set();
|
|
19
20
|
const edgeTarget = (edge) => typeof edge === "string" ? edge : edge.resource;
|
|
20
21
|
function visit(name) {
|
|
21
22
|
const state = colour.get(name);
|
|
@@ -25,9 +26,8 @@ function detectDependencyCycles(registry) {
|
|
|
25
26
|
if (state === GREY) {
|
|
26
27
|
const cycleStart = stack.indexOf(name);
|
|
27
28
|
const cycle = [...stack.slice(cycleStart), name].join(" \u2192 ");
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
);
|
|
29
|
+
cyclesFound.add(cycle);
|
|
30
|
+
return;
|
|
31
31
|
}
|
|
32
32
|
if (state === void 0) {
|
|
33
33
|
throw new RbacRegistryError(
|
|
@@ -47,6 +47,11 @@ function detectDependencyCycles(registry) {
|
|
|
47
47
|
for (const r of registry) {
|
|
48
48
|
visit(r.resource);
|
|
49
49
|
}
|
|
50
|
+
if (cyclesFound.size > 0) {
|
|
51
|
+
console.warn(
|
|
52
|
+
`[auth-rbac] dependency cycle(s) detected in resource registry. Cycles are runtime-safe \u2014 the matrix UI's cascade only flips cells on and skips already-on cells \u2014 but they may indicate unintended edges. Found: ${Array.from(cyclesFound).join("; ")}`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
50
55
|
}
|
|
51
56
|
function defineAuthRbac(resources, runtime) {
|
|
52
57
|
detectDependencyCycles(resources);
|
|
@@ -130,4 +135,4 @@ export {
|
|
|
130
135
|
detectRbacSchema,
|
|
131
136
|
createHttpFetcher
|
|
132
137
|
};
|
|
133
|
-
//# sourceMappingURL=chunk-
|
|
138
|
+
//# sourceMappingURL=chunk-WL4QZ7HO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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. Throws on\n * **dangling** edges (an edge points at a resource that isn't in\n * the registry — almost always a typo). Cycles are *not* fatal:\n * the matrix UI's cascade is cycle-safe by construction (it only\n * flips cells on and skips already-on cells), and real-world\n * dependency graphs in property-management / multi-entity SaaS are\n * naturally cyclic — properties and units mutually need each\n * other's read to render either detail view. We emit a single\n * console warning per registry build so unintended cycles are\n * still visible during development, without breaking adopters whose\n * graphs are cyclic by design.\n *\n * Pre-0.4.1: cycles threw RbacRegistryError. The throw was wrong —\n * the cascade has always been safe under cycles. The change in\n * 0.4.1 keeps the diagnostic but downgrades it to a warning.\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 const cyclesFound = new Set<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 // Cycle. Record the path for the warning but keep walking so\n // we don't mask further dangling-edge errors deeper in the\n // graph.\n const cycleStart = stack.indexOf(name);\n const cycle = [...stack.slice(cycleStart), name].join(\" → \");\n cyclesFound.add(cycle);\n return;\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 if (cyclesFound.size > 0) {\n // eslint-disable-next-line no-console\n console.warn(\n `[auth-rbac] dependency cycle(s) detected in resource registry. ` +\n `Cycles are runtime-safe — the matrix UI's cascade only flips ` +\n `cells on and skips already-on cells — but they may indicate ` +\n `unintended edges. Found: ${Array.from(cyclesFound).join(\"; \")}`,\n );\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":[]}
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/define.ts","../src/fetchers.ts"],"sourcesContent":["/**\n * Public entry — transport-agnostic. Use this in non-React code\n * (Node, scripts, edge workers). React hosts should import from\n * `snipe-auth-rbac/react`.\n */\n\nexport type {\n Action,\n AuthRbacFetcher,\n CompanyMembership,\n DependencyEdge,\n DirectGrantMap,\n FrontendConfig,\n PermissionGrid,\n PermissionMap,\n ResourceDescriptor,\n ResourceRegistry,\n ResourceScope,\n RoleSummary,\n UserProfile,\n} from \"./types.js\";\n\nexport {\n buildPermissionResolver,\n groupResources,\n type AuthRbacClient,\n type CanOptions,\n type ClientOptions,\n} from \"./client.js\";\n\nexport { RbacRegistryError } from \"./define.js\";\n\nexport {\n createSupabaseFetcher,\n createHttpFetcher,\n detectRbacSchema,\n} from \"./fetchers.js\";\n","/**\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 /**\n * Direct-grant lookup: returns true only if the user has the\n * action granted on the resource as a direct admin grant —\n * `<action>_granted_via IS NULL` in `rbac.role_permissions`.\n * Implied rows (granted as a side-effect of a parent resource's\n * `dependsOn` cascade) return false here.\n *\n * Use for top-level navigation / list-page gating: a Verwalter\n * with only `leases:read` direct gets the Leases sidebar item but\n * not Tenants / Units / Properties, even though `can(...)` returns\n * true for those (because the implied rows let the lease detail\n * page render its joined data).\n *\n * Available since 0.4.0. For older SQL that doesn't return\n * `direct_*` maps, every cell answers false — equivalent to\n * \"no direct grants known\". Adopters running pre-0.4.0 SQL should\n * keep using `can(...)`.\n */\n const canAccessSection = (\n resource: string,\n action: Action = \"read\",\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 return false;\n }\n if (scope === \"system\") {\n return readDirect(profile, action, resource);\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 readDirectMembership(membership, action, resource);\n };\n\n return {\n can,\n canAccessSection,\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 readDirect(\n profile: UserProfile,\n action: Action,\n resource: string,\n): boolean {\n const map =\n action === \"read\"\n ? profile.system_direct_reads\n : action === \"write\"\n ? profile.system_direct_writes\n : action === \"update\"\n ? profile.system_direct_updates\n : profile.system_direct_deletes;\n return map?.[resource] === true;\n}\n\nfunction readDirectMembership(\n membership: { direct_reads?: Readonly<Record<string, boolean>>;\n direct_writes?: Readonly<Record<string, boolean>>;\n direct_updates?: Readonly<Record<string, boolean>>;\n direct_deletes?: Readonly<Record<string, boolean>>; },\n action: Action,\n resource: string,\n): boolean {\n const map =\n action === \"read\"\n ? membership.direct_reads\n : action === \"write\"\n ? membership.direct_writes\n : action === \"update\"\n ? membership.direct_updates\n : membership.direct_deletes;\n return map?.[resource] === true;\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","/**\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":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/define.ts","../src/fetchers.ts"],"sourcesContent":["/**\n * Public entry — transport-agnostic. Use this in non-React code\n * (Node, scripts, edge workers). React hosts should import from\n * `snipe-auth-rbac/react`.\n */\n\nexport type {\n Action,\n AuthRbacFetcher,\n CompanyMembership,\n DependencyEdge,\n DirectGrantMap,\n FrontendConfig,\n PermissionGrid,\n PermissionMap,\n ResourceDescriptor,\n ResourceRegistry,\n ResourceScope,\n RoleSummary,\n UserProfile,\n} from \"./types.js\";\n\nexport {\n buildPermissionResolver,\n groupResources,\n type AuthRbacClient,\n type CanOptions,\n type ClientOptions,\n} from \"./client.js\";\n\nexport { RbacRegistryError } from \"./define.js\";\n\nexport {\n createSupabaseFetcher,\n createHttpFetcher,\n detectRbacSchema,\n} from \"./fetchers.js\";\n","/**\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 /**\n * Direct-grant lookup: returns true only if the user has the\n * action granted on the resource as a direct admin grant —\n * `<action>_granted_via IS NULL` in `rbac.role_permissions`.\n * Implied rows (granted as a side-effect of a parent resource's\n * `dependsOn` cascade) return false here.\n *\n * Use for top-level navigation / list-page gating: a Verwalter\n * with only `leases:read` direct gets the Leases sidebar item but\n * not Tenants / Units / Properties, even though `can(...)` returns\n * true for those (because the implied rows let the lease detail\n * page render its joined data).\n *\n * Available since 0.4.0. For older SQL that doesn't return\n * `direct_*` maps, every cell answers false — equivalent to\n * \"no direct grants known\". Adopters running pre-0.4.0 SQL should\n * keep using `can(...)`.\n */\n const canAccessSection = (\n resource: string,\n action: Action = \"read\",\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 return false;\n }\n if (scope === \"system\") {\n return readDirect(profile, action, resource);\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 readDirectMembership(membership, action, resource);\n };\n\n return {\n can,\n canAccessSection,\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 readDirect(\n profile: UserProfile,\n action: Action,\n resource: string,\n): boolean {\n const map =\n action === \"read\"\n ? profile.system_direct_reads\n : action === \"write\"\n ? profile.system_direct_writes\n : action === \"update\"\n ? profile.system_direct_updates\n : profile.system_direct_deletes;\n return map?.[resource] === true;\n}\n\nfunction readDirectMembership(\n membership: { direct_reads?: Readonly<Record<string, boolean>>;\n direct_writes?: Readonly<Record<string, boolean>>;\n direct_updates?: Readonly<Record<string, boolean>>;\n direct_deletes?: Readonly<Record<string, boolean>>; },\n action: Action,\n resource: string,\n): boolean {\n const map =\n action === \"read\"\n ? membership.direct_reads\n : action === \"write\"\n ? membership.direct_writes\n : action === \"update\"\n ? membership.direct_updates\n : membership.direct_deletes;\n return map?.[resource] === true;\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","/**\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. Throws on\n * **dangling** edges (an edge points at a resource that isn't in\n * the registry — almost always a typo). Cycles are *not* fatal:\n * the matrix UI's cascade is cycle-safe by construction (it only\n * flips cells on and skips already-on cells), and real-world\n * dependency graphs in property-management / multi-entity SaaS are\n * naturally cyclic — properties and units mutually need each\n * other's read to render either detail view. We emit a single\n * console warning per registry build so unintended cycles are\n * still visible during development, without breaking adopters whose\n * graphs are cyclic by design.\n *\n * Pre-0.4.1: cycles threw RbacRegistryError. The throw was wrong —\n * the cascade has always been safe under cycles. The change in\n * 0.4.1 keeps the diagnostic but downgrades it to a warning.\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 const cyclesFound = new Set<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 // Cycle. Record the path for the warning but keep walking so\n // we don't mask further dangling-edge errors deeper in the\n // graph.\n const cycleStart = stack.indexOf(name);\n const cycle = [...stack.slice(cycleStart), name].join(\" → \");\n cyclesFound.add(cycle);\n return;\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 if (cyclesFound.size > 0) {\n // eslint-disable-next-line no-console\n console.warn(\n `[auth-rbac] dependency cycle(s) detected in resource registry. ` +\n `Cycles are runtime-safe — the matrix UI's cascade only flips ` +\n `cells on and skips already-on cells — but they may indicate ` +\n `unintended edges. Found: ${Array.from(cyclesFound).join(\"; \")}`,\n );\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":[]}
|
package/dist/index.js
CHANGED
package/dist/react/index.cjs
CHANGED
|
@@ -324,6 +324,7 @@ function detectDependencyCycles(registry) {
|
|
|
324
324
|
colour.set(r.resource, WHITE);
|
|
325
325
|
}
|
|
326
326
|
const stack = [];
|
|
327
|
+
const cyclesFound = /* @__PURE__ */ new Set();
|
|
327
328
|
const edgeTarget = (edge) => typeof edge === "string" ? edge : edge.resource;
|
|
328
329
|
function visit(name) {
|
|
329
330
|
const state = colour.get(name);
|
|
@@ -333,9 +334,8 @@ function detectDependencyCycles(registry) {
|
|
|
333
334
|
if (state === GREY) {
|
|
334
335
|
const cycleStart = stack.indexOf(name);
|
|
335
336
|
const cycle = [...stack.slice(cycleStart), name].join(" \u2192 ");
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
);
|
|
337
|
+
cyclesFound.add(cycle);
|
|
338
|
+
return;
|
|
339
339
|
}
|
|
340
340
|
if (state === void 0) {
|
|
341
341
|
throw new RbacRegistryError(
|
|
@@ -355,6 +355,11 @@ function detectDependencyCycles(registry) {
|
|
|
355
355
|
for (const r of registry) {
|
|
356
356
|
visit(r.resource);
|
|
357
357
|
}
|
|
358
|
+
if (cyclesFound.size > 0) {
|
|
359
|
+
console.warn(
|
|
360
|
+
`[auth-rbac] dependency cycle(s) detected in resource registry. Cycles are runtime-safe \u2014 the matrix UI's cascade only flips cells on and skips already-on cells \u2014 but they may indicate unintended edges. Found: ${Array.from(cyclesFound).join("; ")}`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
358
363
|
}
|
|
359
364
|
function defineAuthRbac(resources, runtime) {
|
|
360
365
|
detectDependencyCycles(resources);
|
package/dist/react/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/index.ts","../../src/react/AuthRbacProvider.tsx","../../src/client.ts","../../src/react/useCan.ts","../../src/react/useCanAccessSection.ts","../../src/react/Can.tsx","../../src/react/RequirePermission.tsx","../../src/react/useActiveCompany.ts","../../src/react/useFrontendConfig.ts","../../src/define.ts","../../src/fetchers.ts"],"sourcesContent":["/**\n * React + Next.js entry. Import this in browser code:\n *\n * import { AuthRbacProvider, useCan } from \"snipe-auth-rbac/react\";\n *\n * The non-React entry (`snipe-auth-rbac`) re-exports types and the\n * pure resolver, suitable for Node, edge workers, and tests.\n */\n\nexport {\n AuthRbacProvider,\n useAuthRbac,\n type AuthRbacProviderProps,\n} from \"./AuthRbacProvider.js\";\n\nexport { useCan } from \"./useCan.js\";\nexport { useCanAccessSection } from \"./useCanAccessSection.js\";\nexport { Can, type CanProps } from \"./Can.js\";\nexport {\n RequirePermission,\n type RequirePermissionProps,\n} from \"./RequirePermission.js\";\nexport { useActiveCompany, type ActiveCompany } from \"./useActiveCompany.js\";\nexport { useFrontendConfig } from \"./useFrontendConfig.js\";\n\n// Re-exports for convenience so consumers don't need two imports.\nexport type {\n Action,\n AuthRbacFetcher,\n CompanyMembership,\n DependencyEdge,\n DirectGrantMap,\n FrontendConfig,\n PermissionGrid,\n PermissionMap,\n ResourceDescriptor,\n ResourceRegistry,\n ResourceScope,\n RoleSummary,\n UserProfile,\n} from \"../types.js\";\n\nexport { RbacRegistryError } from \"../define.js\";\n\nexport {\n createSupabaseFetcher,\n createHttpFetcher,\n detectRbacSchema,\n} from \"../fetchers.js\";\n\nimport { defineAuthRbac as _defineAuthRbac } from \"../define.js\";\nimport { Can } from \"./Can.js\";\nimport { RequirePermission } from \"./RequirePermission.js\";\nimport { useCan } from \"./useCan.js\";\nimport { useCanAccessSection } from \"./useCanAccessSection.js\";\n\nimport type { ResourceDescriptor } from \"../types.js\";\nimport type { TypedGuards } from \"../define.js\";\n\n/**\n * Typed factory — pass a const-asserted resource registry and get\n * back guards whose `resource` arg is constrained to the registered\n * names. Recommended at the top of every host project.\n *\n * See ../define.ts for the full doc + example.\n */\nexport function defineAuthRbac<\n const Reg extends ReadonlyArray<ResourceDescriptor>,\n>(resources: Reg): TypedGuards<Reg[number][\"resource\"]> {\n return _defineAuthRbac(resources, {\n useCan,\n useCanAccessSection,\n Can,\n RequirePermission,\n });\n}\n\nexport type { TypedGuards } from \"../define.js\";\n","import {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n type ReactNode,\n} from \"react\";\n\nimport {\n buildPermissionResolver,\n type AuthRbacClient,\n} from \"../client.js\";\nimport type {\n AuthRbacFetcher,\n PermissionMap,\n ResourceRegistry,\n UserProfile,\n} from \"../types.js\";\n\ninterface AuthRbacContextValue {\n /**\n * `null` means we haven't hydrated yet. Components should treat\n * the not-loaded state as \"no permissions\" (fail-closed).\n */\n profile: UserProfile | null;\n loading: boolean;\n error: Error | null;\n resources: ResourceRegistry;\n activeCompanyId: string | null;\n setActiveCompany: (id: string | null) => void;\n refresh: () => Promise<void>;\n resolver: AuthRbacClient;\n}\n\nconst AuthRbacContext = createContext<AuthRbacContextValue | null>(null);\n\nexport interface AuthRbacProviderProps {\n fetcher: AuthRbacFetcher;\n resources: ResourceRegistry;\n /**\n * Initial active company. Common patterns:\n * - read from URL query/path\n * - read from localStorage\n * - omit and let the user pick from the switcher\n */\n initialCompanyId?: string | null;\n /**\n * Persistence hook. Called every time the active company changes.\n * Default: writes to `localStorage` under\n * `auth-rbac:active-company`. Pass `false` to disable.\n */\n persistActiveCompany?:\n | ((id: string | null) => void)\n | false;\n children: ReactNode;\n}\n\nconst STORAGE_KEY = \"auth-rbac:active-company\";\n\nconst defaultPersist = (id: string | null) => {\n if (typeof window === \"undefined\") {\n return;\n }\n try {\n if (id == null) {\n window.localStorage.removeItem(STORAGE_KEY);\n } else {\n window.localStorage.setItem(STORAGE_KEY, id);\n }\n } catch {\n // localStorage may be unavailable (private browsing, SSR) —\n // fall back to in-memory only.\n }\n};\n\nconst readPersisted = (): string | null => {\n if (typeof window === \"undefined\") {\n return null;\n }\n try {\n return window.localStorage.getItem(STORAGE_KEY);\n } catch {\n return null;\n }\n};\n\nexport function AuthRbacProvider(props: AuthRbacProviderProps) {\n const { fetcher, resources, initialCompanyId, persistActiveCompany } = props;\n\n const [profile, setProfile] = useState<UserProfile | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const [activeCompanyId, setActiveCompanyState] = useState<string | null>(\n initialCompanyId ?? readPersisted(),\n );\n\n const persist = useMemo(() => {\n if (persistActiveCompany === false) {\n return () => {};\n }\n return persistActiveCompany ?? defaultPersist;\n }, [persistActiveCompany]);\n\n const setActiveCompany = useCallback(\n (id: string | null) => {\n setActiveCompanyState(id);\n persist(id);\n },\n [persist],\n );\n\n const refresh = useCallback(async () => {\n setLoading(true);\n setError(null);\n try {\n const next = await fetcher.fetchProfile();\n setProfile(next);\n // If the persisted company isn't a membership, fall back to\n // the first one (or null for users with no memberships).\n const stillMember =\n activeCompanyId != null &&\n next.memberships.some((m) => m.company_id === activeCompanyId);\n if (!stillMember) {\n const fallback =\n next.memberships[0]?.company_id ?? null;\n setActiveCompanyState(fallback);\n persist(fallback);\n }\n } catch (e) {\n setError(e instanceof Error ? e : new Error(String(e)));\n } finally {\n setLoading(false);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [fetcher]);\n\n useEffect(() => {\n void refresh();\n }, [refresh]);\n\n const resolver = useMemo<AuthRbacClient>(() => {\n if (profile == null) {\n // Empty resolver until the profile lands. Always returns\n // false → guards fall through to the unauthenticated branch.\n return {\n can: () => false,\n canAccessSection: () => false,\n activePermissions: () => ({}) as PermissionMap,\n systemPermissions: () => ({}) as PermissionMap,\n };\n }\n return buildPermissionResolver(resources, profile, activeCompanyId);\n }, [profile, resources, activeCompanyId]);\n\n const value = useMemo<AuthRbacContextValue>(\n () => ({\n profile,\n loading,\n error,\n resources,\n activeCompanyId,\n setActiveCompany,\n refresh,\n resolver,\n }),\n [\n profile,\n loading,\n error,\n resources,\n activeCompanyId,\n setActiveCompany,\n refresh,\n resolver,\n ],\n );\n\n return (\n <AuthRbacContext.Provider value={value}>\n {props.children}\n </AuthRbacContext.Provider>\n );\n}\n\nexport function useAuthRbac(): AuthRbacContextValue {\n const ctx = useContext(AuthRbacContext);\n if (!ctx) {\n throw new Error(\n \"useAuthRbac must be used within an <AuthRbacProvider> — wrap your app at the root.\",\n );\n }\n return ctx;\n}\n","/**\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 /**\n * Direct-grant lookup: returns true only if the user has the\n * action granted on the resource as a direct admin grant —\n * `<action>_granted_via IS NULL` in `rbac.role_permissions`.\n * Implied rows (granted as a side-effect of a parent resource's\n * `dependsOn` cascade) return false here.\n *\n * Use for top-level navigation / list-page gating: a Verwalter\n * with only `leases:read` direct gets the Leases sidebar item but\n * not Tenants / Units / Properties, even though `can(...)` returns\n * true for those (because the implied rows let the lease detail\n * page render its joined data).\n *\n * Available since 0.4.0. For older SQL that doesn't return\n * `direct_*` maps, every cell answers false — equivalent to\n * \"no direct grants known\". Adopters running pre-0.4.0 SQL should\n * keep using `can(...)`.\n */\n const canAccessSection = (\n resource: string,\n action: Action = \"read\",\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 return false;\n }\n if (scope === \"system\") {\n return readDirect(profile, action, resource);\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 readDirectMembership(membership, action, resource);\n };\n\n return {\n can,\n canAccessSection,\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 readDirect(\n profile: UserProfile,\n action: Action,\n resource: string,\n): boolean {\n const map =\n action === \"read\"\n ? profile.system_direct_reads\n : action === \"write\"\n ? profile.system_direct_writes\n : action === \"update\"\n ? profile.system_direct_updates\n : profile.system_direct_deletes;\n return map?.[resource] === true;\n}\n\nfunction readDirectMembership(\n membership: { direct_reads?: Readonly<Record<string, boolean>>;\n direct_writes?: Readonly<Record<string, boolean>>;\n direct_updates?: Readonly<Record<string, boolean>>;\n direct_deletes?: Readonly<Record<string, boolean>>; },\n action: Action,\n resource: string,\n): boolean {\n const map =\n action === \"read\"\n ? membership.direct_reads\n : action === \"write\"\n ? membership.direct_writes\n : action === \"update\"\n ? membership.direct_updates\n : membership.direct_deletes;\n return map?.[resource] === true;\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","import type { Action } from \"../types.js\";\nimport type { CanOptions } from \"../client.js\";\n\nimport { useAuthRbac } from \"./AuthRbacProvider.js\";\n\n/**\n * Boolean permission check.\n *\n * @example\n * const canEdit = useCan(\"properties\", \"update\");\n * <Button disabled={!canEdit}>Speichern</Button>\n *\n * @example explicitly target a non-active company\n * const canRead = useCan(\"payments\", \"read\", { companyId: targetId });\n */\nexport function useCan(\n resource: string,\n action: Action,\n options?: CanOptions,\n): boolean {\n const { resolver } = useAuthRbac();\n return resolver.can(resource, action, options);\n}\n","import type { Action } from \"../types.js\";\nimport type { CanOptions } from \"../client.js\";\n\nimport { useAuthRbac } from \"./AuthRbacProvider.js\";\n\n/**\n * Direct-grant check, for sidebar items and list-page route guards.\n *\n * Returns `true` only when the action is granted on the resource as\n * a **direct** admin grant (no `<action>_granted_via`). Rows that\n * exist only because a parent resource's `dependsOn` cascade\n * materialised them return `false` here — even though\n * `useCan(resource, action)` would return `true` for the same role.\n *\n * Use case: a Verwalter with only `leases:read` direct should see\n * Mietverträge in the sidebar but **not** Mieter / Einheiten /\n * Liegenschaften — those reads are implied so the lease detail page\n * can render, not because the role should navigate to them as\n * top-level sections.\n *\n * @example sidebar item filtering\n * const showLeasesInSidebar = useCanAccessSection(\"leases\");\n * const showUnitsInSidebar = useCanAccessSection(\"units\");\n *\n * @example list-route gating\n * if (!useCanAccessSection(\"units\")) return <Forbidden />;\n *\n * Available since 0.4.0. With older SQL that doesn't return\n * `direct_*` maps, this always answers `false` — adopters still on\n * pre-0.4.0 SQL should keep using `useCan`.\n */\nexport function useCanAccessSection(\n resource: string,\n action: Action = \"read\",\n options?: CanOptions,\n): boolean {\n const { resolver } = useAuthRbac();\n return resolver.canAccessSection(resource, action, options);\n}\n","import type { ReactNode } from \"react\";\n\nimport type { Action } from \"../types.js\";\nimport type { CanOptions } from \"../client.js\";\n\nimport { useCan } from \"./useCan.js\";\n\nexport interface CanProps extends CanOptions {\n resource: string;\n action: Action;\n /** Rendered when the user has the permission. */\n children: ReactNode;\n /**\n * Rendered when the user does NOT have the permission. Defaults\n * to `null` (silent hide). Pass a `<NoPermissionView />` or a\n * tooltip-wrapper to surface the denial explicitly.\n */\n fallback?: ReactNode;\n}\n\n/**\n * Subtree gate. Bails before children render so any data fetching\n * inside `children` is skipped for users without permission.\n */\nexport function Can(props: CanProps) {\n const { resource, action, companyId, children, fallback = null } = props;\n const allowed = useCan(resource, action, { companyId });\n return <>{allowed ? children : fallback}</>;\n}\n","import type { ReactNode } from \"react\";\n\nimport type { Action } from \"../types.js\";\nimport type { CanOptions } from \"../client.js\";\n\nimport { useAuthRbac } from \"./AuthRbacProvider.js\";\nimport { useCan } from \"./useCan.js\";\n\nexport interface RequirePermissionProps extends CanOptions {\n resource: string;\n action: Action;\n /**\n * What to render while the profile is still loading. Defaults to\n * `null` (no flash) — pass a spinner if your routes typically\n * mount before the profile lands.\n */\n loadingFallback?: ReactNode;\n /**\n * What to render when access is denied. Defaults to a minimal\n * \"Sie haben keinen Zugriff\" message; pass your own component to\n * theme it.\n */\n deniedFallback?: ReactNode;\n /**\n * For `react-router-dom v6` route-element usage, pass an `<Outlet />`\n * here — the gate resolves to either the outlet or the denied\n * fallback. For component-tree usage, pass any children.\n */\n children?: ReactNode;\n}\n\n/**\n * Route- or component-level guard. Three render branches:\n *\n * - profile not yet loaded → `loadingFallback`\n * - permission denied → `deniedFallback`\n * - permission granted → `children`\n *\n * Drop-in replacement for the legacy `<RequireRolesRoute>` pattern.\n *\n * @example\n * // App.tsx route table\n * <Route element={\n * <RequirePermission resource=\"payments\" action=\"read\">\n * <Outlet />\n * </RequirePermission>\n * }>\n * <Route path=\"/payments\" element={<PaymentsPage />} />\n * </Route>\n */\nexport function RequirePermission(props: RequirePermissionProps) {\n const {\n resource,\n action,\n companyId,\n loadingFallback = null,\n deniedFallback = (\n <div role=\"alert\" style={{ padding: 24 }}>\n <strong>Sie haben keinen Zugriff.</strong>\n </div>\n ),\n children = null,\n } = props;\n\n const { profile, loading } = useAuthRbac();\n const allowed = useCan(resource, action, { companyId });\n\n if (loading || profile == null) {\n return <>{loadingFallback}</>;\n }\n if (!allowed) {\n return <>{deniedFallback}</>;\n }\n return <>{children}</>;\n}\n","import { useMemo } from \"react\";\n\nimport type { CompanyMembership } from \"../types.js\";\n\nimport { useAuthRbac } from \"./AuthRbacProvider.js\";\n\nexport interface ActiveCompany {\n id: string | null;\n membership: CompanyMembership | null;\n memberships: CompanyMembership[];\n setActive: (id: string | null) => void;\n}\n\n/**\n * Read + switch the active company.\n *\n * @example\n * const { id, memberships, setActive } = useActiveCompany();\n *\n * return (\n * <select value={id ?? \"\"} onChange={(e) => setActive(e.target.value || null)}>\n * {memberships.map((m) => (\n * <option key={m.company_id} value={m.company_id}>{m.company_name}</option>\n * ))}\n * </select>\n * );\n */\nexport function useActiveCompany(): ActiveCompany {\n const { profile, activeCompanyId, setActiveCompany } = useAuthRbac();\n\n return useMemo(() => {\n const memberships = profile?.memberships ?? [];\n const membership =\n memberships.find((m) => m.company_id === activeCompanyId) ?? null;\n return {\n id: activeCompanyId,\n membership,\n memberships,\n setActive: setActiveCompany,\n };\n }, [profile, activeCompanyId, setActiveCompany]);\n}\n","import { useMemo } from \"react\";\n\nimport type { FrontendConfig } from \"../types.js\";\n\nimport { useAuthRbac } from \"./AuthRbacProvider.js\";\n\n/**\n * Reads the merged `frontend_config` for the user. Sources in\n * priority order: active company's membership > system roles. Use\n * this to drive sidebar items, dashboard defaults, and any other\n * \"what should this role see\" UX without hardcoded role checks.\n *\n * The shape is intentionally `Record<string, unknown>` — your host\n * project owns the schema. Document your keys (e.g. `sidebar`,\n * `default_dashboard`) once and stick to them.\n */\nexport function useFrontendConfig(): FrontendConfig {\n const { profile, activeCompanyId } = useAuthRbac();\n return useMemo(() => {\n if (!profile) {\n return {};\n }\n const membershipConfig =\n profile.memberships.find((m) => m.company_id === activeCompanyId)\n ?.frontend_config ?? {};\n return { ...profile.system_frontend_config, ...membershipConfig };\n }, [profile, activeCompanyId]);\n}\n","/**\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":["defineAuthRbac","import_jsx_runtime","import_jsx_runtime","import_react","import_react","defineAuthRbac"]}
|
|
1
|
+
{"version":3,"sources":["../../src/react/index.ts","../../src/react/AuthRbacProvider.tsx","../../src/client.ts","../../src/react/useCan.ts","../../src/react/useCanAccessSection.ts","../../src/react/Can.tsx","../../src/react/RequirePermission.tsx","../../src/react/useActiveCompany.ts","../../src/react/useFrontendConfig.ts","../../src/define.ts","../../src/fetchers.ts"],"sourcesContent":["/**\n * React + Next.js entry. Import this in browser code:\n *\n * import { AuthRbacProvider, useCan } from \"snipe-auth-rbac/react\";\n *\n * The non-React entry (`snipe-auth-rbac`) re-exports types and the\n * pure resolver, suitable for Node, edge workers, and tests.\n */\n\nexport {\n AuthRbacProvider,\n useAuthRbac,\n type AuthRbacProviderProps,\n} from \"./AuthRbacProvider.js\";\n\nexport { useCan } from \"./useCan.js\";\nexport { useCanAccessSection } from \"./useCanAccessSection.js\";\nexport { Can, type CanProps } from \"./Can.js\";\nexport {\n RequirePermission,\n type RequirePermissionProps,\n} from \"./RequirePermission.js\";\nexport { useActiveCompany, type ActiveCompany } from \"./useActiveCompany.js\";\nexport { useFrontendConfig } from \"./useFrontendConfig.js\";\n\n// Re-exports for convenience so consumers don't need two imports.\nexport type {\n Action,\n AuthRbacFetcher,\n CompanyMembership,\n DependencyEdge,\n DirectGrantMap,\n FrontendConfig,\n PermissionGrid,\n PermissionMap,\n ResourceDescriptor,\n ResourceRegistry,\n ResourceScope,\n RoleSummary,\n UserProfile,\n} from \"../types.js\";\n\nexport { RbacRegistryError } from \"../define.js\";\n\nexport {\n createSupabaseFetcher,\n createHttpFetcher,\n detectRbacSchema,\n} from \"../fetchers.js\";\n\nimport { defineAuthRbac as _defineAuthRbac } from \"../define.js\";\nimport { Can } from \"./Can.js\";\nimport { RequirePermission } from \"./RequirePermission.js\";\nimport { useCan } from \"./useCan.js\";\nimport { useCanAccessSection } from \"./useCanAccessSection.js\";\n\nimport type { ResourceDescriptor } from \"../types.js\";\nimport type { TypedGuards } from \"../define.js\";\n\n/**\n * Typed factory — pass a const-asserted resource registry and get\n * back guards whose `resource` arg is constrained to the registered\n * names. Recommended at the top of every host project.\n *\n * See ../define.ts for the full doc + example.\n */\nexport function defineAuthRbac<\n const Reg extends ReadonlyArray<ResourceDescriptor>,\n>(resources: Reg): TypedGuards<Reg[number][\"resource\"]> {\n return _defineAuthRbac(resources, {\n useCan,\n useCanAccessSection,\n Can,\n RequirePermission,\n });\n}\n\nexport type { TypedGuards } from \"../define.js\";\n","import {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n type ReactNode,\n} from \"react\";\n\nimport {\n buildPermissionResolver,\n type AuthRbacClient,\n} from \"../client.js\";\nimport type {\n AuthRbacFetcher,\n PermissionMap,\n ResourceRegistry,\n UserProfile,\n} from \"../types.js\";\n\ninterface AuthRbacContextValue {\n /**\n * `null` means we haven't hydrated yet. Components should treat\n * the not-loaded state as \"no permissions\" (fail-closed).\n */\n profile: UserProfile | null;\n loading: boolean;\n error: Error | null;\n resources: ResourceRegistry;\n activeCompanyId: string | null;\n setActiveCompany: (id: string | null) => void;\n refresh: () => Promise<void>;\n resolver: AuthRbacClient;\n}\n\nconst AuthRbacContext = createContext<AuthRbacContextValue | null>(null);\n\nexport interface AuthRbacProviderProps {\n fetcher: AuthRbacFetcher;\n resources: ResourceRegistry;\n /**\n * Initial active company. Common patterns:\n * - read from URL query/path\n * - read from localStorage\n * - omit and let the user pick from the switcher\n */\n initialCompanyId?: string | null;\n /**\n * Persistence hook. Called every time the active company changes.\n * Default: writes to `localStorage` under\n * `auth-rbac:active-company`. Pass `false` to disable.\n */\n persistActiveCompany?:\n | ((id: string | null) => void)\n | false;\n children: ReactNode;\n}\n\nconst STORAGE_KEY = \"auth-rbac:active-company\";\n\nconst defaultPersist = (id: string | null) => {\n if (typeof window === \"undefined\") {\n return;\n }\n try {\n if (id == null) {\n window.localStorage.removeItem(STORAGE_KEY);\n } else {\n window.localStorage.setItem(STORAGE_KEY, id);\n }\n } catch {\n // localStorage may be unavailable (private browsing, SSR) —\n // fall back to in-memory only.\n }\n};\n\nconst readPersisted = (): string | null => {\n if (typeof window === \"undefined\") {\n return null;\n }\n try {\n return window.localStorage.getItem(STORAGE_KEY);\n } catch {\n return null;\n }\n};\n\nexport function AuthRbacProvider(props: AuthRbacProviderProps) {\n const { fetcher, resources, initialCompanyId, persistActiveCompany } = props;\n\n const [profile, setProfile] = useState<UserProfile | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const [activeCompanyId, setActiveCompanyState] = useState<string | null>(\n initialCompanyId ?? readPersisted(),\n );\n\n const persist = useMemo(() => {\n if (persistActiveCompany === false) {\n return () => {};\n }\n return persistActiveCompany ?? defaultPersist;\n }, [persistActiveCompany]);\n\n const setActiveCompany = useCallback(\n (id: string | null) => {\n setActiveCompanyState(id);\n persist(id);\n },\n [persist],\n );\n\n const refresh = useCallback(async () => {\n setLoading(true);\n setError(null);\n try {\n const next = await fetcher.fetchProfile();\n setProfile(next);\n // If the persisted company isn't a membership, fall back to\n // the first one (or null for users with no memberships).\n const stillMember =\n activeCompanyId != null &&\n next.memberships.some((m) => m.company_id === activeCompanyId);\n if (!stillMember) {\n const fallback =\n next.memberships[0]?.company_id ?? null;\n setActiveCompanyState(fallback);\n persist(fallback);\n }\n } catch (e) {\n setError(e instanceof Error ? e : new Error(String(e)));\n } finally {\n setLoading(false);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [fetcher]);\n\n useEffect(() => {\n void refresh();\n }, [refresh]);\n\n const resolver = useMemo<AuthRbacClient>(() => {\n if (profile == null) {\n // Empty resolver until the profile lands. Always returns\n // false → guards fall through to the unauthenticated branch.\n return {\n can: () => false,\n canAccessSection: () => false,\n activePermissions: () => ({}) as PermissionMap,\n systemPermissions: () => ({}) as PermissionMap,\n };\n }\n return buildPermissionResolver(resources, profile, activeCompanyId);\n }, [profile, resources, activeCompanyId]);\n\n const value = useMemo<AuthRbacContextValue>(\n () => ({\n profile,\n loading,\n error,\n resources,\n activeCompanyId,\n setActiveCompany,\n refresh,\n resolver,\n }),\n [\n profile,\n loading,\n error,\n resources,\n activeCompanyId,\n setActiveCompany,\n refresh,\n resolver,\n ],\n );\n\n return (\n <AuthRbacContext.Provider value={value}>\n {props.children}\n </AuthRbacContext.Provider>\n );\n}\n\nexport function useAuthRbac(): AuthRbacContextValue {\n const ctx = useContext(AuthRbacContext);\n if (!ctx) {\n throw new Error(\n \"useAuthRbac must be used within an <AuthRbacProvider> — wrap your app at the root.\",\n );\n }\n return ctx;\n}\n","/**\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 /**\n * Direct-grant lookup: returns true only if the user has the\n * action granted on the resource as a direct admin grant —\n * `<action>_granted_via IS NULL` in `rbac.role_permissions`.\n * Implied rows (granted as a side-effect of a parent resource's\n * `dependsOn` cascade) return false here.\n *\n * Use for top-level navigation / list-page gating: a Verwalter\n * with only `leases:read` direct gets the Leases sidebar item but\n * not Tenants / Units / Properties, even though `can(...)` returns\n * true for those (because the implied rows let the lease detail\n * page render its joined data).\n *\n * Available since 0.4.0. For older SQL that doesn't return\n * `direct_*` maps, every cell answers false — equivalent to\n * \"no direct grants known\". Adopters running pre-0.4.0 SQL should\n * keep using `can(...)`.\n */\n const canAccessSection = (\n resource: string,\n action: Action = \"read\",\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 return false;\n }\n if (scope === \"system\") {\n return readDirect(profile, action, resource);\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 readDirectMembership(membership, action, resource);\n };\n\n return {\n can,\n canAccessSection,\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 readDirect(\n profile: UserProfile,\n action: Action,\n resource: string,\n): boolean {\n const map =\n action === \"read\"\n ? profile.system_direct_reads\n : action === \"write\"\n ? profile.system_direct_writes\n : action === \"update\"\n ? profile.system_direct_updates\n : profile.system_direct_deletes;\n return map?.[resource] === true;\n}\n\nfunction readDirectMembership(\n membership: { direct_reads?: Readonly<Record<string, boolean>>;\n direct_writes?: Readonly<Record<string, boolean>>;\n direct_updates?: Readonly<Record<string, boolean>>;\n direct_deletes?: Readonly<Record<string, boolean>>; },\n action: Action,\n resource: string,\n): boolean {\n const map =\n action === \"read\"\n ? membership.direct_reads\n : action === \"write\"\n ? membership.direct_writes\n : action === \"update\"\n ? membership.direct_updates\n : membership.direct_deletes;\n return map?.[resource] === true;\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","import type { Action } from \"../types.js\";\nimport type { CanOptions } from \"../client.js\";\n\nimport { useAuthRbac } from \"./AuthRbacProvider.js\";\n\n/**\n * Boolean permission check.\n *\n * @example\n * const canEdit = useCan(\"properties\", \"update\");\n * <Button disabled={!canEdit}>Speichern</Button>\n *\n * @example explicitly target a non-active company\n * const canRead = useCan(\"payments\", \"read\", { companyId: targetId });\n */\nexport function useCan(\n resource: string,\n action: Action,\n options?: CanOptions,\n): boolean {\n const { resolver } = useAuthRbac();\n return resolver.can(resource, action, options);\n}\n","import type { Action } from \"../types.js\";\nimport type { CanOptions } from \"../client.js\";\n\nimport { useAuthRbac } from \"./AuthRbacProvider.js\";\n\n/**\n * Direct-grant check, for sidebar items and list-page route guards.\n *\n * Returns `true` only when the action is granted on the resource as\n * a **direct** admin grant (no `<action>_granted_via`). Rows that\n * exist only because a parent resource's `dependsOn` cascade\n * materialised them return `false` here — even though\n * `useCan(resource, action)` would return `true` for the same role.\n *\n * Use case: a Verwalter with only `leases:read` direct should see\n * Mietverträge in the sidebar but **not** Mieter / Einheiten /\n * Liegenschaften — those reads are implied so the lease detail page\n * can render, not because the role should navigate to them as\n * top-level sections.\n *\n * @example sidebar item filtering\n * const showLeasesInSidebar = useCanAccessSection(\"leases\");\n * const showUnitsInSidebar = useCanAccessSection(\"units\");\n *\n * @example list-route gating\n * if (!useCanAccessSection(\"units\")) return <Forbidden />;\n *\n * Available since 0.4.0. With older SQL that doesn't return\n * `direct_*` maps, this always answers `false` — adopters still on\n * pre-0.4.0 SQL should keep using `useCan`.\n */\nexport function useCanAccessSection(\n resource: string,\n action: Action = \"read\",\n options?: CanOptions,\n): boolean {\n const { resolver } = useAuthRbac();\n return resolver.canAccessSection(resource, action, options);\n}\n","import type { ReactNode } from \"react\";\n\nimport type { Action } from \"../types.js\";\nimport type { CanOptions } from \"../client.js\";\n\nimport { useCan } from \"./useCan.js\";\n\nexport interface CanProps extends CanOptions {\n resource: string;\n action: Action;\n /** Rendered when the user has the permission. */\n children: ReactNode;\n /**\n * Rendered when the user does NOT have the permission. Defaults\n * to `null` (silent hide). Pass a `<NoPermissionView />` or a\n * tooltip-wrapper to surface the denial explicitly.\n */\n fallback?: ReactNode;\n}\n\n/**\n * Subtree gate. Bails before children render so any data fetching\n * inside `children` is skipped for users without permission.\n */\nexport function Can(props: CanProps) {\n const { resource, action, companyId, children, fallback = null } = props;\n const allowed = useCan(resource, action, { companyId });\n return <>{allowed ? children : fallback}</>;\n}\n","import type { ReactNode } from \"react\";\n\nimport type { Action } from \"../types.js\";\nimport type { CanOptions } from \"../client.js\";\n\nimport { useAuthRbac } from \"./AuthRbacProvider.js\";\nimport { useCan } from \"./useCan.js\";\n\nexport interface RequirePermissionProps extends CanOptions {\n resource: string;\n action: Action;\n /**\n * What to render while the profile is still loading. Defaults to\n * `null` (no flash) — pass a spinner if your routes typically\n * mount before the profile lands.\n */\n loadingFallback?: ReactNode;\n /**\n * What to render when access is denied. Defaults to a minimal\n * \"Sie haben keinen Zugriff\" message; pass your own component to\n * theme it.\n */\n deniedFallback?: ReactNode;\n /**\n * For `react-router-dom v6` route-element usage, pass an `<Outlet />`\n * here — the gate resolves to either the outlet or the denied\n * fallback. For component-tree usage, pass any children.\n */\n children?: ReactNode;\n}\n\n/**\n * Route- or component-level guard. Three render branches:\n *\n * - profile not yet loaded → `loadingFallback`\n * - permission denied → `deniedFallback`\n * - permission granted → `children`\n *\n * Drop-in replacement for the legacy `<RequireRolesRoute>` pattern.\n *\n * @example\n * // App.tsx route table\n * <Route element={\n * <RequirePermission resource=\"payments\" action=\"read\">\n * <Outlet />\n * </RequirePermission>\n * }>\n * <Route path=\"/payments\" element={<PaymentsPage />} />\n * </Route>\n */\nexport function RequirePermission(props: RequirePermissionProps) {\n const {\n resource,\n action,\n companyId,\n loadingFallback = null,\n deniedFallback = (\n <div role=\"alert\" style={{ padding: 24 }}>\n <strong>Sie haben keinen Zugriff.</strong>\n </div>\n ),\n children = null,\n } = props;\n\n const { profile, loading } = useAuthRbac();\n const allowed = useCan(resource, action, { companyId });\n\n if (loading || profile == null) {\n return <>{loadingFallback}</>;\n }\n if (!allowed) {\n return <>{deniedFallback}</>;\n }\n return <>{children}</>;\n}\n","import { useMemo } from \"react\";\n\nimport type { CompanyMembership } from \"../types.js\";\n\nimport { useAuthRbac } from \"./AuthRbacProvider.js\";\n\nexport interface ActiveCompany {\n id: string | null;\n membership: CompanyMembership | null;\n memberships: CompanyMembership[];\n setActive: (id: string | null) => void;\n}\n\n/**\n * Read + switch the active company.\n *\n * @example\n * const { id, memberships, setActive } = useActiveCompany();\n *\n * return (\n * <select value={id ?? \"\"} onChange={(e) => setActive(e.target.value || null)}>\n * {memberships.map((m) => (\n * <option key={m.company_id} value={m.company_id}>{m.company_name}</option>\n * ))}\n * </select>\n * );\n */\nexport function useActiveCompany(): ActiveCompany {\n const { profile, activeCompanyId, setActiveCompany } = useAuthRbac();\n\n return useMemo(() => {\n const memberships = profile?.memberships ?? [];\n const membership =\n memberships.find((m) => m.company_id === activeCompanyId) ?? null;\n return {\n id: activeCompanyId,\n membership,\n memberships,\n setActive: setActiveCompany,\n };\n }, [profile, activeCompanyId, setActiveCompany]);\n}\n","import { useMemo } from \"react\";\n\nimport type { FrontendConfig } from \"../types.js\";\n\nimport { useAuthRbac } from \"./AuthRbacProvider.js\";\n\n/**\n * Reads the merged `frontend_config` for the user. Sources in\n * priority order: active company's membership > system roles. Use\n * this to drive sidebar items, dashboard defaults, and any other\n * \"what should this role see\" UX without hardcoded role checks.\n *\n * The shape is intentionally `Record<string, unknown>` — your host\n * project owns the schema. Document your keys (e.g. `sidebar`,\n * `default_dashboard`) once and stick to them.\n */\nexport function useFrontendConfig(): FrontendConfig {\n const { profile, activeCompanyId } = useAuthRbac();\n return useMemo(() => {\n if (!profile) {\n return {};\n }\n const membershipConfig =\n profile.memberships.find((m) => m.company_id === activeCompanyId)\n ?.frontend_config ?? {};\n return { ...profile.system_frontend_config, ...membershipConfig };\n }, [profile, activeCompanyId]);\n}\n","/**\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. Throws on\n * **dangling** edges (an edge points at a resource that isn't in\n * the registry — almost always a typo). Cycles are *not* fatal:\n * the matrix UI's cascade is cycle-safe by construction (it only\n * flips cells on and skips already-on cells), and real-world\n * dependency graphs in property-management / multi-entity SaaS are\n * naturally cyclic — properties and units mutually need each\n * other's read to render either detail view. We emit a single\n * console warning per registry build so unintended cycles are\n * still visible during development, without breaking adopters whose\n * graphs are cyclic by design.\n *\n * Pre-0.4.1: cycles threw RbacRegistryError. The throw was wrong —\n * the cascade has always been safe under cycles. The change in\n * 0.4.1 keeps the diagnostic but downgrades it to a warning.\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 const cyclesFound = new Set<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 // Cycle. Record the path for the warning but keep walking so\n // we don't mask further dangling-edge errors deeper in the\n // graph.\n const cycleStart = stack.indexOf(name);\n const cycle = [...stack.slice(cycleStart), name].join(\" → \");\n cyclesFound.add(cycle);\n return;\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 if (cyclesFound.size > 0) {\n // eslint-disable-next-line no-console\n console.warn(\n `[auth-rbac] dependency cycle(s) detected in resource registry. ` +\n `Cycles are runtime-safe — the matrix UI's cascade only flips ` +\n `cells on and skips already-on cells — but they may indicate ` +\n `unintended edges. Found: ${Array.from(cyclesFound).join(\"; \")}`,\n );\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":["defineAuthRbac","import_jsx_runtime","import_jsx_runtime","import_react","import_react","defineAuthRbac"]}
|
package/dist/react/index.js
CHANGED
package/package.json
CHANGED
|
@@ -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":[]}
|