nyte 1.0.0 → 1.1.0

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.
@@ -1 +1 @@
1
- export {};
1
+ export declare function DevIndicator(): import("react/jsx-runtime").JSX.Element | null;
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DevIndicator = DevIndicator;
36
37
  const jsx_runtime_1 = require("react/jsx-runtime");
37
38
  /*
38
39
  * This file is part of the Nyte.js Project.
@@ -214,184 +215,72 @@ function App({ componentMap, routes, initialComponentPath, initialParams, layout
214
215
  // Adiciona o indicador de dev se não for produção
215
216
  return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [content, process.env.NODE_ENV !== 'production' && (0, jsx_runtime_1.jsx)(DevIndicator, {})] }));
216
217
  }
217
- // --- Constantes de Configuração ---
218
- const DEV_INDICATOR_SIZE = 48;
219
- const DEV_INDICATOR_CORNERS = [
220
- { top: 16, left: 16 }, // 0: topo-esquerda
221
- { top: 16, right: 16 }, // 1: topo-direita
222
- { bottom: 16, left: 16 }, // 2: baixo-esquerda
223
- { bottom: 16, right: 16 }, // 3: baixo-direita
224
- ];
225
218
  function DevIndicator() {
226
- const [corner, setCorner] = (0, react_1.useState)(3); // Canto atual (0-3)
227
- const [isDragging, setIsDragging] = (0, react_1.useState)(false); // Estado de arrastar
228
- const [isBuilding, setIsBuilding] = (0, react_1.useState)(false); // Estado de build
229
- // Posição visual do indicador durante o arraste
230
- const [position, setPosition] = (0, react_1.useState)({ top: 0, left: 0 });
231
- const indicatorRef = (0, react_1.useRef)(null);
232
- const dragStartRef = (0, react_1.useRef)(null);
233
- // Escuta eventos de hot reload para mostrar estado de build
234
- (0, react_1.useEffect)(() => {
235
- if (typeof window === 'undefined')
236
- return;
237
- const handleHotReloadMessage = (event) => {
238
- try {
239
- const message = JSON.parse(event.data);
240
- // Quando detecta mudança em arquivo, ativa loading
241
- if (message.type === 'frontend-reload' ||
242
- message.type === 'backend-api-reload' ||
243
- message.type === 'src-reload') {
244
- setIsBuilding(true);
245
- }
246
- // Quando o build termina ou servidor fica pronto, desativa loading
247
- if (message.type === 'server-ready' || message.type === 'build-complete') {
248
- setIsBuilding(false);
249
- }
250
- }
251
- catch (e) {
252
- // Ignora mensagens que não são JSON
253
- }
254
- };
255
- // Intercepta mensagens WebSocket
256
- const originalWebSocket = window.WebSocket;
257
- window.WebSocket = class extends originalWebSocket {
258
- constructor(url, protocols) {
259
- super(url, protocols);
260
- this.addEventListener('message', (event) => {
261
- if (url.toString().includes('hweb-hotreload')) {
262
- handleHotReloadMessage(event);
263
- }
264
- });
265
- }
266
- };
267
- return () => {
268
- window.WebSocket = originalWebSocket;
269
- };
270
- }, []);
271
- // --- Estilos Dinâmicos ---
272
- const getIndicatorStyle = () => {
273
- const baseStyle = {
274
- position: 'fixed',
275
- zIndex: 9999,
276
- width: DEV_INDICATOR_SIZE,
277
- height: DEV_INDICATOR_SIZE,
278
- borderRadius: '50%',
279
- background: isBuilding
280
- ? 'linear-gradient(135deg, #f093fb, #f5576c)' // Gradiente Rosa/Vermelho quando building
281
- : 'linear-gradient(135deg, #8e2de2, #4a00e0)', // Gradiente Roxo normal
282
- color: 'white',
283
- fontWeight: 'bold',
284
- fontSize: 28,
285
- boxShadow: isBuilding
286
- ? '0 4px 25px rgba(245, 87, 108, 0.6)' // Shadow mais forte quando building
287
- : '0 4px 15px rgba(0,0,0,0.2)',
288
- display: 'flex',
289
- alignItems: 'center',
290
- justifyContent: 'center',
291
- cursor: isDragging ? 'grabbing' : 'grab',
292
- userSelect: 'none',
293
- transition: isDragging ? 'none' : 'all 0.3s ease-out',
294
- animation: isBuilding ? 'hweb-pulse 1.5s ease-in-out infinite' : 'none',
295
- };
296
- if (isDragging) {
297
- return {
298
- ...baseStyle,
299
- top: position.top,
300
- left: position.left,
301
- };
302
- }
303
- return { ...baseStyle, ...DEV_INDICATOR_CORNERS[corner] };
304
- };
305
- const getMenuPositionStyle = () => {
306
- // Posiciona o menu dependendo do canto
307
- switch (corner) {
308
- case 0: return { top: '110%', left: '0' }; // Top-Left
309
- case 1: return { top: '110%', right: '0' }; // Top-Right
310
- case 2: return { bottom: '110%', left: '0' }; // Bottom-Left
311
- case 3: return { bottom: '110%', right: '0' }; // Bottom-Right
312
- default: return {};
313
- }
314
- };
315
- // --- Lógica de Eventos ---
316
- const handleMouseDown = (e) => {
317
- e.preventDefault();
318
- dragStartRef.current = { x: e.clientX, y: e.clientY, moved: false };
319
- if (indicatorRef.current) {
320
- const rect = indicatorRef.current.getBoundingClientRect();
321
- setPosition({ top: rect.top, left: rect.left });
322
- }
323
- setIsDragging(true);
324
- };
325
- const handleMouseMove = (0, react_1.useCallback)((e) => {
326
- if (!isDragging || !dragStartRef.current)
327
- return;
328
- const deltaX = e.clientX - dragStartRef.current.x;
329
- const deltaY = e.clientY - dragStartRef.current.y;
330
- // Diferencia clique de arrastar (threshold de 5px)
331
- if (!dragStartRef.current.moved && Math.hypot(deltaX, deltaY) > 5) {
332
- dragStartRef.current.moved = true;
333
- }
334
- if (dragStartRef.current.moved) {
335
- setPosition(prevPos => ({
336
- top: prevPos.top + deltaY,
337
- left: prevPos.left + deltaX,
338
- }));
339
- // Atualiza a referência para o próximo movimento
340
- dragStartRef.current.x = e.clientX;
341
- dragStartRef.current.y = e.clientY;
342
- }
343
- }, [isDragging]);
344
- const handleMouseUp = (0, react_1.useCallback)((e) => {
345
- if (!isDragging)
346
- return;
347
- setIsDragging(false);
348
- // Se moveu, calcula o canto mais próximo
349
- if (dragStartRef.current?.moved) {
350
- const { clientX, clientY } = e;
351
- const w = window.innerWidth;
352
- const h = window.innerHeight;
353
- const dists = [
354
- Math.hypot(clientX, clientY), // TL
355
- Math.hypot(w - clientX, clientY), // TR
356
- Math.hypot(clientX, h - clientY), // BL
357
- Math.hypot(w - clientX, h - clientY), // BR
358
- ];
359
- setCorner(dists.indexOf(Math.min(...dists)));
360
- }
361
- dragStartRef.current = null;
362
- }, [isDragging]);
363
- // Adiciona e remove listeners globais
364
- (0, react_1.useEffect)(() => {
365
- if (isDragging) {
366
- window.addEventListener('mousemove', handleMouseMove);
367
- window.addEventListener('mouseup', handleMouseUp);
368
- }
369
- return () => {
370
- window.removeEventListener('mousemove', handleMouseMove);
371
- window.removeEventListener('mouseup', handleMouseUp);
372
- };
373
- }, [isDragging, handleMouseMove, handleMouseUp]);
219
+ const [isVisible, setIsVisible] = (0, react_1.useState)(true);
220
+ if (!isVisible)
221
+ return null;
374
222
  return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("style", { children: `
375
- @keyframes hweb-pulse {
376
- 0%, 100% {
377
- transform: scale(1);
378
- opacity: 1;
379
- }
380
- 50% {
381
- transform: scale(1.1);
382
- opacity: 0.8;
383
- }
384
- }
385
-
386
- @keyframes hweb-spin {
387
- from {
388
- transform: rotate(0deg);
389
- }
390
- to {
391
- transform: rotate(360deg);
392
- }
393
- }
394
- ` }), (0, jsx_runtime_1.jsx)("div", { ref: indicatorRef, style: getIndicatorStyle(), onMouseDown: handleMouseDown, title: isBuilding ? "Building..." : "Modo Dev Nyte.js", children: isBuilding ? ((0, jsx_runtime_1.jsx)("span", { style: { animation: 'hweb-spin 1s linear infinite' }, children: "\u27F3" })) : ('H') })] }));
223
+ @keyframes nyte-pulse {
224
+ 0% { opacity: 0.4; }
225
+ 50% { opacity: 1; }
226
+ 100% { opacity: 0.4; }
227
+ }
228
+ .nyte-dev-badge {
229
+ position: fixed;
230
+ bottom: 20px;
231
+ right: 20px;
232
+ z-index: 999999;
233
+ display: flex;
234
+ align-items: center;
235
+ gap: 12px;
236
+ padding: 8px 14px;
237
+ background: rgba(15, 15, 20, 0.8);
238
+ backdrop-filter: blur(12px);
239
+ -webkit-backdrop-filter: blur(12px);
240
+
241
+ border-radius: 10px;
242
+ color: #fff;
243
+ font-family: 'Inter', ui-monospace, SFMono-Regular, Menlo, monospace;
244
+ font-size: 12px;
245
+ font-weight: 600;
246
+ letter-spacing: 0.05em;
247
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
248
+ transition: all 0.2s ease;
249
+ cursor: default;
250
+ user-select: none;
251
+ }
252
+ .nyte-dev-badge:hover {
253
+ border-color: rgba(142, 45, 226, 0.5);
254
+ transform: translateY(-2px);
255
+ }
256
+ .nyte-status-dot {
257
+ width: 8px;
258
+ height: 8px;
259
+ background: #10b981; /* Verde esmeralda */
260
+ border-radius: 50%;
261
+ box-shadow: 0 0 10px #10b981;
262
+ animation: nyte-pulse 2s infinite ease-in-out;
263
+ }
264
+ .nyte-label {
265
+ color: rgba(255, 255, 255, 0.5);
266
+ text-transform: uppercase;
267
+ font-size: 10px;
268
+ }
269
+ .nyte-logo {
270
+ background: linear-gradient(135deg, #00a3a3, #808080);
271
+ -webkit-background-clip: text;
272
+ -webkit-text-fill-color: transparent;
273
+ font-weight: 800;
274
+ }
275
+ ` }), (0, jsx_runtime_1.jsxs)("div", { className: "nyte-dev-badge", children: [(0, jsx_runtime_1.jsx)("div", { className: "nyte-status-dot" }), (0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("span", { className: "nyte-logo", children: "NYTE.JS" }) }), (0, jsx_runtime_1.jsx)("button", { onClick: () => setIsVisible(false), style: {
276
+ background: 'none',
277
+ border: 'none',
278
+ color: 'rgba(255,255,255,0.3)',
279
+ cursor: 'pointer',
280
+ fontSize: '14px',
281
+ padding: '0 0 0 4px',
282
+ marginLeft: '4px'
283
+ }, title: "Fechar", children: "\u00D7" })] })] }));
395
284
  }
396
285
  // --- Inicialização do Cliente (CSR - Client-Side Rendering) ---
397
286
  function deobfuscateData(obfuscated) {
package/dist/hotReload.js CHANGED
@@ -213,6 +213,7 @@ class HotReloadManager {
213
213
  });
214
214
  try {
215
215
  this.frontendChangeCallback?.();
216
+ this.backendApiChangeCallback?.();
216
217
  await Promise.race([buildPromise, timeoutPromise]);
217
218
  dm.end(`Build complete for ${path.basename(filePath)}, reloading frontend.`);
218
219
  this.frontendChangeCallback?.();
@@ -230,13 +231,13 @@ class HotReloadManager {
230
231
  }
231
232
  // Se for arquivo de backend, recarrega o módulo e notifica
232
233
  if (isBackendFile) {
233
- console_1.default.logWithout(console_1.Levels.INFO, console_1.Colors.BgRed, `Reloading backend...`);
234
+ console_1.default.logWithout(console_1.Levels.INFO, undefined, `Reloading backend...`);
234
235
  this.backendApiChangeCallback?.();
235
236
  this.notifyClients('backend-api-reload', { file: filePath, event: 'change' });
236
237
  }
237
238
  // Fallback: se não for nem frontend nem backend detectado, recarrega tudo
238
239
  if (!isFrontendFile && !isBackendFile) {
239
- console_1.default.logWithout(console_1.Levels.INFO, console_1.Colors.BgRed, `Reloading application...`);
240
+ console_1.default.logWithout(console_1.Levels.INFO, undefined, `Reloading application...`);
240
241
  this.frontendChangeCallback?.();
241
242
  this.backendApiChangeCallback?.();
242
243
  this.notifyClients('src-reload', { file: filePath, event: 'change' });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Símbolo único para marcar funções expostas.
3
+ * Usamos Symbol para garantir que não possa ser falsificado via JSON no payload.
4
+ */
5
+ export declare const RPC_EXPOSED_KEY: unique symbol;
6
+ type AnyFn = (...args: any[]) => any;
7
+ /**
8
+ * Marca uma ou mais funções como seguras para RPC.
9
+ */
10
+ export default function Expose<T extends AnyFn>(fn: T): T;
11
+ export default function Expose<T extends AnyFn[]>(fns: [...T]): T;
12
+ export default function Expose<T extends AnyFn[]>(...fns: T): T;
13
+ export {};
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RPC_EXPOSED_KEY = void 0;
4
+ exports.default = Expose;
5
+ /**
6
+ * Símbolo único para marcar funções expostas.
7
+ * Usamos Symbol para garantir que não possa ser falsificado via JSON no payload.
8
+ */
9
+ exports.RPC_EXPOSED_KEY = Symbol('__rpc_exposed__');
10
+ function Expose(...input) {
11
+ const fns = Array.isArray(input[0]) ? input[0] : input;
12
+ for (const fn of fns) {
13
+ if (typeof fn !== 'function') {
14
+ throw new TypeError('Expose aceita apenas funções');
15
+ }
16
+ fn[exports.RPC_EXPOSED_KEY] = true;
17
+ }
18
+ // Retorno:
19
+ // - se veio uma função → retorna ela
20
+ // - se veio lista → retorna a lista
21
+ return input.length === 1 ? input[0] : input;
22
+ }
@@ -7,7 +7,7 @@
7
7
  * you may not use this file except in compliance with the License.
8
8
  * You may obtain a copy of the License at
9
9
  *
10
- * http://www.apache.org/licenses/LICENSE-2.0
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
11
  *
12
12
  * Unless required by applicable law or agreed to in writing, software
13
13
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -23,6 +23,8 @@ exports.executeRpc = executeRpc;
23
23
  const fs_1 = __importDefault(require("fs"));
24
24
  const path_1 = __importDefault(require("path"));
25
25
  const http_1 = require("../api/http");
26
+ // Importamos a chave para verificar a anotação de segurança
27
+ const annotations_1 = require("./annotations");
26
28
  const DEFAULT_ALLOWED_SERVER_DIRS = ['src/backend'];
27
29
  function normalizeToPosix(p) {
28
30
  return p.replace(/\\/g, '/');
@@ -52,7 +54,7 @@ function tryResolveWithinAllowedDirs(projectDir, allowedDirs, requestedFile) {
52
54
  const baseAbs = path_1.default.resolve(projectDir, d);
53
55
  // Interpret client path as relative to src/web (where it's typically authored)
54
56
  const fromWebAbs = path_1.default.resolve(projectDir, 'src/web', req);
55
- // Map: <project>/src/backend/* (coming from ../../backend/* from web code)
57
+ // Map: <project>/src/backend/* (coming from ../../backend/* from web code)
56
58
  const mappedFromWebAbs = fromWebAbs.replace(path_1.default.resolve(projectDir, 'backend') + path_1.default.sep, path_1.default.resolve(projectDir, 'src', 'backend') + path_1.default.sep);
57
59
  // Also accept callers passing a backend-relative path like "./auth" or "auth"
58
60
  const fromBackendAbs = path_1.default.resolve(baseAbs, req);
@@ -155,6 +157,20 @@ async function executeRpc(ctx, payload) {
155
157
  if (typeof fnValue !== 'function') {
156
158
  return { success: false, error: `RPC function not found: ${fnName}` };
157
159
  }
160
+ // --- SECURITY CHECK (Expose Annotation or Allowlist) ---
161
+ // 1. Verifica se a função possui o Symbol definido em annotations.ts (Via wrapper Expose())
162
+ const isAnnotated = fnValue[annotations_1.RPC_EXPOSED_KEY];
163
+ // 2. Verifica se o módulo exporta uma lista explícita chamada 'exposed' ou 'rpcMethods'
164
+ // Isso permite o uso de "export function" sem wrapper, listando os nomes no final do arquivo.
165
+ const allowList = mod.exposed || mod.rpcMethods;
166
+ const isListed = Array.isArray(allowList) && allowList.includes(fnName);
167
+ if (!isAnnotated && !isListed) {
168
+ return {
169
+ success: false,
170
+ error: `Function '${fnName}' is not exposed via RPC. Mark it with Expose() or add it to an exported 'exposed' array.`
171
+ };
172
+ }
173
+ // ------------------------------------------
158
174
  const rpcRequest = ctx.request ? new http_1.NyteRequest(ctx.request) : buildRpcRequestFromPayload(payload);
159
175
  const result = await fnValue(rpcRequest, ...payload.args);
160
176
  return { success: true, return: result };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nyte",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Nyte.js is a high-level framework for building web applications with ease and speed. It provides a robust set of tools and features to streamline development and enhance productivity.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -47,6 +47,11 @@
47
47
  "types": "./dist/eslint/index.d.ts",
48
48
  "import": "./dist/eslint/index.js",
49
49
  "require": "./dist/eslint/index.js"
50
+ },
51
+ "./rpc": {
52
+ "types": "./dist/rpc/annotations.d.ts",
53
+ "import": "./dist/rpc/annotations.js",
54
+ "require": "./dist/rpc/annotations.js"
50
55
  }
51
56
  },
52
57
  "peerDependencies": {
@@ -223,221 +223,92 @@ function App({ componentMap, routes, initialComponentPath, initialParams, layout
223
223
 
224
224
 
225
225
 
226
- // --- Constantes de Configuração ---
227
- const DEV_INDICATOR_SIZE = 48;
228
- const DEV_INDICATOR_CORNERS = [
229
- { top: 16, left: 16 }, // 0: topo-esquerda
230
- { top: 16, right: 16 }, // 1: topo-direita
231
- { bottom: 16, left: 16 }, // 2: baixo-esquerda
232
- { bottom: 16, right: 16 },// 3: baixo-direita
233
- ];
234
-
235
- function DevIndicator() {
236
- const [corner, setCorner] = useState(3); // Canto atual (0-3)
237
- const [isDragging, setIsDragging] = useState(false); // Estado de arrastar
238
- const [isBuilding, setIsBuilding] = useState(false); // Estado de build
239
-
240
- // Posição visual do indicador durante o arraste
241
- const [position, setPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 });
242
-
243
- const indicatorRef = useRef<HTMLDivElement>(null);
244
- const dragStartRef = useRef<{ x: number; y: number; moved: boolean } | null>(null);
245
-
246
- // Escuta eventos de hot reload para mostrar estado de build
247
- useEffect(() => {
248
- if (typeof window === 'undefined') return;
249
-
250
- const handleHotReloadMessage = (event: MessageEvent) => {
251
- try {
252
- const message = JSON.parse(event.data);
253
-
254
- // Quando detecta mudança em arquivo, ativa loading
255
- if (message.type === 'frontend-reload' ||
256
- message.type === 'backend-api-reload' ||
257
- message.type === 'src-reload') {
258
- setIsBuilding(true);
259
- }
260
-
261
- // Quando o build termina ou servidor fica pronto, desativa loading
262
- if (message.type === 'server-ready' || message.type === 'build-complete') {
263
- setIsBuilding(false);
264
- }
265
- } catch (e) {
266
- // Ignora mensagens que não são JSON
267
- }
268
- };
269
-
270
- // Intercepta mensagens WebSocket
271
- const originalWebSocket = window.WebSocket;
272
- window.WebSocket = class extends originalWebSocket {
273
- constructor(url: string | URL, protocols?: string | string[]) {
274
- super(url, protocols);
275
-
276
- this.addEventListener('message', (event) => {
277
- if (url.toString().includes('hweb-hotreload')) {
278
- handleHotReloadMessage(event);
279
- }
280
- });
281
- }
282
- } as any;
283
-
284
- return () => {
285
- window.WebSocket = originalWebSocket;
286
- };
287
- }, []);
288
-
289
- // --- Estilos Dinâmicos ---
290
- const getIndicatorStyle = (): React.CSSProperties => {
291
- const baseStyle: React.CSSProperties = {
292
- position: 'fixed',
293
- zIndex: 9999,
294
- width: DEV_INDICATOR_SIZE,
295
- height: DEV_INDICATOR_SIZE,
296
- borderRadius: '50%',
297
- background: isBuilding
298
- ? 'linear-gradient(135deg, #f093fb, #f5576c)' // Gradiente Rosa/Vermelho quando building
299
- : 'linear-gradient(135deg, #8e2de2, #4a00e0)', // Gradiente Roxo normal
300
- color: 'white',
301
- fontWeight: 'bold',
302
- fontSize: 28,
303
- boxShadow: isBuilding
304
- ? '0 4px 25px rgba(245, 87, 108, 0.6)' // Shadow mais forte quando building
305
- : '0 4px 15px rgba(0,0,0,0.2)',
306
- display: 'flex',
307
- alignItems: 'center',
308
- justifyContent: 'center',
309
- cursor: isDragging ? 'grabbing' : 'grab',
310
- userSelect: 'none',
311
- transition: isDragging ? 'none' : 'all 0.3s ease-out',
312
- animation: isBuilding ? 'hweb-pulse 1.5s ease-in-out infinite' : 'none',
313
- };
314
226
 
315
- if (isDragging) {
316
- return {
317
- ...baseStyle,
318
- top: position.top,
319
- left: position.left,
320
- };
321
- }
322
-
323
- return { ...baseStyle, ...DEV_INDICATOR_CORNERS[corner] };
324
- };
325
-
326
- const getMenuPositionStyle = (): React.CSSProperties => {
327
- // Posiciona o menu dependendo do canto
328
- switch (corner) {
329
- case 0: return { top: '110%', left: '0' }; // Top-Left
330
- case 1: return { top: '110%', right: '0' }; // Top-Right
331
- case 2: return { bottom: '110%', left: '0' }; // Bottom-Left
332
- case 3: return { bottom: '110%', right: '0' }; // Bottom-Right
333
- default: return {};
334
- }
335
- };
336
-
337
- // --- Lógica de Eventos ---
338
- const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
339
- e.preventDefault();
340
- dragStartRef.current = { x: e.clientX, y: e.clientY, moved: false };
341
- if (indicatorRef.current) {
342
- const rect = indicatorRef.current.getBoundingClientRect();
343
- setPosition({ top: rect.top, left: rect.left });
344
- }
345
- setIsDragging(true);
346
- };
347
-
348
- const handleMouseMove = useCallback((e: MouseEvent) => {
349
- if (!isDragging || !dragStartRef.current) return;
350
227
 
351
- const deltaX = e.clientX - dragStartRef.current.x;
352
- const deltaY = e.clientY - dragStartRef.current.y;
353
-
354
- // Diferencia clique de arrastar (threshold de 5px)
355
- if (!dragStartRef.current.moved && Math.hypot(deltaX, deltaY) > 5) {
356
- dragStartRef.current.moved = true;
357
- }
358
-
359
- if (dragStartRef.current.moved) {
360
- setPosition(prevPos => ({
361
- top: prevPos.top + deltaY,
362
- left: prevPos.left + deltaX,
363
- }));
364
- // Atualiza a referência para o próximo movimento
365
- dragStartRef.current.x = e.clientX;
366
- dragStartRef.current.y = e.clientY;
367
- }
368
- }, [isDragging]);
369
-
370
- const handleMouseUp = useCallback((e: MouseEvent) => {
371
- if (!isDragging) return;
372
- setIsDragging(false);
373
-
374
- // Se moveu, calcula o canto mais próximo
375
- if (dragStartRef.current?.moved) {
376
- const { clientX, clientY } = e;
377
- const w = window.innerWidth;
378
- const h = window.innerHeight;
379
-
380
- const dists = [
381
- Math.hypot(clientX, clientY), // TL
382
- Math.hypot(w - clientX, clientY), // TR
383
- Math.hypot(clientX, h - clientY), // BL
384
- Math.hypot(w - clientX, h - clientY), // BR
385
- ];
386
- setCorner(dists.indexOf(Math.min(...dists)));
387
- }
388
-
389
- dragStartRef.current = null;
390
- }, [isDragging]);
391
-
392
- // Adiciona e remove listeners globais
393
- useEffect(() => {
394
- if (isDragging) {
395
- window.addEventListener('mousemove', handleMouseMove);
396
- window.addEventListener('mouseup', handleMouseUp);
397
- }
398
- return () => {
399
- window.removeEventListener('mousemove', handleMouseMove);
400
- window.removeEventListener('mouseup', handleMouseUp);
401
- };
402
- }, [isDragging, handleMouseMove, handleMouseUp]);
228
+ export function DevIndicator() {
229
+ const [isVisible, setIsVisible] = useState(true);
403
230
 
231
+ if (!isVisible) return null;
404
232
 
405
233
  return (
406
234
  <>
407
235
  <style>
408
236
  {`
409
- @keyframes hweb-pulse {
410
- 0%, 100% {
411
- transform: scale(1);
412
- opacity: 1;
413
- }
414
- 50% {
415
- transform: scale(1.1);
416
- opacity: 0.8;
417
- }
418
- }
419
-
420
- @keyframes hweb-spin {
421
- from {
422
- transform: rotate(0deg);
423
- }
424
- to {
425
- transform: rotate(360deg);
426
- }
427
- }
428
- `}
237
+ @keyframes nyte-pulse {
238
+ 0% { opacity: 0.4; }
239
+ 50% { opacity: 1; }
240
+ 100% { opacity: 0.4; }
241
+ }
242
+ .nyte-dev-badge {
243
+ position: fixed;
244
+ bottom: 20px;
245
+ right: 20px;
246
+ z-index: 999999;
247
+ display: flex;
248
+ align-items: center;
249
+ gap: 12px;
250
+ padding: 8px 14px;
251
+ background: rgba(15, 15, 20, 0.8);
252
+ backdrop-filter: blur(12px);
253
+ -webkit-backdrop-filter: blur(12px);
254
+
255
+ border-radius: 10px;
256
+ color: #fff;
257
+ font-family: 'Inter', ui-monospace, SFMono-Regular, Menlo, monospace;
258
+ font-size: 12px;
259
+ font-weight: 600;
260
+ letter-spacing: 0.05em;
261
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
262
+ transition: all 0.2s ease;
263
+ cursor: default;
264
+ user-select: none;
265
+ }
266
+ .nyte-dev-badge:hover {
267
+ border-color: rgba(142, 45, 226, 0.5);
268
+ transform: translateY(-2px);
269
+ }
270
+ .nyte-status-dot {
271
+ width: 8px;
272
+ height: 8px;
273
+ background: #10b981; /* Verde esmeralda */
274
+ border-radius: 50%;
275
+ box-shadow: 0 0 10px #10b981;
276
+ animation: nyte-pulse 2s infinite ease-in-out;
277
+ }
278
+ .nyte-label {
279
+ color: rgba(255, 255, 255, 0.5);
280
+ text-transform: uppercase;
281
+ font-size: 10px;
282
+ }
283
+ .nyte-logo {
284
+ background: linear-gradient(135deg, #00a3a3, #808080);
285
+ -webkit-background-clip: text;
286
+ -webkit-text-fill-color: transparent;
287
+ font-weight: 800;
288
+ }
289
+ `}
429
290
  </style>
