snipe-auth-rbac 0.1.0 → 0.2.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/bin/cli.mjs +140 -0
- package/dist/admin/index.cjs +30 -12
- package/dist/admin/index.cjs.map +1 -1
- package/dist/admin/index.d.cts +37 -6
- package/dist/admin/index.d.ts +37 -6
- package/dist/admin/index.js +30 -13
- package/dist/admin/index.js.map +1 -1
- package/dist/{chunk-4WTV6J44.js → chunk-C76JHCKM.js} +1 -1
- package/dist/chunk-C76JHCKM.js.map +1 -0
- package/dist/{chunk-BRCJUCDG.js → chunk-NRDW233A.js} +17 -3
- package/dist/chunk-NRDW233A.js.map +1 -0
- package/dist/index.cjs +17 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -7
- package/dist/index.d.ts +35 -7
- package/dist/index.js +5 -3
- package/dist/react/index.cjs +17 -2
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +3 -3
- package/dist/react/index.d.ts +3 -3
- package/dist/react/index.js +5 -3
- package/dist/react/index.js.map +1 -1
- package/dist/{types-BEc5SCIo.d.cts → types-DxvFudPF.d.cts} +1 -1
- package/dist/{types-BEc5SCIo.d.ts → types-DxvFudPF.d.ts} +1 -1
- package/package.json +8 -2
- package/sql/0001_initial.sql +512 -0
- package/sql/0002_seed_defaults.sql +57 -0
- package/dist/chunk-4WTV6J44.js.map +0 -1
- package/dist/chunk-BRCJUCDG.js.map +0 -1
package/dist/admin/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { R as ResourceScope, F as FrontendConfig, a as ResourceDescriptor, A as Action } from '../types-
|
|
1
|
+
import { R as ResourceScope, F as FrontendConfig, a as ResourceDescriptor, A as Action } from '../types-DxvFudPF.js';
|
|
2
2
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -54,7 +54,7 @@ interface AdminMember {
|
|
|
54
54
|
*/
|
|
55
55
|
interface AdminTransport {
|
|
56
56
|
/**
|
|
57
|
-
* Upsert resource descriptors into `
|
|
57
|
+
* Upsert resource descriptors into `rbac.resources`. Call
|
|
58
58
|
* once at app boot (or behind a SuperAdmin button) so the
|
|
59
59
|
* permission matrix UI mirrors the host's typed resource
|
|
60
60
|
* registry. Returns the number of rows upserted.
|
|
@@ -82,6 +82,16 @@ interface AdminTransport {
|
|
|
82
82
|
action: Action;
|
|
83
83
|
value: boolean;
|
|
84
84
|
}): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Materialise `rbac.role_permissions` rows from a template role's
|
|
87
|
+
* `default_permissions` JSONB pattern. Calls the SQL function
|
|
88
|
+
* `rbac.apply_template_defaults(role_id, only_missing)`. Returns
|
|
89
|
+
* the number of rows the function reports as upserted.
|
|
90
|
+
*/
|
|
91
|
+
applyTemplateDefaults(args: {
|
|
92
|
+
role_id: string;
|
|
93
|
+
only_missing?: boolean;
|
|
94
|
+
}): Promise<number>;
|
|
85
95
|
listCompanies(): Promise<AdminCompany[]>;
|
|
86
96
|
createCompany(input: {
|
|
87
97
|
name: string;
|
|
@@ -105,15 +115,19 @@ interface AdminTransport {
|
|
|
105
115
|
|
|
106
116
|
/**
|
|
107
117
|
* Default Supabase implementation of the admin transport. Hits the
|
|
108
|
-
* package's tables
|
|
109
|
-
* endpoint for invites.
|
|
118
|
+
* package's tables in the `rbac` schema directly via `.schema('rbac').
|
|
119
|
+
* from(...)` and the auth admin endpoint for invites.
|
|
120
|
+
*
|
|
121
|
+
* Adopters must add `rbac` to their PostgREST exposed-schemas list
|
|
122
|
+
* (Supabase Studio → Settings → API → Exposed schemas) for these
|
|
123
|
+
* calls to reach the tables.
|
|
110
124
|
*
|
|
111
125
|
* Projects that route admin writes through their own backend
|
|
112
126
|
* (e.g. for audit logging or extra validation) skip this and
|
|
113
127
|
* implement `AdminTransport` themselves.
|
|
114
128
|
*/
|
|
115
129
|
|
|
116
|
-
interface
|
|
130
|
+
interface RbacSchemaClient {
|
|
117
131
|
from(table: string): {
|
|
118
132
|
select: (cols: string) => {
|
|
119
133
|
eq: (col: string, value: unknown) => any;
|
|
@@ -145,6 +159,15 @@ interface SupabaseAdmin {
|
|
|
145
159
|
eq: (col: string, value: unknown) => any;
|
|
146
160
|
};
|
|
147
161
|
};
|
|
162
|
+
rpc(fn: string, args: Record<string, unknown>): Promise<{
|
|
163
|
+
data: unknown;
|
|
164
|
+
error: {
|
|
165
|
+
message: string;
|
|
166
|
+
} | null;
|
|
167
|
+
}>;
|
|
168
|
+
}
|
|
169
|
+
interface SupabaseAdmin {
|
|
170
|
+
schema(name: string): RbacSchemaClient;
|
|
148
171
|
auth: {
|
|
149
172
|
admin: {
|
|
150
173
|
inviteUserByEmail: (email: string, opts?: {
|
|
@@ -230,6 +253,14 @@ declare function useSetRolePermissionCell(): {
|
|
|
230
253
|
value: boolean;
|
|
231
254
|
}) => Promise<void>;
|
|
232
255
|
};
|
|
256
|
+
declare function useApplyTemplateDefaults(): {
|
|
257
|
+
isPending: boolean;
|
|
258
|
+
error: Error | null;
|
|
259
|
+
mutate: (args: {
|
|
260
|
+
role_id: string;
|
|
261
|
+
only_missing?: boolean;
|
|
262
|
+
}) => Promise<number>;
|
|
263
|
+
};
|
|
233
264
|
declare function useCreateCompany(): {
|
|
234
265
|
isPending: boolean;
|
|
235
266
|
error: Error | null;
|
|
@@ -343,4 +374,4 @@ interface InviteMemberFormProps {
|
|
|
343
374
|
}
|
|
344
375
|
declare function InviteMemberForm(props: InviteMemberFormProps): react_jsx_runtime.JSX.Element;
|
|
345
376
|
|
|
346
|
-
export { type AdminCompany, type AdminMember, type AdminRole, type AdminRolePermission, type AdminTransport, AdminTransportProvider, type AdminTransportProviderProps, InviteMemberForm, type InviteMemberFormProps, type InviteMemberFormRenderArgs, type MatrixGroup, type MatrixRenderArgs, PermissionsMatrix, type PermissionsMatrixProps, type RolePermissionGrid, RolesList, type RolesListProps, type RolesListRenderArgs, type SupabaseAdminClientOptions, createSupabaseAdminClient, useAdminCompanies, useAdminCompanyMembers, useAdminRolePermissions, useAdminRoles, useCreateCompany, useCreateRole, useDeleteRole, useInviteCompanyMember, useRolePermissionGrid, useSetRolePermissionCell, useUpdateRole };
|
|
377
|
+
export { type AdminCompany, type AdminMember, type AdminRole, type AdminRolePermission, type AdminTransport, AdminTransportProvider, type AdminTransportProviderProps, InviteMemberForm, type InviteMemberFormProps, type InviteMemberFormRenderArgs, type MatrixGroup, type MatrixRenderArgs, PermissionsMatrix, type PermissionsMatrixProps, type RolePermissionGrid, RolesList, type RolesListProps, type RolesListRenderArgs, type SupabaseAdminClientOptions, createSupabaseAdminClient, useAdminCompanies, useAdminCompanyMembers, useAdminRolePermissions, useAdminRoles, useApplyTemplateDefaults, useCreateCompany, useCreateRole, useDeleteRole, useInviteCompanyMember, useRolePermissionGrid, useSetRolePermissionCell, useUpdateRole };
|
package/dist/admin/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
groupResources
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-C76JHCKM.js";
|
|
4
4
|
|
|
5
5
|
// src/admin/transport.ts
|
|
6
6
|
var ACTION_COLUMN = {
|
|
@@ -11,6 +11,7 @@ var ACTION_COLUMN = {
|
|
|
11
11
|
};
|
|
12
12
|
function createSupabaseAdminClient(opts) {
|
|
13
13
|
const sb = opts.supabase;
|
|
14
|
+
const rbac = sb.schema("rbac");
|
|
14
15
|
return {
|
|
15
16
|
async syncResources(resources) {
|
|
16
17
|
if (resources.length === 0) {
|
|
@@ -23,14 +24,14 @@ function createSupabaseAdminClient(opts) {
|
|
|
23
24
|
description: r.description ?? null,
|
|
24
25
|
group_label: r.group ?? null
|
|
25
26
|
}));
|
|
26
|
-
const { error } = await
|
|
27
|
+
const { error } = await rbac.from("resources").upsert(payload, { onConflict: "resource" });
|
|
27
28
|
if (error) {
|
|
28
29
|
throw new Error(`syncResources: ${error.message}`);
|
|
29
30
|
}
|
|
30
31
|
return resources.length;
|
|
31
32
|
},
|
|
32
33
|
async listRoles({ scope, companyId, templatesOnly }) {
|
|
33
|
-
let q =
|
|
34
|
+
let q = rbac.from("roles").select("*").eq("scope", scope);
|
|
34
35
|
if (templatesOnly) {
|
|
35
36
|
q = q.is("company_id", null);
|
|
36
37
|
} else if (companyId !== void 0) {
|
|
@@ -43,7 +44,7 @@ function createSupabaseAdminClient(opts) {
|
|
|
43
44
|
return data ?? [];
|
|
44
45
|
},
|
|
45
46
|
async listRolePermissions(roleId) {
|
|
46
|
-
const { data, error } = await
|
|
47
|
+
const { data, error } = await rbac.from("role_permissions").select("*").eq("role_id", roleId);
|
|
47
48
|
if (error) {
|
|
48
49
|
throw new Error(`listRolePermissions: ${error.message}`);
|
|
49
50
|
}
|
|
@@ -57,21 +58,21 @@ function createSupabaseAdminClient(opts) {
|
|
|
57
58
|
description: input.description ?? null,
|
|
58
59
|
frontend_config: input.frontend_config ?? {}
|
|
59
60
|
};
|
|
60
|
-
const { data, error } = await
|
|
61
|
+
const { data, error } = await rbac.from("roles").insert(row).select("*").single();
|
|
61
62
|
if (error) {
|
|
62
63
|
throw new Error(`createRole: ${error.message}`);
|
|
63
64
|
}
|
|
64
65
|
return data;
|
|
65
66
|
},
|
|
66
67
|
async updateRole(id, patch) {
|
|
67
|
-
const { data, error } = await
|
|
68
|
+
const { data, error } = await rbac.from("roles").update(patch).eq("id", id).select("*").single();
|
|
68
69
|
if (error) {
|
|
69
70
|
throw new Error(`updateRole: ${error.message}`);
|
|
70
71
|
}
|
|
71
72
|
return data;
|
|
72
73
|
},
|
|
73
74
|
async deleteRole(id) {
|
|
74
|
-
const { error } = await
|
|
75
|
+
const { error } = await rbac.from("roles").delete().eq("id", id);
|
|
75
76
|
if (error) {
|
|
76
77
|
throw new Error(`deleteRole: ${error.message}`);
|
|
77
78
|
}
|
|
@@ -83,20 +84,31 @@ function createSupabaseAdminClient(opts) {
|
|
|
83
84
|
resource,
|
|
84
85
|
[column]: value
|
|
85
86
|
};
|
|
86
|
-
const { error } = await
|
|
87
|
+
const { error } = await rbac.from("role_permissions").upsert(row, { onConflict: "role_id,resource" });
|
|
87
88
|
if (error) {
|
|
88
89
|
throw new Error(`setRolePermissionCell: ${error.message}`);
|
|
89
90
|
}
|
|
90
91
|
},
|
|
92
|
+
async applyTemplateDefaults({ role_id, only_missing = true }) {
|
|
93
|
+
const { data, error } = await rbac.rpc("apply_template_defaults", {
|
|
94
|
+
p_role_id: role_id,
|
|
95
|
+
p_only_missing: only_missing
|
|
96
|
+
});
|
|
97
|
+
if (error) {
|
|
98
|
+
throw new Error(`applyTemplateDefaults: ${error.message}`);
|
|
99
|
+
}
|
|
100
|
+
if (typeof data === "number") return data;
|
|
101
|
+
return Number(data ?? 0);
|
|
102
|
+
},
|
|
91
103
|
async listCompanies() {
|
|
92
|
-
const { data, error } = await
|
|
104
|
+
const { data, error } = await rbac.from("companies").select("*").order("name", { ascending: true });
|
|
93
105
|
if (error) {
|
|
94
106
|
throw new Error(`listCompanies: ${error.message}`);
|
|
95
107
|
}
|
|
96
108
|
return data ?? [];
|
|
97
109
|
},
|
|
98
110
|
async createCompany(input) {
|
|
99
|
-
const { data, error } = await
|
|
111
|
+
const { data, error } = await rbac.from("companies").insert({
|
|
100
112
|
name: input.name,
|
|
101
113
|
slug: input.slug ?? null,
|
|
102
114
|
type: input.type ?? null
|
|
@@ -107,7 +119,7 @@ function createSupabaseAdminClient(opts) {
|
|
|
107
119
|
return data;
|
|
108
120
|
},
|
|
109
121
|
async listCompanyMembers(companyId) {
|
|
110
|
-
const { data, error } = await
|
|
122
|
+
const { data, error } = await rbac.from("user_company_roles").select("user_id, role_id, assigned_at").eq("company_id", companyId);
|
|
111
123
|
if (error) {
|
|
112
124
|
throw new Error(`listCompanyMembers: ${error.message}`);
|
|
113
125
|
}
|
|
@@ -132,8 +144,8 @@ function createSupabaseAdminClient(opts) {
|
|
|
132
144
|
async inviteCompanyMember({ companyId, email, roleIds }) {
|
|
133
145
|
const { error } = await sb.auth.admin.inviteUserByEmail(email, {
|
|
134
146
|
data: {
|
|
135
|
-
|
|
136
|
-
|
|
147
|
+
rbac_company_id: companyId,
|
|
148
|
+
rbac_role_ids: roleIds
|
|
137
149
|
},
|
|
138
150
|
redirectTo: opts.inviteRedirectUrl
|
|
139
151
|
});
|
|
@@ -249,6 +261,10 @@ function useSetRolePermissionCell() {
|
|
|
249
261
|
const transport = useAdminTransport();
|
|
250
262
|
return useMutation(transport.setRolePermissionCell);
|
|
251
263
|
}
|
|
264
|
+
function useApplyTemplateDefaults() {
|
|
265
|
+
const transport = useAdminTransport();
|
|
266
|
+
return useMutation(transport.applyTemplateDefaults);
|
|
267
|
+
}
|
|
252
268
|
function useCreateCompany() {
|
|
253
269
|
const transport = useAdminTransport();
|
|
254
270
|
return useMutation(transport.createCompany);
|
|
@@ -449,6 +465,7 @@ export {
|
|
|
449
465
|
useAdminCompanyMembers,
|
|
450
466
|
useAdminRolePermissions,
|
|
451
467
|
useAdminRoles,
|
|
468
|
+
useApplyTemplateDefaults,
|
|
452
469
|
useCreateCompany,
|
|
453
470
|
useCreateRole,
|
|
454
471
|
useDeleteRole,
|
package/dist/admin/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/admin/transport.ts","../../src/admin/hooks.tsx","../../src/admin/PermissionsMatrix.tsx","../../src/admin/RolesList.tsx","../../src/admin/InviteMemberForm.tsx"],"sourcesContent":["/**\n * Default Supabase implementation of the admin transport. Hits the\n * package's tables directly via `from(...)` and the auth admin\n * endpoint for invites.\n *\n * Projects that route admin writes through their own backend\n * (e.g. for audit logging or extra validation) skip this and\n * implement `AdminTransport` themselves.\n */\n\nimport type { Action, ResourceDescriptor } from \"../types.js\";\n\nimport type {\n AdminCompany,\n AdminMember,\n AdminRole,\n AdminRolePermission,\n AdminTransport,\n} from \"./types.js\";\n\ninterface SupabaseAdmin {\n from(table: string): {\n select: (cols: string) => {\n eq: (col: string, value: unknown) => any;\n is: (col: string, value: unknown) => any;\n order: (col: string, opts?: { ascending: boolean }) => any;\n };\n insert: (row: Record<string, unknown>) => {\n select: (cols: string) => { single: () => any };\n };\n update: (patch: Record<string, unknown>) => {\n eq: (col: string, value: unknown) => {\n select: (cols: string) => { single: () => any };\n };\n };\n upsert: (\n row: Record<string, unknown> | Array<Record<string, unknown>>,\n opts?: { onConflict: string },\n ) => Promise<{ error: { message: string } | null }>;\n delete: () => { eq: (col: string, value: unknown) => any };\n };\n auth: {\n admin: {\n inviteUserByEmail: (\n email: string,\n opts?: { data?: Record<string, unknown>; redirectTo?: string },\n ) => Promise<{ data: unknown; error: { message: string } | null }>;\n };\n };\n}\n\nexport interface SupabaseAdminClientOptions {\n supabase: SupabaseAdmin;\n /** Where the invitee should land after setting their password. */\n inviteRedirectUrl?: string;\n}\n\nconst ACTION_COLUMN: Record<Action, string> = {\n read: \"can_read\",\n write: \"can_write\",\n update: \"can_update\",\n delete: \"can_delete\",\n};\n\nexport function createSupabaseAdminClient(\n opts: SupabaseAdminClientOptions,\n): AdminTransport {\n const sb = opts.supabase;\n\n return {\n async syncResources(resources) {\n if (resources.length === 0) {\n return 0;\n }\n const payload = resources.map((r) => ({\n resource: r.resource,\n scope: r.scope,\n label: r.label,\n description: r.description ?? null,\n group_label: r.group ?? null,\n }));\n const { error } = await sb\n .from(\"auth_rbac_resources\")\n .upsert(payload, { onConflict: \"resource\" });\n if (error) {\n throw new Error(`syncResources: ${error.message}`);\n }\n return resources.length;\n },\n\n async listRoles({ scope, companyId, templatesOnly }) {\n let q = sb\n .from(\"auth_rbac_roles\")\n .select(\"*\")\n .eq(\"scope\", scope);\n if (templatesOnly) {\n q = q.is(\"company_id\", null);\n } else if (companyId !== undefined) {\n q = companyId === null ? q.is(\"company_id\", null) : q.eq(\"company_id\", companyId);\n }\n const { data, error } = await q.order(\"name\", { ascending: true });\n if (error) {\n throw new Error(`listRoles: ${error.message}`);\n }\n return (data ?? []) as AdminRole[];\n },\n\n async listRolePermissions(roleId) {\n const { data, error } = await sb\n .from(\"auth_rbac_role_permissions\")\n .select(\"*\")\n .eq(\"role_id\", roleId);\n if (error) {\n throw new Error(`listRolePermissions: ${error.message}`);\n }\n return (data ?? []) as AdminRolePermission[];\n },\n\n async createRole(input) {\n const row = {\n scope: input.scope,\n company_id: input.companyId ?? null,\n name: input.name,\n description: input.description ?? null,\n frontend_config: input.frontend_config ?? {},\n };\n const { data, error } = await sb\n .from(\"auth_rbac_roles\")\n .insert(row)\n .select(\"*\")\n .single();\n if (error) {\n throw new Error(`createRole: ${error.message}`);\n }\n return data as AdminRole;\n },\n\n async updateRole(id, patch) {\n const { data, error } = await sb\n .from(\"auth_rbac_roles\")\n .update(patch)\n .eq(\"id\", id)\n .select(\"*\")\n .single();\n if (error) {\n throw new Error(`updateRole: ${error.message}`);\n }\n return data as AdminRole;\n },\n\n async deleteRole(id) {\n const { error } = await sb\n .from(\"auth_rbac_roles\")\n .delete()\n .eq(\"id\", id);\n if (error) {\n throw new Error(`deleteRole: ${error.message}`);\n }\n },\n\n async setRolePermissionCell({ role_id, resource, action, value }) {\n const column = ACTION_COLUMN[action];\n const row: Record<string, unknown> = {\n role_id,\n resource,\n [column]: value,\n };\n const { error } = await sb\n .from(\"auth_rbac_role_permissions\")\n .upsert(row, { onConflict: \"role_id,resource\" });\n if (error) {\n throw new Error(`setRolePermissionCell: ${error.message}`);\n }\n },\n\n async listCompanies() {\n const { data, error } = await sb\n .from(\"auth_rbac_companies\")\n .select(\"*\")\n .order(\"name\", { ascending: true });\n if (error) {\n throw new Error(`listCompanies: ${error.message}`);\n }\n return (data ?? []) as AdminCompany[];\n },\n\n async createCompany(input) {\n const { data, error } = await sb\n .from(\"auth_rbac_companies\")\n .insert({\n name: input.name,\n slug: input.slug ?? null,\n type: input.type ?? null,\n })\n .select(\"*\")\n .single();\n if (error) {\n throw new Error(`createCompany: ${error.message}`);\n }\n return data as AdminCompany;\n },\n\n async listCompanyMembers(companyId) {\n // The package doesn't ship a view that joins users + invitations\n // out of the box because the host's auth.users schema may differ.\n // Adopters that need a richer join replace this with their own\n // transport. Fallback: list raw assignments.\n const { data, error } = await sb\n .from(\"auth_rbac_user_company_roles\")\n .select(\"user_id, role_id, assigned_at\")\n .eq(\"company_id\", companyId);\n if (error) {\n throw new Error(`listCompanyMembers: ${error.message}`);\n }\n const grouped = new Map<string, AdminMember>();\n for (const row of (data ?? []) as Array<{\n user_id: string;\n role_id: string;\n assigned_at: string;\n }>) {\n const existing = grouped.get(row.user_id);\n if (existing) {\n existing.role_ids.push(row.role_id);\n } else {\n grouped.set(row.user_id, {\n user_id: row.user_id,\n email: null,\n full_name: null,\n role_ids: [row.role_id],\n invited_at: row.assigned_at,\n invitation_status: \"accepted\",\n });\n }\n }\n return Array.from(grouped.values());\n },\n\n async inviteCompanyMember({ companyId, email, roleIds }) {\n const { error } = await sb.auth.admin.inviteUserByEmail(email, {\n data: {\n auth_rbac_company_id: companyId,\n auth_rbac_role_ids: roleIds,\n },\n redirectTo: opts.inviteRedirectUrl,\n });\n if (error) {\n throw new Error(`inviteCompanyMember: ${error.message}`);\n }\n return { invited: true };\n },\n };\n}\n","/**\n * React hooks for the admin surface. UI-kit-agnostic — adopters\n * render whatever JSX they like with the data + mutations these\n * expose. A copy-paste reference page styled with Tailwind primitives\n * lives in `examples/react-admin/`.\n *\n * Pattern: each hook returns `{ data, isLoading, error, refresh }`\n * and where applicable `{ mutate }`. We deliberately avoid pulling in\n * react-query as a dependency so the package stays peer-light;\n * adopters that already use react-query can wrap these primitives\n * with an extra hook of their own (5 lines).\n */\n\nimport { createContext, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\n\nimport type { Action, FrontendConfig, ResourceScope } from \"../types.js\";\n\nimport type {\n AdminCompany,\n AdminMember,\n AdminRole,\n AdminRolePermission,\n AdminTransport,\n} from \"./types.js\";\n\n// ─────────────────────────────────────────────────────────────────\n// Context — adopter mounts <AdminTransportProvider> once\n// ─────────────────────────────────────────────────────────────────\n\nconst AdminTransportContext = createContext<AdminTransport | null>(null);\n\nexport interface AdminTransportProviderProps {\n transport: AdminTransport;\n children: React.ReactNode;\n}\n\nexport function AdminTransportProvider(props: AdminTransportProviderProps) {\n return (\n <AdminTransportContext.Provider value={props.transport}>\n {props.children}\n </AdminTransportContext.Provider>\n );\n}\n\nfunction useAdminTransport(): AdminTransport {\n const t = useContext(AdminTransportContext);\n if (!t) {\n throw new Error(\n \"auth-rbac admin hooks require <AdminTransportProvider> — wrap your admin pages with one.\",\n );\n }\n return t;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Tiny generic async-state helper. Avoids reinventing react-query\n// while keeping the boilerplate per-hook to a single line.\n// ─────────────────────────────────────────────────────────────────\n\ninterface AsyncState<T> {\n data: T | null;\n isLoading: boolean;\n error: Error | null;\n}\n\nfunction useAsync<T>(loader: () => Promise<T>, deps: ReadonlyArray<unknown>) {\n const [state, setState] = useState<AsyncState<T>>({\n data: null,\n isLoading: true,\n error: null,\n });\n\n const refresh = useCallback(async () => {\n setState((s) => ({ ...s, isLoading: true, error: null }));\n try {\n const data = await loader();\n setState({ data, isLoading: false, error: null });\n } catch (e) {\n setState({\n data: null,\n isLoading: false,\n error: e instanceof Error ? e : new Error(String(e)),\n });\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n\n useEffect(() => {\n void refresh();\n }, [refresh]);\n\n return { ...state, refresh };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Reads\n// ─────────────────────────────────────────────────────────────────\n\nexport function useAdminRoles(args: {\n scope: ResourceScope;\n companyId?: string | null;\n templatesOnly?: boolean;\n}) {\n const transport = useAdminTransport();\n return useAsync(\n () => transport.listRoles(args),\n [transport, args.scope, args.companyId, args.templatesOnly],\n );\n}\n\nexport function useAdminRolePermissions(roleId: string | null) {\n const transport = useAdminTransport();\n return useAsync(\n async () =>\n roleId == null ? [] : transport.listRolePermissions(roleId),\n [transport, roleId],\n );\n}\n\nexport function useAdminCompanies() {\n const transport = useAdminTransport();\n return useAsync(() => transport.listCompanies(), [transport]);\n}\n\nexport function useAdminCompanyMembers(companyId: string | null) {\n const transport = useAdminTransport();\n return useAsync(\n async () =>\n companyId == null ? [] : transport.listCompanyMembers(companyId),\n [transport, companyId],\n );\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Mutations — return `{ mutate, isPending, error }`. Adopters wrap\n// these in their own toast / error-boundary as needed.\n// ─────────────────────────────────────────────────────────────────\n\ninterface MutationState {\n isPending: boolean;\n error: Error | null;\n}\n\nfunction useMutation<TArgs extends unknown[], TResult>(\n fn: (...args: TArgs) => Promise<TResult>,\n) {\n const [state, setState] = useState<MutationState>({\n isPending: false,\n error: null,\n });\n\n const mutate = useCallback(\n async (...args: TArgs): Promise<TResult> => {\n setState({ isPending: true, error: null });\n try {\n const result = await fn(...args);\n setState({ isPending: false, error: null });\n return result;\n } catch (e) {\n const err = e instanceof Error ? e : new Error(String(e));\n setState({ isPending: false, error: err });\n throw err;\n }\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [fn],\n );\n\n return { mutate, ...state };\n}\n\nexport function useCreateRole() {\n const transport = useAdminTransport();\n return useMutation(transport.createRole);\n}\n\nexport function useUpdateRole() {\n const transport = useAdminTransport();\n return useMutation(transport.updateRole);\n}\n\nexport function useDeleteRole() {\n const transport = useAdminTransport();\n return useMutation(transport.deleteRole);\n}\n\nexport function useSetRolePermissionCell() {\n const transport = useAdminTransport();\n return useMutation(transport.setRolePermissionCell);\n}\n\nexport function useCreateCompany() {\n const transport = useAdminTransport();\n return useMutation(transport.createCompany);\n}\n\nexport function useInviteCompanyMember() {\n const transport = useAdminTransport();\n return useMutation(transport.inviteCompanyMember);\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Convenience: hold a role's full state (role + permission grid)\n// in one hook, with a `setCell` mutator that optimistically updates\n// the local cache and writes through to the transport.\n// ─────────────────────────────────────────────────────────────────\n\nexport interface RolePermissionGrid {\n // resource → action → boolean\n [resource: string]: { [A in Action]: boolean };\n}\n\nexport function useRolePermissionGrid(roleId: string | null) {\n const { data, isLoading, error, refresh } = useAdminRolePermissions(roleId);\n const setCell = useSetRolePermissionCell();\n\n const grid = useMemo<RolePermissionGrid>(() => {\n const out: RolePermissionGrid = {};\n for (const row of data ?? []) {\n out[row.resource] = {\n read: row.can_read,\n write: row.can_write,\n update: row.can_update,\n delete: row.can_delete,\n };\n }\n return out;\n }, [data]);\n\n const updateCell = useCallback(\n async (resource: string, action: Action, value: boolean) => {\n if (!roleId) {\n return;\n }\n await setCell.mutate({ role_id: roleId, resource, action, value });\n void refresh();\n },\n [roleId, setCell, refresh],\n );\n\n return {\n grid,\n isLoading,\n error,\n refresh,\n updateCell,\n isUpdating: setCell.isPending,\n updateError: setCell.error,\n };\n}\n","/**\n * Headless permissions matrix.\n *\n * Owns:\n * - reading the role's current permission grid\n * - debounced write-through on every cell toggle\n * - grouping resources by `group` for a sectioned UI\n *\n * Owns NOTHING about styling — the consumer renders all JSX via the\n * single `children` render-prop. A copy-paste reference styled with\n * Tailwind + Radix lives in `examples/react-admin/`.\n *\n * @example minimum viable adoption\n *\n * <PermissionsMatrix\n * roleId={role.id}\n * resources={resources.filter(r => r.scope === role.scope)}\n * >\n * {({ groups, isCellEnabled, setCell, isLoading }) =>\n * groups.map((g) => (\n * <section key={g.group}>\n * <h3>{g.group}</h3>\n * {g.resources.map((r) => (\n * <div key={r.resource}>\n * <span>{r.label}</span>\n * {([\"read\", \"write\", \"update\", \"delete\"] as const).map((a) => (\n * <input\n * key={a}\n * type=\"checkbox\"\n * checked={isCellEnabled(r.resource, a)}\n * disabled={isLoading}\n * onChange={(e) => setCell(r.resource, a, e.target.checked)}\n * />\n * ))}\n * </div>\n * ))}\n * </section>\n * ))\n * }\n * </PermissionsMatrix>\n */\n\nimport { useMemo } from \"react\";\n\nimport type {\n Action,\n ResourceDescriptor,\n} from \"../types.js\";\nimport { groupResources } from \"../client.js\";\n\nimport { useRolePermissionGrid } from \"./hooks.js\";\n\nexport interface MatrixGroup {\n group: string;\n resources: ResourceDescriptor[];\n}\n\nexport interface MatrixRenderArgs {\n /** Resources grouped by their `group` label, original insertion order. */\n groups: MatrixGroup[];\n /** Read a single cell from the current grid. */\n isCellEnabled: (resource: string, action: Action) => boolean;\n /** Write a single cell. Optimistic in the local cache + writes through. */\n setCell: (resource: string, action: Action, value: boolean) => Promise<void>;\n isLoading: boolean;\n isUpdating: boolean;\n error: Error | null;\n /** All four actions, exposed for the consumer to render headers. */\n actions: ReadonlyArray<Action>;\n}\n\nexport interface PermissionsMatrixProps {\n roleId: string | null;\n resources: ReadonlyArray<ResourceDescriptor>;\n children: (args: MatrixRenderArgs) => React.ReactNode;\n}\n\nconst ACTIONS = [\"read\", \"write\", \"update\", \"delete\"] as const;\n\nexport function PermissionsMatrix(props: PermissionsMatrixProps) {\n const { grid, isLoading, error, updateCell, isUpdating } =\n useRolePermissionGrid(props.roleId);\n\n const groups = useMemo<MatrixGroup[]>(\n () => groupResources(props.resources),\n [props.resources],\n );\n\n const isCellEnabled = (resource: string, action: Action): boolean => {\n return grid[resource]?.[action] ?? false;\n };\n\n const setCell = async (resource: string, action: Action, value: boolean) => {\n await updateCell(resource, action, value);\n };\n\n return (\n <>\n {props.children({\n groups,\n isCellEnabled,\n setCell,\n isLoading,\n isUpdating,\n error,\n actions: ACTIONS,\n })}\n </>\n );\n}\n","/**\n * Headless roles-list controller. Tracks selection + create/delete\n * mutations; consumer renders the list, the new-role dialog, and\n * the destructive-action confirmation.\n */\n\nimport { useCallback, useState } from \"react\";\n\nimport type { ResourceScope } from \"../types.js\";\n\nimport {\n useAdminRoles,\n useCreateRole,\n useDeleteRole,\n} from \"./hooks.js\";\nimport type { AdminRole } from \"./types.js\";\n\nexport interface RolesListRenderArgs {\n roles: AdminRole[];\n isLoading: boolean;\n error: Error | null;\n\n selectedRoleId: string | null;\n selectRole: (id: string | null) => void;\n\n createRole: (input: {\n name: string;\n description?: string;\n }) => Promise<AdminRole>;\n isCreating: boolean;\n createError: Error | null;\n\n deleteRole: (id: string) => Promise<void>;\n isDeleting: boolean;\n deleteError: Error | null;\n\n refresh: () => Promise<void>;\n}\n\nexport interface RolesListProps {\n scope: ResourceScope;\n /** Required for company-scope. Pass `null` for templates. */\n companyId?: string | null;\n /** Pre-select the first role on load. Default: true. */\n autoSelectFirst?: boolean;\n children: (args: RolesListRenderArgs) => React.ReactNode;\n}\n\nexport function RolesList(props: RolesListProps) {\n const { scope, companyId, autoSelectFirst = true } = props;\n\n const list = useAdminRoles({ scope, companyId });\n const create = useCreateRole();\n const remove = useDeleteRole();\n\n const [selectedRoleId, setSelectedRoleId] = useState<string | null>(null);\n\n // Auto-select first role on load.\n if (\n autoSelectFirst &&\n selectedRoleId == null &&\n list.data != null &&\n list.data.length > 0\n ) {\n setSelectedRoleId(list.data[0]!.id);\n }\n\n const createRole = useCallback(\n async (input: { name: string; description?: string }) => {\n const role = await create.mutate({\n scope,\n companyId: companyId ?? null,\n name: input.name,\n description: input.description,\n });\n await list.refresh();\n setSelectedRoleId(role.id);\n return role;\n },\n [create, scope, companyId, list],\n );\n\n const deleteRole = useCallback(\n async (id: string) => {\n await remove.mutate(id);\n if (selectedRoleId === id) {\n setSelectedRoleId(null);\n }\n await list.refresh();\n },\n [remove, list, selectedRoleId],\n );\n\n return (\n <>\n {props.children({\n roles: list.data ?? [],\n isLoading: list.isLoading,\n error: list.error,\n selectedRoleId,\n selectRole: setSelectedRoleId,\n createRole,\n isCreating: create.isPending,\n createError: create.error,\n deleteRole,\n isDeleting: remove.isPending,\n deleteError: remove.error,\n refresh: list.refresh,\n })}\n </>\n );\n}\n","/**\n * Headless invite-member form state. Tracks email + selected role\n * ids, runs basic local validation, and exposes a submit handler\n * that calls the configured transport (Supabase Auth invite by\n * default).\n */\n\nimport { useCallback, useState } from \"react\";\n\nimport { useAdminRoles, useInviteCompanyMember } from \"./hooks.js\";\nimport type { AdminRole } from \"./types.js\";\n\nexport interface InviteMemberFormRenderArgs {\n // form state\n email: string;\n setEmail: (v: string) => void;\n selectedRoleIds: Set<string>;\n toggleRole: (roleId: string) => void;\n resetForm: () => void;\n\n // catalog\n roles: AdminRole[];\n rolesLoading: boolean;\n rolesError: Error | null;\n\n // submission\n submit: () => Promise<void>;\n isSubmitting: boolean;\n submitError: Error | null;\n submittedSuccessfully: boolean;\n\n // validation\n isValid: boolean;\n errors: { email?: string; roles?: string };\n}\n\nexport interface InviteMemberFormProps {\n companyId: string;\n /** Called after a successful invite — typically clears a dialog. */\n onSuccess?: () => void;\n children: (args: InviteMemberFormRenderArgs) => React.ReactNode;\n}\n\nexport function InviteMemberForm(props: InviteMemberFormProps) {\n const rolesQuery = useAdminRoles({\n scope: \"company\",\n companyId: props.companyId,\n });\n const invite = useInviteCompanyMember();\n\n const [email, setEmail] = useState(\"\");\n const [selectedRoleIds, setSelectedRoleIds] = useState<Set<string>>(\n new Set(),\n );\n const [submittedSuccessfully, setSubmittedSuccessfully] = useState(false);\n\n const toggleRole = useCallback((roleId: string) => {\n setSelectedRoleIds((prev) => {\n const next = new Set(prev);\n if (next.has(roleId)) {\n next.delete(roleId);\n } else {\n next.add(roleId);\n }\n return next;\n });\n }, []);\n\n const resetForm = useCallback(() => {\n setEmail(\"\");\n setSelectedRoleIds(new Set());\n setSubmittedSuccessfully(false);\n }, []);\n\n const errors: InviteMemberFormRenderArgs[\"errors\"] = {};\n if (email.trim() && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email.trim())) {\n errors.email = \"Bitte gib eine gültige E-Mail-Adresse ein.\";\n }\n if (selectedRoleIds.size === 0) {\n errors.roles = \"Bitte mindestens eine Rolle auswählen.\";\n }\n const isValid =\n email.trim().length > 0 &&\n Object.keys(errors).length === 0;\n\n const submit = useCallback(async () => {\n if (!isValid) {\n return;\n }\n await invite.mutate({\n companyId: props.companyId,\n email: email.trim(),\n roleIds: Array.from(selectedRoleIds),\n });\n setSubmittedSuccessfully(true);\n props.onSuccess?.();\n }, [invite, props, email, selectedRoleIds, isValid]);\n\n return (\n <>\n {props.children({\n email,\n setEmail,\n selectedRoleIds,\n toggleRole,\n resetForm,\n roles: rolesQuery.data ?? [],\n rolesLoading: rolesQuery.isLoading,\n rolesError: rolesQuery.error,\n submit,\n isSubmitting: invite.isPending,\n submitError: invite.error,\n submittedSuccessfully,\n isValid,\n errors,\n })}\n </>\n );\n}\n"],"mappings":"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","names":["useMemo","jsx","useMemo","useCallback","useState","Fragment","jsx","useState","useCallback","useCallback","useState","Fragment","jsx","useState","useCallback"]}
|
|
1
|
+
{"version":3,"sources":["../../src/admin/transport.ts","../../src/admin/hooks.tsx","../../src/admin/PermissionsMatrix.tsx","../../src/admin/RolesList.tsx","../../src/admin/InviteMemberForm.tsx"],"sourcesContent":["/**\n * Default Supabase implementation of the admin transport. Hits the\n * package's tables in the `rbac` schema directly via `.schema('rbac').\n * from(...)` and the auth admin endpoint for invites.\n *\n * Adopters must add `rbac` to their PostgREST exposed-schemas list\n * (Supabase Studio → Settings → API → Exposed schemas) for these\n * calls to reach the tables.\n *\n * Projects that route admin writes through their own backend\n * (e.g. for audit logging or extra validation) skip this and\n * implement `AdminTransport` themselves.\n */\n\nimport type { Action, ResourceDescriptor } from \"../types.js\";\n\nimport type {\n AdminCompany,\n AdminMember,\n AdminRole,\n AdminRolePermission,\n AdminTransport,\n} from \"./types.js\";\n\ninterface RbacSchemaClient {\n from(table: string): {\n select: (cols: string) => {\n eq: (col: string, value: unknown) => any;\n is: (col: string, value: unknown) => any;\n order: (col: string, opts?: { ascending: boolean }) => any;\n };\n insert: (row: Record<string, unknown>) => {\n select: (cols: string) => { single: () => any };\n };\n update: (patch: Record<string, unknown>) => {\n eq: (col: string, value: unknown) => {\n select: (cols: string) => { single: () => any };\n };\n };\n upsert: (\n row: Record<string, unknown> | Array<Record<string, unknown>>,\n opts?: { onConflict: string },\n ) => Promise<{ error: { message: string } | null }>;\n delete: () => { eq: (col: string, value: unknown) => any };\n };\n rpc(\n fn: string,\n args: Record<string, unknown>,\n ): Promise<{ data: unknown; error: { message: string } | null }>;\n}\n\ninterface SupabaseAdmin {\n schema(name: string): RbacSchemaClient;\n auth: {\n admin: {\n inviteUserByEmail: (\n email: string,\n opts?: { data?: Record<string, unknown>; redirectTo?: string },\n ) => Promise<{ data: unknown; error: { message: string } | null }>;\n };\n };\n}\n\nexport interface SupabaseAdminClientOptions {\n supabase: SupabaseAdmin;\n /** Where the invitee should land after setting their password. */\n inviteRedirectUrl?: string;\n}\n\nconst ACTION_COLUMN: Record<Action, string> = {\n read: \"can_read\",\n write: \"can_write\",\n update: \"can_update\",\n delete: \"can_delete\",\n};\n\nexport function createSupabaseAdminClient(\n opts: SupabaseAdminClientOptions,\n): AdminTransport {\n const sb = opts.supabase;\n const rbac = sb.schema(\"rbac\");\n\n return {\n async syncResources(resources) {\n if (resources.length === 0) {\n return 0;\n }\n const payload = resources.map((r: ResourceDescriptor) => ({\n resource: r.resource,\n scope: r.scope,\n label: r.label,\n description: r.description ?? null,\n group_label: r.group ?? null,\n }));\n const { error } = await rbac\n .from(\"resources\")\n .upsert(payload, { onConflict: \"resource\" });\n if (error) {\n throw new Error(`syncResources: ${error.message}`);\n }\n return resources.length;\n },\n\n async listRoles({ scope, companyId, templatesOnly }) {\n let q = rbac.from(\"roles\").select(\"*\").eq(\"scope\", scope);\n if (templatesOnly) {\n q = q.is(\"company_id\", null);\n } else if (companyId !== undefined) {\n q = companyId === null ? q.is(\"company_id\", null) : q.eq(\"company_id\", companyId);\n }\n const { data, error } = await q.order(\"name\", { ascending: true });\n if (error) {\n throw new Error(`listRoles: ${error.message}`);\n }\n return (data ?? []) as AdminRole[];\n },\n\n async listRolePermissions(roleId) {\n const { data, error } = await rbac\n .from(\"role_permissions\")\n .select(\"*\")\n .eq(\"role_id\", roleId);\n if (error) {\n throw new Error(`listRolePermissions: ${error.message}`);\n }\n return (data ?? []) as AdminRolePermission[];\n },\n\n async createRole(input) {\n const row = {\n scope: input.scope,\n company_id: input.companyId ?? null,\n name: input.name,\n description: input.description ?? null,\n frontend_config: input.frontend_config ?? {},\n };\n const { data, error } = await rbac\n .from(\"roles\")\n .insert(row)\n .select(\"*\")\n .single();\n if (error) {\n throw new Error(`createRole: ${error.message}`);\n }\n return data as AdminRole;\n },\n\n async updateRole(id, patch) {\n const { data, error } = await rbac\n .from(\"roles\")\n .update(patch)\n .eq(\"id\", id)\n .select(\"*\")\n .single();\n if (error) {\n throw new Error(`updateRole: ${error.message}`);\n }\n return data as AdminRole;\n },\n\n async deleteRole(id) {\n const { error } = await rbac.from(\"roles\").delete().eq(\"id\", id);\n if (error) {\n throw new Error(`deleteRole: ${error.message}`);\n }\n },\n\n async setRolePermissionCell({ role_id, resource, action, value }) {\n const column = ACTION_COLUMN[action];\n const row: Record<string, unknown> = {\n role_id,\n resource,\n [column]: value,\n };\n const { error } = await rbac\n .from(\"role_permissions\")\n .upsert(row, { onConflict: \"role_id,resource\" });\n if (error) {\n throw new Error(`setRolePermissionCell: ${error.message}`);\n }\n },\n\n async applyTemplateDefaults({ role_id, only_missing = true }) {\n const { data, error } = await rbac.rpc(\"apply_template_defaults\", {\n p_role_id: role_id,\n p_only_missing: only_missing,\n });\n if (error) {\n throw new Error(`applyTemplateDefaults: ${error.message}`);\n }\n if (typeof data === \"number\") return data;\n return Number(data ?? 0);\n },\n\n async listCompanies() {\n const { data, error } = await rbac\n .from(\"companies\")\n .select(\"*\")\n .order(\"name\", { ascending: true });\n if (error) {\n throw new Error(`listCompanies: ${error.message}`);\n }\n return (data ?? []) as AdminCompany[];\n },\n\n async createCompany(input) {\n const { data, error } = await rbac\n .from(\"companies\")\n .insert({\n name: input.name,\n slug: input.slug ?? null,\n type: input.type ?? null,\n })\n .select(\"*\")\n .single();\n if (error) {\n throw new Error(`createCompany: ${error.message}`);\n }\n return data as AdminCompany;\n },\n\n async listCompanyMembers(companyId) {\n // The package doesn't ship a view that joins users + invitations\n // out of the box because the host's auth.users schema may differ.\n // Adopters that need a richer join replace this with their own\n // transport. Fallback: list raw assignments.\n const { data, error } = await rbac\n .from(\"user_company_roles\")\n .select(\"user_id, role_id, assigned_at\")\n .eq(\"company_id\", companyId);\n if (error) {\n throw new Error(`listCompanyMembers: ${error.message}`);\n }\n const grouped = new Map<string, AdminMember>();\n for (const row of (data ?? []) as Array<{\n user_id: string;\n role_id: string;\n assigned_at: string;\n }>) {\n const existing = grouped.get(row.user_id);\n if (existing) {\n existing.role_ids.push(row.role_id);\n } else {\n grouped.set(row.user_id, {\n user_id: row.user_id,\n email: null,\n full_name: null,\n role_ids: [row.role_id],\n invited_at: row.assigned_at,\n invitation_status: \"accepted\",\n });\n }\n }\n return Array.from(grouped.values());\n },\n\n async inviteCompanyMember({ companyId, email, roleIds }) {\n const { error } = await sb.auth.admin.inviteUserByEmail(email, {\n data: {\n rbac_company_id: companyId,\n rbac_role_ids: roleIds,\n },\n redirectTo: opts.inviteRedirectUrl,\n });\n if (error) {\n throw new Error(`inviteCompanyMember: ${error.message}`);\n }\n return { invited: true };\n },\n };\n}\n","/**\n * React hooks for the admin surface. UI-kit-agnostic — adopters\n * render whatever JSX they like with the data + mutations these\n * expose. A copy-paste reference page styled with Tailwind primitives\n * lives in `examples/react-admin/`.\n *\n * Pattern: each hook returns `{ data, isLoading, error, refresh }`\n * and where applicable `{ mutate }`. We deliberately avoid pulling in\n * react-query as a dependency so the package stays peer-light;\n * adopters that already use react-query can wrap these primitives\n * with an extra hook of their own (5 lines).\n */\n\nimport { createContext, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\n\nimport type { Action, FrontendConfig, ResourceScope } from \"../types.js\";\n\nimport type {\n AdminCompany,\n AdminMember,\n AdminRole,\n AdminRolePermission,\n AdminTransport,\n} from \"./types.js\";\n\n// ─────────────────────────────────────────────────────────────────\n// Context — adopter mounts <AdminTransportProvider> once\n// ─────────────────────────────────────────────────────────────────\n\nconst AdminTransportContext = createContext<AdminTransport | null>(null);\n\nexport interface AdminTransportProviderProps {\n transport: AdminTransport;\n children: React.ReactNode;\n}\n\nexport function AdminTransportProvider(props: AdminTransportProviderProps) {\n return (\n <AdminTransportContext.Provider value={props.transport}>\n {props.children}\n </AdminTransportContext.Provider>\n );\n}\n\nfunction useAdminTransport(): AdminTransport {\n const t = useContext(AdminTransportContext);\n if (!t) {\n throw new Error(\n \"auth-rbac admin hooks require <AdminTransportProvider> — wrap your admin pages with one.\",\n );\n }\n return t;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Tiny generic async-state helper. Avoids reinventing react-query\n// while keeping the boilerplate per-hook to a single line.\n// ─────────────────────────────────────────────────────────────────\n\ninterface AsyncState<T> {\n data: T | null;\n isLoading: boolean;\n error: Error | null;\n}\n\nfunction useAsync<T>(loader: () => Promise<T>, deps: ReadonlyArray<unknown>) {\n const [state, setState] = useState<AsyncState<T>>({\n data: null,\n isLoading: true,\n error: null,\n });\n\n const refresh = useCallback(async () => {\n setState((s) => ({ ...s, isLoading: true, error: null }));\n try {\n const data = await loader();\n setState({ data, isLoading: false, error: null });\n } catch (e) {\n setState({\n data: null,\n isLoading: false,\n error: e instanceof Error ? e : new Error(String(e)),\n });\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps);\n\n useEffect(() => {\n void refresh();\n }, [refresh]);\n\n return { ...state, refresh };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Reads\n// ─────────────────────────────────────────────────────────────────\n\nexport function useAdminRoles(args: {\n scope: ResourceScope;\n companyId?: string | null;\n templatesOnly?: boolean;\n}) {\n const transport = useAdminTransport();\n return useAsync(\n () => transport.listRoles(args),\n [transport, args.scope, args.companyId, args.templatesOnly],\n );\n}\n\nexport function useAdminRolePermissions(roleId: string | null) {\n const transport = useAdminTransport();\n return useAsync(\n async () =>\n roleId == null ? [] : transport.listRolePermissions(roleId),\n [transport, roleId],\n );\n}\n\nexport function useAdminCompanies() {\n const transport = useAdminTransport();\n return useAsync(() => transport.listCompanies(), [transport]);\n}\n\nexport function useAdminCompanyMembers(companyId: string | null) {\n const transport = useAdminTransport();\n return useAsync(\n async () =>\n companyId == null ? [] : transport.listCompanyMembers(companyId),\n [transport, companyId],\n );\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Mutations — return `{ mutate, isPending, error }`. Adopters wrap\n// these in their own toast / error-boundary as needed.\n// ─────────────────────────────────────────────────────────────────\n\ninterface MutationState {\n isPending: boolean;\n error: Error | null;\n}\n\nfunction useMutation<TArgs extends unknown[], TResult>(\n fn: (...args: TArgs) => Promise<TResult>,\n) {\n const [state, setState] = useState<MutationState>({\n isPending: false,\n error: null,\n });\n\n const mutate = useCallback(\n async (...args: TArgs): Promise<TResult> => {\n setState({ isPending: true, error: null });\n try {\n const result = await fn(...args);\n setState({ isPending: false, error: null });\n return result;\n } catch (e) {\n const err = e instanceof Error ? e : new Error(String(e));\n setState({ isPending: false, error: err });\n throw err;\n }\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [fn],\n );\n\n return { mutate, ...state };\n}\n\nexport function useCreateRole() {\n const transport = useAdminTransport();\n return useMutation(transport.createRole);\n}\n\nexport function useUpdateRole() {\n const transport = useAdminTransport();\n return useMutation(transport.updateRole);\n}\n\nexport function useDeleteRole() {\n const transport = useAdminTransport();\n return useMutation(transport.deleteRole);\n}\n\nexport function useSetRolePermissionCell() {\n const transport = useAdminTransport();\n return useMutation(transport.setRolePermissionCell);\n}\n\nexport function useApplyTemplateDefaults() {\n const transport = useAdminTransport();\n return useMutation(transport.applyTemplateDefaults);\n}\n\nexport function useCreateCompany() {\n const transport = useAdminTransport();\n return useMutation(transport.createCompany);\n}\n\nexport function useInviteCompanyMember() {\n const transport = useAdminTransport();\n return useMutation(transport.inviteCompanyMember);\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Convenience: hold a role's full state (role + permission grid)\n// in one hook, with a `setCell` mutator that optimistically updates\n// the local cache and writes through to the transport.\n// ─────────────────────────────────────────────────────────────────\n\nexport interface RolePermissionGrid {\n // resource → action → boolean\n [resource: string]: { [A in Action]: boolean };\n}\n\nexport function useRolePermissionGrid(roleId: string | null) {\n const { data, isLoading, error, refresh } = useAdminRolePermissions(roleId);\n const setCell = useSetRolePermissionCell();\n\n const grid = useMemo<RolePermissionGrid>(() => {\n const out: RolePermissionGrid = {};\n for (const row of data ?? []) {\n out[row.resource] = {\n read: row.can_read,\n write: row.can_write,\n update: row.can_update,\n delete: row.can_delete,\n };\n }\n return out;\n }, [data]);\n\n const updateCell = useCallback(\n async (resource: string, action: Action, value: boolean) => {\n if (!roleId) {\n return;\n }\n await setCell.mutate({ role_id: roleId, resource, action, value });\n void refresh();\n },\n [roleId, setCell, refresh],\n );\n\n return {\n grid,\n isLoading,\n error,\n refresh,\n updateCell,\n isUpdating: setCell.isPending,\n updateError: setCell.error,\n };\n}\n","/**\n * Headless permissions matrix.\n *\n * Owns:\n * - reading the role's current permission grid\n * - debounced write-through on every cell toggle\n * - grouping resources by `group` for a sectioned UI\n *\n * Owns NOTHING about styling — the consumer renders all JSX via the\n * single `children` render-prop. A copy-paste reference styled with\n * Tailwind + Radix lives in `examples/react-admin/`.\n *\n * @example minimum viable adoption\n *\n * <PermissionsMatrix\n * roleId={role.id}\n * resources={resources.filter(r => r.scope === role.scope)}\n * >\n * {({ groups, isCellEnabled, setCell, isLoading }) =>\n * groups.map((g) => (\n * <section key={g.group}>\n * <h3>{g.group}</h3>\n * {g.resources.map((r) => (\n * <div key={r.resource}>\n * <span>{r.label}</span>\n * {([\"read\", \"write\", \"update\", \"delete\"] as const).map((a) => (\n * <input\n * key={a}\n * type=\"checkbox\"\n * checked={isCellEnabled(r.resource, a)}\n * disabled={isLoading}\n * onChange={(e) => setCell(r.resource, a, e.target.checked)}\n * />\n * ))}\n * </div>\n * ))}\n * </section>\n * ))\n * }\n * </PermissionsMatrix>\n */\n\nimport { useMemo } from \"react\";\n\nimport type {\n Action,\n ResourceDescriptor,\n} from \"../types.js\";\nimport { groupResources } from \"../client.js\";\n\nimport { useRolePermissionGrid } from \"./hooks.js\";\n\nexport interface MatrixGroup {\n group: string;\n resources: ResourceDescriptor[];\n}\n\nexport interface MatrixRenderArgs {\n /** Resources grouped by their `group` label, original insertion order. */\n groups: MatrixGroup[];\n /** Read a single cell from the current grid. */\n isCellEnabled: (resource: string, action: Action) => boolean;\n /** Write a single cell. Optimistic in the local cache + writes through. */\n setCell: (resource: string, action: Action, value: boolean) => Promise<void>;\n isLoading: boolean;\n isUpdating: boolean;\n error: Error | null;\n /** All four actions, exposed for the consumer to render headers. */\n actions: ReadonlyArray<Action>;\n}\n\nexport interface PermissionsMatrixProps {\n roleId: string | null;\n resources: ReadonlyArray<ResourceDescriptor>;\n children: (args: MatrixRenderArgs) => React.ReactNode;\n}\n\nconst ACTIONS = [\"read\", \"write\", \"update\", \"delete\"] as const;\n\nexport function PermissionsMatrix(props: PermissionsMatrixProps) {\n const { grid, isLoading, error, updateCell, isUpdating } =\n useRolePermissionGrid(props.roleId);\n\n const groups = useMemo<MatrixGroup[]>(\n () => groupResources(props.resources),\n [props.resources],\n );\n\n const isCellEnabled = (resource: string, action: Action): boolean => {\n return grid[resource]?.[action] ?? false;\n };\n\n const setCell = async (resource: string, action: Action, value: boolean) => {\n await updateCell(resource, action, value);\n };\n\n return (\n <>\n {props.children({\n groups,\n isCellEnabled,\n setCell,\n isLoading,\n isUpdating,\n error,\n actions: ACTIONS,\n })}\n </>\n );\n}\n","/**\n * Headless roles-list controller. Tracks selection + create/delete\n * mutations; consumer renders the list, the new-role dialog, and\n * the destructive-action confirmation.\n */\n\nimport { useCallback, useState } from \"react\";\n\nimport type { ResourceScope } from \"../types.js\";\n\nimport {\n useAdminRoles,\n useCreateRole,\n useDeleteRole,\n} from \"./hooks.js\";\nimport type { AdminRole } from \"./types.js\";\n\nexport interface RolesListRenderArgs {\n roles: AdminRole[];\n isLoading: boolean;\n error: Error | null;\n\n selectedRoleId: string | null;\n selectRole: (id: string | null) => void;\n\n createRole: (input: {\n name: string;\n description?: string;\n }) => Promise<AdminRole>;\n isCreating: boolean;\n createError: Error | null;\n\n deleteRole: (id: string) => Promise<void>;\n isDeleting: boolean;\n deleteError: Error | null;\n\n refresh: () => Promise<void>;\n}\n\nexport interface RolesListProps {\n scope: ResourceScope;\n /** Required for company-scope. Pass `null` for templates. */\n companyId?: string | null;\n /** Pre-select the first role on load. Default: true. */\n autoSelectFirst?: boolean;\n children: (args: RolesListRenderArgs) => React.ReactNode;\n}\n\nexport function RolesList(props: RolesListProps) {\n const { scope, companyId, autoSelectFirst = true } = props;\n\n const list = useAdminRoles({ scope, companyId });\n const create = useCreateRole();\n const remove = useDeleteRole();\n\n const [selectedRoleId, setSelectedRoleId] = useState<string | null>(null);\n\n // Auto-select first role on load.\n if (\n autoSelectFirst &&\n selectedRoleId == null &&\n list.data != null &&\n list.data.length > 0\n ) {\n setSelectedRoleId(list.data[0]!.id);\n }\n\n const createRole = useCallback(\n async (input: { name: string; description?: string }) => {\n const role = await create.mutate({\n scope,\n companyId: companyId ?? null,\n name: input.name,\n description: input.description,\n });\n await list.refresh();\n setSelectedRoleId(role.id);\n return role;\n },\n [create, scope, companyId, list],\n );\n\n const deleteRole = useCallback(\n async (id: string) => {\n await remove.mutate(id);\n if (selectedRoleId === id) {\n setSelectedRoleId(null);\n }\n await list.refresh();\n },\n [remove, list, selectedRoleId],\n );\n\n return (\n <>\n {props.children({\n roles: list.data ?? [],\n isLoading: list.isLoading,\n error: list.error,\n selectedRoleId,\n selectRole: setSelectedRoleId,\n createRole,\n isCreating: create.isPending,\n createError: create.error,\n deleteRole,\n isDeleting: remove.isPending,\n deleteError: remove.error,\n refresh: list.refresh,\n })}\n </>\n );\n}\n","/**\n * Headless invite-member form state. Tracks email + selected role\n * ids, runs basic local validation, and exposes a submit handler\n * that calls the configured transport (Supabase Auth invite by\n * default).\n */\n\nimport { useCallback, useState } from \"react\";\n\nimport { useAdminRoles, useInviteCompanyMember } from \"./hooks.js\";\nimport type { AdminRole } from \"./types.js\";\n\nexport interface InviteMemberFormRenderArgs {\n // form state\n email: string;\n setEmail: (v: string) => void;\n selectedRoleIds: Set<string>;\n toggleRole: (roleId: string) => void;\n resetForm: () => void;\n\n // catalog\n roles: AdminRole[];\n rolesLoading: boolean;\n rolesError: Error | null;\n\n // submission\n submit: () => Promise<void>;\n isSubmitting: boolean;\n submitError: Error | null;\n submittedSuccessfully: boolean;\n\n // validation\n isValid: boolean;\n errors: { email?: string; roles?: string };\n}\n\nexport interface InviteMemberFormProps {\n companyId: string;\n /** Called after a successful invite — typically clears a dialog. */\n onSuccess?: () => void;\n children: (args: InviteMemberFormRenderArgs) => React.ReactNode;\n}\n\nexport function InviteMemberForm(props: InviteMemberFormProps) {\n const rolesQuery = useAdminRoles({\n scope: \"company\",\n companyId: props.companyId,\n });\n const invite = useInviteCompanyMember();\n\n const [email, setEmail] = useState(\"\");\n const [selectedRoleIds, setSelectedRoleIds] = useState<Set<string>>(\n new Set(),\n );\n const [submittedSuccessfully, setSubmittedSuccessfully] = useState(false);\n\n const toggleRole = useCallback((roleId: string) => {\n setSelectedRoleIds((prev) => {\n const next = new Set(prev);\n if (next.has(roleId)) {\n next.delete(roleId);\n } else {\n next.add(roleId);\n }\n return next;\n });\n }, []);\n\n const resetForm = useCallback(() => {\n setEmail(\"\");\n setSelectedRoleIds(new Set());\n setSubmittedSuccessfully(false);\n }, []);\n\n const errors: InviteMemberFormRenderArgs[\"errors\"] = {};\n if (email.trim() && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email.trim())) {\n errors.email = \"Bitte gib eine gültige E-Mail-Adresse ein.\";\n }\n if (selectedRoleIds.size === 0) {\n errors.roles = \"Bitte mindestens eine Rolle auswählen.\";\n }\n const isValid =\n email.trim().length > 0 &&\n Object.keys(errors).length === 0;\n\n const submit = useCallback(async () => {\n if (!isValid) {\n return;\n }\n await invite.mutate({\n companyId: props.companyId,\n email: email.trim(),\n roleIds: Array.from(selectedRoleIds),\n });\n setSubmittedSuccessfully(true);\n props.onSuccess?.();\n }, [invite, props, email, selectedRoleIds, isValid]);\n\n return (\n <>\n {props.children({\n email,\n setEmail,\n selectedRoleIds,\n toggleRole,\n resetForm,\n roles: rolesQuery.data ?? [],\n rolesLoading: rolesQuery.isLoading,\n rolesError: rolesQuery.error,\n submit,\n isSubmitting: invite.isPending,\n submitError: invite.error,\n submittedSuccessfully,\n isValid,\n errors,\n })}\n </>\n );\n}\n"],"mappings":"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","names":["useMemo","jsx","useMemo","useCallback","useState","Fragment","jsx","useState","useCallback","useCallback","useState","Fragment","jsx","useState","useCallback"]}
|
|
@@ -0,0 +1 @@
|
|
|
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":[]}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
function createSupabaseFetcher(opts) {
|
|
3
3
|
return {
|
|
4
4
|
async fetchProfile() {
|
|
5
|
-
const { data, error } = await opts.supabase.rpc(
|
|
6
|
-
"
|
|
5
|
+
const { data, error } = await opts.supabase.schema("rbac").rpc(
|
|
6
|
+
"user_profile",
|
|
7
7
|
{ p_user_id: opts.userId }
|
|
8
8
|
);
|
|
9
9
|
if (error) {
|
|
@@ -15,6 +15,19 @@ function createSupabaseFetcher(opts) {
|
|
|
15
15
|
}
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
+
async function detectRbacSchema(supabase) {
|
|
19
|
+
try {
|
|
20
|
+
const { error } = await supabase.schema("rbac").rpc("user_can", {
|
|
21
|
+
p_user_id: "00000000-0000-0000-0000-000000000000",
|
|
22
|
+
p_resource: "__rbac_self_check__",
|
|
23
|
+
p_action: "read",
|
|
24
|
+
p_company_id: null
|
|
25
|
+
});
|
|
26
|
+
return error === null;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
18
31
|
function createHttpFetcher(opts) {
|
|
19
32
|
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
20
33
|
return {
|
|
@@ -50,6 +63,7 @@ function normalizeProfile(raw) {
|
|
|
50
63
|
|
|
51
64
|
export {
|
|
52
65
|
createSupabaseFetcher,
|
|
66
|
+
detectRbacSchema,
|
|
53
67
|
createHttpFetcher
|
|
54
68
|
};
|
|
55
|
-
//# sourceMappingURL=chunk-
|
|
69
|
+
//# sourceMappingURL=chunk-NRDW233A.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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":[]}
|
package/dist/index.cjs
CHANGED
|
@@ -23,6 +23,7 @@ __export(src_exports, {
|
|
|
23
23
|
buildPermissionResolver: () => buildPermissionResolver,
|
|
24
24
|
createHttpFetcher: () => createHttpFetcher,
|
|
25
25
|
createSupabaseFetcher: () => createSupabaseFetcher,
|
|
26
|
+
detectRbacSchema: () => detectRbacSchema,
|
|
26
27
|
groupResources: () => groupResources
|
|
27
28
|
});
|
|
28
29
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -93,8 +94,8 @@ function groupResources(registry) {
|
|
|
93
94
|
function createSupabaseFetcher(opts) {
|
|
94
95
|
return {
|
|
95
96
|
async fetchProfile() {
|
|
96
|
-
const { data, error } = await opts.supabase.rpc(
|
|
97
|
-
"
|
|
97
|
+
const { data, error } = await opts.supabase.schema("rbac").rpc(
|
|
98
|
+
"user_profile",
|
|
98
99
|
{ p_user_id: opts.userId }
|
|
99
100
|
);
|
|
100
101
|
if (error) {
|
|
@@ -106,6 +107,19 @@ function createSupabaseFetcher(opts) {
|
|
|
106
107
|
}
|
|
107
108
|
};
|
|
108
109
|
}
|
|
110
|
+
async function detectRbacSchema(supabase) {
|
|
111
|
+
try {
|
|
112
|
+
const { error } = await supabase.schema("rbac").rpc("user_can", {
|
|
113
|
+
p_user_id: "00000000-0000-0000-0000-000000000000",
|
|
114
|
+
p_resource: "__rbac_self_check__",
|
|
115
|
+
p_action: "read",
|
|
116
|
+
p_company_id: null
|
|
117
|
+
});
|
|
118
|
+
return error === null;
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
109
123
|
function createHttpFetcher(opts) {
|
|
110
124
|
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
111
125
|
return {
|
|
@@ -143,6 +157,7 @@ function normalizeProfile(raw) {
|
|
|
143
157
|
buildPermissionResolver,
|
|
144
158
|
createHttpFetcher,
|
|
145
159
|
createSupabaseFetcher,
|
|
160
|
+
detectRbacSchema,
|
|
146
161
|
groupResources
|
|
147
162
|
});
|
|
148
163
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -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} 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 * `auth_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 `auth_rbac_user_profile(uuid)` via\n * a Supabase JS client. Easiest path when the host project already\n * uses Supabase.\n *\n * @example\n * createSupabaseFetcher({ supabase, userId: session.user.id })\n */\nexport function createSupabaseFetcher(opts: {\n supabase: {\n rpc: (\n fn: string,\n args: Record<string, unknown>,\n ) => Promise<{ data: unknown; error: { message: string } | null }>;\n };\n userId: string;\n}): AuthRbacFetcher {\n return {\n async fetchProfile(): Promise<UserProfile> {\n const { data, error } = await opts.supabase.rpc(\n \"auth_rbac_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 * 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/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":"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","names":[]}
|