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,601 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route Context - Enhanced context wrapper for custom routes
|
|
3
|
+
*
|
|
4
|
+
* Provides clean, dev-friendly access to database, resources, and utilities
|
|
5
|
+
* without verbose c.get('customRouteContext').database.resources.xxx
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // Automatic (default behavior)
|
|
9
|
+
* routes: {
|
|
10
|
+
* '/:id': async (c, ctx) => {
|
|
11
|
+
* const { db, resources, resource, validator } = ctx;
|
|
12
|
+
* const url = await resources.urls.get(c.req.param('id'));
|
|
13
|
+
* return c.json({ url });
|
|
14
|
+
* }
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* RouteContext class - provides clean access to database, resources, and helpers
|
|
20
|
+
*/
|
|
21
|
+
export class RouteContext {
|
|
22
|
+
/**
|
|
23
|
+
* Create RouteContext
|
|
24
|
+
* @param {Object} honoContext - Hono context (c)
|
|
25
|
+
* @param {Object} database - s3db.js Database instance
|
|
26
|
+
* @param {Object} resource - Current resource (for resource-level routes)
|
|
27
|
+
* @param {Object} plugins - Plugin instances
|
|
28
|
+
*/
|
|
29
|
+
constructor(honoContext, database, resource = null, plugins = {}) {
|
|
30
|
+
this.c = honoContext;
|
|
31
|
+
this.db = database;
|
|
32
|
+
this.database = database; // Alias
|
|
33
|
+
this._currentResource = resource;
|
|
34
|
+
this.plugins = plugins;
|
|
35
|
+
|
|
36
|
+
// Create resources proxy for clean access
|
|
37
|
+
this.resources = this._createResourcesProxy();
|
|
38
|
+
|
|
39
|
+
// Validator helper
|
|
40
|
+
this.validator = this._createValidator();
|
|
41
|
+
|
|
42
|
+
// Current resource shortcut (for resource-level routes)
|
|
43
|
+
this.resource = resource;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create Proxy for easy resource access
|
|
48
|
+
* @private
|
|
49
|
+
*/
|
|
50
|
+
_createResourcesProxy() {
|
|
51
|
+
return new Proxy({}, {
|
|
52
|
+
get: (target, prop) => {
|
|
53
|
+
// Check if resource exists
|
|
54
|
+
if (this.database.resources[prop]) {
|
|
55
|
+
return this.database.resources[prop];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Helpful error message
|
|
59
|
+
const available = Object.keys(this.database.resources);
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Resource "${prop}" not found. Available resources: ${available.join(', ')}`
|
|
62
|
+
);
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// List available resources (for debugging)
|
|
66
|
+
ownKeys: () => {
|
|
67
|
+
return Object.keys(this.database.resources);
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
// Make resources enumerable
|
|
71
|
+
getOwnPropertyDescriptor: (target, prop) => {
|
|
72
|
+
if (this.database.resources[prop]) {
|
|
73
|
+
return {
|
|
74
|
+
enumerable: true,
|
|
75
|
+
configurable: true
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create validator helper
|
|
85
|
+
* @private
|
|
86
|
+
*/
|
|
87
|
+
_createValidator() {
|
|
88
|
+
const ctx = this;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
/**
|
|
92
|
+
* Validate data against resource schema
|
|
93
|
+
* @param {string|Object} resourceOrData - Resource name or data object
|
|
94
|
+
* @param {Object} data - Data to validate (if first param is resource name)
|
|
95
|
+
* @returns {Object} { valid: boolean, errors?: Array }
|
|
96
|
+
*/
|
|
97
|
+
validate(resourceOrData, data = null) {
|
|
98
|
+
let resource;
|
|
99
|
+
let dataToValidate;
|
|
100
|
+
|
|
101
|
+
// Case 1: validate(data) - use current resource (resource-level routes)
|
|
102
|
+
if (typeof resourceOrData === 'object' && data === null) {
|
|
103
|
+
if (!ctx._currentResource) {
|
|
104
|
+
throw new Error('validator.validate(data) requires a current resource. Use validator.validate("resourceName", data) instead.');
|
|
105
|
+
}
|
|
106
|
+
resource = ctx._currentResource;
|
|
107
|
+
dataToValidate = resourceOrData;
|
|
108
|
+
}
|
|
109
|
+
// Case 2: validate("resourceName", data)
|
|
110
|
+
else if (typeof resourceOrData === 'string' && data !== null) {
|
|
111
|
+
resource = ctx.resources[resourceOrData]; // Will throw if not found (via Proxy)
|
|
112
|
+
dataToValidate = data;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
throw new Error('Invalid arguments. Use validator.validate(data) or validator.validate("resourceName", data)');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Run validation via resource schema
|
|
119
|
+
const validation = resource.schema.validate(dataToValidate);
|
|
120
|
+
|
|
121
|
+
if (validation === true) {
|
|
122
|
+
return { valid: true };
|
|
123
|
+
} else {
|
|
124
|
+
return {
|
|
125
|
+
valid: false,
|
|
126
|
+
errors: Array.isArray(validation) ? validation : [validation]
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Validate and throw if invalid
|
|
133
|
+
* @param {string|Object} resourceOrData - Resource name or data object
|
|
134
|
+
* @param {Object} data - Data to validate
|
|
135
|
+
* @throws {Error} Validation error with details
|
|
136
|
+
*/
|
|
137
|
+
validateOrThrow(resourceOrData, data = null) {
|
|
138
|
+
const result = this.validate(resourceOrData, data);
|
|
139
|
+
|
|
140
|
+
if (!result.valid) {
|
|
141
|
+
const error = new Error('Validation failed');
|
|
142
|
+
error.code = 'VALIDATION_ERROR';
|
|
143
|
+
error.errors = result.errors;
|
|
144
|
+
error.status = 400;
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Validate request body against resource schema
|
|
151
|
+
* @param {string} resourceName - Resource name (optional if current resource exists)
|
|
152
|
+
* @returns {Promise<Object>} { valid: boolean, data?: Object, errors?: Array }
|
|
153
|
+
*/
|
|
154
|
+
async validateBody(resourceName = null) {
|
|
155
|
+
const body = await ctx.c.req.json();
|
|
156
|
+
|
|
157
|
+
if (resourceName) {
|
|
158
|
+
const result = this.validate(resourceName, body);
|
|
159
|
+
return { ...result, data: body };
|
|
160
|
+
} else {
|
|
161
|
+
const result = this.validate(body);
|
|
162
|
+
return { ...result, data: body };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ============================================
|
|
169
|
+
// Request Helpers (proxy to Hono context)
|
|
170
|
+
// ============================================
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get path parameter
|
|
174
|
+
* @param {string} name - Parameter name
|
|
175
|
+
* @returns {string} Parameter value
|
|
176
|
+
*/
|
|
177
|
+
param(name) {
|
|
178
|
+
return this.c.req.param(name);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get all path parameters
|
|
183
|
+
* @returns {Object} All parameters
|
|
184
|
+
*/
|
|
185
|
+
params() {
|
|
186
|
+
return this.c.req.param();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get query parameter
|
|
191
|
+
* @param {string} name - Query parameter name
|
|
192
|
+
* @returns {string|undefined} Query value
|
|
193
|
+
*/
|
|
194
|
+
query(name) {
|
|
195
|
+
return this.c.req.query(name);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get all query parameters
|
|
200
|
+
* @returns {Object} All query parameters
|
|
201
|
+
*/
|
|
202
|
+
queries() {
|
|
203
|
+
return this.c.req.query();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get request header
|
|
208
|
+
* @param {string} name - Header name
|
|
209
|
+
* @returns {string|undefined} Header value
|
|
210
|
+
*/
|
|
211
|
+
header(name) {
|
|
212
|
+
return this.c.req.header(name);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Parse JSON body
|
|
217
|
+
* @returns {Promise<Object>} Parsed body
|
|
218
|
+
*/
|
|
219
|
+
async body() {
|
|
220
|
+
return await this.c.req.json();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get request body as text
|
|
225
|
+
* @returns {Promise<string>} Body text
|
|
226
|
+
*/
|
|
227
|
+
async text() {
|
|
228
|
+
return await this.c.req.text();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get request body as FormData
|
|
233
|
+
* @returns {Promise<FormData>} FormData
|
|
234
|
+
*/
|
|
235
|
+
async formData() {
|
|
236
|
+
return await this.c.req.formData();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ============================================
|
|
240
|
+
// Response Helpers (shortcuts)
|
|
241
|
+
// ============================================
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Send JSON response
|
|
245
|
+
* @param {Object} data - Response data
|
|
246
|
+
* @param {number} status - HTTP status code
|
|
247
|
+
* @returns {Response} Hono response
|
|
248
|
+
*/
|
|
249
|
+
json(data, status = 200) {
|
|
250
|
+
return this.c.json(data, status);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Send success response
|
|
255
|
+
* @param {Object} data - Response data
|
|
256
|
+
* @param {number} status - HTTP status code
|
|
257
|
+
* @returns {Response} Success response
|
|
258
|
+
*/
|
|
259
|
+
success(data, status = 200) {
|
|
260
|
+
return this.c.json({
|
|
261
|
+
success: true,
|
|
262
|
+
data
|
|
263
|
+
}, status);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Send error response
|
|
268
|
+
* @param {string} message - Error message
|
|
269
|
+
* @param {number} status - HTTP status code
|
|
270
|
+
* @returns {Response} Error response
|
|
271
|
+
*/
|
|
272
|
+
error(message, status = 400) {
|
|
273
|
+
return this.c.json({
|
|
274
|
+
success: false,
|
|
275
|
+
error: {
|
|
276
|
+
message,
|
|
277
|
+
code: 'ERROR',
|
|
278
|
+
status
|
|
279
|
+
}
|
|
280
|
+
}, status);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Send 404 Not Found
|
|
285
|
+
* @param {string} message - Optional message
|
|
286
|
+
* @returns {Response} 404 response
|
|
287
|
+
*/
|
|
288
|
+
notFound(message = 'Not found') {
|
|
289
|
+
return this.c.json({
|
|
290
|
+
success: false,
|
|
291
|
+
error: {
|
|
292
|
+
message,
|
|
293
|
+
code: 'NOT_FOUND',
|
|
294
|
+
status: 404
|
|
295
|
+
}
|
|
296
|
+
}, 404);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Send 401 Unauthorized
|
|
301
|
+
* @param {string} message - Optional message
|
|
302
|
+
* @returns {Response} 401 response
|
|
303
|
+
*/
|
|
304
|
+
unauthorized(message = 'Unauthorized') {
|
|
305
|
+
return this.c.json({
|
|
306
|
+
success: false,
|
|
307
|
+
error: {
|
|
308
|
+
message,
|
|
309
|
+
code: 'UNAUTHORIZED',
|
|
310
|
+
status: 401
|
|
311
|
+
}
|
|
312
|
+
}, 401);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Send 403 Forbidden
|
|
317
|
+
* @param {string} message - Optional message
|
|
318
|
+
* @returns {Response} 403 response
|
|
319
|
+
*/
|
|
320
|
+
forbidden(message = 'Forbidden') {
|
|
321
|
+
return this.c.json({
|
|
322
|
+
success: false,
|
|
323
|
+
error: {
|
|
324
|
+
message,
|
|
325
|
+
code: 'FORBIDDEN',
|
|
326
|
+
status: 403
|
|
327
|
+
}
|
|
328
|
+
}, 403);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Send HTML response
|
|
333
|
+
* @param {string} html - HTML content
|
|
334
|
+
* @param {number} status - HTTP status code
|
|
335
|
+
* @returns {Response} HTML response
|
|
336
|
+
*/
|
|
337
|
+
html(html, status = 200) {
|
|
338
|
+
return this.c.html(html, status);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Redirect to URL
|
|
343
|
+
* @param {string} url - Target URL
|
|
344
|
+
* @param {number} status - HTTP status code (default 302)
|
|
345
|
+
* @returns {Response} Redirect response
|
|
346
|
+
*/
|
|
347
|
+
redirect(url, status = 302) {
|
|
348
|
+
return this.c.redirect(url, status);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Render template (if template engine is configured)
|
|
353
|
+
* @param {string|JSX.Element} template - Template name or JSX element
|
|
354
|
+
* @param {Object} data - Data to pass to template
|
|
355
|
+
* @param {Object} options - Render options
|
|
356
|
+
* @returns {Promise<Response>} Rendered HTML response
|
|
357
|
+
*/
|
|
358
|
+
async render(template, data = {}, options = {}) {
|
|
359
|
+
if (!this.c.render) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
'Template engine not configured. Use ApiPlugin with templates: { engine: "ejs" | "pug" | "jsx" }'
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return await this.c.render(template, data, options);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ============================================
|
|
369
|
+
// Context Helpers
|
|
370
|
+
// ============================================
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Get authenticated user (if auth is enabled)
|
|
374
|
+
* @returns {Object|null} User object
|
|
375
|
+
*/
|
|
376
|
+
get user() {
|
|
377
|
+
return this.c.get('user') || null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Get session (if session tracking enabled)
|
|
382
|
+
* @returns {Object|null} Session object
|
|
383
|
+
*/
|
|
384
|
+
get session() {
|
|
385
|
+
return this.c.get('session') || null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Get session ID (if session tracking enabled)
|
|
390
|
+
* @returns {string|null} Session ID
|
|
391
|
+
*/
|
|
392
|
+
get sessionId() {
|
|
393
|
+
return this.c.get('sessionId') || null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Get request ID (if request ID tracking enabled)
|
|
398
|
+
* @returns {string|null} Request ID
|
|
399
|
+
*/
|
|
400
|
+
get requestId() {
|
|
401
|
+
return this.c.get('requestId') || null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Check if user is authenticated
|
|
406
|
+
* @returns {boolean} True if authenticated
|
|
407
|
+
*/
|
|
408
|
+
get isAuthenticated() {
|
|
409
|
+
return !!this.user;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Check if user has scope
|
|
414
|
+
* @param {string} scope - Scope to check
|
|
415
|
+
* @returns {boolean} True if user has scope
|
|
416
|
+
*/
|
|
417
|
+
hasScope(scope) {
|
|
418
|
+
return this.user?.scopes?.includes(scope) || false;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Check if user has any of the scopes
|
|
423
|
+
* @param {Array<string>} scopes - Scopes to check
|
|
424
|
+
* @returns {boolean} True if user has any scope
|
|
425
|
+
*/
|
|
426
|
+
hasAnyScope(...scopes) {
|
|
427
|
+
return scopes.some(scope => this.hasScope(scope));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Check if user has all scopes
|
|
432
|
+
* @param {Array<string>} scopes - Scopes to check
|
|
433
|
+
* @returns {boolean} True if user has all scopes
|
|
434
|
+
*/
|
|
435
|
+
hasAllScopes(...scopes) {
|
|
436
|
+
return scopes.every(scope => this.hasScope(scope));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Require authentication (throw if not authenticated)
|
|
441
|
+
* @throws {Error} If not authenticated
|
|
442
|
+
*/
|
|
443
|
+
requireAuth() {
|
|
444
|
+
if (!this.isAuthenticated) {
|
|
445
|
+
throw Object.assign(
|
|
446
|
+
new Error('Authentication required'),
|
|
447
|
+
{ status: 401, code: 'UNAUTHORIZED' }
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Require scope (throw if not authorized)
|
|
454
|
+
* @param {string} scope - Required scope
|
|
455
|
+
* @throws {Error} If scope missing
|
|
456
|
+
*/
|
|
457
|
+
requireScope(scope) {
|
|
458
|
+
this.requireAuth();
|
|
459
|
+
|
|
460
|
+
if (!this.hasScope(scope)) {
|
|
461
|
+
throw Object.assign(
|
|
462
|
+
new Error(`Scope required: ${scope}`),
|
|
463
|
+
{ status: 403, code: 'FORBIDDEN' }
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ============================================
|
|
469
|
+
// Partition Helpers (for Guards)
|
|
470
|
+
// ============================================
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Set partition filter for current query (used by guards for tenant isolation)
|
|
474
|
+
* @param {string} partitionName - Partition name (e.g., 'byUserId')
|
|
475
|
+
* @param {Object} partitionFields - Partition field values (e.g., { userId: 'user123' })
|
|
476
|
+
* @returns {void}
|
|
477
|
+
*
|
|
478
|
+
* @example
|
|
479
|
+
* // In guard:
|
|
480
|
+
* users.guard = {
|
|
481
|
+
* list: (ctx) => {
|
|
482
|
+
* if (ctx.user.scopes?.includes('preset:admin')) {
|
|
483
|
+
* return true; // Admin sees everything
|
|
484
|
+
* }
|
|
485
|
+
*
|
|
486
|
+
* // Regular user sees only their data (O(1) via partition)
|
|
487
|
+
* ctx.setPartition('byUserId', { userId: ctx.user.id });
|
|
488
|
+
* return true;
|
|
489
|
+
* }
|
|
490
|
+
* };
|
|
491
|
+
*/
|
|
492
|
+
setPartition(partitionName, partitionFields) {
|
|
493
|
+
if (!this._partitionFilters) {
|
|
494
|
+
this._partitionFilters = [];
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
this._partitionFilters.push({ partitionName, partitionFields });
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Get partition filters set by guards
|
|
502
|
+
* @returns {Array} Partition filters
|
|
503
|
+
* @internal
|
|
504
|
+
*/
|
|
505
|
+
getPartitionFilters() {
|
|
506
|
+
return this._partitionFilters || [];
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Clear partition filters
|
|
511
|
+
* @returns {void}
|
|
512
|
+
* @internal
|
|
513
|
+
*/
|
|
514
|
+
clearPartitionFilters() {
|
|
515
|
+
this._partitionFilters = [];
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Check if partition filters are set
|
|
520
|
+
* @returns {boolean} True if partition filters exist
|
|
521
|
+
*/
|
|
522
|
+
hasPartitionFilters() {
|
|
523
|
+
return this._partitionFilters && this._partitionFilters.length > 0;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Wrap route handler to provide enhanced context
|
|
529
|
+
*
|
|
530
|
+
* @param {Function} handler - Route handler (c, ctx) => Response
|
|
531
|
+
* @param {Object} options - Options
|
|
532
|
+
* @param {Object} options.resource - Current resource (for resource-level routes)
|
|
533
|
+
* @returns {Function} Wrapped handler
|
|
534
|
+
*
|
|
535
|
+
* @example
|
|
536
|
+
* // Plugin-level route
|
|
537
|
+
* routes: {
|
|
538
|
+
* 'GET /health': withContext(async (c, ctx) => {
|
|
539
|
+
* const { db, resources } = ctx;
|
|
540
|
+
* return ctx.success({ uptime: process.uptime() });
|
|
541
|
+
* })
|
|
542
|
+
* }
|
|
543
|
+
*
|
|
544
|
+
* @example
|
|
545
|
+
* // Resource-level route (ctx.resource is auto-populated)
|
|
546
|
+
* resources: {
|
|
547
|
+
* users: {
|
|
548
|
+
* api: {
|
|
549
|
+
* 'POST /users/:id/reset-password': withContext(async (c, ctx) => {
|
|
550
|
+
* const { resource, validator } = ctx;
|
|
551
|
+
* const id = ctx.param('id');
|
|
552
|
+
*
|
|
553
|
+
* // Validate body against current resource schema
|
|
554
|
+
* const { valid, errors } = await ctx.validator.validateBody();
|
|
555
|
+
* if (!valid) return ctx.error(errors, 400);
|
|
556
|
+
*
|
|
557
|
+
* // Use current resource
|
|
558
|
+
* const user = await resource.get(id);
|
|
559
|
+
* return ctx.success({ user });
|
|
560
|
+
* })
|
|
561
|
+
* }
|
|
562
|
+
* }
|
|
563
|
+
* }
|
|
564
|
+
*/
|
|
565
|
+
export function withContext(handler, options = {}) {
|
|
566
|
+
return async (c) => {
|
|
567
|
+
// Extract legacy context (for backward compatibility)
|
|
568
|
+
const legacyContext = c.get('customRouteContext') || {};
|
|
569
|
+
const { database, resource, plugins = {} } = legacyContext;
|
|
570
|
+
|
|
571
|
+
// Use options.resource if provided (resource-level routes)
|
|
572
|
+
const currentResource = options.resource || resource || null;
|
|
573
|
+
|
|
574
|
+
// Create enhanced context
|
|
575
|
+
const ctx = new RouteContext(c, database, currentResource, plugins);
|
|
576
|
+
|
|
577
|
+
// Call handler with both Hono context and enhanced context
|
|
578
|
+
return await handler(c, ctx);
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Auto-wrap handler - automatically wraps handlers to provide enhanced context
|
|
584
|
+
* This is used internally by the API Plugin to make enhanced context the default
|
|
585
|
+
*
|
|
586
|
+
* @param {Function} handler - Route handler
|
|
587
|
+
* @param {Object} options - Options
|
|
588
|
+
* @returns {Function} Wrapped handler
|
|
589
|
+
* @private
|
|
590
|
+
*/
|
|
591
|
+
export function autoWrapHandler(handler, options = {}) {
|
|
592
|
+
// Check if handler is already wrapped or if it only expects 1 argument (c)
|
|
593
|
+
// In that case, don't wrap (backward compatibility)
|
|
594
|
+
if (handler.length === 1) {
|
|
595
|
+
// Handler only expects (c) - use legacy behavior
|
|
596
|
+
return handler;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Handler expects (c, ctx) - wrap it
|
|
600
|
+
return withContext(handler, options);
|
|
601
|
+
}
|