nicola-framework 1.0.0 → 1.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 ADDED
@@ -0,0 +1,274 @@
1
+ # ⚡ Nicola Framework
2
+
3
+ [![npm version](https://img.shields.io/npm/v/nicola-framework.svg)](https://www.npmjs.com/package/nicola-framework)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Node.js Version](https://img.shields.io/badge/node-%3E%3D16.0.0-brightgreen.svg)](https://nodejs.org)
6
+
7
+ > Framework HTTP minimalista para Node.js (ESM): servidor, router, middlewares, JWT y una capa ORM sencilla.
8
+
9
+ Nicola expone un **servidor HTTP nativo** con un **router tipo Express** y utilidades integradas. El proyecto está escrito como **ES Modules** (`"type": "module"`), por lo que los ejemplos usan `import`.
10
+
11
+ ---
12
+
13
+ ## ✅ Estado actual del proyecto (lo que realmente hay)
14
+
15
+ - **Core/Router**: `Nicola` (default) extiende `Remote`.
16
+ - **Body parsing**: JSON si `Content-Type` incluye `application/json` (límite ~2MB). Si no, `req.body = {}`.
17
+ - **Helpers de response**: `res.json(data)` y `res.send(text)`. (No existe `res.status()`.)
18
+ - **CORS**: `EasyCors` permite `*` y responde `OPTIONS` con `204`.
19
+ - **Security headers**: `Teleforce` aplica headers básicos (no-sniff, frame deny, etc.).
20
+ - **Logger**: `Shadowgraph` loggea al terminar la respuesta.
21
+ - **Errores**: si un handler lanza error o llama `next(err)`, se responde HTML via `BlackBox`.
22
+ - **JWT**: `Coherer` (HS256) funciona vía métodos **estáticos** y requiere `NICOLA_SECRET` en env.
23
+ - **ORM**: `Dynamo` soporta **Postgres** hoy (driver `postgres`). La lib `pg` es **dependencia opcional** (se instala aparte).
24
+ - **Hot reload**: `LiveCurrent` reinicia el proceso Node al detectar cambios en el directorio.
25
+
26
+ ---
27
+
28
+ ## 📦 Instalación
29
+
30
+ ```bash
31
+ npm install nicola-framework
32
+ ```
33
+
34
+ ### (Opcional) Postgres
35
+
36
+ El dialecto Postgres usa `pg` por import dinámico.
37
+
38
+ ```bash
39
+ npm install pg
40
+ ```
41
+
42
+ ---
43
+
44
+ ## ⚡ Quickstart
45
+
46
+ ### Servidor HTTP básico
47
+
48
+ ```js
49
+ import Nicola from 'nicola-framework';
50
+
51
+ const app = new Nicola();
52
+
53
+ app.get('/', (req, res) => {
54
+ res.json({ ok: true, message: 'Hello from Nicola!' });
55
+ });
56
+
57
+ app.listen(3000, () => {
58
+ console.log('Server running on http://localhost:3000');
59
+ });
60
+ ```
61
+
62
+ ### Router anidado y params
63
+
64
+ ```js
65
+ import { Nicola, Remote } from 'nicola-framework';
66
+
67
+ const app = new Nicola();
68
+ const api = new Remote();
69
+
70
+ api.get('/users', (req, res) => {
71
+ res.json({ users: ['Alice', 'Bob'] });
72
+ });
73
+
74
+ api.get('/users/:id', (req, res) => {
75
+ res.json({ userId: req.params.id });
76
+ });
77
+
78
+ app.use('/api', api);
79
+ app.listen(3000);
80
+ ```
81
+
82
+ ---
83
+
84
+ ## 🧠 API (resumen fiel al código)
85
+
86
+ ### `Nicola` (Core)
87
+
88
+ - `new Nicola()`
89
+ - `app.get/post/put/patch/delete(path, ...handlers)`
90
+ - `app.use([path], ...handlers | router)`
91
+ - `app.listen(port, [callback])`
92
+
93
+ Notas:
94
+ - `Nicola.listen()` ejecuta internamente `Shadowgraph`, `EasyCors` y `Teleforce` en cada request.
95
+ - `req.query` se construye desde querystring.
96
+
97
+ ### `Remote` (Router)
98
+
99
+ `Remote` es el router base. Soporta middlewares y rutas con params (`/users/:id`).
100
+
101
+ Ejemplo de middleware simple:
102
+
103
+ ```js
104
+ app.use((req, res, next) => {
105
+ // No existe res.status(); usa statusCode
106
+ if (req.url === '/blocked') {
107
+ res.statusCode = 403;
108
+ res.end('Forbidden');
109
+ return;
110
+ }
111
+ next();
112
+ });
113
+ ```
114
+
115
+ Errores:
116
+
117
+ ```js
118
+ app.get('/boom', (req, res) => {
119
+ throw new Error('Boom');
120
+ });
121
+ ```
122
+
123
+ ---
124
+
125
+ ## 🔐 Seguridad
126
+
127
+ ### `Regulator` (.env)
128
+
129
+ Lee `.env` desde el directorio actual (`process.cwd()`) y lo copia a `process.env`.
130
+
131
+ ```js
132
+ import { Regulator } from 'nicola-framework';
133
+
134
+ Regulator.load();
135
+ ```
136
+
137
+ ### `Coherer` (JWT HS256)
138
+
139
+ `Coherer` es una clase con métodos **estáticos**.
140
+
141
+ ```js
142
+ import { Regulator, Coherer } from 'nicola-framework';
143
+
144
+ Regulator.load();
145
+
146
+ const token = Coherer.sign(
147
+ { userId: 123, role: 'admin' },
148
+ { expiresIn: '24h' }
149
+ );
150
+
151
+ const payload = Coherer.verify(token);
152
+ console.log(payload.userId);
153
+ ```
154
+
155
+ `.env` mínimo:
156
+
157
+ ```env
158
+ NICOLA_SECRET=mi-secreto-super-seguro
159
+ ```
160
+
161
+ ---
162
+
163
+ ## 🗃️ Dynamo (ORM)
164
+
165
+ ### Configuración
166
+
167
+ `Dynamo.connect()` **no recibe config**: toma la configuración desde variables de entorno.
168
+
169
+ ```js
170
+ import { Regulator, Dynamo } from 'nicola-framework';
171
+
172
+ Regulator.load();
173
+ await Dynamo.connect();
174
+ ```
175
+
176
+ `.env` para Postgres:
177
+
178
+ ```env
179
+ DB_DRIVER=postgres
180
+ DB_HOST=localhost
181
+ DB_PORT=5432
182
+ DB_USER=postgres
183
+ DB_PASS=postgres
184
+ DB_NAME=mydb
185
+ ```
186
+
187
+ ### Modelos
188
+
189
+ ```js
190
+ import { Dynamo } from 'nicola-framework';
191
+
192
+ export default class User extends Dynamo.Model {
193
+ static tableName = 'users';
194
+
195
+ static schema = {
196
+ name: { type: 'string', required: true },
197
+ email: { type: 'string', required: true },
198
+ age: { type: 'number', required: false }
199
+ };
200
+ }
201
+ ```
202
+
203
+ ### Queries
204
+
205
+ ```js
206
+ // All
207
+ const users = await User.all();
208
+
209
+ // Where
210
+ const active = await User.where('active', true).get();
211
+
212
+ // Insert (valida contra schema)
213
+ const created = await User.create({ name: 'Alice', email: 'a@a.com', age: 20 });
214
+
215
+ // Update / Delete
216
+ await User.where('id', 1).update({ name: 'Alice 2' });
217
+ await User.where('id', 1).delete();
218
+
219
+ // Select específico (usa string con coma)
220
+ const names = await User.select('name,email').get();
221
+
222
+ // Order/limit/offset vía QueryBuilder
223
+ const latest = await User.query().orderBy('id', 'DESC').limit(10).offset(0).get();
224
+ ```
225
+
226
+ ---
227
+
228
+ ## 🧩 Middlewares
229
+
230
+ ### `Insulator(schema)`
231
+
232
+ Valida `req.body` con un esquema **simple** de tipos (`typeof`).
233
+
234
+ ```js
235
+ import { Insulator } from 'nicola-framework';
236
+
237
+ const schema = {
238
+ name: 'string',
239
+ age: 'number'
240
+ };
241
+
242
+ app.post('/users', Insulator(schema), (req, res) => {
243
+ res.json({ ok: true });
244
+ });
245
+ ```
246
+
247
+ ### `Shadowgraph`, `Teleforce`, `EasyCors`
248
+
249
+ Se ejecutan automáticamente dentro de `Nicola.listen()`. También puedes llamarlos manualmente si estás usando `Remote` por separado.
250
+
251
+ ---
252
+
253
+ ## 🔥 LiveCurrent (hot reload)
254
+
255
+ ```js
256
+ import { LiveCurrent } from 'nicola-framework';
257
+
258
+ const dev = new LiveCurrent('app.js');
259
+ dev.boot();
260
+ ```
261
+
262
+ ---
263
+
264
+ ## 🤝 Contribuir
265
+
266
+ 1. Fork
267
+ 2. Rama feature
268
+ 3. PR
269
+
270
+ ---
271
+
272
+ ## 📝 Licencia
273
+
274
+ MIT © Erick Mauricio Tiznado Rodriguez
package/core/Core.js CHANGED
@@ -1,84 +1,89 @@
1
- import http from 'http'
2
- import Remote from './Remote.js';
3
- import BlackBox from '../middlewares/BlackBox.js'
4
- import Shadowgraph from '../middlewares/Shadowgraph.js';
5
- import Teleforce from '../middlewares/Teleforce.js';
6
- import EasyCors from '../middlewares/EasyCors.js';
1
+ import http from "http";
2
+ import Remote from "./Remote.js";
3
+ import BlackBox from "../middlewares/BlackBox.js";
4
+ import Shadowgraph from "../middlewares/Shadowgraph.js";
5
+ import Teleforce from "../middlewares/Teleforce.js";
6
+ import EasyCors from "../middlewares/EasyCors.js";
7
7
  class Core extends Remote {
8
-
9
- constructor() {
10
- super();
11
- }
12
-
13
-
14
-
15
-
16
- listen(port, callback) {
17
-
18
- const server = http.createServer((req, res) => {
19
- const myURL = new URL(req.url, 'http://' + req.headers.host);
20
- const pathURL = myURL.pathname;
21
- const urlParams = Object.fromEntries(myURL.searchParams);
22
-
23
- req.url = pathURL;
24
- req.query = urlParams;
25
-
26
- Shadowgraph(req, res, () => {
27
- this.__addHelper(res);
28
- EasyCors(req, res, () =>{
29
-
30
- Teleforce(req, res, () => {
31
- const done = (err) => {
32
- if (!err) {
33
- res.statusCode = 404;
34
- res.end('Not Found')
35
- }
36
- else {
37
- BlackBox.ignite(err, req, res);
38
- }
39
- }
40
-
41
- let dataString = ''
42
- req.on('data', chunk => {
43
- dataString += chunk;
44
-
45
- })
46
-
47
- req.on('end', () => {
48
- try {
49
- if (dataString) {
50
- req.body = JSON.parse(dataString);
51
- }
52
- else {
53
- req.body = {}
54
- }
55
- }
56
- catch (error) {
57
- req.body = {}
58
- }
59
- this.handle(req, res, done);
60
-
61
- });
62
- })
63
- })
64
- })
65
- })
66
- server.listen(port, callback);
67
- }
68
-
69
- __addHelper(res) {
70
- res.json = (data) => {
71
- res.setHeader('Content-Type', 'application/json');
72
- res.end(JSON.stringify(data));
73
- }
74
-
75
- res.send = (data) => {
76
- res.setHeader('Content-Type', 'text/plain');
77
- res.end(data);
78
- }
79
- }
80
-
81
-
8
+ constructor() {
9
+ super();
10
+ }
11
+
12
+ listen(port, callback) {
13
+ const server = http.createServer((req, res) => {
14
+ const myURL = new URL(req.url, "http://" + req.headers.host);
15
+ const pathURL = myURL.pathname;
16
+ const urlParams = Object.fromEntries(myURL.searchParams);
17
+
18
+ req.url = pathURL;
19
+ req.query = urlParams;
20
+
21
+ Shadowgraph(req, res, () => {
22
+ this.__addHelper(res);
23
+ EasyCors()(req, res, () => {
24
+ Teleforce(req, res, () => {
25
+ const done = (err) => {
26
+ if (!err) {
27
+ res.statusCode = 404;
28
+ res.end("Not Found");
29
+ } else {
30
+ BlackBox.ignite(err, req, res);
31
+ }
32
+ };
33
+ if (req.headers["content-type"]?.includes("application/json")) {
34
+ let dataString = [];
35
+ let chunklenght = 0;
36
+ req.on("data", (chunk) => {
37
+ dataString.push(chunk);
38
+ chunklenght = chunklenght + chunk.length;
39
+
40
+ if (chunklenght > 2e6) {
41
+ req.pause();
42
+ res.statusCode = 413;
43
+ res.end("Request Entity Too Large");
44
+ return;
45
+ }
46
+ });
47
+
48
+ req.on("end", () => {
49
+ try {
50
+ if (dataString.length > 0) {
51
+ const buffer = Buffer.concat(dataString).toString();
52
+ req.body = JSON.parse(buffer);
53
+ } else {
54
+ req.body = {};
55
+ }
56
+ } catch (error) {
57
+ res.statusCode = 400;
58
+ res.end("Bad Request: Invalid JSON");
59
+ return;
60
+ }
61
+ this.handle(req, res, done);
62
+ });
63
+ }
64
+ else {
65
+ req.body = {};
66
+ this.handle(req, res, done);
67
+ }
68
+ });
69
+ });
70
+ });
71
+ });
72
+ server.listen(port, callback);
73
+ return server;
74
+ }
75
+
76
+ __addHelper(res) {
77
+ res.json = (data) => {
78
+ res.setHeader("Content-Type", "application/json");
79
+ res.end(JSON.stringify(data));
80
+ };
81
+
82
+ res.send = (data) => {
83
+ res.setHeader("Content-Type", "text/plain");
84
+ res.end(data);
85
+ };
86
+ }
82
87
  }
83
88
 
84
89
  export default Core;
package/core/Remote.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { isPromise } from "../utils/isPromise.js";
2
+
1
3
 
2
4
  class Remote {
3
5
 
@@ -29,7 +31,11 @@ class Remote {
29
31
  }
30
32
 
31
33
  try {
32
- fn(req, res, internalNext);
34
+ let results = fn(req, res, internalNext);
35
+
36
+ if(isPromise(results)){
37
+ results.catch(internalNext);
38
+ }
33
39
  }
34
40
  catch (error) {
35
41
  internalNext(error);
@@ -118,7 +124,6 @@ class Remote {
118
124
  }
119
125
  route.handler.handle(req, res, done);
120
126
 
121
- console.log('Esto es un sub router')
122
127
  } else {
123
128
 
124
129
  route.handler(req, res, next);
@@ -19,20 +19,15 @@ class Connection {
19
19
  await this.client.connect()
20
20
  return;
21
21
  }
22
- if(DB_DRIVER === 'mysql'){
23
- await this.__connectMysql()
24
- return;
25
- }
22
+
26
23
  throw new Error("Driver no soportado" + DB_DRIVER)
27
24
  }
28
25
 
29
26
 
30
27
 
31
- static async __connectMysql(){
32
-
33
- }
34
28
 
35
29
  static async query(sql, params){
30
+ if(!this.client) throw new Error("No hay conexion activa")
36
31
  return this.client.query(sql, params);
37
32
  }
38
33
  }
package/database/Model.js CHANGED
@@ -34,7 +34,7 @@ class Model {
34
34
  }
35
35
 
36
36
  static orderBy(...params){
37
- return this.query().orderby(...params);
37
+ return this.query().orderBy(...params);
38
38
  }
39
39
  static limit(...params){
40
40
  return this.query().limit(...params);
@@ -40,10 +40,10 @@ class Postgres extends Driver {
40
40
  }
41
41
  catch (e) {
42
42
  if (e.code === 'ERR_MODULE_NOT_FOUND') {
43
- console.log('Por favor utiliza el comando npm install pg')
43
+ throw new Error('Por favor utiliza el comando npm install pg')
44
44
  }
45
45
  else {
46
- console.error(e)
46
+ throw new Error(e.message)
47
47
  }
48
48
  }
49
49
  }
@@ -51,14 +51,19 @@ class Postgres extends Driver {
51
51
 
52
52
  async query(sql, params) {
53
53
  if (!this.client) {
54
- return console.log("Dynamo: No hay ninguna conexion activa")
54
+ throw new Error("Database not connected")
55
55
  }
56
+ try{
56
57
  const result = await this.client.query(sql, params);
57
58
  return {
58
59
  rows: result.rows,
59
60
  count: result.rowCount
60
61
  }
61
62
  }
63
+ catch(err){
64
+ throw new Error(`Database Query Failed:${err.code}: ${err.message}`)
65
+ }
66
+ }
62
67
 
63
68
  compileSelect(builder) {
64
69
  const stringColums = builder.columns.join(', ')
@@ -1,6 +1,6 @@
1
1
 
2
2
  import LiveCurrent from "./LiveCurrent.js";
3
3
 
4
- const Live = new LiveCurrent('Server.js')
4
+ const Live = new LiveCurrent('app.js')
5
5
 
6
6
  Live.boot()
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs/promises'
2
2
  import { spawn } from 'child_process'
3
- import chalk from 'chalk'
3
+ import { green } from '../utils/console.js';
4
4
  class LiveCurrent{
5
5
  constructor(entryPoint){
6
6
  this.entryPoint = entryPoint;
@@ -14,7 +14,7 @@ class LiveCurrent{
14
14
 
15
15
  ignite(){
16
16
  this.process= spawn('node', [this.entryPoint], {stdio: 'inherit'});
17
- console.log(chalk.greenBright(`LiveCurrent: Iniciando Servidor...`));
17
+ console.log(green(`LiveCurrent: Iniciando Servidor...`));
18
18
  }
19
19
  reload(){
20
20
  if(this.process){
package/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Tesla Framework - Main Export
2
+ * Nicola Framework - Main Export
3
3
  *
4
4
  * Punto de entrada principal del framework
5
5
  */
6
6
 
7
7
  // Core
8
- export { default as Tesla } from './core/Core.js';
8
+ export { default as Nicola } from './core/Core.js';
9
9
  export { default as Remote } from './core/Remote.js';
10
10
 
11
11
  // Middlewares
@@ -21,11 +21,10 @@ export { default as Regulator } from './security/Regulator.js';
21
21
 
22
22
  // Dev Tools
23
23
  export { default as LiveCurrent } from './dev-tools/LiveCurrent.js';
24
- export { default as DevRunner } from './dev-tools/DevRunner.js';
25
24
 
26
25
  // Database (Dynamo ORM)
27
26
  export { default as Dynamo } from './database/index.js';
28
27
 
29
- // Default export (Tesla Core)
30
- import Tesla from './core/Core.js';
31
- export default Tesla;
28
+ // Default export (Nicola Core)
29
+ import Nicola from './core/Core.js';
30
+ export default Nicola;
@@ -1,19 +1,38 @@
1
+ const EasyCors = (options = {}) => {
2
+ const allowedOrigin = options.origin || "*";
1
3
 
4
+ return (req, res, next) => {
5
+ const whitelist = Array.isArray(allowedOrigin)
6
+ ? allowedOrigin
7
+ : [allowedOrigin];
8
+ const incomingOrigin = req.headers.origin;
9
+ if ((incomingOrigin && whitelist.includes(incomingOrigin)) || (whitelist.includes('*'))) {
10
+ if (whitelist.includes("*")) {
11
+ res.setHeader("Access-Control-Allow-Origin", `*`);
12
+ }
13
+ else{
14
+ res.setHeader("Access-Control-Allow-Origin", `${req.headers.origin}`);
15
+ }
16
+ res.setHeader(
17
+ "Access-Control-Allow-Methods",
18
+ "GET, POST, PUT, DELETE, OPTIONS, PATCH"
19
+ );
20
+ res.setHeader(
21
+ "Access-Control-Allow-Headers",
22
+ "Content-Type, Authorization"
23
+ );
2
24
 
3
- const EasyCors = (req, res, next) =>{
4
- res.setHeader('Access-Control-Allow-Origin','*')
5
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH')
6
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
7
-
8
- if(req.method === 'OPTIONS'){
9
- res.statusCode = 204
10
- res.end()
25
+ if (req.method === "OPTIONS") {
26
+ res.statusCode = 204;
27
+ res.end();
11
28
  return;
29
+ } else {
30
+ next();
31
+ }
32
+ } else {
33
+ next();
12
34
  }
13
- else{
14
- next()
15
- }
16
- }
17
-
35
+ };
36
+ };
18
37
 
19
- export default EasyCors;
38
+ export default EasyCors;
@@ -1,10 +1,9 @@
1
- import chalk from 'chalk'
2
-
1
+ import { green } from "../utils/console.js";
3
2
  const Shadowgraph = (req, res, next) =>{
4
3
  const inicio = Date.now()
5
4
  res.on('finish', () =>{
6
5
  const duracion = Date.now() - inicio
7
- console.log(chalk.green(`[${req.method}] ${req.url} - ${res.statusCode} ${res.statusMessage} - ${duracion}ms`))
6
+ console.log(green(`[${req.method}] ${req.url} - ${res.statusCode} ${res.statusMessage} - ${duracion}ms`))
8
7
  })
9
8
  next()
10
9
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nicola-framework",
3
- "version": "1.0.0",
4
- "description": "Zero-dependency web framework for Node.js",
3
+ "version": "1.0.2",
4
+ "description": "Web framework for Node.js",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "exports": {
@@ -13,7 +13,7 @@
13
13
  "./database": "./database/index.js"
14
14
  },
15
15
  "scripts": {
16
- "test": "echo \"Error: no test specified\" && exit 1"
16
+ "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js"
17
17
  },
18
18
  "keywords": [
19
19
  "framework",
@@ -22,12 +22,12 @@
22
22
  "server",
23
23
  "router",
24
24
  "middleware",
25
- "zero-dependency",
26
25
  "orm"
27
26
  ],
28
- "author": "Your Name",
27
+ "author": "Erick Mauricio Tiznado Rodriguez",
29
28
  "license": "MIT",
30
- "dependencies": {
31
- "chalk": "^5.6.2"
29
+ "devDependencies": {
30
+ "jest": "^30.2.0",
31
+ "supertest": "^7.2.2"
32
32
  }
33
33
  }
@@ -1,53 +1,78 @@
1
- import crypto from 'crypto'
2
- import Regulator from './Regulator.js';
1
+ import crypto from "crypto";
2
+ import Regulator from "./Regulator.js";
3
+ import { getExpTime } from "../utils/expTime.js";
3
4
 
4
- class Coherer{
5
- constructor(){
5
+ class Coherer {
6
+ constructor() {}
6
7
 
7
- }
8
- static SECRET = process.env.TESLA_SECRET || 'tesla_secret_dev_key'
8
+ static codec(jsonData) {
9
+ const dataString = JSON.stringify(jsonData);
10
+ const buffer = Buffer.from(dataString);
11
+ return buffer.toString("base64url");
12
+ }
13
+
14
+ static sign(Payload, options) {
15
+ const SECRET = process.env.NICOLA_SECRET
16
+ if (!SECRET)
17
+ throw new Error("Please configure, NICOLA_SECRET in the .env file");
18
+
19
+ let payloadB64 = "";
9
20
 
10
- static codec(jsonData){
11
- const dataString = JSON.stringify(jsonData);
12
- const buffer = Buffer.from(dataString);
13
- return buffer.toString('base64url')
21
+ if ("expiresIn" in options) {
22
+ const time = getExpTime(options.expiresIn);
23
+ const newPayload = { ...Payload, exp: time };
24
+ payloadB64 = this.codec(newPayload);
25
+ } else {
26
+ throw new Error("Expire time invalid");
14
27
  }
15
28
 
16
- static sign(Payload){
17
- const payloadB64 = this.codec(Payload);
18
- const header = {
19
- alg: 'HS256',
20
- typ: 'JWT'
21
- }
22
- const headerB64 = this.codec(header)
29
+ const header = {
30
+ alg: "HS256",
31
+ typ: "JWT",
32
+ };
33
+ const headerB64 = this.codec(header);
23
34
 
24
- const data = headerB64 + '.' + payloadB64
35
+ const data = headerB64 + "." + payloadB64;
25
36
 
26
- const signature = crypto.createHmac('sha256', this.SECRET)
27
- .update(data)
28
- .digest('base64url')
37
+ const signature = crypto
38
+ .createHmac("sha256", SECRET)
39
+ .update(data)
40
+ .digest("base64url");
29
41
 
30
- return data + '.' + signature
31
- }
32
- static verify(data){
33
- const [headerB64, payloadB64, signature] = data.token.split('.');
42
+ return data + "." + signature;
43
+ }
34
44
 
35
- const dataToCheck = headerB64 + '.' + payloadB64
45
+ static verify(token) {
46
+ const SECRET = process.env.NICOLA_SECRET
47
+ if (!SECRET)
48
+ throw new Error("Please configure, NICOLA_SECRET in the .env file");
49
+ const [headerB64, payloadB64, signature] = token.split(".");
36
50
 
37
- const signatureToChecks = crypto.createHmac('sha256', this.SECRET)
38
- .update(dataToCheck)
39
- .digest('base64url')
51
+ const dataToCheck = headerB64 + "." + payloadB64;
40
52
 
41
- if(signature === signatureToChecks){
42
- let decodedPayload = Buffer.from(payloadB64, 'base64url').toString('utf-8')
43
- decodedPayload = JSON.parse(decodedPayload)
44
- return decodedPayload
45
- }
46
- else{
47
- throw new Error('Token Invalido')
53
+ const signatureToChecks = crypto
54
+ .createHmac("sha256", SECRET)
55
+ .update(dataToCheck)
56
+ .digest("base64url");
57
+
58
+ if (signature === signatureToChecks) {
59
+ let decodedPayload = Buffer.from(payloadB64, "base64url").toString(
60
+ "utf-8"
61
+ );
62
+
63
+ decodedPayload = JSON.parse(decodedPayload);
64
+ if ("exp" in decodedPayload) {
65
+ const datenow = Date.now() / 1000;
66
+
67
+ if (datenow > decodedPayload.exp) {
68
+ throw new Error("Token Expired");
48
69
  }
70
+ }
71
+ return decodedPayload;
72
+ } else {
73
+ throw new Error("Token Invalido");
49
74
  }
75
+ }
50
76
  }
51
77
 
52
-
53
- export default Coherer;
78
+ export default Coherer;
@@ -0,0 +1,73 @@
1
+ import Coherer from '../security/Coherer.js'; // Ajusta la ruta según tu estructura
2
+ // Nota: Jest inyecta 'describe', 'test', 'expect' globalmente, no hace falta importarlos.
3
+
4
+ describe('🛡️ Módulo de Seguridad (Coherer)', () => {
5
+
6
+ // Antes de todos los tests, configuramos el entorno
7
+ beforeAll(() => {
8
+ process.env.NICOLA_SECRET = 'test_secret_key_123';
9
+ });
10
+
11
+ // Limpiamos después (buena práctica)
12
+ afterAll(() => {
13
+ delete process.env.NICOLA_SECRET;
14
+ });
15
+
16
+ describe('Happy Path (Lo que debe salir bien)', () => {
17
+
18
+ test('Debe firmar y verificar un token correctamente', () => {
19
+ const payload = { userId: 1, role: 'admin' };
20
+ const options = { expiresIn: '1h' };
21
+
22
+ // 1. Firmar
23
+ const token = Coherer.sign(payload, options);
24
+
25
+ // Verificamos que sea un string y tenga 3 partes (header.payload.signature)
26
+ expect(typeof token).toBe('string');
27
+ expect(token.split('.')).toHaveLength(3);
28
+
29
+ // 2. Verificar
30
+ const decoded = Coherer.verify(token);
31
+
32
+ // Verificamos que la data sea la misma
33
+ expect(decoded.userId).toBe(payload.userId);
34
+ expect(decoded.role).toBe(payload.role);
35
+
36
+ // Verificamos que se haya agregado la expiración
37
+ expect(decoded).toHaveProperty('exp');
38
+ });
39
+ });
40
+
41
+ describe('Sad Path (Errores esperados)', () => {
42
+
43
+ test('Debe fallar si no se define una expiración', () => {
44
+ const payload = { userId: 1 };
45
+
46
+ // En Jest, para probar errores, envolvemos la llamada en una función anónima () =>
47
+ expect(() => {
48
+ Coherer.sign(payload, {});
49
+
50
+ }).toThrow('Expire time invalid');
51
+ });
52
+
53
+ test('Debe fallar si el token es basura', () => {
54
+ const tokenBasura = 'esto.no.es.valido';
55
+
56
+ expect(() => {
57
+ Coherer.verify(tokenBasura);
58
+ }).toThrow(); // Esperamos cualquier error (Firma inválida, malformado, etc.)
59
+ });
60
+
61
+ test('Debe fallar si la firma ha sido manipulada', () => {
62
+ const tokenReal = Coherer.sign({ id: 1 }, { expiresIn: '1h' });
63
+ const partes = tokenReal.split('.');
64
+
65
+ // Hack: Cambiamos la firma (última parte) por otra cosa
66
+ const tokenFalso = `${partes[0]}.${partes[1]}.FIRMA_FALSA`;
67
+
68
+ expect(() => {
69
+ Coherer.verify(tokenFalso);
70
+ }).toThrow('Token Invalido');
71
+ });
72
+ });
73
+ });
@@ -0,0 +1,96 @@
1
+ import request from "supertest";
2
+ import Core from "../core/Core.js";
3
+
4
+ describe("🧠 Core System (Core.js)", () => {
5
+ let app;
6
+ let server;
7
+
8
+ beforeAll((done) => {
9
+ app = new Core();
10
+
11
+ app.get("/ping", (req, res) => {
12
+ res.statusCode = 200;
13
+ res.end("pong");
14
+ });
15
+
16
+ app.get("/query", (req, res) => {
17
+ res.json({ query: req.query });
18
+ });
19
+
20
+ app.post("/json", (req, res) => {
21
+ res.json({ body: req.body });
22
+ });
23
+
24
+ server = app.listen(0, done);
25
+ });
26
+
27
+ afterAll((done) => {
28
+ server.close(done);
29
+ });
30
+
31
+ test("GET /ping responde 200", async () => {
32
+ const res = await request(server).get("/ping");
33
+ expect(res.statusCode).toBe(200);
34
+ expect(res.text).toBe("pong");
35
+ });
36
+
37
+ test("Aplica headers de Teleforce", async () => {
38
+ const res = await request(server).get("/ping");
39
+ expect(res.headers["x-content-type-options"]).toBe("nosniff");
40
+ expect(res.headers["x-frame-options"]).toBe("Deny");
41
+ expect(res.headers["x-xss-protection"]).toBe("1");
42
+ });
43
+
44
+ test("Parsea querystring y lo expone en req.query", async () => {
45
+ const res = await request(server).get("/query?x=1&y=hola");
46
+ expect(res.statusCode).toBe(200);
47
+ expect(res.body).toEqual({ query: { x: "1", y: "hola" } });
48
+ });
49
+
50
+ test("POST JSON válido -> req.body parseado", async () => {
51
+ const res = await request(server)
52
+ .post("/json")
53
+ .set("Content-Type", "application/json; charset=utf-8")
54
+ .send(JSON.stringify({ a: 1 }));
55
+
56
+ expect(res.statusCode).toBe(200);
57
+ expect(res.body).toEqual({ body: { a: 1 } });
58
+ });
59
+
60
+ test("POST JSON inválido -> 400", async () => {
61
+ const res = await request(server)
62
+ .post("/json")
63
+ .set("Content-Type", "application/json")
64
+ .send("{invalid");
65
+
66
+ expect(res.statusCode).toBe(400);
67
+ expect(res.text).toBe("Bad Request: Invalid JSON");
68
+ });
69
+
70
+ test("Sin Content-Type -> req.body = {}", async () => {
71
+ const res = await request(server).post("/json").send("no-json");
72
+ expect(res.statusCode).toBe(200);
73
+ expect(res.body).toEqual({ body: {} });
74
+ });
75
+
76
+ test("OPTIONS responde 204 y headers CORS", async () => {
77
+ const res = await request(server).options("/ping");
78
+ expect(res.statusCode).toBe(204);
79
+ expect(res.headers["access-control-allow-origin"]).toBe("*");
80
+ expect(res.headers["access-control-allow-methods"]).toBe(
81
+ "GET, POST, PUT, DELETE, OPTIONS, PATCH"
82
+ );
83
+ });
84
+
85
+ test("Body > ~2MB -> 413", async () => {
86
+ const big = "{\"x\":\"" + "a".repeat(2_000_001) + "\"}";
87
+
88
+ const res = await request(server)
89
+ .post("/json")
90
+ .set("Content-Type", "application/json")
91
+ .send(big);
92
+
93
+ expect(res.statusCode).toBe(413);
94
+ expect(res.text).toBe("Request Entity Too Large");
95
+ });
96
+ });
@@ -0,0 +1,76 @@
1
+ import request from 'supertest';
2
+ import { createServer } from 'http';
3
+ import Remote from '../core/Remote.js'; // Ajusta la ruta si es necesario
4
+
5
+ describe('🚦 Router System (Remote.js)', () => {
6
+ let router;
7
+ let server;
8
+
9
+ // Antes de todos los tests, configuramos una "App de Mentira"
10
+ beforeAll(() => {
11
+ router = new Remote();
12
+
13
+ // 1. Ruta Básica
14
+ router.get('/', (req, res) => {
15
+ res.statusCode = 200;
16
+ res.end('Hola Nicola');
17
+ });
18
+
19
+ // 2. Ruta con Parámetros
20
+ router.get('/user/:id', (req, res) => {
21
+ res.statusCode = 200;
22
+ res.setHeader('Content-Type', 'application/json');
23
+ res.end(JSON.stringify({ id: req.params.id }));
24
+ });
25
+
26
+ // 3. Ruta Suicida (Async Error) - Para probar tu arreglo P0
27
+ router.get('/crash', async (req, res) => {
28
+ throw new Error('Boom Async!');
29
+ });
30
+
31
+ // Creamos el servidor nativo que usa tu Router
32
+ server = createServer((req, res) => {
33
+ // Tu router recibe (req, res, done)
34
+ router.handle(req, res, (err) => {
35
+ // Este es el "Final Handler" (lo que sería tu BlackBox o default)
36
+ if (err) {
37
+ res.statusCode = 500;
38
+ res.end(err.message); // Devolvemos el error capturado
39
+ } else {
40
+ res.statusCode = 404;
41
+ res.end('Not Found');
42
+ }
43
+ });
44
+ });
45
+ });
46
+
47
+ describe('Rutas y Navegación', () => {
48
+ test('GET / debe responder 200 OK', async () => {
49
+ const response = await request(server).get('/');
50
+ expect(response.statusCode).toBe(200);
51
+ expect(response.text).toBe('Hola Nicola');
52
+ });
53
+
54
+ test('GET /unknown debe responder 404', async () => {
55
+ const response = await request(server).get('/ruta-que-no-existe');
56
+ expect(response.statusCode).toBe(404);
57
+ });
58
+
59
+ test('GET /user/123 debe capturar parámetros', async () => {
60
+ const response = await request(server).get('/user/555');
61
+ expect(response.statusCode).toBe(200);
62
+ expect(response.body).toEqual({ id: '555' });
63
+ });
64
+ });
65
+
66
+ describe('🛡️ Blindaje de Errores (P0 Fix)', () => {
67
+ test('Debe capturar errores en handlers async y no colgarse', async () => {
68
+ // Si tu fix del principio funciona, esto devolverá 500.
69
+ // Si NO funciona, este test dará timeout porque el servidor se quedará colgado.
70
+ const response = await request(server).get('/crash');
71
+
72
+ expect(response.statusCode).toBe(500);
73
+ expect(response.text).toBe('Boom Async!');
74
+ });
75
+ });
76
+ });
@@ -0,0 +1,31 @@
1
+ const CODE = {
2
+ reset: "\x1b[0m",
3
+ negrita: "\x1b[1m",
4
+ brigth: "\x1b[1m",
5
+ red: "\x1b[31m",
6
+ green: "\x1b[32m",
7
+ yellow: "\x1b[33m",
8
+ blue: "\x1b[34m",
9
+ cyan: "\x1b[36m",
10
+ magent: "\x1b[35m",
11
+ };
12
+
13
+
14
+
15
+ export const paint = (color, text) =>{
16
+ if(!process.stdout.isTTY) return text;
17
+
18
+
19
+ return `${CODE[color] || ''}${text}${CODE.reset}`
20
+ }
21
+
22
+
23
+
24
+ export const red = (text) => paint('red', text);
25
+ export const green = (text) => paint('green', text);
26
+ export const yellow = (text) => paint('yellow', text);
27
+ export const blue = (text) => paint('blue', text);
28
+ export const cyan = (text) => paint('cyan', text);
29
+ export const magent = (text) => paint('magent', text);
30
+ export const brigth = (text) => paint('brigth', text);
31
+ export const bold = (text) => paint('negrita', text);
@@ -0,0 +1,30 @@
1
+
2
+ export const getExpTime = (durationStr) => {
3
+
4
+ const nowInSeconds = Math.floor(Date.now() / 1000);
5
+
6
+ if (!durationStr || typeof durationStr !== 'string') return null;
7
+
8
+ const match = durationStr.match(/^(\d+)([smhdy])$/);
9
+
10
+ if(!match){
11
+ throw new Error(`Invalid Format, use for example: 10h, 10s, 10m, 10d`)
12
+ }
13
+
14
+ const value = parseInt(match[1], 10);
15
+ const unit = match[2];
16
+
17
+ let multiplier = 1;
18
+ switch(unit){
19
+ case 'm' : multiplier = 60; break;
20
+ case 'h' : multiplier = 60 * 60; break;
21
+ case 'd' : multiplier = 24 * 60 * 60; break;
22
+ case 'y' : multiplier = 365 * 24 * 60 * 60; break;
23
+ default: multiplier = 1;
24
+ }
25
+
26
+ const secondsToadd = value * multiplier;
27
+
28
+ return nowInSeconds + secondsToadd;
29
+
30
+ }
@@ -0,0 +1,7 @@
1
+
2
+
3
+ export const isPromise = (value) =>{
4
+ return (!!value &&
5
+ (typeof value === 'object' || typeof value === 'function')
6
+ && typeof value.then === 'function')
7
+ }
File without changes