@jmruthers/pace-core 0.5.38 → 0.5.41

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.
Files changed (124) hide show
  1. package/dist/{DataTable-SUM2QMUM.js → DataTable-2HA7VQZ4.js} +2 -2
  2. package/dist/{DataTable-Da0D1BHX.d.ts → DataTable-DqDDvBfI.d.ts} +1 -1
  3. package/dist/{chunk-2XAK7GBW.js → chunk-7SLC6HXW.js} +2 -2
  4. package/dist/{chunk-JHQ7DMPJ.js → chunk-MQS7SEE6.js} +57 -3
  5. package/dist/chunk-MQS7SEE6.js.map +1 -0
  6. package/dist/components.d.ts +2 -2
  7. package/dist/components.js +2 -2
  8. package/dist/hooks.d.ts +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +2 -2
  11. package/dist/rbac/cli/policy-manager.js +278 -0
  12. package/dist/rbac/cli/policy-manager.js.map +1 -0
  13. package/dist/{types-zOqe4P1s.d.ts → types-E5WSpEtz.d.ts} +7 -0
  14. package/dist/utils.d.ts +2 -2
  15. package/dist/utils.js +1 -1
  16. package/docs/api/classes/ErrorBoundary.md +1 -1
  17. package/docs/api/classes/InvalidScopeError.md +1 -1
  18. package/docs/api/classes/MissingUserContextError.md +1 -1
  19. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  20. package/docs/api/classes/PermissionDeniedError.md +1 -1
  21. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  22. package/docs/api/classes/RBACAuditManager.md +1 -1
  23. package/docs/api/classes/RBACCache.md +1 -1
  24. package/docs/api/classes/RBACEngine.md +1 -1
  25. package/docs/api/classes/RBACError.md +1 -1
  26. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  27. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  28. package/docs/api/interfaces/AggregateConfig.md +4 -4
  29. package/docs/api/interfaces/ButtonProps.md +1 -1
  30. package/docs/api/interfaces/CardProps.md +1 -1
  31. package/docs/api/interfaces/ColorPalette.md +1 -1
  32. package/docs/api/interfaces/ColorShade.md +1 -1
  33. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  34. package/docs/api/interfaces/DataTableAction.md +14 -14
  35. package/docs/api/interfaces/DataTableColumn.md +31 -5
  36. package/docs/api/interfaces/DataTableProps.md +1 -1
  37. package/docs/api/interfaces/DataTableToolbarButton.md +7 -7
  38. package/docs/api/interfaces/EmptyStateConfig.md +5 -5
  39. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  40. package/docs/api/interfaces/EventContextType.md +1 -1
  41. package/docs/api/interfaces/EventLogoProps.md +1 -1
  42. package/docs/api/interfaces/EventProviderProps.md +1 -1
  43. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  44. package/docs/api/interfaces/FileUploadProps.md +1 -1
  45. package/docs/api/interfaces/FooterProps.md +1 -1
  46. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  47. package/docs/api/interfaces/InputProps.md +1 -1
  48. package/docs/api/interfaces/LabelProps.md +1 -1
  49. package/docs/api/interfaces/LoginFormProps.md +1 -1
  50. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  51. package/docs/api/interfaces/NavigationContextType.md +1 -1
  52. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  53. package/docs/api/interfaces/NavigationItem.md +1 -1
  54. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  55. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  56. package/docs/api/interfaces/Organisation.md +1 -1
  57. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  58. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  59. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  60. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  61. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  62. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  63. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  64. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  65. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  66. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  67. package/docs/api/interfaces/PaletteData.md +1 -1
  68. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  69. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  70. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  71. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  72. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  73. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  74. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  75. package/docs/api/interfaces/RBACConfig.md +1 -1
  76. package/docs/api/interfaces/RBACContextType.md +1 -1
  77. package/docs/api/interfaces/RBACLogger.md +1 -1
  78. package/docs/api/interfaces/RBACProviderProps.md +1 -1
  79. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  80. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  81. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  82. package/docs/api/interfaces/RouteConfig.md +1 -1
  83. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  84. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  85. package/docs/api/interfaces/StorageConfig.md +1 -1
  86. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  87. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  88. package/docs/api/interfaces/StorageListOptions.md +1 -1
  89. package/docs/api/interfaces/StorageListResult.md +1 -1
  90. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  91. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  92. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  93. package/docs/api/interfaces/StyleImport.md +1 -1
  94. package/docs/api/interfaces/ToastActionElement.md +1 -1
  95. package/docs/api/interfaces/ToastProps.md +1 -1
  96. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  97. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  98. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  99. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  100. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  101. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  102. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  103. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  104. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  105. package/docs/api/interfaces/UserEventAccess.md +1 -1
  106. package/docs/api/interfaces/UserMenuProps.md +1 -1
  107. package/docs/api/interfaces/UserProfile.md +1 -1
  108. package/docs/api/modules.md +2 -2
  109. package/docs/implementation-guides/data-tables.md +189 -0
  110. package/docs/implementation-guides/datatable-filtering.md +313 -0
  111. package/docs/rbac/README-rbac-rls-integration.md +358 -0
  112. package/docs/rbac/examples/rbac-rls-integration-example.md +332 -0
  113. package/docs/rbac/rbac-rls-integration.md +377 -0
  114. package/package.json +19 -3
  115. package/src/components/DataTable/components/ActionButtons.tsx +22 -2
  116. package/src/components/DataTable/components/DataTableCore.tsx +28 -0
  117. package/src/components/DataTable/components/EditableRow.tsx +6 -0
  118. package/src/components/DataTable/components/FilterRow.tsx +14 -2
  119. package/src/components/DataTable/components/UnifiedTableBody.tsx +6 -0
  120. package/src/components/DataTable/types.ts +4 -0
  121. package/src/rbac/cli/policy-manager.ts +443 -0
  122. package/dist/chunk-JHQ7DMPJ.js.map +0 -1
  123. /package/dist/{DataTable-SUM2QMUM.js.map → DataTable-2HA7VQZ4.js.map} +0 -0
  124. /package/dist/{chunk-2XAK7GBW.js.map → chunk-7SLC6HXW.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jmruthers/pace-core",
3
- "version": "0.5.38",
3
+ "version": "0.5.41",
4
4
  "description": "Clean, modern React component library with Tailwind v4 styling and native utilities",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -104,6 +104,10 @@
104
104
  "./source/rbac": {
105
105
  "import": "./src/rbac/index.ts",
106
106
  "default": "./src/rbac/index.ts"
107
+ },
108
+ "./rbac/cli": {
109
+ "import": "./dist/rbac/cli/policy-manager.js",
110
+ "default": "./dist/rbac/cli/policy-manager.js"
107
111
  }
108
112
  },
