@auxiora/dashboard 1.0.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.
Files changed (97) hide show
  1. package/LICENSE +191 -0
  2. package/dist/auth.d.ts +13 -0
  3. package/dist/auth.d.ts.map +1 -0
  4. package/dist/auth.js +69 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/cloud-types.d.ts +71 -0
  7. package/dist/cloud-types.d.ts.map +1 -0
  8. package/dist/cloud-types.js +2 -0
  9. package/dist/cloud-types.js.map +1 -0
  10. package/dist/index.d.ts +6 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +4 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/router.d.ts +13 -0
  15. package/dist/router.d.ts.map +1 -0
  16. package/dist/router.js +2250 -0
  17. package/dist/router.js.map +1 -0
  18. package/dist/types.d.ts +314 -0
  19. package/dist/types.d.ts.map +1 -0
  20. package/dist/types.js +7 -0
  21. package/dist/types.js.map +1 -0
  22. package/dist-ui/assets/index-BfY0i5jw.css +1 -0
  23. package/dist-ui/assets/index-CXpk9mvw.js +60 -0
  24. package/dist-ui/icon.svg +59 -0
  25. package/dist-ui/index.html +20 -0
  26. package/package.json +32 -0
  27. package/src/auth.ts +83 -0
  28. package/src/cloud-types.ts +63 -0
  29. package/src/index.ts +5 -0
  30. package/src/router.ts +2494 -0
  31. package/src/types.ts +269 -0
  32. package/tests/auth.test.ts +51 -0
  33. package/tests/cloud-router.test.ts +249 -0
  34. package/tests/desktop-router.test.ts +151 -0
  35. package/tests/router.test.ts +388 -0
  36. package/tests/trust-router.test.ts +170 -0
  37. package/tsconfig.json +12 -0
  38. package/tsconfig.tsbuildinfo +1 -0
  39. package/ui/index.html +19 -0
  40. package/ui/node_modules/.bin/browserslist +17 -0
  41. package/ui/node_modules/.bin/tsc +17 -0
  42. package/ui/node_modules/.bin/tsserver +17 -0
  43. package/ui/node_modules/.bin/vite +17 -0
  44. package/ui/package.json +23 -0
  45. package/ui/public/icon.svg +59 -0
  46. package/ui/src/App.tsx +63 -0
  47. package/ui/src/api.ts +238 -0
  48. package/ui/src/components/ActivityFeed.tsx +123 -0
  49. package/ui/src/components/BehaviorHealth.tsx +105 -0
  50. package/ui/src/components/DataTable.tsx +39 -0
  51. package/ui/src/components/Layout.tsx +160 -0
  52. package/ui/src/components/PasswordStrength.tsx +31 -0
  53. package/ui/src/components/SetupProgress.tsx +26 -0
  54. package/ui/src/components/StatusBadge.tsx +12 -0
  55. package/ui/src/components/ThemeSelector.tsx +39 -0
  56. package/ui/src/contexts/ThemeContext.tsx +58 -0
  57. package/ui/src/hooks/useApi.ts +19 -0
  58. package/ui/src/hooks/usePolling.ts +8 -0
  59. package/ui/src/main.tsx +16 -0
  60. package/ui/src/pages/AuditLog.tsx +36 -0
  61. package/ui/src/pages/Behaviors.tsx +426 -0
  62. package/ui/src/pages/Chat.tsx +688 -0
  63. package/ui/src/pages/Login.tsx +64 -0
  64. package/ui/src/pages/Overview.tsx +56 -0
  65. package/ui/src/pages/Sessions.tsx +26 -0
  66. package/ui/src/pages/SettingsAmbient.tsx +185 -0
  67. package/ui/src/pages/SettingsConnections.tsx +201 -0
  68. package/ui/src/pages/SettingsNotifications.tsx +241 -0
  69. package/ui/src/pages/SetupAppearance.tsx +45 -0
  70. package/ui/src/pages/SetupChannels.tsx +143 -0
  71. package/ui/src/pages/SetupComplete.tsx +31 -0
  72. package/ui/src/pages/SetupConnections.tsx +80 -0
  73. package/ui/src/pages/SetupDashboardPassword.tsx +50 -0
  74. package/ui/src/pages/SetupIdentity.tsx +68 -0
  75. package/ui/src/pages/SetupPersonality.tsx +78 -0
  76. package/ui/src/pages/SetupProvider.tsx +65 -0
  77. package/ui/src/pages/SetupVault.tsx +50 -0
  78. package/ui/src/pages/SetupWelcome.tsx +19 -0
  79. package/ui/src/pages/UnlockVault.tsx +56 -0
  80. package/ui/src/pages/Webhooks.tsx +158 -0
  81. package/ui/src/pages/settings/Appearance.tsx +63 -0
  82. package/ui/src/pages/settings/Channels.tsx +138 -0
  83. package/ui/src/pages/settings/Identity.tsx +61 -0
  84. package/ui/src/pages/settings/Personality.tsx +54 -0
  85. package/ui/src/pages/settings/PersonalityEditor.tsx +577 -0
  86. package/ui/src/pages/settings/Provider.tsx +537 -0
  87. package/ui/src/pages/settings/Security.tsx +111 -0
  88. package/ui/src/styles/global.css +2308 -0
  89. package/ui/src/styles/themes/index.css +7 -0
  90. package/ui/src/styles/themes/monolith.css +125 -0
  91. package/ui/src/styles/themes/nebula.css +90 -0
  92. package/ui/src/styles/themes/neon.css +149 -0
  93. package/ui/src/styles/themes/polar.css +151 -0
  94. package/ui/src/styles/themes/signal.css +163 -0
  95. package/ui/src/styles/themes/terra.css +146 -0
  96. package/ui/tsconfig.json +14 -0
  97. package/ui/vite.config.ts +20 -0
