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.
Files changed (193) hide show
  1. package/README.md +139 -43
  2. package/dist/s3db.cjs +72425 -38970
  3. package/dist/s3db.cjs.map +1 -1
  4. package/dist/s3db.es.js +72177 -38764
  5. package/dist/s3db.es.js.map +1 -1
  6. package/mcp/lib/base-handler.js +157 -0
  7. package/mcp/lib/handlers/connection-handler.js +280 -0
  8. package/mcp/lib/handlers/query-handler.js +533 -0
  9. package/mcp/lib/handlers/resource-handler.js +428 -0
  10. package/mcp/lib/tool-registry.js +336 -0
  11. package/mcp/lib/tools/connection-tools.js +161 -0
  12. package/mcp/lib/tools/query-tools.js +267 -0
  13. package/mcp/lib/tools/resource-tools.js +404 -0
  14. package/package.json +94 -49
  15. package/src/clients/memory-client.class.js +346 -191
  16. package/src/clients/memory-storage.class.js +300 -84
  17. package/src/clients/s3-client.class.js +7 -6
  18. package/src/concerns/geo-encoding.js +19 -2
  19. package/src/concerns/ip.js +59 -9
  20. package/src/concerns/money.js +8 -1
  21. package/src/concerns/password-hashing.js +49 -8
  22. package/src/concerns/plugin-storage.js +186 -18
  23. package/src/concerns/storage-drivers/filesystem-driver.js +284 -0
  24. package/src/database.class.js +139 -29
  25. package/src/errors.js +332 -42
  26. package/src/plugins/api/auth/oidc-auth.js +66 -17
  27. package/src/plugins/api/auth/strategies/base-strategy.class.js +74 -0
  28. package/src/plugins/api/auth/strategies/factory.class.js +63 -0
  29. package/src/plugins/api/auth/strategies/global-strategy.class.js +44 -0
  30. package/src/plugins/api/auth/strategies/path-based-strategy.class.js +83 -0
  31. package/src/plugins/api/auth/strategies/path-rules-strategy.class.js +118 -0
  32. package/src/plugins/api/concerns/failban-manager.js +106 -57
  33. package/src/plugins/api/concerns/opengraph-helper.js +116 -0
  34. package/src/plugins/api/concerns/route-context.js +601 -0
  35. package/src/plugins/api/concerns/state-machine.js +288 -0
  36. package/src/plugins/api/index.js +180 -41
  37. package/src/plugins/api/routes/auth-routes.js +198 -30
  38. package/src/plugins/api/routes/resource-routes.js +19 -4
  39. package/src/plugins/api/server/health-manager.class.js +163 -0
  40. package/src/plugins/api/server/middleware-chain.class.js +310 -0
  41. package/src/plugins/api/server/router.class.js +472 -0
  42. package/src/plugins/api/server.js +280 -1303
  43. package/src/plugins/api/utils/custom-routes.js +17 -5
  44. package/src/plugins/api/utils/guards.js +76 -17
  45. package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
  46. package/src/plugins/api/utils/openapi-generator.js +7 -6
  47. package/src/plugins/api/utils/template-engine.js +77 -3
  48. package/src/plugins/audit.plugin.js +30 -8
  49. package/src/plugins/backup.plugin.js +110 -14
  50. package/src/plugins/cache/cache.class.js +22 -5
  51. package/src/plugins/cache/filesystem-cache.class.js +116 -19
  52. package/src/plugins/cache/memory-cache.class.js +211 -57
  53. package/src/plugins/cache/multi-tier-cache.class.js +371 -0
  54. package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
  55. package/src/plugins/cache/redis-cache.class.js +552 -0
  56. package/src/plugins/cache/s3-cache.class.js +17 -8
  57. package/src/plugins/cache.plugin.js +176 -61
  58. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
  59. package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
  60. package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
  61. package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
  62. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
  63. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
  64. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
  65. package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
  66. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
  67. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
  68. package/src/plugins/cloud-inventory/index.js +29 -8
  69. package/src/plugins/cloud-inventory/registry.js +64 -42
  70. package/src/plugins/cloud-inventory.plugin.js +240 -138
  71. package/src/plugins/concerns/plugin-dependencies.js +54 -0
  72. package/src/plugins/concerns/resource-names.js +100 -0
  73. package/src/plugins/consumers/index.js +10 -2
  74. package/src/plugins/consumers/sqs-consumer.js +12 -2
  75. package/src/plugins/cookie-farm-suite.plugin.js +278 -0
  76. package/src/plugins/cookie-farm.errors.js +73 -0
  77. package/src/plugins/cookie-farm.plugin.js +869 -0
  78. package/src/plugins/costs.plugin.js +7 -1
  79. package/src/plugins/eventual-consistency/analytics.js +94 -19
  80. package/src/plugins/eventual-consistency/config.js +15 -7
  81. package/src/plugins/eventual-consistency/consolidation.js +29 -11
  82. package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
  83. package/src/plugins/eventual-consistency/helpers.js +39 -14
  84. package/src/plugins/eventual-consistency/install.js +21 -2
  85. package/src/plugins/eventual-consistency/utils.js +32 -10
  86. package/src/plugins/fulltext.plugin.js +38 -11
  87. package/src/plugins/geo.plugin.js +61 -9
  88. package/src/plugins/identity/concerns/config.js +61 -0
  89. package/src/plugins/identity/concerns/mfa-manager.js +15 -2
  90. package/src/plugins/identity/concerns/rate-limit.js +124 -0
  91. package/src/plugins/identity/concerns/resource-schemas.js +9 -1
  92. package/src/plugins/identity/concerns/token-generator.js +29 -4
  93. package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
  94. package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
  95. package/src/plugins/identity/drivers/index.js +18 -0
  96. package/src/plugins/identity/drivers/password-driver.js +122 -0
  97. package/src/plugins/identity/email-service.js +17 -2
  98. package/src/plugins/identity/index.js +413 -69
  99. package/src/plugins/identity/oauth2-server.js +413 -30
  100. package/src/plugins/identity/oidc-discovery.js +16 -8
  101. package/src/plugins/identity/rsa-keys.js +115 -35
  102. package/src/plugins/identity/server.js +166 -45
  103. package/src/plugins/identity/session-manager.js +53 -7
  104. package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
  105. package/src/plugins/identity/ui/routes.js +363 -255
  106. package/src/plugins/importer/index.js +153 -20
  107. package/src/plugins/index.js +9 -2
  108. package/src/plugins/kubernetes-inventory/index.js +6 -0
  109. package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
  110. package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
  111. package/src/plugins/kubernetes-inventory.plugin.js +980 -0
  112. package/src/plugins/metrics.plugin.js +64 -16
  113. package/src/plugins/ml/base-model.class.js +25 -15
  114. package/src/plugins/ml/regression-model.class.js +1 -1
  115. package/src/plugins/ml.errors.js +57 -25
  116. package/src/plugins/ml.plugin.js +28 -4
  117. package/src/plugins/namespace.js +210 -0
  118. package/src/plugins/plugin.class.js +180 -8
  119. package/src/plugins/puppeteer/console-monitor.js +729 -0
  120. package/src/plugins/puppeteer/cookie-manager.js +492 -0
  121. package/src/plugins/puppeteer/network-monitor.js +816 -0
  122. package/src/plugins/puppeteer/performance-manager.js +746 -0
  123. package/src/plugins/puppeteer/proxy-manager.js +478 -0
  124. package/src/plugins/puppeteer/stealth-manager.js +556 -0
  125. package/src/plugins/puppeteer.errors.js +81 -0
  126. package/src/plugins/puppeteer.plugin.js +1327 -0
  127. package/src/plugins/queue-consumer.plugin.js +69 -14
  128. package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
  129. package/src/plugins/recon/concerns/command-runner.js +148 -0
  130. package/src/plugins/recon/concerns/diff-detector.js +372 -0
  131. package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
  132. package/src/plugins/recon/concerns/process-manager.js +338 -0
  133. package/src/plugins/recon/concerns/report-generator.js +478 -0
  134. package/src/plugins/recon/concerns/security-analyzer.js +571 -0
  135. package/src/plugins/recon/concerns/target-normalizer.js +68 -0
  136. package/src/plugins/recon/config/defaults.js +321 -0
  137. package/src/plugins/recon/config/resources.js +370 -0
  138. package/src/plugins/recon/index.js +778 -0
  139. package/src/plugins/recon/managers/dependency-manager.js +174 -0
  140. package/src/plugins/recon/managers/scheduler-manager.js +179 -0
  141. package/src/plugins/recon/managers/storage-manager.js +745 -0
  142. package/src/plugins/recon/managers/target-manager.js +274 -0
  143. package/src/plugins/recon/stages/asn-stage.js +314 -0
  144. package/src/plugins/recon/stages/certificate-stage.js +84 -0
  145. package/src/plugins/recon/stages/dns-stage.js +107 -0
  146. package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
  147. package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
  148. package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
  149. package/src/plugins/recon/stages/http-stage.js +89 -0
  150. package/src/plugins/recon/stages/latency-stage.js +148 -0
  151. package/src/plugins/recon/stages/massdns-stage.js +302 -0
  152. package/src/plugins/recon/stages/osint-stage.js +1373 -0
  153. package/src/plugins/recon/stages/ports-stage.js +169 -0
  154. package/src/plugins/recon/stages/screenshot-stage.js +94 -0
  155. package/src/plugins/recon/stages/secrets-stage.js +514 -0
  156. package/src/plugins/recon/stages/subdomains-stage.js +295 -0
  157. package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
  158. package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
  159. package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
  160. package/src/plugins/recon/stages/whois-stage.js +349 -0
  161. package/src/plugins/recon.plugin.js +75 -0
  162. package/src/plugins/recon.plugin.js.backup +2635 -0
  163. package/src/plugins/relation.errors.js +87 -14
  164. package/src/plugins/replicator.plugin.js +514 -137
  165. package/src/plugins/replicators/base-replicator.class.js +89 -1
  166. package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
  167. package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
  168. package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
  169. package/src/plugins/replicators/mysql-replicator.class.js +52 -17
  170. package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
  171. package/src/plugins/replicators/postgres-replicator.class.js +62 -27
  172. package/src/plugins/replicators/s3db-replicator.class.js +25 -18
  173. package/src/plugins/replicators/schema-sync.helper.js +3 -3
  174. package/src/plugins/replicators/sqs-replicator.class.js +8 -2
  175. package/src/plugins/replicators/turso-replicator.class.js +23 -3
  176. package/src/plugins/replicators/webhook-replicator.class.js +42 -4
  177. package/src/plugins/s3-queue.plugin.js +464 -65
  178. package/src/plugins/scheduler.plugin.js +20 -6
  179. package/src/plugins/state-machine.plugin.js +40 -9
  180. package/src/plugins/tfstate/README.md +126 -126
  181. package/src/plugins/tfstate/base-driver.js +28 -4
  182. package/src/plugins/tfstate/errors.js +65 -10
  183. package/src/plugins/tfstate/filesystem-driver.js +52 -8
  184. package/src/plugins/tfstate/index.js +163 -90
  185. package/src/plugins/tfstate/s3-driver.js +64 -6
  186. package/src/plugins/ttl.plugin.js +72 -17
  187. package/src/plugins/vector/distances.js +18 -12
  188. package/src/plugins/vector/kmeans.js +26 -4
  189. package/src/resource.class.js +115 -19
  190. package/src/testing/factory.class.js +20 -3
  191. package/src/testing/seeder.class.js +7 -1
  192. package/src/clients/memory-client.md +0 -917
  193. 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
- allowRegistration = true
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 (allowRegistration) {
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
- ...dataWithoutId, // Include all fields from request except id
73
- [usernameField]: username, // Override to ensure correct value
74
- [passwordField]: password // Will be auto-encrypted by schema (secret field)
75
- };
76
-
77
- // Add optional fields only if not provided
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
- if (userData.active === undefined) {
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: userWithoutPassword,
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
- const { [passwordField]: _, ...userWithoutPassword } = user;
366
+ if (loginThrottleConfig.enabled && throttleKey) {
367
+ loginAttempts.delete(throttleKey);
368
+ }
193
369
 
194
370
  const response = formatter.success({
195
- user: userWithoutPassword,
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
- if (!user.password) {
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 usersResource.update(user.id, {
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
- // Use query if filters are present
148
- if (Object.keys(filters).length > 0) {
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
- } else if (partition && partitionValues) {
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
- } else {
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
+ }