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
|
@@ -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;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HealthManager - Manages health check endpoints
|
|
3
|
+
*
|
|
4
|
+
* Provides Kubernetes-compatible health endpoints:
|
|
5
|
+
* - /health - Generic health check
|
|
6
|
+
* - /health/live - Liveness probe (is app alive?)
|
|
7
|
+
* - /health/ready - Readiness probe (is app ready for traffic?)
|
|
8
|
+
*
|
|
9
|
+
* Supports custom health checks for external dependencies (database, redis, etc.)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as formatter from '../../shared/response-formatter.js';
|
|
13
|
+
|
|
14
|
+
export class HealthManager {
|
|
15
|
+
constructor({ database, healthConfig, verbose }) {
|
|
16
|
+
this.database = database;
|
|
17
|
+
this.healthConfig = healthConfig || {};
|
|
18
|
+
this.verbose = verbose;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Register all health endpoints on Hono app
|
|
23
|
+
* @param {Hono} app - Hono application instance
|
|
24
|
+
*/
|
|
25
|
+
register(app) {
|
|
26
|
+
// Liveness probe
|
|
27
|
+
app.get('/health/live', (c) => this.livenessProbe(c));
|
|
28
|
+
|
|
29
|
+
// Readiness probe
|
|
30
|
+
app.get('/health/ready', (c) => this.readinessProbe(c));
|
|
31
|
+
|
|
32
|
+
// Generic health
|
|
33
|
+
app.get('/health', (c) => this.genericHealth(c));
|
|
34
|
+
|
|
35
|
+
if (this.verbose) {
|
|
36
|
+
console.log('[HealthManager] Health endpoints registered:');
|
|
37
|
+
console.log('[HealthManager] GET /health');
|
|
38
|
+
console.log('[HealthManager] GET /health/live');
|
|
39
|
+
console.log('[HealthManager] GET /health/ready');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Liveness probe - checks if app is alive
|
|
45
|
+
* If this fails, Kubernetes will restart the pod
|
|
46
|
+
* @private
|
|
47
|
+
*/
|
|
48
|
+
livenessProbe(c) {
|
|
49
|
+
const response = formatter.success({
|
|
50
|
+
status: 'alive',
|
|
51
|
+
timestamp: new Date().toISOString()
|
|
52
|
+
});
|
|
53
|
+
return c.json(response);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Readiness probe - checks if app is ready to receive traffic
|
|
58
|
+
* If this fails, Kubernetes will remove pod from service endpoints
|
|
59
|
+
* @private
|
|
60
|
+
*/
|
|
61
|
+
async readinessProbe(c) {
|
|
62
|
+
const checks = {};
|
|
63
|
+
let isHealthy = true;
|
|
64
|
+
|
|
65
|
+
// Get custom checks configuration
|
|
66
|
+
const customChecks = this.healthConfig.readiness?.checks || [];
|
|
67
|
+
|
|
68
|
+
// Built-in: Database check
|
|
69
|
+
try {
|
|
70
|
+
const startTime = Date.now();
|
|
71
|
+
const isDbReady = this.database &&
|
|
72
|
+
this.database.connected &&
|
|
73
|
+
Object.keys(this.database.resources).length > 0;
|
|
74
|
+
const latency = Date.now() - startTime;
|
|
75
|
+
|
|
76
|
+
if (isDbReady) {
|
|
77
|
+
checks.s3db = {
|
|
78
|
+
status: 'healthy',
|
|
79
|
+
latency_ms: latency,
|
|
80
|
+
resources: Object.keys(this.database.resources).length
|
|
81
|
+
};
|
|
82
|
+
} else {
|
|
83
|
+
checks.s3db = {
|
|
84
|
+
status: 'unhealthy',
|
|
85
|
+
connected: this.database?.connected || false,
|
|
86
|
+
resources: Object.keys(this.database?.resources || {}).length
|
|
87
|
+
};
|
|
88
|
+
isHealthy = false;
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
checks.s3db = {
|
|
92
|
+
status: 'unhealthy',
|
|
93
|
+
error: err.message
|
|
94
|
+
};
|
|
95
|
+
isHealthy = false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Execute custom checks
|
|
99
|
+
for (const check of customChecks) {
|
|
100
|
+
try {
|
|
101
|
+
const startTime = Date.now();
|
|
102
|
+
const timeout = check.timeout || 5000;
|
|
103
|
+
|
|
104
|
+
// Run check with timeout
|
|
105
|
+
const result = await Promise.race([
|
|
106
|
+
check.check(),
|
|
107
|
+
new Promise((_, reject) =>
|
|
108
|
+
setTimeout(() => reject(new Error('Timeout')), timeout)
|
|
109
|
+
)
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
const latency = Date.now() - startTime;
|
|
113
|
+
|
|
114
|
+
checks[check.name] = {
|
|
115
|
+
status: result.healthy ? 'healthy' : 'unhealthy',
|
|
116
|
+
latency_ms: latency,
|
|
117
|
+
...result
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Only mark as unhealthy if check is not optional
|
|
121
|
+
if (!result.healthy && !check.optional) {
|
|
122
|
+
isHealthy = false;
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
checks[check.name] = {
|
|
126
|
+
status: 'unhealthy',
|
|
127
|
+
error: err.message
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Only mark as unhealthy if check is not optional
|
|
131
|
+
if (!check.optional) {
|
|
132
|
+
isHealthy = false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const status = isHealthy ? 200 : 503;
|
|
138
|
+
|
|
139
|
+
return c.json({
|
|
140
|
+
status: isHealthy ? 'healthy' : 'unhealthy',
|
|
141
|
+
timestamp: new Date().toISOString(),
|
|
142
|
+
uptime: process.uptime(),
|
|
143
|
+
checks
|
|
144
|
+
}, status);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Generic health check
|
|
149
|
+
* @private
|
|
150
|
+
*/
|
|
151
|
+
genericHealth(c) {
|
|
152
|
+
const response = formatter.success({
|
|
153
|
+
status: 'ok',
|
|
154
|
+
uptime: process.uptime(),
|
|
155
|
+
timestamp: new Date().toISOString(),
|
|
156
|
+
checks: {
|
|
157
|
+
liveness: '/health/live',
|
|
158
|
+
readiness: '/health/ready'
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return c.json(response);
|
|
162
|
+
}
|
|
163
|
+
}
|