s3db.js 13.6.0 → 14.0.2

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 (193) hide show
  1. package/README.md +139 -43
  2. package/dist/s3db.cjs +72425 -38970
  3. package/dist/s3db.cjs.map +1 -1
  4. package/dist/s3db.es.js +72177 -38764
  5. package/dist/s3db.es.js.map +1 -1
  6. package/mcp/lib/base-handler.js +157 -0
  7. package/mcp/lib/handlers/connection-handler.js +280 -0
  8. package/mcp/lib/handlers/query-handler.js +533 -0
  9. package/mcp/lib/handlers/resource-handler.js +428 -0
  10. package/mcp/lib/tool-registry.js +336 -0
  11. package/mcp/lib/tools/connection-tools.js +161 -0
  12. package/mcp/lib/tools/query-tools.js +267 -0
  13. package/mcp/lib/tools/resource-tools.js +404 -0
  14. package/package.json +94 -49
  15. package/src/clients/memory-client.class.js +346 -191
  16. package/src/clients/memory-storage.class.js +300 -84
  17. package/src/clients/s3-client.class.js +7 -6
  18. package/src/concerns/geo-encoding.js +19 -2
  19. package/src/concerns/ip.js +59 -9
  20. package/src/concerns/money.js +8 -1
  21. package/src/concerns/password-hashing.js +49 -8
  22. package/src/concerns/plugin-storage.js +186 -18
  23. package/src/concerns/storage-drivers/filesystem-driver.js +284 -0
  24. package/src/database.class.js +139 -29
  25. package/src/errors.js +332 -42
  26. package/src/plugins/api/auth/oidc-auth.js +66 -17
  27. package/src/plugins/api/auth/strategies/base-strategy.class.js +74 -0
  28. package/src/plugins/api/auth/strategies/factory.class.js +63 -0
  29. package/src/plugins/api/auth/strategies/global-strategy.class.js +44 -0
  30. package/src/plugins/api/auth/strategies/path-based-strategy.class.js +83 -0
  31. package/src/plugins/api/auth/strategies/path-rules-strategy.class.js +118 -0
  32. package/src/plugins/api/concerns/failban-manager.js +106 -57
  33. package/src/plugins/api/concerns/opengraph-helper.js +116 -0
  34. package/src/plugins/api/concerns/route-context.js +601 -0
  35. package/src/plugins/api/concerns/state-machine.js +288 -0
  36. package/src/plugins/api/index.js +180 -41
  37. package/src/plugins/api/routes/auth-routes.js +198 -30
  38. package/src/plugins/api/routes/resource-routes.js +19 -4
  39. package/src/plugins/api/server/health-manager.class.js +163 -0
  40. package/src/plugins/api/server/middleware-chain.class.js +310 -0
  41. package/src/plugins/api/server/router.class.js +472 -0
  42. package/src/plugins/api/server.js +280 -1303
  43. package/src/plugins/api/utils/custom-routes.js +17 -5
  44. package/src/plugins/api/utils/guards.js +76 -17
  45. package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
  46. package/src/plugins/api/utils/openapi-generator.js +7 -6
  47. package/src/plugins/api/utils/template-engine.js +77 -3
  48. package/src/plugins/audit.plugin.js +30 -8
  49. package/src/plugins/backup.plugin.js +110 -14
  50. package/src/plugins/cache/cache.class.js +22 -5
  51. package/src/plugins/cache/filesystem-cache.class.js +116 -19
  52. package/src/plugins/cache/memory-cache.class.js +211 -57
  53. package/src/plugins/cache/multi-tier-cache.class.js +371 -0
  54. package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
  55. package/src/plugins/cache/redis-cache.class.js +552 -0
  56. package/src/plugins/cache/s3-cache.class.js +17 -8
  57. package/src/plugins/cache.plugin.js +176 -61
  58. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
  59. package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
  60. package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
  61. package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
  62. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
  63. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
  64. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
  65. package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
  66. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
  67. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
  68. package/src/plugins/cloud-inventory/index.js +29 -8
  69. package/src/plugins/cloud-inventory/registry.js +64 -42
  70. package/src/plugins/cloud-inventory.plugin.js +240 -138
  71. package/src/plugins/concerns/plugin-dependencies.js +54 -0
  72. package/src/plugins/concerns/resource-names.js +100 -0
  73. package/src/plugins/consumers/index.js +10 -2
  74. package/src/plugins/consumers/sqs-consumer.js +12 -2
  75. package/src/plugins/cookie-farm-suite.plugin.js +278 -0
  76. package/src/plugins/cookie-farm.errors.js +73 -0
  77. package/src/plugins/cookie-farm.plugin.js +869 -0
  78. package/src/plugins/costs.plugin.js +7 -1
  79. package/src/plugins/eventual-consistency/analytics.js +94 -19
  80. package/src/plugins/eventual-consistency/config.js +15 -7
  81. package/src/plugins/eventual-consistency/consolidation.js +29 -11
  82. package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
  83. package/src/plugins/eventual-consistency/helpers.js +39 -14
  84. package/src/plugins/eventual-consistency/install.js +21 -2
  85. package/src/plugins/eventual-consistency/utils.js +32 -10
  86. package/src/plugins/fulltext.plugin.js +38 -11
  87. package/src/plugins/geo.plugin.js +61 -9
  88. package/src/plugins/identity/concerns/config.js +61 -0
  89. package/src/plugins/identity/concerns/mfa-manager.js +15 -2
  90. package/src/plugins/identity/concerns/rate-limit.js +124 -0
  91. package/src/plugins/identity/concerns/resource-schemas.js +9 -1
  92. package/src/plugins/identity/concerns/token-generator.js +29 -4
  93. package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
  94. package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
  95. package/src/plugins/identity/drivers/index.js +18 -0
  96. package/src/plugins/identity/drivers/password-driver.js +122 -0
  97. package/src/plugins/identity/email-service.js +17 -2
  98. package/src/plugins/identity/index.js +413 -69
  99. package/src/plugins/identity/oauth2-server.js +413 -30
  100. package/src/plugins/identity/oidc-discovery.js +16 -8
  101. package/src/plugins/identity/rsa-keys.js +115 -35
  102. package/src/plugins/identity/server.js +166 -45
  103. package/src/plugins/identity/session-manager.js +53 -7
  104. package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
  105. package/src/plugins/identity/ui/routes.js +363 -255
  106. package/src/plugins/importer/index.js +153 -20
  107. package/src/plugins/index.js +9 -2
  108. package/src/plugins/kubernetes-inventory/index.js +6 -0
  109. package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
  110. package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
  111. package/src/plugins/kubernetes-inventory.plugin.js +980 -0
  112. package/src/plugins/metrics.plugin.js +64 -16
  113. package/src/plugins/ml/base-model.class.js +25 -15
  114. package/src/plugins/ml/regression-model.class.js +1 -1
  115. package/src/plugins/ml.errors.js +57 -25
  116. package/src/plugins/ml.plugin.js +28 -4
  117. package/src/plugins/namespace.js +210 -0
  118. package/src/plugins/plugin.class.js +180 -8
  119. package/src/plugins/puppeteer/console-monitor.js +729 -0
  120. package/src/plugins/puppeteer/cookie-manager.js +492 -0
  121. package/src/plugins/puppeteer/network-monitor.js +816 -0
  122. package/src/plugins/puppeteer/performance-manager.js +746 -0
  123. package/src/plugins/puppeteer/proxy-manager.js +478 -0
  124. package/src/plugins/puppeteer/stealth-manager.js +556 -0
  125. package/src/plugins/puppeteer.errors.js +81 -0
  126. package/src/plugins/puppeteer.plugin.js +1327 -0
  127. package/src/plugins/queue-consumer.plugin.js +69 -14
  128. package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
  129. package/src/plugins/recon/concerns/command-runner.js +148 -0
  130. package/src/plugins/recon/concerns/diff-detector.js +372 -0
  131. package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
  132. package/src/plugins/recon/concerns/process-manager.js +338 -0
  133. package/src/plugins/recon/concerns/report-generator.js +478 -0
  134. package/src/plugins/recon/concerns/security-analyzer.js +571 -0
  135. package/src/plugins/recon/concerns/target-normalizer.js +68 -0
  136. package/src/plugins/recon/config/defaults.js +321 -0
  137. package/src/plugins/recon/config/resources.js +370 -0
  138. package/src/plugins/recon/index.js +778 -0
  139. package/src/plugins/recon/managers/dependency-manager.js +174 -0
  140. package/src/plugins/recon/managers/scheduler-manager.js +179 -0
  141. package/src/plugins/recon/managers/storage-manager.js +745 -0
  142. package/src/plugins/recon/managers/target-manager.js +274 -0
  143. package/src/plugins/recon/stages/asn-stage.js +314 -0
  144. package/src/plugins/recon/stages/certificate-stage.js +84 -0
  145. package/src/plugins/recon/stages/dns-stage.js +107 -0
  146. package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
  147. package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
  148. package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
  149. package/src/plugins/recon/stages/http-stage.js +89 -0
  150. package/src/plugins/recon/stages/latency-stage.js +148 -0
  151. package/src/plugins/recon/stages/massdns-stage.js +302 -0
  152. package/src/plugins/recon/stages/osint-stage.js +1373 -0
  153. package/src/plugins/recon/stages/ports-stage.js +169 -0
  154. package/src/plugins/recon/stages/screenshot-stage.js +94 -0
  155. package/src/plugins/recon/stages/secrets-stage.js +514 -0
  156. package/src/plugins/recon/stages/subdomains-stage.js +295 -0
  157. package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
  158. package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
  159. package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
  160. package/src/plugins/recon/stages/whois-stage.js +349 -0
  161. package/src/plugins/recon.plugin.js +75 -0
  162. package/src/plugins/recon.plugin.js.backup +2635 -0
  163. package/src/plugins/relation.errors.js +87 -14
  164. package/src/plugins/replicator.plugin.js +514 -137
  165. package/src/plugins/replicators/base-replicator.class.js +89 -1
  166. package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
  167. package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
  168. package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
  169. package/src/plugins/replicators/mysql-replicator.class.js +52 -17
  170. package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
  171. package/src/plugins/replicators/postgres-replicator.class.js +62 -27
  172. package/src/plugins/replicators/s3db-replicator.class.js +25 -18
  173. package/src/plugins/replicators/schema-sync.helper.js +3 -3
  174. package/src/plugins/replicators/sqs-replicator.class.js +8 -2
  175. package/src/plugins/replicators/turso-replicator.class.js +23 -3
  176. package/src/plugins/replicators/webhook-replicator.class.js +42 -4
  177. package/src/plugins/s3-queue.plugin.js +464 -65
  178. package/src/plugins/scheduler.plugin.js +20 -6
  179. package/src/plugins/state-machine.plugin.js +40 -9
  180. package/src/plugins/tfstate/README.md +126 -126
  181. package/src/plugins/tfstate/base-driver.js +28 -4
  182. package/src/plugins/tfstate/errors.js +65 -10
  183. package/src/plugins/tfstate/filesystem-driver.js +52 -8
  184. package/src/plugins/tfstate/index.js +163 -90
  185. package/src/plugins/tfstate/s3-driver.js +64 -6
  186. package/src/plugins/ttl.plugin.js +72 -17
  187. package/src/plugins/vector/distances.js +18 -12
  188. package/src/plugins/vector/kmeans.js +26 -4
  189. package/src/resource.class.js +115 -19
  190. package/src/testing/factory.class.js +20 -3
  191. package/src/testing/seeder.class.js +7 -1
  192. package/src/clients/memory-client.md +0 -917
  193. package/src/plugins/cloud-inventory/drivers/mock-drivers.js +0 -449
