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,1052 @@
1
+ /**
2
+ * Identity Provider Plugin - OAuth2/OIDC Authorization Server
3
+ *
4
+ * Provides complete OAuth2 + OpenID Connect server functionality:
5
+ * - RSA key management for token signing
6
+ * - OAuth2 grant types (authorization_code, client_credentials, refresh_token)
7
+ * - OIDC flows (id_token, userinfo endpoint)
8
+ * - Token introspection
9
+ * - Client registration
10
+ *
11
+ * @example
12
+ * import { Database } from 's3db.js';
13
+ * import { IdentityPlugin } from 's3db.js/plugins/identity';
14
+ *
15
+ * const db = new Database({ connectionString: '...' });
16
+ * await db.connect();
17
+ *
18
+ * await db.usePlugin(new IdentityPlugin({
19
+ * port: 4000,
20
+ * issuer: 'http://localhost:4000',
21
+ * supportedScopes: ['openid', 'profile', 'email', 'read:api', 'write:api'],
22
+ * supportedGrantTypes: ['authorization_code', 'refresh_token', 'client_credentials'],
23
+ * accessTokenExpiry: '15m',
24
+ * idTokenExpiry: '15m',
25
+ * refreshTokenExpiry: '7d'
26
+ * }));
27
+ */
28
+
29
+ import { Plugin } from '../plugin.class.js';
30
+ import { requirePluginDependency } from '../concerns/plugin-dependencies.js';
31
+ import tryFn from '../../concerns/try-fn.js';
32
+ import { OAuth2Server } from './oauth2-server.js';
33
+ import {
34
+ BASE_USER_ATTRIBUTES,
35
+ BASE_TENANT_ATTRIBUTES,
36
+ BASE_CLIENT_ATTRIBUTES,
37
+ validateResourcesConfig,
38
+ mergeResourceConfig
39
+ } from './concerns/resource-schemas.js';
40
+
41
+ /**
42
+ * Identity Provider Plugin class
43
+ * @class
44
+ * @extends Plugin
45
+ */
46
+ export class IdentityPlugin extends Plugin {
47
+ /**
48
+ * Create Identity Provider Plugin instance
49
+ * @param {Object} options - Plugin configuration
50
+ */
51
+ constructor(options = {}) {
52
+ super(options);
53
+
54
+ // Validate required resources configuration
55
+ const resourcesValidation = validateResourcesConfig(options.resources);
56
+ if (!resourcesValidation.valid) {
57
+ throw new Error(
58
+ 'IdentityPlugin configuration error:\n' +
59
+ resourcesValidation.errors.join('\n')
60
+ );
61
+ }
62
+
63
+ // Validate resource configs (will throw if invalid)
64
+ mergeResourceConfig(
65
+ { attributes: BASE_USER_ATTRIBUTES },
66
+ options.resources.users,
67
+ 'users'
68
+ );
69
+ mergeResourceConfig(
70
+ { attributes: BASE_TENANT_ATTRIBUTES },
71
+ options.resources.tenants,
72
+ 'tenants'
73
+ );
74
+ mergeResourceConfig(
75
+ { attributes: BASE_CLIENT_ATTRIBUTES },
76
+ options.resources.clients,
77
+ 'clients'
78
+ );
79
+
80
+ this.config = {
81
+ // Server configuration
82
+ port: options.port || 4000,
83
+ host: options.host || '0.0.0.0',
84
+ verbose: options.verbose || false,
85
+
86
+ // OAuth2/OIDC configuration
87
+ issuer: options.issuer || `http://localhost:${options.port || 4000}`,
88
+ supportedScopes: options.supportedScopes || ['openid', 'profile', 'email', 'offline_access'],
89
+ supportedGrantTypes: options.supportedGrantTypes || ['authorization_code', 'client_credentials', 'refresh_token'],
90
+ supportedResponseTypes: options.supportedResponseTypes || ['code', 'token', 'id_token'],
91
+
92
+ // Token expiration
93
+ accessTokenExpiry: options.accessTokenExpiry || '15m',
94
+ idTokenExpiry: options.idTokenExpiry || '15m',
95
+ refreshTokenExpiry: options.refreshTokenExpiry || '7d',
96
+ authCodeExpiry: options.authCodeExpiry || '10m',
97
+
98
+ // Resource configuration (REQUIRED)
99
+ // User must declare: users, tenants, clients with full resource config
100
+ resources: {
101
+ users: {
102
+ userConfig: options.resources.users, // Store user's full config
103
+ mergedConfig: null // Will be populated in _createResources()
104
+ },
105
+ tenants: {
106
+ userConfig: options.resources.tenants,
107
+ mergedConfig: null
108
+ },
109
+ clients: {
110
+ userConfig: options.resources.clients,
111
+ mergedConfig: null
112
+ }
113
+ },
114
+
115
+ // CORS configuration
116
+ cors: {
117
+ enabled: options.cors?.enabled !== false, // Enabled by default for identity servers
118
+ origin: options.cors?.origin || '*',
119
+ methods: options.cors?.methods || ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
120
+ allowedHeaders: options.cors?.allowedHeaders || ['Content-Type', 'Authorization', 'X-API-Key'],
121
+ credentials: options.cors?.credentials !== false,
122
+ maxAge: options.cors?.maxAge || 86400
123
+ },
124
+
125
+ // Security headers
126
+ security: {
127
+ enabled: options.security?.enabled !== false,
128
+ contentSecurityPolicy: {
129
+ enabled: true,
130
+ directives: {
131
+ 'default-src': ["'self'"],
132
+ 'script-src': ["'self'", "'unsafe-inline'"],
133
+ 'style-src': ["'self'", "'unsafe-inline'", 'https://unpkg.com'],
134
+ 'img-src': ["'self'", 'data:', 'https:'],
135
+ 'font-src': ["'self'", 'https://unpkg.com'],
136
+ ...options.security?.contentSecurityPolicy?.directives
137
+ },
138
+ reportOnly: options.security?.contentSecurityPolicy?.reportOnly || false,
139
+ reportUri: options.security?.contentSecurityPolicy?.reportUri || null
140
+ }
141
+ },
142
+
143
+ // Logging
144
+ logging: {
145
+ enabled: options.logging?.enabled || false,
146
+ format: options.logging?.format || ':method :path :status :response-time ms'
147
+ },
148
+
149
+ // Session Management
150
+ session: {
151
+ sessionExpiry: options.session?.sessionExpiry || '24h',
152
+ cookieName: options.session?.cookieName || 's3db_session',
153
+ cookiePath: options.session?.cookiePath || '/',
154
+ cookieHttpOnly: options.session?.cookieHttpOnly !== false,
155
+ cookieSecure: options.session?.cookieSecure || false, // Set true in production with HTTPS
156
+ cookieSameSite: options.session?.cookieSameSite || 'Lax',
157
+ cleanupInterval: options.session?.cleanupInterval || 3600000, // 1 hour
158
+ enableCleanup: options.session?.enableCleanup !== false
159
+ },
160
+
161
+ // Password Policy
162
+ passwordPolicy: {
163
+ minLength: options.passwordPolicy?.minLength || 8,
164
+ maxLength: options.passwordPolicy?.maxLength || 128,
165
+ requireUppercase: options.passwordPolicy?.requireUppercase !== false,
166
+ requireLowercase: options.passwordPolicy?.requireLowercase !== false,
167
+ requireNumbers: options.passwordPolicy?.requireNumbers !== false,
168
+ requireSymbols: options.passwordPolicy?.requireSymbols || false,
169
+ bcryptRounds: options.passwordPolicy?.bcryptRounds || 10
170
+ },
171
+
172
+ // Registration Configuration
173
+ registration: {
174
+ enabled: options.registration?.enabled !== false, // Enabled by default
175
+ requireEmailVerification: options.registration?.requireEmailVerification !== false, // Required by default
176
+ allowedDomains: options.registration?.allowedDomains || null, // null = allow all domains
177
+ blockedDomains: options.registration?.blockedDomains || [], // Block specific domains
178
+ customMessage: options.registration?.customMessage || null // Custom message when disabled
179
+ },
180
+
181
+ // UI Configuration (white-label customization)
182
+ ui: {
183
+ // Branding
184
+ title: options.ui?.title || 'S3DB Identity',
185
+ companyName: options.ui?.companyName || 'S3DB',
186
+ legalName: options.ui?.legalName || options.ui?.companyName || 'S3DB Corp',
187
+ tagline: options.ui?.tagline || 'Secure Identity & Access Management',
188
+ welcomeMessage: options.ui?.welcomeMessage || 'Welcome back!',
189
+ logoUrl: options.ui?.logoUrl || null,
190
+ logo: options.ui?.logo || null, // Deprecated, use logoUrl
191
+ favicon: options.ui?.favicon || null,
192
+
193
+ // Colors (11 options)
194
+ primaryColor: options.ui?.primaryColor || '#007bff',
195
+ secondaryColor: options.ui?.secondaryColor || '#6c757d',
196
+ successColor: options.ui?.successColor || '#28a745',
197
+ dangerColor: options.ui?.dangerColor || '#dc3545',
198
+ warningColor: options.ui?.warningColor || '#ffc107',
199
+ infoColor: options.ui?.infoColor || '#17a2b8',
200
+ textColor: options.ui?.textColor || '#212529',
201
+ textMuted: options.ui?.textMuted || '#6c757d',
202
+ backgroundColor: options.ui?.backgroundColor || '#ffffff',
203
+ backgroundLight: options.ui?.backgroundLight || '#f8f9fa',
204
+ borderColor: options.ui?.borderColor || '#dee2e6',
205
+
206
+ // Typography
207
+ fontFamily: options.ui?.fontFamily || '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
208
+ fontSize: options.ui?.fontSize || '16px',
209
+
210
+ // Layout
211
+ borderRadius: options.ui?.borderRadius || '0.375rem',
212
+ boxShadow: options.ui?.boxShadow || '0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)',
213
+
214
+ // Company Info
215
+ footerText: options.ui?.footerText || null,
216
+ supportEmail: options.ui?.supportEmail || null,
217
+ privacyUrl: options.ui?.privacyUrl || '/privacy',
218
+ termsUrl: options.ui?.termsUrl || '/terms',
219
+
220
+ // Social Links
221
+ socialLinks: options.ui?.socialLinks || null,
222
+
223
+ // Custom CSS
224
+ customCSS: options.ui?.customCSS || null,
225
+
226
+ // Custom Pages (override default pages)
227
+ customPages: options.ui?.customPages || {},
228
+
229
+ // Base URL
230
+ baseUrl: options.ui?.baseUrl || `http://localhost:${options.port || 4000}`
231
+ },
232
+
233
+ // Email Configuration (SMTP)
234
+ email: {
235
+ enabled: options.email?.enabled !== false,
236
+ from: options.email?.from || 'noreply@s3db.identity',
237
+ replyTo: options.email?.replyTo || null,
238
+ smtp: {
239
+ host: options.email?.smtp?.host || 'localhost',
240
+ port: options.email?.smtp?.port || 587,
241
+ secure: options.email?.smtp?.secure || false,
242
+ auth: {
243
+ user: options.email?.smtp?.auth?.user || '',
244
+ pass: options.email?.smtp?.auth?.pass || ''
245
+ },
246
+ tls: {
247
+ rejectUnauthorized: options.email?.smtp?.tls?.rejectUnauthorized !== false
248
+ }
249
+ },
250
+ templates: {
251
+ baseUrl: options.email?.templates?.baseUrl || options.ui?.baseUrl || `http://localhost:${options.port || 4000}`,
252
+ brandName: options.email?.templates?.brandName || options.ui?.title || 'S3DB Identity',
253
+ brandLogo: options.email?.templates?.brandLogo || options.ui?.logo || null,
254
+ brandColor: options.email?.templates?.brandColor || options.ui?.primaryColor || '#007bff',
255
+ supportEmail: options.email?.templates?.supportEmail || options.email?.replyTo || null,
256
+ customFooter: options.email?.templates?.customFooter || null
257
+ }
258
+ },
259
+
260
+ // MFA Configuration (Multi-Factor Authentication)
261
+ mfa: {
262
+ enabled: options.mfa?.enabled || false, // Enable MFA/TOTP
263
+ required: options.mfa?.required || false, // Require MFA for all users
264
+ issuer: options.mfa?.issuer || options.ui?.title || 'S3DB Identity', // TOTP issuer name
265
+ algorithm: options.mfa?.algorithm || 'SHA1', // SHA1, SHA256, SHA512
266
+ digits: options.mfa?.digits || 6, // 6 or 8 digits
267
+ period: options.mfa?.period || 30, // 30 seconds
268
+ window: options.mfa?.window || 1, // Time window tolerance
269
+ backupCodesCount: options.mfa?.backupCodesCount || 10, // Number of backup codes
270
+ backupCodeLength: options.mfa?.backupCodeLength || 8 // Backup code length
271
+ },
272
+
273
+ // Audit Configuration (Compliance & Security Logging)
274
+ audit: {
275
+ enabled: options.audit?.enabled !== false, // Enable audit logging
276
+ includeData: options.audit?.includeData !== false, // Store before/after data
277
+ includePartitions: options.audit?.includePartitions !== false, // Track partition info
278
+ maxDataSize: options.audit?.maxDataSize || 10000, // Max bytes for data field
279
+ resources: options.audit?.resources || ['users', 'plg_oauth_clients'], // Resources to audit
280
+ events: options.audit?.events || [ // Custom events to audit
281
+ 'login', 'logout', 'login_failed',
282
+ 'account_locked', 'account_unlocked',
283
+ 'ip_banned', 'ip_unbanned',
284
+ 'password_reset_requested', 'password_changed',
285
+ 'email_verified', 'user_created', 'user_deleted',
286
+ 'mfa_enrolled', 'mfa_disabled', 'mfa_verified', 'mfa_failed'
287
+ ]
288
+ },
289
+
290
+ // Account Lockout Configuration (Per-User Brute Force Protection)
291
+ accountLockout: {
292
+ enabled: options.accountLockout?.enabled !== false, // Enable account lockout
293
+ maxAttempts: options.accountLockout?.maxAttempts || 5, // Max failed attempts before lockout
294
+ lockoutDuration: options.accountLockout?.lockoutDuration || 900000, // Lockout duration (15 min)
295
+ resetOnSuccess: options.accountLockout?.resetOnSuccess !== false // Reset counter on successful login
296
+ },
297
+
298
+ // Failban Configuration (IP-Based Brute Force Protection)
299
+ failban: {
300
+ enabled: options.failban?.enabled !== false, // Enable failban protection
301
+ maxViolations: options.failban?.maxViolations || 5, // Max failed attempts before ban
302
+ violationWindow: options.failban?.violationWindow || 300000, // Time window for violations (5 min)
303
+ banDuration: options.failban?.banDuration || 900000, // Ban duration (15 min)
304
+ whitelist: options.failban?.whitelist || ['127.0.0.1', '::1'], // IPs to never ban
305
+ blacklist: options.failban?.blacklist || [], // IPs to always ban
306
+ persistViolations: options.failban?.persistViolations !== false, // Persist violations to DB
307
+ endpoints: {
308
+ login: options.failban?.endpoints?.login !== false, // Protect /oauth/authorize POST
309
+ token: options.failban?.endpoints?.token !== false, // Protect /oauth/token
310
+ register: options.failban?.endpoints?.register !== false // Protect /register
311
+ },
312
+ geo: {
313
+ enabled: options.failban?.geo?.enabled || false, // Enable GeoIP blocking
314
+ databasePath: options.failban?.geo?.databasePath || null, // Path to GeoLite2-Country.mmdb
315
+ allowedCountries: options.failban?.geo?.allowedCountries || [], // Whitelist countries (ISO codes)
316
+ blockedCountries: options.failban?.geo?.blockedCountries || [], // Blacklist countries (ISO codes)
317
+ blockUnknown: options.failban?.geo?.blockUnknown || false // Block IPs with unknown country
318
+ }
319
+ },
320
+
321
+ // Features (MVP - Phase 1)
322
+ features: {
323
+ // Endpoints (can be disabled individually)
324
+ discovery: options.features?.discovery !== false, // GET /.well-known/openid-configuration
325
+ jwks: options.features?.jwks !== false, // GET /.well-known/jwks.json
326
+ token: options.features?.token !== false, // POST /oauth/token
327
+ authorize: options.features?.authorize !== false, // GET/POST /oauth/authorize
328
+ userinfo: options.features?.userinfo !== false, // GET /oauth/userinfo
329
+ introspection: options.features?.introspection !== false, // POST /oauth/introspect
330
+ revocation: options.features?.revocation !== false, // POST /oauth/revoke
331
+ registration: options.features?.registration !== false, // POST /oauth/register (RFC 7591)
332
+
333
+ // Authorization Code Flow UI
334
+ builtInLoginUI: options.features?.builtInLoginUI !== false, // HTML login form
335
+ customLoginHandler: options.features?.customLoginHandler || null, // Custom UI handler
336
+
337
+ // PKCE (Proof Key for Code Exchange - RFC 7636)
338
+ pkce: {
339
+ enabled: options.features?.pkce?.enabled !== false, // PKCE support
340
+ required: options.features?.pkce?.required || false, // Force PKCE for public clients
341
+ methods: options.features?.pkce?.methods || ['S256', 'plain'] // Supported methods
342
+ },
343
+
344
+ // Refresh tokens
345
+ refreshTokens: options.features?.refreshTokens !== false, // Enable refresh tokens
346
+ refreshTokenRotation: options.features?.refreshTokenRotation || false, // Rotate on each use
347
+ revokeOldRefreshTokens: options.features?.revokeOldRefreshTokens !== false, // Revoke old tokens after rotation
348
+
349
+ // Future features (Phase 2 - commented for reference)
350
+ // admin: { enabled: false, apiKey: null, endpoints: {...} },
351
+ // consent: { enabled: false, skipForTrustedClients: true },
352
+ // mfa: { enabled: false, methods: ['totp', 'sms', 'email'] },
353
+ // emailVerification: { enabled: false, required: false },
354
+ // passwordPolicy: { enabled: false, minLength: 8, ... },
355
+ // webhooks: { enabled: false, endpoints: [], events: [] }
356
+ }
357
+ };
358
+
359
+ this.server = null;
360
+ this.oauth2Server = null;
361
+ this.sessionManager = null;
362
+ this.emailService = null;
363
+ this.failbanManager = null;
364
+ this.auditPlugin = null;
365
+ this.mfaManager = null;
366
+
367
+ // Internal plugin resources (prefixed with plg_)
368
+ this.oauth2KeysResource = null;
369
+ this.oauth2AuthCodesResource = null;
370
+ this.sessionsResource = null;
371
+ this.passwordResetTokensResource = null;
372
+ this.mfaDevicesResource = null;
373
+
374
+ // User-managed resources (user chooses names)
375
+ this.usersResource = null;
376
+ this.tenantsResource = null;
377
+ this.clientsResource = null;
378
+ }
379
+
380
+ /**
381
+ * Validate plugin dependencies
382
+ * @private
383
+ */
384
+ async _validateDependencies() {
385
+ await requirePluginDependency('identity-plugin', {
386
+ throwOnError: true,
387
+ checkVersions: true
388
+ });
389
+ }
390
+
391
+ /**
392
+ * Install plugin
393
+ */
394
+ async onInstall() {
395
+ if (this.config.verbose) {
396
+ console.log('[Identity Plugin] Installing...');
397
+ }
398
+
399
+ // Validate dependencies
400
+ try {
401
+ await this._validateDependencies();
402
+ } catch (err) {
403
+ console.error('[Identity Plugin] Dependency validation failed:', err.message);
404
+ throw err;
405
+ }
406
+
407
+ // Create user-managed resources (users, tenants, clients) with merged attributes
408
+ await this._createUserManagedResources();
409
+
410
+ // Create OAuth2 internal resources (keys, auth_codes, sessions, etc.)
411
+ await this._createOAuth2Resources();
412
+
413
+ // Initialize OAuth2 Server
414
+ await this._initializeOAuth2Server();
415
+
416
+ // Initialize Session Manager
417
+ await this._initializeSessionManager();
418
+
419
+ // Initialize Email Service
420
+ await this._initializeEmailService();
421
+
422
+ // Initialize Failban Manager
423
+ await this._initializeFailbanManager();
424
+
425
+ // Initialize Audit Plugin
426
+ await this._initializeAuditPlugin();
427
+
428
+ // Initialize MFA Manager
429
+ await this._initializeMFAManager();
430
+
431
+ if (this.config.verbose) {
432
+ console.log('[Identity Plugin] Installed successfully');
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Create OAuth2 resources for authorization server
438
+ * @private
439
+ */
440
+ async _createOAuth2Resources() {
441
+ // 1. OAuth Keys Resource (RSA keys for token signing)
442
+ const [okKeys, errKeys, keysResource] = await tryFn(() =>
443
+ this.database.createResource({
444
+ name: 'plg_oauth_keys',
445
+ attributes: {
446
+ kid: 'string|required',
447
+ publicKey: 'string|required',
448
+ privateKey: 'secret|required',
449
+ algorithm: 'string|default:RS256',
450
+ use: 'string|default:sig',
451
+ active: 'boolean|default:true',
452
+ createdAt: 'string|optional'
453
+ },
454
+ behavior: 'body-overflow',
455
+ timestamps: true,
456
+ createdBy: 'IdentityPlugin'
457
+ })
458
+ );
459
+
460
+ if (okKeys) {
461
+ this.oauth2KeysResource = keysResource;
462
+ if (this.config.verbose) {
463
+ console.log('[Identity Plugin] Created plg_oauth_keys resource');
464
+ }
465
+ } else if (this.database.resources.plg_oauth_keys) {
466
+ this.oauth2KeysResource = this.database.resources.plg_oauth_keys;
467
+ if (this.config.verbose) {
468
+ console.log('[Identity Plugin] Using existing plg_oauth_keys resource');
469
+ }
470
+ } else {
471
+ throw errKeys;
472
+ }
473
+
474
+ // 2. OAuth Authorization Codes Resource (authorization_code flow)
475
+ const [okCodes, errCodes, codesResource] = await tryFn(() =>
476
+ this.database.createResource({
477
+ name: 'plg_auth_codes',
478
+ attributes: {
479
+ code: 'string|required',
480
+ clientId: 'string|required',
481
+ userId: 'string|required',
482
+ redirectUri: 'string|required',
483
+ scope: 'string|optional',
484
+ expiresAt: 'string|required',
485
+ used: 'boolean|default:false',
486
+ codeChallenge: 'string|optional', // PKCE support
487
+ codeChallengeMethod: 'string|optional', // PKCE support
488
+ createdAt: 'string|optional'
489
+ },
490
+ behavior: 'body-overflow',
491
+ timestamps: true,
492
+ createdBy: 'IdentityPlugin'
493
+ })
494
+ );
495
+
496
+ if (okCodes) {
497
+ this.oauth2AuthCodesResource = codesResource;
498
+ if (this.config.verbose) {
499
+ console.log('[Identity Plugin] Created plg_auth_codes resource');
500
+ }
501
+ } else if (this.database.resources.plg_auth_codes) {
502
+ this.oauth2AuthCodesResource = this.database.resources.plg_auth_codes;
503
+ if (this.config.verbose) {
504
+ console.log('[Identity Plugin] Using existing plg_auth_codes resource');
505
+ }
506
+ } else {
507
+ throw errCodes;
508
+ }
509
+
510
+ // 3. Sessions Resource (user sessions for UI/admin)
511
+ const [okSessions, errSessions, sessionsResource] = await tryFn(() =>
512
+ this.database.createResource({
513
+ name: 'plg_sessions',
514
+ attributes: {
515
+ userId: 'string|required',
516
+ expiresAt: 'string|required',
517
+ ipAddress: 'ip4|optional',
518
+ userAgent: 'string|optional',
519
+ metadata: 'object|optional',
520
+ createdAt: 'string|optional'
521
+ },
522
+ behavior: 'body-overflow',
523
+ timestamps: true,
524
+ createdBy: 'IdentityPlugin'
525
+ })
526
+ );
527
+
528
+ if (okSessions) {
529
+ this.sessionsResource = sessionsResource;
530
+ if (this.config.verbose) {
531
+ console.log('[Identity Plugin] Created plg_sessions resource');
532
+ }
533
+ } else if (this.database.resources.plg_sessions) {
534
+ this.sessionsResource = this.database.resources.plg_sessions;
535
+ if (this.config.verbose) {
536
+ console.log('[Identity Plugin] Using existing plg_sessions resource');
537
+ }
538
+ } else {
539
+ throw errSessions;
540
+ }
541
+
542
+ // 4. Password Reset Tokens Resource (for password reset flow)
543
+ const [okResetTokens, errResetTokens, resetTokensResource] = await tryFn(() =>
544
+ this.database.createResource({
545
+ name: 'plg_password_reset_tokens',
546
+ attributes: {
547
+ userId: 'string|required',
548
+ token: 'string|required',
549
+ expiresAt: 'string|required',
550
+ used: 'boolean|default:false',
551
+ createdAt: 'string|optional'
552
+ },
553
+ behavior: 'body-overflow',
554
+ timestamps: true,
555
+ createdBy: 'IdentityPlugin'
556
+ })
557
+ );
558
+
559
+ if (okResetTokens) {
560
+ this.passwordResetTokensResource = resetTokensResource;
561
+ if (this.config.verbose) {
562
+ console.log('[Identity Plugin] Created plg_password_reset_tokens resource');
563
+ }
564
+ } else if (this.database.resources.plg_password_reset_tokens) {
565
+ this.passwordResetTokensResource = this.database.resources.plg_password_reset_tokens;
566
+ if (this.config.verbose) {
567
+ console.log('[Identity Plugin] Using existing plg_password_reset_tokens resource');
568
+ }
569
+ } else {
570
+ throw errResetTokens;
571
+ }
572
+
573
+ // 5. MFA Devices Resource (for multi-factor authentication)
574
+ if (this.config.mfa.enabled) {
575
+ const [okMFA, errMFA, mfaResource] = await tryFn(() =>
576
+ this.database.createResource({
577
+ name: 'plg_mfa_devices',
578
+ attributes: {
579
+ userId: 'string|required',
580
+ type: 'string|required', // 'totp', 'sms', 'email'
581
+ secret: 'secret|required', // TOTP secret (encrypted by S3DB)
582
+ verified: 'boolean|default:false',
583
+ backupCodes: 'array|items:string', // Hashed backup codes
584
+ enrolledAt: 'string',
585
+ lastUsedAt: 'string|optional',
586
+ deviceName: 'string|optional', // User-friendly name
587
+ metadata: 'object|optional'
588
+ },
589
+ behavior: 'body-overflow',
590
+ timestamps: true,
591
+ partitions: {
592
+ byUser: {
593
+ fields: { userId: 'string' }
594
+ }
595
+ },
596
+ createdBy: 'IdentityPlugin'
597
+ })
598
+ );
599
+
600
+ if (okMFA) {
601
+ this.mfaDevicesResource = mfaResource;
602
+ if (this.config.verbose) {
603
+ console.log('[Identity Plugin] Created plg_mfa_devices resource');
604
+ }
605
+ } else if (this.database.resources.plg_mfa_devices) {
606
+ this.mfaDevicesResource = this.database.resources.plg_mfa_devices;
607
+ if (this.config.verbose) {
608
+ console.log('[Identity Plugin] Using existing plg_mfa_devices resource');
609
+ }
610
+ } else {
611
+ console.warn('[Identity Plugin] MFA enabled but failed to create plg_mfa_devices resource:', errMFA?.message);
612
+ }
613
+ }
614
+ }
615
+
616
+ /**
617
+ * Create user-managed resources (users, tenants, clients) with merged config
618
+ * @private
619
+ */
620
+ async _createUserManagedResources() {
621
+ // 1. Create Users Resource
622
+ const usersConfig = this.config.resources.users;
623
+
624
+ // Base config for users
625
+ const usersBaseConfig = {
626
+ attributes: BASE_USER_ATTRIBUTES,
627
+ behavior: 'body-overflow',
628
+ timestamps: true
629
+ };
630
+
631
+ // Deep merge user config with base config
632
+ const usersMergedConfig = mergeResourceConfig(
633
+ usersBaseConfig,
634
+ usersConfig.userConfig,
635
+ 'users'
636
+ );
637
+
638
+ // Store merged config for reference
639
+ usersConfig.mergedConfig = usersMergedConfig;
640
+
641
+ const [okUsers, errUsers, usersResource] = await tryFn(() =>
642
+ this.database.createResource(usersMergedConfig)
643
+ );
644
+
645
+ if (okUsers) {
646
+ this.usersResource = usersResource;
647
+ if (this.config.verbose) {
648
+ console.log(`[Identity Plugin] Created ${usersMergedConfig.name} resource with merged config`);
649
+ }
650
+ } else if (this.database.resources[usersMergedConfig.name]) {
651
+ this.usersResource = this.database.resources[usersMergedConfig.name];
652
+ if (this.config.verbose) {
653
+ console.log(`[Identity Plugin] Using existing ${usersMergedConfig.name} resource`);
654
+ }
655
+ } else {
656
+ throw errUsers;
657
+ }
658
+
659
+ // 2. Create Tenants Resource (multi-tenancy support)
660
+ const tenantsConfig = this.config.resources.tenants;
661
+
662
+ const tenantsBaseConfig = {
663
+ attributes: BASE_TENANT_ATTRIBUTES,
664
+ behavior: 'body-overflow',
665
+ timestamps: true
666
+ };
667
+
668
+ const tenantsMergedConfig = mergeResourceConfig(
669
+ tenantsBaseConfig,
670
+ tenantsConfig.userConfig,
671
+ 'tenants'
672
+ );
673
+
674
+ tenantsConfig.mergedConfig = tenantsMergedConfig;
675
+
676
+ const [okTenants, errTenants, tenantsResource] = await tryFn(() =>
677
+ this.database.createResource(tenantsMergedConfig)
678
+ );
679
+
680
+ if (okTenants) {
681
+ this.tenantsResource = tenantsResource;
682
+ if (this.config.verbose) {
683
+ console.log(`[Identity Plugin] Created ${tenantsMergedConfig.name} resource with merged config`);
684
+ }
685
+ } else if (this.database.resources[tenantsMergedConfig.name]) {
686
+ this.tenantsResource = this.database.resources[tenantsMergedConfig.name];
687
+ if (this.config.verbose) {
688
+ console.log(`[Identity Plugin] Using existing ${tenantsMergedConfig.name} resource`);
689
+ }
690
+ } else {
691
+ throw errTenants;
692
+ }
693
+
694
+ // 3. Create OAuth2 Clients Resource
695
+ const clientsConfig = this.config.resources.clients;
696
+
697
+ const clientsBaseConfig = {
698
+ attributes: BASE_CLIENT_ATTRIBUTES,
699
+ behavior: 'body-overflow',
700
+ timestamps: true
701
+ };
702
+
703
+ const clientsMergedConfig = mergeResourceConfig(
704
+ clientsBaseConfig,
705
+ clientsConfig.userConfig,
706
+ 'clients'
707
+ );
708
+
709
+ clientsConfig.mergedConfig = clientsMergedConfig;
710
+
711
+ const [okClients, errClients, clientsResource] = await tryFn(() =>
712
+ this.database.createResource(clientsMergedConfig)
713
+ );
714
+
715
+ if (okClients) {
716
+ this.clientsResource = clientsResource;
717
+ if (this.config.verbose) {
718
+ console.log(`[Identity Plugin] Created ${clientsMergedConfig.name} resource with merged config`);
719
+ }
720
+ } else if (this.database.resources[clientsMergedConfig.name]) {
721
+ this.clientsResource = this.database.resources[clientsMergedConfig.name];
722
+ if (this.config.verbose) {
723
+ console.log(`[Identity Plugin] Using existing ${clientsMergedConfig.name} resource`);
724
+ }
725
+ } else {
726
+ throw errClients;
727
+ }
728
+ }
729
+
730
+ /**
731
+ * Initialize OAuth2 Server instance
732
+ * @private
733
+ */
734
+ async _initializeOAuth2Server() {
735
+ this.oauth2Server = new OAuth2Server({
736
+ issuer: this.config.issuer,
737
+ keyResource: this.oauth2KeysResource,
738
+ userResource: this.usersResource,
739
+ clientResource: this.clientsResource,
740
+ authCodeResource: this.oauth2AuthCodesResource,
741
+ supportedScopes: this.config.supportedScopes,
742
+ supportedGrantTypes: this.config.supportedGrantTypes,
743
+ supportedResponseTypes: this.config.supportedResponseTypes,
744
+ accessTokenExpiry: this.config.accessTokenExpiry,
745
+ idTokenExpiry: this.config.idTokenExpiry,
746
+ refreshTokenExpiry: this.config.refreshTokenExpiry,
747
+ authCodeExpiry: this.config.authCodeExpiry
748
+ });
749
+
750
+ await this.oauth2Server.initialize();
751
+
752
+ if (this.config.verbose) {
753
+ console.log('[Identity Plugin] OAuth2 Server initialized');
754
+ console.log(`[Identity Plugin] Issuer: ${this.config.issuer}`);
755
+ console.log(`[Identity Plugin] Supported scopes: ${this.config.supportedScopes.join(', ')}`);
756
+ console.log(`[Identity Plugin] Supported grant types: ${this.config.supportedGrantTypes.join(', ')}`);
757
+ }
758
+ }
759
+
760
+ /**
761
+ * Initialize Session Manager
762
+ * @private
763
+ */
764
+ async _initializeSessionManager() {
765
+ const { SessionManager } = await import('./session-manager.js');
766
+
767
+ this.sessionManager = new SessionManager({
768
+ sessionResource: this.sessionsResource,
769
+ config: this.config.session
770
+ });
771
+
772
+ if (this.config.verbose) {
773
+ console.log('[Identity Plugin] Session Manager initialized');
774
+ console.log(`[Identity Plugin] Session expiry: ${this.config.session.sessionExpiry}`);
775
+ console.log(`[Identity Plugin] Cookie name: ${this.config.session.cookieName}`);
776
+ }
777
+ }
778
+
779
+ /**
780
+ * Initialize email service
781
+ * @private
782
+ */
783
+ async _initializeEmailService() {
784
+ const { EmailService } = await import('./email-service.js');
785
+
786
+ this.emailService = new EmailService({
787
+ enabled: this.config.email.enabled,
788
+ from: this.config.email.from,
789
+ replyTo: this.config.email.replyTo,
790
+ smtp: this.config.email.smtp,
791
+ templates: this.config.email.templates,
792
+ verbose: this.config.verbose
793
+ });
794
+
795
+ if (this.config.verbose) {
796
+ console.log('[Identity Plugin] Email Service initialized');
797
+ console.log(`[Identity Plugin] Email enabled: ${this.config.email.enabled}`);
798
+ if (this.config.email.enabled) {
799
+ console.log(`[Identity Plugin] SMTP host: ${this.config.email.smtp.host}:${this.config.email.smtp.port}`);
800
+ console.log(`[Identity Plugin] From address: ${this.config.email.from}`);
801
+ }
802
+ }
803
+ }
804
+
805
+ /**
806
+ * Initialize failban manager
807
+ * @private
808
+ */
809
+ async _initializeFailbanManager() {
810
+ if (!this.config.failban.enabled) {
811
+ if (this.config.verbose) {
812
+ console.log('[Identity Plugin] Failban disabled');
813
+ }
814
+ return;
815
+ }
816
+
817
+ const { FailbanManager } = await import('../api/concerns/failban-manager.js');
818
+
819
+ this.failbanManager = new FailbanManager({
820
+ database: this.database,
821
+ enabled: this.config.failban.enabled,
822
+ maxViolations: this.config.failban.maxViolations,
823
+ violationWindow: this.config.failban.violationWindow,
824
+ banDuration: this.config.failban.banDuration,
825
+ whitelist: this.config.failban.whitelist,
826
+ blacklist: this.config.failban.blacklist,
827
+ persistViolations: this.config.failban.persistViolations,
828
+ verbose: this.config.verbose,
829
+ geo: this.config.failban.geo
830
+ });
831
+
832
+ await this.failbanManager.initialize();
833
+
834
+ if (this.config.verbose) {
835
+ console.log('[Identity Plugin] Failban Manager initialized');
836
+ console.log(`[Identity Plugin] Max violations: ${this.config.failban.maxViolations}`);
837
+ console.log(`[Identity Plugin] Violation window: ${this.config.failban.violationWindow}ms`);
838
+ console.log(`[Identity Plugin] Ban duration: ${this.config.failban.banDuration}ms`);
839
+ console.log(`[Identity Plugin] Protected endpoints: login=${this.config.failban.endpoints.login}, token=${this.config.failban.endpoints.token}, register=${this.config.failban.endpoints.register}`);
840
+ if (this.config.failban.geo.enabled) {
841
+ console.log(`[Identity Plugin] GeoIP enabled`);
842
+ console.log(`[Identity Plugin] Allowed countries: ${this.config.failban.geo.allowedCountries.join(', ') || 'all'}`);
843
+ console.log(`[Identity Plugin] Blocked countries: ${this.config.failban.geo.blockedCountries.join(', ') || 'none'}`);
844
+ }
845
+ }
846
+ }
847
+
848
+ /**
849
+ * Initialize audit plugin
850
+ * @private
851
+ */
852
+ async _initializeAuditPlugin() {
853
+ if (!this.config.audit.enabled) {
854
+ if (this.config.verbose) {
855
+ console.log('[Identity Plugin] Audit logging disabled');
856
+ }
857
+ return;
858
+ }
859
+
860
+ const { AuditPlugin } = await import('../audit.plugin.js');
861
+
862
+ this.auditPlugin = new AuditPlugin({
863
+ includeData: this.config.audit.includeData,
864
+ includePartitions: this.config.audit.includePartitions,
865
+ maxDataSize: this.config.audit.maxDataSize,
866
+ resources: this.config.audit.resources
867
+ });
868
+
869
+ await this.database.usePlugin(this.auditPlugin);
870
+
871
+ if (this.config.verbose) {
872
+ console.log('[Identity Plugin] Audit Plugin initialized');
873
+ console.log(`[Identity Plugin] Auditing resources: ${this.config.audit.resources.join(', ')}`);
874
+ console.log(`[Identity Plugin] Include data: ${this.config.audit.includeData}`);
875
+ console.log(`[Identity Plugin] Max data size: ${this.config.audit.maxDataSize} bytes`);
876
+ }
877
+ }
878
+
879
+ /**
880
+ * Log custom audit event
881
+ * @param {string} event - Event name
882
+ * @param {Object} data - Event data
883
+ * @private
884
+ */
885
+ async _logAuditEvent(event, data = {}) {
886
+ if (!this.config.audit.enabled || !this.auditPlugin) {
887
+ return;
888
+ }
889
+
890
+ if (!this.config.audit.events.includes(event)) {
891
+ return;
892
+ }
893
+
894
+ try {
895
+ await this.auditPlugin.logCustomEvent(event, data);
896
+
897
+ if (this.config.verbose) {
898
+ console.log(`[Audit] ${event}:`, JSON.stringify(data));
899
+ }
900
+ } catch (error) {
901
+ console.error(`[Audit] Failed to log event ${event}:`, error.message);
902
+ }
903
+ }
904
+
905
+ /**
906
+ * Initialize MFA Manager (Multi-Factor Authentication)
907
+ * @private
908
+ */
909
+ async _initializeMFAManager() {
910
+ if (!this.config.mfa.enabled) {
911
+ if (this.config.verbose) {
912
+ console.log('[Identity Plugin] MFA disabled');
913
+ }
914
+ return;
915
+ }
916
+
917
+ const { MFAManager } = await import('./concerns/mfa-manager.js');
918
+
919
+ this.mfaManager = new MFAManager({
920
+ issuer: this.config.mfa.issuer,
921
+ algorithm: this.config.mfa.algorithm,
922
+ digits: this.config.mfa.digits,
923
+ period: this.config.mfa.period,
924
+ window: this.config.mfa.window,
925
+ backupCodesCount: this.config.mfa.backupCodesCount,
926
+ backupCodeLength: this.config.mfa.backupCodeLength
927
+ });
928
+
929
+ await this.mfaManager.initialize();
930
+
931
+ if (this.config.verbose) {
932
+ console.log('[Identity Plugin] MFA Manager initialized');
933
+ console.log(`[Identity Plugin] Issuer: ${this.config.mfa.issuer}`);
934
+ console.log(`[Identity Plugin] Algorithm: ${this.config.mfa.algorithm}`);
935
+ console.log(`[Identity Plugin] Digits: ${this.config.mfa.digits}`);
936
+ console.log(`[Identity Plugin] Period: ${this.config.mfa.period}s`);
937
+ console.log(`[Identity Plugin] Required: ${this.config.mfa.required}`);
938
+ }
939
+ }
940
+
941
+ /**
942
+ * Start plugin
943
+ */
944
+ async onStart() {
945
+ if (this.config.verbose) {
946
+ console.log('[Identity Plugin] Starting server...');
947
+ }
948
+
949
+ // Dynamic import of server (will create in next step)
950
+ const { IdentityServer } = await import('./server.js');
951
+
952
+ // Create server instance
953
+ this.server = new IdentityServer({
954
+ port: this.config.port,
955
+ host: this.config.host,
956
+ verbose: this.config.verbose,
957
+ issuer: this.config.issuer,
958
+ oauth2Server: this.oauth2Server,
959
+ sessionManager: this.sessionManager,
960
+ usersResource: this.usersResource,
961
+ identityPlugin: this,
962
+ failbanManager: this.failbanManager,
963
+ failbanConfig: this.config.failban,
964
+ accountLockoutConfig: this.config.accountLockout,
965
+ cors: this.config.cors,
966
+ security: this.config.security,
967
+ logging: this.config.logging
968
+ });
969
+
970
+ // Start server
971
+ await this.server.start();
972
+
973
+ this.emit('plugin.started', {
974
+ port: this.config.port,
975
+ host: this.config.host,
976
+ issuer: this.config.issuer
977
+ });
978
+ }
979
+
980
+ /**
981
+ * Stop plugin
982
+ */
983
+ async onStop() {
984
+ if (this.config.verbose) {
985
+ console.log('[Identity Plugin] Stopping server...');
986
+ }
987
+
988
+ if (this.server) {
989
+ await this.server.stop();
990
+ this.server = null;
991
+ }
992
+
993
+ // Stop session cleanup timer
994
+ if (this.sessionManager) {
995
+ this.sessionManager.stopCleanup();
996
+ }
997
+
998
+ // Close email service connection
999
+ if (this.emailService) {
1000
+ await this.emailService.close();
1001
+ }
1002
+
1003
+ // Cleanup failban manager
1004
+ if (this.failbanManager) {
1005
+ await this.failbanManager.cleanup();
1006
+ }
1007
+
1008
+ this.emit('plugin.stopped');
1009
+ }
1010
+
1011
+ /**
1012
+ * Uninstall plugin
1013
+ */
1014
+ async onUninstall(options = {}) {
1015
+ const { purgeData = false } = options;
1016
+
1017
+ // Stop server if running
1018
+ await this.onStop();
1019
+
1020
+ // Optionally delete OAuth2 resources
1021
+ if (purgeData) {
1022
+ const resourcesToDelete = ['plg_oauth_keys', 'plg_oauth_clients', 'plg_auth_codes'];
1023
+
1024
+ for (const resourceName of resourcesToDelete) {
1025
+ const [ok] = await tryFn(() => this.database.deleteResource(resourceName));
1026
+ if (ok && this.config.verbose) {
1027
+ console.log(`[Identity Plugin] Deleted ${resourceName} resource`);
1028
+ }
1029
+ }
1030
+ }
1031
+
1032
+ if (this.config.verbose) {
1033
+ console.log('[Identity Plugin] Uninstalled successfully');
1034
+ }
1035
+ }
1036
+
1037
+ /**
1038
+ * Get server information
1039
+ * @returns {Object} Server info
1040
+ */
1041
+ getServerInfo() {
1042
+ return this.server ? this.server.getInfo() : { isRunning: false };
1043
+ }
1044
+
1045
+ /**
1046
+ * Get OAuth2 Server instance (for advanced usage)
1047
+ * @returns {OAuth2Server|null}
1048
+ */
1049
+ getOAuth2Server() {
1050
+ return this.oauth2Server;
1051
+ }
1052
+ }