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
@@ -67,21 +67,31 @@ async function verifyPassword(inputPassword, storedPassword, passphrase) {
67
67
  * Create Basic Auth middleware
68
68
  * @param {Object} options - Basic Auth options
69
69
  * @param {string} options.realm - Authentication realm (default: 'API Access')
70
- * @param {Object} options.usersResource - Users resource for credential validation
70
+ * @param {Object} options.authResource - Resource for credential validation
71
+ * @param {string} options.usernameField - Field name for username (default: 'email')
72
+ * @param {string} options.passwordField - Field name for password (default: 'password')
71
73
  * @param {string} options.passphrase - Passphrase for password decryption
72
74
  * @param {boolean} options.optional - If true, allows requests without auth
75
+ * @param {Object} options.adminUser - Root admin credentials (bypasses DB lookup)
76
+ * @param {boolean} options.adminUser.enabled - Enable admin root user bypass (default: false)
77
+ * @param {string} options.adminUser.username - Admin username
78
+ * @param {string} options.adminUser.password - Admin password (plain text)
79
+ * @param {Array<string>} options.adminUser.scopes - Admin scopes (default: ['admin'])
73
80
  * @returns {Function} Hono middleware
74
81
  */
75
82
  export function basicAuth(options = {}) {
76
83
  const {
77
84
  realm = 'API Access',
78
- usersResource,
85
+ authResource,
86
+ usernameField = 'email',
87
+ passwordField = 'password',
79
88
  passphrase = 'secret',
80
- optional = false
89
+ optional = false,
90
+ adminUser = null
81
91
  } = options;
82
92
 
83
- if (!usersResource) {
84
- throw new Error('usersResource is required for Basic authentication');
93
+ if (!authResource) {
94
+ throw new Error('authResource is required for Basic authentication');
85
95
  }
86
96
 
87
97
  return async (c, next) => {
@@ -107,9 +117,26 @@ export function basicAuth(options = {}) {
107
117
 
108
118
  const { username, password } = credentials;
109
119
 
110
- // Query user by username
120
+ // Check admin user first (bypasses DB lookup)
121
+ if (adminUser && adminUser.enabled === true) {
122
+ if (username === adminUser.username && password === adminUser.password) {
123
+ c.set('user', {
124
+ id: 'root',
125
+ username: adminUser.username,
126
+ email: adminUser.username,
127
+ scopes: adminUser.scopes || ['admin'],
128
+ authMethod: 'basic-admin'
129
+ });
130
+ c.set('authMethod', 'basic');
131
+ await next();
132
+ return;
133
+ }
134
+ }
135
+
136
+ // Query user by configured username field
111
137
  try {
112
- const users = await usersResource.query({ username });
138
+ const queryFilter = { [usernameField]: username };
139
+ const users = await authResource.query(queryFilter);
113
140
 
114
141
  if (!users || users.length === 0) {
115
142
  c.header('WWW-Authenticate', `Basic realm="${realm}"`);
@@ -119,14 +146,17 @@ export function basicAuth(options = {}) {
119
146
 
120
147
  const user = users[0];
121
148
 
122
- if (!user.active) {
149
+ // Check if user is active (if field exists)
150
+ if (user.active !== undefined && !user.active) {
123
151
  c.header('WWW-Authenticate', `Basic realm="${realm}"`);
124
152
  const response = unauthorized('User account is inactive');
125
153
  return c.json(response, response._status);
126
154
  }
127
155
 
128
- // Verify password
129
- const isValid = await verifyPassword(password, user.password, passphrase);
156
+ // Verify password using configured password field
157
+ // Schema handles encryption/decryption for 'secret' field types
158
+ const storedPassword = user[passwordField];
159
+ const isValid = storedPassword === password;
130
160
 
131
161
  if (!isValid) {
132
162
  c.header('WWW-Authenticate', `Basic realm="${realm}"`);
@@ -7,17 +7,23 @@
7
7
  import { jwtAuth } from './jwt-auth.js';
8
8
  import { apiKeyAuth } from './api-key-auth.js';
9
9
  import { basicAuth } from './basic-auth.js';
10
+ import { createOAuth2Handler } from './oauth2-auth.js';
11
+ import { OIDCClient } from './oidc-client.js';
10
12
  import { unauthorized } from '../utils/response-formatter.js';
11
13
 
12
14
  /**
13
15
  * Create authentication middleware that supports multiple auth methods
14
16
  * @param {Object} options - Authentication options
15
- * @param {Array<string>} options.methods - Allowed auth methods (['jwt', 'apiKey', 'basic'])
17
+ * @param {Array<string>} options.methods - Allowed auth methods (['jwt', 'apiKey', 'basic', 'oauth2'])
16
18
  * @param {Object} options.jwt - JWT configuration
17
19
  * @param {Object} options.apiKey - API Key configuration
18
20
  * @param {Object} options.basic - Basic Auth configuration
21
+ * @param {Object} options.oauth2 - OAuth2 configuration
22
+ * @param {Function} options.oidc - OIDC middleware (already configured)
19
23
  * @param {Object} options.usersResource - Users resource
20
24
  * @param {boolean} options.optional - If true, allows requests without auth
25
+ * @param {string} options.strategy - Auth strategy: 'any' (default, OR logic) or 'priority' (waterfall with explicit order)
26
+ * @param {Object} options.priorities - Priority map for 'priority' strategy { jwt: 1, oidc: 2, basic: 3 }
21
27
  * @returns {Function} Hono middleware
22
28
  */
23
29
  export function createAuthMiddleware(options = {}) {
@@ -26,8 +32,12 @@ export function createAuthMiddleware(options = {}) {
26
32
  jwt: jwtConfig = {},
27
33
  apiKey: apiKeyConfig = {},
28
34
  basic: basicConfig = {},
35
+ oauth2: oauth2Config = {},
36
+ oidc: oidcMiddleware = null,
29
37
  usersResource,
30
- optional = false
38
+ optional = false,
39
+ strategy = 'any',
40
+ priorities = {}
31
41
  } = options;
32
42
 
33
43
  // If no methods specified, allow all requests
@@ -72,6 +82,38 @@ export function createAuthMiddleware(options = {}) {
72
82
  });
73
83
  }
74
84
 
85
+ if (methods.includes('oauth2') && oauth2Config.issuer) {
86
+ const oauth2Handler = createOAuth2Handler(oauth2Config, usersResource);
87
+ middlewares.push({
88
+ name: 'oauth2',
89
+ middleware: async (c, next) => {
90
+ const user = await oauth2Handler(c);
91
+ if (user) {
92
+ c.set('user', user);
93
+ return await next();
94
+ }
95
+ // No user, try next method
96
+ }
97
+ });
98
+ }
99
+
100
+ // OIDC middleware (session-based authentication)
101
+ if (oidcMiddleware) {
102
+ middlewares.push({
103
+ name: 'oidc',
104
+ middleware: oidcMiddleware
105
+ });
106
+ }
107
+
108
+ // Sort middlewares by priority if strategy is 'priority'
109
+ if (strategy === 'priority' && Object.keys(priorities).length > 0) {
110
+ middlewares.sort((a, b) => {
111
+ const priorityA = priorities[a.name] || 999; // Unspecified = lowest priority
112
+ const priorityB = priorities[b.name] || 999;
113
+ return priorityA - priorityB; // Lower number = higher priority
114
+ });
115
+ }
116
+
75
117
  // Return combined middleware
76
118
  return async (c, next) => {
77
119
  // Try each auth method
@@ -104,9 +146,13 @@ export function createAuthMiddleware(options = {}) {
104
146
  };
105
147
  }
106
148
 
149
+ export { OIDCClient };
150
+
107
151
  export default {
108
152
  createAuthMiddleware,
109
153
  jwtAuth,
110
154
  apiKeyAuth,
111
- basicAuth
155
+ basicAuth,
156
+ createOAuth2Handler,
157
+ OIDCClient
112
158
  };
@@ -0,0 +1,171 @@
1
+ /**
2
+ * OAuth2/OIDC Authentication Driver (Resource Server)
3
+ *
4
+ * Validates JWT access tokens issued by an OAuth2/OIDC Authorization Server.
5
+ * Fetches public keys from JWKS endpoint and verifies token signatures.
6
+ *
7
+ * Use this driver when your application acts as a Resource Server
8
+ * consuming tokens from an external Authorization Server (SSO).
9
+ *
10
+ * @example
11
+ * {
12
+ * driver: 'oauth2',
13
+ * config: {
14
+ * issuer: 'http://localhost:4000',
15
+ * jwksUri: 'http://localhost:4000/.well-known/jwks.json',
16
+ * audience: 'my-api',
17
+ * algorithms: ['RS256'],
18
+ * cacheTTL: 3600000 // 1 hour
19
+ * }
20
+ * }
21
+ */
22
+
23
+ import { createRemoteJWKSet, jwtVerify } from 'jose';
24
+
25
+ // Cache for JWKS (avoids fetching on every request)
26
+ const jwksCache = new Map();
27
+
28
+ /**
29
+ * Create OAuth2 authentication handler
30
+ * @param {Object} config - OAuth2 configuration
31
+ * @param {Object} usersResource - s3db.js users resource
32
+ * @returns {Function} Hono middleware
33
+ */
34
+ export function createOAuth2Handler(config, usersResource) {
35
+ const {
36
+ issuer,
37
+ jwksUri,
38
+ audience = null,
39
+ algorithms = ['RS256', 'ES256'],
40
+ cacheTTL = 3600000, // 1 hour
41
+ clockTolerance = 60, // 60 seconds tolerance for exp/nbf
42
+ validateScopes = true,
43
+ fetchUserInfo = true
44
+ } = config;
45
+
46
+ if (!issuer) {
47
+ throw new Error('[OAuth2 Auth] Missing required config: issuer');
48
+ }
49
+
50
+ // Construct JWKS URI from issuer if not provided
51
+ const finalJwksUri = jwksUri || `${issuer}/.well-known/jwks.json`;
52
+
53
+ // Get or create JWKS fetcher (cached)
54
+ const getJWKS = () => {
55
+ const cacheKey = finalJwksUri;
56
+
57
+ if (jwksCache.has(cacheKey)) {
58
+ const cached = jwksCache.get(cacheKey);
59
+ if (Date.now() - cached.timestamp < cacheTTL) {
60
+ return cached.jwks;
61
+ }
62
+ }
63
+
64
+ // Create remote JWKS fetcher
65
+ const jwks = createRemoteJWKSet(new URL(finalJwksUri), {
66
+ cooldownDuration: 30000, // 30 seconds cooldown between fetches
67
+ cacheMaxAge: cacheTTL
68
+ });
69
+
70
+ jwksCache.set(cacheKey, {
71
+ jwks,
72
+ timestamp: Date.now()
73
+ });
74
+
75
+ return jwks;
76
+ };
77
+
78
+ /**
79
+ * OAuth2 authentication middleware
80
+ */
81
+ return async (c) => {
82
+ // Extract token from Authorization header
83
+ const authHeader = c.req.header('authorization') || c.req.header('Authorization');
84
+
85
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
86
+ return null; // No OAuth2 token, try next auth method
87
+ }
88
+
89
+ const token = authHeader.substring(7); // Remove 'Bearer ' prefix
90
+
91
+ try {
92
+ // Verify JWT token with remote JWKS
93
+ const jwks = getJWKS();
94
+
95
+ const verifyOptions = {
96
+ issuer,
97
+ algorithms,
98
+ clockTolerance
99
+ };
100
+
101
+ if (audience) {
102
+ verifyOptions.audience = audience;
103
+ }
104
+
105
+ const { payload } = await jwtVerify(token, jwks, verifyOptions);
106
+
107
+ // Extract user info from token claims
108
+ const userId = payload.sub; // Subject (user ID)
109
+ const email = payload.email || null;
110
+ const username = payload.preferred_username || payload.username || email;
111
+ const scopes = payload.scope ? payload.scope.split(' ') : (payload.scopes || []);
112
+ const role = payload.role || 'user';
113
+
114
+ // Optionally fetch full user info from database
115
+ let user = null;
116
+
117
+ if (fetchUserInfo && userId && usersResource) {
118
+ try {
119
+ // Try to find user by ID
120
+ user = await usersResource.get(userId).catch(() => null);
121
+
122
+ // If not found by ID, try by email
123
+ if (!user && email) {
124
+ const users = await usersResource.query({ email }, { limit: 1 });
125
+ user = users[0] || null;
126
+ }
127
+ } catch (err) {
128
+ // User not found in local database, use token claims only
129
+ }
130
+ }
131
+
132
+ // If user found in database, merge with token claims
133
+ if (user) {
134
+ return {
135
+ ...user,
136
+ scopes: user.scopes || scopes, // Prefer database scopes
137
+ role: user.role || role,
138
+ tokenClaims: payload // Include full token claims
139
+ };
140
+ }
141
+
142
+ // User not in database, create virtual user from token
143
+ return {
144
+ id: userId,
145
+ username: username || userId,
146
+ email,
147
+ role,
148
+ scopes,
149
+ active: true,
150
+ tokenClaims: payload,
151
+ isVirtual: true // Flag to indicate user is not in local database
152
+ };
153
+
154
+ } catch (err) {
155
+ // Token verification failed
156
+ if (config.verbose) {
157
+ console.error('[OAuth2 Auth] Token verification failed:', err.message);
158
+ }
159
+ return null; // Invalid token, try next auth method
160
+ }
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Clear JWKS cache (useful for testing or when keys are rotated)
166
+ */
167
+ export function clearJWKSCache() {
168
+ jwksCache.clear();
169
+ }
170
+
171
+ export default createOAuth2Handler;