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,462 @@
1
+ /**
2
+ * OIDC Client Middleware for Resource Servers
3
+ *
4
+ * Validates RS256 JWT tokens issued by an OAuth2/OIDC Authorization Server.
5
+ * Fetches and caches JWKS (public keys) from the issuer's /.well-known/jwks.json endpoint.
6
+ *
7
+ * @example
8
+ * import { OIDCClient } from 's3db.js/plugins/api/auth/oidc-client';
9
+ *
10
+ * const oidcClient = new OIDCClient({
11
+ * issuer: 'https://sso.example.com',
12
+ * audience: 'https://api.example.com',
13
+ * jwksCacheTTL: 3600000 // 1 hour
14
+ * });
15
+ *
16
+ * await oidcClient.initialize();
17
+ *
18
+ * // Use with API plugin
19
+ * apiPlugin.addAuthDriver('oidc', oidcClient.middleware.bind(oidcClient));
20
+ *
21
+ * // Or use directly in routes
22
+ * apiPlugin.addRoute({
23
+ * path: '/protected',
24
+ * method: 'GET',
25
+ * handler: async (req, res) => {
26
+ * // req.user contains validated token payload
27
+ * res.json({ user: req.user });
28
+ * },
29
+ * auth: 'oidc'
30
+ * });
31
+ */
32
+
33
+ import { createVerify, createPublicKey } from 'crypto';
34
+
35
+ /**
36
+ * Validate JWT claims
37
+ * @param {Object} payload - Token payload
38
+ * @param {Object} options - Validation options
39
+ * @returns {Object} Validation result
40
+ */
41
+ function validateClaims(payload, options = {}) {
42
+ const {
43
+ issuer,
44
+ audience,
45
+ clockTolerance = 60
46
+ } = options;
47
+
48
+ const now = Math.floor(Date.now() / 1000);
49
+
50
+ // Check required claims
51
+ if (!payload.sub) {
52
+ return { valid: false, error: 'Missing required claim: sub' };
53
+ }
54
+
55
+ if (!payload.iat) {
56
+ return { valid: false, error: 'Missing required claim: iat' };
57
+ }
58
+
59
+ if (!payload.exp) {
60
+ return { valid: false, error: 'Missing required claim: exp' };
61
+ }
62
+
63
+ // Validate issuer
64
+ if (issuer && payload.iss !== issuer) {
65
+ return {
66
+ valid: false,
67
+ error: `Invalid issuer. Expected: ${issuer}, Got: ${payload.iss}`
68
+ };
69
+ }
70
+
71
+ // Validate audience
72
+ if (audience) {
73
+ const audiences = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
74
+
75
+ if (!audiences.includes(audience)) {
76
+ return {
77
+ valid: false,
78
+ error: `Invalid audience. Expected: ${audience}, Got: ${audiences.join(', ')}`
79
+ };
80
+ }
81
+ }
82
+
83
+ // Validate expiration with clock tolerance
84
+ if (payload.exp < (now - clockTolerance)) {
85
+ return { valid: false, error: 'Token has expired' };
86
+ }
87
+
88
+ // Validate not before (if present)
89
+ if (payload.nbf && payload.nbf > (now + clockTolerance)) {
90
+ return { valid: false, error: 'Token not yet valid (nbf)' };
91
+ }
92
+
93
+ // Validate issued at (basic sanity check - not in future)
94
+ if (payload.iat > (now + clockTolerance)) {
95
+ return { valid: false, error: 'Token issued in the future' };
96
+ }
97
+
98
+ return { valid: true, error: null };
99
+ }
100
+
101
+ /**
102
+ * OIDC Client for validating tokens from Authorization Server
103
+ */
104
+ export class OIDCClient {
105
+ constructor(options = {}) {
106
+ const {
107
+ issuer,
108
+ audience,
109
+ jwksUri,
110
+ jwksCacheTTL = 3600000, // 1 hour
111
+ clockTolerance = 60,
112
+ autoRefreshJWKS = true,
113
+ discoveryUri
114
+ } = options;
115
+
116
+ if (!issuer) {
117
+ throw new Error('issuer is required for OIDCClient');
118
+ }
119
+
120
+ this.issuer = issuer.replace(/\/$/, '');
121
+ this.audience = audience;
122
+ this.jwksUri = jwksUri || `${this.issuer}/.well-known/jwks.json`;
123
+ this.discoveryUri = discoveryUri || `${this.issuer}/.well-known/openid-configuration`;
124
+ this.jwksCacheTTL = jwksCacheTTL;
125
+ this.clockTolerance = clockTolerance;
126
+ this.autoRefreshJWKS = autoRefreshJWKS;
127
+
128
+ this.jwksCache = null;
129
+ this.jwksCacheExpiry = null;
130
+ this.discoveryCache = null;
131
+ this.keys = new Map(); // kid → publicKey (PEM)
132
+ }
133
+
134
+ /**
135
+ * Initialize OIDC client - fetch discovery document and JWKS
136
+ */
137
+ async initialize() {
138
+ await this.fetchDiscovery();
139
+ await this.fetchJWKS();
140
+
141
+ // Auto-refresh JWKS if enabled
142
+ if (this.autoRefreshJWKS) {
143
+ this.startJWKSRefresh();
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Fetch OIDC discovery document
149
+ */
150
+ async fetchDiscovery() {
151
+ try {
152
+ const response = await fetch(this.discoveryUri);
153
+
154
+ if (!response.ok) {
155
+ throw new Error(`Failed to fetch discovery document: ${response.status}`);
156
+ }
157
+
158
+ this.discoveryCache = await response.json();
159
+
160
+ // Update jwksUri from discovery if available
161
+ if (this.discoveryCache.jwks_uri) {
162
+ this.jwksUri = this.discoveryCache.jwks_uri;
163
+ }
164
+
165
+ return this.discoveryCache;
166
+ } catch (error) {
167
+ throw new Error(`Failed to fetch OIDC discovery: ${error.message}`);
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Fetch JWKS from issuer
173
+ */
174
+ async fetchJWKS(force = false) {
175
+ const now = Date.now();
176
+
177
+ // Return cached JWKS if still valid
178
+ if (!force && this.jwksCache && this.jwksCacheExpiry > now) {
179
+ return this.jwksCache;
180
+ }
181
+
182
+ try {
183
+ const response = await fetch(this.jwksUri);
184
+
185
+ if (!response.ok) {
186
+ throw new Error(`Failed to fetch JWKS: ${response.status}`);
187
+ }
188
+
189
+ const jwks = await response.json();
190
+
191
+ // Convert JWKs to PEM format and cache
192
+ for (const jwk of jwks.keys) {
193
+ if (jwk.kty === 'RSA' && jwk.use === 'sig') {
194
+ const publicKey = this.jwkToPem(jwk);
195
+ this.keys.set(jwk.kid, publicKey);
196
+ }
197
+ }
198
+
199
+ this.jwksCache = jwks;
200
+ this.jwksCacheExpiry = now + this.jwksCacheTTL;
201
+
202
+ return jwks;
203
+ } catch (error) {
204
+ throw new Error(`Failed to fetch JWKS: ${error.message}`);
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Convert JWK to PEM format
210
+ */
211
+ jwkToPem(jwk) {
212
+ try {
213
+ // Use Node.js crypto to import JWK
214
+ const keyObject = createPublicKey({
215
+ key: jwk,
216
+ format: 'jwk'
217
+ });
218
+
219
+ // Export as PEM
220
+ return keyObject.export({
221
+ type: 'spki',
222
+ format: 'pem'
223
+ });
224
+ } catch (error) {
225
+ throw new Error(`Failed to convert JWK to PEM: ${error.message}`);
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Get public key by kid
231
+ */
232
+ async getPublicKey(kid) {
233
+ let publicKey = this.keys.get(kid);
234
+
235
+ // If key not found, try refreshing JWKS
236
+ if (!publicKey) {
237
+ await this.fetchJWKS(true);
238
+ publicKey = this.keys.get(kid);
239
+ }
240
+
241
+ return publicKey;
242
+ }
243
+
244
+ /**
245
+ * Verify RS256 JWT token
246
+ */
247
+ async verifyToken(token) {
248
+ try {
249
+ const parts = token.split('.');
250
+ if (parts.length !== 3) {
251
+ return { valid: false, error: 'Invalid token format' };
252
+ }
253
+
254
+ const [encodedHeader, encodedPayload, signature] = parts;
255
+
256
+ // Decode header
257
+ const header = JSON.parse(Buffer.from(encodedHeader, 'base64url').toString());
258
+
259
+ // Verify algorithm
260
+ if (header.alg !== 'RS256') {
261
+ return { valid: false, error: `Unsupported algorithm: ${header.alg}` };
262
+ }
263
+
264
+ // Get public key
265
+ const publicKey = await this.getPublicKey(header.kid);
266
+
267
+ if (!publicKey) {
268
+ return { valid: false, error: `Public key not found for kid: ${header.kid}` };
269
+ }
270
+
271
+ // Verify signature
272
+ const verify = createVerify('RSA-SHA256');
273
+ verify.update(`${encodedHeader}.${encodedPayload}`);
274
+ verify.end();
275
+
276
+ const isValid = verify.verify(publicKey, signature, 'base64url');
277
+
278
+ if (!isValid) {
279
+ return { valid: false, error: 'Invalid signature' };
280
+ }
281
+
282
+ // Decode payload
283
+ const payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString());
284
+
285
+ // Validate claims
286
+ const claimValidation = validateClaims(payload, {
287
+ issuer: this.issuer,
288
+ audience: this.audience,
289
+ clockTolerance: this.clockTolerance
290
+ });
291
+
292
+ if (!claimValidation.valid) {
293
+ return { valid: false, error: claimValidation.error };
294
+ }
295
+
296
+ return {
297
+ valid: true,
298
+ header,
299
+ payload
300
+ };
301
+ } catch (error) {
302
+ return { valid: false, error: error.message };
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Express middleware for OIDC authentication
308
+ */
309
+ async middleware(req, res, next) {
310
+ try {
311
+ // Extract token from Authorization header
312
+ const authHeader = req.headers.authorization;
313
+
314
+ if (!authHeader) {
315
+ return res.status(401).json({
316
+ error: 'unauthorized',
317
+ error_description: 'Missing Authorization header'
318
+ });
319
+ }
320
+
321
+ if (!authHeader.startsWith('Bearer ')) {
322
+ return res.status(401).json({
323
+ error: 'unauthorized',
324
+ error_description: 'Invalid Authorization header format. Expected: Bearer <token>'
325
+ });
326
+ }
327
+
328
+ const token = authHeader.substring(7);
329
+
330
+ if (!token) {
331
+ return res.status(401).json({
332
+ error: 'unauthorized',
333
+ error_description: 'Missing token'
334
+ });
335
+ }
336
+
337
+ // Verify token
338
+ const verification = await this.verifyToken(token);
339
+
340
+ if (!verification.valid) {
341
+ return res.status(401).json({
342
+ error: 'invalid_token',
343
+ error_description: verification.error
344
+ });
345
+ }
346
+
347
+ // Attach user to request
348
+ req.user = verification.payload;
349
+ req.token = token;
350
+
351
+ // Continue to next middleware
352
+ next();
353
+ } catch (error) {
354
+ res.status(500).json({
355
+ error: 'server_error',
356
+ error_description: error.message
357
+ });
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Start auto-refresh of JWKS
363
+ */
364
+ startJWKSRefresh() {
365
+ // Refresh JWKS periodically (half of TTL to ensure fresh keys)
366
+ const refreshInterval = Math.floor(this.jwksCacheTTL / 2);
367
+
368
+ this.refreshInterval = setInterval(async () => {
369
+ try {
370
+ await this.fetchJWKS(true);
371
+ } catch (error) {
372
+ console.error('Failed to refresh JWKS:', error);
373
+ }
374
+ }, refreshInterval);
375
+ }
376
+
377
+ /**
378
+ * Stop auto-refresh of JWKS
379
+ */
380
+ stopJWKSRefresh() {
381
+ if (this.refreshInterval) {
382
+ clearInterval(this.refreshInterval);
383
+ this.refreshInterval = null;
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Introspect token via Authorization Server (RFC 7662)
389
+ */
390
+ async introspectToken(token, clientId, clientSecret) {
391
+ if (!this.discoveryCache || !this.discoveryCache.introspection_endpoint) {
392
+ throw new Error('Introspection endpoint not available');
393
+ }
394
+
395
+ try {
396
+ const response = await fetch(this.discoveryCache.introspection_endpoint, {
397
+ method: 'POST',
398
+ headers: {
399
+ 'Content-Type': 'application/x-www-form-urlencoded',
400
+ 'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`
401
+ },
402
+ body: new URLSearchParams({ token })
403
+ });
404
+
405
+ if (!response.ok) {
406
+ throw new Error(`Introspection failed: ${response.status}`);
407
+ }
408
+
409
+ return await response.json();
410
+ } catch (error) {
411
+ throw new Error(`Token introspection failed: ${error.message}`);
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Get discovery document
417
+ */
418
+ getDiscovery() {
419
+ return this.discoveryCache;
420
+ }
421
+
422
+ /**
423
+ * Get cached JWKS
424
+ */
425
+ getJWKS() {
426
+ return this.jwksCache;
427
+ }
428
+
429
+ /**
430
+ * Cleanup resources
431
+ */
432
+ destroy() {
433
+ this.stopJWKSRefresh();
434
+ this.keys.clear();
435
+ this.jwksCache = null;
436
+ this.discoveryCache = null;
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Create OIDC middleware factory for easy integration
442
+ */
443
+ export function createOIDCMiddleware(options) {
444
+ const client = new OIDCClient(options);
445
+
446
+ // Return async middleware that initializes on first use
447
+ let initialized = false;
448
+
449
+ return async (req, res, next) => {
450
+ if (!initialized) {
451
+ await client.initialize();
452
+ initialized = true;
453
+ }
454
+
455
+ return client.middleware(req, res, next);
456
+ };
457
+ }
458
+
459
+ export default {
460
+ OIDCClient,
461
+ createOIDCMiddleware
462
+ };