@@ -40,6 +40,24 @@ import {
40
40
  generateClientId,
41
41
  generateClientSecret
42
42
  } from './oidc-discovery.js';
43
+ import tryFn from '../../concerns/try-fn.js';
44
+ import { verifyPassword } from './concerns/password.js';
45
+ import { PluginError } from '../../errors.js';
46
+
47
+ function constantTimeEqual(a, b) {
48
+ const valueA = Buffer.from(String(a ?? ''), 'utf8');
49
+ const valueB = Buffer.from(String(b ?? ''), 'utf8');
50
+
51
+ if (valueA.length !== valueB.length) {
52
+ return false;
53
+ }
54
+
55
+ let mismatch = 0;
56
+ for (let i = 0; i < valueA.length; i += 1) {
57
+ mismatch |= valueA[i] ^ valueB[i];
58
+ }
59
+ return mismatch === 0;
60
+ }
43
61
 
44
62
  /**
45
63
  * OAuth2/OIDC Authorization Server
@@ -53,7 +71,7 @@ export class OAuth2Server {
53
71
  clientResource,
54
72
  authCodeResource,
55
73
  supportedScopes = ['openid', 'profile', 'email', 'offline_access'],
56
- supportedGrantTypes = ['authorization_code', 'client_credentials', 'refresh_token'],
74
+ supportedGrantTypes = ['authorization_code', 'client_credentials', 'refresh_token', 'password'],
57
75
  supportedResponseTypes = ['code', 'token', 'id_token'],
58
76
  accessTokenExpiry = '15m',
59
77
  idTokenExpiry = '15m',
@@ -62,15 +80,33 @@ export class OAuth2Server {
62
80
  } = options;
63
81
 
64
82
  if (!issuer) {
65
- throw new Error('Issuer URL is required for OAuth2Server');
83
+ throw new PluginError('Issuer URL is required for OAuth2Server', {
84
+ pluginName: 'IdentityPlugin',
85
+ operation: 'OAuth2Server.constructor',
86
+ statusCode: 400,
87
+ retriable: false,
88
+ suggestion: 'Pass { issuer: "https://auth.example.com" } when initializing OAuth2Server.'
89
+ });
66
90
  }
67
91
 
68
92
  if (!keyResource) {
69
- throw new Error('keyResource is required for OAuth2Server');
93
+ throw new PluginError('keyResource is required for OAuth2Server', {
94
+ pluginName: 'IdentityPlugin',
95
+ operation: 'OAuth2Server.constructor',
96
+ statusCode: 400,
97
+ retriable: false,
98
+ suggestion: 'Provide a keyResource (S3DB resource) to store signing keys.'
99
+ });
70
100
  }
71
101
 
72
102
  if (!userResource) {
73
- throw new Error('userResource is required for OAuth2Server');
103
+ throw new PluginError('userResource is required for OAuth2Server', {
104
+ pluginName: 'IdentityPlugin',
105
+ operation: 'OAuth2Server.constructor',
106
+ statusCode: 400,
107
+ retriable: false,
108
+ suggestion: 'Provide a userResource to look up user accounts during token exchange.'
109
+ });
74
110
  }
75
111
 
76
112
  this.issuer = issuer.replace(/\/$/, '');
@@ -87,6 +123,7 @@ export class OAuth2Server {
87
123
  this.authCodeExpiry = authCodeExpiry;
88
124
 
89
125
  this.keyManager = new KeyManager(keyResource);
126
+ this.identityPlugin = null;
90
127
  }
91
128
 
92
129
  /**
@@ -96,6 +133,10 @@ export class OAuth2Server {
96
133
  await this.keyManager.initialize();
97
134
  }
98
135
 
136
+ setIdentityPlugin(identityPlugin) {
137
+ this.identityPlugin = identityPlugin;
138
+ }
139
+
99
140
  /**
100
141
  * OIDC Discovery endpoint handler
101
142
  * GET /.well-known/openid-configuration
@@ -170,27 +211,45 @@ export class OAuth2Server {
170
211
  });
171
212
  }
172
213
 
173
- // Authenticate client if clientResource is provided
214
+ let authenticatedClient = null;
215
+
174
216
  if (this.clientResource) {
175
- const client = await this.authenticateClient(client_id, client_secret);
176
- if (!client) {
217
+ authenticatedClient = await this.authenticateClient(client_id, client_secret);
218
+ if (!authenticatedClient) {
177
219
  return res.status(401).json({
178
220
  error: 'invalid_client',
179
221
  error_description: 'Client authentication failed'
180
222
  });
181
223
  }
224
+ } else if (client_id) {
225
+ authenticatedClient = { clientId: client_id };
182
226
  }
183
227
 
228
+ req.authenticatedClient = authenticatedClient;
229
+
184
230
  // Handle different grant types
185
231
  switch (grant_type) {
186
232
  case 'client_credentials':
187
- return await this.handleClientCredentials(req, res, { client_id, scope });
233
+ return await this.handleClientCredentials(req, res, {
234
+ client: authenticatedClient,
235
+ client_id,
236
+ scope
237
+ });
188
238
 
189
239
  case 'authorization_code':
190
240
  return await this.handleAuthorizationCode(req, res);
191
241
 
192
242
  case 'refresh_token':
193
- return await this.handleRefreshToken(req, res);
243
+ return await this.handleRefreshToken(req, res, {
244
+ client: authenticatedClient,
245
+ scope
246
+ });
247
+
248
+ case 'password':
249
+ return await this.handlePasswordGrant(req, res, {
250
+ client: authenticatedClient,
251
+ scope
252
+ });
194
253
 
195
254
  default:
196
255
  return res.status(400).json({
@@ -209,7 +268,30 @@ export class OAuth2Server {
209
268
  /**
210
269
  * Client Credentials flow handler
211
270
  */
