bro-framework 2.4.2 → 2.4.3

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 CHANGED
@@ -35,6 +35,7 @@
35
35
  - [Architecture & Request Lifecycle](#architecture--request-lifecycle)
36
36
  - [Tech Stack Breakdown](#tech-stack-breakdown)
37
37
  - [CLI Reference](#cli-reference)
38
+ - [Next.js App Router Integration](#nextjs-app-router-integration)
38
39
  - [Author & License](#author--license)
39
40
 
40
41
  ---
@@ -309,9 +310,36 @@ For Redis-backed integration tests without an external Redis server, install `io
309
310
  | `bro init` | Automated workspace scaffolder. Generates configuration files and forcefully ensures your `package.json` respects `"type": "module"`. |
310
311
  | `bro sdk` | Route parser and browser client compiler. Generates your typed `bro-sdk.js` frontend SDK in one hit. |
311
312
 
313
+ ## Next.js App Router Integration
314
+
315
+ You can natively use `bro.js` syntax, Zod validation, and Authentication inside your Next.js API routes (`app/api/.../route.ts`)!
316
+
317
+ First, create your factory instance (e.g., `lib/bro.ts`):
318
+ ```typescript
319
+ import { createBro } from 'bro-framework/next';
320
+
321
+ export const { defineRoute, z } = createBro({
322
+ auth: { apiKey: process.env.API_KEY }
323
+ });
324
+ ```
325
+
326
+ Then use it seamlessly in your route files:
327
+ ```typescript
328
+ import { defineRoute, z } from '@/lib/bro';
329
+
330
+ export const POST = defineRoute({
331
+ body: z.object({ name: z.string() }),
332
+ handler: async ({ body }) => {
333
+ return { success: true, hello: body.name };
334
+ }
335
+ });
336
+ ```
337
+
338
+ > **Capabilities & Limitations**: Because Next.js API routes are "Serverless" (meaning they sleep when not actively processing a request), features that require a constantly running server such as **WebSockets**, **Background Tasks (Cron)**, **Rate Limiting**, and **Auto-generated Docs** are strictly limited to the standalone `bro.js` framework and are not available in the Next.js adapter.
339
+
312
340
  ---
313
341
 
314
342
  ## Author & License
315
343
 
316
- - **Author**: Yessin (@medyass1ne)
344
+ - **Author**: Yass1n (@medyass1ne)
317
345
  - **License**: MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bro-framework",
3
- "version": "2.4.2",
3
+ "version": "2.4.3",
4
4
  "description": "The No-BS Backend Framework for Node.js",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,7 +15,15 @@
15
15
  "exports": {
16
16
  ".": {
17
17
  "types": "./src/index.d.ts",
18
+ "import": "./src/index.js",
19
+ "require": "./src/index.js",
18
20
  "default": "./src/index.js"
21
+ },
22
+ "./next": {
23
+ "types": "./src/next.d.ts",
24
+ "import": "./src/next.js",
25
+ "require": "./src/next.js",
26
+ "default": "./src/next.js"
19
27
  }
20
28
  },
21
29
  "bin": {
package/src/next.d.ts ADDED
@@ -0,0 +1,61 @@
1
+ import { z, ZodTypeAny } from 'zod';
2
+
3
+ export { z };
4
+
5
+ export interface UploadedFile {
6
+ name: string;
7
+ type: string;
8
+ size: number;
9
+ arrayBuffer: () => Promise<ArrayBuffer>;
10
+ stream: () => ReadableStream;
11
+ text: () => Promise<string>;
12
+ }
13
+
14
+ export interface NextBroGlobalConfig<TDb = any> {
15
+ locale?: any;
16
+ redisUrl?: string;
17
+ auth?: {
18
+ jwtSecret?: string;
19
+ apiKey?: string | string[];
20
+ };
21
+ db?: TDb | Promise<TDb> | (() => TDb | Promise<TDb>) | { init: () => TDb | Promise<TDb> };
22
+ }
23
+
24
+ export interface NextRouteContext<TBody = any, TQuery = any, TParams = any, TDb = any> {
25
+ req: Request;
26
+ env: Record<string, string | undefined>;
27
+ db: TDb;
28
+ redis: any;
29
+ io: { emit: (event: string, data: any) => void };
30
+ body: TBody extends ZodTypeAny ? z.infer<TBody> : any;
31
+ query: TQuery extends ZodTypeAny ? z.infer<TQuery> : any;
32
+ params: TParams extends ZodTypeAny ? z.infer<TParams> : any;
33
+ file?: UploadedFile;
34
+ files?: Record<string, UploadedFile[]>;
35
+ locale: string;
36
+ t: (key: string, values?: any) => string;
37
+ user?: any;
38
+ jwt: { sign: (payload: any, opts?: any) => string };
39
+ error: (status: number, message: string) => never;
40
+ }
41
+
42
+ export interface NextRouteConfig<TBody = any, TQuery = any, TParams = any, TDb = any> {
43
+ auth?: boolean | string[] | 'api-key' | string;
44
+ body?: TBody;
45
+ query?: TQuery;
46
+ params?: TParams;
47
+ handler: (ctx: NextRouteContext<TBody, TQuery, TParams, TDb>) => Promise<any> | any;
48
+ }
49
+
50
+ export interface BroNextInstance<TDb = any> {
51
+ z: typeof z;
52
+ defineRoute: <
53
+ TBody extends ZodTypeAny = any,
54
+ TQuery extends ZodTypeAny = any,
55
+ TParams extends ZodTypeAny = any
56
+ >(
57
+ config: NextRouteConfig<TBody, TQuery, TParams, TDb>
58
+ ) => (req: Request | any, context: any) => Promise<any>;
59
+ }
60
+
61
+ export declare function createBro<TDb = any>(config?: NextBroGlobalConfig<TDb>): BroNextInstance<TDb>;
package/src/next.js ADDED
@@ -0,0 +1,211 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { z } from 'zod';
3
+ import { verifyJwt, signJwt } from './auth.js';
4
+ import { createClient } from 'redis';
5
+
6
+ export function createBro(globalConfig = {}) {
7
+ let isInitialized = false;
8
+ let initPromise = null;
9
+
10
+ let globalDb = null;
11
+ let globalRedis = null;
12
+ let globalLocale = null;
13
+
14
+ async function ensureInitialized() {
15
+ if (isInitialized) return;
16
+ if (initPromise) return initPromise;
17
+
18
+ initPromise = (async () => {
19
+ try {
20
+ // Locale Dummy or Real
21
+ if (globalConfig.locale && typeof globalConfig.locale.resolveLocale === 'function') {
22
+ globalLocale = globalConfig.locale;
23
+ } else {
24
+ globalLocale = {
25
+ resolveLocale: () => 'en',
26
+ translate: (locale, key) => key
27
+ };
28
+ }
29
+
30
+ // DB
31
+ if (typeof globalConfig.db === 'function') {
32
+ globalDb = await globalConfig.db();
33
+ } else if (globalConfig.db && typeof globalConfig.db.init === 'function') {
34
+ globalDb = await globalConfig.db.init();
35
+ } else if (globalConfig.db) {
36
+ globalDb = globalConfig.db;
37
+ if (globalDb instanceof Promise) globalDb = await globalDb;
38
+ }
39
+
40
+ // Redis
41
+ if (globalConfig.redisUrl) {
42
+ globalRedis = createClient({ url: globalConfig.redisUrl });
43
+ globalRedis.on('error', (err) => console.error('[bro.js/next] Redis Error:', err));
44
+ if (globalRedis.status === 'wait' || !globalRedis.status) {
45
+ await globalRedis.connect().catch(err => {
46
+ if (!err.message.includes('already connecting') && !err.message.includes('already connected')) {
47
+ throw err;
48
+ }
49
+ });
50
+ }
51
+ }
52
+
53
+ isInitialized = true;
54
+ } catch (err) {
55
+ console.error('[bro.js/next] Initialization Error:', err);
56
+ throw err;
57
+ }
58
+ })();
59
+
60
+ return initPromise;
61
+ }
62
+
63
+ const errorHelper = (status, message) => {
64
+ const err = new Error(message);
65
+ err.status = status;
66
+ throw err;
67
+ };
68
+
69
+ function defineRoute(config) {
70
+ return async function (req, context) {
71
+ try {
72
+ await ensureInitialized();
73
+
74
+ const rawParams = context?.params ? await context.params : {};
75
+
76
+ const url = new URL(req.url);
77
+ const rawQuery = Object.fromEntries(url.searchParams.entries());
78
+
79
+ let rawBody = {};
80
+ const parsedFiles = {};
81
+ let totalFiles = 0;
82
+
83
+ if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
84
+ const contentType = req.headers.get('content-type') || '';
85
+
86
+ if (contentType.includes('multipart/form-data')) {
87
+ try {
88
+ const formData = await req.formData();
89
+ for (const [key, value] of formData.entries()) {
90
+ if (value instanceof File || value instanceof Blob) {
91
+ if (!parsedFiles[key]) parsedFiles[key] = [];
92
+ parsedFiles[key].push(value);
93
+ totalFiles++;
94
+ } else {
95
+ rawBody[key] = value;
96
+ }
97
+ }
98
+ } catch (err) {}
99
+ } else {
100
+ try {
101
+ rawBody = await req.json();
102
+ } catch (err) {}
103
+ }
104
+ }
105
+
106
+ let body, params, query;
107
+ try {
108
+ if (config.body) body = await config.body.parseAsync(rawBody);
109
+ if (config.params) params = await config.params.parseAsync(rawParams);
110
+ if (config.query) query = await config.query.parseAsync(rawQuery);
111
+ } catch (err) {
112
+ if (err instanceof z.ZodError) {
113
+ return NextResponse.json(
114
+ { error: 'Validation Error', issues: err.issues },
115
+ { status: 400 }
116
+ );
117
+ }
118
+ throw err;
119
+ }
120
+
121
+ let user = null;
122
+ if (config.auth) {
123
+ if (config.auth === 'api-key') {
124
+ const apiKey = req.headers.get('x-api-key') || req.headers.get('authorization')?.replace('Bearer ', '');
125
+ const configuredKey = globalConfig?.auth?.apiKey || process.env.API_KEY;
126
+
127
+ let isValid = false;
128
+ if (configuredKey) {
129
+ const keys = (Array.isArray(configuredKey) ? configuredKey : configuredKey.split(',')).map(k => String(k).trim());
130
+ isValid = keys.includes(apiKey);
131
+ }
132
+
133
+ if (!isValid) {
134
+ return NextResponse.json({ error: 'Unauthorized', details: 'Missing or invalid API key' }, { status: 401 });
135
+ }
136
+ } else {
137
+ const authHeader = req.headers.get('authorization');
138
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
139
+ return NextResponse.json({ error: 'Unauthorized', details: 'Missing Bearer token' }, { status: 401 });
140
+ }
141
+
142
+ const token = authHeader.split(' ')[1];
143
+ const secret = globalConfig?.auth?.jwtSecret || process.env.JWT_SECRET;
144
+
145
+ if (!secret) {
146
+ return NextResponse.json({ error: 'Internal Server Error', details: 'JWT_SECRET is not configured' }, { status: 500 });
147
+ }
148
+
149
+ const decoded = verifyJwt(token, secret);
150
+ if (!decoded.valid) {
151
+ return NextResponse.json({ error: 'Unauthorized', details: decoded.error }, { status: 401 });
152
+ }
153
+
154
+ user = decoded.payload;
155
+
156
+ if (Array.isArray(config.auth) && config.auth.length > 0) {
157
+ if (!user.role || !config.auth.includes(user.role)) {
158
+ return NextResponse.json({ error: 'Forbidden', details: 'Insufficient permissions' }, { status: 403 });
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ const resolvedLocale = globalLocale.resolveLocale({ headers: Object.fromEntries(req.headers.entries()) });
165
+
166
+ let ctxFile = undefined;
167
+ let ctxFiles = undefined;
168
+ if (totalFiles === 1) {
169
+ const keys = Object.keys(parsedFiles);
170
+ ctxFile = parsedFiles[keys[0]][0];
171
+ ctxFiles = parsedFiles;
172
+ } else if (totalFiles > 1) {
173
+ ctxFiles = parsedFiles;
174
+ }
175
+
176
+ const ctx = {
177
+ req,
178
+ env: process.env,
179
+ db: globalDb,
180
+ redis: globalRedis,
181
+ io: { emit: () => console.warn('[bro.js/next] WebSockets require standard bro.js server.') },
182
+ body,
183
+ params,
184
+ query,
185
+ file: ctxFile,
186
+ files: ctxFiles,
187
+ locale: resolvedLocale,
188
+ t: (key, values) => globalLocale.translate(resolvedLocale, key, values),
189
+ user,
190
+ jwt: { sign: (payload, opts) => signJwt(payload, globalConfig?.auth?.jwtSecret || process.env.JWT_SECRET, opts) },
191
+ error: errorHelper
192
+ };
193
+
194
+ const result = await config.handler(ctx);
195
+ return NextResponse.json(result, { status: 200 });
196
+
197
+ } catch (error) {
198
+ if (error.status) {
199
+ return NextResponse.json({ error: error.message }, { status: error.status });
200
+ }
201
+ console.error('[bro.js/next] Unhandled Error:', error);
202
+ return NextResponse.json(
203
+ { error: 'Internal Server Error', message: error.message },
204
+ { status: 500 }
205
+ );
206
+ }
207
+ };
208
+ }
209
+
210
+ return { defineRoute, z };
211
+ }