s3db.js 13.4.0 → 13.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +25 -10
  2. package/dist/{s3db.cjs.js → s3db.cjs} +38801 -32446
  3. package/dist/s3db.cjs.map +1 -0
  4. package/dist/s3db.es.js +38653 -32291
  5. package/dist/s3db.es.js.map +1 -1
  6. package/package.json +218 -22
  7. package/src/concerns/id.js +90 -6
  8. package/src/concerns/index.js +2 -1
  9. package/src/concerns/password-hashing.js +150 -0
  10. package/src/database.class.js +6 -2
  11. package/src/plugins/api/auth/basic-auth.js +40 -10
  12. package/src/plugins/api/auth/index.js +49 -3
  13. package/src/plugins/api/auth/oauth2-auth.js +171 -0
  14. package/src/plugins/api/auth/oidc-auth.js +789 -0
  15. package/src/plugins/api/auth/oidc-client.js +462 -0
  16. package/src/plugins/api/auth/path-auth-matcher.js +284 -0
  17. package/src/plugins/api/concerns/event-emitter.js +134 -0
  18. package/src/plugins/api/concerns/failban-manager.js +651 -0
  19. package/src/plugins/api/concerns/guards-helpers.js +402 -0
  20. package/src/plugins/api/concerns/metrics-collector.js +346 -0
  21. package/src/plugins/api/index.js +510 -57
  22. package/src/plugins/api/middlewares/failban.js +305 -0
  23. package/src/plugins/api/middlewares/rate-limit.js +301 -0
  24. package/src/plugins/api/middlewares/request-id.js +74 -0
  25. package/src/plugins/api/middlewares/security-headers.js +120 -0
  26. package/src/plugins/api/middlewares/session-tracking.js +194 -0
  27. package/src/plugins/api/routes/auth-routes.js +119 -78
  28. package/src/plugins/api/routes/resource-routes.js +73 -30
  29. package/src/plugins/api/server.js +1139 -45
  30. package/src/plugins/api/utils/custom-routes.js +102 -0
  31. package/src/plugins/api/utils/guards.js +213 -0
  32. package/src/plugins/api/utils/mime-types.js +154 -0
  33. package/src/plugins/api/utils/openapi-generator.js +91 -12
  34. package/src/plugins/api/utils/path-matcher.js +173 -0
  35. package/src/plugins/api/utils/static-filesystem.js +262 -0
  36. package/src/plugins/api/utils/static-s3.js +231 -0
  37. package/src/plugins/api/utils/template-engine.js +188 -0
  38. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
  39. package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
  40. package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
  41. package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
  42. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
  43. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
  44. package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
  45. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
  46. package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
  47. package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
  48. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
  49. package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
  50. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
  51. package/src/plugins/cloud-inventory/index.js +20 -0
  52. package/src/plugins/cloud-inventory/registry.js +146 -0
  53. package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
  54. package/src/plugins/cloud-inventory.plugin.js +1333 -0
  55. package/src/plugins/concerns/plugin-dependencies.js +62 -2
  56. package/src/plugins/eventual-consistency/analytics.js +1 -0
  57. package/src/plugins/eventual-consistency/consolidation.js +2 -2
  58. package/src/plugins/eventual-consistency/garbage-collection.js +2 -2
  59. package/src/plugins/eventual-consistency/install.js +2 -2
  60. package/src/plugins/identity/README.md +335 -0
  61. package/src/plugins/identity/concerns/mfa-manager.js +204 -0
  62. package/src/plugins/identity/concerns/password.js +138 -0
  63. package/src/plugins/identity/concerns/resource-schemas.js +273 -0
  64. package/src/plugins/identity/concerns/token-generator.js +172 -0
  65. package/src/plugins/identity/email-service.js +422 -0
  66. package/src/plugins/identity/index.js +1052 -0
  67. package/src/plugins/identity/oauth2-server.js +1033 -0
  68. package/src/plugins/identity/oidc-discovery.js +285 -0
  69. package/src/plugins/identity/rsa-keys.js +323 -0
  70. package/src/plugins/identity/server.js +500 -0
  71. package/src/plugins/identity/session-manager.js +453 -0
  72. package/src/plugins/identity/ui/layouts/base.js +251 -0
  73. package/src/plugins/identity/ui/middleware.js +135 -0
  74. package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
  75. package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
  76. package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
  77. package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
  78. package/src/plugins/identity/ui/pages/admin/users.js +263 -0
  79. package/src/plugins/identity/ui/pages/consent.js +262 -0
  80. package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
  81. package/src/plugins/identity/ui/pages/login.js +144 -0
  82. package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
  83. package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
  84. package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
  85. package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
  86. package/src/plugins/identity/ui/pages/profile.js +361 -0
  87. package/src/plugins/identity/ui/pages/register.js +226 -0
  88. package/src/plugins/identity/ui/pages/reset-password.js +128 -0
  89. package/src/plugins/identity/ui/pages/verify-email.js +172 -0
  90. package/src/plugins/identity/ui/routes.js +2541 -0
  91. package/src/plugins/identity/ui/styles/main.css +465 -0
  92. package/src/plugins/index.js +4 -1
  93. package/src/plugins/ml/base-model.class.js +65 -16
  94. package/src/plugins/ml/classification-model.class.js +1 -1
  95. package/src/plugins/ml/timeseries-model.class.js +3 -1
  96. package/src/plugins/ml.plugin.js +584 -31
  97. package/src/plugins/shared/error-handler.js +147 -0
  98. package/src/plugins/shared/index.js +9 -0
  99. package/src/plugins/shared/middlewares/compression.js +117 -0
  100. package/src/plugins/shared/middlewares/cors.js +49 -0
  101. package/src/plugins/shared/middlewares/index.js +11 -0
  102. package/src/plugins/shared/middlewares/logging.js +54 -0
  103. package/src/plugins/shared/middlewares/rate-limit.js +73 -0
  104. package/src/plugins/shared/middlewares/security.js +158 -0
  105. package/src/plugins/shared/response-formatter.js +264 -0
  106. package/src/plugins/state-machine.plugin.js +57 -2
  107. package/src/resource.class.js +140 -12
  108. package/src/schema.class.js +30 -1
  109. package/src/validator.class.js +57 -6
  110. package/dist/s3db.cjs.js.map +0 -1
