s3db.js 13.6.1 → 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 +56 -15
- package/dist/s3db.cjs +72446 -39022
- package/dist/s3db.cjs.map +1 -1
- package/dist/s3db.es.js +72172 -38790
- 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 +85 -50
- 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/route-context.js +601 -0
- package/src/plugins/api/index.js +168 -40
- 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/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/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
package/src/plugins/api/index.js
CHANGED
|
@@ -37,6 +37,7 @@ import { requirePluginDependency } from '../concerns/plugin-dependencies.js';
|
|
|
37
37
|
import tryFn from '../../concerns/try-fn.js';
|
|
38
38
|
import { ApiServer } from './server.js';
|
|
39
39
|
import { idGenerator } from '../../concerns/id.js';
|
|
40
|
+
import { resolveResourceName } from '../concerns/resource-names.js';
|
|
40
41
|
|
|
41
42
|
const AUTH_DRIVER_KEYS = ['jwt', 'apiKey', 'basic', 'oidc', 'oauth2'];
|
|
42
43
|
|
|
@@ -61,9 +62,10 @@ function normalizeAuthConfig(authOptions = {}) {
|
|
|
61
62
|
pathAuth: authOptions.pathAuth,
|
|
62
63
|
strategy: authOptions.strategy || 'any',
|
|
63
64
|
priorities: authOptions.priorities || {},
|
|
64
|
-
resource: authOptions.resource
|
|
65
|
+
resource: authOptions.resource,
|
|
65
66
|
usernameField: authOptions.usernameField || 'email',
|
|
66
|
-
passwordField: authOptions.passwordField || 'password'
|
|
67
|
+
passwordField: authOptions.passwordField || 'password',
|
|
68
|
+
createResource: authOptions.createResource !== false
|
|
67
69
|
};
|
|
68
70
|
|
|
69
71
|
const seen = new Set();
|
|
@@ -130,7 +132,29 @@ export class ApiPlugin extends Plugin {
|
|
|
130
132
|
constructor(options = {}) {
|
|
131
133
|
super(options);
|
|
132
134
|
|
|
135
|
+
const resourceNamesOption = options.resourceNames || {};
|
|
136
|
+
this._usersResourceDescriptor = {
|
|
137
|
+
defaultName: 'plg_api_users',
|
|
138
|
+
override: resourceNamesOption.authUsers || options.auth?.resource
|
|
139
|
+
};
|
|
133
140
|
const normalizedAuth = normalizeAuthConfig(options.auth);
|
|
141
|
+
normalizedAuth.registration = {
|
|
142
|
+
enabled: options.auth?.registration?.enabled === true,
|
|
143
|
+
allowedFields: Array.isArray(options.auth?.registration?.allowedFields)
|
|
144
|
+
? options.auth.registration.allowedFields
|
|
145
|
+
: [],
|
|
146
|
+
defaultRole: options.auth?.registration?.defaultRole || 'user'
|
|
147
|
+
};
|
|
148
|
+
normalizedAuth.loginThrottle = {
|
|
149
|
+
enabled: options.auth?.loginThrottle?.enabled !== false,
|
|
150
|
+
maxAttempts: options.auth?.loginThrottle?.maxAttempts || 5,
|
|
151
|
+
windowMs: options.auth?.loginThrottle?.windowMs || 60_000,
|
|
152
|
+
blockDurationMs: options.auth?.loginThrottle?.blockDurationMs || 300_000,
|
|
153
|
+
maxEntries: options.auth?.loginThrottle?.maxEntries || 10_000
|
|
154
|
+
};
|
|
155
|
+
this.usersResourceName = this._resolveUsersResourceName();
|
|
156
|
+
normalizedAuth.resource = this.usersResourceName;
|
|
157
|
+
normalizedAuth.createResource = options.auth?.createResource !== false;
|
|
134
158
|
|
|
135
159
|
this.config = {
|
|
136
160
|
// Server configuration
|
|
@@ -182,7 +206,8 @@ export class ApiPlugin extends Plugin {
|
|
|
182
206
|
enabled: options.rateLimit?.enabled || false,
|
|
183
207
|
windowMs: options.rateLimit?.windowMs || 60000, // 1 minute
|
|
184
208
|
maxRequests: options.rateLimit?.maxRequests || 100,
|
|
185
|
-
keyGenerator: options.rateLimit?.keyGenerator || null
|
|
209
|
+
keyGenerator: options.rateLimit?.keyGenerator || null,
|
|
210
|
+
maxUniqueKeys: options.rateLimit?.maxUniqueKeys || 1000
|
|
186
211
|
},
|
|
187
212
|
|
|
188
213
|
// Logging configuration
|
|
@@ -288,7 +313,22 @@ export class ApiPlugin extends Plugin {
|
|
|
288
313
|
},
|
|
289
314
|
|
|
290
315
|
// Custom global middlewares
|
|
291
|
-
middlewares: options.middlewares || []
|
|
316
|
+
middlewares: options.middlewares || [],
|
|
317
|
+
|
|
318
|
+
requestId: options.requestId || { enabled: false },
|
|
319
|
+
sessionTracking: options.sessionTracking || { enabled: false },
|
|
320
|
+
events: options.events || { enabled: false },
|
|
321
|
+
metrics: options.metrics || { enabled: false },
|
|
322
|
+
failban: {
|
|
323
|
+
...(options.failban || {}),
|
|
324
|
+
enabled: options.failban?.enabled === true,
|
|
325
|
+
resourceNames: resourceNamesOption.failban || options.failban?.resourceNames || {}
|
|
326
|
+
},
|
|
327
|
+
static: Array.isArray(options.static) ? options.static : [],
|
|
328
|
+
health: typeof options.health === 'object'
|
|
329
|
+
? options.health
|
|
330
|
+
: { enabled: options.health !== false },
|
|
331
|
+
maxBodySize: options.maxBodySize || 10 * 1024 * 1024
|
|
292
332
|
};
|
|
293
333
|
|
|
294
334
|
this.config.resources = this._normalizeResourcesConfig(options.resources);
|
|
@@ -410,18 +450,43 @@ export class ApiPlugin extends Plugin {
|
|
|
410
450
|
* @private
|
|
411
451
|
*/
|
|
412
452
|
async _createUsersResource() {
|
|
453
|
+
const existingResource = this._findExistingUsersResource();
|
|
454
|
+
|
|
455
|
+
if (!this.config.auth.createResource) {
|
|
456
|
+
if (!existingResource) {
|
|
457
|
+
throw new Error(
|
|
458
|
+
`[API Plugin] Auth resource "${this.usersResourceName}" not found and auth.createResource is false`
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
this.usersResource = existingResource;
|
|
462
|
+
this.config.auth.resource = existingResource.name;
|
|
463
|
+
if (this.config.verbose) {
|
|
464
|
+
console.log(`[API Plugin] Using existing ${existingResource.name} resource for authentication`);
|
|
465
|
+
}
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (existingResource) {
|
|
470
|
+
this.usersResource = existingResource;
|
|
471
|
+
this.config.auth.resource = existingResource.name;
|
|
472
|
+
if (this.config.verbose) {
|
|
473
|
+
console.log(`[API Plugin] Reusing existing ${existingResource.name} resource for authentication`);
|
|
474
|
+
}
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
413
478
|
const [ok, err, resource] = await tryFn(() =>
|
|
414
479
|
this.database.createResource({
|
|
415
|
-
name:
|
|
480
|
+
name: this.usersResourceName,
|
|
416
481
|
attributes: {
|
|
417
482
|
id: 'string|required',
|
|
418
483
|
username: 'string|required|minlength:3',
|
|
419
|
-
email: 'string|required|email',
|
|
484
|
+
email: 'string|required|email',
|
|
420
485
|
password: 'secret|required|minlength:8',
|
|
421
486
|
apiKey: 'string|optional',
|
|
422
487
|
jwtSecret: 'string|optional',
|
|
423
488
|
role: 'string|default:user',
|
|
424
|
-
scopes: 'array|items:string|optional',
|
|
489
|
+
scopes: 'array|items:string|optional',
|
|
425
490
|
active: 'boolean|default:true',
|
|
426
491
|
createdAt: 'string|optional',
|
|
427
492
|
lastLoginAt: 'string|optional',
|
|
@@ -433,20 +498,40 @@ export class ApiPlugin extends Plugin {
|
|
|
433
498
|
})
|
|
434
499
|
);
|
|
435
500
|
|
|
436
|
-
if (ok) {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
501
|
+
if (!ok) {
|
|
502
|
+
throw err;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
this.usersResource = resource;
|
|
506
|
+
this.config.auth.resource = resource.name;
|
|
507
|
+
if (this.config.verbose) {
|
|
508
|
+
console.log(`[API Plugin] Created ${this.usersResourceName} resource for authentication`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
_findExistingUsersResource() {
|
|
513
|
+
const candidates = new Set([this.usersResourceName]);
|
|
514
|
+
|
|
515
|
+
const identityPlugin = this.database?.plugins?.identity || this.database?.plugins?.Identity;
|
|
516
|
+
if (identityPlugin) {
|
|
517
|
+
const identityNames = [
|
|
518
|
+
identityPlugin.usersResource?.name,
|
|
519
|
+
identityPlugin.config?.resources?.users?.mergedConfig?.name,
|
|
520
|
+
identityPlugin.config?.resources?.users?.userConfig?.name
|
|
521
|
+
].filter(Boolean);
|
|
522
|
+
for (const name of identityNames) {
|
|
523
|
+
candidates.add(name);
|
|
440
524
|
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
if (
|
|
445
|
-
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
for (const name of candidates) {
|
|
528
|
+
if (!name) continue;
|
|
529
|
+
const resource = this.database.resources?.[name];
|
|
530
|
+
if (resource) {
|
|
531
|
+
return resource;
|
|
446
532
|
}
|
|
447
|
-
} else {
|
|
448
|
-
throw err;
|
|
449
533
|
}
|
|
534
|
+
return null;
|
|
450
535
|
}
|
|
451
536
|
/**
|
|
452
537
|
* Setup middlewares
|
|
@@ -584,25 +669,41 @@ export class ApiPlugin extends Plugin {
|
|
|
584
669
|
*/
|
|
585
670
|
async _createRateLimitMiddleware() {
|
|
586
671
|
const requests = new Map();
|
|
587
|
-
const { windowMs, maxRequests, keyGenerator } = this.config.rateLimit;
|
|
672
|
+
const { windowMs, maxRequests, keyGenerator, maxUniqueKeys } = this.config.rateLimit;
|
|
673
|
+
|
|
674
|
+
const getClientIp = (c) => {
|
|
675
|
+
const forwarded = c.req.header('x-forwarded-for');
|
|
676
|
+
if (forwarded) {
|
|
677
|
+
return forwarded.split(',')[0].trim();
|
|
678
|
+
}
|
|
679
|
+
const cfConnecting = c.req.header('cf-connecting-ip');
|
|
680
|
+
if (cfConnecting) {
|
|
681
|
+
return cfConnecting;
|
|
682
|
+
}
|
|
683
|
+
return c.req.raw?.socket?.remoteAddress || 'unknown';
|
|
684
|
+
};
|
|
588
685
|
|
|
589
686
|
return async (c, next) => {
|
|
590
687
|
// Generate key (IP or custom)
|
|
591
|
-
const key = keyGenerator
|
|
592
|
-
? keyGenerator(c)
|
|
593
|
-
: c.req.header('x-forwarded-for') || c.req.header('cf-connecting-ip') || 'unknown';
|
|
688
|
+
const key = keyGenerator ? keyGenerator(c) : getClientIp(c) || 'unknown';
|
|
594
689
|
|
|
595
|
-
|
|
596
|
-
if (!requests.has(key)) {
|
|
597
|
-
requests.set(key, { count: 0, resetAt: Date.now() + windowMs });
|
|
598
|
-
}
|
|
690
|
+
let record = requests.get(key);
|
|
599
691
|
|
|
600
|
-
|
|
692
|
+
// Reset expired records to prevent unbounded memory growth
|
|
693
|
+
if (record && Date.now() > record.resetAt) {
|
|
694
|
+
requests.delete(key);
|
|
695
|
+
record = null;
|
|
696
|
+
}
|
|
601
697
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
698
|
+
if (!record) {
|
|
699
|
+
record = { count: 0, resetAt: Date.now() + windowMs };
|
|
700
|
+
requests.set(key, record);
|
|
701
|
+
if (requests.size > maxUniqueKeys) {
|
|
702
|
+
const oldestKey = requests.keys().next().value;
|
|
703
|
+
if (oldestKey) {
|
|
704
|
+
requests.delete(oldestKey);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
606
707
|
}
|
|
607
708
|
|
|
608
709
|
// Check limit
|
|
@@ -706,14 +807,15 @@ export class ApiPlugin extends Plugin {
|
|
|
706
807
|
return;
|
|
707
808
|
}
|
|
708
809
|
|
|
709
|
-
// Skip if already compressed
|
|
710
|
-
if (c.res.headers.has('content-encoding')) {
|
|
810
|
+
// Skip if already compressed or body consumed
|
|
811
|
+
if (c.res.headers.has('content-encoding') || c.res.bodyUsed) {
|
|
711
812
|
return;
|
|
712
813
|
}
|
|
713
814
|
|
|
714
815
|
// Skip if content-type should not be compressed
|
|
715
816
|
const contentType = c.res.headers.get('content-type') || '';
|
|
716
|
-
|
|
817
|
+
const isTextLike = contentType.startsWith('text/') || contentType.includes('json');
|
|
818
|
+
if (skipContentTypes.some(type => contentType.startsWith(type)) || !isTextLike) {
|
|
717
819
|
return;
|
|
718
820
|
}
|
|
719
821
|
|
|
@@ -909,11 +1011,22 @@ export class ApiPlugin extends Plugin {
|
|
|
909
1011
|
port: this.config.port,
|
|
910
1012
|
host: this.config.host,
|
|
911
1013
|
database: this.database,
|
|
1014
|
+
namespace: this.namespace,
|
|
912
1015
|
versionPrefix: this.config.versionPrefix,
|
|
913
1016
|
resources: this.config.resources,
|
|
914
1017
|
routes: this.config.routes,
|
|
915
1018
|
templates: this.config.templates,
|
|
916
1019
|
middlewares: this.compiledMiddlewares,
|
|
1020
|
+
cors: this.config.cors,
|
|
1021
|
+
security: this.config.security,
|
|
1022
|
+
requestId: this.config.requestId,
|
|
1023
|
+
sessionTracking: this.config.sessionTracking,
|
|
1024
|
+
events: this.config.events,
|
|
1025
|
+
metrics: this.config.metrics,
|
|
1026
|
+
failban: this.config.failban,
|
|
1027
|
+
static: this.config.static,
|
|
1028
|
+
health: this.config.health,
|
|
1029
|
+
maxBodySize: this.config.maxBodySize,
|
|
917
1030
|
verbose: this.config.verbose,
|
|
918
1031
|
auth: this.config.auth,
|
|
919
1032
|
docsEnabled: this.config.docs.enabled,
|
|
@@ -942,10 +1055,24 @@ export class ApiPlugin extends Plugin {
|
|
|
942
1055
|
|
|
943
1056
|
if (this.server) {
|
|
944
1057
|
await this.server.stop();
|
|
945
|
-
this.server = null;
|
|
946
1058
|
}
|
|
1059
|
+
this.server = null;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
_resolveUsersResourceName() {
|
|
1063
|
+
return resolveResourceName('api', this._usersResourceDescriptor, {
|
|
1064
|
+
namespace: this.namespace
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
947
1067
|
|
|
948
|
-
|
|
1068
|
+
onNamespaceChanged() {
|
|
1069
|
+
this.usersResourceName = this._resolveUsersResourceName();
|
|
1070
|
+
if (this.config?.auth) {
|
|
1071
|
+
this.config.auth.resource = this.usersResourceName;
|
|
1072
|
+
}
|
|
1073
|
+
if (this.server?.failban) {
|
|
1074
|
+
this.server.failban.setNamespace(this.namespace);
|
|
1075
|
+
}
|
|
949
1076
|
}
|
|
950
1077
|
|
|
951
1078
|
/**
|
|
@@ -959,11 +1086,9 @@ export class ApiPlugin extends Plugin {
|
|
|
959
1086
|
|
|
960
1087
|
// Optionally delete users resource
|
|
961
1088
|
if (purgeData && this.usersResource) {
|
|
962
|
-
|
|
963
|
-
const [ok] = await tryFn(() => this.database.deleteResource('plg_users'));
|
|
964
|
-
|
|
1089
|
+
const [ok] = await tryFn(() => this.database.deleteResource(this.usersResourceName));
|
|
965
1090
|
if (ok && this.config.verbose) {
|
|
966
|
-
console.log(
|
|
1091
|
+
console.log(`[API Plugin] Deleted ${this.usersResourceName} resource`);
|
|
967
1092
|
}
|
|
968
1093
|
}
|
|
969
1094
|
|
|
@@ -1006,3 +1131,6 @@ export {
|
|
|
1006
1131
|
createNotificationStateMachine,
|
|
1007
1132
|
createAttemptStateMachine
|
|
1008
1133
|
} from './concerns/state-machine.js';
|
|
1134
|
+
|
|
1135
|
+
// Export route context utilities (NEW!)
|
|
1136
|
+
export { RouteContext, withContext } from './concerns/route-context.js';
|
|
@@ -9,8 +9,8 @@ import { asyncHandler } from '../utils/error-handler.js';
|
|
|
9
9
|
import * as formatter from '../utils/response-formatter.js';
|
|
10
10
|
import { createToken } from '../auth/jwt-auth.js';
|
|
11
11
|
import { generateApiKey } from '../auth/api-key-auth.js';
|
|
12
|
-
import { encrypt } from '../../../concerns/crypto.js';
|
|
13
12
|
import tryFn from '../../../concerns/try-fn.js';
|
|
13
|
+
import { hashPassword } from '../../../concerns/password-hashing.js';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Create authentication routes
|
|
@@ -27,16 +27,133 @@ export function createAuthRoutes(authResource, config = {}) {
|
|
|
27
27
|
jwtSecret,
|
|
28
28
|
jwtExpiresIn = '7d',
|
|
29
29
|
passphrase = 'secret',
|
|
30
|
-
|
|
30
|
+
registration = {},
|
|
31
|
+
loginThrottle = {}
|
|
31
32
|
} = config;
|
|
32
33
|
|
|
34
|
+
const registrationConfig = {
|
|
35
|
+
enabled: registration.enabled === true,
|
|
36
|
+
allowedFields: Array.isArray(registration.allowedFields) ? registration.allowedFields : [],
|
|
37
|
+
defaultRole: registration.defaultRole || 'user'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const schemaAttributes = authResource.schema?.attributes || {};
|
|
41
|
+
const passwordAttribute = schemaAttributes?.[passwordField];
|
|
42
|
+
const isPasswordType = typeof passwordAttribute === 'string'
|
|
43
|
+
? passwordAttribute.includes('password')
|
|
44
|
+
: passwordAttribute?.type === 'password';
|
|
45
|
+
|
|
46
|
+
const allowedRegistrationFields = new Set([usernameField, passwordField]);
|
|
47
|
+
for (const field of registrationConfig.allowedFields) {
|
|
48
|
+
if (typeof field === 'string' && field && field !== passwordField) {
|
|
49
|
+
allowedRegistrationFields.add(field);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const blockedRegistrationFields = new Set([
|
|
54
|
+
'role',
|
|
55
|
+
'active',
|
|
56
|
+
'apiKey',
|
|
57
|
+
'jwtSecret',
|
|
58
|
+
'scopes',
|
|
59
|
+
'createdAt',
|
|
60
|
+
'updatedAt',
|
|
61
|
+
'metadata',
|
|
62
|
+
'id'
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
const loginThrottleConfig = {
|
|
66
|
+
enabled: loginThrottle?.enabled !== false,
|
|
67
|
+
maxAttempts: loginThrottle?.maxAttempts ?? 5,
|
|
68
|
+
windowMs: loginThrottle?.windowMs ?? 60_000,
|
|
69
|
+
blockDurationMs: loginThrottle?.blockDurationMs ?? 300_000,
|
|
70
|
+
maxEntries: loginThrottle?.maxEntries ?? 10_000
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const loginAttempts = new Map();
|
|
74
|
+
|
|
75
|
+
const getClientIp = (c) => {
|
|
76
|
+
const forwarded = c.req.header('x-forwarded-for');
|
|
77
|
+
if (forwarded) {
|
|
78
|
+
return forwarded.split(',')[0].trim();
|
|
79
|
+
}
|
|
80
|
+
const cfConnecting = c.req.header('cf-connecting-ip');
|
|
81
|
+
if (cfConnecting) {
|
|
82
|
+
return cfConnecting;
|
|
83
|
+
}
|
|
84
|
+
return c.req.raw?.socket?.remoteAddress || 'unknown';
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const cleanupLoginAttempts = () => {
|
|
88
|
+
if (loginAttempts.size <= loginThrottleConfig.maxEntries) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const oldestKey = loginAttempts.keys().next().value;
|
|
92
|
+
if (oldestKey) {
|
|
93
|
+
loginAttempts.delete(oldestKey);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const getThrottleRecord = (key, now) => {
|
|
98
|
+
if (!loginThrottleConfig.enabled) return null;
|
|
99
|
+
let record = loginAttempts.get(key);
|
|
100
|
+
if (record && record.blockedUntil && now > record.blockedUntil) {
|
|
101
|
+
loginAttempts.delete(key);
|
|
102
|
+
record = null;
|
|
103
|
+
}
|
|
104
|
+
if (!record || now - record.firstAttemptAt > loginThrottleConfig.windowMs) {
|
|
105
|
+
record = { attempts: 0, firstAttemptAt: now, blockedUntil: null };
|
|
106
|
+
loginAttempts.set(key, record);
|
|
107
|
+
cleanupLoginAttempts();
|
|
108
|
+
}
|
|
109
|
+
return record;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const registerFailedAttempt = (record, now) => {
|
|
113
|
+
if (!loginThrottleConfig.enabled || !record) {
|
|
114
|
+
return { blocked: false };
|
|
115
|
+
}
|
|
116
|
+
record.attempts += 1;
|
|
117
|
+
record.lastAttemptAt = now;
|
|
118
|
+
if (record.attempts >= loginThrottleConfig.maxAttempts) {
|
|
119
|
+
record.blockedUntil = now + loginThrottleConfig.blockDurationMs;
|
|
120
|
+
const retryAfter = Math.ceil((record.blockedUntil - now) / 1000);
|
|
121
|
+
return { blocked: true, retryAfter };
|
|
122
|
+
}
|
|
123
|
+
return { blocked: false };
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const buildPublicUser = (user) => {
|
|
127
|
+
const publicUser = { id: user.id };
|
|
128
|
+
const identifier = user[usernameField] ?? user.email ?? user.username;
|
|
129
|
+
if (identifier !== undefined) {
|
|
130
|
+
publicUser[usernameField] = identifier;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const field of allowedRegistrationFields) {
|
|
134
|
+
if (field === usernameField || field === passwordField) continue;
|
|
135
|
+
if (user[field] !== undefined) {
|
|
136
|
+
publicUser[field] = user[field];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (schemaAttributes.role !== undefined && user.role !== undefined) {
|
|
141
|
+
publicUser.role = user.role;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (schemaAttributes.active !== undefined && user.active !== undefined) {
|
|
145
|
+
publicUser.active = user.active;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return publicUser;
|
|
149
|
+
};
|
|
150
|
+
|
|
33
151
|
// POST /auth/register - Register new user
|
|
34
|
-
if (
|
|
152
|
+
if (registrationConfig.enabled) {
|
|
35
153
|
app.post('/register', asyncHandler(async (c) => {
|
|
36
154
|
const data = await c.req.json();
|
|
37
155
|
const username = data[usernameField];
|
|
38
156
|
const password = data[passwordField];
|
|
39
|
-
const role = data.role || 'user';
|
|
40
157
|
|
|
41
158
|
// Validate input
|
|
42
159
|
if (!username || !password) {
|
|
@@ -68,17 +185,28 @@ export function createAuthRoutes(authResource, config = {}) {
|
|
|
68
185
|
// Create user with dynamic fields
|
|
69
186
|
// Only include fields from request + required auth fields
|
|
70
187
|
const { id, ...dataWithoutId } = data; // Exclude id from request data
|
|
71
|
-
const userData = {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (!userData.role) {
|
|
79
|
-
userData.role = role;
|
|
188
|
+
const userData = {};
|
|
189
|
+
|
|
190
|
+
for (const [key, value] of Object.entries(dataWithoutId)) {
|
|
191
|
+
if (!allowedRegistrationFields.has(key)) continue;
|
|
192
|
+
if (blockedRegistrationFields.has(key)) continue;
|
|
193
|
+
if (key === usernameField || key === passwordField) continue;
|
|
194
|
+
userData[key] = value;
|
|
80
195
|
}
|
|
81
|
-
|
|
196
|
+
|
|
197
|
+
userData[usernameField] = username;
|
|
198
|
+
|
|
199
|
+
if (isPasswordType) {
|
|
200
|
+
userData[passwordField] = password;
|
|
201
|
+
} else {
|
|
202
|
+
userData[passwordField] = await hashPassword(password);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (schemaAttributes.role !== undefined) {
|
|
206
|
+
userData.role = registrationConfig.defaultRole;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (schemaAttributes.active !== undefined) {
|
|
82
210
|
userData.active = true;
|
|
83
211
|
}
|
|
84
212
|
|
|
@@ -98,11 +226,8 @@ export function createAuthRoutes(authResource, config = {}) {
|
|
|
98
226
|
);
|
|
99
227
|
}
|
|
100
228
|
|
|
101
|
-
// Remove sensitive data from response
|
|
102
|
-
const { [passwordField]: _, ...userWithoutPassword } = user;
|
|
103
|
-
|
|
104
229
|
const response = formatter.created({
|
|
105
|
-
user:
|
|
230
|
+
user: buildPublicUser(user),
|
|
106
231
|
...(token && { token }) // Only include token if JWT driver
|
|
107
232
|
}, `/auth/users/${user.id}`);
|
|
108
233
|
|
|
@@ -127,6 +252,25 @@ export function createAuthRoutes(authResource, config = {}) {
|
|
|
127
252
|
const queryFilter = { [usernameField]: username };
|
|
128
253
|
const users = await authResource.query(queryFilter);
|
|
129
254
|
if (!users || users.length === 0) {
|
|
255
|
+
const now = Date.now();
|
|
256
|
+
let throttleRecord = null;
|
|
257
|
+
let throttleKey = null;
|
|
258
|
+
if (loginThrottleConfig.enabled) {
|
|
259
|
+
const ip = getClientIp(c);
|
|
260
|
+
throttleKey = `${ip}:${username}`;
|
|
261
|
+
throttleRecord = getThrottleRecord(throttleKey, now);
|
|
262
|
+
const throttleResult = registerFailedAttempt(throttleRecord, now);
|
|
263
|
+
if (throttleResult.blocked) {
|
|
264
|
+
c.header('Retry-After', throttleResult.retryAfter.toString());
|
|
265
|
+
const response = formatter.error('Too many login attempts. Try again later.', {
|
|
266
|
+
status: 429,
|
|
267
|
+
code: 'TOO_MANY_ATTEMPTS',
|
|
268
|
+
details: { retryAfter: throttleResult.retryAfter }
|
|
269
|
+
});
|
|
270
|
+
return c.json(response, response._status);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
130
274
|
const response = formatter.unauthorized('Invalid credentials');
|
|
131
275
|
return c.json(response, response._status);
|
|
132
276
|
}
|
|
@@ -139,6 +283,25 @@ export function createAuthRoutes(authResource, config = {}) {
|
|
|
139
283
|
return c.json(response, response._status);
|
|
140
284
|
}
|
|
141
285
|
|
|
286
|
+
const now = Date.now();
|
|
287
|
+
let throttleRecord = null;
|
|
288
|
+
let throttleKey = null;
|
|
289
|
+
if (loginThrottleConfig.enabled) {
|
|
290
|
+
const ip = getClientIp(c);
|
|
291
|
+
throttleKey = `${ip}:${username}`;
|
|
292
|
+
throttleRecord = getThrottleRecord(throttleKey, now);
|
|
293
|
+
if (throttleRecord && throttleRecord.blockedUntil && now < throttleRecord.blockedUntil) {
|
|
294
|
+
const retryAfter = Math.ceil((throttleRecord.blockedUntil - now) / 1000);
|
|
295
|
+
c.header('Retry-After', retryAfter.toString());
|
|
296
|
+
const response = formatter.error('Too many login attempts. Try again later.', {
|
|
297
|
+
status: 429,
|
|
298
|
+
code: 'TOO_MANY_ATTEMPTS',
|
|
299
|
+
details: { retryAfter }
|
|
300
|
+
});
|
|
301
|
+
return c.json(response, response._status);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
142
305
|
// Verify password (compare with password field)
|
|
143
306
|
// For 'password' field type (bcrypt hash), use verifyPassword
|
|
144
307
|
// For 'secret' field type (AES encryption), compare directly
|
|
@@ -163,6 +326,17 @@ export function createAuthRoutes(authResource, config = {}) {
|
|
|
163
326
|
}
|
|
164
327
|
|
|
165
328
|
if (!isValid) {
|
|
329
|
+
const throttleResult = registerFailedAttempt(throttleRecord, now);
|
|
330
|
+
if (throttleResult.blocked) {
|
|
331
|
+
c.header('Retry-After', throttleResult.retryAfter.toString());
|
|
332
|
+
const response = formatter.error('Too many login attempts. Try again later.', {
|
|
333
|
+
status: 429,
|
|
334
|
+
code: 'TOO_MANY_ATTEMPTS',
|
|
335
|
+
details: { retryAfter: throttleResult.retryAfter }
|
|
336
|
+
});
|
|
337
|
+
return c.json(response, response._status);
|
|
338
|
+
}
|
|
339
|
+
|
|
166
340
|
const response = formatter.unauthorized('Invalid credentials');
|
|
167
341
|
return c.json(response, response._status);
|
|
168
342
|
}
|
|
@@ -189,10 +363,12 @@ export function createAuthRoutes(authResource, config = {}) {
|
|
|
189
363
|
}
|
|
190
364
|
|
|
191
365
|
// Remove sensitive data from response
|
|
192
|
-
|
|
366
|
+
if (loginThrottleConfig.enabled && throttleKey) {
|
|
367
|
+
loginAttempts.delete(throttleKey);
|
|
368
|
+
}
|
|
193
369
|
|
|
194
370
|
const response = formatter.success({
|
|
195
|
-
user:
|
|
371
|
+
user: buildPublicUser(user),
|
|
196
372
|
token,
|
|
197
373
|
expiresIn: jwtExpiresIn
|
|
198
374
|
});
|
|
@@ -237,15 +413,7 @@ export function createAuthRoutes(authResource, config = {}) {
|
|
|
237
413
|
}
|
|
238
414
|
|
|
239
415
|
// If user is from JWT payload (no password field), return as is
|
|
240
|
-
|
|
241
|
-
const response = formatter.success(user);
|
|
242
|
-
return c.json(response, response._status);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Remove sensitive data
|
|
246
|
-
const { password: _, ...userWithoutPassword } = user;
|
|
247
|
-
|
|
248
|
-
const response = formatter.success(userWithoutPassword);
|
|
416
|
+
const response = formatter.success(buildPublicUser(user));
|
|
249
417
|
return c.json(response, response._status);
|
|
250
418
|
}));
|
|
251
419
|
|
|
@@ -262,7 +430,7 @@ export function createAuthRoutes(authResource, config = {}) {
|
|
|
262
430
|
const newApiKey = generateApiKey();
|
|
263
431
|
|
|
264
432
|
// Update user
|
|
265
|
-
await
|
|
433
|
+
await authResource.update(user.id, {
|
|
266
434
|
apiKey: newApiKey
|
|
267
435
|
});
|
|
268
436
|
|
|
@@ -141,17 +141,30 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
// ✅ NEW: Check for partition filters from guards (set via ctx.setPartition())
|
|
145
|
+
const guardPartitionFilters = c.get('partitionFilters') || [];
|
|
146
|
+
|
|
144
147
|
let items;
|
|
145
148
|
let total;
|
|
146
149
|
|
|
147
|
-
//
|
|
148
|
-
if (
|
|
150
|
+
// ✅ Priority 1: Partition filters from guards (tenant isolation)
|
|
151
|
+
if (guardPartitionFilters.length > 0) {
|
|
152
|
+
const { partitionName, partitionFields } = guardPartitionFilters[0];
|
|
153
|
+
|
|
154
|
+
// Use partition query for O(1) tenant isolation
|
|
155
|
+
items = await resource.listPartition(partitionName, partitionFields, { limit, offset });
|
|
156
|
+
total = items.length;
|
|
157
|
+
}
|
|
158
|
+
// Priority 2: Use query if filters are present
|
|
159
|
+
else if (Object.keys(filters).length > 0) {
|
|
149
160
|
// Query with native offset support (efficient!)
|
|
150
161
|
items = await resource.query(filters, { limit, offset });
|
|
151
162
|
// Note: total is approximate (length of returned items)
|
|
152
163
|
// For exact total count with filters, would need separate count query
|
|
153
164
|
total = items.length;
|
|
154
|
-
}
|
|
165
|
+
}
|
|
166
|
+
// Priority 3: Partition from query params
|
|
167
|
+
else if (partition && partitionValues) {
|
|
155
168
|
// Query specific partition
|
|
156
169
|
items = await resource.listPartition({
|
|
157
170
|
partition,
|
|
@@ -160,7 +173,9 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
|
|
|
160
173
|
offset
|
|
161
174
|
});
|
|
162
175
|
total = items.length;
|
|
163
|
-
}
|
|
176
|
+
}
|
|
177
|
+
// Priority 4: Regular list (full scan)
|
|
178
|
+
else {
|
|
164
179
|
// Regular list
|
|
165
180
|
items = await resource.list({ limit, offset });
|
|
166
181
|
total = items.length;
|