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.
- package/README.md +139 -43
- package/dist/s3db.cjs +72425 -38970
- package/dist/s3db.cjs.map +1 -1
- package/dist/s3db.es.js +72177 -38764
- package/dist/s3db.es.js.map +1 -1
- package/mcp/lib/base-handler.js +157 -0
- package/mcp/lib/handlers/connection-handler.js +280 -0
- package/mcp/lib/handlers/query-handler.js +533 -0
- package/mcp/lib/handlers/resource-handler.js +428 -0
- package/mcp/lib/tool-registry.js +336 -0
- package/mcp/lib/tools/connection-tools.js +161 -0
- package/mcp/lib/tools/query-tools.js +267 -0
- package/mcp/lib/tools/resource-tools.js +404 -0
- package/package.json +94 -49
- package/src/clients/memory-client.class.js +346 -191
- package/src/clients/memory-storage.class.js +300 -84
- package/src/clients/s3-client.class.js +7 -6
- package/src/concerns/geo-encoding.js +19 -2
- package/src/concerns/ip.js +59 -9
- package/src/concerns/money.js +8 -1
- package/src/concerns/password-hashing.js +49 -8
- package/src/concerns/plugin-storage.js +186 -18
- package/src/concerns/storage-drivers/filesystem-driver.js +284 -0
- package/src/database.class.js +139 -29
- package/src/errors.js +332 -42
- package/src/plugins/api/auth/oidc-auth.js +66 -17
- package/src/plugins/api/auth/strategies/base-strategy.class.js +74 -0
- package/src/plugins/api/auth/strategies/factory.class.js +63 -0
- package/src/plugins/api/auth/strategies/global-strategy.class.js +44 -0
- package/src/plugins/api/auth/strategies/path-based-strategy.class.js +83 -0
- package/src/plugins/api/auth/strategies/path-rules-strategy.class.js +118 -0
- package/src/plugins/api/concerns/failban-manager.js +106 -57
- package/src/plugins/api/concerns/opengraph-helper.js +116 -0
- package/src/plugins/api/concerns/route-context.js +601 -0
- package/src/plugins/api/concerns/state-machine.js +288 -0
- package/src/plugins/api/index.js +180 -41
- package/src/plugins/api/routes/auth-routes.js +198 -30
- package/src/plugins/api/routes/resource-routes.js +19 -4
- package/src/plugins/api/server/health-manager.class.js +163 -0
- package/src/plugins/api/server/middleware-chain.class.js +310 -0
- package/src/plugins/api/server/router.class.js +472 -0
- package/src/plugins/api/server.js +280 -1303
- package/src/plugins/api/utils/custom-routes.js +17 -5
- package/src/plugins/api/utils/guards.js +76 -17
- package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
- package/src/plugins/api/utils/openapi-generator.js +7 -6
- package/src/plugins/api/utils/template-engine.js +77 -3
- package/src/plugins/audit.plugin.js +30 -8
- package/src/plugins/backup.plugin.js +110 -14
- package/src/plugins/cache/cache.class.js +22 -5
- package/src/plugins/cache/filesystem-cache.class.js +116 -19
- package/src/plugins/cache/memory-cache.class.js +211 -57
- package/src/plugins/cache/multi-tier-cache.class.js +371 -0
- package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
- package/src/plugins/cache/redis-cache.class.js +552 -0
- package/src/plugins/cache/s3-cache.class.js +17 -8
- package/src/plugins/cache.plugin.js +176 -61
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
- package/src/plugins/cloud-inventory/index.js +29 -8
- package/src/plugins/cloud-inventory/registry.js +64 -42
- package/src/plugins/cloud-inventory.plugin.js +240 -138
- package/src/plugins/concerns/plugin-dependencies.js +54 -0
- package/src/plugins/concerns/resource-names.js +100 -0
- package/src/plugins/consumers/index.js +10 -2
- package/src/plugins/consumers/sqs-consumer.js +12 -2
- package/src/plugins/cookie-farm-suite.plugin.js +278 -0
- package/src/plugins/cookie-farm.errors.js +73 -0
- package/src/plugins/cookie-farm.plugin.js +869 -0
- package/src/plugins/costs.plugin.js +7 -1
- package/src/plugins/eventual-consistency/analytics.js +94 -19
- package/src/plugins/eventual-consistency/config.js +15 -7
- package/src/plugins/eventual-consistency/consolidation.js +29 -11
- package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
- package/src/plugins/eventual-consistency/helpers.js +39 -14
- package/src/plugins/eventual-consistency/install.js +21 -2
- package/src/plugins/eventual-consistency/utils.js +32 -10
- package/src/plugins/fulltext.plugin.js +38 -11
- package/src/plugins/geo.plugin.js +61 -9
- package/src/plugins/identity/concerns/config.js +61 -0
- package/src/plugins/identity/concerns/mfa-manager.js +15 -2
- package/src/plugins/identity/concerns/rate-limit.js +124 -0
- package/src/plugins/identity/concerns/resource-schemas.js +9 -1
- package/src/plugins/identity/concerns/token-generator.js +29 -4
- package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
- package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
- package/src/plugins/identity/drivers/index.js +18 -0
- package/src/plugins/identity/drivers/password-driver.js +122 -0
- package/src/plugins/identity/email-service.js +17 -2
- package/src/plugins/identity/index.js +413 -69
- package/src/plugins/identity/oauth2-server.js +413 -30
- package/src/plugins/identity/oidc-discovery.js +16 -8
- package/src/plugins/identity/rsa-keys.js +115 -35
- package/src/plugins/identity/server.js +166 -45
- package/src/plugins/identity/session-manager.js +53 -7
- package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
- package/src/plugins/identity/ui/routes.js +363 -255
- package/src/plugins/importer/index.js +153 -20
- package/src/plugins/index.js +9 -2
- package/src/plugins/kubernetes-inventory/index.js +6 -0
- package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
- package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
- package/src/plugins/kubernetes-inventory.plugin.js +980 -0
- package/src/plugins/metrics.plugin.js +64 -16
- package/src/plugins/ml/base-model.class.js +25 -15
- package/src/plugins/ml/regression-model.class.js +1 -1
- package/src/plugins/ml.errors.js +57 -25
- package/src/plugins/ml.plugin.js +28 -4
- package/src/plugins/namespace.js +210 -0
- package/src/plugins/plugin.class.js +180 -8
- package/src/plugins/puppeteer/console-monitor.js +729 -0
- package/src/plugins/puppeteer/cookie-manager.js +492 -0
- package/src/plugins/puppeteer/network-monitor.js +816 -0
- package/src/plugins/puppeteer/performance-manager.js +746 -0
- package/src/plugins/puppeteer/proxy-manager.js +478 -0
- package/src/plugins/puppeteer/stealth-manager.js +556 -0
- package/src/plugins/puppeteer.errors.js +81 -0
- package/src/plugins/puppeteer.plugin.js +1327 -0
- package/src/plugins/queue-consumer.plugin.js +69 -14
- package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
- package/src/plugins/recon/concerns/command-runner.js +148 -0
- package/src/plugins/recon/concerns/diff-detector.js +372 -0
- package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
- package/src/plugins/recon/concerns/process-manager.js +338 -0
- package/src/plugins/recon/concerns/report-generator.js +478 -0
- package/src/plugins/recon/concerns/security-analyzer.js +571 -0
- package/src/plugins/recon/concerns/target-normalizer.js +68 -0
- package/src/plugins/recon/config/defaults.js +321 -0
- package/src/plugins/recon/config/resources.js +370 -0
- package/src/plugins/recon/index.js +778 -0
- package/src/plugins/recon/managers/dependency-manager.js +174 -0
- package/src/plugins/recon/managers/scheduler-manager.js +179 -0
- package/src/plugins/recon/managers/storage-manager.js +745 -0
- package/src/plugins/recon/managers/target-manager.js +274 -0
- package/src/plugins/recon/stages/asn-stage.js +314 -0
- package/src/plugins/recon/stages/certificate-stage.js +84 -0
- package/src/plugins/recon/stages/dns-stage.js +107 -0
- package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
- package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
- package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
- package/src/plugins/recon/stages/http-stage.js +89 -0
- package/src/plugins/recon/stages/latency-stage.js +148 -0
- package/src/plugins/recon/stages/massdns-stage.js +302 -0
- package/src/plugins/recon/stages/osint-stage.js +1373 -0
- package/src/plugins/recon/stages/ports-stage.js +169 -0
- package/src/plugins/recon/stages/screenshot-stage.js +94 -0
- package/src/plugins/recon/stages/secrets-stage.js +514 -0
- package/src/plugins/recon/stages/subdomains-stage.js +295 -0
- package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
- package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
- package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
- package/src/plugins/recon/stages/whois-stage.js +349 -0
- package/src/plugins/recon.plugin.js +75 -0
- package/src/plugins/recon.plugin.js.backup +2635 -0
- package/src/plugins/relation.errors.js +87 -14
- package/src/plugins/replicator.plugin.js +514 -137
- package/src/plugins/replicators/base-replicator.class.js +89 -1
- package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
- package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mysql-replicator.class.js +52 -17
- package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
- package/src/plugins/replicators/postgres-replicator.class.js +62 -27
- package/src/plugins/replicators/s3db-replicator.class.js +25 -18
- package/src/plugins/replicators/schema-sync.helper.js +3 -3
- package/src/plugins/replicators/sqs-replicator.class.js +8 -2
- package/src/plugins/replicators/turso-replicator.class.js +23 -3
- package/src/plugins/replicators/webhook-replicator.class.js +42 -4
- package/src/plugins/s3-queue.plugin.js +464 -65
- package/src/plugins/scheduler.plugin.js +20 -6
- package/src/plugins/state-machine.plugin.js +40 -9
- package/src/plugins/tfstate/README.md +126 -126
- package/src/plugins/tfstate/base-driver.js +28 -4
- package/src/plugins/tfstate/errors.js +65 -10
- package/src/plugins/tfstate/filesystem-driver.js +52 -8
- package/src/plugins/tfstate/index.js +163 -90
- package/src/plugins/tfstate/s3-driver.js +64 -6
- package/src/plugins/ttl.plugin.js +72 -17
- package/src/plugins/vector/distances.js +18 -12
- package/src/plugins/vector/kmeans.js +26 -4
- package/src/resource.class.js +115 -19
- package/src/testing/factory.class.js +20 -3
- package/src/testing/seeder.class.js +7 -1
- package/src/clients/memory-client.md +0 -917
- 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
|
|
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.
|
|
189
|
-
this.
|
|
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.
|
|
203
|
-
|
|
204
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
285
|
-
|
|
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
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
274
|
-
|
|
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
|
|
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',
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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',
|
|
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',
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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',
|
|
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',
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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 {
|
|
20
|
+
const {
|
|
21
|
+
error = null,
|
|
22
|
+
email = '',
|
|
23
|
+
remember = '0',
|
|
24
|
+
challenge,
|
|
25
|
+
config = {}
|
|
26
|
+
} = props;
|
|
20
27
|
|
|
21
|
-
|
|
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="${
|
|
114
|
-
<input type="hidden" name="
|
|
115
|
-
<input type="hidden" name="
|
|
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>
|