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.
@@ -0,0 +1,191 @@
1
+ import { c as ResourceRegistry, U as UserProfile, A as Action, e as PermissionMap, b as AuthRbacFetcher, a as ResourceDescriptor } from './types-Oj9yfWvz.js';
2
+ import { ComponentType, ReactNode } from 'react';
3
+
4
+ /**
5
+ * Transport-agnostic client: turns an adopter-supplied
6
+ * `AuthRbacFetcher` into a permission resolver. The React provider
7
+ * wraps this; non-React consumers (Node scripts, edge functions)
8
+ * can use it directly.
9
+ */
10
+
11
+ interface AuthRbacClientOptions {
12
+ fetcher: AuthRbacFetcher;
13
+ /**
14
+ * The host project's full resource list. Required so the resolver
15
+ * can look up a resource's scope without a DB round-trip per call.
16
+ * Re-using the same array the host syncs into the
17
+ * `rbac.resources` table at boot keeps everything in lockstep.
18
+ */
19
+ resources: ResourceRegistry;
20
+ }
21
+ interface CanOptions {
22
+ /**
23
+ * Override the active company. Omit to use the company the
24
+ * caller has currently activated (the React provider tracks
25
+ * this; for direct client use you must pass it).
26
+ */
27
+ companyId?: string | null;
28
+ }
29
+ /**
30
+ * Pure resolver. Given a hydrated profile it answers boolean
31
+ * questions instantly — no I/O. The `resourceMap` is built once at
32
+ * construction so per-call work is two map lookups.
33
+ */
34
+ declare function buildPermissionResolver(resources: ResourceRegistry, profile: UserProfile, defaultCompanyId: string | null): {
35
+ can: (resource: string, action: Action, options?: CanOptions) => boolean;
36
+ canAccessSection: (resource: string, action?: Action, options?: CanOptions) => boolean;
37
+ /** Permission map for the active (or specified) company. */
38
+ activePermissions: (companyId?: string | null) => PermissionMap;
39
+ systemPermissions: () => PermissionMap;
40
+ };
41
+ /**
42
+ * Helper: groups a resource registry by `group` for the matrix UI.
43
+ * Returns groups in insertion order with their resources.
44
+ */
45
+ declare function groupResources(registry: ResourceRegistry): Array<{
46
+ group: string;
47
+ resources: ResourceDescriptor[];
48
+ }>;
49
+ type AuthRbacClient = ReturnType<typeof buildPermissionResolver>;
50
+
51
+ /**
52
+ * Typed factory — turns a const-asserted resource registry into a
53
+ * set of hooks/components whose `resource` arg is constrained to
54
+ * the registered names. Typos become TypeScript errors instead of
55
+ * silent runtime `false`.
56
+ *
57
+ * @example
58
+ * // src/auth/resources.ts
59
+ * import { defineAuthRbac } from "snipe-auth-rbac/react";
60
+ *
61
+ * export const RESOURCES = [
62
+ * { resource: "properties", scope: "company", label: "Liegenschaften", group: "Stammdaten" },
63
+ * { resource: "payments", scope: "company", label: "Zahlungen", group: "Finanzen" },
64
+ * { resource: "system_audit", scope: "system", label: "Audit-Log", group: "Plattform" },
65
+ * ] as const;
66
+ *
67
+ * export const { useCan, Can, RequirePermission } = defineAuthRbac(RESOURCES);
68
+ *
69
+ * // ----- elsewhere -----
70
+ * useCan("properties", "update"); // ✓
71
+ * useCan("paymetns", "update"); // ✗ TS error: not assignable to type "properties" | "payments" | "system_audit"
72
+ */
73
+
74
+ /**
75
+ * Thrown by `defineAuthRbac` when the registry contains a cycle in
76
+ * its `dependsOn` graph. The cycle path is included in the message,
77
+ * named in registry order.
78
+ */
79
+ declare class RbacRegistryError extends Error {
80
+ constructor(message: string);
81
+ }
82
+ /**
83
+ * Drop-in replacement signatures for the three guards, with a
84
+ * narrowed `resource` arg.
85
+ */
86
+ interface TypedGuards<R extends string> {
87
+ useCan: (resource: R, action: Action, options?: {
88
+ companyId?: string | null;
89
+ }) => boolean;
90
+ /**
91
+ * `useCan` for sidebar / list-page access: returns true only when
92
+ * the user holds the action on the resource as a **direct** grant
93
+ * (no `<action>_granted_via`). Implied rows answer false here.
94
+ *
95
+ * `action` defaults to `'read'` because the canonical use is
96
+ * top-level-nav gating. Available since 0.4.0.
97
+ */
98
+ useCanAccessSection: (resource: R, action?: Action, options?: {
99
+ companyId?: string | null;
100
+ }) => boolean;
101
+ Can: ComponentType<{
102
+ resource: R;
103
+ action: Action;
104
+ companyId?: string | null;
105
+ children: ReactNode;
106
+ fallback?: ReactNode;
107
+ }>;
108
+ RequirePermission: ComponentType<{
109
+ resource: R;
110
+ action: Action;
111
+ companyId?: string | null;
112
+ loadingFallback?: ReactNode;
113
+ deniedFallback?: ReactNode;
114
+ children?: ReactNode;
115
+ }>;
116
+ /** The const-asserted registry, re-exported so call-sites can iterate. */
117
+ resources: ResourceRegistry;
118
+ /** All registered resource names as a union — handy for typing
119
+ * application-side data structures. */
120
+ resourceNames: ReadonlyArray<R>;
121
+ }
122
+
123
+ /**
124
+ * Built-in fetchers — adopters can use these or pass their own
125
+ * implementation of `AuthRbacFetcher`.
126
+ */
127
+
128
+ /**
129
+ * Calls the package's SQL function `rbac.user_profile(uuid)` via
130
+ * a Supabase JS client. Easiest path when the host project already
131
+ * uses Supabase.
132
+ *
133
+ * The function lives in the dedicated `rbac` Postgres schema, so the
134
+ * adopter must add `rbac` to their PostgREST exposed-schemas list
135
+ * (Supabase Studio → Settings → API → Exposed schemas) for the
136
+ * `.schema('rbac')` call below to reach it.
137
+ *
138
+ * @example
139
+ * createSupabaseFetcher({ supabase, userId: session.user.id })
140
+ */
141
+ declare function createSupabaseFetcher(opts: {
142
+ supabase: {
143
+ schema: (name: string) => {
144
+ rpc: (fn: string, args: Record<string, unknown>) => Promise<{
145
+ data: unknown;
146
+ error: {
147
+ message: string;
148
+ } | null;
149
+ }>;
150
+ };
151
+ };
152
+ userId: string;
153
+ }): AuthRbacFetcher;
154
+ /**
155
+ * Cheap probe — returns true if the package's `rbac` schema looks
156
+ * reachable. Useful at app start to fail loudly if the migration
157
+ * hasn't been applied OR if `rbac` isn't in the project's PostgREST
158
+ * exposed-schemas list.
159
+ *
160
+ * @example
161
+ * if (!(await detectRbacSchema(supabase))) {
162
+ * console.error("rbac schema not reachable — apply 0001_initial.sql");
163
+ * }
164
+ */
165
+ declare function detectRbacSchema(supabase: {
166
+ schema: (name: string) => {
167
+ rpc: (fn: string, args: Record<string, unknown>) => Promise<{
168
+ data: unknown;
169
+ error: {
170
+ message: string;
171
+ } | null;
172
+ }>;
173
+ };
174
+ }): Promise<boolean>;
175
+ /**
176
+ * Calls a regular HTTP endpoint that returns a `UserProfile` JSON
177
+ * payload. Use this when the host project has its own backend that
178
+ * wraps the package's Python helpers (or any equivalent).
179
+ *
180
+ * @example
181
+ * createHttpFetcher({ url: "/api/users/me/profile" })
182
+ */
183
+ declare function createHttpFetcher(opts: {
184
+ url: string;
185
+ /** Forwarded as-is to `fetch`. Use this to attach auth headers. */
186
+ init?: RequestInit;
187
+ /** Override the global `fetch` if you're in a non-browser env. */
188
+ fetch?: typeof fetch;
189
+ }): AuthRbacFetcher;
190
+
191
+ export { type AuthRbacClient as A, type CanOptions as C, RbacRegistryError as R, type TypedGuards as T, createSupabaseFetcher as a, type AuthRbacClientOptions as b, createHttpFetcher as c, detectRbacSchema as d, buildPermissionResolver as e, groupResources as g };
package/dist/index.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
+ RbacRegistryError: () => RbacRegistryError,
23
24
  buildPermissionResolver: () => buildPermissionResolver,