430
- <div
431
- ref={indicatorRef}
432
- style={getIndicatorStyle()}
433
- onMouseDown={handleMouseDown}
434
- title={isBuilding ? "Building..." : "Modo Dev Nyte.js"}
435
- >
436
- {isBuilding ? (
437
- <span style={{ animation: 'hweb-spin 1s linear infinite' }}>⟳</span>
438
- ) : (
439
- 'H'
440
- )}
291
+
292
+ <div className="nyte-dev-badge">
293
+ <div className="nyte-status-dot" />
294
+ <div>
295
+ <span className="nyte-logo">NYTE.JS</span>
296
+ </div>
297
+ <button
298
+ onClick={() => setIsVisible(false)}
299
+ style={{
300
+ background: 'none',
301
+ border: 'none',
302
+ color: 'rgba(255,255,255,0.3)',
303
+ cursor: 'pointer',
304
+ fontSize: '14px',
305
+ padding: '0 0 0 4px',
306
+ marginLeft: '4px'
307
+ }}
308
+ title="Fechar"
309
+ >
310
+ ×
311
+ </button>
441
312
  </div>
442
313
  </>
443
314
  );
package/src/hotReload.ts CHANGED
@@ -219,6 +219,7 @@ export class HotReloadManager {
219
219
 
220
220
  try {
221
221
  this.frontendChangeCallback?.();
222
+ this.backendApiChangeCallback?.();
222
223
  await Promise.race([buildPromise, timeoutPromise]);
223
224
  dm.end(`Build complete for ${path.basename(filePath)}, reloading frontend.`);
224
225
  this.frontendChangeCallback?.();
@@ -235,14 +236,14 @@ export class HotReloadManager {
235
236
 
236
237
  // Se for arquivo de backend, recarrega o módulo e notifica
237
238
  if (isBackendFile) {
238
- Console.logWithout(Levels.INFO, Colors.BgRed,`Reloading backend...`);
239
+ Console.logWithout(Levels.INFO, undefined,`Reloading backend...`);
239
240
  this.backendApiChangeCallback?.();
240
241
  this.notifyClients('backend-api-reload', { file: filePath, event: 'change' });
241
242
  }
242
243
 
243
244
  // Fallback: se não for nem frontend nem backend detectado, recarrega tudo
244
245
  if (!isFrontendFile && !isBackendFile) {
245
- Console.logWithout(Levels.INFO, Colors.BgRed,`Reloading application...`);
246
+ Console.logWithout(Levels.INFO, undefined,`Reloading application...`);
246
247
  this.frontendChangeCallback?.();
247
248
  this.backendApiChangeCallback?.();
248
249
  this.notifyClients('src-reload', { file: filePath, event: 'change' });
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Símbolo único para marcar funções expostas.
3
+ * Usamos Symbol para garantir que não possa ser falsificado via JSON no payload.
4
+ */
5
+ export const RPC_EXPOSED_KEY = Symbol('__rpc_exposed__');
6
+
7
+ type AnyFn = (...args: any[]) => any;
8
+
9
+ /**
10
+ * Marca uma ou mais funções como seguras para RPC.
11
+ */
12
+ export default function Expose<T extends AnyFn>(fn: T): T;
13
+ export default function Expose<T extends AnyFn[]>(fns: [...T]): T;
14
+ export default function Expose<T extends AnyFn[]>(...fns: T): T;
15
+ export default function Expose(...input: any[]): any {
16
+ const fns: AnyFn[] =
17
+ Array.isArray(input[0]) ? input[0] : input;
18
+
19
+ for (const fn of fns) {
20
+ if (typeof fn !== 'function') {
21
+ throw new TypeError('Expose aceita apenas funções');
22
+ }
23
+ (fn as any)[RPC_EXPOSED_KEY] = true;
24
+ }
25
+
26
+ // Retorno:
27
+ // - se veio uma função → retorna ela
28
+ // - se veio lista → retorna a lista
29
+ return input.length === 1 ? input[0] : input;
30
+ }
package/src/rpc/server.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * you may not use this file except in compliance with the License.
7
7
  * You may obtain a copy of the License at
8
8
  *
9
- * http://www.apache.org/licenses/LICENSE-2.0
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
10
  *
11
11
  * Unless required by applicable law or agreed to in writing, software
12
12
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -20,6 +20,8 @@ import path from 'path';
20
20
  import { RpcRequestPayload, RpcResponsePayload } from './types';
21
21
  import { NyteRequest } from '../api/http';
22
22
  import type { GenericRequest } from '../types/framework';
23
+ // Importamos a chave para verificar a anotação de segurança
24
+ import { RPC_EXPOSED_KEY } from './annotations';
23
25
 
24
26
  const DEFAULT_ALLOWED_SERVER_DIRS = ['src/backend'] as const;
25
27
 
@@ -64,7 +66,7 @@ function tryResolveWithinAllowedDirs(projectDir: string, allowedDirs: string[],
64
66
  // Interpret client path as relative to src/web (where it's typically authored)
65
67
  const fromWebAbs = path.resolve(projectDir, 'src/web', req);
66
68
 
67
- // Map: <project>/src/backend/* (coming from ../../backend/* from web code)
69
+ // Map: <project>/src/backend/* (coming from ../../backend/* from web code)
68
70
  const mappedFromWebAbs = fromWebAbs.replace(
69
71
  path.resolve(projectDir, 'backend') + path.sep,
70
72
  path.resolve(projectDir, 'src', 'backend') + path.sep
@@ -180,6 +182,23 @@ export async function executeRpc(ctx: RpcExecutionContext, payload: any): Promis
180
182
  return { success: false, error: `RPC function not found: ${fnName}` };
181
183
  }
182
184
 
185
+ // --- SECURITY CHECK (Expose Annotation or Allowlist) ---
186
+ // 1. Verifica se a função possui o Symbol definido em annotations.ts (Via wrapper Expose())
187
+ const isAnnotated = (fnValue as any)[RPC_EXPOSED_KEY];
188
+
189
+ // 2. Verifica se o módulo exporta uma lista explícita chamada 'exposed' ou 'rpcMethods'
190
+ // Isso permite o uso de "export function" sem wrapper, listando os nomes no final do arquivo.
191
+ const allowList = mod.exposed || mod.rpcMethods;
192
+ const isListed = Array.isArray(allowList) && allowList.includes(fnName);
193
+
194
+ if (!isAnnotated && !isListed) {
195
+ return {
196
+ success: false,
197
+ error: `Function '${fnName}' is not exposed via RPC. Mark it with Expose() or add it to an exported 'exposed' array.`
198
+ };
199
+ }
200
+ // ------------------------------------------
201
+
183
202
  const rpcRequest = ctx.request ? new NyteRequest(ctx.request) : buildRpcRequestFromPayload(payload);
184
203
  const result = await fnValue(rpcRequest, ...payload.args);
185
204
  return { success: true, return: result };
@@ -187,4 +206,4 @@ export async function executeRpc(ctx: RpcExecutionContext, payload: any): Promis
187
206
  const message = typeof err?.message === 'string' ? err.message : 'Unknown RPC error';
188
207
  return { success: false, error: message };
189
208
  }
190
- }
209
+ }