package/src/types.ts ADDED
@@ -0,0 +1,269 @@
1
+ export interface DashboardConfig {
2
+ enabled: boolean;
3
+ sessionTtlMs: number;
4
+ }
5
+
6
+ export interface DashboardSession {
7
+ id: string;
8
+ createdAt: number;
9
+ lastActive: number;
10
+ ip: string;
11
+ }
12
+
13
+ export interface PersonalityTemplateSummary {
14
+ id: string;
15
+ name: string;
16
+ description: string;
17
+ preview: string;
18
+ }
19
+
20
+ export interface SetupDeps {
21
+ personality?: {
22
+ listTemplates(): Promise<PersonalityTemplateSummary[]>;
23
+ applyTemplate(id: string): Promise<void>;
24
+ buildCustom(config: Record<string, unknown>): Promise<string>;
25
+ getActiveTemplate?(): Promise<{ id: string; name: string } | null>;
26
+ };
27
+ saveConfig?: (updates: Record<string, unknown>) => Promise<void>;
28
+ getAgentName?: () => string;
29
+ getAgentPronouns?: () => string;
30
+ getAgentConfig?: () => Record<string, unknown>;
31
+ getSoulContent?: () => Promise<string | null>;
32
+ saveSoulContent?: (content: string) => Promise<void>;
33
+ hasSoulFile?: () => Promise<boolean>;
34
+ vaultExists?: () => Promise<boolean>;
35
+ onSetupComplete?: () => Promise<void>;
36
+ }
37
+
38
+ export interface DashboardDeps {
39
+ vault: {
40
+ get(name: string): string | undefined;
41
+ has(name: string): boolean;
42
+ add(name: string, value: string): Promise<void>;
43
+ unlock(password: string): Promise<void>;
44
+ changePassword(newPassword: string): Promise<void>;
45
+ };
46
+ onVaultUnlocked?: () => Promise<void>;
47
+ getActiveModel?: () => { provider: string; model: string };
48
+ behaviors?: {
49
+ list(filter?: { type?: string; status?: string }): Promise<any[]>;
50
+ get(id: string): Promise<any | undefined>;
51
+ create(input: Record<string, unknown>): Promise<any>;
52
+ update(id: string, updates: Record<string, unknown>): Promise<any>;
53
+ remove(id: string): Promise<boolean>;
54
+ };
55
+ webhooks?: {
56
+ list(): Promise<any[]>;
57
+ create(options: Record<string, unknown>): Promise<any>;
58
+ update?(id: string, updates: Record<string, unknown>): Promise<any>;
59
+ delete(id: string): Promise<boolean>;
60
+ };
61
+ getConfiguredChannels?: () => Array<{ type: string; enabled: boolean }>;
62
+ getActiveAgents?: () => Array<{
63
+ id: string;
64
+ type: string;
65
+ description: string;
66
+ channelType?: string;
67
+ startedAt: string;
68
+ }>;
69
+ getConnections: () => Array<{
70
+ id: string;
71
+ authenticated: boolean;
72
+ channelType: string;
73
+ lastActive: number;
74
+ voiceActive?: boolean;
75
+ }>;
76
+ getAuditEntries: (limit?: number) => Promise<any[]>;
77
+ getPlugins?: () => Array<{
78
+ name: string;
79
+ version: string;
80
+ file: string;
81
+ toolCount: number;
82
+ toolNames: string[];
83
+ behaviorNames: string[];
84
+ providerNames: string[];
85
+ permissions: string[];
86
+ status: string;
87
+ error?: string;
88
+ }>;
89
+ pluginManager?: {
90
+ enable(id: string): Promise<boolean>;
91
+ disable(id: string): Promise<boolean>;
92
+ remove(id: string): Promise<boolean>;
93
+ getConfig(id: string): Record<string, unknown> | null;
94
+ setConfig(id: string, config: Record<string, unknown>): Promise<boolean>;
95
+ getPermissions(id: string): string[] | null;
96
+ setPermissions(id: string, permissions: string[]): Promise<boolean>;
97
+ };
98
+ marketplace?: {
99
+ search(query: string): Promise<any[]>;
100
+ getPlugin(id: string): Promise<any | null>;
101
+ install(id: string): Promise<{ success: boolean; error?: string }>;
102
+ };
103
+ getMemories?: () => Promise<Array<{
104
+ id: string;
105
+ content: string;
106
+ category: string;
107
+ source: string;
108
+ createdAt: number;
109
+ updatedAt: number;
110
+ accessCount: number;
111
+ }>>;
112
+ memory?: {
113
+ getLivingState(): Promise<{
114
+ facts: any[];
115
+ relationships: any[];
116
+ patterns: any[];
117
+ adaptations: any[];
118
+ stats: any;
119
+ }>;
120
+ getStats(): Promise<any>;
121
+ getAdaptations(): Promise<any[]>;
122
+ deleteMemory(id: string): Promise<boolean>;
123
+ exportAll(): Promise<any>;
124
+ importAll(data: { memories: any[] }): Promise<{ imported: number; skipped: number }>;
125
+ };
126
+ setup?: SetupDeps;
127
+ orchestration?: {
128
+ getConfig(): {
129
+ enabled: boolean;
130
+ maxConcurrentAgents: number;
131
+ allowedPatterns: string[];
132
+ };
133
+ getHistory(limit?: number): Array<{
134
+ workflowId: string;
135
+ pattern: string;
136
+ taskCount: number;
137
+ totalCost: number;
138
+ duration: number;
139
+ timestamp: number;
140
+ }>;
141
+ };
142
+ models?: {
143
+ listProviders(): Array<{
144
+ name: string;
145
+ displayName: string;
146
+ available: boolean;
147
+ models: Record<string, unknown>;
148
+ }>;
149
+ getRoutingConfig(): {
150
+ enabled: boolean;
151
+ primary: string;
152
+ fallback?: string;
153
+ defaultModel?: string;
154
+ rules: unknown[];
155
+ preferences: Record<string, unknown>;
156
+ costLimits: Record<string, unknown>;
157
+ };
158
+ getCostSummary(): {
159
+ today: number;
160
+ thisMonth: number;
161
+ budgetRemaining?: number;
162
+ isOverBudget: boolean;
163
+ warningThresholdReached: boolean;
164
+ };
165
+ };
166
+ // --- [P13] Connectors ---
167
+ connectors?: {
168
+ list(): Array<{ id: string; name: string; category: string; auth: { type: string } }>;
169
+ get(id: string): any | undefined;
170
+ connect(connectorId: string, credentials: Record<string, string>, label?: string): Promise<any | null>;
171
+ disconnect(connectorId: string): Promise<boolean>;
172
+ getActions(connectorId: string): any[];
173
+ executeAction(connectorId: string, actionId: string, params: Record<string, unknown>): Promise<{ success: boolean; data?: unknown; error?: string }>;
174
+ };
175
+ // --- Trust / Autonomy (Phase 12) ---
176
+ trust?: {
177
+ getLevels(): Record<string, number>;
178
+ getLevel(domain: string): number;
179
+ setLevel(domain: string, level: number, reason: string): Promise<void>;
180
+ getAuditEntries(limit?: number): any[];
181
+ getAuditEntry(id: string): any | undefined;
182
+ rollback(id: string): Promise<{ success: boolean; error?: string }>;
183
+ getPromotions(): any[];
184
+ };
185
+ // --- [P6] Desktop ---
186
+ desktop?: {
187
+ getStatus(): {
188
+ status: string;
189
+ autoStart: boolean;
190
+ hotkey: string;
191
+ notificationsEnabled: boolean;
192
+ ollamaRunning: boolean;
193
+ updateChannel: string;
194
+ };
195
+ updateConfig(updates: Record<string, unknown>): Promise<Record<string, unknown>>;
196
+ sendNotification(payload: { title: string; body: string }): Promise<void>;
197
+ checkUpdates(): Promise<{
198
+ available: boolean;
199
+ currentVersion: string;
200
+ latestVersion?: string;
201
+ channel: string;
202
+ }>;
203
+ };
204
+ // --- Cloud (Phase 7) ---
205
+ cloud?: import('./cloud-types.js').CloudDeps;
206
+ // --- [P14] Team / Social ---
207
+ team?: {
208
+ listUsers(): Promise<any[]>;
209
+ createUser(name: string, role: string, channels?: any[]): Promise<any>;
210
+ deleteUser(id: string): Promise<boolean>;
211
+ };
212
+ // --- [P14] Workflows ---
213
+ workflows?: {
214
+ listActive(): Promise<any[]>;
215
+ listAll(): Promise<any[]>;
216
+ getStatus(id: string): Promise<any | undefined>;
217
+ createWorkflow(options: any): Promise<any>;
218
+ completeStep(workflowId: string, stepId: string, completedBy: string): Promise<any>;
219
+ cancelWorkflow(id: string): Promise<boolean>;
220
+ getPendingApprovals(userId?: string): Promise<any[]>;
221
+ approve(id: string, userId: string, reason?: string): Promise<any>;
222
+ reject(id: string, userId: string, reason?: string): Promise<any>;
223
+ };
224
+ // --- [P14] Agent Protocol ---
225
+ agentProtocol?: {
226
+ getIdentity(): any;
227
+ getInbox(limit?: number): any[];
228
+ discover(query: string): Promise<any[]>;
229
+ getDirectory(): Promise<any[]>;
230
+ };
231
+ // --- [P15] Screen ---
232
+ screen?: {
233
+ capture(): Promise<{ image: string; dimensions: { width: number; height: number } }>;
234
+ analyze(question?: string): Promise<string>;
235
+ };
236
+ // --- [P15] Ambient ---
237
+ ambient?: {
238
+ getPatterns(): any[];
239
+ getNotifications(): any[];
240
+ dismissNotification(id: string): boolean;
241
+ getBriefing(time: string): any;
242
+ getAnticipations(): any[];
243
+ };
244
+ // --- [P15] Conversation ---
245
+ conversation?: {
246
+ getState(): string;
247
+ start(): void;
248
+ stop(): void;
249
+ getTurnCount(): number;
250
+ };
251
+ // --- Chat session history ---
252
+ sessions?: {
253
+ getWebchatMessages(): Promise<Array<{ id: string; role: string; content: string; timestamp: number }>>;
254
+ listChats(options?: { archived?: boolean; limit?: number; offset?: number }): Array<{ id: string; title: string; channel: string; createdAt: number; updatedAt: number; archived: boolean }>;
255
+ createChat(title?: string): { id: string; title: string; channel: string; createdAt: number; updatedAt: number; archived: boolean };
256
+ renameChat(chatId: string, title: string): void;
257
+ archiveChat(chatId: string): void;
258
+ deleteChat(chatId: string): void;
259
+ getChatMessages(chatId: string): Array<{ id: string; role: string; content: string; timestamp: number }>;
260
+ };
261
+ }
262
+
263
+ export const DEFAULT_DASHBOARD_CONFIG: DashboardConfig = {
264
+ enabled: false,
265
+ sessionTtlMs: 86_400_000,
266
+ };
267
+
268
+ export const MAX_LOGIN_ATTEMPTS = 5;
269
+ export const LOGIN_WINDOW_MS = 60_000;
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { DashboardAuth } from '../src/auth.js';
3
+ import { MAX_LOGIN_ATTEMPTS } from '../src/types.js';
4
+
5
+ describe('DashboardAuth', () => {
6
+ let auth: DashboardAuth;
7
+
8
+ beforeEach(() => {
9
+ auth = new DashboardAuth(3_600_000); // 1 hour TTL
10
+ });
11
+
12
+ it('should create and validate a session', () => {
13
+ const sessionId = auth.createSession('127.0.0.1');
14
+ expect(auth.validateSession(sessionId)).toBe(true);
15
+ });
16
+
17
+ it('should reject unknown session', () => {
18
+ expect(auth.validateSession('nonexistent')).toBe(false);
19
+ });
20
+
21
+ it('should expire sessions after TTL', () => {
22
+ const shortAuth = new DashboardAuth(1); // 1ms TTL
23
+ const sessionId = shortAuth.createSession('127.0.0.1');
24
+
25
+ // Wait for expiry
26
+ vi.useFakeTimers();
27
+ vi.advanceTimersByTime(10);
28
+ expect(shortAuth.validateSession(sessionId)).toBe(false);
29
+ vi.useRealTimers();
30
+ });
31
+
32
+ it('should destroy a session on logout', () => {
33
+ const sessionId = auth.createSession('127.0.0.1');
34
+ expect(auth.destroySession(sessionId)).toBe(true);
35
+ expect(auth.validateSession(sessionId)).toBe(false);
36
+ });
37
+
38
+ it('should rate limit after max attempts', () => {
39
+ const ip = '192.168.1.1';
40
+ for (let i = 0; i < MAX_LOGIN_ATTEMPTS; i++) {
41
+ auth.recordAttempt(ip);
42
+ }
43
+ expect(auth.isRateLimited(ip)).toBe(true);
44
+ });
45
+
46
+ it('should not rate limit under the threshold', () => {
47
+ const ip = '192.168.1.1';
48
+ auth.recordAttempt(ip);
49
+ expect(auth.isRateLimited(ip)).toBe(false);
50
+ });
51
+ });
@@ -0,0 +1,249 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import express from 'express';
3
+ import request from 'supertest';
4
+ import { createDashboardRouter } from '../src/router.js';
5
+ import type { DashboardDeps } from '../src/types.js';
6
+ import type { CloudDeps } from '../src/cloud-types.js';
7
+
8
+ function createMockCloud(): CloudDeps {
9
+ return {
10
+ signup: vi.fn().mockResolvedValue({ tenantId: 'tenant-123', token: 'jwt-token-abc' }),
11
+ login: vi.fn().mockResolvedValue({ tenantId: 'tenant-123', token: 'jwt-token-abc' }),
12
+ getTenant: vi.fn().mockResolvedValue({
13
+ id: 'tenant-123',
14
+ name: 'Alice',
15
+ email: 'alice@test.com',
16
+ plan: 'pro',
17
+ status: 'active',
18
+ createdAt: '2025-01-01T00:00:00Z',
19
+ }),
20
+ changePlan: vi.fn().mockResolvedValue({ success: true }),
21
+ getUsage: vi.fn().mockResolvedValue({
22
+ usage: { maxMessages: 42 },
23
+ quotas: { maxMessages: 5000 },
24
+ }),
25
+ getBilling: vi.fn().mockResolvedValue({
26
+ plan: 'pro',
27
+ invoices: [{ id: 'inv_1', amount: 1900, status: 'paid', created: '2025-01-01' }],
28
+ }),
29
+ addPaymentMethod: vi.fn().mockResolvedValue({ success: true }),
30
+ exportData: vi.fn().mockResolvedValue({ downloadUrl: '/exports/tenant-123.zip' }),
31
+ deleteTenant: vi.fn().mockResolvedValue({ success: true }),
32
+ };
33
+ }
34
+
35
+ function createMockDeps(cloud?: CloudDeps): DashboardDeps {
36
+ return {
37
+ vault: {
38
+ get: vi.fn(),
39
+ has: vi.fn().mockReturnValue(false),
40
+ add: vi.fn(),
41
+ },
42
+ getConnections: vi.fn().mockReturnValue([]),
43
+ getAuditEntries: vi.fn().mockResolvedValue([]),
44
+ cloud,
45
+ };
46
+ }
47
+
48
+ function createApp(deps: DashboardDeps) {
49
+ const app = express();
50
+ app.use(express.json());
51
+ const { router, auth } = createDashboardRouter({
52
+ deps,
53
+ config: { enabled: true, sessionTtlMs: 86_400_000 },
54
+ verifyPassword: (input: string) => input === 'correct-password',
55
+ });
56
+ app.use('/api/v1/dashboard', router);
57
+ return { app, auth };
58
+ }
59
+
60
+ function loginAndGetCookie(app: express.Express): Promise<string> {
61
+ return request(app)
62
+ .post('/api/v1/dashboard/auth/login')
63
+ .send({ password: 'correct-password' })
64
+ .then((res) => {
65
+ const cookie = res.headers['set-cookie'];
66
+ return Array.isArray(cookie) ? cookie[0] : cookie;
67
+ });
68
+ }
69
+
70
+ describe('Cloud Dashboard Routes', () => {
71
+ let cloud: CloudDeps;
72
+ let app: express.Express;
73
+
74
+ beforeEach(() => {
75
+ cloud = createMockCloud();
76
+ const deps = createMockDeps(cloud);
77
+ ({ app } = createApp(deps));
78
+ });
79
+
80
+ describe('POST /cloud/signup', () => {
81
+ it('should create a new tenant', async () => {
82
+ const cookie = await loginAndGetCookie(app);
83
+ const res = await request(app)
84
+ .post('/api/v1/dashboard/cloud/signup')
85
+ .set('Cookie', cookie)
86
+ .send({ email: 'alice@test.com', name: 'Alice', password: 'secret123' });
87
+ expect(res.status).toBe(201);
88
+ expect(res.body.data.tenantId).toBe('tenant-123');
89
+ expect(res.body.data.token).toBeDefined();
90
+ });
91
+
92
+ it('should reject missing fields', async () => {
93
+ const cookie = await loginAndGetCookie(app);
94
+ const res = await request(app)
95
+ .post('/api/v1/dashboard/cloud/signup')
96
+ .set('Cookie', cookie)
97
+ .send({ email: 'alice@test.com' });
98
+ expect(res.status).toBe(400);
99
+ });
100
+
101
+ it('should return 503 when cloud not configured', async () => {
102
+ const deps = createMockDeps(); // no cloud
103
+ const { app: noCloudApp } = createApp(deps);
104
+ const cookie = await loginAndGetCookie(noCloudApp);
105
+ const res = await request(noCloudApp)
106
+ .post('/api/v1/dashboard/cloud/signup')
107
+ .set('Cookie', cookie)
108
+ .send({ email: 'a@b.com', name: 'A', password: 'pass' });
109
+ expect(res.status).toBe(503);
110
+ });
111
+ });
112
+
113
+ describe('POST /cloud/login', () => {
114
+ it('should login a tenant', async () => {
115
+ const cookie = await loginAndGetCookie(app);
116
+ const res = await request(app)
117
+ .post('/api/v1/dashboard/cloud/login')
118
+ .set('Cookie', cookie)
119
+ .send({ email: 'alice@test.com', password: 'secret123' });
120
+ expect(res.status).toBe(200);
121
+ expect(res.body.data.token).toBeDefined();
122
+ });
123
+
124
+ it('should reject invalid credentials', async () => {
125
+ (cloud.login as any).mockResolvedValue(null);
126
+ const cookie = await loginAndGetCookie(app);
127
+ const res = await request(app)
128
+ .post('/api/v1/dashboard/cloud/login')
129
+ .set('Cookie', cookie)
130
+ .send({ email: 'alice@test.com', password: 'wrong' });
131
+ expect(res.status).toBe(401);
132
+ });
133
+ });
134
+
135
+ describe('GET /cloud/tenant', () => {
136
+ it('should return tenant info', async () => {
137
+ const cookie = await loginAndGetCookie(app);
138
+ const res = await request(app)
139
+ .get('/api/v1/dashboard/cloud/tenant')
140
+ .set('Cookie', cookie)
141
+ .set('x-tenant-id', 'tenant-123');
142
+ expect(res.status).toBe(200);
143
+ expect(res.body.data.email).toBe('alice@test.com');
144
+ });
145
+
146
+ it('should require x-tenant-id header', async () => {
147
+ const cookie = await loginAndGetCookie(app);
148
+ const res = await request(app)
149
+ .get('/api/v1/dashboard/cloud/tenant')
150
+ .set('Cookie', cookie);
151
+ expect(res.status).toBe(400);
152
+ });
153
+
154
+ it('should return 404 for unknown tenant', async () => {
155
+ (cloud.getTenant as any).mockResolvedValue(null);
156
+ const cookie = await loginAndGetCookie(app);
157
+ const res = await request(app)
158
+ .get('/api/v1/dashboard/cloud/tenant')
159
+ .set('Cookie', cookie)
160
+ .set('x-tenant-id', 'nonexistent');
161
+ expect(res.status).toBe(404);
162
+ });
163
+ });
164
+
165
+ describe('POST /cloud/tenant/plan', () => {
166
+ it('should change plan', async () => {
167
+ const cookie = await loginAndGetCookie(app);
168
+ const res = await request(app)
169
+ .post('/api/v1/dashboard/cloud/tenant/plan')
170
+ .set('Cookie', cookie)
171
+ .set('x-tenant-id', 'tenant-123')
172
+ .send({ plan: 'team' });
173
+ expect(res.status).toBe(200);
174
+ expect(res.body.data.success).toBe(true);
175
+ });
176
+ });
177
+
178
+ describe('GET /cloud/tenant/usage', () => {
179
+ it('should return usage data', async () => {
180
+ const cookie = await loginAndGetCookie(app);
181
+ const res = await request(app)
182
+ .get('/api/v1/dashboard/cloud/tenant/usage')
183
+ .set('Cookie', cookie)
184
+ .set('x-tenant-id', 'tenant-123');
185
+ expect(res.status).toBe(200);
186
+ expect(res.body.data.usage.maxMessages).toBe(42);
187
+ });
188
+ });
189
+
190
+ describe('GET /cloud/tenant/billing', () => {
191
+ it('should return billing info', async () => {
192
+ const cookie = await loginAndGetCookie(app);
193
+ const res = await request(app)
194
+ .get('/api/v1/dashboard/cloud/tenant/billing')
195
+ .set('Cookie', cookie)
196
+ .set('x-tenant-id', 'tenant-123');
197
+ expect(res.status).toBe(200);
198
+ expect(res.body.data.plan).toBe('pro');
199
+ expect(res.body.data.invoices).toHaveLength(1);
200
+ });
201
+ });
202
+
203
+ describe('POST /cloud/tenant/billing/payment-method', () => {
204
+ it('should add payment method', async () => {
205
+ const cookie = await loginAndGetCookie(app);
206
+ const res = await request(app)
207
+ .post('/api/v1/dashboard/cloud/tenant/billing/payment-method')
208
+ .set('Cookie', cookie)
209
+ .set('x-tenant-id', 'tenant-123')
210
+ .send({ token: 'tok_visa' });
211
+ expect(res.status).toBe(200);
212
+ expect(res.body.data.success).toBe(true);
213
+ });
214
+
215
+ it('should reject missing token', async () => {
216
+ const cookie = await loginAndGetCookie(app);
217
+ const res = await request(app)
218
+ .post('/api/v1/dashboard/cloud/tenant/billing/payment-method')
219
+ .set('Cookie', cookie)
220
+ .set('x-tenant-id', 'tenant-123')
221
+ .send({});
222
+ expect(res.status).toBe(400);
223
+ });
224
+ });
225
+
226
+ describe('POST /cloud/tenant/export', () => {
227
+ it('should export tenant data', async () => {
228
+ const cookie = await loginAndGetCookie(app);
229
+ const res = await request(app)
230
+ .post('/api/v1/dashboard/cloud/tenant/export')
231
+ .set('Cookie', cookie)
232
+ .set('x-tenant-id', 'tenant-123');
233
+ expect(res.status).toBe(200);
234
+ expect(res.body.data.downloadUrl).toBeDefined();
235
+ });
236
+ });
237
+
238
+ describe('DELETE /cloud/tenant', () => {
239
+ it('should delete tenant', async () => {
240
+ const cookie = await loginAndGetCookie(app);
241
+ const res = await request(app)
242
+ .delete('/api/v1/dashboard/cloud/tenant')
243
+ .set('Cookie', cookie)
244
+ .set('x-tenant-id', 'tenant-123');
245
+ expect(res.status).toBe(200);
246
+ expect(res.body.data.success).toBe(true);
247
+ });
248
+ });
249
+ });