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.
- package/README.md +139 -43
- package/dist/s3db.cjs +72425 -38970
- package/dist/s3db.cjs.map +1 -1
- package/dist/s3db.es.js +72177 -38764
- package/dist/s3db.es.js.map +1 -1
- package/mcp/lib/base-handler.js +157 -0
- package/mcp/lib/handlers/connection-handler.js +280 -0
- package/mcp/lib/handlers/query-handler.js +533 -0
- package/mcp/lib/handlers/resource-handler.js +428 -0
- package/mcp/lib/tool-registry.js +336 -0
- package/mcp/lib/tools/connection-tools.js +161 -0
- package/mcp/lib/tools/query-tools.js +267 -0
- package/mcp/lib/tools/resource-tools.js +404 -0
- package/package.json +94 -49
- package/src/clients/memory-client.class.js +346 -191
- package/src/clients/memory-storage.class.js +300 -84
- package/src/clients/s3-client.class.js +7 -6
- package/src/concerns/geo-encoding.js +19 -2
- package/src/concerns/ip.js +59 -9
- package/src/concerns/money.js +8 -1
- package/src/concerns/password-hashing.js +49 -8
- package/src/concerns/plugin-storage.js +186 -18
- package/src/concerns/storage-drivers/filesystem-driver.js +284 -0
- package/src/database.class.js +139 -29
- package/src/errors.js +332 -42
- package/src/plugins/api/auth/oidc-auth.js +66 -17
- package/src/plugins/api/auth/strategies/base-strategy.class.js +74 -0
- package/src/plugins/api/auth/strategies/factory.class.js +63 -0
- package/src/plugins/api/auth/strategies/global-strategy.class.js +44 -0
- package/src/plugins/api/auth/strategies/path-based-strategy.class.js +83 -0
- package/src/plugins/api/auth/strategies/path-rules-strategy.class.js +118 -0
- package/src/plugins/api/concerns/failban-manager.js +106 -57
- package/src/plugins/api/concerns/opengraph-helper.js +116 -0
- package/src/plugins/api/concerns/route-context.js +601 -0
- package/src/plugins/api/concerns/state-machine.js +288 -0
- package/src/plugins/api/index.js +180 -41
- package/src/plugins/api/routes/auth-routes.js +198 -30
- package/src/plugins/api/routes/resource-routes.js +19 -4
- package/src/plugins/api/server/health-manager.class.js +163 -0
- package/src/plugins/api/server/middleware-chain.class.js +310 -0
- package/src/plugins/api/server/router.class.js +472 -0
- package/src/plugins/api/server.js +280 -1303
- package/src/plugins/api/utils/custom-routes.js +17 -5
- package/src/plugins/api/utils/guards.js +76 -17
- package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
- package/src/plugins/api/utils/openapi-generator.js +7 -6
- package/src/plugins/api/utils/template-engine.js +77 -3
- package/src/plugins/audit.plugin.js +30 -8
- package/src/plugins/backup.plugin.js +110 -14
- package/src/plugins/cache/cache.class.js +22 -5
- package/src/plugins/cache/filesystem-cache.class.js +116 -19
- package/src/plugins/cache/memory-cache.class.js +211 -57
- package/src/plugins/cache/multi-tier-cache.class.js +371 -0
- package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
- package/src/plugins/cache/redis-cache.class.js +552 -0
- package/src/plugins/cache/s3-cache.class.js +17 -8
- package/src/plugins/cache.plugin.js +176 -61
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
- package/src/plugins/cloud-inventory/index.js +29 -8
- package/src/plugins/cloud-inventory/registry.js +64 -42
- package/src/plugins/cloud-inventory.plugin.js +240 -138
- package/src/plugins/concerns/plugin-dependencies.js +54 -0
- package/src/plugins/concerns/resource-names.js +100 -0
- package/src/plugins/consumers/index.js +10 -2
- package/src/plugins/consumers/sqs-consumer.js +12 -2
- package/src/plugins/cookie-farm-suite.plugin.js +278 -0
- package/src/plugins/cookie-farm.errors.js +73 -0
- package/src/plugins/cookie-farm.plugin.js +869 -0
- package/src/plugins/costs.plugin.js +7 -1
- package/src/plugins/eventual-consistency/analytics.js +94 -19
- package/src/plugins/eventual-consistency/config.js +15 -7
- package/src/plugins/eventual-consistency/consolidation.js +29 -11
- package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
- package/src/plugins/eventual-consistency/helpers.js +39 -14
- package/src/plugins/eventual-consistency/install.js +21 -2
- package/src/plugins/eventual-consistency/utils.js +32 -10
- package/src/plugins/fulltext.plugin.js +38 -11
- package/src/plugins/geo.plugin.js +61 -9
- package/src/plugins/identity/concerns/config.js +61 -0
- package/src/plugins/identity/concerns/mfa-manager.js +15 -2
- package/src/plugins/identity/concerns/rate-limit.js +124 -0
- package/src/plugins/identity/concerns/resource-schemas.js +9 -1
- package/src/plugins/identity/concerns/token-generator.js +29 -4
- package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
- package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
- package/src/plugins/identity/drivers/index.js +18 -0
- package/src/plugins/identity/drivers/password-driver.js +122 -0
- package/src/plugins/identity/email-service.js +17 -2
- package/src/plugins/identity/index.js +413 -69
- package/src/plugins/identity/oauth2-server.js +413 -30
- package/src/plugins/identity/oidc-discovery.js +16 -8
- package/src/plugins/identity/rsa-keys.js +115 -35
- package/src/plugins/identity/server.js +166 -45
- package/src/plugins/identity/session-manager.js +53 -7
- package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
- package/src/plugins/identity/ui/routes.js +363 -255
- package/src/plugins/importer/index.js +153 -20
- package/src/plugins/index.js +9 -2
- package/src/plugins/kubernetes-inventory/index.js +6 -0
- package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
- package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
- package/src/plugins/kubernetes-inventory.plugin.js +980 -0
- package/src/plugins/metrics.plugin.js +64 -16
- package/src/plugins/ml/base-model.class.js +25 -15
- package/src/plugins/ml/regression-model.class.js +1 -1
- package/src/plugins/ml.errors.js +57 -25
- package/src/plugins/ml.plugin.js +28 -4
- package/src/plugins/namespace.js +210 -0
- package/src/plugins/plugin.class.js +180 -8
- package/src/plugins/puppeteer/console-monitor.js +729 -0
- package/src/plugins/puppeteer/cookie-manager.js +492 -0
- package/src/plugins/puppeteer/network-monitor.js +816 -0
- package/src/plugins/puppeteer/performance-manager.js +746 -0
- package/src/plugins/puppeteer/proxy-manager.js +478 -0
- package/src/plugins/puppeteer/stealth-manager.js +556 -0
- package/src/plugins/puppeteer.errors.js +81 -0
- package/src/plugins/puppeteer.plugin.js +1327 -0
- package/src/plugins/queue-consumer.plugin.js +69 -14
- package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
- package/src/plugins/recon/concerns/command-runner.js +148 -0
- package/src/plugins/recon/concerns/diff-detector.js +372 -0
- package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
- package/src/plugins/recon/concerns/process-manager.js +338 -0
- package/src/plugins/recon/concerns/report-generator.js +478 -0
- package/src/plugins/recon/concerns/security-analyzer.js +571 -0
- package/src/plugins/recon/concerns/target-normalizer.js +68 -0
- package/src/plugins/recon/config/defaults.js +321 -0
- package/src/plugins/recon/config/resources.js +370 -0
- package/src/plugins/recon/index.js +778 -0
- package/src/plugins/recon/managers/dependency-manager.js +174 -0
- package/src/plugins/recon/managers/scheduler-manager.js +179 -0
- package/src/plugins/recon/managers/storage-manager.js +745 -0
- package/src/plugins/recon/managers/target-manager.js +274 -0
- package/src/plugins/recon/stages/asn-stage.js +314 -0
- package/src/plugins/recon/stages/certificate-stage.js +84 -0
- package/src/plugins/recon/stages/dns-stage.js +107 -0
- package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
- package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
- package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
- package/src/plugins/recon/stages/http-stage.js +89 -0
- package/src/plugins/recon/stages/latency-stage.js +148 -0
- package/src/plugins/recon/stages/massdns-stage.js +302 -0
- package/src/plugins/recon/stages/osint-stage.js +1373 -0
- package/src/plugins/recon/stages/ports-stage.js +169 -0
- package/src/plugins/recon/stages/screenshot-stage.js +94 -0
- package/src/plugins/recon/stages/secrets-stage.js +514 -0
- package/src/plugins/recon/stages/subdomains-stage.js +295 -0
- package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
- package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
- package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
- package/src/plugins/recon/stages/whois-stage.js +349 -0
- package/src/plugins/recon.plugin.js +75 -0
- package/src/plugins/recon.plugin.js.backup +2635 -0
- package/src/plugins/relation.errors.js +87 -14
- package/src/plugins/replicator.plugin.js +514 -137
- package/src/plugins/replicators/base-replicator.class.js +89 -1
- package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
- package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mysql-replicator.class.js +52 -17
- package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
- package/src/plugins/replicators/postgres-replicator.class.js +62 -27
- package/src/plugins/replicators/s3db-replicator.class.js +25 -18
- package/src/plugins/replicators/schema-sync.helper.js +3 -3
- package/src/plugins/replicators/sqs-replicator.class.js +8 -2
- package/src/plugins/replicators/turso-replicator.class.js +23 -3
- package/src/plugins/replicators/webhook-replicator.class.js +42 -4
- package/src/plugins/s3-queue.plugin.js +464 -65
- package/src/plugins/scheduler.plugin.js +20 -6
- package/src/plugins/state-machine.plugin.js +40 -9
- package/src/plugins/tfstate/README.md +126 -126
- package/src/plugins/tfstate/base-driver.js +28 -4
- package/src/plugins/tfstate/errors.js +65 -10
- package/src/plugins/tfstate/filesystem-driver.js +52 -8
- package/src/plugins/tfstate/index.js +163 -90
- package/src/plugins/tfstate/s3-driver.js +64 -6
- package/src/plugins/ttl.plugin.js +72 -17
- package/src/plugins/vector/distances.js +18 -12
- package/src/plugins/vector/kmeans.js +26 -4
- package/src/resource.class.js +115 -19
- package/src/testing/factory.class.js +20 -3
- package/src/testing/seeder.class.js +7 -1
- package/src/clients/memory-client.md +0 -917
- 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
|
+
};
|
package/src/plugins/api/index.js
CHANGED
|
@@ -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
|
|
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:
|
|
480
|
+
name: this.usersResourceName,
|
|
416
481
|
attributes: {
|
|
417
482
|
id: 'string|required',
|
|
418
483
|
username: 'string|required|minlength:3',
|
|
419
|
-
email: 'string|required|email',
|
|
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',
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
if (
|
|
445
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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';
|