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
@@ -5,7 +5,8 @@
5
5
  * Zero external dependencies - uses Node.js crypto only
6
6
  */
7
7
 
8
- import { generateKeyPairSync, createSign, createVerify, createHash } from 'crypto';
8
+ import { generateKeyPairSync, createSign, createVerify, createHash, createPublicKey } from 'crypto';
9
+ import { PluginError } from '../../errors.js';
9
10
 
10
11
  /**
11
12
  * Generate RSA key pair for RS256
@@ -74,7 +75,13 @@ export function createRS256Token(payload, privateKey, kid, expiresIn = '15m') {
74
75
  // Parse expiresIn
75
76
  const match = expiresIn.match(/^(\d+)([smhd])$/);
76
77
  if (!match) {
77
- throw new Error('Invalid expiresIn format. Use: 60s, 30m, 24h, 7d');
78
+ throw new PluginError('Invalid expiresIn format. Use: 60s, 30m, 24h, 7d', {
79
+ pluginName: 'IdentityPlugin',
80
+ operation: 'createToken',
81
+ statusCode: 400,
82
+ retriable: false,
83
+ suggestion: 'Provide a duration string ending with s, m, h, or d (e.g., "15m" for 15 minutes).'
84
+ });
78
85
  }
79
86
 
80
87
  const [, value, unit] = match;
@@ -174,19 +181,15 @@ export function getKidFromToken(token) {
174
181
  }
175
182
  }
176
183
 
177
- /**
178
- * Import createPublicKey for JWK conversion
179
- */
180
- import { createPublicKey } from 'crypto';
181
-
182
184
  /**
183
185
  * Key Manager class - manages key rotation and storage
184
186
  */
