tlc-claude-code 1.3.0 → 1.4.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.
Files changed (105) hide show
  1. package/dashboard/dist/components/AuditPane.d.ts +30 -0
  2. package/dashboard/dist/components/AuditPane.js +127 -0
  3. package/dashboard/dist/components/AuditPane.test.d.ts +1 -0
  4. package/dashboard/dist/components/AuditPane.test.js +339 -0
  5. package/dashboard/dist/components/CompliancePane.d.ts +39 -0
  6. package/dashboard/dist/components/CompliancePane.js +96 -0
  7. package/dashboard/dist/components/CompliancePane.test.d.ts +1 -0
  8. package/dashboard/dist/components/CompliancePane.test.js +183 -0
  9. package/dashboard/dist/components/SSOPane.d.ts +36 -0
  10. package/dashboard/dist/components/SSOPane.js +71 -0
  11. package/dashboard/dist/components/SSOPane.test.d.ts +1 -0
  12. package/dashboard/dist/components/SSOPane.test.js +155 -0
  13. package/dashboard/dist/components/WorkspaceDocsPane.js +0 -16
  14. package/dashboard/dist/components/WorkspacePane.d.ts +1 -1
  15. package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
  16. package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
  17. package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
  18. package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
  19. package/package.json +1 -1
  20. package/server/lib/access-control-doc.js +541 -0
  21. package/server/lib/access-control-doc.test.js +672 -0
  22. package/server/lib/adr-generator.js +423 -0
  23. package/server/lib/adr-generator.test.js +586 -0
  24. package/server/lib/agent-progress-monitor.js +223 -0
  25. package/server/lib/agent-progress-monitor.test.js +202 -0
  26. package/server/lib/audit-attribution.js +191 -0
  27. package/server/lib/audit-attribution.test.js +359 -0
  28. package/server/lib/audit-classifier.js +202 -0
  29. package/server/lib/audit-classifier.test.js +209 -0
  30. package/server/lib/audit-command.js +275 -0
  31. package/server/lib/audit-command.test.js +325 -0
  32. package/server/lib/audit-exporter.js +380 -0
  33. package/server/lib/audit-exporter.test.js +464 -0
  34. package/server/lib/audit-logger.js +236 -0
  35. package/server/lib/audit-logger.test.js +364 -0
  36. package/server/lib/audit-query.js +257 -0
  37. package/server/lib/audit-query.test.js +352 -0
  38. package/server/lib/audit-storage.js +269 -0
  39. package/server/lib/audit-storage.test.js +272 -0
  40. package/server/lib/bulk-repo-init.js +342 -0
  41. package/server/lib/bulk-repo-init.test.js +388 -0
  42. package/server/lib/compliance-checklist.js +866 -0
  43. package/server/lib/compliance-checklist.test.js +476 -0
  44. package/server/lib/compliance-command.js +616 -0
  45. package/server/lib/compliance-command.test.js +551 -0
  46. package/server/lib/compliance-reporter.js +692 -0
  47. package/server/lib/compliance-reporter.test.js +707 -0
  48. package/server/lib/data-flow-doc.js +665 -0
  49. package/server/lib/data-flow-doc.test.js +659 -0
  50. package/server/lib/ephemeral-storage.js +249 -0
  51. package/server/lib/ephemeral-storage.test.js +254 -0
  52. package/server/lib/evidence-collector.js +627 -0
  53. package/server/lib/evidence-collector.test.js +901 -0
  54. package/server/lib/flow-diagram-generator.js +474 -0
  55. package/server/lib/flow-diagram-generator.test.js +446 -0
  56. package/server/lib/idp-manager.js +626 -0
  57. package/server/lib/idp-manager.test.js +587 -0
  58. package/server/lib/memory-exclusion.js +326 -0
  59. package/server/lib/memory-exclusion.test.js +241 -0
  60. package/server/lib/mfa-handler.js +452 -0
  61. package/server/lib/mfa-handler.test.js +490 -0
  62. package/server/lib/oauth-flow.js +375 -0
  63. package/server/lib/oauth-flow.test.js +487 -0
  64. package/server/lib/oauth-registry.js +190 -0
  65. package/server/lib/oauth-registry.test.js +306 -0
  66. package/server/lib/readme-generator.js +490 -0
  67. package/server/lib/readme-generator.test.js +493 -0
  68. package/server/lib/repo-dependency-tracker.js +261 -0
  69. package/server/lib/repo-dependency-tracker.test.js +350 -0
  70. package/server/lib/retention-policy.js +281 -0
  71. package/server/lib/retention-policy.test.js +486 -0
  72. package/server/lib/role-mapper.js +236 -0
  73. package/server/lib/role-mapper.test.js +395 -0
  74. package/server/lib/saml-provider.js +765 -0
  75. package/server/lib/saml-provider.test.js +643 -0
  76. package/server/lib/security-policy-generator.js +682 -0
  77. package/server/lib/security-policy-generator.test.js +544 -0
  78. package/server/lib/sensitive-detector.js +112 -0
  79. package/server/lib/sensitive-detector.test.js +209 -0
  80. package/server/lib/service-interaction-diagram.js +700 -0
  81. package/server/lib/service-interaction-diagram.test.js +638 -0
  82. package/server/lib/service-summary.js +553 -0
  83. package/server/lib/service-summary.test.js +619 -0
  84. package/server/lib/session-purge.js +460 -0
  85. package/server/lib/session-purge.test.js +312 -0
  86. package/server/lib/sso-command.js +544 -0
  87. package/server/lib/sso-command.test.js +552 -0
  88. package/server/lib/sso-session.js +492 -0
  89. package/server/lib/sso-session.test.js +670 -0
  90. package/server/lib/workspace-command.js +249 -0
  91. package/server/lib/workspace-command.test.js +264 -0
  92. package/server/lib/workspace-config.js +270 -0
  93. package/server/lib/workspace-config.test.js +312 -0
  94. package/server/lib/workspace-docs-command.js +547 -0
  95. package/server/lib/workspace-docs-command.test.js +692 -0
  96. package/server/lib/workspace-memory.js +451 -0
  97. package/server/lib/workspace-memory.test.js +403 -0
  98. package/server/lib/workspace-scanner.js +452 -0
  99. package/server/lib/workspace-scanner.test.js +677 -0
  100. package/server/lib/workspace-test-runner.js +315 -0
  101. package/server/lib/workspace-test-runner.test.js +294 -0
  102. package/server/lib/zero-retention-command.js +439 -0
  103. package/server/lib/zero-retention-command.test.js +448 -0
  104. package/server/lib/zero-retention.js +322 -0
  105. package/server/lib/zero-retention.test.js +258 -0
