s3db.js 11.3.2 → 12.0.1

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 (83) hide show
  1. package/README.md +102 -8
  2. package/dist/s3db.cjs.js +36945 -15510
  3. package/dist/s3db.cjs.js.map +1 -1
  4. package/dist/s3db.d.ts +66 -1
  5. package/dist/s3db.es.js +36914 -15534
  6. package/dist/s3db.es.js.map +1 -1
  7. package/mcp/entrypoint.js +58 -0
  8. package/mcp/tools/documentation.js +434 -0
  9. package/mcp/tools/index.js +4 -0
  10. package/package.json +35 -15
  11. package/src/behaviors/user-managed.js +13 -6
  12. package/src/client.class.js +79 -49
  13. package/src/concerns/base62.js +85 -0
  14. package/src/concerns/dictionary-encoding.js +294 -0
  15. package/src/concerns/geo-encoding.js +256 -0
  16. package/src/concerns/high-performance-inserter.js +34 -30
  17. package/src/concerns/ip.js +325 -0
  18. package/src/concerns/metadata-encoding.js +345 -66
  19. package/src/concerns/money.js +193 -0
  20. package/src/concerns/partition-queue.js +7 -4
  21. package/src/concerns/plugin-storage.js +97 -47
  22. package/src/database.class.js +76 -74
  23. package/src/errors.js +0 -4
  24. package/src/plugins/api/auth/api-key-auth.js +88 -0
  25. package/src/plugins/api/auth/basic-auth.js +154 -0
  26. package/src/plugins/api/auth/index.js +112 -0
  27. package/src/plugins/api/auth/jwt-auth.js +169 -0
  28. package/src/plugins/api/index.js +544 -0
  29. package/src/plugins/api/middlewares/index.js +15 -0
  30. package/src/plugins/api/middlewares/validator.js +185 -0
  31. package/src/plugins/api/routes/auth-routes.js +241 -0
  32. package/src/plugins/api/routes/resource-routes.js +304 -0
  33. package/src/plugins/api/server.js +354 -0
  34. package/src/plugins/api/utils/error-handler.js +147 -0
  35. package/src/plugins/api/utils/openapi-generator.js +1240 -0
  36. package/src/plugins/api/utils/response-formatter.js +218 -0
  37. package/src/plugins/backup/streaming-exporter.js +132 -0
  38. package/src/plugins/backup.plugin.js +103 -50
  39. package/src/plugins/cache/s3-cache.class.js +95 -47
  40. package/src/plugins/cache.plugin.js +107 -9
  41. package/src/plugins/concerns/plugin-dependencies.js +313 -0
  42. package/src/plugins/concerns/prometheus-formatter.js +255 -0
  43. package/src/plugins/consumers/rabbitmq-consumer.js +4 -0
  44. package/src/plugins/consumers/sqs-consumer.js +4 -0
  45. package/src/plugins/costs.plugin.js +255 -39
  46. package/src/plugins/eventual-consistency/helpers.js +15 -1
  47. package/src/plugins/geo.plugin.js +873 -0
  48. package/src/plugins/importer/index.js +1020 -0
  49. package/src/plugins/index.js +11 -0
  50. package/src/plugins/metrics.plugin.js +163 -4
  51. package/src/plugins/queue-consumer.plugin.js +6 -27
  52. package/src/plugins/relation.errors.js +139 -0
  53. package/src/plugins/relation.plugin.js +1242 -0
  54. package/src/plugins/replicator.plugin.js +2 -1
  55. package/src/plugins/replicators/bigquery-replicator.class.js +180 -8
  56. package/src/plugins/replicators/dynamodb-replicator.class.js +383 -0
  57. package/src/plugins/replicators/index.js +28 -3
  58. package/src/plugins/replicators/mongodb-replicator.class.js +391 -0
  59. package/src/plugins/replicators/mysql-replicator.class.js +558 -0
  60. package/src/plugins/replicators/planetscale-replicator.class.js +409 -0
  61. package/src/plugins/replicators/postgres-replicator.class.js +182 -7
  62. package/src/plugins/replicators/s3db-replicator.class.js +1 -12
  63. package/src/plugins/replicators/schema-sync.helper.js +601 -0
  64. package/src/plugins/replicators/sqs-replicator.class.js +11 -9
  65. package/src/plugins/replicators/turso-replicator.class.js +416 -0
  66. package/src/plugins/replicators/webhook-replicator.class.js +612 -0
  67. package/src/plugins/state-machine.plugin.js +122 -68
  68. package/src/plugins/tfstate/README.md +745 -0
  69. package/src/plugins/tfstate/base-driver.js +80 -0
  70. package/src/plugins/tfstate/errors.js +112 -0
  71. package/src/plugins/tfstate/filesystem-driver.js +129 -0
  72. package/src/plugins/tfstate/index.js +2660 -0
  73. package/src/plugins/tfstate/s3-driver.js +192 -0
  74. package/src/plugins/ttl.plugin.js +536 -0
  75. package/src/resource.class.js +315 -36
  76. package/src/s3db.d.ts +66 -1
  77. package/src/schema.class.js +366 -32
  78. package/SECURITY.md +0 -76
  79. package/src/partition-drivers/base-partition-driver.js +0 -106
  80. package/src/partition-drivers/index.js +0 -66
  81. package/src/partition-drivers/memory-partition-driver.js +0 -289
  82. package/src/partition-drivers/sqs-partition-driver.js +0 -337
  83. package/src/partition-drivers/sync-partition-driver.js +0 -38
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Validation Middleware - Schema validation for resource operations
3
+ *
4
+ * Uses s3db.js resource schemas to validate request data
5
+ */
6
+
7
+ import { validationError } from '../utils/response-formatter.js';
8
+
9
+ /**
10
+ * Create validation middleware for a resource
11
+ * @param {Object} resource - s3db.js Resource instance
12
+ * @param {Object} options - Validation options
13
+ * @param {boolean} options.validateOnInsert - Validate on POST (default: true)
14
+ * @param {boolean} options.validateOnUpdate - Validate on PUT/PATCH (default: true)
15
+ * @param {boolean} options.partial - Allow partial validation for PATCH (default: true)
16
+ * @returns {Function} Hono middleware
17
+ */
18
+ export function createValidationMiddleware(resource, options = {}) {
19
+ const {
20
+ validateOnInsert = true,
21
+ validateOnUpdate = true,
22
+ partial = true
23
+ } = options;
24
+
25
+ const schema = resource.schema;
26
+
27
+ return async (c, next) => {
28
+ const method = c.req.method;
29
+ const shouldValidate =
30
+ (method === 'POST' && validateOnInsert) ||
31
+ ((method === 'PUT' || method === 'PATCH') && validateOnUpdate);
32
+
33
+ if (!shouldValidate) {
34
+ return await next();
35
+ }
36
+
37
+ // Get request body
38
+ let data;
39
+ try {
40
+ data = await c.req.json();
41
+ } catch (err) {
42
+ const response = validationError([
43
+ { field: 'body', message: 'Invalid JSON in request body' }
44
+ ]);
45
+ return c.json(response, response._status);
46
+ }
47
+
48
+ // For PATCH, allow partial data
49
+ const isPartial = method === 'PATCH' && partial;
50
+
51
+ // Validate using resource schema
52
+ const validationResult = schema.validate(data, {
53
+ partial: isPartial,
54
+ strict: !isPartial
55
+ });
56
+
57
+ if (!validationResult.valid) {
58
+ const errors = validationResult.errors.map(err => ({
59
+ field: err.field || err.attribute || 'unknown',
60
+ message: err.message,
61
+ expected: err.expected,
62
+ actual: err.actual
63
+ }));
64
+
65
+ const response = validationError(errors);
66
+ return c.json(response, response._status);
67
+ }
68
+
69
+ // Store validated data in context (optional)
70
+ c.set('validatedData', validationResult.data || data);
71
+
72
+ await next();
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Create validation middleware that validates query parameters
78
+ * @param {Object} schema - Validation schema for query params
79
+ * @returns {Function} Hono middleware
80
+ */
81
+ export function createQueryValidation(schema = {}) {
82
+ return async (c, next) => {
83
+ const query = c.req.query();
84
+ const errors = [];
85
+
86
+ // Validate each query parameter
87
+ for (const [key, rules] of Object.entries(schema)) {
88
+ const value = query[key];
89
+
90
+ // Check required
91
+ if (rules.required && !value) {
92
+ errors.push({
93
+ field: key,
94
+ message: `Query parameter '${key}' is required`
95
+ });
96
+ continue;
97
+ }
98
+
99
+ if (!value) continue;
100
+
101
+ // Check type
102
+ if (rules.type) {
103
+ if (rules.type === 'number' && isNaN(Number(value))) {
104
+ errors.push({
105
+ field: key,
106
+ message: `Query parameter '${key}' must be a number`,
107
+ actual: value
108
+ });
109
+ }
110
+
111
+ if (rules.type === 'boolean' && !['true', 'false', '1', '0'].includes(value.toLowerCase())) {
112
+ errors.push({
113
+ field: key,
114
+ message: `Query parameter '${key}' must be a boolean`,
115
+ actual: value
116
+ });
117
+ }
118
+ }
119
+
120
+ // Check min/max for numbers
121
+ if (rules.type === 'number') {
122
+ const num = Number(value);
123
+
124
+ if (rules.min !== undefined && num < rules.min) {
125
+ errors.push({
126
+ field: key,
127
+ message: `Query parameter '${key}' must be at least ${rules.min}`,
128
+ actual: num
129
+ });
130
+ }
131
+
132
+ if (rules.max !== undefined && num > rules.max) {
133
+ errors.push({
134
+ field: key,
135
+ message: `Query parameter '${key}' must be at most ${rules.max}`,
136
+ actual: num
137
+ });
138
+ }
139
+ }
140
+
141
+ // Check enum values
142
+ if (rules.enum && !rules.enum.includes(value)) {
143
+ errors.push({
144
+ field: key,
145
+ message: `Query parameter '${key}' must be one of: ${rules.enum.join(', ')}`,
146
+ actual: value
147
+ });
148
+ }
149
+ }
150
+
151
+ if (errors.length > 0) {
152
+ const response = validationError(errors);
153
+ return c.json(response, response._status);
154
+ }
155
+
156
+ await next();
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Standard query parameters validation for list endpoints
162
+ */
163
+ export const listQueryValidation = createQueryValidation({
164
+ limit: {
165
+ type: 'number',
166
+ min: 1,
167
+ max: 1000
168
+ },
169
+ offset: {
170
+ type: 'number',
171
+ min: 0
172
+ },
173
+ partition: {
174
+ type: 'string'
175
+ },
176
+ partitionValues: {
177
+ type: 'string' // JSON string
178
+ }
179
+ });
180
+
181
+ export default {
182
+ createValidationMiddleware,
183
+ createQueryValidation,
184
+ listQueryValidation
185
+ };
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Authentication Routes - Login, register, and token management endpoints
3
+ *
4
+ * Provides user authentication endpoints for the API
5
+ */
6
+
7
+ import { Hono } from 'hono';
8
+ import { asyncHandler } from '../utils/error-handler.js';
9
+ import * as formatter from '../utils/response-formatter.js';
10
+ import { createToken } from '../auth/jwt-auth.js';
11
+ import { generateApiKey } from '../auth/api-key-auth.js';
12
+ import { encrypt } from '../../../concerns/crypto.js';
13
+ import tryFn from '../../../concerns/try-fn.js';
14
+
15
+ /**
16
+ * Create authentication routes
17
+ * @param {Object} usersResource - s3db.js users resource
18
+ * @param {Object} config - Auth configuration
19
+ * @returns {Hono} Hono app with auth routes
20
+ */
21
+ export function createAuthRoutes(usersResource, config = {}) {
22
+ const app = new Hono();
23
+ const {
24
+ jwtSecret,
25
+ jwtExpiresIn = '7d',
26
+ passphrase = 'secret',
27
+ allowRegistration = true
28
+ } = config;
29
+
30
+ // POST /auth/register - Register new user
31
+ if (allowRegistration) {
32
+ app.post('/register', asyncHandler(async (c) => {
33
+ const data = await c.req.json();
34
+ const { username, password, email, role = 'user' } = data;
35
+
36
+ // Validate input
37
+ if (!username || !password) {
38
+ const response = formatter.validationError([
39
+ { field: 'username', message: 'Username is required' },
40
+ { field: 'password', message: 'Password is required' }
41
+ ]);
42
+ return c.json(response, response._status);
43
+ }
44
+
45
+ if (password.length < 8) {
46
+ const response = formatter.validationError([
47
+ { field: 'password', message: 'Password must be at least 8 characters' }
48
+ ]);
49
+ return c.json(response, response._status);
50
+ }
51
+
52
+ // Check if username already exists
53
+ const existing = await usersResource.query({ username });
54
+ if (existing && existing.length > 0) {
55
+ const response = formatter.error('Username already exists', {
56
+ status: 409,
57
+ code: 'CONFLICT'
58
+ });
59
+ return c.json(response, response._status);
60
+ }
61
+
62
+ // Create user
63
+ const user = await usersResource.insert({
64
+ username,
65
+ password, // Will be auto-encrypted by schema (secret field)
66
+ email,
67
+ role,
68
+ active: true,
69
+ apiKey: generateApiKey(),
70
+ createdAt: new Date().toISOString()
71
+ });
72
+
73
+ // Generate JWT token
74
+ let token = null;
75
+ if (jwtSecret) {
76
+ token = createToken(
77
+ { userId: user.id, username: user.username, role: user.role },
78
+ jwtSecret,
79
+ jwtExpiresIn
80
+ );
81
+ }
82
+
83
+ // Remove sensitive data from response
84
+ const { password: _, ...userWithoutPassword } = user;
85
+
86
+ const response = formatter.created({
87
+ user: userWithoutPassword,
88
+ token
89
+ }, `/auth/users/${user.id}`);
90
+
91
+ return c.json(response, response._status);
92
+ }));
93
+ }
94
+
95
+ // POST /auth/login - Login with username/password
96
+ app.post('/login', asyncHandler(async (c) => {
97
+ const data = await c.req.json();
98
+ const { username, password } = data;
99
+
100
+ // Validate input
101
+ if (!username || !password) {
102
+ const response = formatter.unauthorized('Username and password are required');
103
+ return c.json(response, response._status);
104
+ }
105
+
106
+ // Find user
107
+ const users = await usersResource.query({ username });
108
+ if (!users || users.length === 0) {
109
+ const response = formatter.unauthorized('Invalid credentials');
110
+ return c.json(response, response._status);
111
+ }
112
+
113
+ const user = users[0];
114
+
115
+ if (!user.active) {
116
+ const response = formatter.unauthorized('User account is inactive');
117
+ return c.json(response, response._status);
118
+ }
119
+
120
+ // Verify password (decrypt and compare)
121
+ // Note: In production, use proper password hashing (bcrypt, argon2)
122
+ const [ok, err, decrypted] = await tryFn(() =>
123
+ user.password // Password is already decrypted by autoDecrypt
124
+ );
125
+
126
+ // For secret fields, we need to manually decrypt if autoDecrypt is off
127
+ // But by default autoDecrypt is true, so user.password should be plain text here
128
+ // Let's just compare directly since schema handles encryption/decryption
129
+ const isValid = user.password === password;
130
+
131
+ if (!isValid) {
132
+ const response = formatter.unauthorized('Invalid credentials');
133
+ return c.json(response, response._status);
134
+ }
135
+
136
+ // Update last login
137
+ await usersResource.update(user.id, {
138
+ lastLoginAt: new Date().toISOString()
139
+ });
140
+
141
+ // Generate JWT token
142
+ let token = null;
143
+ if (jwtSecret) {
144
+ token = createToken(
145
+ { userId: user.id, username: user.username, role: user.role },
146
+ jwtSecret,
147
+ jwtExpiresIn
148
+ );
149
+ }
150
+
151
+ // Remove sensitive data from response
152
+ const { password: _, ...userWithoutPassword } = user;
153
+
154
+ const response = formatter.success({
155
+ user: userWithoutPassword,
156
+ token,
157
+ expiresIn: jwtExpiresIn
158
+ });
159
+
160
+ return c.json(response, response._status);
161
+ }));
162
+
163
+ // POST /auth/token/refresh - Refresh JWT token
164
+ if (jwtSecret) {
165
+ app.post('/token/refresh', asyncHandler(async (c) => {
166
+ const user = c.get('user');
167
+
168
+ if (!user) {
169
+ const response = formatter.unauthorized('Authentication required');
170
+ return c.json(response, response._status);
171
+ }
172
+
173
+ // Generate new token
174
+ const token = createToken(
175
+ { userId: user.id, username: user.username, role: user.role },
176
+ jwtSecret,
177
+ jwtExpiresIn
178
+ );
179
+
180
+ const response = formatter.success({
181
+ token,
182
+ expiresIn: jwtExpiresIn
183
+ });
184
+
185
+ return c.json(response, response._status);
186
+ }));
187
+ }
188
+
189
+ // GET /auth/me - Get current user info
190
+ app.get('/me', asyncHandler(async (c) => {
191
+ const user = c.get('user');
192
+
193
+ if (!user) {
194
+ const response = formatter.unauthorized('Authentication required');
195
+ return c.json(response, response._status);
196
+ }
197
+
198
+ // If user is from JWT payload (no password field), return as is
199
+ if (!user.password) {
200
+ const response = formatter.success(user);
201
+ return c.json(response, response._status);
202
+ }
203
+
204
+ // Remove sensitive data
205
+ const { password: _, ...userWithoutPassword } = user;
206
+
207
+ const response = formatter.success(userWithoutPassword);
208
+ return c.json(response, response._status);
209
+ }));
210
+
211
+ // POST /auth/api-key/regenerate - Regenerate API key
212
+ app.post('/api-key/regenerate', asyncHandler(async (c) => {
213
+ const user = c.get('user');
214
+
215
+ if (!user) {
216
+ const response = formatter.unauthorized('Authentication required');
217
+ return c.json(response, response._status);
218
+ }
219
+
220
+ // Generate new API key
221
+ const newApiKey = generateApiKey();
222
+
223
+ // Update user
224
+ await usersResource.update(user.id, {
225
+ apiKey: newApiKey
226
+ });
227
+
228
+ const response = formatter.success({
229
+ apiKey: newApiKey,
230
+ message: 'API key regenerated successfully'
231
+ });
232
+
233
+ return c.json(response, response._status);
234
+ }));
235
+
236
+ return app;
237
+ }
238
+
239
+ export default {
240
+ createAuthRoutes
241
+ };