snipe-auth-rbac 0.5.0 → 0.6.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.
@@ -54,6 +54,21 @@ interface AdminResourceDependency {
54
54
  child_resource: string;
55
55
  action: Action;
56
56
  }
57
+ /**
58
+ * One row of `rbac.role_permission_overrides` — an explicit
59
+ * per-role suppression that takes a cell OFF even when the
60
+ * dependency graph would otherwise imply it (or when a direct
61
+ * grant row exists). Used to express "Anwalt has tenants:read
62
+ * but should NOT see payments, even though tenants.dependsOn
63
+ * includes payments".
64
+ *
65
+ * Available since 0.6.0.
66
+ */
67
+ interface AdminRolePermissionOverride {
68
+ role_id: string;
69
+ resource: string;
70
+ action: Action;
71
+ }
57
72
  interface AdminCompany {
58
73
  id: string;
59
74
  name: string;
@@ -144,6 +159,26 @@ interface AdminTransport {
144
159
  * cascade on a parent toggle.
145
160
  */
146
161
  listResourceDependencies(): Promise<AdminResourceDependency[]>;
162
+ /**
163
+ * 0.6.0+. Read every per-role override for the given role.
164
+ * Returns the (resource, action) cells that the admin has
165
+ * explicitly suppressed, even though dependency-graph expansion
166
+ * would otherwise grant them.
167
+ */
168
+ listRolePermissionOverrides(roleId: string): Promise<AdminRolePermissionOverride[]>;
169
+ /**
170
+ * 0.6.0+. Set or clear a per-role override:
171
+ * * suppress=true → INSERT (idempotent via ON CONFLICT)
172
+ * * suppress=false → DELETE
173
+ * Resolver functions subtract the row from the user's expanded
174
+ * grant set as soon as it's written.
175
+ */
176
+ setRolePermissionOverride(args: {
177
+ role_id: string;
178
+ resource: string;
179
+ action: Action;
180
+ suppress: boolean;
181
+ }): Promise<void>;
147
182
  /**
148
183
  * Materialise `rbac.role_permissions` rows from a template role's
149
184
  * `default_permissions` JSONB pattern. Calls the SQL function
@@ -342,6 +377,25 @@ declare function useAdminResourceDependencies(): {
342
377
  isLoading: boolean;
343
378
  error: Error | null;
344
379
  };
380
+ /**
381
+ * 0.6.0+. Per-role override map. Returns a Set of
382
+ * `"<resource>:<action>"` keys for the given role plus a
383
+ * `setOverride(resource, action, suppress)` mutator. Optimistic —
384
+ * the local set flips immediately, then a re-fetch reconciles.
385
+ *
386
+ * Use this in tandem with `useRolePermissionGrid` to render a
387
+ * matrix UI that distinguishes:
388
+ * * direct grants (the row is on rbac.role_permissions)
389
+ * * implied grants (resource_dependencies expansion)
390
+ * * overrides (this hook's set — admin clicked an implied cell
391
+ * off to carve it out for this specific role)
392
+ */
393
+ declare function useRolePermissionOverrides(roleId: string | null): {
394
+ overrides: Set<string>;
395
+ setOverride: (resource: string, action: Action, suppress: boolean) => Promise<void>;
396
+ error: Error | null;
397
+ refresh: () => Promise<void>;
398
+ };
345
399
  declare function useCreateCompany(): {
346
400
  isPending: boolean;
347
401
  error: Error | null;
@@ -368,15 +422,19 @@ interface RolePermissionGrid {
368
422
  };
369
423
  }
370
424
  /**
371
- * 0.4.0+ shape (0.5.0 semantics): per-cell origin marker. `'direct'`
372
- * for an explicit admin grant, the name of the parent resource for
373
- * an implied grant (from rbac.resource_dependencies), or `null` /
374
- * absent when neither.
425
+ * Per-cell origin marker:
426
+ * * `'direct'` — explicit admin grant on rbac.role_permissions
427
+ * * `'override'` — admin set an override on rbac.role_permission_overrides
428
+ * (0.6.0+; cell is OFF even if a direct or
429
+ * implied grant would otherwise apply)
430
+ * * `<string>` — name of the parent resource that implies this
431
+ * cell via rbac.resource_dependencies
432
+ * * `null` — neither granted nor implied
375
433
  *
376
434
  * In 0.4.x this was driven by the `<action>_granted_via` columns on
377
- * rbac.role_permissions. In 0.5.0 implied rows are no longer
435
+ * rbac.role_permissions. In 0.5.0+ implied rows are no longer
378
436
  * materialised — origin is computed client-side from the dependency
379
- * graph + the role's direct grants.
437
+ * graph + the role's direct grants + (0.6.0+) any overrides.
380
438
  */
381
439
  interface RolePermissionOriginGrid {
382
440
  [resource: string]: {
@@ -390,6 +448,15 @@ declare function useRolePermissionGrid(roleId: string | null): {
390
448
  parent: string;
391
449
  action: Action;
392
450
  }[]>;
451
+ /** 0.6.0+. Set of `"<resource>:<action>"` for this role's overrides. */
452
+ overrides: Set<string>;
453
+ /**
454
+ * 0.6.0+. Suppress (`suppress=true`) or restore (`suppress=false`)
455
+ * an implied permission for this role. The grid + originGrid
456
+ * re-render with `'override'` state on the cell as soon as the
457
+ * optimistic flip lands.
458
+ */
459
+ setOverride: (resource: string, action: Action, suppress: boolean) => Promise<void>;
393
460
  isLoading: boolean;
394
461
  error: Error | null;
395
462
  refresh: () => Promise<void>;
@@ -405,26 +472,38 @@ interface MatrixGroup {
405
472
  interface MatrixRenderArgs {
406
473
  /** Resources grouped by their `group` label, original insertion order. */
407
474
  groups: MatrixGroup[];
408
- /** Read a single cell from the current grid. */
475
+ /**
476
+ * Effective state of a cell after applying direct grants, the
477
+ * resource-dependency expansion, and any per-role overrides.
478
+ * What the resolver would answer for a user holding this role.
479
+ */
409
480
  isCellEnabled: (resource: string, action: Action) => boolean;
410
481
  /**
411
- * Origin of a single cell — `'direct'` for a direct admin grant
412
- * (or off), or the name of the parent resource whose `dependsOn`
413
- * edge implied the row. The consumer renders an "Implied by …"
414
- * badge whenever this returns a non-`'direct'` value.
482
+ * Origin of a single cell:
483
+ * * `'direct'` — explicit admin grant on rbac.role_permissions
484
+ * * `'override'` — admin suppressed it via rbac.role_permission_overrides
485
+ * (0.6.0+; cell is off even if a parent would imply)
486
+ * * `<string>` — the name of the parent resource whose
487
+ * `dependsOn` edge implies this cell
415
488
  *
416
- * Available since 0.4.0. With pre-0.4.0 SQL (no granted_via
417
- * columns) this always returns `'direct'`.
489
+ * Available since 0.4.0; the `'override'` value is 0.6.0+.
418
490
  */
419
- cellOrigin: (resource: string, action: Action) => "direct" | string;
491
+ cellOrigin: (resource: string, action: Action) => "direct" | "override" | string;
420
492
  /**
421
- * Write a single cell. Optimistic in the local cache + writes
422
- * through. On toggle-on, also writes implied rows for every
423
- * `dependsOn` edge whose `actions` include the toggled action —
424
- * those rows carry the parent's name in
425
- * `<action>_granted_via`. Toggle-off never cascades.
493
+ * Toggle a DIRECT grant on rbac.role_permissions. Use for cells
494
+ * that the matrix UI shows as "direct" (no implied parent). For
495
+ * cells that are implied, use `setOverride` instead that's what
496
+ * lets the admin opt a single role out of a cascade without
497
+ * touching the parent grant or the registry.
426
498
  */
427
499
  setCell: (resource: string, action: Action, value: boolean) => Promise<void>;
500
+ /**
501
+ * 0.6.0+. Suppress (`suppress=true`) or restore (`suppress=false`)
502
+ * an implied permission for this role via
503
+ * rbac.role_permission_overrides. Writes are optimistic; the
504
+ * `cellOrigin` reflects the new state immediately.
505
+ */
506
+ setOverride: (resource: string, action: Action, suppress: boolean) => Promise<void>;
428
507
  isLoading: boolean;
429
508
  isUpdating: boolean;
430
509
  error: Error | null;
@@ -492,4 +571,4 @@ interface InviteMemberFormProps {
492
571
  }
493
572
  declare function InviteMemberForm(props: InviteMemberFormProps): react_jsx_runtime.JSX.Element;
494
573
 
495
- export { type AdminCompany, type AdminMember, type AdminResourceDependency, type AdminRole, type AdminRolePermission, type AdminTransport, AdminTransportProvider, type AdminTransportProviderProps, InviteMemberForm, type InviteMemberFormProps, type InviteMemberFormRenderArgs, type MatrixGroup, type MatrixRenderArgs, PermissionsMatrix, type PermissionsMatrixProps, type RolePermissionGrid, type RolePermissionOriginGrid, RolesList, type RolesListProps, type RolesListRenderArgs, type SupabaseAdminClientOptions, createSupabaseAdminClient, extractResourceDependencies, useAdminCompanies, useAdminCompanyMembers, useAdminResourceDependencies, useAdminRolePermissions, useAdminRoles, useApplyTemplateDefaults, useCreateCompany, useCreateRole, useDeleteRole, useInviteCompanyMember, useRolePermissionGrid, useSetRolePermissionCell, useUpdateRole };
574
+ export { type AdminCompany, type AdminMember, type AdminResourceDependency, type AdminRole, type AdminRolePermission, type AdminRolePermissionOverride, type AdminTransport, AdminTransportProvider, type AdminTransportProviderProps, InviteMemberForm, type InviteMemberFormProps, type InviteMemberFormRenderArgs, type MatrixGroup, type MatrixRenderArgs, PermissionsMatrix, type PermissionsMatrixProps, type RolePermissionGrid, type RolePermissionOriginGrid, RolesList, type RolesListProps, type RolesListRenderArgs, type SupabaseAdminClientOptions, createSupabaseAdminClient, extractResourceDependencies, useAdminCompanies, useAdminCompanyMembers, useAdminResourceDependencies, useAdminRolePermissions, useAdminRoles, useApplyTemplateDefaults, useCreateCompany, useCreateRole, useDeleteRole, useInviteCompanyMember, useRolePermissionGrid, useRolePermissionOverrides, useSetRolePermissionCell, useUpdateRole };
@@ -54,6 +54,21 @@ interface AdminResourceDependency {
54
54
  child_resource: string;
55
55
  action: Action;
56
56
  }
57
+ /**
58
+ * One row of `rbac.role_permission_overrides` — an explicit
59
+ * per-role suppression that takes a cell OFF even when the
60
+ * dependency graph would otherwise imply it (or when a direct
61
+ * grant row exists). Used to express "Anwalt has tenants:read
62
+ * but should NOT see payments, even though tenants.dependsOn
63
+ * includes payments".
64
+ *
65
+ * Available since 0.6.0.
66
+ */
67
+ interface AdminRolePermissionOverride {
68
+ role_id: string;
69
+ resource: string;
70
+ action: Action;
71
+ }
57
72
  interface AdminCompany {
58
73
  id: string;
59
74
  name: string;
@@ -144,6 +159,26 @@ interface AdminTransport {
144
159
  * cascade on a parent toggle.
145
160
  */
146
161
  listResourceDependencies(): Promise<AdminResourceDependency[]>;
162
+ /**
163
+ * 0.6.0+. Read every per-role override for the given role.
164
+ * Returns the (resource, action) cells that the admin has
165
+ * explicitly suppressed, even though dependency-graph expansion
166
+ * would otherwise grant them.
167
+ */
168
+ listRolePermissionOverrides(roleId: string): Promise<AdminRolePermissionOverride[]>;
169
+ /**
170
+ * 0.6.0+. Set or clear a per-role override:
171
+ * * suppress=true → INSERT (idempotent via ON CONFLICT)
172
+ * * suppress=false → DELETE
173
+ * Resolver functions subtract the row from the user's expanded
174
+ * grant set as soon as it's written.
175
+ */
176
+ setRolePermissionOverride(args: {
177
+ role_id: string;
178
+ resource: string;
179
+ action: Action;
180
+ suppress: boolean;
181
+ }): Promise<void>;
147
182
  /**
148
183
  * Materialise `rbac.role_permissions` rows from a template role's
149
184
  * `default_permissions` JSONB pattern. Calls the SQL function
@@ -342,6 +377,25 @@ declare function useAdminResourceDependencies(): {
342
377
  isLoading: boolean;
343
378
  error: Error | null;
344
379
  };
380
+ /**
381
+ * 0.6.0+. Per-role override map. Returns a Set of
382
+ * `"<resource>:<action>"` keys for the given role plus a
383
+ * `setOverride(resource, action, suppress)` mutator. Optimistic —
384
+ * the local set flips immediately, then a re-fetch reconciles.
385
+ *
386
+ * Use this in tandem with `useRolePermissionGrid` to render a
387
+ * matrix UI that distinguishes:
388
+ * * direct grants (the row is on rbac.role_permissions)
389
+ * * implied grants (resource_dependencies expansion)
390
+ * * overrides (this hook's set — admin clicked an implied cell
391
+ * off to carve it out for this specific role)
392
+ */
393
+ declare function useRolePermissionOverrides(roleId: string | null): {
394
+ overrides: Set<string>;
395
+ setOverride: (resource: string, action: Action, suppress: boolean) => Promise<void>;
396
+ error: Error | null;
397
+ refresh: () => Promise<void>;
398
+ };
345
399
  declare function useCreateCompany(): {
346
400
  isPending: boolean;
347
401
  error: Error | null;
@@ -368,15 +422,19 @@ interface RolePermissionGrid {
368
422
  };
369
423
  }
370
424
  /**
371
- * 0.4.0+ shape (0.5.0 semantics): per-cell origin marker. `'direct'`
372
- * for an explicit admin grant, the name of the parent resource for
373
- * an implied grant (from rbac.resource_dependencies), or `null` /
374
- * absent when neither.
425
+ * Per-cell origin marker:
426
+ * * `'direct'` — explicit admin grant on rbac.role_permissions
427
+ * * `'override'` — admin set an override on rbac.role_permission_overrides
428
+ * (0.6.0+; cell is OFF even if a direct or
429
+ * implied grant would otherwise apply)
430
+ * * `<string>` — name of the parent resource that implies this
431
+ * cell via rbac.resource_dependencies
432
+ * * `null` — neither granted nor implied
375
433
  *
376
434
  * In 0.4.x this was driven by the `<action>_granted_via` columns on
377
- * rbac.role_permissions. In 0.5.0 implied rows are no longer
435
+ * rbac.role_permissions. In 0.5.0+ implied rows are no longer
378
436
  * materialised — origin is computed client-side from the dependency
379
- * graph + the role's direct grants.
437
+ * graph + the role's direct grants + (0.6.0+) any overrides.
380
438
  */
381
439
  interface RolePermissionOriginGrid {
382
440
  [resource: string]: {
@@ -390,6 +448,15 @@ declare function useRolePermissionGrid(roleId: string | null): {
390
448
  parent: string;
391
449
  action: Action;
392
450
  }[]>;
451
+ /** 0.6.0+. Set of `"<resource>:<action>"` for this role's overrides. */
452
+ overrides: Set<string>;
453
+ /**
454
+ * 0.6.0+. Suppress (`suppress=true`) or restore (`suppress=false`)
455
+ * an implied permission for this role. The grid + originGrid
456
+ * re-render with `'override'` state on the cell as soon as the
457
+ * optimistic flip lands.
458
+ */
459
+ setOverride: (resource: string, action: Action, suppress: boolean) => Promise<void>;
393
460
  isLoading: boolean;
394
461
  error: Error | null;
395
462
  refresh: () => Promise<void>;
@@ -405,26 +472,38 @@ interface MatrixGroup {
405
472
  interface MatrixRenderArgs {
406
473
  /** Resources grouped by their `group` label, original insertion order. */
407
474
  groups: MatrixGroup[];
408
- /** Read a single cell from the current grid. */
475
+ /**
476
+ * Effective state of a cell after applying direct grants, the
477
+ * resource-dependency expansion, and any per-role overrides.
478
+ * What the resolver would answer for a user holding this role.
479
+ */
409
480
  isCellEnabled: (resource: string, action: Action) => boolean;
410
481
  /**
411
- * Origin of a single cell — `'direct'` for a direct admin grant
412
- * (or off), or the name of the parent resource whose `dependsOn`
413
- * edge implied the row. The consumer renders an "Implied by …"
414
- * badge whenever this returns a non-`'direct'` value.
482
+ * Origin of a single cell:
483
+ * * `'direct'` — explicit admin grant on rbac.role_permissions
484
+ * * `'override'` — admin suppressed it via rbac.role_permission_overrides
485
+ * (0.6.0+; cell is off even if a parent would imply)
486
+ * * `<string>` — the name of the parent resource whose
487
+ * `dependsOn` edge implies this cell
415
488
  *
416
- * Available since 0.4.0. With pre-0.4.0 SQL (no granted_via
417
- * columns) this always returns `'direct'`.
489
+ * Available since 0.4.0; the `'override'` value is 0.6.0+.
418
490
  */
419
- cellOrigin: (resource: string, action: Action) => "direct" | string;
491
+ cellOrigin: (resource: string, action: Action) => "direct" | "override" | string;
420
492
  /**
421
- * Write a single cell. Optimistic in the local cache + writes
422
- * through. On toggle-on, also writes implied rows for every
423
- * `dependsOn` edge whose `actions` include the toggled action —
424
- * those rows carry the parent's name in
425
- * `<action>_granted_via`. Toggle-off never cascades.
493
+ * Toggle a DIRECT grant on rbac.role_permissions. Use for cells
494
+ * that the matrix UI shows as "direct" (no implied parent). For
495
+ * cells that are implied, use `setOverride` instead that's what
496
+ * lets the admin opt a single role out of a cascade without
497
+ * touching the parent grant or the registry.
426
498
  */
427
499
  setCell: (resource: string, action: Action, value: boolean) => Promise<void>;
500
+ /**
501
+ * 0.6.0+. Suppress (`suppress=true`) or restore (`suppress=false`)
502
+ * an implied permission for this role via
503
+ * rbac.role_permission_overrides. Writes are optimistic; the
504
+ * `cellOrigin` reflects the new state immediately.
505
+ */
506
+ setOverride: (resource: string, action: Action, suppress: boolean) => Promise<void>;
428
507
  isLoading: boolean;
429
508
  isUpdating: boolean;
430
509
  error: Error | null;
@@ -492,4 +571,4 @@ interface InviteMemberFormProps {
492
571
  }
493
572
  declare function InviteMemberForm(props: InviteMemberFormProps): react_jsx_runtime.JSX.Element;
494
573
 
495
- export { type AdminCompany, type AdminMember, type AdminResourceDependency, type AdminRole, type AdminRolePermission, type AdminTransport, AdminTransportProvider, type AdminTransportProviderProps, InviteMemberForm, type InviteMemberFormProps, type InviteMemberFormRenderArgs, type MatrixGroup, type MatrixRenderArgs, PermissionsMatrix, type PermissionsMatrixProps, type RolePermissionGrid, type RolePermissionOriginGrid, RolesList, type RolesListProps, type RolesListRenderArgs, type SupabaseAdminClientOptions, createSupabaseAdminClient, extractResourceDependencies, useAdminCompanies, useAdminCompanyMembers, useAdminResourceDependencies, useAdminRolePermissions, useAdminRoles, useApplyTemplateDefaults, useCreateCompany, useCreateRole, useDeleteRole, useInviteCompanyMember, useRolePermissionGrid, useSetRolePermissionCell, useUpdateRole };
574
+ export { type AdminCompany, type AdminMember, type AdminResourceDependency, type AdminRole, type AdminRolePermission, type AdminRolePermissionOverride, type AdminTransport, AdminTransportProvider, type AdminTransportProviderProps, InviteMemberForm, type InviteMemberFormProps, type InviteMemberFormRenderArgs, type MatrixGroup, type MatrixRenderArgs, PermissionsMatrix, type PermissionsMatrixProps, type RolePermissionGrid, type RolePermissionOriginGrid, RolesList, type RolesListProps, type RolesListRenderArgs, type SupabaseAdminClientOptions, createSupabaseAdminClient, extractResourceDependencies, useAdminCompanies, useAdminCompanyMembers, useAdminResourceDependencies, useAdminRolePermissions, useAdminRoles, useApplyTemplateDefaults, useCreateCompany, useCreateRole, useDeleteRole, useInviteCompanyMember, useRolePermissionGrid, useRolePermissionOverrides, useSetRolePermissionCell, useUpdateRole };
@@ -185,6 +185,32 @@ function createSupabaseAdminClient(opts) {
185
185
  }
186
186
  return data ?? [];
187
187
  },
188
+ async listRolePermissionOverrides(roleId) {
189
+ const { data, error } = await rbac.from("role_permission_overrides").select("role_id, resource, action").eq("role_id", roleId);
190
+ if (error) {
191
+ if (/role_permission_overrides/i.test(error.message) && /does not exist/i.test(error.message)) {
192
+ return [];
193
+ }
194
+ throw new Error(`listRolePermissionOverrides: ${error.message}`);
195
+ }
196
+ return data ?? [];
197
+ },
198
+ async setRolePermissionOverride({ role_id, resource, action, suppress }) {
199
+ if (suppress) {
200
+ const { error: error2 } = await rbac.from("role_permission_overrides").upsert(
201
+ { role_id, resource, action },
202
+ { onConflict: "role_id,resource,action" }
203
+ );
204
+ if (error2) {
205
+ throw new Error(`setRolePermissionOverride(insert): ${error2.message}`);
206
+ }
207
+ return;
208
+ }
209
+ const { error } = await rbac.from("role_permission_overrides").delete().eq("role_id", role_id).eq("resource", resource).eq("action", action);
210
+ if (error) {
211
+ throw new Error(`setRolePermissionOverride(delete): ${error.message}`);
212
+ }
213
+ },
188
214
  async applyTemplateDefaults({ role_id, only_missing = true }) {
189
215
  const { data, error } = await rbac.rpc("apply_template_defaults", {
190
216
  p_role_id: role_id,
@@ -368,6 +394,57 @@ function useAdminResourceDependencies() {
368
394
  [transport]
369
395
  );
370
396
  }
397
+ function useRolePermissionOverrides(roleId) {
398
+ const transport = useAdminTransport();
399
+ const [overrides, setOverrides] = useState(() => /* @__PURE__ */ new Set());
400
+ const [error, setError] = useState(null);
401
+ const fetchOverrides = useCallback(async () => {
402
+ if (!roleId) {
403
+ setOverrides(/* @__PURE__ */ new Set());
404
+ return;
405
+ }
406
+ try {
407
+ const rows = await transport.listRolePermissionOverrides(roleId);
408
+ setOverrides(new Set(rows.map((r) => `${r.resource}:${r.action}`)));
409
+ setError(null);
410
+ } catch (e) {
411
+ setError(e instanceof Error ? e : new Error(String(e)));
412
+ }
413
+ }, [transport, roleId]);
414
+ useEffect(() => {
415
+ void fetchOverrides();
416
+ }, [fetchOverrides]);
417
+ const setOverride = useCallback(
418
+ async (resource, action, suppress) => {
419
+ if (!roleId) {
420
+ return;
421
+ }
422
+ const key = `${resource}:${action}`;
423
+ setOverrides((prev) => {
424
+ const next = new Set(prev);
425
+ if (suppress) {
426
+ next.add(key);
427
+ } else {
428
+ next.delete(key);
429
+ }
430
+ return next;
431
+ });
432
+ try {
433
+ await transport.setRolePermissionOverride({
434
+ role_id: roleId,
435
+ resource,
436
+ action,
437
+ suppress
438
+ });
439
+ } catch (e) {
440
+ setError(e instanceof Error ? e : new Error(String(e)));
441
+ }
442
+ void fetchOverrides();
443
+ },
444
+ [transport, roleId, fetchOverrides]
445
+ );
446
+ return { overrides, setOverride, error, refresh: fetchOverrides };
447
+ }
371
448
  function useCreateCompany() {
372
449
  const transport = useAdminTransport();
373
450
  return useMutation(transport.createCompany);
@@ -379,6 +456,7 @@ function useInviteCompanyMember() {
379
456
  function useRolePermissionGrid(roleId) {
380
457
  const { data, isLoading, error, refresh } = useAdminRolePermissions(roleId);
381
458
  const dependencies = useAdminResourceDependencies();
459
+ const overridesHook = useRolePermissionOverrides(roleId);
382
460
  const setCell = useSetRolePermissionCell();
383
461
  const transport = useAdminTransport();
384
462
  const grid = useMemo(() => {
@@ -410,6 +488,7 @@ function useRolePermissionGrid(roleId) {
410
488
  for (const child of parentsByChild.keys()) {
411
489
  resources.add(child);
412
490
  }
491
+ const overrides = overridesHook.overrides;
413
492
  for (const resource of resources) {
414
493
  const directs = grid[resource];
415
494
  const cellOrigins = {
@@ -419,6 +498,10 @@ function useRolePermissionGrid(roleId) {
419
498
  delete: null
420
499
  };
421
500
  for (const action of ACTIONS) {
501
+ if (overrides.has(`${resource}:${action}`)) {
502
+ cellOrigins[action] = "override";
503
+ continue;
504
+ }
422
505
  if (directs?.[action]) {
423
506
  cellOrigins[action] = "direct";
424
507
  continue;
@@ -432,7 +515,7 @@ function useRolePermissionGrid(roleId) {
432
515
  out[resource] = cellOrigins;
433
516
  }
434
517
  return out;
435
- }, [grid, parentsByChild]);
518
+ }, [grid, parentsByChild, overridesHook.overrides]);
436
519
  const updateCell = useCallback(
437
520
  async (resource, action, value) => {
438
521
  if (!roleId) {
@@ -454,8 +537,17 @@ function useRolePermissionGrid(roleId) {
454
537
  grid,
455
538
  originGrid,
456
539
  parentsByChild,
540
+ /** 0.6.0+. Set of `"<resource>:<action>"` for this role's overrides. */
541
+ overrides: overridesHook.overrides,
542
+ /**
543
+ * 0.6.0+. Suppress (`suppress=true`) or restore (`suppress=false`)
544
+ * an implied permission for this role. The grid + originGrid
545
+ * re-render with `'override'` state on the cell as soon as the
546
+ * optimistic flip lands.
547
+ */
548
+ setOverride: overridesHook.setOverride,
457
549
  isLoading: isLoading || dependencies.isLoading,
458
- error: error ?? dependencies.error,
550
+ error: error ?? dependencies.error ?? overridesHook.error,
459
551
  refresh,
460
552
  updateCell,
461
553
  isUpdating: setCell.isPending,
@@ -469,16 +561,25 @@ import { useMemo as useMemo2 } from "react";
469
561
  import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
470
562
  var ACTIONS2 = ["read", "write", "update", "delete"];
471
563
  function PermissionsMatrix(props) {
472
- const { grid, originGrid, isLoading, error, updateCell, isUpdating } = useRolePermissionGrid(props.roleId);
564
+ const {
565
+ grid,
566
+ originGrid,
567
+ isLoading,
568
+ error,
569
+ updateCell,
570
+ isUpdating,
571
+ setOverride: gridSetOverride
572
+ } = useRolePermissionGrid(props.roleId);
473
573
  const groups = useMemo2(
474
574
  () => groupResources(props.resources),
475
575
  [props.resources]
476
576
  );
477
577
  const isCellEnabled = (resource, action) => {
478
- if (grid[resource]?.[action]) {
479
- return true;
578
+ const origin = originGrid[resource]?.[action];
579
+ if (origin == null || origin === "override") {
580
+ return false;
480
581
  }
481
- return originGrid[resource]?.[action] != null;
582
+ return true;
482
583
  };
483
584
  const cellOrigin = (resource, action) => {
484
585
  const origin = originGrid[resource]?.[action];
@@ -487,11 +588,15 @@ function PermissionsMatrix(props) {
487
588
  const setCell = async (resource, action, value) => {
488
589
  await updateCell(resource, action, value);
489
590
  };
591
+ const setOverride = async (resource, action, suppress) => {
592
+ await gridSetOverride(resource, action, suppress);
593
+ };
490
594
  return /* @__PURE__ */ jsx2(Fragment, { children: props.children({
491
595
  groups,
492
596
  isCellEnabled,
493
597
  cellOrigin,
494
598
  setCell,
599
+ setOverride,
495
600
  isLoading,
496
601
  isUpdating,
497
602
  error,
@@ -636,6 +741,7 @@ export {
636
741
  useDeleteRole,
637
742
  useInviteCompanyMember,
638
743
  useRolePermissionGrid,
744
+ useRolePermissionOverrides,
639
745
  useSetRolePermissionCell,
640
746
  useUpdateRole
641
747
  };