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
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { asyncHandler } from './error-handler.js';
9
+ import { withContext, autoWrapHandler } from '../concerns/route-context.js';
9
10
 
10
11
  /**
11
12
  * Parse route definition from key
@@ -31,30 +32,41 @@ export function parseRouteKey(key) {
31
32
  * @param {Object} routes - Routes object { 'METHOD /path': handler }
32
33
  * @param {Object} context - Context to pass to handlers (resource, database, etc.)
33
34
  * @param {boolean} verbose - Enable verbose logging
35
+ * @param {Object} options - Additional options
36
+ * @param {boolean} options.autoWrap - Auto-wrap handlers with enhanced context (default: true)
34
37
  */
35
- export function mountCustomRoutes(app, routes, context = {}, verbose = false) {
38
+ export function mountCustomRoutes(app, routes, context = {}, verbose = false, options = {}) {
36
39
  if (!routes || typeof routes !== 'object') {
37
40
  return;
38
41
  }
39
42
 
43
+ const { autoWrap = true } = options;
44
+
40
45
  for (const [key, handler] of Object.entries(routes)) {
41
46
  try {
42
47
  const { method, path } = parseRouteKey(key);
43
48
 
44
49
  // Wrap handler with async error handler and context
45
50
  const wrappedHandler = asyncHandler(async (c) => {
46
- // Inject context into Hono context
51
+ // Inject legacy context into Hono context (for backward compatibility)
47
52
  c.set('customRouteContext', context);
48
53
 
49
- // Call user handler with Hono context
50
- return await handler(c);
54
+ // Auto-wrap handler if enabled and handler expects 2 arguments (c, ctx)
55
+ if (autoWrap && handler.length === 2) {
56
+ // Handler expects (c, ctx) - wrap it with enhanced context
57
+ return await withContext(handler, { resource: context.resource })(c);
58
+ } else {
59
+ // Handler expects only (c) - use legacy behavior
60
+ return await handler(c);
61
+ }
51
62
  });
52
63
 
53
64
  // Mount route
54
65
  app.on(method, path, wrappedHandler);
55
66
 
56
67
  if (verbose) {
57
- console.log(`[Custom Routes] Mounted ${method} ${path}`);
68
+ const contextType = (autoWrap && handler.length === 2) ? '(enhanced)' : '(legacy)';
69
+ console.log(`[Custom Routes] Mounted ${method} ${path} ${contextType}`);
58
70
  }
59
71
  } catch (err) {
60
72
  console.error(`[Custom Routes] Error mounting route "${key}":`, err.message);
@@ -3,21 +3,53 @@
3
3
  *
4
4
  * Guards determine if a user can perform an operation on a resource.
5
5
  * Supports: functions, scopes, roles, and combined logic.
6
+ *
7
+ * NEW: Guards can now receive full RouteContext for access to:
8
+ * - ctx.user, ctx.resources, ctx.param(), ctx.query()
9
+ * - ctx.setPartition() for tenant isolation
6
10
  */
7
11
 
8
12
  /**
9
- * Check if user passes guard
10
- * @param {Object} user - Authenticated user object
13
+ * Check if guard passes (NEW: supports RouteContext)
14
+ *
15
+ * @param {Object|RouteContext} ctxOrUser - RouteContext (new) or user object (legacy)
11
16
  * @param {Function|string|Array|Object|null} guard - Guard configuration
12
- * @param {Object} context - Additional context (data, resourceName, operation)
17
+ * @param {Object|null} recordOrContext - Record being accessed (update/delete) or legacy context
13
18
  * @returns {boolean} True if authorized
19
+ *
20
+ * @example
21
+ * // NEW: Guard with RouteContext
22
+ * users.guard = {
23
+ * list: (ctx) => {
24
+ * if (ctx.user.scopes?.includes('preset:admin')) return true;
25
+ * ctx.setPartition('byUserId', { userId: ctx.user.id });
26
+ * return true;
27
+ * }
28
+ * };
29
+ *
30
+ * @example
31
+ * // LEGACY: Guard with user object (still works!)
32
+ * users.guard = {
33
+ * list: (user, context) => {
34
+ * return user.scopes?.includes('preset:admin');
35
+ * }
36
+ * };
14
37
  */
15
- export function checkGuard(user, guard, context = {}) {
38
+ export function checkGuard(ctxOrUser, guard, recordOrContext = null) {
16
39
  // No guard = public access
17
40
  if (!guard) {
18
41
  return true;
19
42
  }
20
43
 
44
+ // Detect if first param is RouteContext (has .user property and ._currentResource)
45
+ const isRouteContext = ctxOrUser && typeof ctxOrUser === 'object' &&
46
+ ('user' in ctxOrUser || '_currentResource' in ctxOrUser);
47
+
48
+ const ctx = isRouteContext ? ctxOrUser : null;
49
+ const user = isRouteContext ? ctxOrUser.user : ctxOrUser;
50
+ const record = isRouteContext ? recordOrContext : null;
51
+ const legacyContext = isRouteContext ? {} : (recordOrContext || {});
52
+
21
53
  // No user = unauthorized (unless guard explicitly allows)
22
54
  if (!user && guard !== true) {
23
55
  return false;
@@ -28,10 +60,23 @@ export function checkGuard(user, guard, context = {}) {
28
60
  return guard;
29
61
  }
30
62
 
31
- // Guard is function: (user, context) => boolean
63
+ // Guard is function
32
64
  if (typeof guard === 'function') {
33
65
  try {
34
- return guard(user, context);
66
+ // NEW: Pass RouteContext if available, else legacy (user, context) signature
67
+ if (ctx) {
68
+ // Detect guard signature: (ctx, record) or (ctx)
69
+ const guardLength = guard.length;
70
+
71
+ if (guardLength >= 2 && record !== null) {
72
+ return guard(ctx, record); // (ctx, record) for update/delete
73
+ } else {
74
+ return guard(ctx); // (ctx) for list/create
75
+ }
76
+ } else {
77
+ // LEGACY: (user, context) signature
78
+ return guard(user, legacyContext);
79
+ }
35
80
  } catch (err) {
36
81
  console.error('[Guards] Error executing guard function:', err);
37
82
  return false;
@@ -72,7 +117,11 @@ export function checkGuard(user, guard, context = {}) {
72
117
  // Check custom function
73
118
  if (guard.check && typeof guard.check === 'function') {
74
119
  try {
75
- return guard.check(user, context);
120
+ if (ctx) {
121
+ return guard.check(ctx, record);
122
+ } else {
123
+ return guard.check(user, legacyContext);
124
+ }
76
125
  } catch (err) {
77
126
  console.error('[Guards] Error executing guard.check function:', err);
78
127
  return false;
@@ -169,22 +218,27 @@ export function getOperationGuard(guards, operation) {
169
218
  }
170
219
 
171
220
  /**
172
- * Create guard middleware for Hono
221
+ * Create guard middleware for Hono (NEW: with RouteContext)
173
222
  * @param {Object} guards - Guards configuration
174
223
  * @param {string} operation - Operation name
224
+ * @param {Object} options - Options { resource, database, plugins }
175
225
  * @returns {Function} Hono middleware
176
226
  */
177
- export function guardMiddleware(guards, operation) {
227
+ export function guardMiddleware(guards, operation, options = {}) {
178
228
  return async (c, next) => {
179
- const user = c.get('user');
229
+ // Import RouteContext dynamically to avoid circular dependency
230
+ const { RouteContext } = await import('../concerns/route-context.js');
231
+
232
+ const legacyContext = c.get('customRouteContext') || {};
233
+ const { database, resource, plugins = {} } = { ...legacyContext, ...options };
234
+
235
+ // Create RouteContext for guard
236
+ const ctx = new RouteContext(c, database, resource, plugins);
237
+
180
238
  const guard = getOperationGuard(guards, operation);
181
239
 
182
- // Check guard
183
- const authorized = checkGuard(user, guard, {
184
- operation,
185
- resourceName: c.req.param('resource'),
186
- data: c.req.method !== 'GET' ? await c.req.json().catch(() => ({})) : {}
187
- });
240
+ // Check guard (pass RouteContext)
241
+ const authorized = checkGuard(ctx, guard, null);
188
242
 
189
243
  if (!authorized) {
190
244
  return c.json({
@@ -194,13 +248,18 @@ export function guardMiddleware(guards, operation) {
194
248
  code: 'FORBIDDEN',
195
249
  details: {
196
250
  operation,
197
- user: user ? { id: user.id, role: user.role } : null
251
+ user: ctx.user ? { id: ctx.user.id, role: ctx.user.role } : null
198
252
  }
199
253
  },
200
254
  _status: 403
201
255
  }, 403);
202
256
  }
203
257
 
258
+ // Store partition filters in context for use by route handlers
259
+ if (ctx.hasPartitionFilters()) {
260
+ c.set('partitionFilters', ctx.getPartitionFilters());
261
+ }
262
+
204
263
  await next();
205
264
  };
206
265
  }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * OpenAPIGeneratorCached - Smart caching wrapper for OpenAPI generation
3
+ *
4
+ * Caches OpenAPI spec and invalidates only when schema changes:
5
+ * - Resource changes (add/remove/modify)
6
+ * - Auth configuration changes
7
+ * - Version changes
8
+ *
9
+ * Performance: 0ms cache hits vs ~50-200ms generation
10
+ */
11
+
12
+ import { generateOpenAPISpec } from './openapi-generator.js';
13
+ import { createHash } from 'crypto';
14
+
15
+ export class OpenAPIGeneratorCached {
16
+ constructor({ database, options }) {
17
+ this.database = database;
18
+ this.options = options;
19
+
20
+ this.cache = null;
21
+ this.cacheKey = null;
22
+
23
+ if (options.verbose) {
24
+ console.log('[OpenAPIGenerator] Caching enabled');
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Generate OpenAPI spec (with caching)
30
+ * @returns {Object} OpenAPI specification
31
+ */
32
+ generate() {
33
+ // Check if cache is valid
34
+ const currentKey = this.generateCacheKey();
35
+
36
+ if (this.cacheKey === currentKey && this.cache) {
37
+ if (this.options.verbose) {
38
+ console.log('[OpenAPIGenerator] Cache HIT (0ms)');
39
+ }
40
+ return this.cache;
41
+ }
42
+
43
+ // Cache miss - regenerate
44
+ if (this.options.verbose) {
45
+ const reason = !this.cache ? 'initial' : 'invalidated';
46
+ console.log(`[OpenAPIGenerator] Cache MISS (${reason})`);
47
+ }
48
+
49
+ const startTime = Date.now();
50
+ this.cache = generateOpenAPISpec(this.database, this.options);
51
+ this.cacheKey = currentKey;
52
+
53
+ if (this.options.verbose) {
54
+ const duration = Date.now() - startTime;
55
+ console.log(`[OpenAPIGenerator] Generated spec in ${duration}ms`);
56
+ }
57
+
58
+ return this.cache;
59
+ }
60
+
61
+ /**
62
+ * Generate cache key based on schema state
63
+ * @private
64
+ * @returns {string} Cache key (hash)
65
+ */
66
+ generateCacheKey() {
67
+ // Components that affect OpenAPI spec
68
+ const components = {
69
+ // Resource names and versions
70
+ resources: Object.keys(this.database.resources).map(name => {
71
+ const resource = this.database.resources[name];
72
+ return {
73
+ name,
74
+ version: resource.config?.currentVersion || resource.version || 'v1',
75
+ // Shallow hash of attributes (type changes invalidate cache)
76
+ attributes: Object.keys(resource.attributes || {}).sort().join(',')
77
+ };
78
+ }),
79
+
80
+ // Auth configuration affects security schemes
81
+ auth: {
82
+ drivers: this.options.auth?.drivers?.map(d => d.driver).sort() || [],
83
+ pathRules: this.options.auth?.pathRules?.length || 0,
84
+ pathAuth: !!this.options.auth?.pathAuth
85
+ },
86
+
87
+ // Resource config affects paths
88
+ resourceConfig: Object.keys(this.options.resources || {}).sort(),
89
+
90
+ // Version prefix affects URLs
91
+ versionPrefix: this.options.versionPrefix,
92
+
93
+ // API info
94
+ apiInfo: {
95
+ title: this.options.title,
96
+ version: this.options.version,
97
+ description: this.options.description
98
+ }
99
+ };
100
+
101
+ // Hash the components
102
+ const hash = createHash('sha256')
103
+ .update(JSON.stringify(components))
104
+ .digest('hex')
105
+ .substring(0, 16); // First 16 chars sufficient for cache key
106
+
107
+ return hash;
108
+ }
109
+
110
+ /**
111
+ * Invalidate cache (force regeneration on next request)
112
+ */
113
+ invalidate() {
114
+ this.cache = null;
115
+ this.cacheKey = null;
116
+
117
+ if (this.options.verbose) {
118
+ console.log('[OpenAPIGenerator] Cache manually invalidated');
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Get cache statistics
124
+ * @returns {Object} Cache stats
125
+ */
126
+ getStats() {
127
+ return {
128
+ cached: !!this.cache,
129
+ cacheKey: this.cacheKey,
130
+ size: this.cache ? JSON.stringify(this.cache).length : 0
131
+ };
132
+ }
133
+ }
@@ -98,12 +98,13 @@ function generateResourceSchema(resource) {
98
98
  const properties = {};
99
99
  const required = [];
100
100
 
101
- const allAttributes = resource.config?.attributes || resource.attributes || {};
101
+ // Use $schema for reliable access to resource definition
102
+ const allAttributes = resource.$schema.attributes || {};
102
103
 
103
104
  // Filter out plugin attributes - they are internal implementation details
104
105
  // and should not be exposed in public API documentation
105
- const pluginAttrNames = resource.schema?._pluginAttributes
106
- ? Object.values(resource.schema._pluginAttributes).flat()
106
+ const pluginAttrNames = resource.$schema._pluginAttributes
107
+ ? Object.values(resource.$schema._pluginAttributes).flat()
107
108
  : [];
108
109
 
109
110
  const attributes = Object.fromEntries(
@@ -111,7 +112,7 @@ function generateResourceSchema(resource) {
111
112
  );
112
113
 
113
114
  // Extract resource description (supports both string and object format)
114
- const resourceDescription = resource.config?.description;
115
+ const resourceDescription = resource.$schema.description;
115
116
  const attributeDescriptions = typeof resourceDescription === 'object'
116
117
  ? (resourceDescription.attributes || {})
117
118
  : {};
@@ -215,8 +216,8 @@ function generateResourcePaths(resource, version, config = {}) {
215
216
  }
216
217
 
217
218
  // Extract partition information for documentation
218
- // Partitions are stored in resource.config.options.partitions
219
- const partitions = resource.config?.options?.partitions || resource.config?.partitions || resource.partitions || {};
219
+ // Use $schema for reliable access to partition definitions
220
+ const partitions = resource.$schema.partitions || {};
220
221
  const partitionNames = Object.keys(partitions);
221
222
  const hasPartitions = partitionNames.length > 0;
222
223
 
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Provides c.render() helper that works with multiple template engines:
5
5
  * - EJS (for mrt-shortner compatibility)
6
+ * - Pug (formerly Jade)
6
7
  * - JSX (Hono native)
7
8
  * - Custom engines via setRenderer()
8
9
  *
@@ -19,6 +20,17 @@
19
20
  * });
20
21
  *
21
22
  * @example
23
+ * // Pug usage
24
+ * app.use('*', setupTemplateEngine({
25
+ * engine: 'pug',
26
+ * templatesDir: './views'
27
+ * }));
28
+ *
29
+ * app.get('/page', async (c) => {
30
+ * return c.render('landing', { urlCount: 1000 });
31
+ * });
32
+ *
33
+ * @example
22
34
  * // JSX usage (no setup needed)
23
35
  * app.get('/page', (c) => {
24
36
  * return c.render(<h1>Hello</h1>);
@@ -45,11 +57,27 @@ async function loadEJS() {
45
57
  }
46
58
  }
47
59
 
60
+ /**
61
+ * Lazy-load Pug (peer dependency)
62
+ * @returns {Promise<Object>} Pug module
63
+ */
64
+ async function loadPug() {
65
+ try {
66
+ const pug = await import('pug');
67
+ return pug.default || pug;
68
+ } catch (err) {
69
+ throw new Error(
70
+ 'Pug template engine not installed. Install with: npm install pug\n' +
71
+ 'Pug is a peer dependency to keep the core package lightweight.'
72
+ );
73
+ }
74
+ }
75
+
48
76
  /**
49
77
  * Setup template engine middleware
50
78
  * @param {Object} options - Template engine options
51
- * @param {string} options.engine - Engine name: 'ejs', 'jsx', 'custom'
52
- * @param {string} options.templatesDir - Directory containing templates (required for EJS)
79
+ * @param {string} options.engine - Engine name: 'ejs', 'pug', 'jsx', 'custom'
80
+ * @param {string} options.templatesDir - Directory containing templates (required for EJS/Pug)
53
81
  * @param {string} options.layout - Default layout template (optional for EJS)
54
82
  * @param {Object} options.engineOptions - Additional engine-specific options
55
83
  * @param {Function} options.customRenderer - Custom render function (for 'custom' engine)
@@ -70,7 +98,7 @@ export function setupTemplateEngine(options = {}) {
70
98
  return async (c, next) => {
71
99
  /**
72
100
  * Render template with data
73
- * @param {string|JSX.Element} template - Template name (for EJS) or JSX element
101
+ * @param {string|JSX.Element} template - Template name (for EJS/Pug) or JSX element
74
102
  * @param {Object} data - Data to pass to template
75
103
  * @param {Object} renderOptions - Render-specific options
76
104
  * @returns {Response} HTML response
@@ -82,6 +110,37 @@ export function setupTemplateEngine(options = {}) {
82
110
  return c.html(template);
83
111
  }
84
112
 
113
+ // Pug: File-based rendering
114
+ if (engine === 'pug') {
115
+ // Lazy-load Pug
116
+ const pug = await loadPug();
117
+
118
+ const templateFile = template.endsWith('.pug') ? template : `${template}.pug`;
119
+ const templatePath = join(templatesPath, templateFile);
120
+
121
+ if (!existsSync(templatePath)) {
122
+ throw new Error(`Template not found: ${templatePath}`);
123
+ }
124
+
125
+ // Merge global data + render data
126
+ const renderData = {
127
+ ...data,
128
+ // Add helpers that Pug templates might expect
129
+ _url: c.req.url,
130
+ _path: c.req.path,
131
+ _method: c.req.method
132
+ };
133
+
134
+ // Render template
135
+ const html = pug.renderFile(templatePath, {
136
+ ...renderData,
137
+ ...engineOptions,
138
+ ...renderOptions
139
+ });
140
+
141
+ return c.html(html);
142
+ }
143
+
85
144
  // EJS: File-based rendering
86
145
  if (engine === 'ejs') {
87
146
  // Lazy-load EJS
@@ -164,6 +223,20 @@ export function ejsEngine(templatesDir, options = {}) {
164
223
  });
165
224
  }
166
225
 
226
+ /**
227
+ * Create Pug template engine middleware (convenience wrapper)
228
+ * @param {string} templatesDir - Directory containing templates
229
+ * @param {Object} options - Additional options
230
+ * @returns {Function} Hono middleware
231
+ */
232
+ export function pugEngine(templatesDir, options = {}) {
233
+ return setupTemplateEngine({
234
+ engine: 'pug',
235
+ templatesDir,
236
+ ...options
237
+ });
238
+ }
239
+
167
240
  /**
168
241
  * Create JSX template engine middleware (convenience wrapper)
169
242
  * Note: JSX rendering is built into Hono, this just provides c.render()
@@ -184,5 +257,6 @@ export function jsxEngine() {
184
257
  export default {
185
258
  setupTemplateEngine,
186
259
  ejsEngine,
260
+ pugEngine,
187
261
  jsxEngine
188
262
  };
@@ -426,12 +426,24 @@
426
426
  */
427
427
 
428
428
  import { Plugin } from "./plugin.class.js";
429
+ import { getValidatedNamespace } from "./namespace.js";
429
430
  import tryFn from "../concerns/try-fn.js";
431
+ import { resolveResourceName } from "./concerns/resource-names.js";
430
432
 
431
433
  export class AuditPlugin extends Plugin {
432
434
  constructor(options = {}) {
433
435
  super(options);
436
+
437
+ // Validate and set namespace (standardized)
438
+ this.namespace = getValidatedNamespace(options, '');
439
+
440
+ const resourceNames = options.resourceNames || {};
434
441
  this.auditResource = null;
442
+ this._auditResourceDescriptor = {
443
+ defaultName: 'plg_audits',
444
+ override: resourceNames.audit || options.resourceName
445
+ };
446
+ this.auditResourceName = this._resolveAuditResourceName();
435
447
  this.config = {
436
448
  includeData: options.includeData !== false,
437
449
  includePartitions: options.includePartitions !== false,
@@ -440,10 +452,20 @@ export class AuditPlugin extends Plugin {
440
452
  };
441
453
  }
442
454
 
455
+ _resolveAuditResourceName() {
456
+ return resolveResourceName('audit', this._auditResourceDescriptor, {
457
+ namespace: this.namespace
458
+ });
459
+ }
460
+
461
+ onNamespaceChanged() {
462
+ this.auditResourceName = this._resolveAuditResourceName();
463
+ }
464
+
443
465
  async onInstall() {
444
466
  // Create audit resource
445
467
  const [ok, err, auditResource] = await tryFn(() => this.database.createResource({
446
- name: 'plg_audits',
468
+ name: this.auditResourceName,
447
469
  attributes: {
448
470
  id: 'string|required',
449
471
  resourceName: 'string|required',
@@ -464,19 +486,19 @@ export class AuditPlugin extends Plugin {
464
486
  },
465
487
  behavior: 'body-overflow'
466
488
  }));
467
- this.auditResource = ok ? auditResource : (this.database.resources.plg_audits || null);
489
+ this.auditResource = ok ? auditResource : (this.database.resources[this.auditResourceName] || null);
468
490
  if (!ok && !this.auditResource) return;
469
491
 
470
492
  // Hook into database for new resources
471
493
  this.database.addHook('afterCreateResource', (context) => {
472
- if (context.resource.name !== 'plg_audits') {
494
+ if (context.resource.name !== this.auditResourceName) {
473
495
  this.setupResourceAuditing(context.resource);
474
496
  }
475
497
  });
476
498
 
477
499
  // Setup existing resources
478
500
  for (const resource of Object.values(this.database.resources)) {
479
- if (resource.name !== 'plg_audits') {
501
+ if (resource.name !== this.auditResourceName) {
480
502
  this.setupResourceAuditing(resource);
481
503
  }
482
504
  }
@@ -630,9 +652,9 @@ export class AuditPlugin extends Plugin {
630
652
 
631
653
  getPartitionValues(data, resource) {
632
654
  if (!this.config.includePartitions) return null;
633
-
634
- // Access partitions from resource.config.partitions, not resource.partitions
635
- const partitions = resource.config?.partitions || resource.partitions;
655
+
656
+ // Use $schema for reliable access to partition definitions
657
+ const partitions = resource.$schema.partitions;
636
658
  if (!partitions) {
637
659
  return null;
638
660
  }
@@ -847,4 +869,4 @@ export class AuditPlugin extends Plugin {
847
869
 
848
870
  return deletedCount;
849
871
  }
850
- }
872
+ }