@@ -0,0 +1,160 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
+ import { render } from 'ink-testing-library';
4
+ import { ZeroRetentionPane } from './ZeroRetentionPane.js';
5
+ describe('ZeroRetentionPane', () => {
6
+ beforeEach(() => {
7
+ vi.useFakeTimers();
8
+ });
9
+ afterEach(() => {
10
+ vi.useRealTimers();
11
+ });
12
+ describe('enabled state', () => {
13
+ it('renders enabled state correctly', () => {
14
+ const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: true }));
15
+ const output = lastFrame();
16
+ expect(output).toContain('Zero-Retention');
17
+ expect(output).toMatch(/enabled|active|on/i);
18
+ });
19
+ });
20
+ describe('disabled state', () => {
21
+ it('renders disabled state correctly', () => {
22
+ const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: false }));
23
+ const output = lastFrame();
24
+ expect(output).toContain('Zero-Retention');
25
+ expect(output).toMatch(/disabled|inactive|off/i);
26
+ });
27
+ });
28
+ describe('toggle callback', () => {
29
+ it('toggle calls onToggle callback', () => {
30
+ const onToggle = vi.fn();
31
+ const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: false, isActive: true, onToggle: onToggle }));
32
+ // Verify toggle control is shown when active and onToggle is provided
33
+ const output = lastFrame();
34
+ expect(output).toMatch(/\[t\]|toggle/i);
35
+ });
36
+ });
37
+ describe('retention policy summary', () => {
38
+ it('shows retention policy summary', () => {
39
+ const policy = {
40
+ retention: 'immediate',
41
+ persist: false,
42
+ sensitivityLevels: {
43
+ critical: { retention: 'immediate', persist: false },
44
+ high: { retention: 'immediate', persist: false },
45
+ medium: { retention: 'session', persist: false },
46
+ low: { retention: '24h', persist: true },
47
+ },
48
+ };
49
+ const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: true, policy: policy }));
50
+ const output = lastFrame();
51
+ expect(output).toContain('Policy');
52
+ expect(output).toMatch(/immediate|session|purge/i);
53
+ });
54
+ });
55
+ describe('purge activity list', () => {
56
+ it('shows purge activity list', () => {
57
+ const purgeHistory = [
58
+ {
59
+ id: '1',
60
+ timestamp: '2024-01-15T10:30:00.000Z',
61
+ itemCount: 5,
62
+ dataTypes: ['secrets', 'pii'],
63
+ },
64
+ {
65
+ id: '2',
66
+ timestamp: '2024-01-15T10:35:00.000Z',
67
+ itemCount: 3,
68
+ dataTypes: ['general'],
69
+ },
70
+ ];
71
+ const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: true, purgeHistory: purgeHistory }));
72
+ const output = lastFrame();
73
+ expect(output).toContain('Purge');
74
+ expect(output).toContain('5');
75
+ expect(output).toContain('3');
76
+ });
77
+ });
78
+ describe('sensitive data warning', () => {
79
+ it('shows warning for sensitive data', () => {
80
+ const sensitiveDataDetected = {
81
+ detected: true,
82
+ count: 3,
83
+ types: ['api_key', 'password', 'pii'],
84
+ };
85
+ const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: true, sensitiveDataDetected: sensitiveDataDetected }));
86
+ const output = lastFrame();
87
+ expect(output).toMatch(/warning|sensitive|detected/i);
88
+ expect(output).toContain('3');
89
+ });
90
+ });
91
+ describe('empty purge history', () => {
92
+ it('handles empty purge history', () => {
93
+ const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: true, purgeHistory: [] }));
94
+ const output = lastFrame();
95
+ expect(output).toContain('Zero-Retention');
96
+ expect(output).toMatch(/no.*purge|empty|none/i);
97
+ });
98
+ });
99
+ describe('policy formatting', () => {
100
+ it('formats policy for display', () => {
101
+ const policy = {
102
+ retention: 'immediate',
103
+ persist: false,
104
+ dataTypes: {
105
+ secrets: { retention: 'immediate', persist: false },
106
+ pii: { retention: 'session', persist: false },
107
+ general: { retention: '7d', persist: true },
108
+ },
109
+ };
110
+ const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: true, policy: policy }));
111
+ const output = lastFrame();
112
+ // Should display policy rules in a readable format
113
+ expect(output).toContain('Policy');
114
+ expect(output).toBeDefined();
115
+ });
116
+ });
117
+ describe('subsystem status', () => {
118
+ it('shows subsystem status when provided', () => {
119
+ const subsystems = {
120
+ ephemeralStorage: true,
121
+ sessionPurge: true,
122
+ memoryExclusion: true,
123
+ };
124
+ const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: true, subsystems: subsystems }));
125
+ const output = lastFrame();
126
+ expect(output).toMatch(/ephemeral|storage/i);
127
+ expect(output).toMatch(/session|purge/i);
128
+ });
129
+ });
130
+ describe('configuration validation', () => {
131
+ it('shows validation warnings when present', () => {
132
+ const validation = {
133
+ valid: false,
134
+ conflicts: ['Ephemeral storage has basePath set'],
135
+ warnings: ['Audit logging conflicts with zero-retention'],
136
+ };
137
+ const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: true, validation: validation }));
138
+ const output = lastFrame();
139
+ expect(output).toMatch(/conflict|warning/i);
140
+ });
141
+ it('shows valid status when no conflicts', () => {
142
+ const validation = {
143
+ valid: true,
144
+ conflicts: [],
145
+ warnings: [],
146
+ };
147
+ const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: true, validation: validation }));
148
+ const output = lastFrame();
149
+ expect(output).toMatch(/valid|ok|configured/i);
150
+ });
151
+ });
152
+ describe('keyboard controls', () => {
153
+ it('shows controls when active', () => {
154
+ const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: false, isActive: true }));
155
+ const output = lastFrame();
156
+ // Should show control hints
157
+ expect(output).toMatch(/\[t\]|toggle|enable|disable/i);
158
+ });
159
+ });
160
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tlc-claude-code",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "TLC - Test Led Coding for Claude Code",
5
5
  "bin": {
6
6
  "tlc": "./bin/tlc.js",
@@ -0,0 +1,541 @@
1
+ /**
2
+ * Access Control Documenter
3
+ * Documents who has access to what for compliance and auditing
4
+ */
5
+
6
+ // TLC roles and permissions (from auth-system.js)
7
+ const USER_ROLES = {
8
+ ADMIN: 'admin',
9
+ ENGINEER: 'engineer',
10
+ QA: 'qa',
11
+ PO: 'po',
12
+ };
13
+
14
+ const ROLE_PERMISSIONS = {
15
+ [USER_ROLES.ADMIN]: ['*'],
16
+ [USER_ROLES.ENGINEER]: ['read', 'write', 'deploy', 'claim', 'release'],
17
+ [USER_ROLES.QA]: ['read', 'verify', 'bug', 'test'],
18
+ [USER_ROLES.PO]: ['read', 'plan', 'verify', 'approve'],
19
+ };
20
+
21
+ const ROLE_DESCRIPTIONS = {
22
+ [USER_ROLES.ADMIN]: 'Full system administrator with all permissions',
23
+ [USER_ROLES.ENGINEER]: 'Developer with read/write, deploy, and task management',
24
+ [USER_ROLES.QA]: 'Quality assurance with testing and verification access',
25
+ [USER_ROLES.PO]: 'Product owner with planning and approval permissions',
26
+ };
27
+
28
+ // All possible permissions (for expansion and orphan detection)
29
+ const ALL_PERMISSIONS = [
30
+ 'read',
31
+ 'write',
32
+ 'deploy',
33
+ 'claim',
34
+ 'release',
35
+ 'verify',
36
+ 'bug',
37
+ 'test',
38
+ 'plan',
39
+ 'approve',
40
+ ];
41
+
42
+ // Role priority for sorting (admin first, then alphabetical)
43
+ const ROLE_PRIORITY = {
44
+ [USER_ROLES.ADMIN]: 0,
45
+ [USER_ROLES.ENGINEER]: 1,
46
+ [USER_ROLES.PO]: 2,
47
+ [USER_ROLES.QA]: 3,
48
+ };
49
+
50
+ /**
51
+ * List all users with their roles
52
+ * @param {Object[]} users - Array of user objects
53
+ * @returns {Object[]} Sanitized users with roles, sorted by role then name
54
+ */
55
+ function listUsers(users) {
56
+ if (!users || !Array.isArray(users) || users.length === 0) {
57
+ return [];
58
+ }
59
+
60
+ // Sanitize and extract relevant fields
61
+ const sanitized = users.map((user) => ({
62
+ id: user.id,
63
+ email: user.email,
64
+ name: user.name,
65
+ role: user.role,
66
+ }));
67
+
68
+ // Sort by role priority, then by name
69
+ sanitized.sort((a, b) => {
70
+ const rolePriorityA = ROLE_PRIORITY[a.role] ?? 999;
71
+ const rolePriorityB = ROLE_PRIORITY[b.role] ?? 999;
72
+
73
+ if (rolePriorityA !== rolePriorityB) {
74
+ return rolePriorityA - rolePriorityB;
75
+ }
76
+
77
+ return (a.name || '').localeCompare(b.name || '');
78
+ });
79
+
80
+ return sanitized;
81
+ }
82
+
83
+ /**
84
+ * List all roles with their permissions
85
+ * @returns {Object} Roles mapped to their permissions and descriptions
86
+ */
87
+ function listRoles() {
88
+ const result = {};
89
+
90
+ for (const [role, permissions] of Object.entries(ROLE_PERMISSIONS)) {
91
+ result[role] = {
92
+ permissions: [...permissions],
93
+ description: ROLE_DESCRIPTIONS[role] || '',
94
+ };
95
+ }
96
+
97
+ return result;
98
+ }
99
+
100
+ /**
101
+ * Get permissions for a specific role
102
+ * @param {string} role - Role name
103
+ * @param {Object} options - Options
104
+ * @param {boolean} options.expand - Expand wildcard to all permissions
105
+ * @returns {string[]} Array of permissions
106
+ */
107
+ function getRolePermissions(role, options = {}) {
108
+ if (!role) {
109
+ return [];
110
+ }
111
+
112
+ const permissions = ROLE_PERMISSIONS[role];
113
+
114
+ if (!permissions) {
115
+ return [];
116
+ }
117
+
118
+ // Expand wildcard if requested
119
+ if (options.expand && permissions.includes('*')) {
120
+ return [...ALL_PERMISSIONS];
121
+ }
122
+
123
+ return [...permissions];
124
+ }
125
+
126
+ /**
127
+ * Get SSO role mappings from config
128
+ * @param {Object} config - TLC config object
129
+ * @returns {Object} SSO mappings and default role
130
+ */
131
+ function getSSOMapping(config) {
132
+ if (!config || !config.sso || !config.sso.roleMappings) {
133
+ return {
134
+ mappings: [],
135
+ defaultRole: null,
136
+ };
137
+ }
138
+
139
+ // Sort mappings by priority
140
+ const sortedMappings = [...config.sso.roleMappings].sort(
141
+ (a, b) => a.priority - b.priority
142
+ );
143
+
144
+ return {
145
+ mappings: sortedMappings,
146
+ defaultRole: config.sso.defaultRole || null,
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Check if user has a specific permission
152
+ * @param {Object} user - User object
153
+ * @param {string} permission - Permission to check
154
+ * @returns {boolean} Whether user has the permission
155
+ */
156
+ function hasPermission(user, permission) {
157
+ if (!user || !user.role) {
158
+ return false;
159
+ }
160
+
161
+ const permissions = ROLE_PERMISSIONS[user.role] || [];
162
+
163
+ return permissions.includes('*') || permissions.includes(permission);
164
+ }
165
+
166
+ /**
167
+ * Generate access matrix showing user/permission relationships
168
+ * @param {Object[]} users - Array of user objects
169
+ * @returns {Object} Access matrix with users, permissions, and matrix
170
+ */
171
+ function getAccessMatrix(users) {
172
+ if (!users || !Array.isArray(users)) {
173
+ users = [];
174
+ }
175
+
176
+ const userEmails = users.map((u) => u.email);
177
+ const matrix = {};
178
+
179
+ for (const user of users) {
180
+ matrix[user.email] = {};
181
+
182
+ for (const permission of ALL_PERMISSIONS) {
183
+ matrix[user.email][permission] = hasPermission(user, permission);
184
+ }
185
+ }
186
+
187
+ return {
188
+ users: userEmails,
189
+ permissions: ALL_PERMISSIONS,
190
+ matrix,
191
+ };
192
+ }
193
+
194
+ // Sequence counter for stable ordering within same timestamp
195
+ let sequenceCounter = 0;
196
+
197
+ /**
198
+ * Track a permission change
199
+ * @param {Object} store - Permission store
200
+ * @param {Object} change - Change details
201
+ * @returns {Object} Recorded change with ID and timestamp
202
+ */
203
+ function trackPermissionChange(store, change) {
204
+ sequenceCounter++;
205
+
206
+ const record = {
207
+ id: generateId(),
208
+ timestamp: new Date().toISOString(),
209
+ sequence: sequenceCounter,
210
+ userId: change.userId,
211
+ oldRole: change.oldRole,
212
+ newRole: change.newRole,
213
+ changedBy: change.changedBy,
214
+ reason: change.reason || null,
215
+ };
216
+
217
+ store.add(record);
218
+
219
+ return record;
220
+ }
221
+
222
+ /**
223
+ * Get permission change history
224
+ * @param {Object} store - Permission store
225
+ * @param {Object} filters - Filter options
226
+ * @returns {Object[]} Array of permission changes
227
+ */
228
+ function getPermissionHistory(store, filters = {}) {
229
+ let history = store.getHistory();
230
+
231
+ // Filter by userId
232
+ if (filters.userId) {
233
+ history = history.filter((h) => h.userId === filters.userId);
234
+ }
235
+
236
+ // Filter by date range
237
+ if (filters.from) {
238
+ const fromDate = new Date(filters.from);
239
+ history = history.filter((h) => new Date(h.timestamp) >= fromDate);
240
+ }
241
+
242
+ if (filters.to) {
243
+ const toDate = new Date(filters.to);
244
+ history = history.filter((h) => new Date(h.timestamp) <= toDate);
245
+ }
246
+
247
+ // Sort by timestamp descending (most recent first), then by sequence for stable ordering
248
+ history.sort((a, b) => {
249
+ const timeDiff = new Date(b.timestamp) - new Date(a.timestamp);
250
+ if (timeDiff !== 0) return timeDiff;
251
+ return (b.sequence || 0) - (a.sequence || 0);
252
+ });
253
+
254
+ return history;
255
+ }
256
+
257
+ /**
258
+ * Export access control data as compliance evidence
259
+ * @param {Object[]} users - Array of user objects
260
+ * @param {Object} config - TLC config
261
+ * @param {Object} options - Export options
262
+ * @returns {Object|string} Evidence in requested format
263
+ */
264
+ function exportAsEvidence(users, config, options = {}) {
265
+ const { format = 'json', permissionStore, exportedBy = 'system' } = options;
266
+
267
+ const evidence = {
268
+ version: '1.0',
269
+ exportDate: new Date().toISOString(),
270
+ exportedBy,
271
+ users: listUsers(users || []),
272
+ roles: listRoles(),
273
+ ssoMappings: getSSOMapping(config || {}),
274
+ accessMatrix: getAccessMatrix(users || []),
275
+ };
276
+
277
+ // Include permission history if store provided
278
+ if (permissionStore) {
279
+ evidence.permissionHistory = getPermissionHistory(permissionStore);
280
+ }
281
+
282
+ if (format === 'csv') {
283
+ return formatAsCSV(evidence);
284
+ }
285
+
286
+ return evidence;
287
+ }
288
+
289
+ /**
290
+ * Format evidence as CSV
291
+ * @param {Object} evidence - Evidence object
292
+ * @returns {string} CSV formatted string
293
+ */
294
+ function formatAsCSV(evidence) {
295
+ const lines = [];
296
+
297
+ // Header
298
+ lines.push('email,name,role,' + ALL_PERMISSIONS.join(','));
299
+
300
+ // User rows
301
+ for (const user of evidence.users) {
302
+ const matrix = evidence.accessMatrix.matrix[user.email] || {};
303
+ const permissions = ALL_PERMISSIONS.map((p) => (matrix[p] ? 'Y' : 'N'));
304
+ lines.push(`${user.email},${user.name},${user.role},${permissions.join(',')}`);
305
+ }
306
+
307
+ return lines.join('\n');
308
+ }
309
+
310
+ /**
311
+ * Format access report for human reading
312
+ * @param {Object[]} users - Array of user objects
313
+ * @param {Object} options - Formatting options
314
+ * @returns {string} Formatted report
315
+ */
316
+ function formatAccessReport(users, options = {}) {
317
+ const { includeMatrix = false, config = null } = options;
318
+
319
+ const lines = [];
320
+ const userList = listUsers(users || []);
321
+
322
+ // Header
323
+ lines.push('# Access Control Report');
324
+ lines.push(`Generated: ${new Date().toISOString()}`);
325
+ lines.push('');
326
+
327
+ // User list
328
+ lines.push('## Users');
329
+ lines.push('');
330
+
331
+ if (userList.length === 0) {
332
+ lines.push('No users configured.');
333
+ } else {
334
+ lines.push('| Email | Name | Role |');
335
+ lines.push('|-------|------|------|');
336
+
337
+ for (const user of userList) {
338
+ lines.push(`| ${user.email} | ${user.name} | ${user.role} |`);
339
+ }
340
+ }
341
+ lines.push('');
342
+
343
+ // Role summary
344
+ lines.push('## Role Summary');
345
+ lines.push('');
346
+
347
+ const roleCounts = {};
348
+ for (const user of userList) {
349
+ roleCounts[user.role] = (roleCounts[user.role] || 0) + 1;
350
+ }
351
+
352
+ for (const [role, count] of Object.entries(roleCounts)) {
353
+ lines.push(`- ${role}: ${count}`);
354
+ }
355
+ lines.push('');
356
+
357
+ // Roles and permissions
358
+ lines.push('## Role Permissions');
359
+ lines.push('');
360
+
361
+ const roles = listRoles();
362
+ for (const [role, info] of Object.entries(roles)) {
363
+ lines.push(`### ${role}`);
364
+ lines.push(`${info.description}`);
365
+ lines.push(`Permissions: ${info.permissions.join(', ')}`);
366
+ lines.push('');
367
+ }
368
+
369
+ // Permission matrix
370
+ if (includeMatrix && userList.length > 0) {
371
+ lines.push('## Permission Matrix');
372
+ lines.push('');
373
+
374
+ const matrix = getAccessMatrix(userList);
375
+ const header = ['Email', ...ALL_PERMISSIONS].join(' | ');
376
+ lines.push(`| ${header} |`);
377
+ lines.push('|' + '------|'.repeat(ALL_PERMISSIONS.length + 1));
378
+
379
+ for (const email of matrix.users) {
380
+ const perms = ALL_PERMISSIONS.map((p) =>
381
+ matrix.matrix[email][p] ? 'Y' : '-'
382
+ );
383
+ lines.push(`| ${email} | ${perms.join(' | ')} |`);
384
+ }
385
+ lines.push('');
386
+ }
387
+
388
+ // SSO mappings
389
+ if (config && config.sso && config.sso.roleMappings) {
390
+ lines.push('## SSO Role Mappings');
391
+ lines.push('');
392
+
393
+ const ssoMapping = getSSOMapping(config);
394
+
395
+ if (ssoMapping.mappings.length === 0) {
396
+ lines.push('No SSO role mappings configured.');
397
+ } else {
398
+ lines.push('| Pattern | Role | Priority |');
399
+ lines.push('|---------|------|----------|');
400
+
401
+ for (const mapping of ssoMapping.mappings) {
402
+ lines.push(
403
+ `| \`${mapping.pattern}\` | ${mapping.role} | ${mapping.priority} |`
404
+ );
405
+ }
406
+
407
+ if (ssoMapping.defaultRole) {
408
+ lines.push('');
409
+ lines.push(`Default role: ${ssoMapping.defaultRole}`);
410
+ }
411
+ }
412
+ lines.push('');
413
+ }
414
+
415
+ return lines.join('\n');
416
+ }
417
+
418
+ /**
419
+ * Detect permissions not assigned to any user
420
+ * @param {Object[]} users - Array of user objects
421
+ * @returns {string[]} Array of orphaned permissions
422
+ */
423
+ function detectOrphanedPermissions(users) {
424
+ if (!users || !Array.isArray(users) || users.length === 0) {
425
+ return [...ALL_PERMISSIONS];
426
+ }
427
+
428
+ const usedPermissions = new Set();
429
+
430
+ for (const user of users) {
431
+ const permissions = ROLE_PERMISSIONS[user.role] || [];
432
+
433
+ // Wildcard means all permissions are used
434
+ if (permissions.includes('*')) {
435
+ return [];
436
+ }
437
+
438
+ for (const perm of permissions) {
439
+ usedPermissions.add(perm);
440
+ }
441
+ }
442
+
443
+ return ALL_PERMISSIONS.filter((p) => !usedPermissions.has(p));
444
+ }
445
+
446
+ /**
447
+ * Generate a unique ID
448
+ * @returns {string} Unique identifier
449
+ */
450
+ function generateId() {
451
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
452
+ }
453
+
454
+ /**
455
+ * Create an access control documenter instance
456
+ * @param {Object} options - Configuration options
457
+ * @returns {Object} Documenter instance
458
+ */
459
+ function createAccessControlDoc(options = {}) {
460
+ const { config = {} } = options;
461
+ const permissionStore = createInternalPermissionStore();
462
+
463
+ return {
464
+ listUsers(users) {
465
+ return listUsers(users);
466
+ },
467
+
468
+ listRoles() {
469
+ return listRoles();
470
+ },
471
+
472
+ getRolePermissions(role, opts) {
473
+ return getRolePermissions(role, opts);
474
+ },
475
+
476
+ getSSOMapping() {
477
+ return getSSOMapping(config);
478
+ },
479
+
480
+ getAccessMatrix(users) {
481
+ return getAccessMatrix(users);
482
+ },
483
+
484
+ trackPermissionChange(change) {
485
+ return trackPermissionChange(permissionStore, change);
486
+ },
487
+
488
+ getPermissionHistory(filters) {
489
+ return getPermissionHistory(permissionStore, filters);
490
+ },
491
+
492
+ exportAsEvidence(users, opts = {}) {
493
+ return exportAsEvidence(users, config, {
494
+ ...opts,
495
+ permissionStore,
496
+ });
497
+ },
498
+
499
+ formatAccessReport(users, opts) {
500
+ return formatAccessReport(users, { ...opts, config });
501
+ },
502
+
503
+ detectOrphanedPermissions(users) {
504
+ return detectOrphanedPermissions(users);
505
+ },
506
+ };
507
+ }
508
+
509
+ /**
510
+ * Create internal permission store for tracking changes
511
+ * @returns {Object} Permission store
512
+ */
513
+ function createInternalPermissionStore() {
514
+ const changes = [];
515
+
516
+ return {
517
+ add(change) {
518
+ changes.push(change);
519
+ },
520
+ getHistory() {
521
+ return [...changes];
522
+ },
523
+ };
524
+ }
525
+
526
+ export {
527
+ createAccessControlDoc,
528
+ listUsers,
529
+ listRoles,
530
+ getRolePermissions,
531
+ getSSOMapping,
532
+ getAccessMatrix,
533
+ trackPermissionChange,
534
+ getPermissionHistory,
535
+ exportAsEvidence,
536
+ formatAccessReport,
537
+ detectOrphanedPermissions,
538
+ USER_ROLES,
539
+ ROLE_PERMISSIONS,
540
+ ALL_PERMISSIONS,
541
+ };