212
- async handleClientCredentials(req, res, { client_id, scope }) {
271
+ async handleClientCredentials(req, res, { client, client_id, scope } = {}) {
272
+ const resolvedClient = client || (client_id ? { clientId: client_id } : null);
273
+ const resolvedClientId = resolvedClient?.clientId || client_id;
274
+
275
+ if (!resolvedClientId) {
276
+ return res.status(400).json({
277
+ error: 'invalid_request',
278
+ error_description: 'client_id is required'
279
+ });
280
+ }
281
+
282
+ const allowedGrantTypes = Array.isArray(resolvedClient?.grantTypes)
283
+ ? resolvedClient.grantTypes
284
+ : Array.isArray(resolvedClient?.allowedGrantTypes)
285
+ ? resolvedClient.allowedGrantTypes
286
+ : null;
287
+
288
+ if (allowedGrantTypes && allowedGrantTypes.length > 0 && !allowedGrantTypes.includes('client_credentials')) {
289
+ return res.status(400).json({
290
+ error: 'unauthorized_client',
291
+ error_description: 'Client is not allowed to use client_credentials grant'
292
+ });
293
+ }
294
+
213
295
  const scopes = parseScopes(scope);
214
296
 
215
297
  // Validate scopes
@@ -221,10 +303,20 @@ export class OAuth2Server {
221
303
  });
222
304
  }
