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,284 @@
1
+ /**
2
+ * Path-based Authentication Matcher
3
+ *
4
+ * Provides path-specific authentication rules with precedence by specificity.
5
+ * More specific paths override less specific ones.
6
+ *
7
+ * @example
8
+ * const rules = [
9
+ * { path: '/app/**', methods: ['oidc'], required: true },
10
+ * { path: '/api/v1/**', methods: ['basic', 'oidc'], required: true },
11
+ * { path: '/health', methods: [], required: false },
12
+ * { path: '/**', methods: [], required: false } // default
13
+ * ];
14
+ *
15
+ * const rule = findAuthRule('/app/dashboard', rules);
16
+ * // => { path: '/app/**', methods: ['oidc'], required: true }
17
+ */
18
+
19
+ /**
20
+ * Calculate path specificity score (higher = more specific)
21
+ * @param {string} pattern - Path pattern with wildcards
22
+ * @returns {number} Specificity score
23
+ */
24
+ function calculateSpecificity(pattern) {
25
+ let score = 0;
26
+
27
+ // Exact matches (no wildcards) are most specific
28
+ if (!pattern.includes('*') && !pattern.includes(':')) {
29
+ score += 10000;
30
+ }
31
+
32
+ // Count path segments (more segments = more specific)
33
+ const segments = pattern.split('/').filter(s => s.length > 0);
34
+ score += segments.length * 100;
35
+
36
+ // Penalize wildcards (fewer wildcards = more specific)
37
+ const singleWildcards = (pattern.match(/(?<!\*)\*(?!\*)/g) || []).length;
38
+ const doubleWildcards = (pattern.match(/\*\*/g) || []).length;
39
+ score -= singleWildcards * 10;
40
+ score -= doubleWildcards * 50;
41
+
42
+ // Penalize route params (e.g., :id)
43
+ const params = (pattern.match(/:[^/]+/g) || []).length;
44
+ score -= params * 5;
45
+
46
+ return score;
47
+ }
48
+
49
+ /**
50
+ * Convert glob pattern to regex
51
+ * @param {string} pattern - Glob pattern
52
+ * @returns {RegExp} Regular expression
53
+ */
54
+ function patternToRegex(pattern) {
55
+ // Escape special regex characters except * and :
56
+ let regexPattern = pattern
57
+ .replace(/[.+?^${}()|[\]\\]/g, '\\$&');
58
+
59
+ // Handle route params (:id, :userId, etc.)
60
+ regexPattern = regexPattern.replace(/:([^/]+)/g, '([^/]+)');
61
+
62
+ // Handle wildcards
63
+ regexPattern = regexPattern
64
+ .replace(/\*\*/g, '___GLOBSTAR___') // Temporary placeholder
65
+ .replace(/\*/g, '[^/]*') // * matches anything except /
66
+ .replace(/___GLOBSTAR___/g, '.*'); // ** matches everything including /
67
+
68
+ // Special case: if pattern ends with /**, it should match with or without trailing content
69
+ // e.g., /app/** should match both /app and /app/dashboard
70
+ if (pattern.endsWith('/**')) {
71
+ // Remove the /.*$ part and make it optional
72
+ regexPattern = regexPattern.replace(/\/\.\*$/, '(?:/.*)?');
73
+ }
74
+
75
+ // Anchor to start and end
76
+ regexPattern = '^' + regexPattern + '$';
77
+
78
+ return new RegExp(regexPattern);
79
+ }
80
+
81
+ /**
82
+ * Check if path matches pattern
83
+ * @param {string} path - Request path
84
+ * @param {string} pattern - Path pattern with wildcards
85
+ * @returns {boolean} True if matches
86
+ */
87
+ export function matchPath(path, pattern) {
88
+ // Exact match (fast path)
89
+ if (path === pattern) return true;
90
+
91
+ // Regex match
92
+ const regex = patternToRegex(pattern);
93
+ return regex.test(path);
94
+ }
95
+
96
+ /**
97
+ * Find matching auth rule for path (most specific wins)
98
+ * @param {string} path - Request path
99
+ * @param {Array<Object>} rules - Auth rules
100
+ * @param {string} rules[].path - Path pattern
101
+ * @param {Array<string>} rules[].methods - Allowed auth methods
102
+ * @param {boolean} rules[].required - If true, auth is required
103
+ * @param {string} rules[].strategy - Auth strategy ('any' or 'priority')
104
+ * @param {Object} rules[].priorities - Priority map for 'priority' strategy
105
+ * @returns {Object|null} Matching rule or null
106
+ */
107
+ export function findAuthRule(path, rules = []) {
108
+ if (!rules || rules.length === 0) {
109
+ return null;
110
+ }
111
+
112
+ // Find all matching rules
113
+ const matches = rules
114
+ .map(rule => ({
115
+ ...rule,
116
+ specificity: calculateSpecificity(rule.path)
117
+ }))
118
+ .filter(rule => matchPath(path, rule.path))
119
+ .sort((a, b) => b.specificity - a.specificity); // Highest specificity first
120
+
121
+ // Return most specific match
122
+ return matches.length > 0 ? matches[0] : null;
123
+ }
124
+
125
+ /**
126
+ * Create path-based auth middleware
127
+ * @param {Object} options - Middleware options
128
+ * @param {Array<Object>} options.rules - Path-based auth rules
129
+ * @param {Object} options.authMiddlewares - Available auth middlewares by name
130
+ * @param {Function} options.unauthorizedHandler - Handler for unauthorized requests
131
+ * @returns {Function} Hono middleware
132
+ */
133
+ export function createPathBasedAuthMiddleware(options = {}) {
134
+ const {
135
+ rules = [],
136
+ authMiddlewares = {},
137
+ unauthorizedHandler = null,
138
+ events = null
139
+ } = options;
140
+
141
+ return async (c, next) => {
142
+ const currentPath = c.req.path;
143
+
144
+ // Find matching rule
145
+ const rule = findAuthRule(currentPath, rules);
146
+
147
+ // No rule = no auth required (default public)
148
+ if (!rule) {
149
+ return await next();
150
+ }
151
+
152
+ // Rule says auth not required = public
153
+ if (!rule.required) {
154
+ return await next();
155
+ }
156
+
157
+ // Rule says auth required but no methods = error in config
158
+ if (rule.methods.length === 0 && rule.required) {
159
+ console.error(`[Path Auth] Invalid rule: path "${rule.path}" requires auth but has no methods`);
160
+ if (unauthorizedHandler) {
161
+ return unauthorizedHandler(c, 'Configuration error');
162
+ }
163
+ return c.json({ error: 'Configuration error' }, 500);
164
+ }
165
+
166
+ // Get allowed auth middlewares for this path
167
+ const allowedMiddlewares = rule.methods
168
+ .map(methodName => ({
169
+ name: methodName,
170
+ middleware: authMiddlewares[methodName]
171
+ }))
172
+ .filter(m => m.middleware);
173
+
174
+ if (allowedMiddlewares.length === 0) {
175
+ console.error(`[Path Auth] No middlewares found for methods: ${rule.methods.join(', ')}`);
176
+ if (unauthorizedHandler) {
177
+ return unauthorizedHandler(c, 'No auth methods available');
178
+ }
179
+ return c.json({ error: 'No auth methods available' }, 500);
180
+ }
181
+
182
+ // Sort by priority if strategy is 'priority'
183
+ const strategy = rule.strategy || 'any';
184
+ const priorities = rule.priorities || {};
185
+
186
+ if (strategy === 'priority' && Object.keys(priorities).length > 0) {
187
+ allowedMiddlewares.sort((a, b) => {
188
+ const priorityA = priorities[a.name] || 999;
189
+ const priorityB = priorities[b.name] || 999;
190
+ return priorityA - priorityB; // Lower number = higher priority
191
+ });
192
+ }
193
+
194
+ // Try each auth method
195
+ for (const { name, middleware } of allowedMiddlewares) {
196
+ let authSuccess = false;
197
+ const tempNext = async () => {
198
+ authSuccess = true;
199
+ };
200
+
201
+ // Try auth method
202
+ await middleware(c, tempNext);
203
+
204
+ // If auth succeeded, continue
205
+ if (authSuccess && c.get('user')) {
206
+ // Emit auth:success event
207
+ if (events) {
208
+ events.emitAuthEvent('success', {
209
+ method: name,
210
+ user: c.get('user'),
211
+ path: currentPath,
212
+ rule: rule.path
213
+ });
214
+ }
215
+ return await next();
216
+ }
217
+ }
218
+
219
+ // Emit auth:failure event
220
+ if (events) {
221
+ events.emitAuthEvent('failure', {
222
+ path: currentPath,
223
+ rule: rule.path,
224
+ allowedMethods: rule.methods,
225
+ ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip')
226
+ });
227
+ }
228
+
229
+ // No auth method succeeded - apply content negotiation
230
+ const acceptHeader = c.req.header('accept') || '';
231
+ const acceptsHtml = acceptHeader.includes('text/html');
232
+
233
+ // Get unauthorized behavior from rule (default: 'auto')
234
+ const unauthorizedBehavior = rule.unauthorizedBehavior || 'auto';
235
+
236
+ // Auto mode: HTML → redirect, JSON → 401
237
+ if (unauthorizedBehavior === 'auto') {
238
+ if (acceptsHtml) {
239
+ // Browser request - redirect to login
240
+ const returnTo = encodeURIComponent(c.req.path);
241
+ return c.redirect(`/auth/login?returnTo=${returnTo}`);
242
+ } else {
243
+ // API request - return 401 JSON
244
+ return c.json({
245
+ error: 'Unauthorized',
246
+ message: `Authentication required. Allowed methods: ${rule.methods.join(', ')}`
247
+ }, 401);
248
+ }
249
+ }
250
+
251
+ // Custom behavior object: { html: 'redirect', json: { status: 401 } }
252
+ if (typeof unauthorizedBehavior === 'object') {
253
+ if (acceptsHtml && unauthorizedBehavior.html === 'redirect') {
254
+ const returnTo = encodeURIComponent(c.req.path);
255
+ const loginPath = unauthorizedBehavior.loginPath || '/auth/login';
256
+ return c.redirect(`${loginPath}?returnTo=${returnTo}`);
257
+ }
258
+
259
+ if (!acceptsHtml && unauthorizedBehavior.json) {
260
+ return c.json(
261
+ unauthorizedBehavior.json,
262
+ unauthorizedBehavior.json.status || 401
263
+ );
264
+ }
265
+ }
266
+
267
+ // Fallback: use custom handler or default 401
268
+ if (unauthorizedHandler) {
269
+ return unauthorizedHandler(c, `Authentication required. Allowed methods: ${rule.methods.join(', ')}`);
270
+ }
271
+
272
+ return c.json({
273
+ error: 'Unauthorized',
274
+ message: `Authentication required. Allowed methods: ${rule.methods.join(', ')}`
275
+ }, 401);
276
+ };
277
+ }
278
+
279
+ export default {
280
+ matchPath,
281
+ findAuthRule,
282
+ calculateSpecificity,
283
+ createPathBasedAuthMiddleware
284
+ };
@@ -0,0 +1,134 @@
1
+ /**
2
+ * API Event Emitter
3
+ *
4
+ * Provides event hooks throughout the API lifecycle for monitoring,
5
+ * analytics, and custom integrations.
6
+ *
7
+ * Supported Events:
8
+ * - user:created - New user created via OIDC
9
+ * - user:login - User logged in
10
+ * - auth:success - Authentication succeeded
11
+ * - auth:failure - Authentication failed
12
+ * - resource:created - Resource record created
13
+ * - resource:updated - Resource record updated
14
+ * - resource:deleted - Resource record deleted
15
+ * - request:start - Request started
16
+ * - request:end - Request ended
17
+ * - request:error - Request errored
18
+ *
19
+ * @example
20
+ * const events = new ApiEventEmitter();
21
+ *
22
+ * // Listen to user creation
23
+ * events.on('user:created', (data) => {
24
+ * console.log('New user:', data.user);
25
+ * });
26
+ *
27
+ * // Listen to all resource changes
28
+ * events.on('resource:*', (data) => {
29
+ * console.log('Resource event:', data.event, data.resource);
30
+ * });
31
+ *
32
+ * // Emit events
33
+ * events.emit('user:created', { user: userObject, source: 'oidc' });
34
+ */
35
+
36
+ import { EventEmitter } from 'events';
37
+
38
+ export class ApiEventEmitter extends EventEmitter {
39
+ constructor(options = {}) {
40
+ super();
41
+
42
+ this.options = {
43
+ enabled: options.enabled !== false, // Enabled by default
44
+ verbose: options.verbose || false,
45
+ maxListeners: options.maxListeners || 10
46
+ };
47
+
48
+ this.setMaxListeners(this.options.maxListeners);
49
+ }
50
+
51
+ /**
52
+ * Emit event with wildcard support
53
+ * @param {string} event - Event name
54
+ * @param {Object} data - Event data
55
+ */
56
+ emit(event, data = {}) {
57
+ if (!this.options.enabled) {
58
+ return false;
59
+ }
60
+
61
+ if (this.options.verbose) {
62
+ console.log(`[API Events] ${event}`, data);
63
+ }
64
+
65
+ // Emit specific event
66
+ super.emit(event, { event, ...data, timestamp: new Date().toISOString() });
67
+
68
+ // Emit wildcard pattern (e.g., "resource:*" for "resource:created")
69
+ if (event.includes(':')) {
70
+ const [prefix] = event.split(':');
71
+ const wildcardEvent = `${prefix}:*`;
72
+ super.emit(wildcardEvent, { event, ...data, timestamp: new Date().toISOString() });
73
+ }
74
+
75
+ return true;
76
+ }
77
+
78
+ /**
79
+ * Helper to emit user events
80
+ * @param {string} action - created, login, updated, deleted
81
+ * @param {Object} data - Event data
82
+ */
83
+ emitUserEvent(action, data) {
84
+ this.emit(`user:${action}`, data);
85
+ }
86
+
87
+ /**
88
+ * Helper to emit auth events
89
+ * @param {string} action - success, failure
90
+ * @param {Object} data - Event data
91
+ */
92
+ emitAuthEvent(action, data) {
93
+ this.emit(`auth:${action}`, data);
94
+ }
95
+
96
+ /**
97
+ * Helper to emit resource events
98
+ * @param {string} action - created, updated, deleted
99
+ * @param {Object} data - Event data
100
+ */
101
+ emitResourceEvent(action, data) {
102
+ this.emit(`resource:${action}`, data);
103
+ }
104
+
105
+ /**
106
+ * Helper to emit request events
107
+ * @param {string} action - start, end, error
108
+ * @param {Object} data - Event data
109
+ */
110
+ emitRequestEvent(action, data) {
111
+ this.emit(`request:${action}`, data);
112
+ }
113
+
114
+ /**
115
+ * Get event statistics
116
+ * @returns {Object} Event statistics
117
+ */
118
+ getStats() {
119
+ const stats = {
120
+ enabled: this.options.enabled,
121
+ maxListeners: this.options.maxListeners,
122
+ listeners: {}
123
+ };
124
+
125
+ // Count listeners per event
126
+ for (const event of this.eventNames()) {
127
+ stats.listeners[event] = this.listenerCount(event);
128
+ }
129
+
130
+ return stats;
131
+ }
132
+ }
133
+
134
+ export default ApiEventEmitter;