snipe-auth-rbac 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,32 +1,42 @@
1
1
  -- snipe-auth-rbac — optional default seed
2
2
  --
3
3
  -- Companion to 0001_initial.sql that seeds:
4
- -- * Two system roles (System Admin with is_super=true, System Support).
5
- -- * Four generic company-role templates (Owner / Manager / Member /
6
- -- Viewer) with sensible default_permissions patterns.
4
+ -- * Two system roles (System-Administrator with is_super=true,
5
+ -- System-Support).
6
+ -- * Four generic company-role templates (Inhaber / Verwalter /
7
+ -- Mitarbeiter / Leser) with sensible default_permissions
8
+ -- patterns.
7
9
  --
8
- -- The four templates use only the `default` action set — they don't
9
- -- reference specific resources or groups, since those are defined by
10
- -- the host. After registering host resources, run
11
- -- ``rbac.apply_template_defaults(role_id)`` to materialise the matrix.
10
+ -- Names are in German that's the package's primary target
11
+ -- audience (German property-management / SaaS). Adopters who
12
+ -- prefer English names skip this file and seed their own.
12
13
  --
13
- -- Domain-specific templates (Property Manager, Tenant Manager,
14
- -- Sales, …) belong in the host's own seed migration where their
15
- -- group/resource defaults can reference real registered resources.
14
+ -- The four templates use only the `default` action set — they
15
+ -- don't reference specific resources or groups, since those are
16
+ -- defined by the host. After registering host resources, run
17
+ -- ``rbac.apply_template_defaults(role_id)`` to materialise the
18
+ -- matrix.
19
+ --
20
+ -- Domain-specific templates (Liegenschaftsverwalter,
21
+ -- Mieterverwalter, Vertrieb, Gutachter, Anwalt, Mieter) belong in
22
+ -- the host's own seed migration where their group/resource
23
+ -- defaults can reference real registered resources.
16
24
  --
17
25
  -- Idempotent: every INSERT uses ON CONFLICT DO NOTHING. Re-running
18
- -- the file leaves an existing deployment untouched.
26
+ -- the file leaves an existing deployment untouched. Note: this
27
+ -- means upgrading from v0.3.0 (English names) does NOT auto-rename
28
+ -- — see CHANGELOG for the rename snippet.
19
29
 
20
30
  BEGIN;
21
31
 
22
32
  -- System roles
23
33
  INSERT INTO rbac.roles (id, scope, company_id, name, description, is_system, is_super, default_permissions)
24
34
  VALUES