223
305
 
306
+ if (Array.isArray(resolvedClient?.allowedScopes) && resolvedClient.allowedScopes.length > 0) {
307
+ const disallowedScopes = scopeValidation.scopes.filter(scopeValue => !resolvedClient.allowedScopes.includes(scopeValue));
308
+ if (disallowedScopes.length > 0) {
309
+ return res.status(400).json({
310
+ error: 'invalid_scope',
311
+ error_description: `Client is not allowed to request scopes: ${disallowedScopes.join(', ')}`
312
+ });
313
+ }
314
+ }
315
+
224
316
  // Create access token
225
317
  const accessToken = this.keyManager.createToken({
226
318
  iss: this.issuer,
227
- sub: client_id,
319
+ sub: resolvedClientId,
228
320
  aud: this.issuer,
229
321
  scope: scopeValidation.scopes.join(' '),
230
322
  token_type: 'access_token'
@@ -271,8 +363,16 @@ export class OAuth2Server {
271
363
  const authCode = authCodes[0];
272
364
 
273
365
  // Validate code expiration
274
- const now = Math.floor(Date.now() / 1000);
275
- if (authCode.expiresAt < now) {
366
+ const expiresAtMs = this.parseAuthCodeExpiry(authCode.expiresAt);
367
+ if (!Number.isFinite(expiresAtMs)) {
368
+ await this.authCodeResource.remove(authCode.id);
369
+ return res.status(400).json({
370
+ error: 'invalid_grant',
371
+ error_description: 'Authorization code is invalid'
372
+ });
373
+ }
374
+
375
+ if (expiresAtMs <= Date.now()) {
276
376
  await this.authCodeResource.remove(authCode.id);
277
377
  return res.status(400).json({
278
378
  error: 'invalid_grant',
@@ -372,11 +472,148 @@ export class OAuth2Server {
372
472
  return res.status(200).json(response);
373
473
  }
374
474
 
475
+ async handlePasswordGrant(req, res, context = {}) {
476
+ if (!this.identityPlugin || typeof this.identityPlugin.authenticateWithPassword !== 'function') {
477
+ return res.status(400).json({
478
+ error: 'unsupported_grant_type',
479
+ error_description: 'Password grant is not configured'
480
+ });
481
+ }
482
+
483
+ const driver = this.identityPlugin.getAuthDriver?.('password');
484
+ if (!driver || (typeof driver.supportsGrant === 'function' && !driver.supportsGrant('password'))) {
485
+ return res.status(400).json({
486
+ error: 'unsupported_grant_type',
487
+ error_description: 'Password grant is not available'
488
+ });
489
+ }
490
+
491
+ const client = context.client ?? req.authenticatedClient ?? null;
492
+
493
+ if (client) {
494
+ if (client.active === false) {
495
+ return res.status(400).json({
496
+ error: 'unauthorized_client',
497
+ error_description: 'Client is inactive'
498
+ });
499
+ }
500
+
501
+ const allowedGrantTypes = Array.isArray(client.grantTypes)
502
+ ? client.grantTypes
503
+ : Array.isArray(client.allowedGrantTypes)
504
+ ? client.allowedGrantTypes
505
+ : null;
506
+
507
+ if (allowedGrantTypes && allowedGrantTypes.length > 0 && !allowedGrantTypes.includes('password')) {
508
+ return res.status(400).json({
509
+ error: 'unauthorized_client',
510
+ error_description: 'Client is not allowed to use password grant'
511
+ });
512
+ }
513
+ }
514
+
515
+ const { username, password } = req.body;
516
+ if (!username || !password) {
517
+ return res.status(400).json({
518
+ error: 'invalid_request',
519
+ error_description: 'username and password are required'
520
+ });
521
+ }
522
+
523
+ const authResult = await this.identityPlugin.authenticateWithPassword({
524
+ email: username,
525
+ password
526
+ });
527
+
528
+ if (!authResult?.success || !authResult.user) {
529
+ return res.status(authResult?.statusCode || 400).json({
530
+ error: authResult?.error || 'invalid_grant',
531
+ error_description: 'Invalid credentials'
532
+ });
533
+ }
534
+
535
+ const user = authResult.user;
536
+ if (user.active !== undefined && user.active === false) {
537
+ return res.status(400).json({
538
+ error: 'invalid_grant',
539
+ error_description: 'User account is inactive'
540
+ });
541
+ }
542
+
543
+ const requestedScope = context.scope ?? req.body.scope;
544
+ const scopes = parseScopes(requestedScope);
545
+ const scopeValidation = validateScopes(scopes, this.supportedScopes);
546
+ if (!scopeValidation.valid) {
547
+ return res.status(400).json({
548
+ error: 'invalid_scope',
549
+ error_description: scopeValidation.error
550
+ });
551
+ }
552
+
553
+ if (client && Array.isArray(client.allowedScopes) && client.allowedScopes.length > 0) {
554
+ const disallowedScopes = scopeValidation.scopes.filter(scopeValue => !client.allowedScopes.includes(scopeValue));
555
+ if (disallowedScopes.length > 0) {
556
+ return res.status(400).json({
557
+ error: 'invalid_scope',
558
+ error_description: `Client is not allowed to request scopes: ${disallowedScopes.join(', ')}`
559
+ });
560
+ }
561
+ }
562
+
563
+ const resolvedAudience = client?.clientId || req.body.client_id || this.issuer;
564
+
565
+ const accessToken = this.keyManager.createToken({
566
+ iss: this.issuer,
567
+ sub: user.id,
568
+ aud: resolvedAudience,
569
+ scope: scopeValidation.scopes.join(' '),
570
+ token_type: 'access_token'
571
+ }, this.accessTokenExpiry);
572
+
573
+ const response = {
574
+ access_token: accessToken,
575
+ token_type: 'Bearer',
576
+ expires_in: this.parseExpiryToSeconds(this.accessTokenExpiry),
577
+ scope: scopeValidation.scopes.join(' ')
578
+ };
579
+
580
+ const allowRefreshToken = scopeValidation.scopes.includes('offline_access') &&
581
+ this.supportedGrantTypes.includes('refresh_token');
582
+
583
+ if (allowRefreshToken) {
584
+ const refreshToken = this.keyManager.createToken({
585
+ iss: this.issuer,
586
+ sub: user.id,
587
+ aud: this.issuer,
588
+ scope: scopeValidation.scopes.join(' '),
589
+ token_type: 'refresh_token'
590
+ }, this.refreshTokenExpiry);
591
+
592
+ response.refresh_token = refreshToken;
593
+ }
594
+
595
+ if (scopeValidation.scopes.includes('openid')) {
596
+ const userClaims = extractUserClaims(user, scopeValidation.scopes);
597
+
598
+ const idToken = this.keyManager.createToken({
599
+ iss: this.issuer,
600
+ sub: user.id,
601
+ aud: resolvedAudience,
602
+ ...userClaims
603
+ }, this.idTokenExpiry);
604
+
605
+ response.id_token = idToken;
606
+ }
607
+
608
+ return res.status(200).json(response);
609
+ }
610
+
375
611
  /**
376
612
  * Refresh Token flow handler
377
613
  */
378
- async handleRefreshToken(req, res) {
614
+ async handleRefreshToken(req, res, context = {}) {
379
615
  const { refresh_token, scope } = req.body;
616
+ const client = context.client ?? req.authenticatedClient ?? null;
380
617
 
381
618
  if (!refresh_token) {
382
619
  return res.status(400).json({
@@ -405,6 +642,33 @@ export class OAuth2Server {
405
642
  });
406
643
  }
407
644
 
645
+ if (client?.clientId && payload.aud && payload.aud !== client.clientId) {
646
+ return res.status(400).json({
647
+ error: 'invalid_grant',
648
+ error_description: 'Refresh token does not belong to this client'
649
+ });
650
+ }
651
+
652
+ if (client?.active === false) {
653
+ return res.status(400).json({
654
+ error: 'unauthorized_client',
655
+ error_description: 'Client is inactive'
656
+ });
657
+ }
658
+
659
+ const allowedGrantTypes = Array.isArray(client?.grantTypes)
660
+ ? client.grantTypes
661
+ : Array.isArray(client?.allowedGrantTypes)
662
+ ? client.allowedGrantTypes
663
+ : null;
664
+
665
+ if (allowedGrantTypes && allowedGrantTypes.length > 0 && !allowedGrantTypes.includes('refresh_token')) {
666
+ return res.status(400).json({
667
+ error: 'unauthorized_client',
668
+ error_description: 'Client is not allowed to use refresh_token grant'
669
+ });
670
+ }
671
+
408
672
  // Validate claims
409
673
  const claimValidation = validateClaims(payload, {
410
674
  issuer: this.issuer,
@@ -419,7 +683,8 @@ export class OAuth2Server {
419
683
  }
420
684
 
421
685
  // Parse scopes (use original scopes if not provided)
422
- const requestedScopes = scope ? parseScopes(scope) : parseScopes(payload.scope);
686
+ const overrideScope = context.scope ?? scope;
687
+ const requestedScopes = overrideScope ? parseScopes(overrideScope) : parseScopes(payload.scope);
423
688
  const originalScopes = parseScopes(payload.scope);
424
689
 
425
690
  // Requested scopes must be subset of original scopes
@@ -431,6 +696,16 @@ export class OAuth2Server {
431
696
  });
432
697
  }
433
698
 
699
+ if (client && Array.isArray(client.allowedScopes) && client.allowedScopes.length > 0) {
700
+ const disallowedScopes = requestedScopes.filter(scopeValue => !client.allowedScopes.includes(scopeValue));
701
+ if (disallowedScopes.length > 0) {
702
+ return res.status(400).json({
703
+ error: 'invalid_scope',
704
+ error_description: `Client is not allowed to request scopes: ${disallowedScopes.join(', ')}`
705
+ });
706
+ }
707
+ }
708
+
434
709
  // Get user
435
710
  const user = await this.userResource.get(payload.sub);
436
711
  if (!user) {
@@ -452,7 +727,8 @@ export class OAuth2Server {
452
727
  const response = {
453
728
  access_token: accessToken,
454
729
  token_type: 'Bearer',
455
- expires_in: this.parseExpiryToSeconds(this.accessTokenExpiry)
730
+ expires_in: this.parseExpiryToSeconds(this.accessTokenExpiry),
731
+ scope: requestedScopes.join(' ')
456
732
  };
457
733
 
458
734
  // Create new ID token if openid scope requested
@@ -601,24 +877,96 @@ export class OAuth2Server {
601
877
  return null;
602
878
  }
603
879
 
604
- try {
605
- const clients = await this.clientResource.query({ clientId });
880
+ const driver = this.identityPlugin?.getAuthDriver?.('client_credentials');
881
+ if (driver) {
882
+ const authResult = await driver.authenticate({ clientId, clientSecret });
883
+ if (authResult?.success) {
884
+ return authResult.client || { clientId };
885
+ }
886
+ return null;
887
+ }
606
888
 
607
- if (clients.length === 0) {
608
- return null;
889
+ const [ok, err, clients] = await tryFn(() => this.clientResource.query({ clientId }));
890
+ if (!ok) {
891
+ if (err && this.identityPlugin?.config?.verbose) {
892
+ console.error('[Identity Plugin] Failed to query clients resource:', err.message);
609
893
  }
894
+ return null;
895
+ }
896
+
897
+ if (!clients || clients.length === 0) {
898
+ return null;
899
+ }
900
+
901
+ const client = clients[0];
610
902
 
611
- const client = clients[0];
903
+ if (client.active === false) {
904
+ return null;
905
+ }
906
+
907
+ const secrets = [];
908
+ if (Array.isArray(client.secrets)) {
909
+ secrets.push(...client.secrets);
910
+ }
911
+ if (client.clientSecret) {
912
+ secrets.push(client.clientSecret);
913
+ }
914
+ if (client.secret) {
915
+ secrets.push(client.secret);
916
+ }
612
917
 
613
- // Verify client secret
614
- if (client.clientSecret !== clientSecret) {
615
- return null;
918
+ if (!secrets.length) {
919
+ return null;
920
+ }
921
+
922
+ let secretMatches = false;
923
+ for (const storedSecret of secrets) {
924
+ if (!storedSecret) continue;
925
+
926
+ if (this._isHashedSecret(storedSecret)) {
927
+ try {
928
+ const okHash = await verifyPassword(clientSecret, storedSecret);
929
+ if (okHash) {
930
+ secretMatches = true;
931
+ break;
932
+ }
933
+ } catch (error) {
934
+ if (this.identityPlugin?.config?.verbose) {
935
+ console.error('[Identity Plugin] Failed to verify client secret hash:', error.message);
936
+ }
937
+ }
938
+ continue;
616
939
  }
617
940
 
618
- return client;
619
- } catch (error) {
941
+ if (constantTimeEqual(clientSecret, storedSecret)) {
942
+ secretMatches = true;
943
+ break;
944
+ }
945
+ }
946
+
947
+ if (!secretMatches) {
620
948
  return null;
621
949
  }
950
+
951
+ const sanitizedClient = { ...client };
952
+ if (sanitizedClient.clientSecret !== undefined) {
953
+ delete sanitizedClient.clientSecret;
954
+ }
955
+ if (sanitizedClient.secret !== undefined) {
956
+ delete sanitizedClient.secret;
957
+ }
958
+ if (sanitizedClient.secrets !== undefined) {
959
+ delete sanitizedClient.secrets;
960
+ }
961
+
962
+ return sanitizedClient;
963
+ }
964
+
965
+ _isHashedSecret(value) {
966
+ if (typeof value !== 'string') {
967
+ return false;
968
+ }
969
+ return value.startsWith('$') || value.startsWith('s3db$');
622
970
  }
623
971
 
624
972
  /**
@@ -646,7 +994,13 @@ export class OAuth2Server {
646
994
  parseExpiryToSeconds(expiresIn) {
647
995
  const match = expiresIn.match(/^(\d+)([smhd])$/);
648
996
  if (!match) {
649
- throw new Error('Invalid expiresIn format');
997
+ throw new PluginError('Invalid expiresIn format', {
998
+ pluginName: 'IdentityPlugin',
999
+ operation: 'parseExpiryToSeconds',
1000
+ statusCode: 400,
1001
+ retriable: false,
1002
+ suggestion: 'Use a duration string such as "15m", "24h", or "30s".'
1003
+ });
650
1004
  }
651
1005
 
652
1006
  const [, value, unit] = match;
@@ -812,9 +1166,19 @@ export class OAuth2Server {
812
1166
 
813
1167
  const user = users[0];
814
1168
 
815
- // Verify password (assuming password is hashed with bcrypt or similar)
816
- // In production, use proper password verification
817
- if (user.password !== password) {
1169
+ const [okVerify, errVerify, isValid] = await tryFn(() =>
1170
+ verifyPassword(password, user.password)
1171
+ );
1172
+
1173
+ if (!okVerify) {
1174
+ console.error('[OAuth2Server] Password verification error:', errVerify);
1175
+ return res.status(500).json({
1176
+ error: 'server_error',
1177
+ error_description: 'Authentication failed'
1178
+ });
1179
+ }
1180
+
1181
+ if (!isValid) {
818
1182
  return res.status(401).json({
819
1183
  error: 'access_denied',
820
1184
  error_description: 'Invalid credentials'
@@ -1028,6 +1392,25 @@ export class OAuth2Server {
1028
1392
  async rotateKeys() {
1029
1393
  return await this.keyManager.rotateKey();
1030
1394
  }
1395
+
1396
+ /**
1397
+ * Normalize authorization code expiry representation
1398
+ * @param {string|number} value
1399
+ * @returns {number} Expiry timestamp in milliseconds or NaN
1400
+ */
1401
+ parseAuthCodeExpiry(value) {
1402
+ if (typeof value === 'number') {
1403
+ // If stored as seconds, convert to milliseconds
1404
+ return value > 1e12 ? value : value * 1000;
1405
+ }
1406
+
1407
+ if (typeof value === 'string') {
1408
+ const parsed = Date.parse(value);
1409
+ return Number.isNaN(parsed) ? NaN : parsed;
1410
+ }
1411
+
1412
+ return NaN;
1413
+ }
1031
1414
  }
1032
1415
 
1033
1416
  export default OAuth2Server;
@@ -5,6 +5,8 @@
5
5
  * Implements OpenID Connect Discovery 1.0 specification
6
6
  */
7
7
 
8
+ import { PluginError } from '../../errors.js';
9
+
8
10
  /**
9
11
  * Generate OpenID Connect Discovery Document
10
12
  * @param {Object} options - Configuration options
@@ -23,7 +25,13 @@ export function generateDiscoveryDocument(options = {}) {
23
25
  } = options;
24
26
 
25
27
  if (!issuer) {
26
- throw new Error('Issuer URL is required for OIDC discovery');
28
+ throw new PluginError('Issuer URL is required for OIDC discovery', {
29
+ pluginName: 'IdentityPlugin',
30
+ operation: 'generateDiscoveryDocument',
31
+ statusCode: 400,
32
+ retriable: false,
33
+ suggestion: 'Provide options.issuer when generating the discovery document.'
34
+ });
27
35
  }
28
36
 
29
37
  // Remove trailing slash from issuer
@@ -31,14 +39,14 @@ export function generateDiscoveryDocument(options = {}) {
31
39
 
32
40
  return {
33
41
  issuer: baseUrl,
34
- authorization_endpoint: `${baseUrl}/auth/authorize`,
35
- token_endpoint: `${baseUrl}/auth/token`,
36
- userinfo_endpoint: `${baseUrl}/auth/userinfo`,
42
+ authorization_endpoint: `${baseUrl}/oauth/authorize`,
43
+ token_endpoint: `${baseUrl}/oauth/token`,
44
+ userinfo_endpoint: `${baseUrl}/oauth/userinfo`,
37
45
  jwks_uri: `${baseUrl}/.well-known/jwks.json`,
38
- registration_endpoint: `${baseUrl}/auth/register`,
39
- introspection_endpoint: `${baseUrl}/auth/introspect`,
40
- revocation_endpoint: `${baseUrl}/auth/revoke`,
41
- end_session_endpoint: `${baseUrl}/auth/logout`,
46
+ registration_endpoint: `${baseUrl}/oauth/register`,
47
+ introspection_endpoint: `${baseUrl}/oauth/introspect`,
48
+ revocation_endpoint: `${baseUrl}/oauth/revoke`,
49
+ end_session_endpoint: `${baseUrl}/logout`,
42
50
 
43
51
  // Supported features
44
52
  scopes_supported: scopes,