24
25
  createHttpFetcher: () => createHttpFetcher,
25
26
  createSupabaseFetcher: () => createSupabaseFetcher,
@@ -56,8 +57,32 @@ function buildPermissionResolver(resources, profile, defaultCompanyId) {
56
57
  }
57
58
  return readGrid(membership.permissions, resource, action);
58
59
  };
60
+ const canAccessSection = (resource, action = "read", options) => {
61
+ if (profile.is_super_admin) {
62
+ return true;
63
+ }
64
+ const scope = scopeByResource.get(resource);
65
+ if (!scope) {
66
+ return false;
67
+ }
68
+ if (scope === "system") {
69
+ return readDirect(profile, action, resource);
70
+ }
71
+ const companyId = options?.companyId ?? defaultCompanyId;
72
+ if (!companyId) {
73
+ return false;
74
+ }
75
+ const membership = profile.memberships.find(
76
+ (m) => m.company_id === companyId
77
+ );
78
+ if (!membership) {
79
+ return false;
80
+ }
81
+ return readDirectMembership(membership, action, resource);
82
+ };
59
83
  return {
60
84
  can,
85
+ canAccessSection,
61
86
  /** Permission map for the active (or specified) company. */
62
87
  activePermissions: (companyId) => {
63
88
  const id = companyId ?? defaultCompanyId;
@@ -69,6 +94,14 @@ function buildPermissionResolver(resources, profile, defaultCompanyId) {
69
94
  systemPermissions: () => profile.system_permissions
70
95
  };
71
96
  }
97
+ function readDirect(profile, action, resource) {
98
+ const map = action === "read" ? profile.system_direct_reads : action === "write" ? profile.system_direct_writes : action === "update" ? profile.system_direct_updates : profile.system_direct_deletes;
99
+ return map?.[resource] === true;
100
+ }
101
+ function readDirectMembership(membership, action, resource) {
102
+ const map = action === "read" ? membership.direct_reads : action === "write" ? membership.direct_writes : action === "update" ? membership.direct_updates : membership.direct_deletes;
103
+ return map?.[resource] === true;
104
+ }
72
105
  function readGrid(map, resource, action) {
73
106
  const grid = map[resource];
74
107
  if (!grid) {
@@ -90,6 +123,14 @@ function groupResources(registry) {
90
123
  return order.map((g) => ({ group: g, resources: buckets.get(g) }));
91
124
  }
92
125
 
126
+ // src/define.ts
127
+ var RbacRegistryError = class extends Error {
128
+ constructor(message) {
129
+ super(`auth-rbac: ${message}`);
130
+ this.name = "RbacRegistryError";
131
+ }
132
+ };
133
+
93
134
  // src/fetchers.ts
94
135
  function createSupabaseFetcher(opts) {
95
136
  return {
@@ -154,6 +195,7 @@ function normalizeProfile(raw) {
154
195
  }
155
196
  // Annotate the CommonJS export names for ESM import in node:
156
197
  0 && (module.exports = {
198
+ RbacRegistryError,
157
199
  buildPermissionResolver,
158
200
  createHttpFetcher,
159
201
  createSupabaseFetcher,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/client.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 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 {\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 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","/**\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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC0CO,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;;;AC1GO,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
+ {"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":[]}
package/dist/index.d.cts CHANGED
@@ -1,118 +1,3 @@
1
- import { c as ResourceRegistry, U as UserProfile, A as Action, d as PermissionMap, b as AuthRbacFetcher, a as ResourceDescriptor } from './types-DxvFudPF.cjs';
2
- export { C as CompanyMembership, F as FrontendConfig, P as PermissionGrid, R as ResourceScope, e as RoleSummary } from './types-DxvFudPF.cjs';
3
-
4
- /**
5
- * Transport-agnostic client: turns an adopter-supplied
6
- * `AuthRbacFetcher` into a permission resolver. The React provider
7
- * wraps this; non-React consumers (Node scripts, edge functions)
8
- * can use it directly.
9
- */
10
-
11
- interface AuthRbacClientOptions {
12
- fetcher: AuthRbacFetcher;
13
- /**
14
- * The host project's full resource list. Required so the resolver
15
- * can look up a resource's scope without a DB round-trip per call.
16
- * Re-using the same array the host syncs into the
17
- * `rbac.resources` table at boot keeps everything in lockstep.
18
- */
19
- resources: ResourceRegistry;
20
- }
21
- interface CanOptions {
22
- /**
23
- * Override the active company. Omit to use the company the
24
- * caller has currently activated (the React provider tracks
25
- * this; for direct client use you must pass it).
26
- */
27
- companyId?: string | null;
28
- }
29
- /**
30
- * Pure resolver. Given a hydrated profile it answers boolean
31
- * questions instantly — no I/O. The `resourceMap` is built once at
32
- * construction so per-call work is two map lookups.
33
- */
34
- declare function buildPermissionResolver(resources: ResourceRegistry, profile: UserProfile, defaultCompanyId: string | null): {
35
- can: (resource: string, action: Action, options?: CanOptions) => boolean;
36
- /** Permission map for the active (or specified) company. */
37
- activePermissions: (companyId?: string | null) => PermissionMap;
38
- systemPermissions: () => PermissionMap;
39
- };
40
- /**
41
- * Helper: groups a resource registry by `group` for the matrix UI.
42
- * Returns groups in insertion order with their resources.
43
- */
44
- declare function groupResources(registry: ResourceRegistry): Array<{
45
- group: string;
46
- resources: ResourceDescriptor[];
47
- }>;
48
- type AuthRbacClient = ReturnType<typeof buildPermissionResolver>;
49
-
50
- /**
51
- * Built-in fetchers — adopters can use these or pass their own
52
- * implementation of `AuthRbacFetcher`.
53
- */
54
-
55
- /**
56
- * Calls the package's SQL function `rbac.user_profile(uuid)` via
57
- * a Supabase JS client. Easiest path when the host project already
58
- * uses Supabase.
59
- *
60
- * The function lives in the dedicated `rbac` Postgres schema, so the
61
- * adopter must add `rbac` to their PostgREST exposed-schemas list
62
- * (Supabase Studio → Settings → API → Exposed schemas) for the
63
- * `.schema('rbac')` call below to reach it.
64
- *
65
- * @example
66
- * createSupabaseFetcher({ supabase, userId: session.user.id })
67
- */
68
- declare function createSupabaseFetcher(opts: {
69
- supabase: {
70
- schema: (name: string) => {
71
- rpc: (fn: string, args: Record<string, unknown>) => Promise<{
72
- data: unknown;
73
- error: {
74
- message: string;
75
- } | null;
76
- }>;
77
- };
78
- };
79
- userId: string;
80
- }): AuthRbacFetcher;
81
- /**
82
- * Cheap probe — returns true if the package's `rbac` schema looks
83
- * reachable. Useful at app start to fail loudly if the migration
84
- * hasn't been applied OR if `rbac` isn't in the project's PostgREST
85
- * exposed-schemas list.
86
- *
87
- * @example
88
- * if (!(await detectRbacSchema(supabase))) {
89
- * console.error("rbac schema not reachable — apply 0001_initial.sql");
90
- * }
91
- */
92
- declare function detectRbacSchema(supabase: {
93
- schema: (name: string) => {
94
- rpc: (fn: string, args: Record<string, unknown>) => Promise<{
95
- data: unknown;
96
- error: {
97
- message: string;
98
- } | null;
99
- }>;
100
- };
101
- }): Promise<boolean>;
102
- /**
103
- * Calls a regular HTTP endpoint that returns a `UserProfile` JSON
104
- * payload. Use this when the host project has its own backend that
105
- * wraps the package's Python helpers (or any equivalent).
106
- *
107
- * @example
108
- * createHttpFetcher({ url: "/api/users/me/profile" })
109
- */
110
- declare function createHttpFetcher(opts: {
111
- url: string;
112
- /** Forwarded as-is to `fetch`. Use this to attach auth headers. */
113
- init?: RequestInit;
114
- /** Override the global `fetch` if you're in a non-browser env. */
115
- fetch?: typeof fetch;
116
- }): AuthRbacFetcher;
117
-
118
- export { Action, type AuthRbacClient, AuthRbacFetcher, type CanOptions, type AuthRbacClientOptions as ClientOptions, PermissionMap, ResourceDescriptor, ResourceRegistry, UserProfile, buildPermissionResolver, createHttpFetcher, createSupabaseFetcher, detectRbacSchema, groupResources };
1
+ export { A as Action, b as AuthRbacFetcher, C as CompanyMembership, D as DependencyEdge, d as DirectGrantMap, F as FrontendConfig, P as PermissionGrid, e as PermissionMap, a as ResourceDescriptor, c as ResourceRegistry, R as ResourceScope, f as RoleSummary, U as UserProfile } from './types-Oj9yfWvz.cjs';
2
+ export { A as AuthRbacClient, C as CanOptions, b as ClientOptions, R as RbacRegistryError, e as buildPermissionResolver, c as createHttpFetcher, a as createSupabaseFetcher, d as detectRbacSchema, g as groupResources } from './index-CJqb5nY5.cjs';
3
+ import 'react';
package/dist/index.d.ts CHANGED
@@ -1,118 +1,3 @@
1
- import { c as ResourceRegistry, U as UserProfile, A as Action, d as PermissionMap, b as AuthRbacFetcher, a as ResourceDescriptor } from './types-DxvFudPF.js';
2
- export { C as CompanyMembership, F as FrontendConfig, P as PermissionGrid, R as ResourceScope, e as RoleSummary } from './types-DxvFudPF.js';
3
-
4
- /**
5
- * Transport-agnostic client: turns an adopter-supplied
6
- * `AuthRbacFetcher` into a permission resolver. The React provider
7
- * wraps this; non-React consumers (Node scripts, edge functions)
8
- * can use it directly.
9
- */
10
-
11
- interface AuthRbacClientOptions {
12
- fetcher: AuthRbacFetcher;
13
- /**
14
- * The host project's full resource list. Required so the resolver
15
- * can look up a resource's scope without a DB round-trip per call.
16
- * Re-using the same array the host syncs into the
17
- * `rbac.resources` table at boot keeps everything in lockstep.
18
- */
19
- resources: ResourceRegistry;
20
- }
21
- interface CanOptions {
22
- /**
23
- * Override the active company. Omit to use the company the
24
- * caller has currently activated (the React provider tracks
25
- * this; for direct client use you must pass it).
26
- */
27
- companyId?: string | null;
28
- }
29
- /**
30
- * Pure resolver. Given a hydrated profile it answers boolean
31
- * questions instantly — no I/O. The `resourceMap` is built once at
32
- * construction so per-call work is two map lookups.
33
- */
34
- declare function buildPermissionResolver(resources: ResourceRegistry, profile: UserProfile, defaultCompanyId: string | null): {
35
- can: (resource: string, action: Action, options?: CanOptions) => boolean;
36
- /** Permission map for the active (or specified) company. */
37
- activePermissions: (companyId?: string | null) => PermissionMap;
38
- systemPermissions: () => PermissionMap;
39
- };
40
- /**
41
- * Helper: groups a resource registry by `group` for the matrix UI.
42
- * Returns groups in insertion order with their resources.
43
- */
44
- declare function groupResources(registry: ResourceRegistry): Array<{
45
- group: string;
46
- resources: ResourceDescriptor[];
47
- }>;
48
- type AuthRbacClient = ReturnType<typeof buildPermissionResolver>;
49
-
50
- /**
51
- * Built-in fetchers — adopters can use these or pass their own
52
- * implementation of `AuthRbacFetcher`.
53
- */
54
-
55
- /**
56
- * Calls the package's SQL function `rbac.user_profile(uuid)` via
57
- * a Supabase JS client. Easiest path when the host project already
58
- * uses Supabase.
59
- *
60
- * The function lives in the dedicated `rbac` Postgres schema, so the
61
- * adopter must add `rbac` to their PostgREST exposed-schemas list
62
- * (Supabase Studio → Settings → API → Exposed schemas) for the
63
- * `.schema('rbac')` call below to reach it.
64
- *
65
- * @example
66
- * createSupabaseFetcher({ supabase, userId: session.user.id })
67
- */
68
- declare function createSupabaseFetcher(opts: {
69
- supabase: {
70
- schema: (name: string) => {
71
- rpc: (fn: string, args: Record<string, unknown>) => Promise<{
72
- data: unknown;
73
- error: {
74
- message: string;
75
- } | null;
76
- }>;
77
- };
78
- };
79
- userId: string;
80
- }): AuthRbacFetcher;
81
- /**
82
- * Cheap probe — returns true if the package's `rbac` schema looks
83
- * reachable. Useful at app start to fail loudly if the migration
84
- * hasn't been applied OR if `rbac` isn't in the project's PostgREST
85
- * exposed-schemas list.
86
- *
87
- * @example
88
- * if (!(await detectRbacSchema(supabase))) {
89
- * console.error("rbac schema not reachable — apply 0001_initial.sql");
90
- * }
91
- */
92
- declare function detectRbacSchema(supabase: {
93
- schema: (name: string) => {
94
- rpc: (fn: string, args: Record<string, unknown>) => Promise<{
95
- data: unknown;
96
- error: {
97
- message: string;
98
- } | null;
99
- }>;
100
- };
101
- }): Promise<boolean>;
102
- /**
103
- * Calls a regular HTTP endpoint that returns a `UserProfile` JSON
104
- * payload. Use this when the host project has its own backend that
105
- * wraps the package's Python helpers (or any equivalent).
106
- *
107
- * @example
108
- * createHttpFetcher({ url: "/api/users/me/profile" })
109
- */
110
- declare function createHttpFetcher(opts: {
111
- url: string;
112
- /** Forwarded as-is to `fetch`. Use this to attach auth headers. */
113
- init?: RequestInit;
114
- /** Override the global `fetch` if you're in a non-browser env. */
115
- fetch?: typeof fetch;
116
- }): AuthRbacFetcher;
117
-
118
- export { Action, type AuthRbacClient, AuthRbacFetcher, type CanOptions, type AuthRbacClientOptions as ClientOptions, PermissionMap, ResourceDescriptor, ResourceRegistry, UserProfile, buildPermissionResolver, createHttpFetcher, createSupabaseFetcher, detectRbacSchema, groupResources };
1
+ export { A as Action, b as AuthRbacFetcher, C as CompanyMembership, D as DependencyEdge, d as DirectGrantMap, F as FrontendConfig, P as PermissionGrid, e as PermissionMap, a as ResourceDescriptor, c as ResourceRegistry, R as ResourceScope, f as RoleSummary, U as UserProfile } from './types-Oj9yfWvz.js';
2
+ export { A as AuthRbacClient, C as CanOptions, b as ClientOptions, R as RbacRegistryError, e as buildPermissionResolver, c as createHttpFetcher, a as createSupabaseFetcher, d as detectRbacSchema, g as groupResources } from './index-nfrns9Ye.js';
3
+ import 'react';
package/dist/index.js CHANGED
@@ -1,13 +1,15 @@
1
1
  import {
2
+ RbacRegistryError,
2
3
  createHttpFetcher,
3
4
  createSupabaseFetcher,
4
5
  detectRbacSchema
5
- } from "./chunk-NRDW233A.js";
6
+ } from "./chunk-5UAIIOKT.js";
6
7
  import {
7
8
  buildPermissionResolver,
8
9
  groupResources
9
- } from "./chunk-C76JHCKM.js";
10
+ } from "./chunk-XHPBUCFN.js";
10
11
  export {
12
+ RbacRegistryError,
11
13
  buildPermissionResolver,
12
14
  createHttpFetcher,
13
15
  createSupabaseFetcher,