109
113
  "files": [
@@ -151,7 +155,15 @@
151
155
  "docs:generate": "node scripts/generate-docs.js generate",
152
156
  "docs:update-index": "node scripts/generate-docs.js update-index",
153
157
  "docs:generate-api": "node scripts/generate-docs.js generate-api",
154
- "docs:all": "node scripts/generate-docs.js all"
158
+ "docs:all": "node scripts/generate-docs.js all",
159
+ "_comment_rbac": "RBAC policy management utilities",
160
+ "rbac:list": "node dist/rbac/cli/policy-manager.js list",
161
+ "rbac:register": "node dist/rbac/cli/policy-manager.js register",
162
+ "rbac:update": "node dist/rbac/cli/policy-manager.js update",
163
+ "rbac:update-all": "node dist/rbac/cli/policy-manager.js update-all",
164
+ "rbac:health": "node dist/rbac/cli/policy-manager.js health",
165
+ "rbac:audit": "node dist/rbac/cli/policy-manager.js audit",
166
+ "rbac:test": "node dist/rbac/cli/policy-manager.js test"
155
167
  },
156
168
  "keywords": [
157
169
  "react",
@@ -209,6 +221,10 @@
209
221
  },
210
222
  "dependencies": {
211
223
  "@supabase/supabase-js": "^2.49.10",
212
- "@tanstack/react-query": "^5.56.2"
224
+ "@tanstack/react-query": "^5.56.2",
225
+ "commander": "^12.0.0",
226
+ "chalk": "^5.3.0",
227
+ "ora": "^8.0.1",
228
+ "cli-table3": "^0.6.3"
213
229
  }
214
230
  }
