s3db.js 13.5.1 → 13.6.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 (108) hide show
  1. package/README.md +89 -19
  2. package/dist/{s3db.cjs.js → s3db.cjs} +29780 -24384
  3. package/dist/s3db.cjs.map +1 -0
  4. package/dist/s3db.es.js +24263 -18860
  5. package/dist/s3db.es.js.map +1 -1
  6. package/package.json +227 -21
  7. package/src/concerns/id.js +90 -6
  8. package/src/concerns/index.js +2 -1
  9. package/src/concerns/password-hashing.js +150 -0
  10. package/src/database.class.js +4 -0
  11. package/src/plugins/api/auth/basic-auth.js +23 -1
  12. package/src/plugins/api/auth/index.js +49 -3
  13. package/src/plugins/api/auth/oauth2-auth.js +171 -0
  14. package/src/plugins/api/auth/oidc-auth.js +789 -0
  15. package/src/plugins/api/auth/oidc-client.js +462 -0
  16. package/src/plugins/api/auth/path-auth-matcher.js +284 -0
  17. package/src/plugins/api/concerns/event-emitter.js +134 -0
  18. package/src/plugins/api/concerns/failban-manager.js +651 -0
  19. package/src/plugins/api/concerns/guards-helpers.js +402 -0
  20. package/src/plugins/api/concerns/metrics-collector.js +346 -0
  21. package/src/plugins/api/concerns/opengraph-helper.js +116 -0
  22. package/src/plugins/api/concerns/state-machine.js +288 -0
  23. package/src/plugins/api/index.js +514 -54
  24. package/src/plugins/api/middlewares/failban.js +305 -0
  25. package/src/plugins/api/middlewares/rate-limit.js +301 -0
  26. package/src/plugins/api/middlewares/request-id.js +74 -0
  27. package/src/plugins/api/middlewares/security-headers.js +120 -0
  28. package/src/plugins/api/middlewares/session-tracking.js +194 -0
  29. package/src/plugins/api/routes/auth-routes.js +23 -3
  30. package/src/plugins/api/routes/resource-routes.js +71 -29
  31. package/src/plugins/api/server.js +1017 -94
  32. package/src/plugins/api/utils/guards.js +213 -0
  33. package/src/plugins/api/utils/mime-types.js +154 -0
  34. package/src/plugins/api/utils/openapi-generator.js +44 -11
  35. package/src/plugins/api/utils/path-matcher.js +173 -0
  36. package/src/plugins/api/utils/static-filesystem.js +262 -0
  37. package/src/plugins/api/utils/static-s3.js +231 -0
  38. package/src/plugins/api/utils/template-engine.js +262 -0
  39. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
  40. package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
  41. package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
  42. package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
  43. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
  44. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
  45. package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
  46. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
  47. package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
  48. package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
  49. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
  50. package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
  51. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
  52. package/src/plugins/cloud-inventory/index.js +20 -0
  53. package/src/plugins/cloud-inventory/registry.js +146 -0
  54. package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
  55. package/src/plugins/cloud-inventory.plugin.js +1333 -0
  56. package/src/plugins/concerns/plugin-dependencies.js +61 -1
  57. package/src/plugins/eventual-consistency/analytics.js +1 -0
  58. package/src/plugins/identity/README.md +335 -0
  59. package/src/plugins/identity/concerns/mfa-manager.js +204 -0
  60. package/src/plugins/identity/concerns/password.js +138 -0
  61. package/src/plugins/identity/concerns/resource-schemas.js +273 -0
  62. package/src/plugins/identity/concerns/token-generator.js +172 -0
  63. package/src/plugins/identity/email-service.js +422 -0
  64. package/src/plugins/identity/index.js +1052 -0
  65. package/src/plugins/identity/oauth2-server.js +1033 -0
  66. package/src/plugins/identity/oidc-discovery.js +285 -0
  67. package/src/plugins/identity/rsa-keys.js +323 -0
  68. package/src/plugins/identity/server.js +500 -0
  69. package/src/plugins/identity/session-manager.js +453 -0
  70. package/src/plugins/identity/ui/layouts/base.js +251 -0
  71. package/src/plugins/identity/ui/middleware.js +135 -0
  72. package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
  73. package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
  74. package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
  75. package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
  76. package/src/plugins/identity/ui/pages/admin/users.js +263 -0
  77. package/src/plugins/identity/ui/pages/consent.js +262 -0
  78. package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
  79. package/src/plugins/identity/ui/pages/login.js +144 -0
  80. package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
  81. package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
  82. package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
  83. package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
  84. package/src/plugins/identity/ui/pages/profile.js +361 -0
  85. package/src/plugins/identity/ui/pages/register.js +226 -0
  86. package/src/plugins/identity/ui/pages/reset-password.js +128 -0
  87. package/src/plugins/identity/ui/pages/verify-email.js +172 -0
  88. package/src/plugins/identity/ui/routes.js +2541 -0
  89. package/src/plugins/identity/ui/styles/main.css +465 -0
  90. package/src/plugins/index.js +4 -1
  91. package/src/plugins/ml/base-model.class.js +32 -7
  92. package/src/plugins/ml/classification-model.class.js +1 -1
  93. package/src/plugins/ml/timeseries-model.class.js +3 -1
  94. package/src/plugins/ml.plugin.js +124 -32
  95. package/src/plugins/shared/error-handler.js +147 -0
  96. package/src/plugins/shared/index.js +9 -0
  97. package/src/plugins/shared/middlewares/compression.js +117 -0
  98. package/src/plugins/shared/middlewares/cors.js +49 -0
  99. package/src/plugins/shared/middlewares/index.js +11 -0
  100. package/src/plugins/shared/middlewares/logging.js +54 -0
  101. package/src/plugins/shared/middlewares/rate-limit.js +73 -0
  102. package/src/plugins/shared/middlewares/security.js +158 -0
  103. package/src/plugins/shared/response-formatter.js +264 -0
  104. package/src/plugins/tfstate/README.md +126 -126
  105. package/src/resource.class.js +140 -12
  106. package/src/schema.class.js +30 -1
  107. package/src/validator.class.js +57 -6
  108. package/dist/s3db.cjs.js.map +0 -1
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Security Headers Middleware
3
+ *
4
+ * Adds standard security headers to all responses for enhanced protection.
5
+ * Helps prevent common web vulnerabilities like XSS, clickjacking, and MIME sniffing.
6
+ *
7
+ * Headers included:
8
+ * - Content-Security-Policy (CSP): Prevents XSS and data injection attacks
9
+ * - Strict-Transport-Security (HSTS): Forces HTTPS connections
10
+ * - X-Frame-Options: Prevents clickjacking
11
+ * - X-Content-Type-Options: Prevents MIME sniffing
12
+ * - Referrer-Policy: Controls referer information
13
+ * - X-XSS-Protection: Legacy XSS protection (for older browsers)
14
+ *
15
+ * @example
16
+ * import { createSecurityHeadersMiddleware } from './middlewares/security-headers.js';
17
+ *
18
+ * const middleware = createSecurityHeadersMiddleware({
19
+ * headers: {
20
+ * csp: "default-src 'self'; script-src 'self' 'unsafe-inline'",
21
+ * hsts: { maxAge: 31536000, includeSubDomains: true },
22
+ * xFrameOptions: 'DENY'
23
+ * }
24
+ * });
25
+ *
26
+ * app.use('*', middleware);
27
+ */
28
+
29
+ /**
30
+ * Create security headers middleware
31
+ *
32
+ * @param {Object} config - Security configuration
33
+ * @param {Object} config.headers - Header configuration
34
+ * @param {string} config.headers.csp - Content Security Policy
35
+ * @param {Object} config.headers.hsts - HSTS configuration
36
+ * @param {number} config.headers.hsts.maxAge - HSTS max age in seconds
37
+ * @param {boolean} config.headers.hsts.includeSubDomains - Include subdomains
38
+ * @param {boolean} config.headers.hsts.preload - Enable HSTS preload
39
+ * @param {string} config.headers.xFrameOptions - X-Frame-Options value (DENY, SAMEORIGIN, ALLOW-FROM)
40
+ * @param {string} config.headers.xContentTypeOptions - X-Content-Type-Options (nosniff)
41
+ * @param {string} config.headers.referrerPolicy - Referrer-Policy value
42
+ * @param {string} config.headers.xssProtection - X-XSS-Protection value
43
+ * @returns {Function} Hono middleware
44
+ */
45
+ export function createSecurityHeadersMiddleware(config = {}) {
46
+ const defaults = {
47
+ csp: "default-src 'self'",
48
+ hsts: { maxAge: 31536000, includeSubDomains: true, preload: false },
49
+ xFrameOptions: 'DENY',
50
+ xContentTypeOptions: 'nosniff',
51
+ referrerPolicy: 'strict-origin-when-cross-origin',
52
+ xssProtection: '1; mode=block',
53
+ permissionsPolicy: 'geolocation=(), microphone=(), camera=()'
54
+ };
55
+
56
+ const settings = {
57
+ ...defaults,
58
+ ...(config.headers || {})
59
+ };
60
+
61
+ // Merge HSTS settings
62
+ if (config.headers?.hsts && typeof config.headers.hsts === 'object') {
63
+ settings.hsts = {
64
+ ...defaults.hsts,
65
+ ...config.headers.hsts
66
+ };
67
+ }
68
+
69
+ return async (c, next) => {
70
+ // Content Security Policy
71
+ if (settings.csp) {
72
+ c.header('Content-Security-Policy', settings.csp);
73
+ }
74
+
75
+ // HTTP Strict Transport Security
76
+ if (settings.hsts) {
77
+ const hsts = settings.hsts;
78
+ let hstsValue = `max-age=${hsts.maxAge}`;
79
+
80
+ if (hsts.includeSubDomains) {
81
+ hstsValue += '; includeSubDomains';
82
+ }
83
+
84
+ if (hsts.preload) {
85
+ hstsValue += '; preload';
86
+ }
87
+
88
+ c.header('Strict-Transport-Security', hstsValue);
89
+ }
90
+
91
+ // X-Frame-Options
92
+ if (settings.xFrameOptions) {
93
+ c.header('X-Frame-Options', settings.xFrameOptions);
94
+ }
95
+
96
+ // X-Content-Type-Options
97
+ if (settings.xContentTypeOptions) {
98
+ c.header('X-Content-Type-Options', settings.xContentTypeOptions);
99
+ }
100
+
101
+ // Referrer-Policy
102
+ if (settings.referrerPolicy) {
103
+ c.header('Referrer-Policy', settings.referrerPolicy);
104
+ }
105
+
106
+ // X-XSS-Protection (legacy, but still useful for older browsers)
107
+ if (settings.xssProtection) {
108
+ c.header('X-XSS-Protection', settings.xssProtection);
109
+ }
110
+
111
+ // Permissions-Policy (formerly Feature-Policy)
112
+ if (settings.permissionsPolicy) {
113
+ c.header('Permissions-Policy', settings.permissionsPolicy);
114
+ }
115
+
116
+ await next();
117
+ };
118
+ }
119
+
120
+ export default createSecurityHeadersMiddleware;
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Session Tracking Middleware
3
+ *
4
+ * Tracks user sessions for analytics and monitoring purposes.
5
+ * Creates persistent session IDs stored in encrypted cookies.
6
+ *
7
+ * Features:
8
+ * - Encrypted session IDs (AES-256-GCM)
9
+ * - Optional database storage
10
+ * - Auto-update on each request
11
+ * - Custom session enrichment
12
+ * - IP, User-Agent, Referer tracking
13
+ *
14
+ * @example
15
+ * import { createSessionTrackingMiddleware } from './middlewares/session-tracking.js';
16
+ *
17
+ * const middleware = createSessionTrackingMiddleware({
18
+ * enabled: true,
19
+ * resource: 'sessions',
20
+ * cookieName: 'session_id',
21
+ * passphrase: process.env.SESSION_SECRET,
22
+ * updateOnRequest: true,
23
+ * enrichSession: async ({ session, context }) => ({
24
+ * userAgent: context.req.header('user-agent'),
25
+ * ip: context.req.header('x-forwarded-for')
26
+ * })
27
+ * }, db);
28
+ *
29
+ * app.use('*', middleware);
30
+ *
31
+ * // In route handlers:
32
+ * app.get('/r/:id', async (c) => {
33
+ * const sessionId = c.get('sessionId');
34
+ * const session = c.get('session');
35
+ * console.log('Session:', sessionId);
36
+ * });
37
+ */
38
+
39
+ import { encrypt, decrypt } from '../../../concerns/crypto.js';
40
+ import { idGenerator } from '../../../concerns/id.js';
41
+
42
+ /**
43
+ * Create session tracking middleware
44
+ *
45
+ * @param {Object} config - Session configuration
46
+ * @param {boolean} config.enabled - Enable session tracking (default: false)
47
+ * @param {string} config.resource - Resource name for DB storage (optional)
48
+ * @param {string} config.cookieName - Cookie name (default: 'session_id')
49
+ * @param {number} config.cookieMaxAge - Cookie max age in ms (default: 30 days)
50
+ * @param {boolean} config.cookieSecure - Secure flag (default: production mode)
51
+ * @param {string} config.cookieSameSite - SameSite policy (default: 'Strict')
52
+ * @param {boolean} config.updateOnRequest - Update session on each request (default: true)
53
+ * @param {string} config.passphrase - Encryption passphrase (required)
54
+ * @param {Function} config.enrichSession - Custom session enrichment function
55
+ * @param {Object} db - Database instance
56
+ * @returns {Function} Hono middleware
57
+ */
58
+ export function createSessionTrackingMiddleware(config = {}, db) {
59
+ const {
60
+ enabled = false,
61
+ resource = null,
62
+ cookieName = 'session_id',
63
+ cookieMaxAge = 2592000000, // 30 days
64
+ cookieSecure = process.env.NODE_ENV === 'production',
65
+ cookieSameSite = 'Strict',
66
+ updateOnRequest = true,
67
+ passphrase = null,
68
+ enrichSession = null
69
+ } = config;
70
+
71
+ // If disabled, return no-op middleware
72
+ if (!enabled) {
73
+ return async (c, next) => await next();
74
+ }
75
+
76
+ // Validate required config
77
+ if (!passphrase) {
78
+ throw new Error('sessionTracking.passphrase is required when sessionTracking.enabled = true');
79
+ }
80
+
81
+ // Get sessions resource if configured
82
+ const sessionsResource = resource && db ? db.resources[resource] : null;
83
+
84
+ return async (c, next) => {
85
+ let session = null;
86
+ let sessionId = null;
87
+ let isNewSession = false;
88
+
89
+ // 1. Check if session cookie exists
90
+ const sessionCookie = c.req.cookie(cookieName);
91
+
92
+ if (sessionCookie) {
93
+ try {
94
+ // Decrypt session ID
95
+ sessionId = await decrypt(sessionCookie, passphrase);
96
+
97
+ // Load from DB if resource configured
98
+ if (sessionsResource) {
99
+ const exists = await sessionsResource.exists(sessionId);
100
+ if (exists) {
101
+ session = await sessionsResource.get(sessionId);
102
+ }
103
+ } else {
104
+ // No DB storage - create minimal session object
105
+ session = { id: sessionId };
106
+ }
107
+ } catch (err) {
108
+ console.error('[SessionTracking] Failed to decrypt cookie:', err.message);
109
+ // Will create new session below
110
+ }
111
+ }
112
+
113
+ // 2. Create new session if needed
114
+ if (!session) {
115
+ isNewSession = true;
116
+ sessionId = idGenerator();
117
+
118
+ const sessionData = {
119
+ id: sessionId,
120
+ userAgent: c.req.header('user-agent') || null,
121
+ ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || null,
122
+ referer: c.req.header('referer') || null,
123
+ createdAt: new Date().toISOString(),
124
+ lastSeenAt: new Date().toISOString()
125
+ };
126
+
127
+ // Enrich with custom data
128
+ if (enrichSession && typeof enrichSession === 'function') {
129
+ try {
130
+ const enriched = await enrichSession({ session: sessionData, context: c });
131
+ if (enriched && typeof enriched === 'object') {
132
+ Object.assign(sessionData, enriched);
133
+ }
134
+ } catch (enrichErr) {
135
+ console.error('[SessionTracking] enrichSession failed:', enrichErr.message);
136
+ }
137
+ }
138
+
139
+ // Save to DB if resource configured
140
+ if (sessionsResource) {
141
+ try {
142
+ session = await sessionsResource.insert(sessionData);
143
+ } catch (insertErr) {
144
+ console.error('[SessionTracking] Failed to insert session:', insertErr.message);
145
+ session = sessionData; // Use in-memory fallback
146
+ }
147
+ } else {
148
+ session = sessionData;
149
+ }
150
+ }
151
+
152
+ // 3. Update session on each request (if enabled and not new)
153
+ else if (updateOnRequest && !isNewSession && sessionsResource) {
154
+ const updates = {
155
+ lastSeenAt: new Date().toISOString(),
156
+ lastUserAgent: c.req.header('user-agent') || null,
157
+ lastIp: c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || null
158
+ };
159
+
160
+ // Fire-and-forget update (don't block request)
161
+ sessionsResource.update(sessionId, updates).catch((updateErr) => {
162
+ console.error('[SessionTracking] Failed to update session:', updateErr.message);
163
+ });
164
+
165
+ // Update local copy
166
+ Object.assign(session, updates);
167
+ }
168
+
169
+ // 4. Set/refresh cookie
170
+ try {
171
+ const encryptedSessionId = await encrypt(sessionId, passphrase);
172
+
173
+ c.header(
174
+ 'Set-Cookie',
175
+ `${cookieName}=${encryptedSessionId}; ` +
176
+ `Max-Age=${Math.floor(cookieMaxAge / 1000)}; ` +
177
+ `Path=/; ` +
178
+ `HttpOnly; ` +
179
+ (cookieSecure ? 'Secure; ' : '') +
180
+ `SameSite=${cookieSameSite}`
181
+ );
182
+ } catch (encryptErr) {
183
+ console.error('[SessionTracking] Failed to encrypt session ID:', encryptErr.message);
184
+ }
185
+
186
+ // 5. Expose to context
187
+ c.set('sessionId', sessionId);
188
+ c.set('session', session);
189
+
190
+ await next();
191
+ };
192
+ }
193
+
194
+ export default createSessionTrackingMiddleware;
@@ -67,8 +67,9 @@ export function createAuthRoutes(authResource, config = {}) {
67
67
 
68
68
  // Create user with dynamic fields
69
69
  // Only include fields from request + required auth fields
70
+ const { id, ...dataWithoutId } = data; // Exclude id from request data
70
71
  const userData = {
71
- ...data, // Include all fields from request first
72
+ ...dataWithoutId, // Include all fields from request except id
72
73
  [usernameField]: username, // Override to ensure correct value
73
74
  [passwordField]: password // Will be auto-encrypted by schema (secret field)
74
75
  };
@@ -139,8 +140,27 @@ export function createAuthRoutes(authResource, config = {}) {
139
140
  }
140
141
 
141
142
  // Verify password (compare with password field)
142
- // Schema handles encryption/decryption for 'secret' field types
143
- const isValid = user[passwordField] === password;
143
+ // For 'password' field type (bcrypt hash), use verifyPassword
144
+ // For 'secret' field type (AES encryption), compare directly
145
+ let isValid = false;
146
+
147
+ const storedPassword = user[passwordField];
148
+ if (!storedPassword) {
149
+ const response = formatter.unauthorized('Invalid credentials');
150
+ return c.json(response, response._status);
151
+ }
152
+
153
+ // Check if it's a bcrypt hash (starts with $ or is compacted 53 chars)
154
+ const isBcryptHash = storedPassword.startsWith('$') || (storedPassword.length === 53 && !storedPassword.includes(':'));
155
+
156
+ if (isBcryptHash) {
157
+ // Import verifyPassword for bcrypt hashes
158
+ const { verifyPassword } = await import('../../../concerns/password-hashing.js');
159
+ isValid = await verifyPassword(password, storedPassword);
160
+ } else {
161
+ // For encrypted/secret fields, direct comparison
162
+ isValid = storedPassword === password;
163
+ }
144
164
 
145
165
  if (!isValid) {
146
166
  const response = formatter.unauthorized('Invalid credentials');
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { asyncHandler } from '../utils/error-handler.js';
8
8
  import * as formatter from '../utils/response-formatter.js';
9
+ import { guardMiddleware } from '../utils/guards.js';
9
10
 
10
11
  /**
11
12
  * Parse custom route definition (e.g., "GET /healthcheck" or "async POST /custom")
@@ -59,12 +60,16 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
59
60
  methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
60
61
  customMiddleware = [],
61
62
  enableValidation = true,
62
- versionPrefix = '' // Empty string by default (calculated in server.js)
63
+ versionPrefix = '', // Empty string by default (calculated in server.js)
64
+ events = null // Event emitter for lifecycle hooks
63
65
  } = config;
64
66
 
65
67
  const resourceName = resource.name;
66
68
  const basePath = versionPrefix ? `/${versionPrefix}/${resourceName}` : `/${resourceName}`;
67
69
 
70
+ // Get guards configuration from resource config
71
+ const guards = resource.config?.guards || null;
72
+
68
73
  // Apply custom middleware
69
74
  customMiddleware.forEach(middleware => {
70
75
  app.use('*', middleware);
@@ -112,7 +117,7 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
112
117
 
113
118
  // LIST - GET /{version}/{resource}
114
119
  if (methods.includes('GET')) {
115
- app.get('/', asyncHandler(async (c) => {
120
+ app.get('/', guardMiddleware(guards, 'list'), asyncHandler(async (c) => {
116
121
  const query = c.req.query();
117
122
  const limit = parseInt(query.limit) || 100;
118
123
  const offset = parseInt(query.offset) || 0;
@@ -141,22 +146,23 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
141
146
 
142
147
  // Use query if filters are present
143
148
  if (Object.keys(filters).length > 0) {
144
- items = await resource.query(filters, { limit: limit + offset });
145
- items = items.slice(offset, offset + limit);
149
+ // Query with native offset support (efficient!)
150
+ items = await resource.query(filters, { limit, offset });
151
+ // Note: total is approximate (length of returned items)
152
+ // For exact total count with filters, would need separate count query
146
153
  total = items.length;
147
154
  } else if (partition && partitionValues) {
148
155
  // Query specific partition
149
156
  items = await resource.listPartition({
150
157
  partition,
151
158
  partitionValues,
152
- limit: limit + offset
159
+ limit,
160
+ offset
153
161
  });
154
- items = items.slice(offset, offset + limit);
155
162
  total = items.length;
156
163
  } else {
157
164
  // Regular list
158
- items = await resource.list({ limit: limit + offset });
159
- items = items.slice(offset, offset + limit);
165
+ items = await resource.list({ limit, offset });
160
166
  total = items.length;
161
167
  }
162
168
 
@@ -177,7 +183,7 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
177
183
 
178
184
  // GET ONE - GET /{version}/{resource}/:id
179
185
  if (methods.includes('GET')) {
180
- app.get('/:id', asyncHandler(async (c) => {
186
+ app.get('/:id', guardMiddleware(guards, 'get'), asyncHandler(async (c) => {
181
187
  const id = c.req.param('id');
182
188
  const query = c.req.query();
183
189
  const partition = query.partition;
@@ -211,12 +217,22 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
211
217
 
212
218
  // CREATE - POST /{version}/{resource}
213
219
  if (methods.includes('POST')) {
214
- app.post('/', asyncHandler(async (c) => {
220
+ app.post('/', guardMiddleware(guards, 'create'), asyncHandler(async (c) => {
215
221
  const data = await c.req.json();
216
222
 
217
223
  // Validation middleware will run if enabled
218
224
  const item = await resource.insert(data);
219
225
 
226
+ // Emit resource:created event
227
+ if (events) {
228
+ events.emitResourceEvent('created', {
229
+ resource: resourceName,
230
+ id: item.id,
231
+ data: item,
232
+ user: c.get('user')
233
+ });
234
+ }
235
+
220
236
  const location = `${basePath}/${item.id}`;
221
237
  const response = formatter.created(item, location);
222
238
 
@@ -227,7 +243,7 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
227
243
 
228
244
  // UPDATE (full) - PUT /{version}/{resource}/:id
229
245
  if (methods.includes('PUT')) {
230
- app.put('/:id', asyncHandler(async (c) => {
246
+ app.put('/:id', guardMiddleware(guards, 'update'), asyncHandler(async (c) => {
231
247
  const id = c.req.param('id');
232
248
  const data = await c.req.json();
233
249
 
@@ -241,6 +257,17 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
241
257
  // Full update
242
258
  const updated = await resource.update(id, data);
243
259
 
260
+ // Emit resource:updated event
261
+ if (events) {
262
+ events.emitResourceEvent('updated', {
263
+ resource: resourceName,
264
+ id: updated.id,
265
+ data: updated,
266
+ previous: existing,
267
+ user: c.get('user')
268
+ });
269
+ }
270
+
244
271
  const response = formatter.success(updated);
245
272
  return c.json(response, response._status);
246
273
  }));
@@ -248,7 +275,7 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
248
275
 
249
276
  // UPDATE (partial) - PATCH /{version}/{resource}/:id
250
277
  if (methods.includes('PATCH')) {
251
- app.patch('/:id', asyncHandler(async (c) => {
278
+ app.patch('/:id', guardMiddleware(guards, 'update'), asyncHandler(async (c) => {
252
279
  const id = c.req.param('id');
253
280
  const data = await c.req.json();
254
281
 
@@ -263,6 +290,18 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
263
290
  const merged = { ...existing, ...data, id };
264
291
  const updated = await resource.update(id, merged);
265
292
 
293
+ // Emit resource:updated event
294
+ if (events) {
295
+ events.emitResourceEvent('updated', {
296
+ resource: resourceName,
297
+ id: updated.id,
298
+ data: updated,
299
+ previous: existing,
300
+ partial: true,
301
+ user: c.get('user')
302
+ });
303
+ }
304
+
266
305
  const response = formatter.success(updated);
267
306
  return c.json(response, response._status);
268
307
  }));
@@ -270,7 +309,7 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
270
309
 
271
310
  // DELETE - DELETE /{version}/{resource}/:id
272
311
  if (methods.includes('DELETE')) {
273
- app.delete('/:id', asyncHandler(async (c) => {
312
+ app.delete('/:id', guardMiddleware(guards, 'delete'), asyncHandler(async (c) => {
274
313
  const id = c.req.param('id');
275
314
 
276
315
  // Check if exists
@@ -282,6 +321,16 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
282
321
 
283
322
  await resource.delete(id);
284
323
 
324
+ // Emit resource:deleted event
325
+ if (events) {
326
+ events.emitResourceEvent('deleted', {
327
+ resource: resourceName,
328
+ id,
329
+ previous: existing,
330
+ user: c.get('user')
331
+ });
332
+ }
333
+
285
334
  const response = formatter.noContent();
286
335
  return c.json(response, response._status);
287
336
  }));
@@ -292,22 +341,11 @@ export function createResourceRoutes(resource, version, config = {}, Hono) {
292
341
  app.on('HEAD', '/', asyncHandler(async (c) => {
293
342
  // Get statistics
294
343
  const total = await resource.count();
344
+ const version = resource.config?.currentVersion || resource.version || 'v1';
295
345
 
296
- // Get all items to calculate stats (for small datasets)
297
- // For large datasets, this might need optimization
298
- const allItems = await resource.list({ limit: 1000 });
299
-
300
- // Calculate statistics
301
- const stats = {
302
- total,
303
- version: resource.config?.currentVersion || resource.version || 'v1'
304
- };
305
-
306
- // Add resource-specific stats
346
+ // Set resource metadata headers
307
347
  c.header('X-Total-Count', total.toString());
308
- c.header('X-Resource-Version', stats.version);
309
-
310
- // Add schema info
348
+ c.header('X-Resource-Version', version);
311
349
  c.header('X-Schema-Fields', Object.keys(resource.config?.attributes || {}).length.toString());
312
350
 
313
351
  return c.body(null, 200);
@@ -393,8 +431,12 @@ export function createRelationalRoutes(sourceResource, relationName, relationCon
393
431
 
394
432
  // GET /{version}/{resource}/:id/{relation}
395
433
  // Examples: GET /v1/users/user123/posts, GET /v1/users/user123/profile
396
- app.get('/:id', asyncHandler(async (c) => {
397
- const id = c.req.param('id');
434
+ // Note: The :id param comes from parent route mounting (see server.js:469)
435
+ app.get('/', asyncHandler(async (c) => {
436
+ // Get parent route's :id param
437
+ const pathParts = c.req.path.split('/');
438
+ const relationNameIndex = pathParts.lastIndexOf(relationName);
439
+ const id = pathParts[relationNameIndex - 1];
398
440
  const query = c.req.query();
399
441
 
400
442
  // Check if source resource exists