25
- (gen_random_uuid(), 'system', NULL, 'System Admin',
35
+ (gen_random_uuid(), 'system', NULL, 'System-Administrator',
26
36
  'Plattform-Vollzugriff. Setzt jede Berechtigungsprüfung außer Kraft.',
27
37
  true, true,
28
38
  '{"default": ["read", "write", "update", "delete"]}'::jsonb),
29
- (gen_random_uuid(), 'system', NULL, 'System Support',
39
+ (gen_random_uuid(), 'system', NULL, 'System-Support',
30
40
  'Lesezugriff auf systemweite Ressourcen für Support-Aufgaben.',
31
41
  true, false,
32
42
  '{"default": ["read"]}'::jsonb)
@@ -36,19 +46,19 @@ ON CONFLICT DO NOTHING;
36
46
  -- Generic shapes only; domain-specific defaults are the host's job.
37
47
  INSERT INTO rbac.roles (id, scope, company_id, name, description, is_system, is_super, default_permissions)
38
48
  VALUES
39
- (gen_random_uuid(), 'company', NULL, 'Owner',
40
- 'Vollzugriff innerhalb der eigenen Company.',
49
+ (gen_random_uuid(), 'company', NULL, 'Inhaber',
50
+ 'Vollzugriff innerhalb des eigenen Mandanten.',
41
51
  true, false,
42
52
  '{"default": ["read", "write", "update", "delete"]}'::jsonb),
43
- (gen_random_uuid(), 'company', NULL, 'Manager',
44
- 'Verwaltet Daten der Company, kann Rollen ändern. Kein Löschen.',
53
+ (gen_random_uuid(), 'company', NULL, 'Verwalter',
54
+ 'Verwaltet Daten des Mandanten, kann Rollen ändern. Kein Löschen.',
45
55
  true, false,
46
56
  '{"default": ["read", "write", "update"]}'::jsonb),
47
- (gen_random_uuid(), 'company', NULL, 'Member',
57
+ (gen_random_uuid(), 'company', NULL, 'Mitarbeiter',
48
58
  'Standard-Mitarbeiter mit Lese- und Schreibzugriff.',
49
59
  true, false,
50
60
  '{"default": ["read", "write"]}'::jsonb),
51
- (gen_random_uuid(), 'company', NULL, 'Viewer',
61
+ (gen_random_uuid(), 'company', NULL, 'Leser',
52
62
  'Nur Lesezugriff.',
53
63
  true, false,
54
64
  '{"default": ["read"]}'::jsonb)
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/client.ts"],"sourcesContent":["/**\n * Transport-agnostic client: turns an adopter-supplied\n * `AuthRbacFetcher` into a permission resolver. The React provider\n * wraps this; non-React consumers (Node scripts, edge functions)\n * can use it directly.\n */\n\nimport type {\n Action,\n AuthRbacFetcher,\n PermissionMap,\n ResourceDescriptor,\n ResourceRegistry,\n ResourceScope,\n UserProfile,\n} from \"./types.js\";\n\nexport interface AuthRbacClientOptions {\n fetcher: AuthRbacFetcher;\n /**\n * The host project's full resource list. Required so the resolver\n * can look up a resource's scope without a DB round-trip per call.\n * Re-using the same array the host syncs into the\n * `rbac.resources` table at boot keeps everything in lockstep.\n */\n resources: ResourceRegistry;\n}\n\nexport interface CanOptions {\n /**\n * Override the active company. Omit to use the company the\n * caller has currently activated (the React provider tracks\n * this; for direct client use you must pass it).\n */\n companyId?: string | null;\n}\n\n/**\n * Pure resolver. Given a hydrated profile it answers boolean\n * questions instantly — no I/O. The `resourceMap` is built once at\n * construction so per-call work is two map lookups.\n */\nexport function buildPermissionResolver(\n resources: ResourceRegistry,\n profile: UserProfile,\n defaultCompanyId: string | null,\n) {\n const scopeByResource = new Map<string, ResourceScope>(\n resources.map((r) => [r.resource, r.scope]),\n );\n\n const can = (\n resource: string,\n action: Action,\n options?: CanOptions,\n ): boolean => {\n if (profile.is_super_admin) {\n return true;\n }\n const scope = scopeByResource.get(resource);\n if (!scope) {\n // Unknown resource — fail closed.\n return false;\n }\n if (scope === \"system\") {\n return readGrid(profile.system_permissions, resource, action);\n }\n const companyId = options?.companyId ?? defaultCompanyId;\n if (!companyId) {\n return false;\n }\n const membership = profile.memberships.find(\n (m) => m.company_id === companyId,\n );\n if (!membership) {\n return false;\n }\n return readGrid(membership.permissions, resource, action);\n };\n\n return {\n can,\n /** Permission map for the active (or specified) company. */\n activePermissions: (companyId?: string | null): PermissionMap => {\n const id = companyId ?? defaultCompanyId;\n if (!id) {\n return {};\n }\n return (\n profile.memberships.find((m) => m.company_id === id)?.permissions ?? {}\n );\n },\n systemPermissions: (): PermissionMap => profile.system_permissions,\n };\n}\n\nfunction readGrid(\n map: PermissionMap,\n resource: string,\n action: Action,\n): boolean {\n const grid = map[resource];\n if (!grid) {\n return false;\n }\n return grid[action];\n}\n\n/**\n * Helper: groups a resource registry by `group` for the matrix UI.\n * Returns groups in insertion order with their resources.\n */\nexport function groupResources(\n registry: ResourceRegistry,\n): Array<{ group: string; resources: ResourceDescriptor[] }> {\n const order: string[] = [];\n const buckets = new Map<string, ResourceDescriptor[]>();\n for (const r of registry) {\n const key = r.group ?? \"Sonstige\";\n if (!buckets.has(key)) {\n buckets.set(key, []);\n order.push(key);\n }\n buckets.get(key)!.push(r);\n }\n return order.map((g) => ({ group: g, resources: buckets.get(g)! }));\n}\n\nexport type AuthRbacClient = ReturnType<typeof buildPermissionResolver>;\nexport type { AuthRbacClientOptions as ClientOptions };\n"],"mappings":";AA0CO,SAAS,wBACd,WACA,SACA,kBACA;AACA,QAAM,kBAAkB,IAAI;AAAA,IAC1B,UAAU,IAAI,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC;AAAA,EAC5C;AAEA,QAAM,MAAM,CACV,UACA,QACA,YACY;AACZ,QAAI,QAAQ,gBAAgB;AAC1B,aAAO;AAAA,IACT;AACA,UAAM,QAAQ,gBAAgB,IAAI,QAAQ;AAC1C,QAAI,CAAC,OAAO;AAEV,aAAO;AAAA,IACT;AACA,QAAI,UAAU,UAAU;AACtB,aAAO,SAAS,QAAQ,oBAAoB,UAAU,MAAM;AAAA,IAC9D;AACA,UAAM,YAAY,SAAS,aAAa;AACxC,QAAI,CAAC,WAAW;AACd,aAAO;AAAA,IACT;AACA,UAAM,aAAa,QAAQ,YAAY;AAAA,MACrC,CAAC,MAAM,EAAE,eAAe;AAAA,IAC1B;AACA,QAAI,CAAC,YAAY;AACf,aAAO;AAAA,IACT;AACA,WAAO,SAAS,WAAW,aAAa,UAAU,MAAM;AAAA,EAC1D;AAEA,SAAO;AAAA,IACL;AAAA;AAAA,IAEA,mBAAmB,CAAC,cAA6C;AAC/D,YAAM,KAAK,aAAa;AACxB,UAAI,CAAC,IAAI;AACP,eAAO,CAAC;AAAA,MACV;AACA,aACE,QAAQ,YAAY,KAAK,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,eAAe,CAAC;AAAA,IAE1E;AAAA,IACA,mBAAmB,MAAqB,QAAQ;AAAA,EAClD;AACF;AAEA,SAAS,SACP,KACA,UACA,QACS;AACT,QAAM,OAAO,IAAI,QAAQ;AACzB,MAAI,CAAC,MAAM;AACT,WAAO;AAAA,EACT;AACA,SAAO,KAAK,MAAM;AACpB;AAMO,SAAS,eACd,UAC2D;AAC3D,QAAM,QAAkB,CAAC;AACzB,QAAM,UAAU,oBAAI,IAAkC;AACtD,aAAW,KAAK,UAAU;AACxB,UAAM,MAAM,EAAE,SAAS;AACvB,QAAI,CAAC,QAAQ,IAAI,GAAG,GAAG;AACrB,cAAQ,IAAI,KAAK,CAAC,CAAC;AACnB,YAAM,KAAK,GAAG;AAAA,IAChB;AACA,YAAQ,IAAI,GAAG,EAAG,KAAK,CAAC;AAAA,EAC1B;AACA,SAAO,MAAM,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,WAAW,QAAQ,IAAI,CAAC,EAAG,EAAE;AACpE;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/fetchers.ts"],"sourcesContent":["/**\n * Built-in fetchers — adopters can use these or pass their own\n * implementation of `AuthRbacFetcher`.\n */\n\nimport type { AuthRbacFetcher, UserProfile } from \"./types.js\";\n\n/**\n * Calls the package's SQL function `rbac.user_profile(uuid)` via\n * a Supabase JS client. Easiest path when the host project already\n * uses Supabase.\n *\n * The function lives in the dedicated `rbac` Postgres schema, so the\n * adopter must add `rbac` to their PostgREST exposed-schemas list\n * (Supabase Studio → Settings → API → Exposed schemas) for the\n * `.schema('rbac')` call below to reach it.\n *\n * @example\n * createSupabaseFetcher({ supabase, userId: session.user.id })\n */\nexport function createSupabaseFetcher(opts: {\n supabase: {\n schema: (name: string) => {\n rpc: (\n fn: string,\n args: Record<string, unknown>,\n ) => Promise<{ data: unknown; error: { message: string } | null }>;\n };\n };\n userId: string;\n}): AuthRbacFetcher {\n return {\n async fetchProfile(): Promise<UserProfile> {\n const { data, error } = await opts.supabase.schema(\"rbac\").rpc(\n \"user_profile\",\n { p_user_id: opts.userId },\n );\n if (error) {\n throw new Error(\n `auth-rbac: failed to load user profile via Supabase RPC: ${error.message}`,\n );\n }\n return normalizeProfile(data);\n },\n };\n}\n\n/**\n * Cheap probe — returns true if the package's `rbac` schema looks\n * reachable. Useful at app start to fail loudly if the migration\n * hasn't been applied OR if `rbac` isn't in the project's PostgREST\n * exposed-schemas list.\n *\n * @example\n * if (!(await detectRbacSchema(supabase))) {\n * console.error(\"rbac schema not reachable — apply 0001_initial.sql\");\n * }\n */\nexport async function detectRbacSchema(supabase: {\n schema: (name: string) => {\n rpc: (\n fn: string,\n args: Record<string, unknown>,\n ) => Promise<{ data: unknown; error: { message: string } | null }>;\n };\n}): Promise<boolean> {\n try {\n const { error } = await supabase.schema(\"rbac\").rpc(\"user_can\", {\n p_user_id: \"00000000-0000-0000-0000-000000000000\",\n p_resource: \"__rbac_self_check__\",\n p_action: \"read\",\n p_company_id: null,\n });\n return error === null;\n } catch {\n return false;\n }\n}\n\n/**\n * Calls a regular HTTP endpoint that returns a `UserProfile` JSON\n * payload. Use this when the host project has its own backend that\n * wraps the package's Python helpers (or any equivalent).\n *\n * @example\n * createHttpFetcher({ url: \"/api/users/me/profile\" })\n */\nexport function createHttpFetcher(opts: {\n url: string;\n /** Forwarded as-is to `fetch`. Use this to attach auth headers. */\n init?: RequestInit;\n /** Override the global `fetch` if you're in a non-browser env. */\n fetch?: typeof fetch;\n}): AuthRbacFetcher {\n const fetchImpl = opts.fetch ?? globalThis.fetch;\n return {\n async fetchProfile(): Promise<UserProfile> {\n const res = await fetchImpl(opts.url, opts.init);\n if (!res.ok) {\n throw new Error(\n `auth-rbac: profile endpoint ${opts.url} returned ${res.status}`,\n );\n }\n const json = (await res.json()) as unknown;\n return normalizeProfile(json);\n },\n };\n}\n\n/**\n * Defensive normalisation: the Supabase RPC returns whatever the SQL\n * function emitted. We coerce missing fields into the empty defaults\n * so consumers can iterate without null checks. Throws if the shape\n * is unrecognisable.\n */\nfunction normalizeProfile(raw: unknown): UserProfile {\n if (!raw || typeof raw !== \"object\") {\n throw new Error(\"auth-rbac: profile payload is not an object\");\n }\n const p = raw as Partial<UserProfile> & Record<string, unknown>;\n if (typeof p.user_id !== \"string\") {\n throw new Error(\"auth-rbac: profile payload missing user_id\");\n }\n return {\n user_id: p.user_id,\n is_super_admin: !!p.is_super_admin,\n system_roles: Array.isArray(p.system_roles) ? p.system_roles : [],\n system_permissions:\n p.system_permissions && typeof p.system_permissions === \"object\"\n ? (p.system_permissions as UserProfile[\"system_permissions\"])\n : {},\n system_frontend_config:\n p.system_frontend_config && typeof p.system_frontend_config === \"object\"\n ? (p.system_frontend_config as UserProfile[\"system_frontend_config\"])\n : {},\n memberships: Array.isArray(p.memberships)\n ? (p.memberships as UserProfile[\"memberships\"])\n : [],\n };\n}\n"],"mappings":";AAoBO,SAAS,sBAAsB,MAUlB;AAClB,SAAO;AAAA,IACL,MAAM,eAAqC;AACzC,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,KAAK,SAAS,OAAO,MAAM,EAAE;AAAA,QACzD;AAAA,QACA,EAAE,WAAW,KAAK,OAAO;AAAA,MAC3B;AACA,UAAI,OAAO;AACT,cAAM,IAAI;AAAA,UACR,4DAA4D,MAAM,OAAO;AAAA,QAC3E;AAAA,MACF;AACA,aAAO,iBAAiB,IAAI;AAAA,IAC9B;AAAA,EACF;AACF;AAaA,eAAsB,iBAAiB,UAOlB;AACnB,MAAI;AACF,UAAM,EAAE,MAAM,IAAI,MAAM,SAAS,OAAO,MAAM,EAAE,IAAI,YAAY;AAAA,MAC9D,WAAW;AAAA,MACX,YAAY;AAAA,MACZ,UAAU;AAAA,MACV,cAAc;AAAA,IAChB,CAAC;AACD,WAAO,UAAU;AAAA,EACnB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAUO,SAAS,kBAAkB,MAMd;AAClB,QAAM,YAAY,KAAK,SAAS,WAAW;AAC3C,SAAO;AAAA,IACL,MAAM,eAAqC;AACzC,YAAM,MAAM,MAAM,UAAU,KAAK,KAAK,KAAK,IAAI;AAC/C,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,IAAI;AAAA,UACR,+BAA+B,KAAK,GAAG,aAAa,IAAI,MAAM;AAAA,QAChE;AAAA,MACF;AACA,YAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,aAAO,iBAAiB,IAAI;AAAA,IAC9B;AAAA,EACF;AACF;AAQA,SAAS,iBAAiB,KAA2B;AACnD,MAAI,CAAC,OAAO,OAAO,QAAQ,UAAU;AACnC,UAAM,IAAI,MAAM,6CAA6C;AAAA,EAC/D;AACA,QAAM,IAAI;AACV,MAAI,OAAO,EAAE,YAAY,UAAU;AACjC,UAAM,IAAI,MAAM,4CAA4C;AAAA,EAC9D;AACA,SAAO;AAAA,IACL,SAAS,EAAE;AAAA,IACX,gBAAgB,CAAC,CAAC,EAAE;AAAA,IACpB,cAAc,MAAM,QAAQ,EAAE,YAAY,IAAI,EAAE,eAAe,CAAC;AAAA,IAChE,oBACE,EAAE,sBAAsB,OAAO,EAAE,uBAAuB,WACnD,EAAE,qBACH,CAAC;AAAA,IACP,wBACE,EAAE,0BAA0B,OAAO,EAAE,2BAA2B,WAC3D,EAAE,yBACH,CAAC;AAAA,IACP,aAAa,MAAM,QAAQ,EAAE,WAAW,IACnC,EAAE,cACH,CAAC;AAAA,EACP;AACF;","names":[]}
@@ -1,69 +0,0 @@
1
- /**
2
- * Core types — used by both the transport-agnostic client and the
3
- * React layer. Adopters that don't use React only need to depend on
4
- * the `index` entry; the `react` entry's types extend these.
5
- */
6
- type Action = "read" | "write" | "update" | "delete";
7
- type ResourceScope = "system" | "company";
8
- interface ResourceDescriptor {
9
- resource: string;
10
- scope: ResourceScope;
11
- label: string;
12
- description?: string;
13
- group?: string;
14
- }
15
- interface RoleSummary {
16
- id: string;
17
- name: string;
18
- is_system?: boolean;
19
- is_super?: boolean;
20
- frontend_config?: FrontendConfig;
21
- }
22
- /**
23
- * Free-form per-role config that the host project can interpret as
24
- * it sees fit. A typical shape includes `sidebar` (list of section
25
- * keys), `default_dashboard` (string), `home_route` (string).
26
- */
27
- type FrontendConfig = Record<string, unknown>;
28
- interface PermissionGrid {
29
- read: boolean;
30
- write: boolean;
31
- update: boolean;
32
- delete: boolean;
33
- }
34
- type PermissionMap = Record<string, PermissionGrid>;
35
- interface CompanyMembership {
36
- company_id: string;
37
- company_name: string;
38
- company_slug?: string | null;
39
- roles: RoleSummary[];
40
- permissions: PermissionMap;
41
- /** Merged frontend_config across all roles the user holds in this company. */
42
- frontend_config: FrontendConfig;
43
- }
44
- interface UserProfile {
45
- user_id: string;
46
- is_super_admin: boolean;
47
- system_roles: RoleSummary[];
48
- system_permissions: PermissionMap;
49
- /** Merged frontend_config across all of the user's system roles. */
50
- system_frontend_config: FrontendConfig;
51
- memberships: CompanyMembership[];
52
- }
53
- /**
54
- * Adopter-supplied transport. Two flavours are supported out of the
55
- * box:
56
- *
57
- * 1. Pass a Supabase client + a JWT-derived `user_id` and the
58
- * library calls the package's SQL RPC `rbac.user_profile`.
59
- * 2. Pass a plain `fetcher` (anything that resolves a `UserProfile`)
60
- * and the library calls that. Use this when your backend serves
61
- * a custom `/api/users/me/profile` endpoint or when you don't
62
- * run Supabase at all.
63
- */
64
- interface AuthRbacFetcher {
65
- fetchProfile(): Promise<UserProfile>;
66
- }
67
- type ResourceRegistry = ReadonlyArray<ResourceDescriptor>;
68
-
69
- export type { Action as A, CompanyMembership as C, FrontendConfig as F, PermissionGrid as P, ResourceScope as R, UserProfile as U, ResourceDescriptor as a, AuthRbacFetcher as b, ResourceRegistry as c, PermissionMap as d, RoleSummary as e };
@@ -1,69 +0,0 @@
1
- /**
2
- * Core types — used by both the transport-agnostic client and the
3
- * React layer. Adopters that don't use React only need to depend on
4
- * the `index` entry; the `react` entry's types extend these.
5
- */
6
- type Action = "read" | "write" | "update" | "delete";
7
- type ResourceScope = "system" | "company";
8
- interface ResourceDescriptor {
9
- resource: string;
10
- scope: ResourceScope;
11
- label: string;
12
- description?: string;
13
- group?: string;
14
- }
15
- interface RoleSummary {
16
- id: string;
17
- name: string;
18
- is_system?: boolean;
19
- is_super?: boolean;
20
- frontend_config?: FrontendConfig;
21
- }
22
- /**
23
- * Free-form per-role config that the host project can interpret as
24
- * it sees fit. A typical shape includes `sidebar` (list of section
25
- * keys), `default_dashboard` (string), `home_route` (string).
26
- */
27
- type FrontendConfig = Record<string, unknown>;
28
- interface PermissionGrid {
29
- read: boolean;
30
- write: boolean;
31
- update: boolean;
32
- delete: boolean;
33
- }
34
- type PermissionMap = Record<string, PermissionGrid>;
35
- interface CompanyMembership {
36
- company_id: string;
37
- company_name: string;
38
- company_slug?: string | null;
39
- roles: RoleSummary[];
40
- permissions: PermissionMap;
41
- /** Merged frontend_config across all roles the user holds in this company. */
42
- frontend_config: FrontendConfig;
43
- }
44
- interface UserProfile {
45
- user_id: string;
46
- is_super_admin: boolean;
47
- system_roles: RoleSummary[];
48
- system_permissions: PermissionMap;
49
- /** Merged frontend_config across all of the user's system roles. */
50
- system_frontend_config: FrontendConfig;
51
- memberships: CompanyMembership[];
52
- }
53
- /**
54
- * Adopter-supplied transport. Two flavours are supported out of the
55
- * box:
56
- *
57
- * 1. Pass a Supabase client + a JWT-derived `user_id` and the
58
- * library calls the package's SQL RPC `rbac.user_profile`.
59
- * 2. Pass a plain `fetcher` (anything that resolves a `UserProfile`)
60
- * and the library calls that. Use this when your backend serves
61
- * a custom `/api/users/me/profile` endpoint or when you don't
62
- * run Supabase at all.
63
- */
64
- interface AuthRbacFetcher {
65
- fetchProfile(): Promise<UserProfile>;
66
- }
67
- type ResourceRegistry = ReadonlyArray<ResourceDescriptor>;
68
-
69
- export type { Action as A, CompanyMembership as C, FrontendConfig as F, PermissionGrid as P, ResourceScope as R, UserProfile as U, ResourceDescriptor as a, AuthRbacFetcher as b, ResourceRegistry as c, PermissionMap as d, RoleSummary as e };