@@ -0,0 +1,453 @@
1
+ /**
2
+ * Session Manager - Handles user sessions for Identity Provider
3
+ *
4
+ * Manages session lifecycle using S3DB resource as storage:
5
+ * - Create/validate/destroy sessions
6
+ * - Cookie-based session handling
7
+ * - Automatic session cleanup (expired sessions)
8
+ * - IP address and user agent tracking
9
+ */
10
+
11
+ import { generateSessionId, calculateExpiration, isExpired } from './concerns/token-generator.js';
12
+ import tryFn from '../../concerns/try-fn.js';
13
+
14
+ /**
15
+ * Default session configuration
16
+ */
17
+ const DEFAULT_CONFIG = {
18
+ sessionExpiry: '24h', // Default: 24 hours
19
+ cookieName: 's3db_session', // Cookie name
20
+ cookiePath: '/', // Cookie path
21
+ cookieHttpOnly: true, // HTTP-only cookie (no JS access)
22
+ cookieSecure: false, // Secure cookie (HTTPS only) - set to true in production
23
+ cookieSameSite: 'Lax', // SameSite attribute ('Strict', 'Lax', 'None')
24
+ cleanupInterval: 3600000, // Cleanup interval: 1 hour (in ms)
25
+ enableCleanup: true // Enable automatic cleanup
26
+ };
27
+
28
+ /**
29
+ * SessionManager class
30
+ * @class
31
+ */
32
+ export class SessionManager {
33
+ /**
34
+ * Create Session Manager
35
+ * @param {Object} options - Configuration options
36
+ * @param {Object} options.sessionResource - S3DB sessions resource
37
+ * @param {Object} [options.config] - Session configuration
38
+ */
39
+ constructor(options = {}) {
40
+ this.sessionResource = options.sessionResource;
41
+ this.config = { ...DEFAULT_CONFIG, ...options.config };
42
+
43
+ this.cleanupTimer = null;
44
+
45
+ if (!this.sessionResource) {
46
+ throw new Error('SessionManager requires a sessionResource');
47
+ }
48
+
49
+ // Start automatic cleanup
50
+ if (this.config.enableCleanup) {
51
+ this._startCleanup();
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Create a new session
57
+ * @param {Object} data - Session data
58
+ * @param {string} data.userId - User ID
59
+ * @param {Object} [data.metadata] - Additional session metadata
60
+ * @param {string} [data.ipAddress] - Client IP address
61
+ * @param {string} [data.userAgent] - Client user agent
62
+ * @param {string} [duration] - Session duration (overrides default)
63
+ * @returns {Promise<{sessionId: string, expiresAt: number, session: Object}>}
64
+ */
65
+ async createSession(data) {
66
+ const { userId, metadata = {}, ipAddress, userAgent, duration } = data;
67
+
68
+ if (!userId) {
69
+ throw new Error('userId is required to create a session');
70
+ }
71
+
72
+ // Generate session ID
73
+ const sessionId = generateSessionId();
74
+
75
+ // Calculate expiration
76
+ const expiresAt = calculateExpiration(duration || this.config.sessionExpiry);
77
+
78
+ // Create session record
79
+ const sessionData = {
80
+ userId,
81
+ expiresAt: new Date(expiresAt).toISOString(),
82
+ ipAddress: ipAddress || null,
83
+ userAgent: userAgent || null,
84
+ metadata,
85
+ createdAt: new Date().toISOString()
86
+ };
87
+
88
+ // Insert into S3DB
89
+ const [ok, err, session] = await tryFn(() =>
90
+ this.sessionResource.insert(sessionData)
91
+ );
92
+
93
+ if (!ok) {
94
+ throw new Error(`Failed to create session: ${err.message}`);
95
+ }
96
+
97
+ return {
98
+ sessionId: session.id, // S3DB auto-generated ID
99
+ expiresAt,
100
+ session
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Validate a session
106
+ * @param {string} sessionId - Session ID to validate
107
+ * @returns {Promise<{valid: boolean, session: Object|null, reason: string|null}>}
108
+ */
109
+ async validateSession(sessionId) {
110
+ if (!sessionId) {
111
+ return { valid: false, session: null, reason: 'No session ID provided' };
112
+ }
113
+
114
+ // Fetch session from S3DB
115
+ const [ok, err, session] = await tryFn(() =>
116
+ this.sessionResource.get(sessionId)
117
+ );
118
+
119
+ if (!ok || !session) {
120
+ return { valid: false, session: null, reason: 'Session not found' };
121
+ }
122
+
123
+ // Check if session is expired
124
+ if (isExpired(session.expiresAt)) {
125
+ // Delete expired session
126
+ await this.destroySession(sessionId);
127
+ return { valid: false, session: null, reason: 'Session expired' };
128
+ }
129
+
130
+ return { valid: true, session, reason: null };
131
+ }
132
+
133
+ /**
134
+ * Get session data without validation
135
+ * @param {string} sessionId - Session ID
136
+ * @returns {Promise<Object|null>} Session object or null
137
+ */
138
+ async getSession(sessionId) {
139
+ if (!sessionId) {
140
+ return null;
141
+ }
142
+
143
+ const [ok, , session] = await tryFn(() =>
144
+ this.sessionResource.get(sessionId)
145
+ );
146
+
147
+ return ok ? session : null;
148
+ }
149
+
150
+ /**
151
+ * Update session metadata
152
+ * @param {string} sessionId - Session ID
153
+ * @param {Object} metadata - New metadata to merge
154
+ * @returns {Promise<Object>} Updated session
155
+ */
156
+ async updateSession(sessionId, metadata) {
157
+ if (!sessionId) {
158
+ throw new Error('sessionId is required');
159
+ }
160
+
161
+ const session = await this.getSession(sessionId);
162
+
163
+ if (!session) {
164
+ throw new Error('Session not found');
165
+ }
166
+
167
+ const updatedMetadata = { ...session.metadata, ...metadata };
168
+
169
+ const [ok, err, updated] = await tryFn(() =>
170
+ this.sessionResource.update(sessionId, {
171
+ metadata: updatedMetadata
172
+ })
173
+ );
174
+
175
+ if (!ok) {
176
+ throw new Error(`Failed to update session: ${err.message}`);
177
+ }
178
+
179
+ return updated;
180
+ }
181
+
182
+ /**
183
+ * Destroy a session (logout)
184
+ * @param {string} sessionId - Session ID to destroy
185
+ * @returns {Promise<boolean>} True if session was destroyed
186
+ */
187
+ async destroySession(sessionId) {
188
+ if (!sessionId) {
189
+ return false;
190
+ }
191
+
192
+ const [ok] = await tryFn(() =>
193
+ this.sessionResource.delete(sessionId)
194
+ );
195
+
196
+ return ok;
197
+ }
198
+
199
+ /**
200
+ * Destroy all sessions for a user (logout all devices)
201
+ * @param {string} userId - User ID
202
+ * @returns {Promise<number>} Number of sessions destroyed
203
+ */
204
+ async destroyUserSessions(userId) {
205
+ if (!userId) {
206
+ return 0;
207
+ }
208
+
209
+ // Query all sessions for user
210
+ const [ok, , sessions] = await tryFn(() =>
211
+ this.sessionResource.query({ userId })
212
+ );
213
+
214
+ if (!ok || !sessions || sessions.length === 0) {
215
+ return 0;
216
+ }
217
+
218
+ // Delete all sessions
219
+ let count = 0;
220
+ for (const session of sessions) {
221
+ const destroyed = await this.destroySession(session.id);
222
+ if (destroyed) count++;
223
+ }
224
+
225
+ return count;
226
+ }
227
+
228
+ /**
229
+ * Get all active sessions for a user
230
+ * @param {string} userId - User ID
231
+ * @returns {Promise<Array>} Array of active sessions
232
+ */
233
+ async getUserSessions(userId) {
234
+ if (!userId) {
235
+ return [];
236
+ }
237
+
238
+ const [ok, , sessions] = await tryFn(() =>
239
+ this.sessionResource.query({ userId })
240
+ );
241
+
242
+ if (!ok || !sessions) {
243
+ return [];
244
+ }
245
+
246
+ // Filter out expired sessions
247
+ const activeSessions = [];
248
+ for (const session of sessions) {
249
+ if (!isExpired(session.expiresAt)) {
250
+ activeSessions.push(session);
251
+ } else {
252
+ // Clean up expired session
253
+ await this.destroySession(session.id);
254
+ }
255
+ }
256
+
257
+ return activeSessions;
258
+ }
259
+
260
+ /**
261
+ * Set session cookie in HTTP response
262
+ * @param {Object} res - HTTP response object (Express/Hono style)
263
+ * @param {string} sessionId - Session ID
264
+ * @param {number} expiresAt - Expiration timestamp (Unix ms)
265
+ */
266
+ setSessionCookie(res, sessionId, expiresAt) {
267
+ const expires = new Date(expiresAt);
268
+
269
+ const cookieOptions = [
270
+ `${this.config.cookieName}=${sessionId}`,
271
+ `Path=${this.config.cookiePath}`,
272
+ `Expires=${expires.toUTCString()}`,
273
+ `Max-Age=${Math.floor((expiresAt - Date.now()) / 1000)}`
274
+ ];
275
+
276
+ if (this.config.cookieHttpOnly) {
277
+ cookieOptions.push('HttpOnly');
278
+ }
279
+
280
+ if (this.config.cookieSecure) {
281
+ cookieOptions.push('Secure');
282
+ }
283
+
284
+ if (this.config.cookieSameSite) {
285
+ cookieOptions.push(`SameSite=${this.config.cookieSameSite}`);
286
+ }
287
+
288
+ const cookieValue = cookieOptions.join('; ');
289
+
290
+ // Set cookie header
291
+ if (typeof res.setHeader === 'function') {
292
+ // Express-style
293
+ res.setHeader('Set-Cookie', cookieValue);
294
+ } else if (typeof res.header === 'function') {
295
+ // Hono-style
296
+ res.header('Set-Cookie', cookieValue);
297
+ } else {
298
+ throw new Error('Unsupported response object');
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Clear session cookie in HTTP response
304
+ * @param {Object} res - HTTP response object
305
+ */
306
+ clearSessionCookie(res) {
307
+ const cookieOptions = [
308
+ `${this.config.cookieName}=`,
309
+ `Path=${this.config.cookiePath}`,
310
+ 'Expires=Thu, 01 Jan 1970 00:00:00 GMT',
311
+ 'Max-Age=0'
312
+ ];
313
+
314
+ if (this.config.cookieHttpOnly) {
315
+ cookieOptions.push('HttpOnly');
316
+ }
317
+
318
+ if (this.config.cookieSecure) {
319
+ cookieOptions.push('Secure');
320
+ }
321
+
322
+ if (this.config.cookieSameSite) {
323
+ cookieOptions.push(`SameSite=${this.config.cookieSameSite}`);
324
+ }
325
+
326
+ const cookieValue = cookieOptions.join('; ');
327
+
328
+ if (typeof res.setHeader === 'function') {
329
+ res.setHeader('Set-Cookie', cookieValue);
330
+ } else if (typeof res.header === 'function') {
331
+ res.header('Set-Cookie', cookieValue);
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Get session ID from HTTP request cookies
337
+ * @param {Object} req - HTTP request object
338
+ * @returns {string|null} Session ID or null
339
+ */
340
+ getSessionIdFromRequest(req) {
341
+ // Parse cookies from request
342
+ const cookieHeader = req.headers?.cookie || req.header?.('cookie');
343
+
344
+ if (!cookieHeader) {
345
+ return null;
346
+ }
347
+
348
+ const cookies = cookieHeader.split(';').reduce((acc, cookie) => {
349
+ const [key, value] = cookie.trim().split('=');
350
+ acc[key] = value;
351
+ return acc;
352
+ }, {});
353
+
354
+ return cookies[this.config.cookieName] || null;
355
+ }
356
+
357
+ /**
358
+ * Cleanup expired sessions
359
+ * @returns {Promise<number>} Number of sessions cleaned up
360
+ */
361
+ async cleanupExpiredSessions() {
362
+ // List all sessions
363
+ const [ok, , sessions] = await tryFn(() =>
364
+ this.sessionResource.list({ limit: 1000 })
365
+ );
366
+
367
+ if (!ok || !sessions) {
368
+ return 0;
369
+ }
370
+
371
+ let count = 0;
372
+ for (const session of sessions) {
373
+ if (isExpired(session.expiresAt)) {
374
+ const destroyed = await this.destroySession(session.id);
375
+ if (destroyed) count++;
376
+ }
377
+ }
378
+
379
+ return count;
380
+ }
381
+
382
+ /**
383
+ * Start automatic cleanup of expired sessions
384
+ * @private
385
+ */
386
+ _startCleanup() {
387
+ if (this.cleanupTimer) {
388
+ return; // Already running
389
+ }
390
+
391
+ this.cleanupTimer = setInterval(async () => {
392
+ try {
393
+ const count = await this.cleanupExpiredSessions();
394
+ if (count > 0) {
395
+ console.log(`[SessionManager] Cleaned up ${count} expired sessions`);
396
+ }
397
+ } catch (error) {
398
+ console.error('[SessionManager] Cleanup error:', error.message);
399
+ }
400
+ }, this.config.cleanupInterval);
401
+ }
402
+
403
+ /**
404
+ * Stop automatic cleanup
405
+ */
406
+ stopCleanup() {
407
+ if (this.cleanupTimer) {
408
+ clearInterval(this.cleanupTimer);
409
+ this.cleanupTimer = null;
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Get session statistics
415
+ * @returns {Promise<Object>} Session statistics
416
+ */
417
+ async getStatistics() {
418
+ const [ok, , sessions] = await tryFn(() =>
419
+ this.sessionResource.list({ limit: 10000 })
420
+ );
421
+
422
+ if (!ok || !sessions) {
423
+ return {
424
+ total: 0,
425
+ active: 0,
426
+ expired: 0,
427
+ users: 0
428
+ };
429
+ }
430
+
431
+ let active = 0;
432
+ let expired = 0;
433
+ const uniqueUsers = new Set();
434
+
435
+ for (const session of sessions) {
436
+ if (isExpired(session.expiresAt)) {
437
+ expired++;
438
+ } else {
439
+ active++;
440
+ uniqueUsers.add(session.userId);
441
+ }
442
+ }
443
+
444
+ return {
445
+ total: sessions.length,
446
+ active,
447
+ expired,
448
+ users: uniqueUsers.size
449
+ };
450
+ }
451
+ }
452
+
453
+ export default SessionManager;
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Base HTML Layout for Identity Provider UI
3
+ * Uses Hono's html helper for server-side rendering
4
+ */
5
+
6
+ import { html } from 'hono/html';
7
+ import { readFileSync } from 'fs';
8
+ import { fileURLToPath } from 'url';
9
+ import { dirname, join } from 'path';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+
14
+ // Read CSS file once at module load
15
+ const cssPath = join(__dirname, '../styles/main.css');
16
+ let cachedCSS = null;
17
+
18
+ function getCSS() {
19
+ if (!cachedCSS) {
20
+ cachedCSS = readFileSync(cssPath, 'utf-8');
21
+ }
22
+ return cachedCSS;
23
+ }
24
+
25
+ /**
26
+ * Convert hex colors to rgba for translucent gradients
27
+ * @param {string} hex - Hex color (#fff or #ffffff)
28
+ * @param {number} alpha - Alpha channel value between 0 and 1
29
+ * @returns {string} rgba(...) string
30
+ */
31
+ function hexToRgba(hex, alpha = 1) {
32
+ if (!hex || typeof hex !== 'string') {
33
+ return `rgba(0, 0, 0, ${alpha})`;
34
+ }
35
+
36
+ const normalized = hex.replace('#', '');
37
+ if (![3, 6].includes(normalized.length)) {
38
+ return `rgba(0, 0, 0, ${alpha})`;
39
+ }
40
+
41
+ const full = normalized.length === 3
42
+ ? normalized.split('').map(char => `${char}${char}`).join('')
43
+ : normalized;
44
+
45
+ const r = parseInt(full.slice(0, 2), 16);
46
+ const g = parseInt(full.slice(2, 4), 16);
47
+ const b = parseInt(full.slice(4, 6), 16);
48
+
49
+ if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {
50
+ return `rgba(0, 0, 0, ${alpha})`;
51
+ }
52
+
53
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
54
+ }
55
+
56
+ /**
57
+ * Base layout component
58
+ * @param {Object} props - Layout properties
59
+ * @param {string} props.title - Page title
60
+ * @param {string} props.content - Page content (HTML string)
61
+ * @param {Object} [props.user] - Authenticated user (if logged in)
62
+ * @param {Object} [props.config] - UI configuration (title, logo, etc.)
63
+ * @param {string} [props.error] - Error message to display
64
+ * @param {string} [props.success] - Success message to display
65
+ * @returns {string} HTML string
66
+ */
67
+ export function BaseLayout(props) {
68
+ const {
69
+ title = 'Identity Provider',
70
+ content = '',
71
+ user = null,
72
+ config = {},
73
+ error = null,
74
+ success = null
75
+ } = props;
76
+
77
+ // Theme configuration with defaults
78
+ const theme = {
79
+ title: config.title || 'S3DB Identity',
80
+ logo: config.logo || null,
81
+ logoUrl: config.logoUrl || null,
82
+ favicon: config.favicon || null,
83
+ registrationEnabled: config.registrationEnabled !== false, // Show register link
84
+
85
+ // Colors
86
+ primaryColor: config.primaryColor || '#007bff',
87
+ secondaryColor: config.secondaryColor || '#6c757d',
88
+ successColor: config.successColor || '#28a745',
89
+ dangerColor: config.dangerColor || '#dc3545',
90
+ warningColor: config.warningColor || '#ffc107',
91
+ infoColor: config.infoColor || '#17a2b8',
92
+
93
+ // Text colors
94
+ textColor: config.textColor || '#212529',
95
+ textMuted: config.textMuted || '#6c757d',
96
+
97
+ // Background colors
98
+ backgroundColor: config.backgroundColor || '#ffffff',
99
+ backgroundLight: config.backgroundLight || '#f8f9fa',
100
+ borderColor: config.borderColor || '#dee2e6',
101
+
102
+ // Typography
103
+ fontFamily: config.fontFamily || '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
104
+ fontSize: config.fontSize || '16px',
105
+
106
+ // Layout
107
+ borderRadius: config.borderRadius || '0.375rem',
108
+ boxShadow: config.boxShadow || '0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)',
109
+
110
+ // Company info
111
+ companyName: config.companyName || 'S3DB',
112
+ legalName: config.legalName || config.companyName || 'S3DB Corp',
113
+ tagline: config.tagline || 'Secure Identity & Access Management',
114
+ welcomeMessage: config.welcomeMessage || 'Welcome back!',
115
+ footerText: config.footerText || null,
116
+ supportEmail: config.supportEmail || null,
117
+ privacyUrl: config.privacyUrl || '/privacy',
118
+ termsUrl: config.termsUrl || '/terms',
119
+
120
+ // Social links
121
+ socialLinks: config.socialLinks || null,
122
+
123
+ // Custom CSS
124
+ customCSS: config.customCSS || null
125
+ };
126
+
127
+ const primaryGlow = hexToRgba(theme.primaryColor, 0.28);
128
+ const secondaryGlow = hexToRgba(theme.secondaryColor, 0.22);
129
+ const surfaceGlow = hexToRgba(theme.backgroundLight, 0.65);
130
+
131
+ // Build dynamic CSS variables
132
+ const themeCSS = `
133
+ :root {
134
+ --color-primary: ${theme.primaryColor};
135
+ --color-secondary: ${theme.secondaryColor};
136
+ --color-success: ${theme.successColor};
137
+ --color-danger: ${theme.dangerColor};
138
+ --color-warning: ${theme.warningColor};
139
+ --color-info: ${theme.infoColor};
140
+
141
+ --color-text: ${theme.textColor};
142
+ --color-text-muted: ${theme.textMuted};
143
+
144
+ --color-bg: ${theme.backgroundColor};
145
+ --color-light: ${theme.backgroundLight};
146
+ --color-border: ${theme.borderColor};
147
+ --color-card-bg: ${theme.backgroundLight};
148
+ --color-primary-glow: ${primaryGlow};
149
+ --color-secondary-glow: ${secondaryGlow};
150
+ --color-surface-glow: ${surfaceGlow};
151
+
152
+ --font-family: ${theme.fontFamily};
153
+ --font-size-base: ${theme.fontSize};
154
+
155
+ --border-radius: ${theme.borderRadius};
156
+ --box-shadow: ${theme.boxShadow};
157
+ }
158
+ `;
159
+
160
+ const backgroundGradient = `
161
+ radial-gradient(circle at 12% 18%, ${primaryGlow} 0%, transparent 52%),
162
+ radial-gradient(circle at 88% 16%, ${secondaryGlow} 0%, transparent 55%),
163
+ linear-gradient(160deg, ${theme.backgroundColor} 0%, ${theme.backgroundLight} 55%, ${theme.backgroundColor} 100%)
164
+ `;
165
+
166
+ const flashContainer = (error || success) ? html`
167
+ <div class="mx-auto mb-8 w-full max-w-3xl space-y-3">
168
+ ${error ? html`
169
+ <div class="rounded-2xl border border-red-500/40 bg-red-500/15 px-4 py-3 text-sm leading-6 text-red-100 shadow-lg shadow-red-900/30 backdrop-blur">
170
+ ${error}
171
+ </div>
172
+ ` : ''}
173
+ ${success ? html`
174
+ <div class="rounded-2xl border border-emerald-400/40 bg-emerald-500/15 px-4 py-3 text-sm leading-6 text-emerald-100 shadow-lg shadow-emerald-900/25 backdrop-blur">
175
+ ${success}
176
+ </div>
177
+ ` : ''}
178
+ </div>
179
+ ` : '';
180
+
181
+ return html`<!DOCTYPE html>
182
+ <html lang="en">
183
+ <head>
184
+ <meta charset="utf-8">
185
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
186
+ <meta http-equiv="x-ua-compatible" content="ie=edge">
187
+ <meta name="description" content="${theme.tagline}">
188
+ <title>${title} - ${theme.title}</title>
189
+
190
+ ${theme.favicon ? html`
191
+ <link rel="shortcut icon" href="${theme.favicon}">
192
+ <link rel="icon" href="${theme.favicon}">
193
+ ` : ''}
194
+
195
+ <script>
196
+ window.tailwind = window.tailwind || {};
197
+ window.tailwind.config = {
198
+ darkMode: 'class',
199
+ theme: {
200
+ extend: {
201
+ colors: {
202
+ primary: 'var(--color-primary)',
203
+ secondary: 'var(--color-secondary)',
204
+ surface: 'var(--color-card-bg)'
205
+ },
206
+ fontFamily: {
207
+ display: ['var(--font-family)'],
208
+ body: ['var(--font-family)']
209
+ },
210
+ boxShadow: {
211
+ surface: 'var(--box-shadow)'
212
+ }
213
+ }
214
+ }
215
+ };
216
+ </script>
217
+ <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
218
+
219
+ <!-- Custom Styles -->
220
+ <style>${themeCSS}</style>
221
+ <style>${getCSS()}</style>
222
+ ${theme.customCSS ? html`<style>${theme.customCSS}</style>` : ''}
223
+ </head>
224
+ <body class="min-h-screen bg-slate-950 antialiased text-white">
225
+ <div
226
+ class="relative flex min-h-screen flex-col overflow-hidden"
227
+ style="
228
+ background-image: ${backgroundGradient};
229
+ background-attachment: fixed;
230
+ background-size: cover;
231
+ color: ${theme.textColor};
232
+ font-family: ${theme.fontFamily};
233
+ font-size: ${theme.fontSize};
234
+ "
235
+ >
236
+ <div class="pointer-events-none absolute inset-0 overflow-hidden">
237
+ <div class="absolute -left-20 top-[10%] h-64 w-64 rounded-full blur-[120px]" style="background: ${primaryGlow}; opacity: 0.85;"></div>
238
+ <div class="absolute right-[-15%] top-[5%] h-72 w-72 rounded-full blur-[120px]" style="background: ${secondaryGlow}; opacity: 0.65;"></div>
239
+ <div class="absolute left-1/2 top-[65%] h-96 w-[36rem] -translate-x-1/2 rounded-[200px] blur-[160px]" style="background: ${surfaceGlow}; opacity: 0.35;"></div>
240
+ </div>
241
+
242
+ <main class="relative z-10 flex flex-1 items-stretch justify-center">
243
+ ${flashContainer}
244
+ ${content}
245
+ </main>
246
+ </div>
247
+ </body>
248
+ </html>`;
249
+ }
250
+
251
+ export default BaseLayout;