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
@@ -0,0 +1,288 @@
1
+ /**
2
+ * State Machine Base Class
3
+ *
4
+ * Provides state transition validation and execution for workflows.
5
+ *
6
+ * @abstract
7
+ */
8
+ class StateMachine {
9
+ constructor(states, transitions) {
10
+ this.STATES = states;
11
+ this.TRANSITIONS = transitions;
12
+ }
13
+
14
+ /**
15
+ * Validate if a transition is allowed from current state
16
+ *
17
+ * @param {string} currentState - Current state
18
+ * @param {string} transitionName - Transition to execute
19
+ * @returns {Object} { valid: boolean, newState?: string, error?: string }
20
+ */
21
+ canTransition(currentState, transitionName) {
22
+ const transition = this.TRANSITIONS[transitionName];
23
+
24
+ if (!transition) {
25
+ return {
26
+ valid: false,
27
+ error: `Unknown transition: ${transitionName}. Valid transitions: ${Object.keys(this.TRANSITIONS).join(', ')}`
28
+ };
29
+ }
30
+
31
+ // Support multiple "from" states
32
+ const validFromStates = Array.isArray(transition.from) ? transition.from : [transition.from];
33
+
34
+ if (!validFromStates.includes(currentState)) {
35
+ return {
36
+ valid: false,
37
+ error: `Cannot transition from "${currentState}" to "${transition.to}" via "${transitionName}". Expected current state to be one of: ${validFromStates.join(', ')}`
38
+ };
39
+ }
40
+
41
+ return { valid: true, newState: transition.to };
42
+ }
43
+
44
+ /**
45
+ * Execute a state transition with validation
46
+ *
47
+ * @param {Object} record - Record to transition (must have status field)
48
+ * @param {string} transitionName - Transition to execute
49
+ * @param {Object} resource - S3DB resource to update
50
+ * @param {Object} metadata - Additional fields to update
51
+ * @returns {Promise<Object>} Updated record
52
+ * @throws {Error} If transition is invalid or update fails
53
+ */
54
+ async transition(record, transitionName, resource, metadata = {}) {
55
+ const validation = this.canTransition(record.status, transitionName);
56
+
57
+ if (!validation.valid) {
58
+ throw new Error(`State machine error: ${validation.error}`);
59
+ }
60
+
61
+ // Prepare update payload
62
+ const updateData = {
63
+ status: validation.newState,
64
+ ...metadata,
65
+ lastTransitionAt: new Date().toISOString(),
66
+ lastTransition: transitionName
67
+ };
68
+
69
+ // Update record
70
+ const updated = await resource.patch(record.id, updateData);
71
+
72
+ return updated;
73
+ }
74
+
75
+ /**
76
+ * Get all valid transitions from current state
77
+ *
78
+ * @param {string} currentState - Current state
79
+ * @returns {Array<string>} List of valid transition names
80
+ */
81
+ getValidTransitions(currentState) {
82
+ return Object.entries(this.TRANSITIONS)
83
+ .filter(([_, transition]) => {
84
+ const validFromStates = Array.isArray(transition.from) ? transition.from : [transition.from];
85
+ return validFromStates.includes(currentState);
86
+ })
87
+ .map(([name]) => name);
88
+ }
89
+
90
+ /**
91
+ * Check if state is terminal (no outgoing transitions)
92
+ *
93
+ * @param {string} state - State to check
94
+ * @returns {boolean}
95
+ */
96
+ isTerminalState(state) {
97
+ return this.getValidTransitions(state).length === 0;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Notification State Machine
103
+ *
104
+ * Manages notification lifecycle: pending → processing → completed/failed
105
+ *
106
+ * States:
107
+ * - pending: Waiting to be processed
108
+ * - processing: Currently being sent
109
+ * - completed: Successfully delivered
110
+ * - failed: Failed after max retries
111
+ *
112
+ * Transitions:
113
+ * - START_PROCESSING: pending → processing
114
+ * - COMPLETE: processing → completed
115
+ * - FAIL: processing → failed
116
+ * - RETRY: processing → pending (for retry)
117
+ *
118
+ * @example
119
+ * const notificationSM = new NotificationStateMachine();
120
+ *
121
+ * // Start processing
122
+ * await notificationSM.transition(
123
+ * notification,
124
+ * 'START_PROCESSING',
125
+ * notificationsResource,
126
+ * { processingStartedAt: new Date().toISOString() }
127
+ * );
128
+ *
129
+ * // Complete
130
+ * await notificationSM.transition(
131
+ * notification,
132
+ * 'COMPLETE',
133
+ * notificationsResource,
134
+ * { completedAt: new Date().toISOString(), lastStatusCode: 200 }
135
+ * );
136
+ */
137
+ export class NotificationStateMachine extends StateMachine {
138
+ constructor() {
139
+ const STATES = {
140
+ PENDING: 'pending',
141
+ PROCESSING: 'processing',
142
+ COMPLETED: 'completed',
143
+ FAILED: 'failed'
144
+ };
145
+
146
+ const TRANSITIONS = {
147
+ START_PROCESSING: { from: 'pending', to: 'processing' },
148
+ COMPLETE: { from: 'processing', to: 'completed' },
149
+ FAIL: { from: 'processing', to: 'failed' },
150
+ RETRY: { from: 'processing', to: 'pending' }
151
+ };
152
+
153
+ super(STATES, TRANSITIONS);
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Attempt State Machine
159
+ *
160
+ * Manages individual attempt lifecycle: queued → running → success/failed/timeout
161
+ *
162
+ * States:
163
+ * - queued: Waiting to be executed
164
+ * - running: Currently executing
165
+ * - success: Completed successfully
166
+ * - failed: Failed (may retry)
167
+ * - timeout: Timed out (may retry)
168
+ *
169
+ * Transitions:
170
+ * - START: queued → running
171
+ * - SUCCEED: running → success
172
+ * - FAIL: running → failed
173
+ * - TIMEOUT: running → timeout
174
+ *
175
+ * @example
176
+ * const attemptSM = new AttemptStateMachine();
177
+ *
178
+ * // Create attempt
179
+ * const attempt = await attemptSM.create(attemptsResource, {
180
+ * notificationId: notification.id,
181
+ * attemptNumber: 1,
182
+ * channel: 'webhook',
183
+ * data: { url: 'https://...' }
184
+ * });
185
+ *
186
+ * // Start execution
187
+ * await attemptSM.transition(attempt, 'START', attemptsResource, {
188
+ * startedAt: new Date().toISOString()
189
+ * });
190
+ *
191
+ * // Complete with success
192
+ * await attemptSM.transition(attempt, 'SUCCEED', attemptsResource, {
193
+ * statusCode: 200,
194
+ * response: { success: true },
195
+ * completedAt: new Date().toISOString()
196
+ * });
197
+ */
198
+ export class AttemptStateMachine extends StateMachine {
199
+ constructor() {
200
+ const STATES = {
201
+ QUEUED: 'queued',
202
+ RUNNING: 'running',
203
+ SUCCESS: 'success',
204
+ FAILED: 'failed',
205
+ TIMEOUT: 'timeout'
206
+ };
207
+
208
+ const TRANSITIONS = {
209
+ START: { from: 'queued', to: 'running' },
210
+ SUCCEED: { from: 'running', to: 'success' },
211
+ FAIL: { from: 'running', to: 'failed' },
212
+ TIMEOUT: { from: 'running', to: 'timeout' }
213
+ };
214
+
215
+ super(STATES, TRANSITIONS);
216
+ }
217
+
218
+ /**
219
+ * Create a new attempt with initial state
220
+ *
221
+ * Note: Uses insert() instead of patch() for attempts table
222
+ *
223
+ * @param {Object} resource - S3DB attempts resource
224
+ * @param {Object} data - Attempt data
225
+ * @returns {Promise<Object>} Created attempt
226
+ */
227
+ async create(resource, data) {
228
+ return await resource.insert({
229
+ ...data,
230
+ status: this.STATES.QUEUED,
231
+ createdAt: new Date().toISOString()
232
+ });
233
+ }
234
+
235
+ /**
236
+ * Execute transition for attempt
237
+ *
238
+ * Note: Uses insert() instead of patch() since attempts are immutable
239
+ *
240
+ * @param {Object} record - Attempt record
241
+ * @param {string} transitionName - Transition to execute
242
+ * @param {Object} resource - S3DB resource
243
+ * @param {Object} metadata - Additional fields
244
+ * @returns {Promise<Object>} New attempt record
245
+ */
246
+ async transition(record, transitionName, resource, metadata = {}) {
247
+ const validation = this.canTransition(record.status, transitionName);
248
+
249
+ if (!validation.valid) {
250
+ throw new Error(`State machine error: ${validation.error}`);
251
+ }
252
+
253
+ // For attempts, we insert a new record (immutable pattern)
254
+ const newRecord = await resource.insert({
255
+ ...record,
256
+ status: validation.newState,
257
+ ...metadata,
258
+ lastTransitionAt: new Date().toISOString(),
259
+ lastTransition: transitionName
260
+ });
261
+
262
+ return newRecord;
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Factory function to create state machines
268
+ *
269
+ * @example
270
+ * import { createNotificationStateMachine, createAttemptStateMachine } from './state-machine.js';
271
+ *
272
+ * const notificationSM = createNotificationStateMachine();
273
+ * const attemptSM = createAttemptStateMachine();
274
+ */
275
+ export function createNotificationStateMachine() {
276
+ return new NotificationStateMachine();
277
+ }
278
+
279
+ export function createAttemptStateMachine() {
280
+ return new AttemptStateMachine();
281
+ }
282
+
283
+ export default {
284
+ NotificationStateMachine,
285
+ AttemptStateMachine,
286
+ createNotificationStateMachine,
287
+ createAttemptStateMachine
288
+ };
@@ -37,6 +37,7 @@ import { requirePluginDependency } from '../concerns/plugin-dependencies.js';
37
37
  import tryFn from '../../concerns/try-fn.js';
38
38
  import { ApiServer } from './server.js';
39
39
  import { idGenerator } from '../../concerns/id.js';
40
+ import { resolveResourceName } from '../concerns/resource-names.js';
40
41
 
41
42
  const AUTH_DRIVER_KEYS = ['jwt', 'apiKey', 'basic', 'oidc', 'oauth2'];
42
43
 
@@ -61,9 +62,10 @@ function normalizeAuthConfig(authOptions = {}) {
61
62
  pathAuth: authOptions.pathAuth,
62
63
  strategy: authOptions.strategy || 'any',
63
64
  priorities: authOptions.priorities || {},
64
- resource: authOptions.resource || 'users',
65
+ resource: authOptions.resource,
65
66
  usernameField: authOptions.usernameField || 'email',
66
- passwordField: authOptions.passwordField || 'password'
67
+ passwordField: authOptions.passwordField || 'password',
68
+ createResource: authOptions.createResource !== false
67
69
  };
68
70
 
69
71
  const seen = new Set();
@@ -130,7 +132,29 @@ export class ApiPlugin extends Plugin {
130
132
  constructor(options = {}) {
131
133
  super(options);
132
134
 
135
+ const resourceNamesOption = options.resourceNames || {};
136
+ this._usersResourceDescriptor = {
137
+ defaultName: 'plg_api_users',
138
+ override: resourceNamesOption.authUsers || options.auth?.resource
139
+ };
133
140
  const normalizedAuth = normalizeAuthConfig(options.auth);
141
+ normalizedAuth.registration = {
142
+ enabled: options.auth?.registration?.enabled === true,
143
+ allowedFields: Array.isArray(options.auth?.registration?.allowedFields)
144
+ ? options.auth.registration.allowedFields
145
+ : [],
146
+ defaultRole: options.auth?.registration?.defaultRole || 'user'
147
+ };
148
+ normalizedAuth.loginThrottle = {
149
+ enabled: options.auth?.loginThrottle?.enabled !== false,
150
+ maxAttempts: options.auth?.loginThrottle?.maxAttempts || 5,
151
+ windowMs: options.auth?.loginThrottle?.windowMs || 60_000,
152
+ blockDurationMs: options.auth?.loginThrottle?.blockDurationMs || 300_000,
153
+ maxEntries: options.auth?.loginThrottle?.maxEntries || 10_000
154
+ };
155
+ this.usersResourceName = this._resolveUsersResourceName();
156
+ normalizedAuth.resource = this.usersResourceName;
157
+ normalizedAuth.createResource = options.auth?.createResource !== false;
134
158
 
135
159
  this.config = {
136
160
  // Server configuration
@@ -182,7 +206,8 @@ export class ApiPlugin extends Plugin {
182
206
  enabled: options.rateLimit?.enabled || false,
183
207
  windowMs: options.rateLimit?.windowMs || 60000, // 1 minute
184
208
  maxRequests: options.rateLimit?.maxRequests || 100,
185
- keyGenerator: options.rateLimit?.keyGenerator || null
209
+ keyGenerator: options.rateLimit?.keyGenerator || null,
210
+ maxUniqueKeys: options.rateLimit?.maxUniqueKeys || 1000
186
211
  },
187
212
 
188
213
  // Logging configuration
@@ -288,7 +313,22 @@ export class ApiPlugin extends Plugin {
288
313
  },
289
314
 
290
315
  // Custom global middlewares
291
- middlewares: options.middlewares || []
316
+ middlewares: options.middlewares || [],
317
+
318
+ requestId: options.requestId || { enabled: false },
319
+ sessionTracking: options.sessionTracking || { enabled: false },
320
+ events: options.events || { enabled: false },
321
+ metrics: options.metrics || { enabled: false },
322
+ failban: {
323
+ ...(options.failban || {}),
324
+ enabled: options.failban?.enabled === true,
325
+ resourceNames: resourceNamesOption.failban || options.failban?.resourceNames || {}
326
+ },
327
+ static: Array.isArray(options.static) ? options.static : [],
328
+ health: typeof options.health === 'object'
329
+ ? options.health
330
+ : { enabled: options.health !== false },
331
+ maxBodySize: options.maxBodySize || 10 * 1024 * 1024
292
332
  };
293
333
 
294
334
  this.config.resources = this._normalizeResourcesConfig(options.resources);
@@ -410,18 +450,43 @@ export class ApiPlugin extends Plugin {
410
450
  * @private
411
451
  */
412
452
  async _createUsersResource() {
453
+ const existingResource = this._findExistingUsersResource();
454
+
455
+ if (!this.config.auth.createResource) {
456
+ if (!existingResource) {
457
+ throw new Error(
458
+ `[API Plugin] Auth resource "${this.usersResourceName}" not found and auth.createResource is false`
459
+ );
460
+ }
461
+ this.usersResource = existingResource;
462
+ this.config.auth.resource = existingResource.name;
463
+ if (this.config.verbose) {
464
+ console.log(`[API Plugin] Using existing ${existingResource.name} resource for authentication`);
465
+ }
466
+ return;
467
+ }
468
+
469
+ if (existingResource) {
470
+ this.usersResource = existingResource;
471
+ this.config.auth.resource = existingResource.name;
472
+ if (this.config.verbose) {
473
+ console.log(`[API Plugin] Reusing existing ${existingResource.name} resource for authentication`);
474
+ }
475
+ return;
476
+ }
477
+
413
478
  const [ok, err, resource] = await tryFn(() =>
414
479
  this.database.createResource({
415
- name: 'plg_users',
480
+ name: this.usersResourceName,
416
481
  attributes: {
417
482
  id: 'string|required',
418
483
  username: 'string|required|minlength:3',
419
- email: 'string|required|email', // Required to support email-based auth
484
+ email: 'string|required|email',
420
485
  password: 'secret|required|minlength:8',
421
486
  apiKey: 'string|optional',
422
487
  jwtSecret: 'string|optional',
423
488
  role: 'string|default:user',
424
- scopes: 'array|items:string|optional', // Authorization scopes (e.g., ['read:users', 'write:cars'])
489
+ scopes: 'array|items:string|optional',
425
490
  active: 'boolean|default:true',
426
491
  createdAt: 'string|optional',
427
492
  lastLoginAt: 'string|optional',
@@ -433,20 +498,40 @@ export class ApiPlugin extends Plugin {
433
498
  })
434
499
  );
435
500
 
436
- if (ok) {
437
- this.usersResource = resource;
438
- if (this.config.verbose) {
439
- console.log('[API Plugin] Created plg_users resource for authentication');
501
+ if (!ok) {
502
+ throw err;
503
+ }
504
+
505
+ this.usersResource = resource;
506
+ this.config.auth.resource = resource.name;
507
+ if (this.config.verbose) {
508
+ console.log(`[API Plugin] Created ${this.usersResourceName} resource for authentication`);
509
+ }
510
+ }
511
+
512
+ _findExistingUsersResource() {
513
+ const candidates = new Set([this.usersResourceName]);
514
+
515
+ const identityPlugin = this.database?.plugins?.identity || this.database?.plugins?.Identity;
516
+ if (identityPlugin) {
517
+ const identityNames = [
518
+ identityPlugin.usersResource?.name,
519
+ identityPlugin.config?.resources?.users?.mergedConfig?.name,
520
+ identityPlugin.config?.resources?.users?.userConfig?.name
521
+ ].filter(Boolean);
522
+ for (const name of identityNames) {
523
+ candidates.add(name);
440
524
  }
441
- } else if (this.database.resources.plg_users) {
442
- // Resource already exists
443
- this.usersResource = this.database.resources.plg_users;
444
- if (this.config.verbose) {
445
- console.log('[API Plugin] Using existing plg_users resource');
525
+ }
526
+
527
+ for (const name of candidates) {
528
+ if (!name) continue;
529
+ const resource = this.database.resources?.[name];
530
+ if (resource) {
531
+ return resource;
446
532
  }
447
- } else {
448
- throw err;
449
533
  }
534
+ return null;
450
535
  }
451
536
  /**
452
537
  * Setup middlewares
@@ -584,25 +669,41 @@ export class ApiPlugin extends Plugin {
584
669
  */
585
670
  async _createRateLimitMiddleware() {
586
671
  const requests = new Map();
587
- const { windowMs, maxRequests, keyGenerator } = this.config.rateLimit;
672
+ const { windowMs, maxRequests, keyGenerator, maxUniqueKeys } = this.config.rateLimit;
673
+
674
+ const getClientIp = (c) => {
675
+ const forwarded = c.req.header('x-forwarded-for');
676
+ if (forwarded) {
677
+ return forwarded.split(',')[0].trim();
678
+ }
679
+ const cfConnecting = c.req.header('cf-connecting-ip');
680
+ if (cfConnecting) {
681
+ return cfConnecting;
682
+ }
683
+ return c.req.raw?.socket?.remoteAddress || 'unknown';
684
+ };
588
685
 
589
686
  return async (c, next) => {
590
687
  // Generate key (IP or custom)
591
- const key = keyGenerator
592
- ? keyGenerator(c)
593
- : c.req.header('x-forwarded-for') || c.req.header('cf-connecting-ip') || 'unknown';
688
+ const key = keyGenerator ? keyGenerator(c) : getClientIp(c) || 'unknown';
594
689
 
595
- // Get or create request count
596
- if (!requests.has(key)) {
597
- requests.set(key, { count: 0, resetAt: Date.now() + windowMs });
598
- }
690
+ let record = requests.get(key);
599
691
 
600
- const record = requests.get(key);
692
+ // Reset expired records to prevent unbounded memory growth
693
+ if (record && Date.now() > record.resetAt) {
694
+ requests.delete(key);
695
+ record = null;
696
+ }
601
697
 
602
- // Reset if window expired
603
- if (Date.now() > record.resetAt) {
604
- record.count = 0;
605
- record.resetAt = Date.now() + windowMs;
698
+ if (!record) {
699
+ record = { count: 0, resetAt: Date.now() + windowMs };
700
+ requests.set(key, record);
701
+ if (requests.size > maxUniqueKeys) {
702
+ const oldestKey = requests.keys().next().value;
703
+ if (oldestKey) {
704
+ requests.delete(oldestKey);
705
+ }
706
+ }
606
707
  }
607
708
 
608
709
  // Check limit
@@ -706,14 +807,15 @@ export class ApiPlugin extends Plugin {
706
807
  return;
707
808
  }
708
809
 
709
- // Skip if already compressed
710
- if (c.res.headers.has('content-encoding')) {
810
+ // Skip if already compressed or body consumed
811
+ if (c.res.headers.has('content-encoding') || c.res.bodyUsed) {
711
812
  return;
712
813
  }
713
814
 
714
815
  // Skip if content-type should not be compressed
715
816
  const contentType = c.res.headers.get('content-type') || '';
716
- if (skipContentTypes.some(type => contentType.startsWith(type))) {
817
+ const isTextLike = contentType.startsWith('text/') || contentType.includes('json');
818
+ if (skipContentTypes.some(type => contentType.startsWith(type)) || !isTextLike) {
717
819
  return;
718
820
  }
719
821
 
@@ -909,11 +1011,22 @@ export class ApiPlugin extends Plugin {
909
1011
  port: this.config.port,
910
1012
  host: this.config.host,
911
1013
  database: this.database,
1014
+ namespace: this.namespace,
912
1015
  versionPrefix: this.config.versionPrefix,
913
1016
  resources: this.config.resources,
914
1017
  routes: this.config.routes,
915
1018
  templates: this.config.templates,
916
1019
  middlewares: this.compiledMiddlewares,
1020
+ cors: this.config.cors,
1021
+ security: this.config.security,
1022
+ requestId: this.config.requestId,
1023
+ sessionTracking: this.config.sessionTracking,
1024
+ events: this.config.events,
1025
+ metrics: this.config.metrics,
1026
+ failban: this.config.failban,
1027
+ static: this.config.static,
1028
+ health: this.config.health,
1029
+ maxBodySize: this.config.maxBodySize,
917
1030
  verbose: this.config.verbose,
918
1031
  auth: this.config.auth,
919
1032
  docsEnabled: this.config.docs.enabled,
@@ -942,10 +1055,24 @@ export class ApiPlugin extends Plugin {
942
1055
 
943
1056
  if (this.server) {
944
1057
  await this.server.stop();
945
- this.server = null;
946
1058
  }
1059
+ this.server = null;
1060
+ }
947
1061
 
948
- this.emit('plugin.stopped');
1062
+ _resolveUsersResourceName() {
1063
+ return resolveResourceName('api', this._usersResourceDescriptor, {
1064
+ namespace: this.namespace
1065
+ });
1066
+ }
1067
+
1068
+ onNamespaceChanged() {
1069
+ this.usersResourceName = this._resolveUsersResourceName();
1070
+ if (this.config?.auth) {
1071
+ this.config.auth.resource = this.usersResourceName;
1072
+ }
1073
+ if (this.server?.failban) {
1074
+ this.server.failban.setNamespace(this.namespace);
1075
+ }
949
1076
  }
950
1077
 
951
1078
  /**
@@ -959,11 +1086,9 @@ export class ApiPlugin extends Plugin {
959
1086
 
960
1087
  // Optionally delete users resource
961
1088
  if (purgeData && this.usersResource) {
962
- // Delete all users (plugin data cleanup happens automatically via base Plugin class)
963
- const [ok] = await tryFn(() => this.database.deleteResource('plg_users'));
964
-
1089
+ const [ok] = await tryFn(() => this.database.deleteResource(this.usersResourceName));
965
1090
  if (ok && this.config.verbose) {
966
- console.log('[API Plugin] Deleted plg_users resource');
1091
+ console.log(`[API Plugin] Deleted ${this.usersResourceName} resource`);
967
1092
  }
968
1093
  }
969
1094
 
@@ -994,4 +1119,18 @@ export { OIDCClient } from './auth/oidc-client.js';
994
1119
  export * from './concerns/guards-helpers.js';
995
1120
 
996
1121
  // Export template engine utilities
997
- export { setupTemplateEngine, ejsEngine, jsxEngine } from './utils/template-engine.js';
1122
+ export { setupTemplateEngine, ejsEngine, pugEngine, jsxEngine } from './utils/template-engine.js';
1123
+
1124
+ // Export OpenGraph helper
1125
+ export { OpenGraphHelper } from './concerns/opengraph-helper.js';
1126
+
1127
+ // Export state machines
1128
+ export {
1129
+ NotificationStateMachine,
1130
+ AttemptStateMachine,
1131
+ createNotificationStateMachine,
1132
+ createAttemptStateMachine
1133
+ } from './concerns/state-machine.js';
1134
+
1135
+ // Export route context utilities (NEW!)
1136
+ export { RouteContext, withContext } from './concerns/route-context.js';