@qwickapps/server 1.3.1 → 1.4.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 (149) hide show
  1. package/README.md +157 -0
  2. package/dist/core/control-panel.d.ts.map +1 -1
  3. package/dist/core/control-panel.js +114 -0
  4. package/dist/core/control-panel.js.map +1 -1
  5. package/dist/core/types.d.ts +19 -0
  6. package/dist/core/types.d.ts.map +1 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +4 -2
  10. package/dist/index.js.map +1 -1
  11. package/dist/plugins/auth/adapter-wrapper.d.ts +47 -0
  12. package/dist/plugins/auth/adapter-wrapper.d.ts.map +1 -0
  13. package/dist/plugins/auth/adapter-wrapper.js +166 -0
  14. package/dist/plugins/auth/adapter-wrapper.js.map +1 -0
  15. package/dist/plugins/auth/adapter-wrapper.test.d.ts +7 -0
  16. package/dist/plugins/auth/adapter-wrapper.test.d.ts.map +1 -0
  17. package/dist/plugins/auth/adapter-wrapper.test.js +303 -0
  18. package/dist/plugins/auth/adapter-wrapper.test.js.map +1 -0
  19. package/dist/plugins/auth/config-store.d.ts +11 -0
  20. package/dist/plugins/auth/config-store.d.ts.map +1 -0
  21. package/dist/plugins/auth/config-store.js +232 -0
  22. package/dist/plugins/auth/config-store.js.map +1 -0
  23. package/dist/plugins/auth/config-store.test.d.ts +7 -0
  24. package/dist/plugins/auth/config-store.test.d.ts.map +1 -0
  25. package/dist/plugins/auth/config-store.test.js +299 -0
  26. package/dist/plugins/auth/config-store.test.js.map +1 -0
  27. package/dist/plugins/auth/env-config.d.ts +51 -1
  28. package/dist/plugins/auth/env-config.d.ts.map +1 -1
  29. package/dist/plugins/auth/env-config.js +640 -7
  30. package/dist/plugins/auth/env-config.js.map +1 -1
  31. package/dist/plugins/auth/index.d.ts +6 -2
  32. package/dist/plugins/auth/index.d.ts.map +1 -1
  33. package/dist/plugins/auth/index.js +5 -1
  34. package/dist/plugins/auth/index.js.map +1 -1
  35. package/dist/plugins/auth/types.d.ts +106 -0
  36. package/dist/plugins/auth/types.d.ts.map +1 -1
  37. package/dist/plugins/index.d.ts +4 -2
  38. package/dist/plugins/index.d.ts.map +1 -1
  39. package/dist/plugins/index.js +3 -1
  40. package/dist/plugins/index.js.map +1 -1
  41. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts +7 -0
  42. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts.map +1 -0
  43. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js +220 -0
  44. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js.map +1 -0
  45. package/dist/plugins/rate-limit/cleanup.d.ts +40 -0
  46. package/dist/plugins/rate-limit/cleanup.d.ts.map +1 -0
  47. package/dist/plugins/rate-limit/cleanup.js +72 -0
  48. package/dist/plugins/rate-limit/cleanup.js.map +1 -0
  49. package/dist/plugins/rate-limit/env-config.d.ts +91 -0
  50. package/dist/plugins/rate-limit/env-config.d.ts.map +1 -0
  51. package/dist/plugins/rate-limit/env-config.js +318 -0
  52. package/dist/plugins/rate-limit/env-config.js.map +1 -0
  53. package/dist/plugins/rate-limit/index.d.ts +76 -0
  54. package/dist/plugins/rate-limit/index.d.ts.map +1 -0
  55. package/dist/plugins/rate-limit/index.js +79 -0
  56. package/dist/plugins/rate-limit/index.js.map +1 -0
  57. package/dist/plugins/rate-limit/middleware.d.ts +40 -0
  58. package/dist/plugins/rate-limit/middleware.d.ts.map +1 -0
  59. package/dist/plugins/rate-limit/middleware.js +169 -0
  60. package/dist/plugins/rate-limit/middleware.js.map +1 -0
  61. package/dist/plugins/rate-limit/rate-limit-plugin.d.ts +44 -0
  62. package/dist/plugins/rate-limit/rate-limit-plugin.d.ts.map +1 -0
  63. package/dist/plugins/rate-limit/rate-limit-plugin.js +354 -0
  64. package/dist/plugins/rate-limit/rate-limit-plugin.js.map +1 -0
  65. package/dist/plugins/rate-limit/rate-limit-service.d.ts +110 -0
  66. package/dist/plugins/rate-limit/rate-limit-service.d.ts.map +1 -0
  67. package/dist/plugins/rate-limit/rate-limit-service.js +172 -0
  68. package/dist/plugins/rate-limit/rate-limit-service.js.map +1 -0
  69. package/dist/plugins/rate-limit/stores/cache-store.d.ts +33 -0
  70. package/dist/plugins/rate-limit/stores/cache-store.d.ts.map +1 -0
  71. package/dist/plugins/rate-limit/stores/cache-store.js +225 -0
  72. package/dist/plugins/rate-limit/stores/cache-store.js.map +1 -0
  73. package/dist/plugins/rate-limit/stores/index.d.ts +8 -0
  74. package/dist/plugins/rate-limit/stores/index.d.ts.map +1 -0
  75. package/dist/plugins/rate-limit/stores/index.js +8 -0
  76. package/dist/plugins/rate-limit/stores/index.js.map +1 -0
  77. package/dist/plugins/rate-limit/stores/postgres-store.d.ts +34 -0
  78. package/dist/plugins/rate-limit/stores/postgres-store.d.ts.map +1 -0
  79. package/dist/plugins/rate-limit/stores/postgres-store.js +320 -0
  80. package/dist/plugins/rate-limit/stores/postgres-store.js.map +1 -0
  81. package/dist/plugins/rate-limit/strategies/fixed-window.d.ts +21 -0
  82. package/dist/plugins/rate-limit/strategies/fixed-window.d.ts.map +1 -0
  83. package/dist/plugins/rate-limit/strategies/fixed-window.js +97 -0
  84. package/dist/plugins/rate-limit/strategies/fixed-window.js.map +1 -0
  85. package/dist/plugins/rate-limit/strategies/index.d.ts +14 -0
  86. package/dist/plugins/rate-limit/strategies/index.d.ts.map +1 -0
  87. package/dist/plugins/rate-limit/strategies/index.js +27 -0
  88. package/dist/plugins/rate-limit/strategies/index.js.map +1 -0
  89. package/dist/plugins/rate-limit/strategies/sliding-window.d.ts +22 -0
  90. package/dist/plugins/rate-limit/strategies/sliding-window.d.ts.map +1 -0
  91. package/dist/plugins/rate-limit/strategies/sliding-window.js +122 -0
  92. package/dist/plugins/rate-limit/strategies/sliding-window.js.map +1 -0
  93. package/dist/plugins/rate-limit/strategies/token-bucket.d.ts +28 -0
  94. package/dist/plugins/rate-limit/strategies/token-bucket.d.ts.map +1 -0
  95. package/dist/plugins/rate-limit/strategies/token-bucket.js +121 -0
  96. package/dist/plugins/rate-limit/strategies/token-bucket.js.map +1 -0
  97. package/dist/plugins/rate-limit/types.d.ts +265 -0
  98. package/dist/plugins/rate-limit/types.d.ts.map +1 -0
  99. package/dist/plugins/rate-limit/types.js +9 -0
  100. package/dist/plugins/rate-limit/types.js.map +1 -0
  101. package/dist-ui/assets/index-D7DoZ9rL.js +478 -0
  102. package/dist-ui/assets/index-D7DoZ9rL.js.map +1 -0
  103. package/dist-ui/index.html +1 -1
  104. package/dist-ui-lib/api/controlPanelApi.d.ts +141 -0
  105. package/dist-ui-lib/dashboard/widgets/AuthStatusWidget.d.ts +9 -0
  106. package/dist-ui-lib/dashboard/widgets/IntegrationStatusWidget.d.ts +9 -0
  107. package/dist-ui-lib/dashboard/widgets/index.d.ts +2 -0
  108. package/dist-ui-lib/index.js +3332 -2343
  109. package/dist-ui-lib/index.js.map +1 -1
  110. package/dist-ui-lib/pages/IntegrationsPage.d.ts +1 -0
  111. package/dist-ui-lib/pages/RateLimitPage.d.ts +1 -0
  112. package/package.json +1 -1
  113. package/src/core/control-panel.ts +128 -0
  114. package/src/core/types.ts +17 -0
  115. package/src/index.ts +38 -0
  116. package/src/plugins/auth/adapter-wrapper.test.ts +395 -0
  117. package/src/plugins/auth/adapter-wrapper.ts +205 -0
  118. package/src/plugins/auth/config-store.test.ts +417 -0
  119. package/src/plugins/auth/config-store.ts +305 -0
  120. package/src/plugins/auth/env-config.ts +714 -7
  121. package/src/plugins/auth/index.ts +22 -1
  122. package/src/plugins/auth/types.ts +138 -0
  123. package/src/plugins/index.ts +49 -0
  124. package/src/plugins/rate-limit/__tests__/rate-limit-plugin.test.ts +259 -0
  125. package/src/plugins/rate-limit/cleanup.ts +117 -0
  126. package/src/plugins/rate-limit/env-config.ts +400 -0
  127. package/src/plugins/rate-limit/index.ts +128 -0
  128. package/src/plugins/rate-limit/middleware.ts +212 -0
  129. package/src/plugins/rate-limit/rate-limit-plugin.ts +400 -0
  130. package/src/plugins/rate-limit/rate-limit-service.ts +228 -0
  131. package/src/plugins/rate-limit/stores/cache-store.ts +261 -0
  132. package/src/plugins/rate-limit/stores/index.ts +8 -0
  133. package/src/plugins/rate-limit/stores/postgres-store.ts +402 -0
  134. package/src/plugins/rate-limit/strategies/fixed-window.ts +116 -0
  135. package/src/plugins/rate-limit/strategies/index.ts +30 -0
  136. package/src/plugins/rate-limit/strategies/sliding-window.ts +157 -0
  137. package/src/plugins/rate-limit/strategies/token-bucket.ts +154 -0
  138. package/src/plugins/rate-limit/types.ts +338 -0
  139. package/ui/src/App.tsx +32 -14
  140. package/ui/src/api/controlPanelApi.ts +226 -0
  141. package/ui/src/dashboard/builtInWidgets.tsx +5 -1
  142. package/ui/src/dashboard/widgets/AuthStatusWidget.tsx +143 -0
  143. package/ui/src/dashboard/widgets/IntegrationStatusWidget.tsx +135 -0
  144. package/ui/src/dashboard/widgets/index.ts +2 -0
  145. package/ui/src/pages/AuthPage.tsx +986 -142
  146. package/ui/src/pages/IntegrationsPage.tsx +288 -0
  147. package/ui/src/pages/RateLimitPage.tsx +292 -0
  148. package/dist-ui/assets/index-BY8OxNgO.js +0 -465
  149. package/dist-ui/assets/index-BY8OxNgO.js.map +0 -1
