bro-framework 2.4.5 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/bin/bro.js +279 -292
- package/package.json +138 -103
- package/src/dashboard.js +81 -0
- package/src/database.js +105 -0
- package/src/edge.js +244 -0
- package/src/engine.js +251 -0
- package/src/index.d.ts +46 -9
- package/src/index.js +10 -0
- package/src/logger.js +47 -0
- package/src/next.d.ts +51 -10
- package/src/next.js +82 -156
- package/src/observability.js +81 -0
- package/src/plugins.js +135 -0
- package/src/policy-auth.js +88 -0
- package/src/router.js +1 -1
- package/src/sdk.js +229 -169
- package/src/server.js +181 -170
- package/src/studio.js +162 -0
- package/src/task-engine.js +88 -0
- package/src/testing.js +142 -0
- package/src/uploads.js +129 -0
package/src/testing.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { createServer } from './server.js';
|
|
3
|
+
import { signJwt } from './auth.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a native Fetch-based testing harness for bro.js
|
|
7
|
+
*/
|
|
8
|
+
export async function createTestHarness(globalConfig = {}, options = {}) {
|
|
9
|
+
let instance;
|
|
10
|
+
const port = options.port || 0;
|
|
11
|
+
let runningServer;
|
|
12
|
+
let testUrl = '';
|
|
13
|
+
|
|
14
|
+
const harness = {
|
|
15
|
+
fixtures: options.fixtures || {},
|
|
16
|
+
stores: options.stores || {},
|
|
17
|
+
|
|
18
|
+
async start() {
|
|
19
|
+
const routesDir = options.routesDir || path.join(process.cwd(), 'routes');
|
|
20
|
+
|
|
21
|
+
// Inject testing fixtures into the framework config
|
|
22
|
+
const testConfig = {
|
|
23
|
+
...globalConfig,
|
|
24
|
+
fixtures: this.fixtures,
|
|
25
|
+
stores: this.stores,
|
|
26
|
+
// Override for testing
|
|
27
|
+
jwtSecret: globalConfig.jwtSecret || 'test_secret_for_harness'
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
instance = await createServer(testConfig, routesDir, options.db || null);
|
|
31
|
+
|
|
32
|
+
runningServer = instance.server;
|
|
33
|
+
await new Promise(resolve => runningServer.listen(port, resolve));
|
|
34
|
+
testUrl = `http://localhost:${runningServer.address().port}`;
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
async stop() {
|
|
38
|
+
if (runningServer && runningServer.closeAllConnections) {
|
|
39
|
+
runningServer.closeAllConnections();
|
|
40
|
+
}
|
|
41
|
+
if (instance?.shutdown) {
|
|
42
|
+
await instance.shutdown();
|
|
43
|
+
} else if (runningServer) {
|
|
44
|
+
await new Promise(resolve => runningServer.close(resolve));
|
|
45
|
+
}
|
|
46
|
+
runningServer = null;
|
|
47
|
+
if (typeof jest !== 'undefined' && jest.clearAllTimers) {
|
|
48
|
+
jest.clearAllTimers();
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
auth(payload) {
|
|
53
|
+
const secret = globalConfig.jwtSecret || 'test_secret_for_harness';
|
|
54
|
+
return signJwt(payload, secret, { expiresIn: '1h' });
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// Clock hooks
|
|
58
|
+
useFakeTimers(now) {
|
|
59
|
+
if (typeof jest !== 'undefined') {
|
|
60
|
+
jest.useFakeTimers();
|
|
61
|
+
if (now) jest.setSystemTime(now);
|
|
62
|
+
} else {
|
|
63
|
+
console.warn('Fake timers only supported when running under Jest/Vitest environments.');
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
useRealTimers() {
|
|
67
|
+
if (typeof jest !== 'undefined') {
|
|
68
|
+
jest.useRealTimers();
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// DB Transactions
|
|
73
|
+
async runInTransaction(testFn) {
|
|
74
|
+
if (!options.db || typeof options.db.transaction !== 'function') {
|
|
75
|
+
throw new Error('Database adapter does not support transactions or no DB provided.');
|
|
76
|
+
}
|
|
77
|
+
return options.db.transaction(async (trx) => {
|
|
78
|
+
try {
|
|
79
|
+
await testFn(trx);
|
|
80
|
+
} finally {
|
|
81
|
+
// If the test framework supports rollback via error, we would throw here,
|
|
82
|
+
// but for isolated testing we assume the trx handles rollback gracefully if test fails.
|
|
83
|
+
if (trx.rollback) await trx.rollback();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
// Direct route testing without network
|
|
89
|
+
async testRoute(routeModule, mockCtx = {}) {
|
|
90
|
+
const config = routeModule.default || routeModule;
|
|
91
|
+
const ctx = {
|
|
92
|
+
env: globalConfig.env || {},
|
|
93
|
+
db: options.db || null,
|
|
94
|
+
user: null,
|
|
95
|
+
body: {},
|
|
96
|
+
query: {},
|
|
97
|
+
params: {},
|
|
98
|
+
...mockCtx
|
|
99
|
+
};
|
|
100
|
+
return config.handler(ctx);
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
client(defaultHeaders = {}) {
|
|
104
|
+
return {
|
|
105
|
+
async fetch(route, fetchOpts = {}) {
|
|
106
|
+
const headers = { ...defaultHeaders, ...(fetchOpts.headers || {}) };
|
|
107
|
+
return fetch(`${testUrl}${route}`, { ...fetchOpts, headers });
|
|
108
|
+
},
|
|
109
|
+
async get(route, fetchOpts) {
|
|
110
|
+
return this.fetch(route, { method: 'GET', ...fetchOpts });
|
|
111
|
+
},
|
|
112
|
+
async post(route, body, fetchOpts = {}) {
|
|
113
|
+
return this.fetch(route, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
body: JSON.stringify(body),
|
|
116
|
+
headers: { 'Content-Type': 'application/json', ...(fetchOpts.headers || {}) },
|
|
117
|
+
...fetchOpts
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
async put(route, body, fetchOpts = {}) {
|
|
121
|
+
return this.fetch(route, {
|
|
122
|
+
method: 'PUT',
|
|
123
|
+
body: JSON.stringify(body),
|
|
124
|
+
headers: { 'Content-Type': 'application/json', ...(fetchOpts.headers || {}) },
|
|
125
|
+
...fetchOpts
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
async delete(route, fetchOpts) {
|
|
129
|
+
return this.fetch(route, { method: 'DELETE', ...fetchOpts });
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// Contract test helper
|
|
133
|
+
async assertContract(response, schema) {
|
|
134
|
+
const json = await response.json();
|
|
135
|
+
return schema.parse(json);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return harness;
|
|
142
|
+
}
|
package/src/uploads.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Secure Upload Pipeline for bro.js
|
|
5
|
+
* Provides streaming abstractions and adapters for object storage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class SecureUploadPipeline {
|
|
9
|
+
constructor(config = {}) {
|
|
10
|
+
this.adapters = config.adapters || {};
|
|
11
|
+
this.defaultAdapter = config.defaultAdapter || 'local';
|
|
12
|
+
this.maxSize = config.maxSize || 50 * 1024 * 1024; // 50MB default
|
|
13
|
+
this.allowedMimeTypes = config.allowedMimeTypes || ['image/jpeg', 'image/png', 'application/pdf'];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async processUpload(file, options = {}) {
|
|
17
|
+
this._validateFile(file, options);
|
|
18
|
+
|
|
19
|
+
const adapterName = options.adapter || this.defaultAdapter;
|
|
20
|
+
const adapter = this.adapters[adapterName];
|
|
21
|
+
|
|
22
|
+
if (!adapter) {
|
|
23
|
+
throw new Error(`Upload adapter '${adapterName}' not found.`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const key = crypto.randomUUID() + '-' + file.originalname;
|
|
27
|
+
const url = await adapter.upload(key, file.buffer || file.stream(), file.mimetype);
|
|
28
|
+
|
|
29
|
+
return { key, url, size: file.size, mimetype: file.mimetype };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_validateFile(file, options) {
|
|
33
|
+
const size = file.size;
|
|
34
|
+
const maxSize = options.maxSize || this.maxSize;
|
|
35
|
+
if (size > maxSize) {
|
|
36
|
+
throw new Error(`File size ${size} exceeds quota of ${maxSize} bytes.`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const mime = file.mimetype || file.type;
|
|
40
|
+
const allowed = options.allowedMimeTypes || this.allowedMimeTypes;
|
|
41
|
+
if (allowed !== '*' && !allowed.includes(mime)) {
|
|
42
|
+
throw new Error(`MIME type ${mime} is not allowed.`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async getSignedUrl(key, adapterName = this.defaultAdapter, expiresIn = 3600) {
|
|
47
|
+
const adapter = this.adapters[adapterName];
|
|
48
|
+
if (!adapter || typeof adapter.getSignedUrl !== 'function') {
|
|
49
|
+
throw new Error(`Adapter '${adapterName}' does not support signed URLs.`);
|
|
50
|
+
}
|
|
51
|
+
return await adapter.getSignedUrl(key, expiresIn);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Example Local Storage Adapter
|
|
57
|
+
*/
|
|
58
|
+
import fs from 'fs';
|
|
59
|
+
import path from 'path';
|
|
60
|
+
|
|
61
|
+
export class LocalStorageAdapter {
|
|
62
|
+
constructor(uploadDir) {
|
|
63
|
+
this.uploadDir = uploadDir || path.join(process.cwd(), 'uploads');
|
|
64
|
+
if (!fs.existsSync(this.uploadDir)) {
|
|
65
|
+
fs.mkdirSync(this.uploadDir, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async save(fileStream, originalName, mimeType) {
|
|
70
|
+
const fileName = `${Date.now()}-${originalName}`;
|
|
71
|
+
const filePath = path.join(this.uploadDir, fileName);
|
|
72
|
+
const writeStream = fs.createWriteStream(filePath);
|
|
73
|
+
await new Promise((resolve, reject) => {
|
|
74
|
+
fileStream.pipe(writeStream);
|
|
75
|
+
fileStream.on('end', resolve);
|
|
76
|
+
fileStream.on('error', reject);
|
|
77
|
+
});
|
|
78
|
+
return { url: `/uploads/${fileName}`, id: fileName, path: filePath };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async delete(id) {
|
|
82
|
+
const filePath = path.join(this.uploadDir, id);
|
|
83
|
+
if (fs.existsSync(filePath)) {
|
|
84
|
+
await fs.promises.unlink(filePath);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class S3StorageAdapter {
|
|
90
|
+
constructor(config) {
|
|
91
|
+
this.config = config;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async save(fileStream, originalName, mimeType) {
|
|
95
|
+
let AWS;
|
|
96
|
+
try {
|
|
97
|
+
AWS = await import('@aws-sdk/client-s3');
|
|
98
|
+
} catch (e) {
|
|
99
|
+
throw new Error('Please install @aws-sdk/client-s3 to use S3StorageAdapter');
|
|
100
|
+
}
|
|
101
|
+
const { S3Client } = AWS;
|
|
102
|
+
const { Upload } = await import('@aws-sdk/lib-storage');
|
|
103
|
+
|
|
104
|
+
const client = new S3Client(this.config);
|
|
105
|
+
const key = `${Date.now()}-${originalName}`;
|
|
106
|
+
|
|
107
|
+
const upload = new Upload({
|
|
108
|
+
client,
|
|
109
|
+
params: {
|
|
110
|
+
Bucket: this.config.bucket,
|
|
111
|
+
Key: key,
|
|
112
|
+
Body: fileStream,
|
|
113
|
+
ContentType: mimeType
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const result = await upload.done();
|
|
118
|
+
return { url: result.Location, id: key };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async delete(id) {
|
|
122
|
+
let AWS = await import('@aws-sdk/client-s3');
|
|
123
|
+
const client = new AWS.S3Client(this.config);
|
|
124
|
+
await client.send(new AWS.DeleteObjectCommand({
|
|
125
|
+
Bucket: this.config.bucket,
|
|
126
|
+
Key: id
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
}
|