s3db.js 11.3.2 → 12.0.0
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 +102 -8
- package/dist/s3db.cjs.js +36664 -15480
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.d.ts +57 -0
- package/dist/s3db.es.js +36661 -15531
- package/dist/s3db.es.js.map +1 -1
- package/mcp/entrypoint.js +58 -0
- package/mcp/tools/documentation.js +434 -0
- package/mcp/tools/index.js +4 -0
- package/package.json +27 -6
- package/src/behaviors/user-managed.js +13 -6
- package/src/client.class.js +41 -46
- package/src/concerns/base62.js +85 -0
- package/src/concerns/dictionary-encoding.js +294 -0
- package/src/concerns/geo-encoding.js +256 -0
- package/src/concerns/high-performance-inserter.js +34 -30
- package/src/concerns/ip.js +325 -0
- package/src/concerns/metadata-encoding.js +345 -66
- package/src/concerns/money.js +193 -0
- package/src/concerns/partition-queue.js +7 -4
- package/src/concerns/plugin-storage.js +39 -19
- package/src/database.class.js +76 -74
- package/src/errors.js +0 -4
- package/src/plugins/api/auth/api-key-auth.js +88 -0
- package/src/plugins/api/auth/basic-auth.js +154 -0
- package/src/plugins/api/auth/index.js +112 -0
- package/src/plugins/api/auth/jwt-auth.js +169 -0
- package/src/plugins/api/index.js +539 -0
- package/src/plugins/api/middlewares/index.js +15 -0
- package/src/plugins/api/middlewares/validator.js +185 -0
- package/src/plugins/api/routes/auth-routes.js +241 -0
- package/src/plugins/api/routes/resource-routes.js +304 -0
- package/src/plugins/api/server.js +350 -0
- package/src/plugins/api/utils/error-handler.js +147 -0
- package/src/plugins/api/utils/openapi-generator.js +1240 -0
- package/src/plugins/api/utils/response-formatter.js +218 -0
- package/src/plugins/backup/streaming-exporter.js +132 -0
- package/src/plugins/backup.plugin.js +103 -50
- package/src/plugins/cache/s3-cache.class.js +95 -47
- package/src/plugins/cache.plugin.js +107 -9
- package/src/plugins/concerns/plugin-dependencies.js +313 -0
- package/src/plugins/concerns/prometheus-formatter.js +255 -0
- package/src/plugins/consumers/rabbitmq-consumer.js +4 -0
- package/src/plugins/consumers/sqs-consumer.js +4 -0
- package/src/plugins/costs.plugin.js +255 -39
- package/src/plugins/eventual-consistency/helpers.js +15 -1
- package/src/plugins/geo.plugin.js +873 -0
- package/src/plugins/importer/index.js +1020 -0
- package/src/plugins/index.js +11 -0
- package/src/plugins/metrics.plugin.js +163 -4
- package/src/plugins/queue-consumer.plugin.js +6 -27
- package/src/plugins/relation.errors.js +139 -0
- package/src/plugins/relation.plugin.js +1242 -0
- package/src/plugins/replicators/bigquery-replicator.class.js +180 -8
- package/src/plugins/replicators/dynamodb-replicator.class.js +383 -0
- package/src/plugins/replicators/index.js +28 -3
- package/src/plugins/replicators/mongodb-replicator.class.js +391 -0
- package/src/plugins/replicators/mysql-replicator.class.js +558 -0
- package/src/plugins/replicators/planetscale-replicator.class.js +409 -0
- package/src/plugins/replicators/postgres-replicator.class.js +182 -7
- package/src/plugins/replicators/s3db-replicator.class.js +1 -12
- package/src/plugins/replicators/schema-sync.helper.js +601 -0
- package/src/plugins/replicators/sqs-replicator.class.js +11 -9
- package/src/plugins/replicators/turso-replicator.class.js +416 -0
- package/src/plugins/replicators/webhook-replicator.class.js +612 -0
- package/src/plugins/state-machine.plugin.js +122 -68
- package/src/plugins/tfstate/README.md +745 -0
- package/src/plugins/tfstate/base-driver.js +80 -0
- package/src/plugins/tfstate/errors.js +112 -0
- package/src/plugins/tfstate/filesystem-driver.js +129 -0
- package/src/plugins/tfstate/index.js +2660 -0
- package/src/plugins/tfstate/s3-driver.js +192 -0
- package/src/plugins/ttl.plugin.js +536 -0
- package/src/resource.class.js +14 -10
- package/src/s3db.d.ts +57 -0
- package/src/schema.class.js +366 -32
- package/SECURITY.md +0 -76
- package/src/partition-drivers/base-partition-driver.js +0 -106
- package/src/partition-drivers/index.js +0 -66
- package/src/partition-drivers/memory-partition-driver.js +0 -289
- package/src/partition-drivers/sqs-partition-driver.js +0 -337
- package/src/partition-drivers/sync-partition-driver.js +0 -38
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Plugin - RESTful HTTP API for s3db.js resources
|
|
3
|
+
*
|
|
4
|
+
* Transforms s3db.js resources into HTTP REST endpoints with:
|
|
5
|
+
* - Multiple authentication methods (JWT, API Key, Basic Auth, Public)
|
|
6
|
+
* - Automatic versioning based on resource version
|
|
7
|
+
* - Production features (CORS, Rate Limiting, Logging, Compression)
|
|
8
|
+
* - Schema validation middleware
|
|
9
|
+
* - Custom middleware support
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const apiPlugin = new ApiPlugin({
|
|
13
|
+
* port: 3000,
|
|
14
|
+
* docs: { enabled: true },
|
|
15
|
+
* auth: {
|
|
16
|
+
* jwt: { enabled: true, secret: 'my-secret' },
|
|
17
|
+
* apiKey: { enabled: true }
|
|
18
|
+
* },
|
|
19
|
+
* resources: {
|
|
20
|
+
* cars: {
|
|
21
|
+
* auth: ['jwt', 'apiKey'],
|
|
22
|
+
* methods: ['GET', 'POST', 'PUT', 'DELETE']
|
|
23
|
+
* }
|
|
24
|
+
* },
|
|
25
|
+
* cors: { enabled: true },
|
|
26
|
+
* rateLimit: { enabled: true, maxRequests: 100 },
|
|
27
|
+
* logging: { enabled: true },
|
|
28
|
+
* compression: { enabled: true },
|
|
29
|
+
* validation: { enabled: true }
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* await database.usePlugin(apiPlugin);
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { Plugin } from '../plugin.class.js';
|
|
36
|
+
import { ApiServer } from './server.js';
|
|
37
|
+
import { requirePluginDependency } from '../concerns/plugin-dependencies.js';
|
|
38
|
+
import tryFn from '../../concerns/try-fn.js';
|
|
39
|
+
|
|
40
|
+
export class ApiPlugin extends Plugin {
|
|
41
|
+
/**
|
|
42
|
+
* Create API Plugin instance
|
|
43
|
+
* @param {Object} options - Plugin configuration
|
|
44
|
+
*/
|
|
45
|
+
constructor(options = {}) {
|
|
46
|
+
super(options);
|
|
47
|
+
|
|
48
|
+
this.config = {
|
|
49
|
+
// Server configuration
|
|
50
|
+
port: options.port || 3000,
|
|
51
|
+
host: options.host || '0.0.0.0',
|
|
52
|
+
verbose: options.verbose || false,
|
|
53
|
+
|
|
54
|
+
// API Documentation (supports both new and legacy formats)
|
|
55
|
+
docs: {
|
|
56
|
+
enabled: options.docs?.enabled !== false && options.docsEnabled !== false, // Enable by default
|
|
57
|
+
ui: options.docs?.ui || 'redoc', // 'swagger' or 'redoc' (redoc is prettier!)
|
|
58
|
+
title: options.docs?.title || options.apiTitle || 's3db.js API',
|
|
59
|
+
version: options.docs?.version || options.apiVersion || '1.0.0',
|
|
60
|
+
description: options.docs?.description || options.apiDescription || 'Auto-generated REST API for s3db.js resources'
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// Authentication configuration
|
|
64
|
+
auth: {
|
|
65
|
+
jwt: {
|
|
66
|
+
enabled: options.auth?.jwt?.enabled || false,
|
|
67
|
+
secret: options.auth?.jwt?.secret || null,
|
|
68
|
+
expiresIn: options.auth?.jwt?.expiresIn || '7d'
|
|
69
|
+
},
|
|
70
|
+
apiKey: {
|
|
71
|
+
enabled: options.auth?.apiKey?.enabled || false,
|
|
72
|
+
headerName: options.auth?.apiKey?.headerName || 'X-API-Key'
|
|
73
|
+
},
|
|
74
|
+
basic: {
|
|
75
|
+
enabled: options.auth?.basic?.enabled || false,
|
|
76
|
+
realm: options.auth?.basic?.realm || 'API Access'
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// Resource configuration
|
|
81
|
+
resources: options.resources || {},
|
|
82
|
+
|
|
83
|
+
// CORS configuration
|
|
84
|
+
cors: {
|
|
85
|
+
enabled: options.cors?.enabled || false,
|
|
86
|
+
origin: options.cors?.origin || '*',
|
|
87
|
+
methods: options.cors?.methods || ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
88
|
+
allowedHeaders: options.cors?.allowedHeaders || ['Content-Type', 'Authorization', 'X-API-Key'],
|
|
89
|
+
exposedHeaders: options.cors?.exposedHeaders || ['X-Total-Count', 'X-Page-Count'],
|
|
90
|
+
credentials: options.cors?.credentials !== false,
|
|
91
|
+
maxAge: options.cors?.maxAge || 86400
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
// Rate limiting configuration
|
|
95
|
+
rateLimit: {
|
|
96
|
+
enabled: options.rateLimit?.enabled || false,
|
|
97
|
+
windowMs: options.rateLimit?.windowMs || 60000, // 1 minute
|
|
98
|
+
maxRequests: options.rateLimit?.maxRequests || 100,
|
|
99
|
+
keyGenerator: options.rateLimit?.keyGenerator || null
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// Logging configuration
|
|
103
|
+
logging: {
|
|
104
|
+
enabled: options.logging?.enabled || false,
|
|
105
|
+
format: options.logging?.format || ':method :path :status :response-time ms',
|
|
106
|
+
verbose: options.logging?.verbose || false
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
// Compression configuration
|
|
110
|
+
compression: {
|
|
111
|
+
enabled: options.compression?.enabled || false,
|
|
112
|
+
threshold: options.compression?.threshold || 1024, // 1KB
|
|
113
|
+
level: options.compression?.level || 6
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// Validation configuration
|
|
117
|
+
validation: {
|
|
118
|
+
enabled: options.validation?.enabled !== false,
|
|
119
|
+
validateOnInsert: options.validation?.validateOnInsert !== false,
|
|
120
|
+
validateOnUpdate: options.validation?.validateOnUpdate !== false,
|
|
121
|
+
returnValidationErrors: options.validation?.returnValidationErrors !== false
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
// Content Security Policy (CSP) configuration
|
|
125
|
+
csp: {
|
|
126
|
+
enabled: options.csp?.enabled || false,
|
|
127
|
+
// Default CSP that works with Redoc v2.5.1 (allows CDN scripts/styles)
|
|
128
|
+
directives: options.csp?.directives || {
|
|
129
|
+
'default-src': ["'self'"],
|
|
130
|
+
'script-src': ["'self'", "'unsafe-inline'", 'https://cdn.redoc.ly/redoc/v2.5.1/'],
|
|
131
|
+
'style-src': ["'self'", "'unsafe-inline'", 'https://cdn.redoc.ly/redoc/v2.5.1/', 'https://fonts.googleapis.com'],
|
|
132
|
+
'font-src': ["'self'", 'https://fonts.gstatic.com'],
|
|
133
|
+
'img-src': ["'self'", 'data:', 'https:'],
|
|
134
|
+
'connect-src': ["'self'"]
|
|
135
|
+
},
|
|
136
|
+
reportOnly: options.csp?.reportOnly || false, // If true, uses Content-Security-Policy-Report-Only
|
|
137
|
+
reportUri: options.csp?.reportUri || null
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
// Custom global middlewares
|
|
141
|
+
middlewares: options.middlewares || []
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
this.server = null;
|
|
145
|
+
this.usersResource = null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Validate plugin dependencies
|
|
150
|
+
* @private
|
|
151
|
+
*/
|
|
152
|
+
async _validateDependencies() {
|
|
153
|
+
await requirePluginDependency('api-plugin', {
|
|
154
|
+
throwOnError: true,
|
|
155
|
+
checkVersions: true
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Install plugin
|
|
161
|
+
*/
|
|
162
|
+
async onInstall() {
|
|
163
|
+
if (this.config.verbose) {
|
|
164
|
+
console.log('[API Plugin] Installing...');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Validate dependencies
|
|
168
|
+
try {
|
|
169
|
+
await this._validateDependencies();
|
|
170
|
+
} catch (err) {
|
|
171
|
+
console.error('[API Plugin] Dependency validation failed:', err.message);
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Create users resource if authentication is enabled
|
|
176
|
+
const authEnabled = this.config.auth.jwt.enabled ||
|
|
177
|
+
this.config.auth.apiKey.enabled ||
|
|
178
|
+
this.config.auth.basic.enabled;
|
|
179
|
+
|
|
180
|
+
if (authEnabled) {
|
|
181
|
+
await this._createUsersResource();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Setup middlewares
|
|
185
|
+
await this._setupMiddlewares();
|
|
186
|
+
|
|
187
|
+
if (this.config.verbose) {
|
|
188
|
+
console.log('[API Plugin] Installed successfully');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Create users resource for authentication
|
|
194
|
+
* @private
|
|
195
|
+
*/
|
|
196
|
+
async _createUsersResource() {
|
|
197
|
+
const [ok, err, resource] = await tryFn(() =>
|
|
198
|
+
this.database.createResource({
|
|
199
|
+
name: 'plg_users',
|
|
200
|
+
attributes: {
|
|
201
|
+
id: 'string|required',
|
|
202
|
+
username: 'string|required|minlength:3',
|
|
203
|
+
email: 'string|optional|email',
|
|
204
|
+
password: 'secret|required|minlength:8',
|
|
205
|
+
apiKey: 'string|optional',
|
|
206
|
+
jwtSecret: 'string|optional',
|
|
207
|
+
role: 'string|default:user',
|
|
208
|
+
active: 'boolean|default:true',
|
|
209
|
+
createdAt: 'string|optional',
|
|
210
|
+
lastLoginAt: 'string|optional',
|
|
211
|
+
metadata: 'json|optional'
|
|
212
|
+
},
|
|
213
|
+
behavior: 'body-overflow',
|
|
214
|
+
timestamps: true,
|
|
215
|
+
createdBy: 'ApiPlugin'
|
|
216
|
+
})
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (ok) {
|
|
220
|
+
this.usersResource = resource;
|
|
221
|
+
if (this.config.verbose) {
|
|
222
|
+
console.log('[API Plugin] Created plg_users resource for authentication');
|
|
223
|
+
}
|
|
224
|
+
} else if (this.database.resources.plg_users) {
|
|
225
|
+
// Resource already exists
|
|
226
|
+
this.usersResource = this.database.resources.plg_users;
|
|
227
|
+
if (this.config.verbose) {
|
|
228
|
+
console.log('[API Plugin] Using existing plg_users resource');
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
throw err;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Setup middlewares
|
|
237
|
+
* @private
|
|
238
|
+
*/
|
|
239
|
+
async _setupMiddlewares() {
|
|
240
|
+
const middlewares = [];
|
|
241
|
+
|
|
242
|
+
// Add request ID middleware
|
|
243
|
+
middlewares.push(async (c, next) => {
|
|
244
|
+
c.set('requestId', crypto.randomUUID());
|
|
245
|
+
c.set('verbose', this.config.verbose);
|
|
246
|
+
await next();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Add CORS middleware
|
|
250
|
+
if (this.config.cors.enabled) {
|
|
251
|
+
const corsMiddleware = await this._createCorsMiddleware();
|
|
252
|
+
middlewares.push(corsMiddleware);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Add CSP middleware
|
|
256
|
+
if (this.config.csp.enabled) {
|
|
257
|
+
const cspMiddleware = await this._createCSPMiddleware();
|
|
258
|
+
middlewares.push(cspMiddleware);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Add rate limiting middleware
|
|
262
|
+
if (this.config.rateLimit.enabled) {
|
|
263
|
+
const rateLimitMiddleware = await this._createRateLimitMiddleware();
|
|
264
|
+
middlewares.push(rateLimitMiddleware);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Add logging middleware
|
|
268
|
+
if (this.config.logging.enabled) {
|
|
269
|
+
const loggingMiddleware = await this._createLoggingMiddleware();
|
|
270
|
+
middlewares.push(loggingMiddleware);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Add compression middleware
|
|
274
|
+
if (this.config.compression.enabled) {
|
|
275
|
+
const compressionMiddleware = await this._createCompressionMiddleware();
|
|
276
|
+
middlewares.push(compressionMiddleware);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Add custom middlewares
|
|
280
|
+
middlewares.push(...this.config.middlewares);
|
|
281
|
+
|
|
282
|
+
// Store compiled middlewares
|
|
283
|
+
this.compiledMiddlewares = middlewares;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Create CORS middleware (placeholder)
|
|
288
|
+
* @private
|
|
289
|
+
*/
|
|
290
|
+
async _createCorsMiddleware() {
|
|
291
|
+
return async (c, next) => {
|
|
292
|
+
const { origin, methods, allowedHeaders, exposedHeaders, credentials, maxAge } = this.config.cors;
|
|
293
|
+
|
|
294
|
+
// Set CORS headers
|
|
295
|
+
c.header('Access-Control-Allow-Origin', origin);
|
|
296
|
+
c.header('Access-Control-Allow-Methods', methods.join(', '));
|
|
297
|
+
c.header('Access-Control-Allow-Headers', allowedHeaders.join(', '));
|
|
298
|
+
c.header('Access-Control-Expose-Headers', exposedHeaders.join(', '));
|
|
299
|
+
|
|
300
|
+
if (credentials) {
|
|
301
|
+
c.header('Access-Control-Allow-Credentials', 'true');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
c.header('Access-Control-Max-Age', maxAge.toString());
|
|
305
|
+
|
|
306
|
+
// Handle OPTIONS preflight
|
|
307
|
+
if (c.req.method === 'OPTIONS') {
|
|
308
|
+
return c.body(null, 204);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
await next();
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Create CSP middleware
|
|
317
|
+
* @private
|
|
318
|
+
*/
|
|
319
|
+
async _createCSPMiddleware() {
|
|
320
|
+
return async (c, next) => {
|
|
321
|
+
const { directives, reportOnly, reportUri } = this.config.csp;
|
|
322
|
+
|
|
323
|
+
// Build CSP header value from directives
|
|
324
|
+
const cspParts = [];
|
|
325
|
+
for (const [directive, values] of Object.entries(directives)) {
|
|
326
|
+
if (Array.isArray(values) && values.length > 0) {
|
|
327
|
+
cspParts.push(`${directive} ${values.join(' ')}`);
|
|
328
|
+
} else if (typeof values === 'string') {
|
|
329
|
+
cspParts.push(`${directive} ${values}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Add report-uri if specified
|
|
334
|
+
if (reportUri) {
|
|
335
|
+
cspParts.push(`report-uri ${reportUri}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const cspValue = cspParts.join('; ');
|
|
339
|
+
|
|
340
|
+
// Set appropriate header (report-only or enforced)
|
|
341
|
+
const headerName = reportOnly
|
|
342
|
+
? 'Content-Security-Policy-Report-Only'
|
|
343
|
+
: 'Content-Security-Policy';
|
|
344
|
+
|
|
345
|
+
c.header(headerName, cspValue);
|
|
346
|
+
|
|
347
|
+
await next();
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Create rate limiting middleware (placeholder)
|
|
353
|
+
* @private
|
|
354
|
+
*/
|
|
355
|
+
async _createRateLimitMiddleware() {
|
|
356
|
+
const requests = new Map();
|
|
357
|
+
const { windowMs, maxRequests, keyGenerator } = this.config.rateLimit;
|
|
358
|
+
|
|
359
|
+
return async (c, next) => {
|
|
360
|
+
// Generate key (IP or custom)
|
|
361
|
+
const key = keyGenerator
|
|
362
|
+
? keyGenerator(c)
|
|
363
|
+
: c.req.header('x-forwarded-for') || c.req.header('cf-connecting-ip') || 'unknown';
|
|
364
|
+
|
|
365
|
+
// Get or create request count
|
|
366
|
+
if (!requests.has(key)) {
|
|
367
|
+
requests.set(key, { count: 0, resetAt: Date.now() + windowMs });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const record = requests.get(key);
|
|
371
|
+
|
|
372
|
+
// Reset if window expired
|
|
373
|
+
if (Date.now() > record.resetAt) {
|
|
374
|
+
record.count = 0;
|
|
375
|
+
record.resetAt = Date.now() + windowMs;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Check limit
|
|
379
|
+
if (record.count >= maxRequests) {
|
|
380
|
+
const retryAfter = Math.ceil((record.resetAt - Date.now()) / 1000);
|
|
381
|
+
c.header('Retry-After', retryAfter.toString());
|
|
382
|
+
c.header('X-RateLimit-Limit', maxRequests.toString());
|
|
383
|
+
c.header('X-RateLimit-Remaining', '0');
|
|
384
|
+
c.header('X-RateLimit-Reset', record.resetAt.toString());
|
|
385
|
+
|
|
386
|
+
return c.json({
|
|
387
|
+
success: false,
|
|
388
|
+
error: {
|
|
389
|
+
message: 'Rate limit exceeded',
|
|
390
|
+
code: 'RATE_LIMIT_EXCEEDED',
|
|
391
|
+
details: { retryAfter }
|
|
392
|
+
}
|
|
393
|
+
}, 429);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Increment count
|
|
397
|
+
record.count++;
|
|
398
|
+
|
|
399
|
+
// Set rate limit headers
|
|
400
|
+
c.header('X-RateLimit-Limit', maxRequests.toString());
|
|
401
|
+
c.header('X-RateLimit-Remaining', (maxRequests - record.count).toString());
|
|
402
|
+
c.header('X-RateLimit-Reset', record.resetAt.toString());
|
|
403
|
+
|
|
404
|
+
await next();
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Create logging middleware (placeholder)
|
|
410
|
+
* @private
|
|
411
|
+
*/
|
|
412
|
+
async _createLoggingMiddleware() {
|
|
413
|
+
return async (c, next) => {
|
|
414
|
+
const start = Date.now();
|
|
415
|
+
const method = c.req.method;
|
|
416
|
+
const path = c.req.path;
|
|
417
|
+
const requestId = c.get('requestId');
|
|
418
|
+
|
|
419
|
+
await next();
|
|
420
|
+
|
|
421
|
+
const duration = Date.now() - start;
|
|
422
|
+
const status = c.res.status;
|
|
423
|
+
const user = c.get('user')?.username || 'anonymous';
|
|
424
|
+
|
|
425
|
+
console.log(`[API Plugin] ${requestId} - ${method} ${path} ${status} ${duration}ms - ${user}`);
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Create compression middleware (placeholder)
|
|
431
|
+
* @private
|
|
432
|
+
*/
|
|
433
|
+
async _createCompressionMiddleware() {
|
|
434
|
+
return async (c, next) => {
|
|
435
|
+
await next();
|
|
436
|
+
|
|
437
|
+
// Note: Actual compression would require proper streaming support
|
|
438
|
+
// For now, this is a placeholder
|
|
439
|
+
const acceptEncoding = c.req.header('accept-encoding') || '';
|
|
440
|
+
|
|
441
|
+
if (acceptEncoding.includes('gzip')) {
|
|
442
|
+
c.header('Content-Encoding', 'gzip');
|
|
443
|
+
} else if (acceptEncoding.includes('deflate')) {
|
|
444
|
+
c.header('Content-Encoding', 'deflate');
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Start plugin
|
|
451
|
+
*/
|
|
452
|
+
async onStart() {
|
|
453
|
+
if (this.config.verbose) {
|
|
454
|
+
console.log('[API Plugin] Starting server...');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Create server instance
|
|
458
|
+
this.server = new ApiServer({
|
|
459
|
+
port: this.config.port,
|
|
460
|
+
host: this.config.host,
|
|
461
|
+
database: this.database,
|
|
462
|
+
resources: this.config.resources,
|
|
463
|
+
middlewares: this.compiledMiddlewares,
|
|
464
|
+
verbose: this.config.verbose,
|
|
465
|
+
auth: this.config.auth,
|
|
466
|
+
docsEnabled: this.config.docs.enabled,
|
|
467
|
+
docsUI: this.config.docs.ui,
|
|
468
|
+
apiTitle: this.config.docs.title,
|
|
469
|
+
apiVersion: this.config.docs.version,
|
|
470
|
+
apiDescription: this.config.docs.description
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Start server
|
|
474
|
+
await this.server.start();
|
|
475
|
+
|
|
476
|
+
this.emit('plugin.started', {
|
|
477
|
+
port: this.config.port,
|
|
478
|
+
host: this.config.host
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Stop plugin
|
|
484
|
+
*/
|
|
485
|
+
async onStop() {
|
|
486
|
+
if (this.config.verbose) {
|
|
487
|
+
console.log('[API Plugin] Stopping server...');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (this.server) {
|
|
491
|
+
await this.server.stop();
|
|
492
|
+
this.server = null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
this.emit('plugin.stopped');
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Uninstall plugin
|
|
500
|
+
*/
|
|
501
|
+
async onUninstall(options = {}) {
|
|
502
|
+
const { purgeData = false } = options;
|
|
503
|
+
|
|
504
|
+
// Stop server if running
|
|
505
|
+
await this.onStop();
|
|
506
|
+
|
|
507
|
+
// Optionally delete users resource
|
|
508
|
+
if (purgeData && this.usersResource) {
|
|
509
|
+
// Delete all users (plugin data cleanup happens automatically via base Plugin class)
|
|
510
|
+
const [ok] = await tryFn(() => this.database.deleteResource('plg_users'));
|
|
511
|
+
|
|
512
|
+
if (ok && this.config.verbose) {
|
|
513
|
+
console.log('[API Plugin] Deleted plg_users resource');
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (this.config.verbose) {
|
|
518
|
+
console.log('[API Plugin] Uninstalled successfully');
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Get server information
|
|
524
|
+
* @returns {Object} Server info
|
|
525
|
+
*/
|
|
526
|
+
getServerInfo() {
|
|
527
|
+
return this.server ? this.server.getInfo() : { isRunning: false };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Get Hono app instance (for advanced usage)
|
|
532
|
+
* @returns {Hono|null} Hono app
|
|
533
|
+
*/
|
|
534
|
+
getApp() {
|
|
535
|
+
return this.server ? this.server.getApp() : null;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export default ApiPlugin;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middlewares - Export all API middlewares
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { createValidationMiddleware, createQueryValidation, listQueryValidation } from './validator.js';
|
|
6
|
+
|
|
7
|
+
// Note: CORS, Rate Limiting, Logging, and Compression middlewares
|
|
8
|
+
// are currently implemented in api.plugin.js as inline functions.
|
|
9
|
+
// They can be extracted to separate files if needed for better organization.
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
createValidationMiddleware,
|
|
13
|
+
createQueryValidation,
|
|
14
|
+
listQueryValidation
|
|
15
|
+
};
|