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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Plugin } from "./plugin.class.js";
|
|
2
2
|
import tryFn from "../concerns/try-fn.js";
|
|
3
|
+
import { PluginError } from "../errors.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* GeoPlugin - Geospatial Queries and Location-Based Features
|
|
@@ -137,9 +138,14 @@ export class GeoPlugin extends Plugin {
|
|
|
137
138
|
|
|
138
139
|
// Validate configuration
|
|
139
140
|
if (!config.latField || !config.lonField) {
|
|
140
|
-
throw new
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
throw new PluginError(`[GeoPlugin] Resource "${resourceName}" must have "latField" and "lonField" configured`, {
|
|
142
|
+
pluginName: 'GeoPlugin',
|
|
143
|
+
operation: 'setupResource',
|
|
144
|
+
resourceName,
|
|
145
|
+
statusCode: 400,
|
|
146
|
+
retriable: false,
|
|
147
|
+
suggestion: 'Update GeoPlugin configuration with { latField: "...", lonField: "..." } for the resource.'
|
|
148
|
+
});
|
|
143
149
|
}
|
|
144
150
|
|
|
145
151
|
if (!config.precision || config.precision < 1 || config.precision > 12) {
|
|
@@ -327,7 +333,14 @@ export class GeoPlugin extends Plugin {
|
|
|
327
333
|
*/
|
|
328
334
|
resource.findNearby = async function({ lat, lon, radius = 10, limit = 100 }) {
|
|
329
335
|
if (lat === undefined || lon === undefined) {
|
|
330
|
-
throw new
|
|
336
|
+
throw new PluginError('Latitude and longitude are required for findNearby()', {
|
|
337
|
+
pluginName: 'GeoPlugin',
|
|
338
|
+
operation: 'findNearby',
|
|
339
|
+
resourceName: resource.name,
|
|
340
|
+
statusCode: 400,
|
|
341
|
+
retriable: false,
|
|
342
|
+
suggestion: 'Call findNearby({ lat, lon, radius }) with both coordinates.'
|
|
343
|
+
});
|
|
331
344
|
}
|
|
332
345
|
|
|
333
346
|
const longitude = lon; // Alias for internal use
|
|
@@ -430,7 +443,14 @@ export class GeoPlugin extends Plugin {
|
|
|
430
443
|
*/
|
|
431
444
|
resource.findInBounds = async function({ north, south, east, west, limit = 100 }) {
|
|
432
445
|
if (north === undefined || south === undefined || east === undefined || west === undefined) {
|
|
433
|
-
throw new
|
|
446
|
+
throw new PluginError('Bounding box requires north, south, east, west coordinates', {
|
|
447
|
+
pluginName: 'GeoPlugin',
|
|
448
|
+
operation: 'findInBounds',
|
|
449
|
+
resourceName: resource.name,
|
|
450
|
+
statusCode: 400,
|
|
451
|
+
retriable: false,
|
|
452
|
+
suggestion: 'Call findInBounds({ north, south, east, west }) with all four boundaries.'
|
|
453
|
+
});
|
|
434
454
|
}
|
|
435
455
|
|
|
436
456
|
let allRecords = [];
|
|
@@ -536,13 +556,30 @@ export class GeoPlugin extends Plugin {
|
|
|
536
556
|
]);
|
|
537
557
|
} catch (err) {
|
|
538
558
|
if (err.name === 'NoSuchKey' || err.message?.includes('No such key')) {
|
|
539
|
-
throw new
|
|
559
|
+
throw new PluginError('One or both records not found for distance calculation', {
|
|
560
|
+
pluginName: 'GeoPlugin',
|
|
561
|
+
operation: 'getDistance',
|
|
562
|
+
resourceName: resource.name,
|
|
563
|
+
statusCode: 404,
|
|
564
|
+
retriable: false,
|
|
565
|
+
suggestion: 'Ensure both record IDs exist before calling getDistance().',
|
|
566
|
+
ids: [id1, id2],
|
|
567
|
+
original: err
|
|
568
|
+
});
|
|
540
569
|
}
|
|
541
570
|
throw err;
|
|
542
571
|
}
|
|
543
572
|
|
|
544
573
|
if (!record1 || !record2) {
|
|
545
|
-
throw new
|
|
574
|
+
throw new PluginError('One or both records not found for distance calculation', {
|
|
575
|
+
pluginName: 'GeoPlugin',
|
|
576
|
+
operation: 'getDistance',
|
|
577
|
+
resourceName: resource.name,
|
|
578
|
+
statusCode: 404,
|
|
579
|
+
retriable: false,
|
|
580
|
+
suggestion: 'Ensure both record IDs exist before calling getDistance().',
|
|
581
|
+
ids: [id1, id2]
|
|
582
|
+
});
|
|
546
583
|
}
|
|
547
584
|
|
|
548
585
|
const lat1 = record1[config.latField];
|
|
@@ -551,7 +588,15 @@ export class GeoPlugin extends Plugin {
|
|
|
551
588
|
const lon2 = record2[config.lonField];
|
|
552
589
|
|
|
553
590
|
if (lat1 === undefined || lon1 === undefined || lat2 === undefined || lon2 === undefined) {
|
|
554
|
-
throw new
|
|
591
|
+
throw new PluginError('One or both records are missing coordinates', {
|
|
592
|
+
pluginName: 'GeoPlugin',
|
|
593
|
+
operation: 'getDistance',
|
|
594
|
+
resourceName: resource.name,
|
|
595
|
+
statusCode: 422,
|
|
596
|
+
retriable: false,
|
|
597
|
+
suggestion: `Check that both records contain ${config.latField} and ${config.lonField} before using geospatial helpers.`,
|
|
598
|
+
ids: [id1, id2]
|
|
599
|
+
});
|
|
555
600
|
}
|
|
556
601
|
|
|
557
602
|
const distance = plugin.calculateDistance(lat1, lon1, lat2, lon2);
|
|
@@ -635,7 +680,14 @@ export class GeoPlugin extends Plugin {
|
|
|
635
680
|
const idx = this.base32.indexOf(chr);
|
|
636
681
|
|
|
637
682
|
if (idx === -1) {
|
|
638
|
-
throw new
|
|
683
|
+
throw new PluginError(`Invalid geohash character: ${chr}`, {
|
|
684
|
+
pluginName: 'GeoPlugin',
|
|
685
|
+
operation: 'decodeGeohash',
|
|
686
|
+
statusCode: 400,
|
|
687
|
+
retriable: false,
|
|
688
|
+
suggestion: 'Ensure geohash strings use the base32 alphabet 0123456789bcdefghjkmnpqrstuvwxyz.',
|
|
689
|
+
geohash
|
|
690
|
+
});
|
|
639
691
|
}
|
|
640
692
|
|
|
641
693
|
for (let n = 4; n >= 0; n--) {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BASE_USER_ATTRIBUTES,
|
|
3
|
+
BASE_TENANT_ATTRIBUTES,
|
|
4
|
+
BASE_CLIENT_ATTRIBUTES,
|
|
5
|
+
validateResourcesConfig,
|
|
6
|
+
mergeResourceConfig
|
|
7
|
+
} from './resource-schemas.js';
|
|
8
|
+
import { PluginError } from '../../../errors.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validate and normalize user-provided resource configurations.
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} resourcesOptions
|
|
14
|
+
* @returns {{users: Object, tenants: Object, clients: Object}}
|
|
15
|
+
*/
|
|
16
|
+
export function prepareResourceConfigs(resourcesOptions = {}) {
|
|
17
|
+
const resourcesValidation = validateResourcesConfig(resourcesOptions);
|
|
18
|
+
if (!resourcesValidation.valid) {
|
|
19
|
+
throw new PluginError('IdentityPlugin configuration error', {
|
|
20
|
+
pluginName: 'IdentityPlugin',
|
|
21
|
+
operation: 'prepareResourceConfigs',
|
|
22
|
+
statusCode: 400,
|
|
23
|
+
retriable: false,
|
|
24
|
+
suggestion: 'Review the resources configuration and fix the validation errors listed in the metadata.',
|
|
25
|
+
metadata: { validationErrors: resourcesValidation.errors }
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
mergeResourceConfig(
|
|
30
|
+
{ attributes: BASE_USER_ATTRIBUTES },
|
|
31
|
+
resourcesOptions.users,
|
|
32
|
+
'users'
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
mergeResourceConfig(
|
|
36
|
+
{ attributes: BASE_TENANT_ATTRIBUTES },
|
|
37
|
+
resourcesOptions.tenants,
|
|
38
|
+
'tenants'
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
mergeResourceConfig(
|
|
42
|
+
{ attributes: BASE_CLIENT_ATTRIBUTES },
|
|
43
|
+
resourcesOptions.clients,
|
|
44
|
+
'clients'
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
users: {
|
|
49
|
+
userConfig: resourcesOptions.users,
|
|
50
|
+
mergedConfig: null
|
|
51
|
+
},
|
|
52
|
+
tenants: {
|
|
53
|
+
userConfig: resourcesOptions.tenants,
|
|
54
|
+
mergedConfig: null
|
|
55
|
+
},
|
|
56
|
+
clients: {
|
|
57
|
+
userConfig: resourcesOptions.clients,
|
|
58
|
+
mergedConfig: null
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
import { requirePluginDependency } from '../../concerns/plugin-dependencies.js';
|
|
32
32
|
import { idGenerator } from '../../../concerns/id.js';
|
|
33
|
+
import { PluginError } from '../../../errors.js';
|
|
33
34
|
|
|
34
35
|
export class MFAManager {
|
|
35
36
|
constructor(options = {}) {
|
|
@@ -64,7 +65,13 @@ export class MFAManager {
|
|
|
64
65
|
*/
|
|
65
66
|
generateEnrollment(accountName) {
|
|
66
67
|
if (!this.OTPAuth) {
|
|
67
|
-
throw new
|
|
68
|
+
throw new PluginError('[MFA] OTPAuth library not initialized', {
|
|
69
|
+
pluginName: 'IdentityPlugin',
|
|
70
|
+
operation: 'mfaGenerateEnrollment',
|
|
71
|
+
statusCode: 500,
|
|
72
|
+
retriable: true,
|
|
73
|
+
suggestion: 'Call MFAManager.initialize() before generating enrollments or ensure otpauth dependency installs successfully.'
|
|
74
|
+
});
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
// Generate TOTP secret
|
|
@@ -103,7 +110,13 @@ export class MFAManager {
|
|
|
103
110
|
*/
|
|
104
111
|
verifyTOTP(secret, token) {
|
|
105
112
|
if (!this.OTPAuth) {
|
|
106
|
-
throw new
|
|
113
|
+
throw new PluginError('[MFA] OTPAuth library not initialized', {
|
|
114
|
+
pluginName: 'IdentityPlugin',
|
|
115
|
+
operation: 'mfaVerify',
|
|
116
|
+
statusCode: 500,
|
|
117
|
+
retriable: true,
|
|
118
|
+
suggestion: 'Initialize MFAManager before verifying codes and confirm otpauth dependency is available.'
|
|
119
|
+
});
|
|
107
120
|
}
|
|
108
121
|
|
|
109
122
|
try {
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sliding window rate limiter for IP-based throttling
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class RateLimiter {
|
|
6
|
+
/**
|
|
7
|
+
* @param {Object} options
|
|
8
|
+
* @param {number} options.windowMs - Window size in milliseconds
|
|
9
|
+
* @param {number} options.max - Maximum number of hits allowed per window
|
|
10
|
+
*/
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.windowMs = options.windowMs ?? 60000;
|
|
13
|
+
this.max = options.max ?? 10;
|
|
14
|
+
this.buckets = new Map(); // key -> { count, expiresAt }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Consume a token for the given key
|
|
19
|
+
* @param {string} key - Identifier (usually IP address)
|
|
20
|
+
* @returns {{allowed: boolean, remaining: number, retryAfter: number}}
|
|
21
|
+
*/
|
|
22
|
+
consume(key) {
|
|
23
|
+
if (!this.enabled()) {
|
|
24
|
+
return { allowed: true, remaining: Infinity, retryAfter: 0 };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const bucket = this.buckets.get(key);
|
|
29
|
+
|
|
30
|
+
if (!bucket || bucket.expiresAt <= now) {
|
|
31
|
+
const expiresAt = now + this.windowMs;
|
|
32
|
+
this.buckets.set(key, { count: 1, expiresAt });
|
|
33
|
+
this._prune(now);
|
|
34
|
+
return {
|
|
35
|
+
allowed: true,
|
|
36
|
+
remaining: Math.max(this.max - 1, 0),
|
|
37
|
+
retryAfter: 0
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (bucket.count < this.max) {
|
|
42
|
+
bucket.count += 1;
|
|
43
|
+
this._prune(now);
|
|
44
|
+
return {
|
|
45
|
+
allowed: true,
|
|
46
|
+
remaining: Math.max(this.max - bucket.count, 0),
|
|
47
|
+
retryAfter: 0
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const retryAfterMs = bucket.expiresAt - now;
|
|
52
|
+
return {
|
|
53
|
+
allowed: false,
|
|
54
|
+
remaining: 0,
|
|
55
|
+
retryAfter: Math.max(Math.ceil(retryAfterMs / 1000), 1)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether the limiter is active
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
enabled() {
|
|
64
|
+
return this.max > 0 && this.windowMs > 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Periodically remove expired buckets
|
|
69
|
+
* @private
|
|
70
|
+
*/
|
|
71
|
+
_prune(now) {
|
|
72
|
+
if (this.buckets.size > 5000) {
|
|
73
|
+
for (const [key, bucket] of this.buckets.entries()) {
|
|
74
|
+
if (bucket.expiresAt <= now) {
|
|
75
|
+
this.buckets.delete(key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Create Hono middleware for API-style responses
|
|
84
|
+
* @param {RateLimiter} limiter
|
|
85
|
+
* @param {(c: import('hono').Context) => string} getKey
|
|
86
|
+
*/
|
|
87
|
+
export function createJsonRateLimitMiddleware(limiter, getKey) {
|
|
88
|
+
return async (c, next) => {
|
|
89
|
+
const key = getKey(c);
|
|
90
|
+
const result = limiter.consume(key);
|
|
91
|
+
|
|
92
|
+
if (result.allowed) {
|
|
93
|
+
return await next();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
c.header('Retry-After', String(result.retryAfter));
|
|
97
|
+
return c.json({
|
|
98
|
+
error: 'too_many_requests',
|
|
99
|
+
error_description: `Too many requests. Try again in ${result.retryAfter} seconds.`
|
|
100
|
+
}, 429);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Create Hono middleware for browser redirect flows (login UI)
|
|
106
|
+
* @param {RateLimiter} limiter
|
|
107
|
+
* @param {(c: import('hono').Context) => string} getKey
|
|
108
|
+
* @param {(retryAfter: number) => string} buildRedirectUrl
|
|
109
|
+
*/
|
|
110
|
+
export function createRedirectRateLimitMiddleware(limiter, getKey, buildRedirectUrl) {
|
|
111
|
+
return async (c, next) => {
|
|
112
|
+
const key = getKey(c);
|
|
113
|
+
const result = limiter.consume(key);
|
|
114
|
+
|
|
115
|
+
if (result.allowed) {
|
|
116
|
+
return await next();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const url = buildRedirectUrl(result.retryAfter);
|
|
120
|
+
return c.redirect(url, 302);
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default RateLimiter;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* These are the REQUIRED attributes that the Identity Plugin needs to function.
|
|
5
5
|
* Users can extend these with custom attributes, but cannot override base fields.
|
|
6
6
|
*/
|
|
7
|
+
import { PluginError } from '../../../errors.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Base attributes for Users resource
|
|
@@ -189,7 +190,14 @@ export function mergeResourceConfig(baseConfig, userConfig = {}, resourceType) {
|
|
|
189
190
|
`Invalid extra attributes for ${resourceType} resource:`,
|
|
190
191
|
...validation.errors.map(err => ` - ${err}`)
|
|
191
192
|
].join('\n');
|
|
192
|
-
throw new
|
|
193
|
+
throw new PluginError('Invalid extra attributes for identity resource', {
|
|
194
|
+
pluginName: 'IdentityPlugin',
|
|
195
|
+
operation: 'mergeResourceConfig',
|
|
196
|
+
statusCode: 400,
|
|
197
|
+
retriable: false,
|
|
198
|
+
suggestion: 'Update the resource schema to match IdentityPlugin validation requirements.',
|
|
199
|
+
description: errorMsg
|
|
200
|
+
});
|
|
193
201
|
}
|
|
194
202
|
}
|
|
195
203
|
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { randomBytes } from 'crypto';
|
|
12
12
|
import { idGenerator } from '../../../concerns/id.js';
|
|
13
|
+
import { PluginError } from '../../../errors.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Generate a secure random token
|
|
@@ -31,7 +32,13 @@ export function generateToken(bytes = 32, encoding = 'hex') {
|
|
|
31
32
|
return buffer.toString('base64url');
|
|
32
33
|
|
|
33
34
|
default:
|
|
34
|
-
throw new
|
|
35
|
+
throw new PluginError(`Invalid encoding: ${encoding}. Use 'hex', 'base64', or 'base64url'.`, {
|
|
36
|
+
pluginName: 'IdentityPlugin',
|
|
37
|
+
operation: 'generateToken',
|
|
38
|
+
statusCode: 400,
|
|
39
|
+
retriable: false,
|
|
40
|
+
suggestion: 'Pass encoding as "hex", "base64", or "base64url" when calling generateToken.'
|
|
41
|
+
});
|
|
35
42
|
}
|
|
36
43
|
}
|
|
37
44
|
|
|
@@ -122,7 +129,13 @@ export function calculateExpiration(duration) {
|
|
|
122
129
|
const match = duration.match(/^(\d+)([smhd])$/);
|
|
123
130
|
|
|
124
131
|
if (!match) {
|
|
125
|
-
throw new
|
|
132
|
+
throw new PluginError(`Invalid duration format: ${duration}. Use '15m', '1h', '7d', etc.`, {
|
|
133
|
+
pluginName: 'IdentityPlugin',
|
|
134
|
+
operation: 'calculateExpiration',
|
|
135
|
+
statusCode: 400,
|
|
136
|
+
retriable: false,
|
|
137
|
+
suggestion: 'Provide durations using number + unit (s, m, h, d), for example "30m" or "1d".'
|
|
138
|
+
});
|
|
126
139
|
}
|
|
127
140
|
|
|
128
141
|
const value = parseInt(match[1], 10);
|
|
@@ -134,10 +147,22 @@ export function calculateExpiration(duration) {
|
|
|
134
147
|
case 'h': ms = value * 60 * 60 * 1000; break; // hours
|
|
135
148
|
case 'd': ms = value * 24 * 60 * 60 * 1000; break; // days
|
|
136
149
|
default:
|
|
137
|
-
throw new
|
|
150
|
+
throw new PluginError(`Invalid duration unit: ${unit}`, {
|
|
151
|
+
pluginName: 'IdentityPlugin',
|
|
152
|
+
operation: 'calculateExpiration',
|
|
153
|
+
statusCode: 400,
|
|
154
|
+
retriable: false,
|
|
155
|
+
suggestion: 'Use s, m, h, or d for seconds, minutes, hours, or days respectively.'
|
|
156
|
+
});
|
|
138
157
|
}
|
|
139
158
|
} else {
|
|
140
|
-
throw new
|
|
159
|
+
throw new PluginError('Duration must be a string or number', {
|
|
160
|
+
pluginName: 'IdentityPlugin',
|
|
161
|
+
operation: 'calculateExpiration',
|
|
162
|
+
statusCode: 400,
|
|
163
|
+
retriable: false,
|
|
164
|
+
suggestion: 'Pass durations as milliseconds (number) or a string like "15m".'
|
|
165
|
+
});
|
|
141
166
|
}
|
|
142
167
|
|
|
143
168
|
return Date.now() + ms;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { PluginError } from '../../../errors.js';
|
|
2
|
+
|
|
3
|
+
export class AuthDriver {
|
|
4
|
+
constructor(name, supportedTypes = []) {
|
|
5
|
+
this.name = name;
|
|
6
|
+
this.supportedTypes = supportedTypes;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Called once during plugin initialization with database/config/resources
|
|
11
|
+
* @param {Object} context
|
|
12
|
+
*/
|
|
13
|
+
// eslint-disable-next-line no-unused-vars
|
|
14
|
+
async initialize(context) {
|
|
15
|
+
throw new PluginError('AuthDriver.initialize(context) must be implemented by subclasses', {
|
|
16
|
+
pluginName: 'IdentityPlugin',
|
|
17
|
+
operation: 'initializeDriver',
|
|
18
|
+
statusCode: 500,
|
|
19
|
+
retriable: false,
|
|
20
|
+
suggestion: `Implement initialize(context) in ${this.constructor.name} or use one of the provided drivers.`
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Authenticate a request (password, client credentials, etc)
|
|
26
|
+
* @param {Object} request
|
|
27
|
+
*/
|
|
28
|
+
// eslint-disable-next-line no-unused-vars
|
|
29
|
+
async authenticate(request) {
|
|
30
|
+
throw new PluginError('AuthDriver.authenticate(request) must be implemented by subclasses', {
|
|
31
|
+
pluginName: 'IdentityPlugin',
|
|
32
|
+
operation: 'authenticateDriver',
|
|
33
|
+
statusCode: 500,
|
|
34
|
+
retriable: false,
|
|
35
|
+
suggestion: `Implement authenticate(request) in ${this.constructor.name} to support the configured grant type.`
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
supportsType(type) {
|
|
40
|
+
if (!type) return false;
|
|
41
|
+
return this.supportedTypes.includes(type);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Whether the driver supports issuing tokens for the given grant type
|
|
46
|
+
* @param {string} grantType
|
|
47
|
+
*/
|
|
48
|
+
// eslint-disable-next-line no-unused-vars
|
|
49
|
+
supportsGrant(grantType) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Optionally issue tokens (if driver is responsible for it)
|
|
55
|
+
* @param {Object} payload
|
|
56
|
+
*/
|
|
57
|
+
// eslint-disable-next-line no-unused-vars
|
|
58
|
+
async issueTokens(payload) {
|
|
59
|
+
throw new PluginError(`AuthDriver ${this.name} does not implement issueTokens`, {
|
|
60
|
+
pluginName: 'IdentityPlugin',
|
|
61
|
+
operation: 'issueTokens',
|
|
62
|
+
statusCode: 500,
|
|
63
|
+
retriable: false,
|
|
64
|
+
suggestion: 'Provide an issueTokens implementation or delegate token issuance to the OAuth2 server.'
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Optionally revoke tokens (if driver manages them)
|
|
70
|
+
* @param {Object} payload
|
|
71
|
+
*/
|
|
72
|
+
// eslint-disable-next-line no-unused-vars
|
|
73
|
+
async revokeTokens(payload) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { AuthDriver } from './auth-driver.interface.js';
|
|
2
|
+
import { PluginError } from '../../../errors.js';
|
|
3
|
+
import { tryFn } from '../../../concerns/try-fn.js';
|
|
4
|
+
|
|
5
|
+
function constantTimeEqual(a, b) {
|
|
6
|
+
const valueA = Buffer.from(String(a ?? ''), 'utf8');
|
|
7
|
+
const valueB = Buffer.from(String(b ?? ''), 'utf8');
|
|
8
|
+
|
|
9
|
+
if (valueA.length !== valueB.length) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let mismatch = 0;
|
|
14
|
+
for (let i = 0; i < valueA.length; i += 1) {
|
|
15
|
+
mismatch |= valueA[i] ^ valueB[i];
|
|
16
|
+
}
|
|
17
|
+
return mismatch === 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class ClientCredentialsAuthDriver extends AuthDriver {
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
super('client-credentials', ['client_credentials']);
|
|
23
|
+
this.options = options;
|
|
24
|
+
this.clientResource = null;
|
|
25
|
+
this.passwordHelper = null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async initialize(context) {
|
|
29
|
+
this.clientResource = context.resources?.clients;
|
|
30
|
+
if (!this.clientResource) {
|
|
31
|
+
throw new PluginError('ClientCredentialsAuthDriver requires clients resource', {
|
|
32
|
+
pluginName: 'IdentityPlugin',
|
|
33
|
+
operation: 'initializeClientCredentialsDriver',
|
|
34
|
+
statusCode: 500,
|
|
35
|
+
retriable: false,
|
|
36
|
+
suggestion: 'Map a clients resource in IdentityPlugin resources before enabling client credentials authentication.'
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
this.passwordHelper = context.helpers?.password || null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
supportsGrant(grantType) {
|
|
43
|
+
return grantType === 'client_credentials';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async authenticate(request) {
|
|
47
|
+
const { clientId, clientSecret } = request;
|
|
48
|
+
if (!clientId || !clientSecret) {
|
|
49
|
+
return { success: false, error: 'invalid_client', statusCode: 401 };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const [ok, err, clients] = await tryFn(() => this.clientResource.query({ clientId }));
|
|
53
|
+
if (!ok) {
|
|
54
|
+
return {
|
|
55
|
+
success: false,
|
|
56
|
+
error: err?.message || 'lookup_failed',
|
|
57
|
+
statusCode: 500
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!clients || clients.length === 0) {
|
|
62
|
+
return { success: false, error: 'invalid_client', statusCode: 401 };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const client = clients[0];
|
|
66
|
+
|
|
67
|
+
if (client.active === false) {
|
|
68
|
+
return { success: false, error: 'inactive_client', statusCode: 403 };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const secrets = [];
|
|
72
|
+
if (Array.isArray(client.secrets)) {
|
|
73
|
+
secrets.push(...client.secrets);
|
|
74
|
+
}
|
|
75
|
+
if (client.clientSecret) {
|
|
76
|
+
secrets.push(client.clientSecret);
|
|
77
|
+
}
|
|
78
|
+
if (client.secret) {
|
|
79
|
+
secrets.push(client.secret);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!secrets.length) {
|
|
83
|
+
return { success: false, error: 'invalid_client', statusCode: 401 };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const secretMatches = await this._verifyAgainstSecrets(clientSecret, secrets);
|
|
87
|
+
if (!secretMatches) {
|
|
88
|
+
return { success: false, error: 'invalid_client', statusCode: 401 };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const sanitizedClient = { ...client };
|
|
92
|
+
if (sanitizedClient.clientSecret !== undefined) {
|
|
93
|
+
delete sanitizedClient.clientSecret;
|
|
94
|
+
}
|
|
95
|
+
if (sanitizedClient.secret !== undefined) {
|
|
96
|
+
delete sanitizedClient.secret;
|
|
97
|
+
}
|
|
98
|
+
if (sanitizedClient.secrets !== undefined) {
|
|
99
|
+
delete sanitizedClient.secrets;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
success: true,
|
|
104
|
+
client: sanitizedClient
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async _verifyAgainstSecrets(providedSecret, storedSecrets) {
|
|
109
|
+
for (const storedSecret of storedSecrets) {
|
|
110
|
+
if (!storedSecret) continue;
|
|
111
|
+
|
|
112
|
+
if (typeof storedSecret === 'string' && storedSecret.startsWith('$') && this.passwordHelper?.verify) {
|
|
113
|
+
const ok = await this.passwordHelper.verify(providedSecret, storedSecret);
|
|
114
|
+
if (ok) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (constantTimeEqual(providedSecret, storedSecret)) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { PasswordAuthDriver } from './password-driver.js';
|
|
2
|
+
import { ClientCredentialsAuthDriver } from './client-credentials-driver.js';
|
|
3
|
+
|
|
4
|
+
export function createBuiltInAuthDrivers(options = {}) {
|
|
5
|
+
const drivers = [];
|
|
6
|
+
|
|
7
|
+
if (options.disablePassword !== true) {
|
|
8
|
+
drivers.push(new PasswordAuthDriver(options.password || {}));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (options.disableClientCredentials !== true) {
|
|
12
|
+
drivers.push(new ClientCredentialsAuthDriver(options.clientCredentials || {}));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return drivers;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { PasswordAuthDriver, ClientCredentialsAuthDriver };
|