@@ -0,0 +1 @@
1
+ export declare function IntegrationsPage(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1 @@
1
+ export declare function RateLimitPage(): import("react/jsx-runtime").JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qwickapps/server",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "Plugin-based application server framework for building websites, APIs, admin dashboards, and full-stack products",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -320,6 +320,22 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
320
320
  }
321
321
  }
322
322
 
323
+ // Serve landing page at root (/) when control panel is mounted elsewhere
324
+ if (mountPath !== '/' && config.landingPage !== false) {
325
+ const landingConfig = config.landingPage || {};
326
+ app.get('/', (_req: Request, res: Response) => {
327
+ const html = generateLandingPageHtml({
328
+ productName: config.productName,
329
+ title: landingConfig.title || config.productName,
330
+ heading: landingConfig.heading || `Welcome to ${config.productName}`,
331
+ description: landingConfig.description || `${config.productName} is running.`,
332
+ controlPanelPath: mountPath,
333
+ links: landingConfig.links,
334
+ });
335
+ res.type('html').send(html);
336
+ });
337
+ }
338
+
323
339
  // Start a plugin with the registry
324
340
  const startPlugin = async (plugin: Plugin, pluginConfig: PluginConfig = {}): Promise<boolean> => {
325
341
  return pluginRegistry.startPlugin(plugin, pluginConfig);
@@ -564,3 +580,115 @@ function generateDashboardHtml(
564
580
  </body>
565
581
  </html>`;
566
582
  }
583
+
584
+ /**
585
+ * Generate landing page HTML for root path
586
+ */
587
+ function generateLandingPageHtml(options: {
588
+ productName: string;
589
+ title: string;
590
+ heading: string;
591
+ description: string;
592
+ controlPanelPath: string;
593
+ links?: Array<{ label: string; url: string }>;
594
+ }): string {
595
+ const { productName, title, heading, description, controlPanelPath, links = [] } = options;
596
+
597
+ const linksHtml = [
598
+ { label: 'Control Panel', url: controlPanelPath },
599
+ ...links,
600
+ ]
601
+ .map(
602
+ (link) =>
603
+ `<a href="${link.url}" class="link">${link.label}</a>`
604
+ )
605
+ .join('');
606
+
607
+ return `<!DOCTYPE html>
608
+ <html lang="en">
609
+ <head>
610
+ <meta charset="UTF-8">
611
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
612
+ <title>${title}</title>
613
+ <style>
614
+ * { margin: 0; padding: 0; box-sizing: border-box; }
615
+ body {
616
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
617
+ background: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%);
618
+ color: #e2e8f0;
619
+ min-height: 100vh;
620
+ display: flex;
621
+ align-items: center;
622
+ justify-content: center;
623
+ }
624
+ .container {
625
+ text-align: center;
626
+ max-width: 600px;
627
+ padding: 2rem;
628
+ }
629
+ .logo {
630
+ width: 80px;
631
+ height: 80px;
632
+ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
633
+ border-radius: 20px;
634
+ margin: 0 auto 2rem;
635
+ display: flex;
636
+ align-items: center;
637
+ justify-content: center;
638
+ font-size: 2rem;
639
+ font-weight: bold;
640
+ }
641
+ h1 {
642
+ font-size: 2.5rem;
643
+ margin-bottom: 1rem;
644
+ background: linear-gradient(135deg, #6366f1 0%, #a78bfa 100%);
645
+ -webkit-background-clip: text;
646
+ -webkit-text-fill-color: transparent;
647
+ background-clip: text;
648
+ }
649
+ p {
650
+ color: #94a3b8;
651
+ font-size: 1.125rem;
652
+ margin-bottom: 2rem;
653
+ line-height: 1.6;
654
+ }
655
+ .links {
656
+ display: flex;
657
+ flex-wrap: wrap;
658
+ gap: 1rem;
659
+ justify-content: center;
660
+ }
661
+ .link {
662
+ display: inline-block;
663
+ padding: 0.875rem 2rem;
664
+ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
665
+ color: white;
666
+ text-decoration: none;
667
+ border-radius: 0.75rem;
668
+ font-weight: 600;
669
+ transition: transform 0.2s, box-shadow 0.2s;
670
+ }
671
+ .link:hover {
672
+ transform: translateY(-2px);
673
+ box-shadow: 0 10px 25px -5px rgba(99, 102, 241, 0.4);
674
+ }
675
+ .footer {
676
+ margin-top: 3rem;
677
+ color: #475569;
678
+ font-size: 0.875rem;
679
+ }
680
+ </style>
681
+ </head>
682
+ <body>
683
+ <div class="container">
684
+ <div class="logo">${productName.charAt(0)}</div>
685
+ <h1>${heading}</h1>
686
+ <p>${description}</p>
687
+ <div class="links">${linksHtml}</div>
688
+ <div class="footer">
689
+ Powered by QwickApps Server
690
+ </div>
691
+ </div>
692
+ </body>
693
+ </html>`;
694
+ }
package/src/core/types.ts CHANGED
@@ -175,6 +175,23 @@ export interface ControlPanelConfig {
175
175
 
176
176
  /** Optional: Custom path to a dist-ui folder for serving a custom React UI */
177
177
  customUiPath?: string;
178
+
179
+ /**
180
+ * Optional: Landing page configuration for root path (/).
181
+ * Only used when mountPath is not '/'.
182
+ * If not provided, a default landing page is generated.
183
+ * Set to false to disable the landing page.
184
+ */
185
+ landingPage?: {
186
+ /** Page title */
187
+ title?: string;
188
+ /** Main heading */
189
+ heading?: string;
190
+ /** Description text */
191
+ description?: string;
192
+ /** Additional links */
193
+ links?: Array<{ label: string; url: string }>;
194
+ } | false;
178
195
  }
179
196
 
180
197
  // Plugin types are now in plugin-registry.ts
package/src/index.ts CHANGED
@@ -91,6 +91,8 @@ export {
91
91
  createAuthPlugin,
92
92
  createAuthPluginFromEnv,
93
93
  getAuthStatus,
94
+ setAuthConfigStore,
95
+ postgresAuthConfigStore,
94
96
  isAuthenticated,
95
97
  getAuthenticatedUser,
96
98
  getAccessToken,
@@ -140,6 +142,28 @@ export {
140
142
  requireAnyEntitlement,
141
143
  requireAllEntitlements,
142
144
  postgresEntitlementSource,
145
+ // Rate Limit plugin
146
+ createRateLimitPlugin,
147
+ createRateLimitPluginFromEnv,
148
+ getRateLimitConfigStatus,
149
+ postgresRateLimitStore,
150
+ createRateLimitCache,
151
+ createNoOpCache,
152
+ createSlidingWindowStrategy,
153
+ createFixedWindowStrategy,
154
+ createTokenBucketStrategy,
155
+ getStrategy,
156
+ rateLimitMiddleware,
157
+ rateLimitStatusMiddleware,
158
+ RateLimitService,
159
+ getRateLimitService,
160
+ isLimited,
161
+ checkLimit,
162
+ incrementLimit,
163
+ getRemainingRequests,
164
+ getLimitStatus,
165
+ clearLimit,
166
+ createCleanupJob,
143
167
  } from './plugins/index.js';
144
168
  export type {
145
169
  HealthPluginConfig,
@@ -166,6 +190,8 @@ export type {
166
190
  AuthPluginState,
167
191
  AuthEnvPluginOptions,
168
192
  AuthConfigStatus,
193
+ AuthConfigStore,
194
+ PostgresAuthConfigStoreConfig,
169
195
  // Users plugin types
170
196
  UsersPluginConfig,
171
197
  UserStore,
@@ -198,4 +224,16 @@ export type {
198
224
  UserEntitlement,
199
225
  CachedEntitlements,
200
226
  EntitlementStats,
227
+ // Rate Limit plugin types
228
+ RateLimitPluginConfig,
229
+ RateLimitEnvPluginOptions,
230
+ RateLimitStrategy,
231
+ LimitStatus,
232
+ StoredLimit,
233
+ IncrementOptions,
234
+ RateLimitStore,
235
+ PostgresRateLimitStoreConfig,
236
+ RateLimitCache,
237
+ RateLimitCacheConfig,
238
+ RateLimitMiddlewareOptions,
201
239
  } from './plugins/index.js';
@@ -0,0 +1,395 @@
1
+ /**
2
+ * Auth Adapter Wrapper Tests
3
+ *
4
+ * Unit tests for the hot-reload adapter wrapper.
5
+ */
6
+
7
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
8
+ import type { Request, Response, NextFunction } from 'express';
9
+ import { createAdapterWrapper } from './adapter-wrapper.js';
10
+ import type { AuthAdapter, AuthenticatedUser } from './types.js';
11
+
12
+ // Helper to create a mock adapter
13
+ function createMockAdapter(name: string, overrides: Partial<AuthAdapter> = {}): AuthAdapter {
14
+ return {
15
+ name,
16
+ initialize: vi.fn().mockReturnValue((_req: Request, _res: Response, next: NextFunction) => next()),
17
+ isAuthenticated: vi.fn().mockReturnValue(false),
18
+ getUser: vi.fn().mockReturnValue(null),
19
+ hasRoles: vi.fn().mockReturnValue(false),
20
+ getAccessToken: vi.fn().mockReturnValue(null),
21
+ onUnauthorized: vi.fn(),
22
+ shutdown: vi.fn().mockResolvedValue(undefined),
23
+ ...overrides,
24
+ };
25
+ }
26
+
27
+ // Helper to create mock request/response
28
+ function createMockReq(): Request {
29
+ return {} as Request;
30
+ }
31
+
32
+ function createMockRes(): Response {
33
+ return {
34
+ status: vi.fn().mockReturnThis(),
35
+ json: vi.fn().mockReturnThis(),
36
+ } as unknown as Response;
37
+ }
38
+
39
+ describe('createAdapterWrapper', () => {
40
+ describe('initialization', () => {
41
+ it('should create wrapper with no-op adapter by default', () => {
42
+ const wrapper = createAdapterWrapper();
43
+
44
+ expect(wrapper.name).toBe('none');
45
+ expect(wrapper.hasAdapter()).toBe(false);
46
+ });
47
+
48
+ it('should create wrapper with initial adapter', () => {
49
+ const mockAdapter = createMockAdapter('test');
50
+ const wrapper = createAdapterWrapper(mockAdapter);
51
+
52
+ expect(wrapper.name).toBe('test');
53
+ expect(wrapper.hasAdapter()).toBe(true);
54
+ });
55
+
56
+ it('should return adapter info', () => {
57
+ const mockAdapter = createMockAdapter('test');
58
+ const wrapper = createAdapterWrapper(mockAdapter);
59
+
60
+ const info = wrapper.getAdapterInfo();
61
+
62
+ expect(info.name).toBe('test');
63
+ expect(info.initialized).toBe(false);
64
+ });
65
+ });
66
+
67
+ describe('initialize', () => {
68
+ it('should call underlying adapter initialize', () => {
69
+ const mockAdapter = createMockAdapter('test');
70
+ const wrapper = createAdapterWrapper(mockAdapter);
71
+
72
+ wrapper.initialize();
73
+
74
+ expect(mockAdapter.initialize).toHaveBeenCalled();
75
+ });
76
+
77
+ it('should mark wrapper as initialized', () => {
78
+ const mockAdapter = createMockAdapter('test');
79
+ const wrapper = createAdapterWrapper(mockAdapter);
80
+
81
+ wrapper.initialize();
82
+
83
+ expect(wrapper.getAdapterInfo().initialized).toBe(true);
84
+ });
85
+
86
+ it('should return middleware from underlying adapter', () => {
87
+ const mockMiddleware = vi.fn();
88
+ const mockAdapter = createMockAdapter('test', {
89
+ initialize: vi.fn().mockReturnValue(mockMiddleware),
90
+ });
91
+ const wrapper = createAdapterWrapper(mockAdapter);
92
+
93
+ const result = wrapper.initialize();
94
+
95
+ expect(result).toBe(mockMiddleware);
96
+ });
97
+ });
98
+
99
+ describe('isAuthenticated', () => {
100
+ it('should delegate to current adapter', () => {
101
+ const mockAdapter = createMockAdapter('test', {
102
+ isAuthenticated: vi.fn().mockReturnValue(true),
103
+ });
104
+ const wrapper = createAdapterWrapper(mockAdapter);
105
+ const req = createMockReq();
106
+
107
+ const result = wrapper.isAuthenticated(req);
108
+
109
+ expect(mockAdapter.isAuthenticated).toHaveBeenCalledWith(req);
110
+ expect(result).toBe(true);
111
+ });
112
+
113
+ it('should use new adapter after swap', async () => {
114
+ const oldAdapter = createMockAdapter('old', {
115
+ isAuthenticated: vi.fn().mockReturnValue(false),
116
+ });
117
+ const newAdapter = createMockAdapter('new', {
118
+ isAuthenticated: vi.fn().mockReturnValue(true),
119
+ });
120
+
121
+ const wrapper = createAdapterWrapper(oldAdapter);
122
+ await wrapper.setAdapter(newAdapter);
123
+
124
+ const req = createMockReq();
125
+ const result = wrapper.isAuthenticated(req);
126
+
127
+ expect(result).toBe(true);
128
+ expect(newAdapter.isAuthenticated).toHaveBeenCalledWith(req);
129
+ });
130
+ });
131
+
132
+ describe('getUser', () => {
133
+ it('should delegate to current adapter', () => {
134
+ const mockUser: AuthenticatedUser = {
135
+ id: '123',
136
+ email: 'test@example.com',
137
+ name: 'Test User',
138
+ };
139
+ const mockAdapter = createMockAdapter('test', {
140
+ getUser: vi.fn().mockReturnValue(mockUser),
141
+ });
142
+ const wrapper = createAdapterWrapper(mockAdapter);
143
+ const req = createMockReq();
144
+
145
+ const result = wrapper.getUser(req);
146
+
147
+ expect(mockAdapter.getUser).toHaveBeenCalledWith(req);
148
+ expect(result).toEqual(mockUser);
149
+ });
150
+
151
+ it('should return null for no-op adapter', () => {
152
+ const wrapper = createAdapterWrapper();
153
+ const req = createMockReq();
154
+
155
+ const result = wrapper.getUser(req);
156
+
157
+ expect(result).toBeNull();
158
+ });
159
+ });
160
+
161
+ describe('hasRoles', () => {
162
+ it('should delegate to current adapter if method exists', () => {
163
+ const mockAdapter = createMockAdapter('test', {
164
+ hasRoles: vi.fn().mockReturnValue(true),
165
+ });
166
+ const wrapper = createAdapterWrapper(mockAdapter);
167
+ const req = createMockReq();
168
+
169
+ // Wrapper always implements hasRoles
170
+ const result = wrapper.hasRoles!(req, ['admin']);
171
+
172
+ expect(mockAdapter.hasRoles).toHaveBeenCalledWith(req, ['admin']);
173
+ expect(result).toBe(true);
174
+ });
175
+
176
+ it('should return false if adapter has no hasRoles method', () => {
177
+ const mockAdapter = createMockAdapter('test');
178
+ delete (mockAdapter as Partial<AuthAdapter>).hasRoles;
179
+ const wrapper = createAdapterWrapper(mockAdapter);
180
+ const req = createMockReq();
181
+
182
+ // Wrapper always implements hasRoles, even if underlying adapter doesn't
183
+ const result = wrapper.hasRoles!(req, ['admin']);
184
+
185
+ expect(result).toBe(false);
186
+ });
187
+ });
188
+
189
+ describe('getAccessToken', () => {
190
+ it('should delegate to current adapter if method exists', () => {
191
+ const mockAdapter = createMockAdapter('test', {
192
+ getAccessToken: vi.fn().mockReturnValue('token123'),
193
+ });
194
+ const wrapper = createAdapterWrapper(mockAdapter);
195
+ const req = createMockReq();
196
+
197
+ // Wrapper always implements getAccessToken
198
+ const result = wrapper.getAccessToken!(req);
199
+
200
+ expect(mockAdapter.getAccessToken).toHaveBeenCalledWith(req);
201
+ expect(result).toBe('token123');
202
+ });
203
+
204
+ it('should return null if adapter has no getAccessToken method', () => {
205
+ const mockAdapter = createMockAdapter('test');
206
+ delete (mockAdapter as Partial<AuthAdapter>).getAccessToken;
207
+ const wrapper = createAdapterWrapper(mockAdapter);
208
+ const req = createMockReq();
209
+
210
+ // Wrapper always implements getAccessToken, returns null when adapter doesn't have it
211
+ const result = wrapper.getAccessToken!(req);
212
+
213
+ expect(result).toBeNull();
214
+ });
215
+ });
216
+
217
+ describe('onUnauthorized', () => {
218
+ it('should delegate to current adapter if method exists', () => {
219
+ const mockAdapter = createMockAdapter('test');
220
+ const wrapper = createAdapterWrapper(mockAdapter);
221
+ const req = createMockReq();
222
+ const res = createMockRes();
223
+
224
+ // Wrapper always implements onUnauthorized
225
+ wrapper.onUnauthorized!(req, res);
226
+
227
+ expect(mockAdapter.onUnauthorized).toHaveBeenCalledWith(req, res);
228
+ });
229
+
230
+ it('should return 401 JSON response if adapter has no onUnauthorized', () => {
231
+ const mockAdapter = createMockAdapter('test');
232
+ delete (mockAdapter as Partial<AuthAdapter>).onUnauthorized;
233
+ const wrapper = createAdapterWrapper(mockAdapter);
234
+ const req = createMockReq();
235
+ const res = createMockRes();
236
+
237
+ // Wrapper always implements onUnauthorized with default behavior
238
+ wrapper.onUnauthorized!(req, res);
239
+
240
+ expect(res.status).toHaveBeenCalledWith(401);
241
+ expect(res.json).toHaveBeenCalledWith({
242
+ error: 'Unauthorized',
243
+ message: 'Authentication required',
244
+ });
245
+ });
246
+ });
247
+
248
+ describe('setAdapter (hot-reload)', () => {
249
+ it('should swap adapter successfully', async () => {
250
+ const oldAdapter = createMockAdapter('old');
251
+ const newAdapter = createMockAdapter('new');
252
+ const wrapper = createAdapterWrapper(oldAdapter);
253
+
254
+ await wrapper.setAdapter(newAdapter);
255
+
256
+ expect(wrapper.name).toBe('new');
257
+ });
258
+
259
+ it('should shutdown old adapter before swap', async () => {
260
+ const oldAdapter = createMockAdapter('old');
261
+ const newAdapter = createMockAdapter('new');
262
+ const wrapper = createAdapterWrapper(oldAdapter);
263
+
264
+ await wrapper.setAdapter(newAdapter);
265
+
266
+ expect(oldAdapter.shutdown).toHaveBeenCalled();
267
+ });
268
+
269
+ it('should initialize new adapter if wrapper was initialized', async () => {
270
+ const oldAdapter = createMockAdapter('old');
271
+ const newAdapter = createMockAdapter('new');
272
+ const wrapper = createAdapterWrapper(oldAdapter);
273
+
274
+ // Initialize wrapper
275
+ wrapper.initialize();
276
+
277
+ // Swap adapter
278
+ await wrapper.setAdapter(newAdapter);
279
+
280
+ expect(newAdapter.initialize).toHaveBeenCalled();
281
+ });
282
+
283
+ it('should not initialize new adapter if wrapper was not initialized', async () => {
284
+ const oldAdapter = createMockAdapter('old');
285
+ const newAdapter = createMockAdapter('new');
286
+ const wrapper = createAdapterWrapper(oldAdapter);
287
+
288
+ // Don't initialize wrapper
289
+
290
+ // Swap adapter
291
+ await wrapper.setAdapter(newAdapter);
292
+
293
+ expect(newAdapter.initialize).not.toHaveBeenCalled();
294
+ });
295
+
296
+ it('should prevent concurrent adapter swaps', async () => {
297
+ const oldAdapter = createMockAdapter('old', {
298
+ shutdown: vi.fn().mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 100))),
299
+ });
300
+ const newAdapter1 = createMockAdapter('new1');
301
+ const newAdapter2 = createMockAdapter('new2');
302
+ const wrapper = createAdapterWrapper(oldAdapter);
303
+
304
+ // Start first swap
305
+ const swap1 = wrapper.setAdapter(newAdapter1);
306
+
307
+ // Try second swap immediately (should fail)
308
+ await expect(wrapper.setAdapter(newAdapter2)).rejects.toThrow('Adapter swap already in progress');
309
+
310
+ // Wait for first swap to complete
311
+ await swap1;
312
+
313
+ // Now second swap should work
314
+ await wrapper.setAdapter(newAdapter2);
315
+ expect(wrapper.name).toBe('new2');
316
+ });
317
+
318
+ it('should handle shutdown errors gracefully', async () => {
319
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
320
+
321
+ const oldAdapter = createMockAdapter('old', {
322
+ shutdown: vi.fn().mockRejectedValue(new Error('Shutdown failed')),
323
+ });
324
+ const newAdapter = createMockAdapter('new');
325
+ const wrapper = createAdapterWrapper(oldAdapter);
326
+
327
+ // Should not throw
328
+ await wrapper.setAdapter(newAdapter);
329
+
330
+ expect(wrapper.name).toBe('new');
331
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
332
+ '[AdapterWrapper] Error shutting down old adapter:',
333
+ expect.any(Error)
334
+ );
335
+
336
+ consoleErrorSpy.mockRestore();
337
+ });
338
+ });
339
+
340
+ describe('shutdown', () => {
341
+ it('should shutdown current adapter', async () => {
342
+ const mockAdapter = createMockAdapter('test');
343
+ const wrapper = createAdapterWrapper(mockAdapter);
344
+ wrapper.initialize();
345
+
346
+ // Wrapper always implements shutdown
347
+ await wrapper.shutdown!();
348
+
349
+ expect(mockAdapter.shutdown).toHaveBeenCalled();
350
+ expect(wrapper.getAdapterInfo().initialized).toBe(false);
351
+ });
352
+
353
+ it('should handle adapters without shutdown method', async () => {
354
+ const mockAdapter = createMockAdapter('test');
355
+ delete (mockAdapter as Partial<AuthAdapter>).shutdown;
356
+ const wrapper = createAdapterWrapper(mockAdapter);
357
+ wrapper.initialize();
358
+
359
+ // Should not throw - wrapper handles missing shutdown gracefully
360
+ await wrapper.shutdown!();
361
+
362
+ expect(wrapper.getAdapterInfo().initialized).toBe(false);
363
+ });
364
+ });
365
+
366
+ describe('no-op adapter', () => {
367
+ it('should return false for isAuthenticated', () => {
368
+ const wrapper = createAdapterWrapper();
369
+ const req = createMockReq();
370
+
371
+ expect(wrapper.isAuthenticated(req)).toBe(false);
372
+ });
373
+
374
+ it('should return null for getUser', () => {
375
+ const wrapper = createAdapterWrapper();
376
+ const req = createMockReq();
377
+
378
+ expect(wrapper.getUser(req)).toBeNull();
379
+ });
380
+
381
+ it('should return middleware that calls next', () => {
382
+ const wrapper = createAdapterWrapper();
383
+ const middleware = wrapper.initialize();
384
+
385
+ const req = createMockReq();
386
+ const res = createMockRes();
387
+ const next = vi.fn();
388
+
389
+ if (typeof middleware === 'function') {
390
+ middleware(req, res, next);
391
+ expect(next).toHaveBeenCalled();
392
+ }
393
+ });
394
+ });
395
+ });