@@ -135,7 +135,17 @@ export function ActionButtons({
135
135
  key={actionIndex}
136
136
  variant={action.variant === 'destructive' ? 'destructive' : 'ghost'}
137
137
  size="sm"
138
- onClick={() => action.onClick(row.original)}
138
+ onClick={() => {
139
+ // DEBUG: Log action click
140
+ if (process.env.NODE_ENV === 'development') {
141
+ console.log('[ActionButtons] Action clicked:', {
142
+ actionLabel: action.label,
143
+ rowOriginal: row.original,
144
+ actionOnClick: typeof action.onClick
145
+ });
146
+ }
147
+ action.onClick(row.original);
148
+ }}
139
149
  disabled={isDisabled}
140
150
  aria-disabled={isDisabled}
141
151
  data-testid={action.testId}
@@ -183,7 +193,17 @@ export function ActionButtons({
183
193
  <SelectItem
184
194
  key={actionIndex}
185
195
  value={`action-${actionIndex}`}
186
- onClick={() => action.onClick(row.original)}
196
+ onClick={() => {
197
+ // DEBUG: Log action click
198
+ if (process.env.NODE_ENV === 'development') {
199
+ console.log('[ActionButtons] Dropdown action clicked:', {
200
+ actionLabel: action.label,
201
+ rowOriginal: row.original,
202
+ actionOnClick: typeof action.onClick
203
+ });
204
+ }
205
+ action.onClick(row.original);
206
+ }}
187
207
  className={action.variant === 'destructive' ? 'text-acc-600' : ''}
188
208
  >
189
209
  {Icon && <Icon className="mr-2 h-4 w-4" />}
@@ -687,6 +687,25 @@ function DataTableInternal<TData extends DataRecord>({
687
687
  // Create a new array to avoid mutating the original
688
688
  const result = [...actions];
689
689
 
690
+ // DEBUG: Log action configuration
691
+ if (process.env.NODE_ENV === 'development') {
692
+ console.log('[DataTable] Action Configuration Debug:', {
693
+ originalActions: actions.length,
694
+ secureFeatures: {
695
+ editing: secureFeatures.editing,
696
+ deletion: secureFeatures.deletion
697
+ },
698
+ secureHandlers: {
699
+ onEditRow: !!secureHandlers.onEditRow,
700
+ onDeleteRow: !!secureHandlers.onDeleteRow
701
+ },
702
+ permissions: {
703
+ canUpdate: permissions.canUpdate.can,
704
+ canDelete: permissions.canDelete.can
705
+ }
706
+ });
707
+ }
708
+
690
709
  // Add Edit action with RBAC check
691
710
  if (secureFeatures.editing && secureHandlers.onEditRow && !result.some(a => a.label === 'Edit')) {
692
711
  result.push({
@@ -723,6 +742,15 @@ function DataTableInternal<TData extends DataRecord>({
723
742
  });
724
743
  }
725
744
 
745
+ // DEBUG: Log final actions
746
+ if (process.env.NODE_ENV === 'development') {
747
+ console.log('[DataTable] Final Actions:', {
748
+ totalActions: result.length,
749
+ actionLabels: result.map(a => a.label),
750
+ hiddenActions: result.filter(a => a.hidden).map(a => a.label)
751
+ });
752
+ }
753
+
726
754
  return result;
727
755
  }, [actions, secureFeatures, permissions, secureHandlers, getRowId, tableActions]);
728
756
 
@@ -28,6 +28,12 @@ const renderEditField = (
28
28
  ) => {
29
29
  const columnDef = column.columnDef;
30
30
 
31
+ // Check if column is editable (default: true)
32
+ if (columnDef.editable === false) {
33
+ // Return the original value as text if column is not editable
34
+ return <span className="text-sm text-gray-600">{value || ''}</span>;
35
+ }
36
+
31
37
  // Check for custom field type
32
38
  if (columnDef.fieldType === 'select' && columnDef.fieldOptions) {
33
39
  // Use editAccessorKey if specified, otherwise use the column id
@@ -16,8 +16,14 @@ export function FilterRow<TData>({ table, visibleColumns }: FilterRowProps<TData
16
16
  const column = table.getColumn(columnId);
17
17
  if (!column) return [];
18
18
 
19
- // Check if column has explicit field options
20
19
  const columnDef = column.columnDef as any;
20
+
21
+ // Check for filterSelectOptions first (preferred for filters)
22
+ if (columnDef.filterSelectOptions && Array.isArray(columnDef.filterSelectOptions)) {
23
+ return columnDef.filterSelectOptions;
24
+ }
25
+
26
+ // Check if column has explicit field options
21
27
  if (columnDef.fieldOptions && Array.isArray(columnDef.fieldOptions)) {
22
28
  return columnDef.fieldOptions;
23
29
  }
@@ -41,11 +47,17 @@ export function FilterRow<TData>({ table, visibleColumns }: FilterRowProps<TData
41
47
  const column = table.getColumn(columnId);
42
48
  if (!column) return 'text';
43
49
 
44
- // Check if column has explicit filter type configuration
45
50
  const columnDef = column.columnDef as any;
51
+
52
+ // Check if column has explicit filter type configuration
46
53
  if (columnDef.filterType) {
47
54
  return columnDef.filterType;
48
55
  }
56
+
57
+ // Auto-detect select filter if filterSelectOptions is provided
58
+ if (columnDef.filterSelectOptions && Array.isArray(columnDef.filterSelectOptions)) {
59
+ return 'select';
60
+ }
49
61
 
50
62
  // Check if it's a date column
51
63
  if (columnId.toLowerCase().includes('date') || columnId.toLowerCase().includes('time')) {
@@ -156,6 +156,12 @@ const renderEditField = (
156
156
  ) => {
157
157
  const columnDef = column.columnDef;
158
158
 
159
+ // Check if column is editable (default: true)
160
+ if (columnDef.editable === false) {
161
+ // Return the original value as text if column is not editable
162
+ return <span className="text-sm text-gray-600">{value || ''}</span>;
163
+ }
164
+
159
165
  // Check for custom field type
160
166
  if (columnDef.fieldType === 'select' && columnDef.fieldOptions) {
161
167
  // Use editAccessorKey if specified, otherwise use the column id
@@ -234,6 +234,10 @@ export interface DataTableColumn<TData extends DataRecord = DataRecord> extends
234
234
  fieldOptions?: Array<{ value: string | number; label: string }>;
235
235
  /** Filter type for column filtering (text, select, number, date) */
236
236
  filterType?: 'text' | 'select' | 'number' | 'date';
237
+ /** Options for select filters (alternative to fieldOptions) */
238
+ filterSelectOptions?: Array<{ value: string | number; label: string }>;
239
+ /** Whether this column is editable in edit mode (default: true) */
240
+ editable?: boolean;
237
241
 
238
242
  // Hierarchical rendering
239
243
  /** Custom renderer for parent rows */
@@ -0,0 +1,443 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * RBAC Policy Manager CLI Tool
5
+ *
6
+ * This tool provides command-line interface for managing RBAC-RLS integration policies.
7
+ * It allows developers and administrators to easily manage dynamic permission policies.
8
+ */
9
+
10
+ import { createClient } from '@supabase/supabase-js';
11
+ import { Command } from 'commander';
12
+ import chalk from 'chalk';
13
+ import ora from 'ora';
14
+ import Table from 'cli-table3';
15
+
16
+ interface PolicyConfig {
17
+ table_name: string;
18
+ page_name: string;
19
+ app_name: string;
20
+ organisation_column: string;
21
+ event_column?: string;
22
+ operations: string[];
23
+ is_active: boolean;
24
+ }
25
+
26
+ interface PolicyAudit {
27
+ table_name: string;
28
+ policy_name: string;
29
+ operation: string;
30
+ action: string;
31
+ changed_at: string;
32
+ success: boolean;
33
+ error_message?: string;
34
+ }
35
+
36
+ interface HealthCheck {
37
+ table_name: string;
38
+ policy_name: string;
39
+ operation: string;
40
+ is_healthy: boolean;
41
+ issues: string[];
42
+ }
43
+
44
+ class PolicyManager {
45
+ private supabase: any;
46
+
47
+ constructor(url: string, key: string) {
48
+ this.supabase = createClient(url, key);
49
+ }
50
+
51
+ /**
52
+ * List all registered tables
53
+ */
54
+ async listTables(): Promise<void> {
55
+ const spinner = ora('Fetching registered tables...').start();
56
+
57
+ try {
58
+ const { data, error } = await this.supabase
59
+ .from('rbac_policy_configs')
60
+ .select('*')
61
+ .eq('is_active', true)
62
+ .order('table_name');
63
+
64
+ if (error) throw error;
65
+
66
+ spinner.succeed('Fetched registered tables');
67
+
68
+ if (data.length === 0) {
69
+ console.log(chalk.yellow('No tables registered for RBAC policy management.'));
70
+ return;
71
+ }
72
+
73
+ const table = new Table({
74
+ head: ['Table', 'Page', 'App', 'Org Column', 'Event Column', 'Operations'],
75
+ colWidths: [20, 15, 10, 15, 15, 30]
76
+ });
77
+
78
+ data.forEach((config: PolicyConfig) => {
79
+ table.push([
80
+ config.table_name,
81
+ config.page_name,
82
+ config.app_name,
83
+ config.organisation_column,
84
+ config.event_column || 'N/A',
85
+ config.operations.join(', ')
86
+ ]);
87
+ });
88
+
89
+ console.log(table.toString());
90
+ } catch (error) {
91
+ spinner.fail('Failed to fetch tables');
92
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Register a new table for RBAC policy management
98
+ */
99
+ async registerTable(
100
+ tableName: string,
101
+ pageName: string,
102
+ appName: string = 'CAKE',
103
+ orgColumn: string = 'organisation_id',
104
+ eventColumn?: string,
105
+ operations: string[] = ['read', 'create', 'update', 'delete']
106
+ ): Promise<void> {
107
+ const spinner = ora(`Registering table ${tableName}...`).start();
108
+
109
+ try {
110
+ const { data, error } = await this.supabase
111
+ .rpc('register_rbac_table', {
112
+ p_table_name: tableName,
113
+ p_page_name: pageName,
114
+ p_app_name: appName,
115
+ p_organisation_column: orgColumn,
116
+ p_event_column: eventColumn,
117
+ p_operations: operations
118
+ });
119
+
120
+ if (error) throw error;
121
+
122
+ if (data) {
123
+ spinner.succeed(`Successfully registered table ${tableName}`);
124
+ } else {
125
+ spinner.fail(`Failed to register table ${tableName}`);
126
+ }
127
+ } catch (error) {
128
+ spinner.fail(`Failed to register table ${tableName}`);
129
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Update policies for a specific table
135
+ */
136
+ async updateTable(tableName: string, pageName: string, appName: string = 'CAKE'): Promise<void> {
137
+ const spinner = ora(`Updating policies for ${tableName}...`).start();
138
+
139
+ try {
140
+ const { data, error } = await this.supabase
141
+ .rpc('update_rbac_policies_for_table', {
142
+ p_table_name: tableName,
143
+ p_page_name: pageName,
144
+ p_app_name: appName
145
+ });
146
+
147
+ if (error) throw error;
148
+
149
+ if (data) {
150
+ spinner.succeed(`Successfully updated policies for ${tableName}`);
151
+ } else {
152
+ spinner.fail(`Failed to update policies for ${tableName}`);
153
+ }
154
+ } catch (error) {
155
+ spinner.fail(`Failed to update policies for ${tableName}`);
156
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Update all RBAC policies
162
+ */
163
+ async updateAll(): Promise<void> {
164
+ const spinner = ora('Updating all RBAC policies...').start();
165
+
166
+ try {
167
+ const { data, error } = await this.supabase
168
+ .rpc('update_all_rbac_policies');
169
+
170
+ if (error) throw error;
171
+
172
+ spinner.succeed('Updated all RBAC policies');
173
+
174
+ // Display results
175
+ const table = new Table({
176
+ head: ['Table', 'Page', 'App', 'Success', 'Error'],
177
+ colWidths: [20, 15, 10, 10, 30]
178
+ });
179
+
180
+ data.forEach((result: any) => {
181
+ table.push([
182
+ result.table_name,
183
+ result.page_name,
184
+ result.app_name,
185
+ result.success ? chalk.green('✓') : chalk.red('✗'),
186
+ result.error_message || ''
187
+ ]);
188
+ });
189
+
190
+ console.log(table.toString());
191
+ } catch (error) {
192
+ spinner.fail('Failed to update all policies');
193
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Check policy health
199
+ */
200
+ async checkHealth(): Promise<void> {
201
+ const spinner = ora('Checking policy health...').start();
202
+
203
+ try {
204
+ const { data, error } = await this.supabase
205
+ .rpc('check_rbac_policy_health');
206
+
207
+ if (error) throw error;
208
+
209
+ spinner.succeed('Policy health check complete');
210
+
211
+ if (data.length === 0) {
212
+ console.log(chalk.yellow('No policies found.'));
213
+ return;
214
+ }
215
+
216
+ const healthyPolicies = data.filter((h: HealthCheck) => h.is_healthy);
217
+ const unhealthyPolicies = data.filter((h: HealthCheck) => !h.is_healthy);
218
+
219
+ console.log(chalk.green(`✓ ${healthyPolicies.length} healthy policies`));
220
+ console.log(chalk.red(`✗ ${unhealthyPolicies.length} unhealthy policies`));
221
+
222
+ if (unhealthyPolicies.length > 0) {
223
+ console.log('\n' + chalk.red('Unhealthy Policies:'));
224
+ const table = new Table({
225
+ head: ['Table', 'Policy', 'Operation', 'Issues'],
226
+ colWidths: [20, 25, 10, 40]
227
+ });
228
+
229
+ unhealthyPolicies.forEach((policy: HealthCheck) => {
230
+ table.push([
231
+ policy.table_name,
232
+ policy.policy_name,
233
+ policy.operation,
234
+ policy.issues.join(', ')
235
+ ]);
236
+ });
237
+
238
+ console.log(table.toString());
239
+ }
240
+ } catch (error) {
241
+ spinner.fail('Failed to check policy health');
242
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Show policy audit log
248
+ */
249
+ async showAudit(limit: number = 20): Promise<void> {
250
+ const spinner = ora('Fetching audit log...').start();
251
+
252
+ try {
253
+ const { data, error } = await this.supabase
254
+ .from('rbac_policy_audit')
255
+ .select('*')
256
+ .order('changed_at', { ascending: false })
257
+ .limit(limit);
258
+
259
+ if (error) throw error;
260
+
261
+ spinner.succeed('Fetched audit log');
262
+
263
+ if (data.length === 0) {
264
+ console.log(chalk.yellow('No audit entries found.'));
265
+ return;
266
+ }
267
+
268
+ const table = new Table({
269
+ head: ['Table', 'Policy', 'Operation', 'Action', 'Success', 'Changed At'],
270
+ colWidths: [15, 20, 10, 10, 8, 20]
271
+ });
272
+
273
+ data.forEach((audit: PolicyAudit) => {
274
+ table.push([
275
+ audit.table_name,
276
+ audit.policy_name,
277
+ audit.operation,
278
+ audit.action,
279
+ audit.success ? chalk.green('✓') : chalk.red('✗'),
280
+ new Date(audit.changed_at).toLocaleString()
281
+ ]);
282
+ });
283
+
284
+ console.log(table.toString());
285
+ } catch (error) {
286
+ spinner.fail('Failed to fetch audit log');
287
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Test permission check
293
+ */
294
+ async testPermission(
295
+ operation: string,
296
+ pageName: string,
297
+ orgId: string,
298
+ eventId?: string,
299
+ appName: string = 'CAKE'
300
+ ): Promise<void> {
301
+ const spinner = ora('Testing permission check...').start();
302
+
303
+ try {
304
+ const { data, error } = await this.supabase
305
+ .rpc('check_rbac_permission_with_context', {
306
+ p_operation: operation,
307
+ p_page_name: pageName,
308
+ p_resource_organisation_id: orgId,
309
+ p_resource_event_id: eventId,
310
+ p_app_name: appName
311
+ });
312
+
313
+ if (error) throw error;
314
+
315
+ spinner.succeed('Permission check complete');
316
+
317
+ const result = data ? chalk.green('ALLOWED') : chalk.red('DENIED');
318
+ console.log(`Permission: ${operation} on ${pageName} = ${result}`);
319
+ } catch (error) {
320
+ spinner.fail('Failed to test permission');
321
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
322
+ }
323
+ }
324
+ }
325
+
326
+ // CLI Setup
327
+ const program = new Command();
328
+
329
+ program
330
+ .name('rbac-policy-manager')
331
+ .description('CLI tool for managing RBAC-RLS integration policies')
332
+ .version('1.0.0');
333
+
334
+ // Global options
335
+ program
336
+ .option('-u, --url <url>', 'Supabase URL', process.env.SUPABASE_URL)
337
+ .option('-k, --key <key>', 'Supabase service key', process.env.SUPABASE_SERVICE_ROLE_KEY)
338
+ .option('--verbose', 'Enable verbose output');
339
+
340
+ // List tables command
341
+ program
342
+ .command('list')
343
+ .description('List all registered tables')
344
+ .action(async (options) => {
345
+ const url = options.parent?.url || process.env.SUPABASE_URL;
346
+ const key = options.parent?.key || process.env.SUPABASE_SERVICE_ROLE_KEY;
347
+ const manager = new PolicyManager(url, key);
348
+ await manager.listTables();
349
+ });
350
+
351
+ // Register table command
352
+ program
353
+ .command('register <tableName> <pageName>')
354
+ .description('Register a table for RBAC policy management')
355
+ .option('-a, --app <appName>', 'App name', 'CAKE')
356
+ .option('-o, --org-column <column>', 'Organisation column name', 'organisation_id')
357
+ .option('-e, --event-column <column>', 'Event column name')
358
+ .option('--operations <operations>', 'Comma-separated operations', 'read,create,update,delete')
359
+ .action(async (tableName, pageName, options) => {
360
+ const operations = options.operations.split(',').map((op: string) => op.trim());
361
+ const url = options.parent?.url || process.env.SUPABASE_URL;
362
+ const key = options.parent?.key || process.env.SUPABASE_SERVICE_ROLE_KEY;
363
+ const manager = new PolicyManager(url, key);
364
+ await manager.registerTable(
365
+ tableName,
366
+ pageName,
367
+ options.app,
368
+ options.orgColumn,
369
+ options.eventColumn,
370
+ operations
371
+ );
372
+ });
373
+
374
+ // Update table command
375
+ program
376
+ .command('update <tableName> <pageName>')
377
+ .description('Update policies for a specific table')
378
+ .option('-a, --app <appName>', 'App name', 'CAKE')
379
+ .action(async (tableName, pageName, options) => {
380
+ const url = options.parent?.url || process.env.SUPABASE_URL;
381
+ const key = options.parent?.key || process.env.SUPABASE_SERVICE_ROLE_KEY;
382
+ const manager = new PolicyManager(url, key);
383
+ await manager.updateTable(tableName, pageName, options.app);
384
+ });
385
+
386
+ // Update all command
387
+ program
388
+ .command('update-all')
389
+ .description('Update all RBAC policies')
390
+ .action(async (options) => {
391
+ const url = options.url || process.env.SUPABASE_URL;
392
+ const key = options.key || process.env.SUPABASE_SERVICE_ROLE_KEY;
393
+ const manager = new PolicyManager(url, key);
394
+ await manager.updateAll();
395
+ });
396
+
397
+ // Health check command
398
+ program
399
+ .command('health')
400
+ .description('Check policy health')
401
+ .action(async (options) => {
402
+ const url = options.url || process.env.SUPABASE_URL;
403
+ const key = options.key || process.env.SUPABASE_SERVICE_ROLE_KEY;
404
+ const manager = new PolicyManager(url, key);
405
+ await manager.checkHealth();
406
+ });
407
+
408
+ // Audit command
409
+ program
410
+ .command('audit')
411
+ .description('Show policy audit log')
412
+ .option('-l, --limit <number>', 'Number of entries to show', '20')
413
+ .action(async (options) => {
414
+ const url = options.parent?.url || process.env.SUPABASE_URL;
415
+ const key = options.parent?.key || process.env.SUPABASE_SERVICE_ROLE_KEY;
416
+ const manager = new PolicyManager(url, key);
417
+ await manager.showAudit(parseInt(options.limit));
418
+ });
419
+
420
+ // Test permission command
421
+ program
422
+ .command('test <operation> <pageName> <orgId>')
423
+ .description('Test a permission check')
424
+ .option('-e, --event-id <eventId>', 'Event ID')
425
+ .option('-a, --app <appName>', 'App name', 'CAKE')
426
+ .action(async (operation, pageName, orgId, options) => {
427
+ const url = options.parent?.url || process.env.SUPABASE_URL;
428
+ const key = options.parent?.key || process.env.SUPABASE_SERVICE_ROLE_KEY;
429
+ const manager = new PolicyManager(url, key);
430
+ await manager.testPermission(operation, pageName, orgId, options.eventId, options.app);
431
+ });
432
+
433
+ // Parse command line arguments
434
+ program.parse(process.argv);
435
+
436
+ // Handle missing required options
437
+ if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_ROLE_KEY) {
438
+ console.error(chalk.red('Error: Missing required environment variables'));
439
+ console.error('Please set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY');
440
+ process.exit(1);
441
+ }
442
+
443
+ export { PolicyManager };