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/src/engine.js ADDED
@@ -0,0 +1,251 @@
1
+
2
+ export function resolveIdentity(req, config = {}) {
3
+ const headers = req.headers || {};
4
+ let ip = req.ip || (req.socket && req.socket.remoteAddress) || 'anonymous';
5
+
6
+ const getHeader = (k) => {
7
+ if (typeof headers.get === 'function') return headers.get(k);
8
+ if (typeof headers[k] === 'string') return headers[k];
9
+ if (Array.isArray(headers[k])) return headers[k][0];
10
+ return null;
11
+ };
12
+
13
+ if (config.trustProxy) {
14
+ const xff = getHeader('x-forwarded-for');
15
+ if (xff) {
16
+ const ips = xff.split(',').map(s => s.trim()).filter(Boolean);
17
+ if (ips.length > 0) {
18
+ ip = ips[0];
19
+ }
20
+ }
21
+ }
22
+
23
+ const auth = getHeader('authorization');
24
+ return getHeader('x-api-key') || (auth && auth.startsWith('Bearer ') ? auth.split(' ')[1] : null) || ip;
25
+ }
26
+
27
+
28
+
29
+
30
+
31
+ export class RouteRegistry {
32
+ constructor() {
33
+ this.routes = [];
34
+ }
35
+ register(routeInfo) {
36
+ this.routes.push(routeInfo);
37
+ }
38
+ getRoutes() {
39
+ return this.routes;
40
+ }
41
+ reset() {
42
+ this.routes = [];
43
+ }
44
+ clear() {
45
+ this.routes = [];
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Validates Zod errors into RFC 7807 problem details
51
+ */
52
+ export function formatZodError(zodError) {
53
+ const details = {};
54
+ for (const err of zodError.errors) {
55
+ details[err.path.join('.')] = err.message;
56
+ }
57
+ return details;
58
+ }
59
+
60
+ export function formatErrorEnvelope(status, title, details, reqId, instanceUrl = 'about:blank') {
61
+ const codeMap = { 400: 'BAD_REQUEST', 401: 'UNAUTHORIZED', 403: 'FORBIDDEN', 404: 'NOT_FOUND', 429: 'TOO_MANY_REQUESTS' };
62
+ const code = codeMap[status] || (status >= 500 ? 'INTERNAL_ERROR' : 'ERROR');
63
+
64
+ const payload = {
65
+ type: `https://brojs.dev/errors/${code.toLowerCase()}`,
66
+ title,
67
+ status,
68
+ instance: instanceUrl,
69
+ requestId: reqId
70
+ };
71
+
72
+ if (details) {
73
+ if (typeof details === 'string') payload.detail = details;
74
+ else payload.errors = details;
75
+ }
76
+
77
+ return payload;
78
+ }
79
+
80
+ /**
81
+ * The transport-agnostic execution pipeline.
82
+ * @param {Object} routeConfig - The exported configuration from defineRoute.
83
+ * @param {Object} requestData - Standardized request object: { method, originalUrl, headers, body, query, params, ip, files }
84
+ * @param {Object} globalConfig - The global framework configuration.
85
+ * @param {Object} ctxExtras - Additional context properties (e.g. db, redis, io, pluginManager).
86
+ * @returns {Promise<{ status: number, body: any, headers: Record<string, string>, error?: Error }>}
87
+ */
88
+ export async function executeRequest(routeConfig, requestData, globalConfig, ctxExtras) {
89
+ const reqId = requestData.headers['x-request-id'] || (ctxExtras.generateId ? ctxExtras.generateId() : Date.now().toString());
90
+ let responseHeaders = { 'X-Request-Id': reqId };
91
+ let errorHeaders = { 'X-Request-Id': reqId, 'Content-Type': 'application/problem+json' };
92
+
93
+ try {
94
+ const ctx = {
95
+ req: requestData.originalUrl,
96
+ method: requestData.method,
97
+ ip: requestData.ip,
98
+ headers: requestData.headers,
99
+ locale: requestData.locale,
100
+ requestId: reqId,
101
+ ...ctxExtras,
102
+ env: globalConfig.envData || process.env,
103
+ body: requestData.body,
104
+ query: requestData.query,
105
+ params: requestData.params,
106
+ files: requestData.files,
107
+ user: null
108
+ };
109
+
110
+ if (ctxExtras.pluginManager) {
111
+ await ctxExtras.pluginManager.runOnContext(ctx);
112
+ }
113
+
114
+ // Rate Limiting Evaluation
115
+ const activeRateLimit = routeConfig.rateLimit === false ? null : (routeConfig.rateLimit || globalConfig.rateLimit);
116
+ const redisClient = ctxExtras.redis;
117
+
118
+ if (activeRateLimit && redisClient) {
119
+ const rlKey = generateRateLimitKey ? await generateRateLimitKey(requestData, globalConfig) : `bro:rl:${requestData.originalUrl}:${requestData.ip}`;
120
+ try {
121
+ const current = await redisClient.incr(rlKey);
122
+ const windowSeconds = Math.floor(activeRateLimit.windowMs / 1000);
123
+ if (current === 1) {
124
+ await redisClient.expire(rlKey, windowSeconds);
125
+ }
126
+ if (current > activeRateLimit.max) {
127
+ responseHeaders['Retry-After'] = windowSeconds.toString();
128
+ return { status: 429, headers: responseHeaders, body: formatErrorEnvelope(429, 'Too Many Requests', null, reqId) };
129
+ }
130
+ } catch (err) {
131
+ console.error('[bro.js] Rate Limit Error:', err);
132
+ }
133
+ }
134
+
135
+ // Auth verification
136
+ if (routeConfig.auth === 'api-key') {
137
+ const apiKey = requestData.headers['x-api-key'] || (requestData.headers['authorization'] || '').replace('Bearer ', '');
138
+ const validKey = globalConfig.auth?.apiKey || process.env.API_KEY;
139
+
140
+ let isValid = false;
141
+ if (Array.isArray(validKey)) {
142
+ isValid = validKey.includes(apiKey);
143
+ } else {
144
+ isValid = apiKey && apiKey === validKey;
145
+ }
146
+
147
+ if (!isValid) {
148
+ return { status: 401, headers: errorHeaders, body: formatErrorEnvelope(401, 'Unauthorized', 'Missing or invalid API key', reqId, requestData.originalUrl) };
149
+ }
150
+ } else if (routeConfig.auth) {
151
+ const authHeader = requestData.headers['authorization'];
152
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
153
+ return { status: 401, headers: errorHeaders, body: formatErrorEnvelope(401, 'Unauthorized', 'Missing or invalid Bearer token', reqId, requestData.originalUrl) };
154
+ }
155
+
156
+ const token = authHeader.split(' ')[1] ?? '';
157
+ const authResult = verifyJwt(token, globalConfig.jwtSecret);
158
+
159
+ if (!authResult.valid) {
160
+ return { status: 401, headers: errorHeaders, body: formatErrorEnvelope(401, 'Unauthorized', authResult.error, reqId, requestData.originalUrl) };
161
+ }
162
+ ctx.user = authResult.payload ?? null;
163
+
164
+ if (Array.isArray(routeConfig.auth) && routeConfig.auth.length > 0) {
165
+ if (!ctx.user || !ctx.user.role || !routeConfig.auth.includes(ctx.user.role)) {
166
+ return { status: 403, headers: errorHeaders, body: formatErrorEnvelope(403, 'Forbidden', 'Insufficient role permissions', reqId, requestData.originalUrl) };
167
+ }
168
+ }
169
+ }
170
+
171
+ // Zod validation
172
+ if (routeConfig.params) {
173
+ const result = await routeConfig.params.safeParseAsync(requestData.params || {});
174
+ if (!result.success) {
175
+ return { status: 400, headers: errorHeaders, body: formatErrorEnvelope(400, 'Invalid URL Parameters', formatZodError(result.error), reqId, requestData.originalUrl) };
176
+ }
177
+ ctx.params = result.data;
178
+ }
179
+
180
+ if (routeConfig.body) {
181
+ const result = await routeConfig.body.safeParseAsync(requestData.body || {});
182
+ if (!result.success) {
183
+ return { status: 400, headers: errorHeaders, body: formatErrorEnvelope(400, 'Invalid Request Body', formatZodError(result.error), reqId, requestData.originalUrl) };
184
+ }
185
+ ctx.body = result.data;
186
+ }
187
+
188
+ if (routeConfig.query) {
189
+ const result = await routeConfig.query.safeParseAsync(requestData.query || {});
190
+ if (!result.success) {
191
+ return { status: 400, headers: errorHeaders, body: formatErrorEnvelope(400, 'Invalid Query Parameters', formatZodError(result.error), reqId, requestData.originalUrl) };
192
+ }
193
+ ctx.query = result.data;
194
+ }
195
+
196
+ if (typeof routeConfig.handler !== 'function') {
197
+ throw new Error('Route "handler" is missing or is not a function');
198
+ }
199
+
200
+ // Cache evaluation
201
+ let cacheKey = null;
202
+
203
+ if (routeConfig.cache && redisClient && requestData.method === 'GET') {
204
+ const authIdentity = crypto.createHash('sha256').update(String(requestData.headers['authorization'] || requestData.headers['x-api-key'] || requestData.ip || 'anonymous')).digest('hex');
205
+ const locale = requestData.locale || 'en';
206
+ // URL has query parameters, so use originalUrl completely
207
+ cacheKey = `bro:cache:${requestData.method}:${requestData.originalUrl}:${locale}:${authIdentity}`;
208
+
209
+ try {
210
+ const cached = await redisClient.get(cacheKey);
211
+ if (cached) {
212
+ return { status: 200, headers: responseHeaders, body: JSON.parse(cached) };
213
+ }
214
+ } catch (err) {
215
+ console.error('[bro.js] Cache parsing failed, deleting key:', cacheKey);
216
+ await redisClient.del(cacheKey).catch(() => {});
217
+ }
218
+ }
219
+
220
+ // Execute User Handler
221
+ let responseData = await routeConfig.handler(ctx);
222
+
223
+ // Runtime Response Validation
224
+ if (routeConfig.response && globalConfig.validateResponse !== false) {
225
+ const result = await routeConfig.response.safeParseAsync(responseData);
226
+ if (!result.success) {
227
+ console.error('[bro.js] ⚠️ Response Validation Failed:', formatZodError(result.error));
228
+ if (process.env.NODE_ENV === 'production' && globalConfig.validateResponse === 'strict') {
229
+ throw new Error('Response Validation Failed');
230
+ } else if (globalConfig.validateResponse !== 'warn') {
231
+ // Strip invalid data, throw 500
232
+ throw new Error('Response Validation Failed');
233
+ }
234
+ } else {
235
+ responseData = result.data;
236
+ }
237
+ }
238
+
239
+ if (cacheKey && routeConfig.cache) {
240
+ await redisClient.setEx(cacheKey, routeConfig.cache, JSON.stringify(responseData));
241
+ }
242
+
243
+ return { status: 200, headers: responseHeaders, body: responseData };
244
+
245
+ } catch (err) {
246
+ const status = err.status || 500;
247
+ const title = status === 500 ? 'Internal Server Error' : err.message;
248
+ const isProd = process.env.NODE_ENV === 'production';
249
+ return { status, headers: errorHeaders, body: formatErrorEnvelope(status, isProd && status >= 500 ? 'Internal Server Error' : title, isProd && status >= 500 ? null : err.details, reqId, requestData.originalUrl), error: status >= 500 ? err : undefined };
250
+ }
251
+ }
package/src/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /// <reference types="node" />
1
2
  import { z, ZodTypeAny } from 'zod';
2
3
 
3
4
  type InferZod<T> = T extends ZodTypeAny ? z.infer<T> : any;
@@ -14,14 +15,20 @@ export interface UploadedFile {
14
15
  buffer?: Buffer;
15
16
  }
16
17
 
17
- export interface BroContext<Body = any, Params = any, Query = any> {
18
- env?: any;
18
+ export interface AppContext<Env = any, Db = any, User = any> {
19
+ env: Env;
20
+ db: Db;
21
+ user: User;
22
+ }
23
+
24
+ export interface BroContext<Body = any, Params = any, Query = any, App extends AppContext = AppContext> {
25
+ env?: App['env'];
19
26
  jwt?: { sign: (payload: any, options?: any) => string };
20
27
  body: InferZod<Body>;
21
28
  params: InferZod<Params>;
22
29
  query: InferZod<Query>;
23
- user?: any;
24
- db?: any;
30
+ user?: App['user'];
31
+ db?: App['db'];
25
32
  io?: any;
26
33
  file?: UploadedFile;
27
34
  files?: UploadedFile[] | Record<string, UploadedFile[]>;
@@ -29,10 +36,12 @@ export interface BroContext<Body = any, Params = any, Query = any> {
29
36
  t: (key: string, values?: Record<string, unknown>) => string;
30
37
  error?: any;
31
38
  redis?: any;
39
+ requestId?: string;
32
40
  }
33
41
 
34
- export interface RouteConfig<Body = any, Params = any, Query = any> {
42
+ export interface RouteConfig<Body = any, Params = any, Query = any, Response = any, App extends AppContext = AppContext> {
35
43
  auth?: boolean | string[] | 'api-key';
44
+ operationId?: string;
36
45
  upload?: boolean | { limits?: any, fields?: { name: string, maxCount?: number }[], single?: string, array?: string, fileFilter?: any, storage?: any };
37
46
  body?: Body;
38
47
  params?: Params;
@@ -44,12 +53,12 @@ export interface RouteConfig<Body = any, Params = any, Query = any> {
44
53
  max: number;
45
54
  };
46
55
  summary?: string;
47
- handler: (ctx: BroContext<Body, Params, Query>) => Promise<any> | any;
56
+ handler: (ctx: BroContext<Body, Params, Query, App>) => Promise<Response extends import('zod').ZodTypeAny ? import('zod').infer<Response> : any> | (Response extends import('zod').ZodTypeAny ? import('zod').infer<Response> : any);
48
57
  }
49
58
 
50
- export function defineRoute<Body = any, Params = any, Query = any>(
51
- config: RouteConfig<Body, Params, Query>
52
- ): RouteConfig<Body, Params, Query>;
59
+ export function defineRoute<Body = any, Params = any, Query = any, Response = any, App extends AppContext = AppContext>(
60
+ config: RouteConfig<Body, Params, Query, Response, App>
61
+ ): RouteConfig<Body, Params, Query, Response, App>;
53
62
 
54
63
  export function loadLocale(directory: string, options?: { defaultLocale?: string }): Promise<{
55
64
  locales: string[];
@@ -58,12 +67,40 @@ export function loadLocale(directory: string, options?: { defaultLocale?: string
58
67
  translate: (locale: string, key: string, values?: Record<string, unknown>) => string;
59
68
  }>;
60
69
 
70
+
71
+ export interface BroPlugin {
72
+ name: string;
73
+ version: string;
74
+ order?: number;
75
+ onInit?: (globalConfig: BroConfig, app: any) => void | Promise<void>;
76
+ onContext?: (ctx: BroContext) => any | Promise<any>;
77
+ onRequest?: (req: any, res: any) => void | Promise<void>;
78
+ onError?: (err: any, req: any, res: any) => void | Promise<void>;
79
+ onShutdown?: () => void | Promise<void>;
80
+ }
61
81
  export interface BroConfig {
82
+ routesDir?: string;
83
+ tasksDir?: string;
84
+ logger?: { level?: string; [key: string]: any };
85
+ plugins?: BroPlugin[];
86
+ fixtures?: Record<string, any>;
87
+ stores?: Record<string, any>;
88
+ health?: boolean | { dbCheck?: (db: any) => Promise<any> };
89
+ locales?: Record<string, any>;
90
+ defaultLocale?: string;
91
+ envData?: any;
92
+ redis?: any;
93
+ port?: number;
94
+ jwtSecret?: string;
95
+ trustProxy?: boolean;
96
+ validateResponse?: boolean | 'strict' | 'warn';
62
97
  env?: ZodTypeAny;
63
98
  server?: {
64
99
  port?: number;
65
100
  cors?: boolean | object;
66
101
  helmet?: boolean | object;
102
+ timeoutMs?: number;
103
+ headersTimeoutMs?: number;
67
104
  };
68
105
  locale?: {
69
106
  directory?: string;
package/src/index.js CHANGED
@@ -27,3 +27,13 @@ export function defineConfig(config) {
27
27
 
28
28
  export { z };
29
29
  export { loadLocale } from './locale.js';
30
+ export { createServer } from './server.js';
31
+ export { createTestHarness as test } from './testing.js';
32
+ export { PluginManager, ObservabilityPlugin, SecurityPlugin } from './plugins.js';
33
+ export { createPinoAdapter, setupOpenTelemetry, createDashboardEventBus } from './observability.js';
34
+ export { BaseDatabaseAdapter, PostgresAdapter } from './database.js';
35
+ export { SecureUploadPipeline, LocalStorageAdapter, S3StorageAdapter } from './uploads.js';
36
+ export { TaskManager } from './task-engine.js';
37
+ export { OidcProvider, PolicyEvaluator, ApiKeyManager, tenantContextPlugin } from './policy-auth.js';
38
+
39
+ export { RouteRegistry } from './engine.js';
package/src/logger.js CHANGED
@@ -77,3 +77,50 @@ export function printHotReload(fileName, event, reloadTimeMs, resourceType = 'Ro
77
77
  console.log(`\n ${colors.cyan}${resourceType} updated:${colors.reset} ${colors.bold}${fileName}${colors.reset} ${colors.dim}(${event})${colors.reset}`);
78
78
  console.log(` ${colors.dim}Remapped in ${time}ms${colors.reset}\n`);
79
79
  }
80
+
81
+ export function createLogger(config = {}) {
82
+ const isJson = config.format === 'json';
83
+ const levelPriority = { debug: 0, info: 1, warn: 2, error: 3, silent: 4 };
84
+ const currentLevel = levelPriority[config.level] ?? levelPriority.info;
85
+
86
+ const redactKeys = config.redact || ['password', 'token', 'secret', 'authorization'];
87
+
88
+ const redact = (obj) => {
89
+ if (typeof obj !== 'object' || obj === null) return obj;
90
+ if (Array.isArray(obj)) return obj.map(redact);
91
+ const newObj = { ...obj };
92
+ for (const key of Object.keys(newObj)) {
93
+ if (redactKeys.some(r => key.toLowerCase().includes(r))) {
94
+ newObj[key] = '[REDACTED]';
95
+ } else if (typeof newObj[key] === 'object') {
96
+ newObj[key] = redact(newObj[key]);
97
+ }
98
+ }
99
+ return newObj;
100
+ };
101
+
102
+ const log = (level, message, meta = {}) => {
103
+ if (levelPriority[level] < currentLevel) return;
104
+ const timestamp = new Date().toISOString();
105
+
106
+ if (isJson) {
107
+ console[level === 'debug' ? 'log' : level](JSON.stringify({ level, timestamp, message, ...redact(meta) }));
108
+ } else {
109
+ const colorMap = { debug: colors.dim, info: colors.cyan, warn: colors.yellow, error: colors.red };
110
+ const c = colorMap[level] || colors.reset;
111
+ let metaStr = Object.keys(meta).length ? ` ${colors.dim}${JSON.stringify(redact(meta))}${colors.reset}` : '';
112
+ console[level === 'debug' ? 'log' : level](`${colors.dim}[${timestamp}]${colors.reset} ${c}[${level.toUpperCase()}]${colors.reset} ${message}${metaStr}`);
113
+ }
114
+ };
115
+
116
+ return {
117
+ debug: (msg, meta) => log('debug', msg, meta),
118
+ info: (msg, meta) => log('info', msg, meta),
119
+ warn: (msg, meta) => log('warn', msg, meta),
120
+ error: (msg, meta) => log('error', msg, meta),
121
+ time: (label) => {
122
+ const start = performance.now();
123
+ return (msg, meta) => log('info', msg || `${label} completed`, { ...meta, durationMs: performance.now() - start });
124
+ }
125
+ };
126
+ }
package/src/next.d.ts CHANGED
@@ -11,10 +11,34 @@ export interface UploadedFile {
11
11
  text: () => Promise<string>;
12
12
  }
13
13
 
14
- export interface NextBroGlobalConfig<TDb = any> {
15
- env?: ZodTypeAny;
14
+ export interface NextBroGlobalConfig<TEnv = any, TDb = any, TUser = any> {
15
+ routesDir?: string;
16
+ tasksDir?: string;
17
+ logger?: { level?: string; [key: string]: any };
18
+ plugins?: any[];
19
+ fixtures?: Record<string, any>;
20
+ stores?: Record<string, any>;
21
+ health?: boolean | { dbCheck?: (db: any) => Promise<any> };
16
22
  locales?: Record<string, any>;
17
23
  defaultLocale?: string;
24
+ envData?: any;
25
+ redis?: any;
26
+ port?: number;
27
+ jwtSecret?: string;
28
+ trustProxy?: boolean;
29
+ validateResponse?: boolean | 'strict' | 'warn';
30
+ env?: ZodTypeAny;
31
+ server?: {
32
+ port?: number;
33
+ cors?: boolean | object;
34
+ helmet?: boolean | object;
35
+ timeoutMs?: number;
36
+ headersTimeoutMs?: number;
37
+ };
38
+ locale?: {
39
+ directory?: string;
40
+ defaultLocale?: string;
41
+ };
18
42
  redisUrl?: string;
19
43
  rateLimit?: { windowMs: number; max: number; };
20
44
  auth?: {
@@ -22,12 +46,23 @@ export interface NextBroGlobalConfig<TDb = any> {
22
46
  apiKey?: string | string[];
23
47
  expiresIn?: string | number;
24
48
  };
49
+ docs?: boolean | { auth?: { user: string; pass: string } };
50
+ upload?: {
51
+ limits?: {
52
+ fileSize?: number;
53
+ files?: number;
54
+ fields?: number;
55
+ [key: string]: any;
56
+ };
57
+ };
25
58
  db?: TDb | Promise<TDb> | (() => TDb | Promise<TDb>) | { init: () => TDb | Promise<TDb> };
59
+ sockets?: (io: any, db: any) => Promise<void> | void;
60
+ onShutdown?: (db: any) => Promise<void> | void;
26
61
  }
27
62
 
28
- export interface NextRouteContext<TBody = any, TQuery = any, TParams = any, TDb = any> {
63
+ export interface NextRouteContext<TBody = any, TQuery = any, TParams = any, TEnv = any, TDb = any, TUser = any> {
29
64
  req: Request;
30
- env: Record<string, string | undefined>;
65
+ env: TEnv;
31
66
  db: TDb;
32
67
  redis: any;
33
68
  io: { emit: (event: string, data: any) => void };
@@ -37,13 +72,18 @@ export interface NextRouteContext<TBody = any, TQuery = any, TParams = any, TDb
37
72
  file?: UploadedFile;
38
73
  files?: Record<string, UploadedFile[]>;
39
74
  locale: string;
75
+ method: string;
76
+ ip: string;
77
+ headers: Record<string, string>;
78
+ requestId: string;
79
+ logger: any;
40
80
  t: (key: string, values?: any) => string;
41
- user?: any;
81
+ user?: TUser;
42
82
  jwt: { sign: (payload: any, opts?: any) => string };
43
83
  error: (status: number, message: string) => never;
44
84
  }
45
85
 
46
- export interface NextRouteConfig<TBody = any, TQuery = any, TParams = any, TDb = any> {
86
+ export interface NextRouteConfig<TBody = any, TQuery = any, TParams = any, TEnv = any, TDb = any, TUser = any> {
47
87
  auth?: boolean | string[] | 'api-key' | string;
48
88
  body?: TBody;
49
89
  query?: TQuery;
@@ -52,19 +92,20 @@ export interface NextRouteConfig<TBody = any, TQuery = any, TParams = any, TDb =
52
92
  rateLimit?: { windowMs: number; max: number; } | false;
53
93
  response?: ZodTypeAny;
54
94
  summary?: string;
95
+ operationId?: string;
55
96
  upload?: any;
56
- handler: (ctx: NextRouteContext<TBody, TQuery, TParams, TDb>) => Promise<any> | any;
97
+ handler: (ctx: NextRouteContext<TBody, TQuery, TParams, TEnv, TDb, TUser>) => Promise<any> | any;
57
98
  }
58
99
 
59
- export interface BroNextInstance<TDb = any> {
100
+ export interface BroNextInstance<TEnv = any, TDb = any, TUser = any> {
60
101
  z: typeof z;
61
102
  defineRoute: <
62
103
  TBody extends ZodTypeAny = any,
63
104
  TQuery extends ZodTypeAny = any,
64
105
  TParams extends ZodTypeAny = any
65
106
  >(
66
- config: NextRouteConfig<TBody, TQuery, TParams, TDb>
107
+ config: NextRouteConfig<TBody, TQuery, TParams, TEnv, TDb, TUser>
67
108
  ) => (req: Request | any, context: any) => Promise<any>;
68
109
  }
69
110
 
70
- export declare function createBro<TDb = any>(config?: NextBroGlobalConfig<TDb>): BroNextInstance<TDb>;
111
+ export declare function createBro<TEnv = any, TDb = any, TUser = any>(config?: NextBroGlobalConfig<TEnv, TDb, TUser>): BroNextInstance<TEnv, TDb, TUser>;