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
package/src/concerns/ip.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import tryFn from './try-fn.js';
|
|
13
|
+
import { ValidationError } from '../errors.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Validate IPv4 address format
|
|
@@ -54,7 +55,12 @@ export function isValidIPv6(ip) {
|
|
|
54
55
|
*/
|
|
55
56
|
export function encodeIPv4(ip) {
|
|
56
57
|
if (!isValidIPv4(ip)) {
|
|
57
|
-
throw new
|
|
58
|
+
throw new ValidationError('Invalid IPv4 address', {
|
|
59
|
+
field: 'ip',
|
|
60
|
+
value: ip,
|
|
61
|
+
retriable: false,
|
|
62
|
+
suggestion: 'Provide a valid IPv4 address (e.g., "192.168.0.1").'
|
|
63
|
+
});
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
const octets = ip.split('.').map(octet => parseInt(octet, 10));
|
|
@@ -70,21 +76,38 @@ export function encodeIPv4(ip) {
|
|
|
70
76
|
*/
|
|
71
77
|
export function decodeIPv4(encoded) {
|
|
72
78
|
if (typeof encoded !== 'string') {
|
|
73
|
-
throw new
|
|
79
|
+
throw new ValidationError('Encoded IPv4 must be a string', {
|
|
80
|
+
field: 'encoded',
|
|
81
|
+
retriable: false,
|
|
82
|
+
suggestion: 'Pass the base64-encoded IPv4 string returned by encodeIPv4().'
|
|
83
|
+
});
|
|
74
84
|
}
|
|
75
85
|
|
|
76
86
|
const [ok, err, result] = tryFn(() => {
|
|
77
87
|
const buffer = Buffer.from(encoded, 'base64');
|
|
78
88
|
|
|
79
89
|
if (buffer.length !== 4) {
|
|
80
|
-
throw new
|
|
90
|
+
throw new ValidationError('Invalid encoded IPv4 length', {
|
|
91
|
+
field: 'encoded',
|
|
92
|
+
value: encoded,
|
|
93
|
+
retriable: false,
|
|
94
|
+
suggestion: 'Ensure the encoded IPv4 string was produced by encodeIPv4().'
|
|
95
|
+
});
|
|
81
96
|
}
|
|
82
97
|
|
|
83
98
|
return Array.from(buffer).join('.');
|
|
84
99
|
});
|
|
85
100
|
|
|
86
101
|
if (!ok) {
|
|
87
|
-
|
|
102
|
+
if (err instanceof ValidationError) {
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
throw new ValidationError('Failed to decode IPv4', {
|
|
106
|
+
field: 'encoded',
|
|
107
|
+
retriable: false,
|
|
108
|
+
suggestion: 'Confirm the value is a base64-encoded IPv4 string generated by encodeIPv4().',
|
|
109
|
+
original: err
|
|
110
|
+
});
|
|
88
111
|
}
|
|
89
112
|
|
|
90
113
|
return result;
|
|
@@ -97,7 +120,12 @@ export function decodeIPv4(encoded) {
|
|
|
97
120
|
*/
|
|
98
121
|
export function expandIPv6(ip) {
|
|
99
122
|
if (!isValidIPv6(ip)) {
|
|
100
|
-
throw new
|
|
123
|
+
throw new ValidationError('Invalid IPv6 address', {
|
|
124
|
+
field: 'ip',
|
|
125
|
+
value: ip,
|
|
126
|
+
retriable: false,
|
|
127
|
+
suggestion: 'Provide a valid IPv6 address (e.g., "2001:db8::1").'
|
|
128
|
+
});
|
|
101
129
|
}
|
|
102
130
|
|
|
103
131
|
// Handle :: expansion
|
|
@@ -196,7 +224,12 @@ export function compressIPv6(ip) {
|
|
|
196
224
|
*/
|
|
197
225
|
export function encodeIPv6(ip) {
|
|
198
226
|
if (!isValidIPv6(ip)) {
|
|
199
|
-
throw new
|
|
227
|
+
throw new ValidationError('Invalid IPv6 address', {
|
|
228
|
+
field: 'ip',
|
|
229
|
+
value: ip,
|
|
230
|
+
retriable: false,
|
|
231
|
+
suggestion: 'Provide a valid IPv6 address (e.g., "2001:db8::1").'
|
|
232
|
+
});
|
|
200
233
|
}
|
|
201
234
|
|
|
202
235
|
// Always encode for consistency (like IPv4)
|
|
@@ -229,7 +262,11 @@ export function encodeIPv6(ip) {
|
|
|
229
262
|
*/
|
|
230
263
|
export function decodeIPv6(encoded, compress = true) {
|
|
231
264
|
if (typeof encoded !== 'string') {
|
|
232
|
-
throw new
|
|
265
|
+
throw new ValidationError('Encoded IPv6 must be a string', {
|
|
266
|
+
field: 'encoded',
|
|
267
|
+
retriable: false,
|
|
268
|
+
suggestion: 'Pass the base64-encoded IPv6 string returned by encodeIPv6().'
|
|
269
|
+
});
|
|
233
270
|
}
|
|
234
271
|
|
|
235
272
|
// SMART DETECTION: Check if this is unencoded IPv6
|
|
@@ -245,7 +282,12 @@ export function decodeIPv6(encoded, compress = true) {
|
|
|
245
282
|
const buffer = Buffer.from(encoded, 'base64');
|
|
246
283
|
|
|
247
284
|
if (buffer.length !== 16) {
|
|
248
|
-
throw new
|
|
285
|
+
throw new ValidationError('Invalid encoded IPv6 length', {
|
|
286
|
+
field: 'encoded',
|
|
287
|
+
value: encoded,
|
|
288
|
+
retriable: false,
|
|
289
|
+
suggestion: 'Ensure the encoded IPv6 string was produced by encodeIPv6().'
|
|
290
|
+
});
|
|
249
291
|
}
|
|
250
292
|
|
|
251
293
|
const groups = [];
|
|
@@ -260,7 +302,15 @@ export function decodeIPv6(encoded, compress = true) {
|
|
|
260
302
|
});
|
|
261
303
|
|
|
262
304
|
if (!ok) {
|
|
263
|
-
|
|
305
|
+
if (err instanceof ValidationError) {
|
|
306
|
+
throw err;
|
|
307
|
+
}
|
|
308
|
+
throw new ValidationError('Failed to decode IPv6', {
|
|
309
|
+
field: 'encoded',
|
|
310
|
+
retriable: false,
|
|
311
|
+
suggestion: 'Confirm the value is a base64-encoded IPv6 string generated by encodeIPv6().',
|
|
312
|
+
original: err
|
|
313
|
+
});
|
|
264
314
|
}
|
|
265
315
|
|
|
266
316
|
return result;
|
package/src/concerns/money.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { encode, decode } from './base62.js';
|
|
19
|
+
import { ValidationError } from '../errors.js';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Currency decimal places (number of decimals in smallest unit)
|
|
@@ -100,7 +101,13 @@ export function encodeMoney(value, currency = 'USD') {
|
|
|
100
101
|
|
|
101
102
|
// Money cannot be negative (validation should happen at schema level)
|
|
102
103
|
if (value < 0) {
|
|
103
|
-
throw new
|
|
104
|
+
throw new ValidationError('Money value cannot be negative', {
|
|
105
|
+
field: 'value',
|
|
106
|
+
value,
|
|
107
|
+
statusCode: 400,
|
|
108
|
+
retriable: false,
|
|
109
|
+
suggestion: 'Provide a non-negative monetary value or store debts in a separate field.'
|
|
110
|
+
});
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
const decimals = getCurrencyDecimals(currency);
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import bcrypt from 'bcrypt';
|
|
10
|
+
import { ValidationError } from '../errors.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Hash a password using bcrypt (synchronous)
|
|
@@ -16,11 +17,21 @@ import bcrypt from 'bcrypt';
|
|
|
16
17
|
*/
|
|
17
18
|
export function hashPasswordSync(password, rounds = 10) {
|
|
18
19
|
if (!password || typeof password !== 'string') {
|
|
19
|
-
throw new
|
|
20
|
+
throw new ValidationError('Password must be a non-empty string', {
|
|
21
|
+
field: 'password',
|
|
22
|
+
statusCode: 400,
|
|
23
|
+
retriable: false,
|
|
24
|
+
suggestion: 'Provide a non-empty string before calling hashPasswordSync().'
|
|
25
|
+
});
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
if (rounds < 4 || rounds > 31) {
|
|
23
|
-
throw new
|
|
29
|
+
throw new ValidationError('Bcrypt rounds must be between 4 and 31', {
|
|
30
|
+
field: 'rounds',
|
|
31
|
+
statusCode: 400,
|
|
32
|
+
retriable: false,
|
|
33
|
+
suggestion: 'Configure bcrypt rounds between 4 and 31 (inclusive).'
|
|
34
|
+
});
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
return bcrypt.hashSync(password, rounds);
|
|
@@ -34,11 +45,21 @@ export function hashPasswordSync(password, rounds = 10) {
|
|
|
34
45
|
*/
|
|
35
46
|
export async function hashPassword(password, rounds = 10) {
|
|
36
47
|
if (!password || typeof password !== 'string') {
|
|
37
|
-
throw new
|
|
48
|
+
throw new ValidationError('Password must be a non-empty string', {
|
|
49
|
+
field: 'password',
|
|
50
|
+
statusCode: 400,
|
|
51
|
+
retriable: false,
|
|
52
|
+
suggestion: 'Provide a non-empty string before calling hashPassword().'
|
|
53
|
+
});
|
|
38
54
|
}
|
|
39
55
|
|
|
40
56
|
if (rounds < 4 || rounds > 31) {
|
|
41
|
-
throw new
|
|
57
|
+
throw new ValidationError('Bcrypt rounds must be between 4 and 31', {
|
|
58
|
+
field: 'rounds',
|
|
59
|
+
statusCode: 400,
|
|
60
|
+
retriable: false,
|
|
61
|
+
suggestion: 'Configure bcrypt rounds between 4 and 31 (inclusive).'
|
|
62
|
+
});
|
|
42
63
|
}
|
|
43
64
|
|
|
44
65
|
return await bcrypt.hash(password, rounds);
|
|
@@ -82,18 +103,33 @@ export async function verifyPassword(plaintext, hash) {
|
|
|
82
103
|
*/
|
|
83
104
|
export function compactHash(bcryptHash) {
|
|
84
105
|
if (!bcryptHash || typeof bcryptHash !== 'string') {
|
|
85
|
-
throw new
|
|
106
|
+
throw new ValidationError('Invalid bcrypt hash', {
|
|
107
|
+
field: 'bcryptHash',
|
|
108
|
+
statusCode: 400,
|
|
109
|
+
retriable: false,
|
|
110
|
+
suggestion: 'Provide a valid bcrypt hash generated by hashPassword().'
|
|
111
|
+
});
|
|
86
112
|
}
|
|
87
113
|
|
|
88
114
|
// Bcrypt format: $2a$10$ or $2b$10$ or $2y$10$
|
|
89
115
|
if (!bcryptHash.startsWith('$2')) {
|
|
90
|
-
throw new
|
|
116
|
+
throw new ValidationError('Not a valid bcrypt hash', {
|
|
117
|
+
field: 'bcryptHash',
|
|
118
|
+
statusCode: 400,
|
|
119
|
+
retriable: false,
|
|
120
|
+
suggestion: 'Ensure the hash starts with "$2" and was produced by bcrypt.'
|
|
121
|
+
});
|
|
91
122
|
}
|
|
92
123
|
|
|
93
124
|
// Remove prefix (e.g., "$2b$10$")
|
|
94
125
|
const parts = bcryptHash.split('$');
|
|
95
126
|
if (parts.length !== 4) {
|
|
96
|
-
throw new
|
|
127
|
+
throw new ValidationError('Invalid bcrypt hash format', {
|
|
128
|
+
field: 'bcryptHash',
|
|
129
|
+
statusCode: 400,
|
|
130
|
+
retriable: false,
|
|
131
|
+
suggestion: 'Provide a complete bcrypt hash (e.g., "$2b$10$...").'
|
|
132
|
+
});
|
|
97
133
|
}
|
|
98
134
|
|
|
99
135
|
// Return just the salt+hash part (last element after split)
|
|
@@ -109,7 +145,12 @@ export function compactHash(bcryptHash) {
|
|
|
109
145
|
*/
|
|
110
146
|
export function expandHash(compactHash, rounds = 10) {
|
|
111
147
|
if (!compactHash || typeof compactHash !== 'string') {
|
|
112
|
-
throw new
|
|
148
|
+
throw new ValidationError('Invalid compacted hash', {
|
|
149
|
+
field: 'compactHash',
|
|
150
|
+
statusCode: 400,
|
|
151
|
+
retriable: false,
|
|
152
|
+
suggestion: 'Provide a compacted hash returned from compactHash().'
|
|
153
|
+
});
|
|
113
154
|
}
|
|
114
155
|
|
|
115
156
|
// If it's already a full hash, return as-is
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import { metadataEncode, metadataDecode } from './metadata-encoding.js';
|
|
32
32
|
import { calculateEffectiveLimit, calculateUTF8Bytes } from './calculator.js';
|
|
33
33
|
import { tryFn } from './try-fn.js';
|
|
34
|
+
import { idGenerator } from './id.js';
|
|
34
35
|
import { PluginStorageError, MetadataLimitError, BehaviorError } from '../errors.js';
|
|
35
36
|
|
|
36
37
|
const S3_METADATA_LIMIT = 2047; // AWS S3 metadata limit in bytes
|
|
@@ -89,10 +90,16 @@ export class PluginStorage {
|
|
|
89
90
|
* @param {number} options.ttl - Time-to-live in seconds (optional)
|
|
90
91
|
* @param {string} options.behavior - 'body-overflow' | 'body-only' | 'enforce-limits'
|
|
91
92
|
* @param {string} options.contentType - Content type (default: application/json)
|
|
92
|
-
* @returns {Promise<
|
|
93
|
+
* @returns {Promise<Object>} Underlying client response (includes ETag when available)
|
|
93
94
|
*/
|
|
94
95
|
async set(key, data, options = {}) {
|
|
95
|
-
const {
|
|
96
|
+
const {
|
|
97
|
+
ttl,
|
|
98
|
+
behavior = 'body-overflow',
|
|
99
|
+
contentType = 'application/json',
|
|
100
|
+
ifMatch,
|
|
101
|
+
ifNoneMatch
|
|
102
|
+
} = options;
|
|
96
103
|
|
|
97
104
|
// Clone data to avoid mutating original
|
|
98
105
|
const dataToSave = { ...data };
|
|
@@ -117,8 +124,15 @@ export class PluginStorage {
|
|
|
117
124
|
putParams.body = JSON.stringify(body);
|
|
118
125
|
}
|
|
119
126
|
|
|
127
|
+
if (ifMatch !== undefined) {
|
|
128
|
+
putParams.ifMatch = ifMatch;
|
|
129
|
+
}
|
|
130
|
+
if (ifNoneMatch !== undefined) {
|
|
131
|
+
putParams.ifNoneMatch = ifNoneMatch;
|
|
132
|
+
}
|
|
133
|
+
|
|
120
134
|
// Save to S3
|
|
121
|
-
const [ok, err] = await tryFn(() => this.client.putObject(putParams));
|
|
135
|
+
const [ok, err, response] = await tryFn(() => this.client.putObject(putParams));
|
|
122
136
|
|
|
123
137
|
if (!ok) {
|
|
124
138
|
throw new PluginStorageError(`Failed to save plugin data`, {
|
|
@@ -131,6 +145,8 @@ export class PluginStorage {
|
|
|
131
145
|
suggestion: 'Check S3 permissions and key format'
|
|
132
146
|
});
|
|
133
147
|
}
|
|
148
|
+
|
|
149
|
+
return response;
|
|
134
150
|
}
|
|
135
151
|
|
|
136
152
|
/**
|
|
@@ -165,7 +181,17 @@ export class PluginStorage {
|
|
|
165
181
|
|
|
166
182
|
if (!ok) {
|
|
167
183
|
// If not found, return null
|
|
168
|
-
|
|
184
|
+
// Check multiple ways the error might indicate "not found":
|
|
185
|
+
// 1. error.name is 'NoSuchKey' (standard S3)
|
|
186
|
+
// 2. error.code is 'NoSuchKey' (ResourceError with code property)
|
|
187
|
+
// 3. error.Code is 'NoSuchKey' (AWS SDK format)
|
|
188
|
+
// 4. statusCode is 404
|
|
189
|
+
if (
|
|
190
|
+
err.name === 'NoSuchKey' ||
|
|
191
|
+
err.code === 'NoSuchKey' ||
|
|
192
|
+
err.Code === 'NoSuchKey' ||
|
|
193
|
+
err.statusCode === 404
|
|
194
|
+
) {
|
|
169
195
|
return null;
|
|
170
196
|
}
|
|
171
197
|
throw new PluginStorageError(`Failed to retrieve plugin data`, {
|
|
@@ -625,40 +651,172 @@ export class PluginStorage {
|
|
|
625
651
|
* @returns {Promise<Object|null>} Lock object or null if couldn't acquire
|
|
626
652
|
*/
|
|
627
653
|
async acquireLock(lockName, options = {}) {
|
|
628
|
-
const {
|
|
654
|
+
const {
|
|
655
|
+
ttl = 30,
|
|
656
|
+
timeout = 0,
|
|
657
|
+
workerId = 'unknown',
|
|
658
|
+
retryDelay = 100,
|
|
659
|
+
maxRetryDelay = 1000
|
|
660
|
+
} = options;
|
|
629
661
|
const key = this.getPluginKey(null, 'locks', lockName);
|
|
662
|
+
const token = idGenerator();
|
|
630
663
|
|
|
631
664
|
const startTime = Date.now();
|
|
665
|
+
let attempt = 0;
|
|
632
666
|
|
|
633
667
|
while (true) {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
668
|
+
const payload = {
|
|
669
|
+
workerId,
|
|
670
|
+
token,
|
|
671
|
+
acquiredAt: Date.now()
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
const [ok, err, putResponse] = await tryFn(() => this.set(key, payload, {
|
|
675
|
+
ttl,
|
|
676
|
+
behavior: 'body-only',
|
|
677
|
+
ifNoneMatch: '*'
|
|
678
|
+
}));
|
|
679
|
+
|
|
680
|
+
if (ok) {
|
|
681
|
+
return {
|
|
682
|
+
name: lockName,
|
|
683
|
+
key,
|
|
684
|
+
token,
|
|
685
|
+
workerId,
|
|
686
|
+
expiresAt: Date.now() + ttl * 1000,
|
|
687
|
+
etag: putResponse?.ETag || null
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const originalError = err?.original || err;
|
|
692
|
+
const errorCode = originalError?.code || originalError?.Code || originalError?.name;
|
|
693
|
+
const statusCode = originalError?.statusCode || originalError?.$metadata?.httpStatusCode;
|
|
694
|
+
const isPreconditionFailure = errorCode === 'PreconditionFailed' || statusCode === 412;
|
|
695
|
+
|
|
696
|
+
if (!isPreconditionFailure) {
|
|
697
|
+
throw err;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Check timeout (0 means don't wait, undefined means wait indefinitely)
|
|
701
|
+
if (timeout !== undefined && Date.now() - startTime >= timeout) {
|
|
702
|
+
return null;
|
|
639
703
|
}
|
|
640
704
|
|
|
641
|
-
//
|
|
642
|
-
|
|
643
|
-
|
|
705
|
+
// Remove expired locks (get deletes expired entries automatically)
|
|
706
|
+
const current = await this.get(key);
|
|
707
|
+
if (!current) {
|
|
708
|
+
continue; // Lock expired - retry immediately
|
|
644
709
|
}
|
|
645
710
|
|
|
646
|
-
|
|
647
|
-
|
|
711
|
+
attempt += 1;
|
|
712
|
+
const delay = this._computeBackoff(attempt, retryDelay, maxRetryDelay);
|
|
713
|
+
await this._sleep(delay);
|
|
648
714
|
}
|
|
649
715
|
}
|
|
650
716
|
|
|
651
717
|
/**
|
|
652
718
|
* Release a distributed lock
|
|
653
719
|
*
|
|
654
|
-
* @param {string}
|
|
720
|
+
* @param {Object|string} lock - Lock object returned by acquireLock or lock name
|
|
721
|
+
* @param {string} [token] - Lock token (required when passing lock name)
|
|
655
722
|
* @returns {Promise<void>}
|
|
656
723
|
*/
|
|
657
|
-
async releaseLock(
|
|
658
|
-
|
|
724
|
+
async releaseLock(lock, token) {
|
|
725
|
+
if (!lock) return;
|
|
726
|
+
|
|
727
|
+
let lockName;
|
|
728
|
+
let key;
|
|
729
|
+
let expectedToken = token;
|
|
730
|
+
|
|
731
|
+
if (typeof lock === 'object') {
|
|
732
|
+
lockName = lock.name || lock.lockName;
|
|
733
|
+
key = lock.key || (lockName ? this.getPluginKey(null, 'locks', lockName) : null);
|
|
734
|
+
expectedToken = lock.token ?? token;
|
|
735
|
+
if (!expectedToken && lock.token !== undefined) {
|
|
736
|
+
throw new PluginStorageError('Lock token missing on lock object', {
|
|
737
|
+
pluginSlug: this.pluginSlug,
|
|
738
|
+
operation: 'releaseLock',
|
|
739
|
+
lockName
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
} else if (typeof lock === 'string') {
|
|
743
|
+
lockName = lock;
|
|
744
|
+
key = this.getPluginKey(null, 'locks', lockName);
|
|
745
|
+
expectedToken = token;
|
|
746
|
+
if (!expectedToken) {
|
|
747
|
+
throw new PluginStorageError('releaseLock(lockName) now requires the lock token', {
|
|
748
|
+
pluginSlug: this.pluginSlug,
|
|
749
|
+
operation: 'releaseLock',
|
|
750
|
+
lockName,
|
|
751
|
+
suggestion: 'Pass the original lock object or provide the token explicitly'
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
} else {
|
|
755
|
+
throw new PluginStorageError('releaseLock expects a lock object or lock name', {
|
|
756
|
+
pluginSlug: this.pluginSlug,
|
|
757
|
+
operation: 'releaseLock'
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (!key) {
|
|
762
|
+
throw new PluginStorageError('Invalid lock key', {
|
|
763
|
+
pluginSlug: this.pluginSlug,
|
|
764
|
+
operation: 'releaseLock'
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const current = await this.get(key);
|
|
769
|
+
if (!current) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (current.token !== undefined) {
|
|
774
|
+
if (!expectedToken) {
|
|
775
|
+
throw new PluginStorageError('releaseLock detected a stored token but none was provided', {
|
|
776
|
+
pluginSlug: this.pluginSlug,
|
|
777
|
+
operation: 'releaseLock',
|
|
778
|
+
lockName,
|
|
779
|
+
suggestion: 'Always release using the lock object returned by acquireLock'
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
if (current.token !== expectedToken) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
659
787
|
await this.delete(key);
|
|
660
788
|
}
|
|
661
789
|
|
|
790
|
+
/**
|
|
791
|
+
* Acquire a lock, execute a callback, and release automatically.
|
|
792
|
+
*
|
|
793
|
+
* @param {string} lockName - Lock identifier
|
|
794
|
+
* @param {Object} options - Options forwarded to acquireLock
|
|
795
|
+
* @param {Function} callback - Async function to execute while holding the lock
|
|
796
|
+
* @returns {Promise<*>} Callback result, or null when lock not acquired
|
|
797
|
+
*/
|
|
798
|
+
async withLock(lockName, options, callback) {
|
|
799
|
+
if (typeof callback !== 'function') {
|
|
800
|
+
throw new PluginStorageError('withLock requires a callback function', {
|
|
801
|
+
pluginSlug: this.pluginSlug,
|
|
802
|
+
operation: 'withLock',
|
|
803
|
+
lockName,
|
|
804
|
+
suggestion: 'Pass an async function as the third argument'
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const lock = await this.acquireLock(lockName, options);
|
|
809
|
+
if (!lock) {
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
try {
|
|
814
|
+
return await callback(lock);
|
|
815
|
+
} finally {
|
|
816
|
+
await tryFn(() => this.releaseLock(lock));
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
662
820
|
/**
|
|
663
821
|
* Check if a lock is currently held
|
|
664
822
|
*
|
|
@@ -671,6 +829,16 @@ export class PluginStorage {
|
|
|
671
829
|
return lock !== null;
|
|
672
830
|
}
|
|
673
831
|
|
|
832
|
+
_computeBackoff(attempt, baseDelay, maxDelay) {
|
|
833
|
+
const exponential = Math.min(baseDelay * Math.pow(2, Math.max(attempt - 1, 0)), maxDelay);
|
|
834
|
+
const jitter = Math.floor(Math.random() * Math.max(baseDelay / 2, 1));
|
|
835
|
+
return exponential + jitter;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
_sleep(ms) {
|
|
839
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
840
|
+
}
|
|
841
|
+
|
|
674
842
|
/**
|
|
675
843
|
* Increment a counter value
|
|
676
844
|
*
|