185
187
  export class KeyManager {
186
188
  constructor(keyResource) {
187
189
  this.keyResource = keyResource;
188
- this.currentKey = null;
189
- this.keys = new Map(); // kid key
190
+ this.keysByPurpose = new Map(); // purpose -> Map(kid -> key)
191
+ this.currentKeys = new Map(); // purpose -> active key
192
+ this.keysByKid = new Map(); // kid -> key
190
193
  }
191
194
 
192
195
  /**
@@ -199,36 +202,34 @@ export class KeyManager {
199
202
  if (existingKeys.length > 0) {
200
203
  // Load keys into memory
201
204
  for (const keyRecord of existingKeys) {
202
- this.keys.set(keyRecord.kid, {
203
- publicKey: keyRecord.publicKey,
204
- privateKey: keyRecord.privateKey,
205
- kid: keyRecord.kid,
206
- createdAt: keyRecord.createdAt,
207
- active: keyRecord.active
205
+ this._storeKeyRecord({
206
+ ...keyRecord,
207
+ purpose: keyRecord.purpose || 'oauth'
208
208
  });
209
-
210
- if (keyRecord.active) {
211
- this.currentKey = keyRecord;
212
- }
213
209
  }
214
210
  }
215
211
 
216
212
  // If no active key, generate one
217
- if (!this.currentKey) {
218
- await this.rotateKey();
213
+ if (!this.currentKeys.get('oauth')) {
214
+ await this.rotateKey('oauth');
219
215
  }
220
216
  }
221
217
 
222
218
  /**
223
219
  * Rotate keys - generate new key pair
224
220
  */
225
- async rotateKey() {
221
+ async rotateKey(purpose = 'oauth') {
222
+ const normalizedPurpose = this._normalizePurpose(purpose);
226
223
  const keyPair = generateKeyPair();
227
224
 
228
225
  // Mark old keys as inactive
229
- const oldKeys = await this.keyResource.query({ active: true });
226
+ const oldKeys = await this.keyResource.query({ active: true, purpose: normalizedPurpose });
230
227
  for (const oldKey of oldKeys) {
231
228
  await this.keyResource.update(oldKey.id, { active: false });
229
+ const stored = this.keysByKid.get(oldKey.kid);
230
+ if (stored) {
231
+ stored.active = false;
232
+ }
232
233
  }
233
234
 
234
235
  // Store new key
@@ -239,11 +240,11 @@ export class KeyManager {
239
240
  algorithm: keyPair.algorithm,
240
241
  use: keyPair.use,
241
242
  active: true,
242
- createdAt: keyPair.createdAt
243
+ createdAt: keyPair.createdAt,
244
+ purpose: normalizedPurpose
243
245
  });
244
246
 
245
- this.currentKey = keyRecord;
246
- this.keys.set(keyRecord.kid, keyRecord);
247
+ this._storeKeyRecord(keyRecord);
247
248
 
248
249
  return keyRecord;
249
250
  }
@@ -251,22 +252,50 @@ export class KeyManager {
251
252
  /**
252
253
  * Get current active key
253
254
  */
254
- getCurrentKey() {
255
- return this.currentKey;
255
+ getCurrentKey(purpose = 'oauth') {
256
+ return this.currentKeys.get(this._normalizePurpose(purpose)) || null;
256
257
  }
257
258
 
258
259
  /**
259
260
  * Get key by kid
260
261
  */
261
262
  getKey(kid) {
262
- return this.keys.get(kid);
263
+ return this.keysByKid.get(kid) || null;
264
+ }
265
+
266
+ /**
267
+ * Ensure a purpose has an active key
268
+ * @param {string} purpose
269
+ * @returns {Promise<Object>}
270
+ */
271
+ async ensurePurpose(purpose = 'oauth') {
272
+ const normalizedPurpose = this._normalizePurpose(purpose);
273
+ const current = this.currentKeys.get(normalizedPurpose);
274
+
275
+ if (current) {
276
+ return current;
277
+ }
278
+
279
+ // Try to find active key in storage
280
+ const [active] = await this.keyResource.query({ purpose: normalizedPurpose, active: true });
281
+ if (active) {
282
+ this._storeKeyRecord({
283
+ ...active,
284
+ purpose: active.purpose || normalizedPurpose
285
+ });
286
+ return this.currentKeys.get(normalizedPurpose);
287
+ }
288
+
289
+ return await this.rotateKey(normalizedPurpose);
263
290
  }
264
291
 
265
292
  /**
266
293
  * Get all public keys in JWKS format
267
294
  */
268
295
  async getJWKS() {
269
- const keys = Array.from(this.keys.values()).map(key => ({
296
+ const keys = Array.from(this.keysByKid.values())
297
+ .filter(key => key.active)
298
+ .map(key => ({
270
299
  kty: 'RSA',
271
300
  use: 'sig',
272
301
  alg: 'RS256',
@@ -280,15 +309,25 @@ export class KeyManager {
280
309
  /**
281
310
  * Create JWT with current active key
282
311
  */
283
- createToken(payload, expiresIn = '15m') {
284
- if (!this.currentKey) {
285
- throw new Error('No active key available');
312
+ createToken(payload, expiresIn = '15m', purpose = 'oauth') {
313
+ const normalizedPurpose = this._normalizePurpose(purpose);
314
+ const activeKey = this.currentKeys.get(normalizedPurpose);
315
+
316
+ if (!activeKey) {
317
+ throw new PluginError(`No active key available for purpose "${normalizedPurpose}"`, {
318
+ pluginName: 'IdentityPlugin',
319
+ operation: 'createToken',
320
+ statusCode: 503,
321
+ retriable: true,
322
+ suggestion: 'Generate or rotate keys before issuing tokens for this purpose.',
323
+ metadata: { purpose: normalizedPurpose }
324
+ });
286
325
  }
287
326
 
288
327
  return createRS256Token(
289
328
  payload,
290
- this.currentKey.privateKey,
291
- this.currentKey.kid,
329
+ activeKey.privateKey,
330
+ activeKey.kid,
292
331
  expiresIn
293
332
  );
294
333
  }
@@ -311,6 +350,47 @@ export class KeyManager {
311
350
 
312
351
  return verifyRS256Token(token, key.publicKey);
313
352
  }
353
+
354
+ /**
355
+ * Normalize purpose name
356
+ * @param {string} purpose
357
+ * @returns {string}
358
+ * @private
359
+ */
360
+ _normalizePurpose(purpose) {
361
+ return typeof purpose === 'string' && purpose.trim().length > 0
362
+ ? purpose.trim()
363
+ : 'oauth';
364
+ }
365
+
366
+ /**
367
+ * Store key record in caches
368
+ * @param {Object} record
369
+ * @private
370
+ */
371
+ _storeKeyRecord(record) {
372
+ const purpose = this._normalizePurpose(record.purpose);
373
+ const entry = {
374
+ publicKey: record.publicKey,
375
+ privateKey: record.privateKey,
376
+ kid: record.kid,
377
+ createdAt: record.createdAt,
378
+ active: record.active,
379
+ purpose,
380
+ id: record.id
381
+ };
382
+
383
+ if (!this.keysByPurpose.has(purpose)) {
384
+ this.keysByPurpose.set(purpose, new Map());
385
+ }
386
+
387
+ this.keysByPurpose.get(purpose).set(entry.kid, entry);
388
+ this.keysByKid.set(entry.kid, entry);
389
+
390
+ if (entry.active) {
391
+ this.currentKeys.set(purpose, entry);
392
+ }
393
+ }
314
394
  }
315
395
 
316
396
  export default {
@@ -12,6 +12,7 @@ import {
12
12
  createLoggingMiddleware
13
13
  } from '../shared/middlewares/index.js';
14
14
  import { idGenerator } from '../../concerns/id.js';
15
+ import { createJsonRateLimitMiddleware } from './concerns/rate-limit.js';
15
16
 
16
17
  /**
17
18
  * Create Express-style response adapter for Hono context
@@ -22,15 +23,123 @@ import { idGenerator } from '../../concerns/id.js';
22
23
  function createExpressStyleResponse(c) {
23
24
  let statusCode = 200;
24
25
 
25
- return {
26
+ const response = {
26
27
  status(code) {
27
28
  statusCode = code;
28
29
  return this;
29
30
  },
30
31
  json(data) {
31
32
  return c.json(data, statusCode);
33
+ },
34
+ header(name, value) {
35
+ c.header(name, value);
36
+ return this;
37
+ },
38
+ setHeader(name, value) {
39
+ c.header(name, value);
40
+ return this;
41
+ },
42
+ send(data) {
43
+ if (data === undefined || data === null) {
44
+ return c.body('', statusCode);
45
+ }
46
+
47
+ if (typeof data === 'string' || data instanceof Uint8Array) {
48
+ return c.body(data, statusCode);
49
+ }
50
+
51
+ // Fallback to JSON serialization for objects
52
+ return c.json(data, statusCode);
53
+ },
54
+ redirect(url, code = 302) {
55
+ return c.redirect(url, code);
56
+ }
57
+ };
58
+
59
+ return response;
60
+ }
61
+
62
+ /**
63
+ * Parse cookies from request header
64
+ * @param {string} cookieHeader
65
+ * @returns {Object}
66
+ */
67
+ function parseCookies(cookieHeader) {
68
+ if (!cookieHeader) {
69
+ return {};
70
+ }
71
+
72
+ return cookieHeader
73
+ .split(';')
74
+ .map(part => part.trim())
75
+ .filter(Boolean)
76
+ .reduce((acc, part) => {
77
+ const [key, ...rest] = part.split('=');
78
+ acc[key] = decodeURIComponent(rest.join('=') || '');
79
+ return acc;
80
+ }, {});
81
+ }
82
+
83
+ /**
84
+ * Create Express-style request adapter for Hono context
85
+ * @param {Object} c - Hono context
86
+ * @returns {Promise<Object>} Express-style request object
87
+ */
88
+ async function createExpressStyleRequest(c) {
89
+ const cached = c.get('expressStyleRequest');
90
+ if (cached) {
91
+ return cached;
92
+ }
93
+
94
+ const raw = c.req.raw;
95
+ const headers = {};
96
+ raw.headers.forEach((value, key) => {
97
+ headers[key.toLowerCase()] = value;
98
+ });
99
+
100
+ const url = new URL(raw.url);
101
+ let body = undefined;
102
+ const contentType = headers['content-type']?.split(';')[0].trim();
103
+
104
+ try {
105
+ if (contentType === 'application/json') {
106
+ body = await c.req.json();
107
+ } else if (
108
+ contentType === 'application/x-www-form-urlencoded' ||
109
+ contentType === 'multipart/form-data'
110
+ ) {
111
+ body = await c.req.parseBody();
112
+ }
113
+ } catch {
114
+ body = undefined;
115
+ }
116
+
117
+ const query = Object.fromEntries(url.searchParams.entries());
118
+ const cookies = parseCookies(headers.cookie);
119
+ const clientIp =
120
+ c.get('clientIp') ||
121
+ c.req.header('x-forwarded-for')?.split(',')[0]?.trim() ||
122
+ c.req.header('x-real-ip') ||
123
+ 'unknown';
124
+
125
+ const expressReq = {
126
+ method: raw.method,
127
+ url: raw.url,
128
+ originalUrl: raw.url,
129
+ path: url.pathname,
130
+ headers,
131
+ query,
132
+ body: body ?? {},
133
+ cookies,
134
+ ip: clientIp,
135
+ protocol: url.protocol.replace(':', ''),
136
+ get(name) {
137
+ return headers[name.toLowerCase()];
32
138
  }
33
139
  };
140
+
141
+ c.set('expressStyleRequest', expressReq);
142
+ return expressReq;
34
143
  }
35
144
 
36
145
  /**
@@ -75,10 +184,7 @@ export class IdentityServer {
75
184
  // Global ban check middleware
76
185
  this.app.use('*', async (c, next) => {
77
186
  // Extract IP address
78
- const ip = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() ||
79
- c.req.header('x-real-ip') ||
80
- c.env?.ip ||
81
- 'unknown';
187
+ const ip = this._extractClientIp(c);
82
188
 
83
189
  // Store IP in context for later use
84
190
  c.set('clientIp', ip);
@@ -156,6 +262,30 @@ export class IdentityServer {
156
262
  }
157
263
  }
158
264
 
265
+ /**
266
+ * Extract client IP from request
267
+ * @param {import('hono').Context} c
268
+ * @returns {string}
269
+ * @private
270
+ */
271
+ _extractClientIp(c) {
272
+ return c.get('clientIp') ||
273
+ c.req.header('x-forwarded-for')?.split(',')[0]?.trim() ||
274
+ c.req.header('x-real-ip') ||
275
+ c.env?.ip ||
276
+ 'unknown';
277
+ }
278
+
279
+ /**
280
+ * Create rate limit middleware for API endpoints
281
+ * @param {RateLimiter} limiter
282
+ * @returns {Function}
283
+ * @private
284
+ */
285
+ _createRateLimitMiddleware(limiter) {
286
+ return createJsonRateLimitMiddleware(limiter, (c) => this._extractClientIp(c));
287
+ }
288
+
159
289
  /**
160
290
  * Setup all routes
161
291
  * @private
@@ -270,59 +400,50 @@ export class IdentityServer {
270
400
  return;
271
401
  }
272
402
 
273
- // OIDC Discovery endpoint
274
- this.app.get('/.well-known/openid-configuration', async (c) => {
403
+ const rateLimiters = this.options.identityPlugin?.rateLimiters || {};
404
+ const wrap = (handler) => async (c) => {
405
+ const req = await createExpressStyleRequest(c);
275
406
  const res = createExpressStyleResponse(c);
276
- return await oauth2Server.discoveryHandler(c.req, res);
277
- });
407
+ return await handler.call(oauth2Server, req, res);
408
+ };
409
+
410
+ // OIDC Discovery endpoint
411
+ this.app.get('/.well-known/openid-configuration', wrap(oauth2Server.discoveryHandler));
278
412
 
279
413
  // JWKS (JSON Web Key Set) endpoint
280
- this.app.get('/.well-known/jwks.json', async (c) => {
281
- const res = createExpressStyleResponse(c);
282
- return await oauth2Server.jwksHandler(c.req, res);
283
- });
414
+ this.app.get('/.well-known/jwks.json', wrap(oauth2Server.jwksHandler));
284
415
 
285
416
  // OAuth2 Token endpoint
286
- this.app.post('/oauth/token', async (c) => {
287
- const res = createExpressStyleResponse(c);
288
- return await oauth2Server.tokenHandler(c.req, res);
289
- });
417
+ const tokenHandler = wrap(oauth2Server.tokenHandler);
418
+ if (rateLimiters.token) {
419
+ this.app.post('/oauth/token', this._createRateLimitMiddleware(rateLimiters.token), tokenHandler);
420
+ } else {
421
+ this.app.post('/oauth/token', tokenHandler);
422
+ }
290
423
 
291
424
  // OIDC UserInfo endpoint
292
- this.app.get('/oauth/userinfo', async (c) => {
293
- const res = createExpressStyleResponse(c);
294
- return await oauth2Server.userinfoHandler(c.req, res);
295
- });
425
+ this.app.get('/oauth/userinfo', wrap(oauth2Server.userinfoHandler));
296
426
 
297
427
  // Token introspection endpoint
298
- this.app.post('/oauth/introspect', async (c) => {
299
- const res = createExpressStyleResponse(c);
300
- return await oauth2Server.introspectHandler(c.req, res);
301
- });
302
-
303
- // Authorization endpoint (GET for user consent UI)
304
- this.app.get('/oauth/authorize', async (c) => {
305
- const res = createExpressStyleResponse(c);
306
- return await oauth2Server.authorizeHandler(c.req, res);
307
- });
308
-
309
- // Authorization endpoint (POST for processing login)
310
- this.app.post('/oauth/authorize', async (c) => {
311
- const res = createExpressStyleResponse(c);
312
- return await oauth2Server.authorizePostHandler(c.req, res);
313
- });
428
+ this.app.post('/oauth/introspect', wrap(oauth2Server.introspectHandler));
429
+
430
+ // Authorization endpoints
431
+ const authorizeGet = wrap(oauth2Server.authorizeHandler);
432
+ const authorizePost = wrap(oauth2Server.authorizePostHandler);
433
+ if (rateLimiters.authorize) {
434
+ const middleware = this._createRateLimitMiddleware(rateLimiters.authorize);
435
+ this.app.get('/oauth/authorize', middleware, authorizeGet);
436
+ this.app.post('/oauth/authorize', middleware, authorizePost);
437
+ } else {
438
+ this.app.get('/oauth/authorize', authorizeGet);
439
+ this.app.post('/oauth/authorize', authorizePost);
440
+ }
314
441
 
315
442
  // Client registration endpoint
316
- this.app.post('/oauth/register', async (c) => {
317
- const res = createExpressStyleResponse(c);
318
- return await oauth2Server.registerClientHandler(c.req, res);
319
- });
443
+ this.app.post('/oauth/register', wrap(oauth2Server.registerClientHandler));
320
444
 
321
445
  // Token revocation endpoint
322
- this.app.post('/oauth/revoke', async (c) => {
323
- const res = createExpressStyleResponse(c);
324
- return await oauth2Server.revokeHandler(c.req, res);
325
- });
446
+ this.app.post('/oauth/revoke', wrap(oauth2Server.revokeHandler));
326
447
 
327
448
  if (this.options.verbose) {
328
449
  console.log('[Identity Server] Mounted OAuth2/OIDC routes:');
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { generateSessionId, calculateExpiration, isExpired } from './concerns/token-generator.js';
12
12
  import tryFn from '../../concerns/try-fn.js';
13
+ import { PluginError } from '../../errors.js';
13
14
 
14
15
  /**
15
16
  * Default session configuration
@@ -43,7 +44,13 @@ export class SessionManager {
43
44
  this.cleanupTimer = null;
44
45
 
45
46
  if (!this.sessionResource) {
46
- throw new Error('SessionManager requires a sessionResource');
47
+ throw new PluginError('SessionManager requires a sessionResource', {
48
+ pluginName: 'IdentityPlugin',
49
+ operation: 'SessionManager.constructor',
50
+ statusCode: 400,
51
+ retriable: false,
52
+ suggestion: 'Pass { sessionResource } when initializing IdentityPlugin or SessionManager.'
53
+ });
47
54
  }
48
55
 
49
56
  // Start automatic cleanup
@@ -66,7 +73,13 @@ export class SessionManager {
66
73
  const { userId, metadata = {}, ipAddress, userAgent, duration } = data;
67
74
 
68
75
  if (!userId) {
69
- throw new Error('userId is required to create a session');
76
+ throw new PluginError('userId is required to create a session', {
77
+ pluginName: 'IdentityPlugin',
78
+ operation: 'SessionManager.createSession',
79
+ statusCode: 400,
80
+ retriable: false,
81
+ suggestion: 'Provide data.userId when calling createSession().'
82
+ });
70
83
  }
71
84
 
72
85
  // Generate session ID
@@ -91,7 +104,14 @@ export class SessionManager {
91
104
  );
92
105
 
93
106
  if (!ok) {
94
- throw new Error(`Failed to create session: ${err.message}`);
107
+ throw new PluginError(`Failed to create session: ${err.message}`, {
108
+ pluginName: 'IdentityPlugin',
109
+ operation: 'SessionManager.createSession',
110
+ statusCode: 500,
111
+ retriable: false,
112
+ suggestion: 'Check session resource permissions and database connectivity.',
113
+ original: err
114
+ });
95
115
  }
96
116
 
97
117
  return {
@@ -155,13 +175,26 @@ export class SessionManager {
155
175
  */
156
176
  async updateSession(sessionId, metadata) {
157
177
  if (!sessionId) {
158
- throw new Error('sessionId is required');
178
+ throw new PluginError('sessionId is required', {
179
+ pluginName: 'IdentityPlugin',
180
+ operation: 'SessionManager.updateSession',
181
+ statusCode: 400,
182
+ retriable: false,
183
+ suggestion: 'Provide a sessionId when calling updateSession().'
184
+ });
159
185
  }
160
186
 
161
187
  const session = await this.getSession(sessionId);
162
188
 
163
189
  if (!session) {
164
- throw new Error('Session not found');
190
+ throw new PluginError('Session not found', {
191
+ pluginName: 'IdentityPlugin',
192
+ operation: 'SessionManager.updateSession',
193
+ statusCode: 404,
194
+ retriable: false,
195
+ suggestion: 'Ensure the session exists before updating metadata.',
196
+ sessionId
197
+ });
165
198
  }
166
199
 
167
200
  const updatedMetadata = { ...session.metadata, ...metadata };
@@ -173,7 +206,14 @@ export class SessionManager {
173
206
  );
174
207
 
175
208
  if (!ok) {
176
- throw new Error(`Failed to update session: ${err.message}`);
209
+ throw new PluginError(`Failed to update session: ${err.message}`, {
210
+ pluginName: 'IdentityPlugin',
211
+ operation: 'SessionManager.updateSession',
212
+ statusCode: 500,
213
+ retriable: false,
214
+ suggestion: 'Check session resource permissions and database connectivity.',
215
+ original: err
216
+ });
177
217
  }
178
218
 
179
219
  return updated;
@@ -295,7 +335,13 @@ export class SessionManager {
295
335
  // Hono-style
296
336
  res.header('Set-Cookie', cookieValue);
297
337
  } else {
298
- throw new Error('Unsupported response object');
338
+ throw new PluginError('Unsupported response object for session cookies', {
339
+ pluginName: 'IdentityPlugin',
340
+ operation: 'SessionManager.setSessionCookie',
341
+ statusCode: 400,
342
+ retriable: false,
343
+ suggestion: 'Pass an HTTP response object that implements setHeader() or header().'
344
+ });
299
345
  }
300
346
  }
301
347
 
@@ -10,21 +10,22 @@ import { BaseLayout } from '../layouts/base.js';
10
10
  * Render MFA verification page
11
11
  * @param {Object} props - Page properties
12
12
  * @param {string} [props.error] - Error message
13
- * @param {string} props.token - JSON string with {email, password}
13
+ * @param {string} props.email - User email
14
14
  * @param {string} [props.remember] - Remember me flag
15
+ * @param {string} props.challenge - MFA challenge token
15
16
  * @param {Object} [props.config] - UI configuration
16
17
  * @returns {string} HTML string
17
18
  */
18
19
  export function MFAVerificationPage(props = {}) {
19
- const { error = null, token, remember = '', config = {} } = props;
20
+ const {
21
+ error = null,
22
+ email = '',
23
+ remember = '0',
24
+ challenge,
25
+ config = {}
26
+ } = props;
20
27
 
21
- // Parse token to get email/password
22
- let userData = { email: '', password: '' };
23
- try {
24
- userData = JSON.parse(token);
25
- } catch (err) {
26
- // Fallback to empty values
27
- }
28
+ const rememberValue = remember === '1' ? '1' : '0';
28
29
 
29
30
  const content = html`
30
31
  <section class="identity-login">
@@ -58,6 +59,7 @@ export function MFAVerificationPage(props = {}) {
58
59
  <header class="identity-login__form-header">
59
60
  <h2>Verify Your Identity</h2>
60
61
  <p>Enter the 6-digit code from your authenticator app.</p>
62
+ ${email ? html`<p class="identity-login__meta text-sm text-slate-400 mt-2">Signing in as <strong>${email}</strong></p>` : ''}
61
63
  </header>
62
64
 
63
65
  ${error ? html`
@@ -68,9 +70,6 @@ export function MFAVerificationPage(props = {}) {
68
70
 
69
71
  <!-- TOTP Token Form -->
70
72
  <form method="POST" action="/login" class="identity-login__form-body" id="mfa-form">
71
- <input type="hidden" name="email" value="${userData.email}" />
72
- <input type="hidden" name="password" value="${userData.password}" />
73
- <input type="hidden" name="remember" value="${remember}" />
74
73
 
75
74
  <div class="identity-login__group">
76
75
  <label for="mfa_token">Verification Code</label>
@@ -91,6 +90,9 @@ export function MFAVerificationPage(props = {}) {
91
90
  The code refreshes every 30 seconds
92
91
  </p>
93
92
  </div>
93
+ <input type="hidden" name="email" value="${email}" />
94
+ <input type="hidden" name="remember" value="${rememberValue}" />
95
+ <input type="hidden" name="mfa_challenge" value="${challenge}" />
94
96
 
95
97
  <button type="submit" class="identity-login__submit">
96
98
  Verify
@@ -110,9 +112,9 @@ export function MFAVerificationPage(props = {}) {
110
112
 
111
113
  <!-- Backup Code Form (hidden by default) -->
112
114
  <form method="POST" action="/login" class="identity-login__form-body mt-6 hidden" id="backup-code-form">
113
- <input type="hidden" name="email" value="${userData.email}" />
114
- <input type="hidden" name="password" value="${userData.password}" />
115
- <input type="hidden" name="remember" value="${remember}" />
115
+ <input type="hidden" name="email" value="${email}" />
116
+ <input type="hidden" name="remember" value="${rememberValue}" />
117
+ <input type="hidden" name="mfa_challenge" value="${challenge}" />
116
118
 
117
119
  <div class="identity-login__group">
118
120
  <label for="backup_code">Backup Code</label>