hightjs 1.0.7 → 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,3 +1,4 @@
1
1
  export { Link } from '../components/Link';
2
2
  export { RouteConfig, Metadata } from "../types";
3
3
  export { router } from './clientRouter';
4
+ export { importServer } from './rpc';
@@ -16,9 +16,12 @@
16
16
  * limitations under the License.
17
17
  */
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.router = exports.Link = void 0;
19
+ exports.importServer = exports.router = exports.Link = void 0;
20
20
  // Este arquivo exporta apenas código seguro para o cliente (navegador)
21
21
  var Link_1 = require("../components/Link");
22
22
  Object.defineProperty(exports, "Link", { enumerable: true, get: function () { return Link_1.Link; } });
23
23
  var clientRouter_1 = require("./clientRouter");
24
24
  Object.defineProperty(exports, "router", { enumerable: true, get: function () { return clientRouter_1.router; } });
25
+ // RPC (client-side)
26
+ var rpc_1 = require("./rpc");
27
+ Object.defineProperty(exports, "importServer", { enumerable: true, get: function () { return rpc_1.importServer; } });
@@ -422,7 +422,6 @@ function initializeClient() {
422
422
  return;
423
423
  }
424
424
  const initialData = deobfuscateData(obfuscated);
425
- console.log(initialData);
426
425
  if (!initialData) {
427
426
  console.error('[hweb] Failed to parse initial data.');
428
427
  return;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `importServer("src/backend/index.ts")` returns a Proxy where every property is
3
+ * a function that performs a POST to `/api/rpc`.
4
+ *
5
+ * Security note: the server will still validate allowlisted directories.
6
+ * @param {string} file
7
+ */
8
+ export declare function importServer<T extends Record<string, any> = Record<string, any>>(file: string): T;
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ /*
3
+ * This file is part of the HightJS Project.
4
+ * Copyright (c) 2025 itsmuzin
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.importServer = importServer;
20
+ const types_1 = require("../rpc/types");
21
+ function asErrorMessage(err) {
22
+ if (err instanceof Error)
23
+ return err.message;
24
+ try {
25
+ return String(err);
26
+ }
27
+ catch {
28
+ return 'Unknown error';
29
+ }
30
+ }
31
+ /**
32
+ * `importServer("src/backend/index.ts")` returns a Proxy where every property is
33
+ * a function that performs a POST to `/api/rpc`.
34
+ *
35
+ * Security note: the server will still validate allowlisted directories.
36
+ * @param {string} file
37
+ */
38
+ function importServer(file) {
39
+ if (!file) {
40
+ throw new Error('importServer(file) requires a non-empty string');
41
+ }
42
+ const handler = {
43
+ get(_target, prop) {
44
+ // allow tooling & debugging
45
+ if (prop === '__file')
46
+ return file;
47
+ if (prop === 'then')
48
+ return undefined; // prevent await Proxy issues
49
+ const fnName = String(prop);
50
+ return async (...args) => {
51
+ const payload = {
52
+ file,
53
+ fn: fnName,
54
+ args,
55
+ request: {
56
+ url: typeof window !== 'undefined' ? window.location.href : undefined,
57
+ method: 'RPC',
58
+ headers: {
59
+ // useful for server-side auth/session logic
60
+ ...(typeof navigator !== 'undefined' ? { 'user-agent': navigator.userAgent } : {}),
61
+ // forward cookies so server can reconstruct request.cookies if needed
62
+ ...(typeof document !== 'undefined' ? { cookie: document.cookie } : {})
63
+ }
64
+ }
65
+ };
66
+ let res;
67
+ try {
68
+ res = await fetch(types_1.RPC_ENDPOINT, {
69
+ method: 'POST',
70
+ headers: {
71
+ 'Content-Type': 'application/json'
72
+ },
73
+ body: JSON.stringify(payload)
74
+ });
75
+ }
76
+ catch (err) {
77
+ throw new Error(asErrorMessage(err));
78
+ }
79
+ let data;
80
+ try {
81
+ data = (await res.json());
82
+ }
83
+ catch {
84
+ throw new Error('Invalid JSON response from RPC');
85
+ }
86
+ if (!data || typeof data !== 'object' || typeof data.success !== 'boolean') {
87
+ throw new Error('Invalid RPC response shape');
88
+ }
89
+ if (data.success) {
90
+ return data.return;
91
+ }
92
+ throw new Error(data.error || 'RPC Error');
93
+ };
94
+ }
95
+ };
96
+ return new Proxy({}, handler);
97
+ }
package/dist/index.js CHANGED
@@ -66,6 +66,9 @@ Object.defineProperty(exports, "HightJSResponse", { enumerable: true, get: funct
66
66
  const hotReload_1 = require("./hotReload");
67
67
  const factory_1 = require("./adapters/factory");
68
68
  const console_1 = __importStar(require("./api/console"));
69
+ // RPC
70
+ const server_1 = require("./rpc/server");
71
+ const types_1 = require("./rpc/types");
69
72
  // Exporta os adapters para uso manual se necessário
70
73
  var express_2 = require("./adapters/express");
71
74
  Object.defineProperty(exports, "ExpressAdapter", { enumerable: true, get: function () { return express_2.ExpressAdapter; } });
@@ -330,15 +333,33 @@ function hweb(options) {
330
333
  },
331
334
  getRequestHandler: () => {
332
335
  return async (req, res) => {
333
- // Detecta automaticamente o framework e cria o adapter apropriado
336
+ // Detecta o framework e cria request/response genéricos
334
337
  const adapter = factory_1.FrameworkAdapterFactory.detectFramework(req, res);
335
338
  const genericReq = adapter.parseRequest(req);
336
339
  const genericRes = adapter.createResponse(res);
337
340
  // Adiciona informações do hweb na requisição genérica
338
341
  genericReq.hwebDev = dev;
339
342
  genericReq.hotReloadManager = hotReloadManager;
340
- const { pathname } = new URL(genericReq.url, `http://${genericReq.headers.host || 'localhost'}`);
341
- const method = genericReq.method.toUpperCase();
343
+ const { hostname } = req.headers;
344
+ const method = (genericReq.method || 'GET').toUpperCase();
345
+ const pathname = new URL(genericReq.url, `http://${hostname}:${port}`).pathname;
346
+ // RPC endpoint (antes das rotas de backend)
347
+ if (pathname === types_1.RPC_ENDPOINT && method === 'POST') {
348
+ try {
349
+ const result = await (0, server_1.executeRpc)({
350
+ projectDir: dir,
351
+ request: genericReq
352
+ }, genericReq.body);
353
+ genericRes.header('Content-Type', 'application/json');
354
+ genericRes.status(200).send(JSON.stringify(result));
355
+ return;
356
+ }
357
+ catch (error) {
358
+ genericRes.header('Content-Type', 'application/json');
359
+ genericRes.status(200).send(JSON.stringify({ success: false, error: 'Internal RPC error' }));
360
+ return;
361
+ }
362
+ }
342
363
  // 1. Verifica se é WebSocket upgrade para hot reload
343
364
  if (pathname === '/hweb-hotreload/' && genericReq.headers.upgrade === 'websocket' && hotReloadManager) {
344
365
  // Framework vai chamar o evento 'upgrade' do servidor HTTP
@@ -0,0 +1,11 @@
1
+ import { RpcResponsePayload } from './types';
2
+ import type { GenericRequest } from '../types/framework';
3
+ export interface RpcExecutionContext {
4
+ /** absolute project root (same as options.dir in start()) */
5
+ projectDir: string;
6
+ /** allow override for tests */
7
+ allowedServerDirs?: string[];
8
+ /** real incoming request (so cookies/headers/session work) */
9
+ request?: GenericRequest;
10
+ }
11
+ export declare function executeRpc(ctx: RpcExecutionContext, payload: any): Promise<RpcResponsePayload>;
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ /*
3
+ * This file is part of the HightJS Project.
4
+ * Copyright (c) 2025 itsmuzin
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */
18
+ var __importDefault = (this && this.__importDefault) || function (mod) {
19
+ return (mod && mod.__esModule) ? mod : { "default": mod };
20
+ };
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.executeRpc = executeRpc;
23
+ const fs_1 = __importDefault(require("fs"));
24
+ const path_1 = __importDefault(require("path"));
25
+ const http_1 = require("../api/http");
26
+ const DEFAULT_ALLOWED_SERVER_DIRS = ['src/backend'];
27
+ function normalizeToPosix(p) {
28
+ return p.replace(/\\/g, '/');
29
+ }
30
+ function isDisallowedPathInput(file) {
31
+ if (!file)
32
+ return true;
33
+ if (file.includes('\u0000'))
34
+ return true;
35
+ // disallow protocol-like or absolute paths (Windows drive letters included)
36
+ if (/^[a-zA-Z]+:/.test(file))
37
+ return true;
38
+ if (file.startsWith('/') || file.startsWith('\\'))
39
+ return true;
40
+ return false;
41
+ }
42
+ function validatePayload(payload) {
43
+ return (payload &&
44
+ typeof payload === 'object' &&
45
+ typeof payload.file === 'string' &&
46
+ typeof payload.fn === 'string' &&
47
+ Array.isArray(payload.args));
48
+ }
49
+ function tryResolveWithinAllowedDirs(projectDir, allowedDirs, requestedFile) {
50
+ const req = requestedFile.replace(/\\/g, '/').replace(/^\.(?:\/|\\)/, '');
51
+ for (const d of allowedDirs) {
52
+ const baseAbs = path_1.default.resolve(projectDir, d);
53
+ // Interpret client path as relative to src/web (where it's typically authored)
54
+ const fromWebAbs = path_1.default.resolve(projectDir, 'src/web', req);
55
+ // Map: <project>/src/backend/* (coming from ../../backend/* from web code)
56
+ 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
+ // Also accept callers passing a backend-relative path like "./auth" or "auth"
58
+ const fromBackendAbs = path_1.default.resolve(baseAbs, req);
59
+ const candidateAbsList = [mappedFromWebAbs, fromBackendAbs];
60
+ for (const candidateAbs of candidateAbsList) {
61
+ const baseWithSep = baseAbs.endsWith(path_1.default.sep) ? baseAbs : baseAbs + path_1.default.sep;
62
+ if (!candidateAbs.startsWith(baseWithSep) && candidateAbs !== baseAbs)
63
+ continue;
64
+ if (!fs_1.default.existsSync(baseAbs))
65
+ continue;
66
+ return candidateAbs;
67
+ }
68
+ }
69
+ return null;
70
+ }
71
+ function toCookies(cookieHeader) {
72
+ if (!cookieHeader)
73
+ return {};
74
+ const out = {};
75
+ for (const part of cookieHeader.split(';')) {
76
+ const idx = part.indexOf('=');
77
+ if (idx === -1)
78
+ continue;
79
+ const k = part.slice(0, idx).trim();
80
+ const v = part.slice(idx + 1).trim();
81
+ if (k)
82
+ out[k] = v;
83
+ }
84
+ return out;
85
+ }
86
+ // Keep this for fallback when ctx.request isn't provided
87
+ function buildRpcRequestFromPayload(payload) {
88
+ const headers = {};
89
+ const rawHeaders = payload.request?.headers || {};
90
+ for (const [k, v] of Object.entries(rawHeaders)) {
91
+ headers[k.toLowerCase()] = v;
92
+ }
93
+ const cookieHeader = headers['cookie'] ?? undefined;
94
+ const req = {
95
+ method: payload.request?.method || 'RPC',
96
+ // HightJSRequest validates URLs; keep it safe & local.
97
+ url: payload.request?.url || 'http://localhost/_rpc',
98
+ headers,
99
+ body: null,
100
+ query: {},
101
+ params: {},
102
+ cookies: payload.request?.cookies || toCookies(cookieHeader),
103
+ raw: null
104
+ };
105
+ return new http_1.HightJSRequest(req);
106
+ }
107
+ async function executeRpc(ctx, payload) {
108
+ try {
109
+ if (!validatePayload(payload)) {
110
+ return { success: false, error: 'Invalid RPC payload' };
111
+ }
112
+ const fileInput = payload.file;
113
+ if (isDisallowedPathInput(fileInput)) {
114
+ return { success: false, error: 'Invalid file path' };
115
+ }
116
+ const allowedDirs = (ctx.allowedServerDirs || [...DEFAULT_ALLOWED_SERVER_DIRS]).map(normalizeToPosix);
117
+ const absFile = tryResolveWithinAllowedDirs(ctx.projectDir, allowedDirs, fileInput);
118
+ if (!absFile) {
119
+ return { success: false, error: 'File not allowed for RPC' };
120
+ }
121
+ if (!absFile.startsWith(path_1.default.resolve(ctx.projectDir) + path_1.default.sep)) {
122
+ return { success: false, error: 'Invalid file path' };
123
+ }
124
+ // Ensure fresh code in dev
125
+ try {
126
+ const resolved = require.resolve(absFile);
127
+ if (require.cache[resolved])
128
+ delete require.cache[resolved];
129
+ }
130
+ catch {
131
+ // ignore
132
+ }
133
+ let mod;
134
+ try {
135
+ mod = require(absFile);
136
+ }
137
+ catch {
138
+ // try again letting require do extension resolution from absFile without explicit extension
139
+ try {
140
+ mod = require(absFile);
141
+ }
142
+ catch {
143
+ return { success: false, error: 'RPC file not found' };
144
+ }
145
+ }
146
+ // Support multiple TS/CJS interop shapes:
147
+ // - module.exports.fn
148
+ // - exports.fn
149
+ // - module.exports.default (function)
150
+ // - module.exports.default.fn
151
+ const fnName = payload.fn;
152
+ const fnValue = (mod && typeof mod[fnName] === 'function' && mod[fnName]) ||
153
+ (mod?.default && typeof mod.default === 'function' && fnName === 'default' && mod.default) ||
154
+ (mod?.default && typeof mod.default[fnName] === 'function' && mod.default[fnName]) ||
155
+ undefined;
156
+ if (typeof fnValue !== 'function') {
157
+ return { success: false, error: `RPC function not found: ${fnName}` };
158
+ }
159
+ const rpcRequest = ctx.request ? new http_1.HightJSRequest(ctx.request) : buildRpcRequestFromPayload(payload);
160
+ const result = await fnValue(rpcRequest, ...payload.args);
161
+ return { success: true, return: result };
162
+ }
163
+ catch (err) {
164
+ const message = typeof err?.message === 'string' ? err.message : 'Unknown RPC error';
165
+ return { success: false, error: message };
166
+ }
167
+ }
@@ -0,0 +1,23 @@
1
+ export declare const RPC_ENDPOINT: "/api/rpc";
2
+ export type RpcArgs = unknown[];
3
+ export interface RpcRequestPayload {
4
+ file: string;
5
+ fn: string;
6
+ args: RpcArgs;
7
+ /** Optional: minimal request data so server can provide a HightJSRequest to the called function */
8
+ request?: {
9
+ url?: string;
10
+ method?: string;
11
+ headers?: Record<string, string>;
12
+ cookies?: Record<string, string>;
13
+ };
14
+ }
15
+ export type RpcSuccessResponse = {
16
+ success: true;
17
+ return: unknown;
18
+ };
19
+ export type RpcErrorResponse = {
20
+ success: false;
21
+ error: string;
22
+ };
23
+ export type RpcResponsePayload = RpcSuccessResponse | RpcErrorResponse;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ /*
3
+ * This file is part of the HightJS Project.
4
+ * Copyright (c) 2025 itsmuzin
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.RPC_ENDPOINT = void 0;
20
+ exports.RPC_ENDPOINT = '/api/rpc';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hightjs",
3
- "version": "1.0.7",
3
+ "version": "1.1.0",
4
4
  "description": "HightJS 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",
@@ -72,11 +72,15 @@
72
72
  "@types/react": "^19.2.0",
73
73
  "@types/react-dom": "^19.2.0",
74
74
  "@types/ws": "^8.18.1",
75
+ "jest": "^30.1.3",
76
+ "ts-jest": "^29.4.4",
77
+ "@types/jest": "^30.0.0",
75
78
  "ts-loader": "^9.5.4",
76
79
  "ts-node": "^10.9.2",
77
80
  "typescript": "^5.9.3"
78
81
  },
79
82
  "scripts": {
80
- "build": "tsc"
83
+ "build": "tsc",
84
+ "test": "jest"
81
85
  }
82
86
  }
@@ -20,6 +20,5 @@ export { Link } from '../components/Link';
20
20
  export { RouteConfig, Metadata } from "../types";
21
21
  export { router } from './clientRouter';
22
22
 
23
-
24
-
25
-
23
+ // RPC (client-side)
24
+ export { importServer } from './rpc';
@@ -481,7 +481,7 @@ function initializeClient() {
481
481
  }
482
482
 
483
483
  const initialData = deobfuscateData(obfuscated);
484
- console.log(initialData)
484
+
485
485
  if (!initialData) {
486
486
  console.error('[hweb] Failed to parse initial data.');
487
487
  return;
@@ -0,0 +1,101 @@
1
+ /*
2
+ * This file is part of the HightJS Project.
3
+ * Copyright (c) 2025 itsmuzin
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ import type { RpcRequestPayload, RpcResponsePayload } from '../rpc/types';
19
+ import { RPC_ENDPOINT } from '../rpc/types';
20
+
21
+ function asErrorMessage(err: unknown): string {
22
+ if (err instanceof Error) return err.message;
23
+ try {
24
+ return String(err);
25
+ } catch {
26
+ return 'Unknown error';
27
+ }
28
+ }
29
+
30
+ /**
31
+ * `importServer("src/backend/index.ts")` returns a Proxy where every property is
32
+ * a function that performs a POST to `/api/rpc`.
33
+ *
34
+ * Security note: the server will still validate allowlisted directories.
35
+ * @param {string} file
36
+ */
37
+ export function importServer<T extends Record<string, any> = Record<string, any>>(file: string): T {
38
+ if (!file) {
39
+ throw new Error('importServer(file) requires a non-empty string');
40
+ }
41
+
42
+ const handler: ProxyHandler<any> = {
43
+ get(_target, prop) {
44
+ // allow tooling & debugging
45
+ if (prop === '__file') return file;
46
+ if (prop === 'then') return undefined; // prevent await Proxy issues
47
+
48
+ const fnName = String(prop);
49
+
50
+ return async (...args: any[]) => {
51
+ const payload: RpcRequestPayload = {
52
+ file,
53
+ fn: fnName,
54
+ args,
55
+ request: {
56
+ url: typeof window !== 'undefined' ? window.location.href : undefined,
57
+ method: 'RPC',
58
+ headers: {
59
+ // useful for server-side auth/session logic
60
+ ...(typeof navigator !== 'undefined' ? { 'user-agent': navigator.userAgent } : {}),
61
+ // forward cookies so server can reconstruct request.cookies if needed
62
+ ...(typeof document !== 'undefined' ? { cookie: document.cookie } : {})
63
+ }
64
+ }
65
+ };
66
+
67
+ let res: Response;
68
+ try {
69
+ res = await fetch(RPC_ENDPOINT, {
70
+ method: 'POST',
71
+ headers: {
72
+ 'Content-Type': 'application/json'
73
+ },
74
+ body: JSON.stringify(payload)
75
+ });
76
+ } catch (err) {
77
+ throw new Error(asErrorMessage(err));
78
+ }
79
+
80
+ let data: RpcResponsePayload;
81
+ try {
82
+ data = (await res.json()) as RpcResponsePayload;
83
+ } catch {
84
+ throw new Error('Invalid JSON response from RPC');
85
+ }
86
+
87
+ if (!data || typeof data !== 'object' || typeof (data as any).success !== 'boolean') {
88
+ throw new Error('Invalid RPC response shape');
89
+ }
90
+
91
+ if (data.success) {
92
+ return (data as any).return;
93
+ }
94
+
95
+ throw new Error((data as any).error || 'RPC Error');
96
+ };
97
+ }
98
+ };
99
+
100
+ return new Proxy({}, handler) as T;
101
+ }
package/src/index.ts CHANGED
@@ -46,6 +46,10 @@ import {FrameworkAdapterFactory} from './adapters/factory';
46
46
  import {GenericRequest, GenericResponse} from './types/framework';
47
47
  import Console, {Colors} from "./api/console"
48
48
 
49
+ // RPC
50
+ import { executeRpc } from './rpc/server';
51
+ import { RPC_ENDPOINT } from './rpc/types';
52
+
49
53
  // Exporta apenas os tipos e classes para o backend
50
54
  export { HightJSRequest, HightJSResponse };
51
55
  export type { BackendRouteConfig, BackendHandler };
@@ -369,17 +373,38 @@ export default function hweb(options: HightJSOptions) {
369
373
  },
370
374
  getRequestHandler: (): RequestHandler => {
371
375
  return async (req: any, res: any) => {
372
- // Detecta automaticamente o framework e cria o adapter apropriado
376
+ // Detecta o framework e cria request/response genéricos
373
377
  const adapter = FrameworkAdapterFactory.detectFramework(req, res);
374
- const genericReq = adapter.parseRequest(req);
375
- const genericRes = adapter.createResponse(res);
378
+ const genericReq: GenericRequest = adapter.parseRequest(req);
379
+ const genericRes: GenericResponse = adapter.createResponse(res);
376
380
 
377
381
  // Adiciona informações do hweb na requisição genérica
378
382
  (genericReq as any).hwebDev = dev;
379
383
  (genericReq as any).hotReloadManager = hotReloadManager;
380
384
 
381
- const {pathname} = new URL(genericReq.url, `http://${genericReq.headers.host || 'localhost'}`);
382
- const method = genericReq.method.toUpperCase();
385
+ const {hostname} = req.headers;
386
+ const method = (genericReq.method || 'GET').toUpperCase();
387
+ const pathname = new URL(genericReq.url, `http://${hostname}:${port}`).pathname;
388
+
389
+ // RPC endpoint (antes das rotas de backend)
390
+ if (pathname === RPC_ENDPOINT && method === 'POST') {
391
+ try {
392
+ const result = await executeRpc(
393
+ {
394
+ projectDir: dir,
395
+ request: genericReq
396
+ },
397
+ genericReq.body
398
+ );
399
+ genericRes.header('Content-Type', 'application/json');
400
+ genericRes.status(200).send(JSON.stringify(result));
401
+ return;
402
+ } catch (error) {
403
+ genericRes.header('Content-Type', 'application/json');
404
+ genericRes.status(200).send(JSON.stringify({ success: false, error: 'Internal RPC error' }));
405
+ return;
406
+ }
407
+ }
383
408
 
384
409
  // 1. Verifica se é WebSocket upgrade para hot reload
385
410
  if (pathname === '/hweb-hotreload/' && genericReq.headers.upgrade === 'websocket' && hotReloadManager) {
@@ -0,0 +1,191 @@
1
+ /*
2
+ * This file is part of the HightJS Project.
3
+ * Copyright (c) 2025 itsmuzin
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ import fs from 'fs';
19
+ import path from 'path';
20
+ import { RpcRequestPayload, RpcResponsePayload } from './types';
21
+ import { HightJSRequest } from '../api/http';
22
+ import type { GenericRequest } from '../types/framework';
23
+
24
+ const DEFAULT_ALLOWED_SERVER_DIRS = ['src/backend'] as const;
25
+
26
+ function normalizeToPosix(p: string): string {
27
+ return p.replace(/\\/g, '/');
28
+ }
29
+
30
+ function isDisallowedPathInput(file: string): boolean {
31
+ if (!file) return true;
32
+ if (file.includes('\u0000')) return true;
33
+ // disallow protocol-like or absolute paths (Windows drive letters included)
34
+ if (/^[a-zA-Z]+:/.test(file)) return true;
35
+ if (file.startsWith('/') || file.startsWith('\\')) return true;
36
+ return false;
37
+ }
38
+
39
+ function validatePayload(payload: any): payload is RpcRequestPayload {
40
+ return (
41
+ payload &&
42
+ typeof payload === 'object' &&
43
+ typeof payload.file === 'string' &&
44
+ typeof payload.fn === 'string' &&
45
+ Array.isArray(payload.args)
46
+ );
47
+ }
48
+
49
+ export interface RpcExecutionContext {
50
+ /** absolute project root (same as options.dir in start()) */
51
+ projectDir: string;
52
+ /** allow override for tests */
53
+ allowedServerDirs?: string[];
54
+ /** real incoming request (so cookies/headers/session work) */
55
+ request?: GenericRequest;
56
+ }
57
+
58
+ function tryResolveWithinAllowedDirs(projectDir: string, allowedDirs: string[], requestedFile: string): string | null {
59
+ const req = requestedFile.replace(/\\/g, '/').replace(/^\.(?:\/|\\)/, '');
60
+
61
+ for (const d of allowedDirs) {
62
+ const baseAbs = path.resolve(projectDir, d);
63
+
64
+ // Interpret client path as relative to src/web (where it's typically authored)
65
+ const fromWebAbs = path.resolve(projectDir, 'src/web', req);
66
+
67
+ // Map: <project>/src/backend/* (coming from ../../backend/* from web code)
68
+ const mappedFromWebAbs = fromWebAbs.replace(
69
+ path.resolve(projectDir, 'backend') + path.sep,
70
+ path.resolve(projectDir, 'src', 'backend') + path.sep
71
+ );
72
+
73
+ // Also accept callers passing a backend-relative path like "./auth" or "auth"
74
+ const fromBackendAbs = path.resolve(baseAbs, req);
75
+
76
+ const candidateAbsList = [mappedFromWebAbs, fromBackendAbs];
77
+
78
+ for (const candidateAbs of candidateAbsList) {
79
+ const baseWithSep = baseAbs.endsWith(path.sep) ? baseAbs : baseAbs + path.sep;
80
+ if (!candidateAbs.startsWith(baseWithSep) && candidateAbs !== baseAbs) continue;
81
+ if (!fs.existsSync(baseAbs)) continue;
82
+ return candidateAbs;
83
+ }
84
+ }
85
+
86
+ return null;
87
+ }
88
+
89
+ function toCookies(cookieHeader: string | undefined): Record<string, string> {
90
+ if (!cookieHeader) return {};
91
+ const out: Record<string, string> = {};
92
+ for (const part of cookieHeader.split(';')) {
93
+ const idx = part.indexOf('=');
94
+ if (idx === -1) continue;
95
+ const k = part.slice(0, idx).trim();
96
+ const v = part.slice(idx + 1).trim();
97
+ if (k) out[k] = v;
98
+ }
99
+ return out;
100
+ }
101
+
102
+ // Keep this for fallback when ctx.request isn't provided
103
+ function buildRpcRequestFromPayload(payload: RpcRequestPayload): HightJSRequest {
104
+ const headers: Record<string, string | string[]> = {};
105
+ const rawHeaders = payload.request?.headers || {};
106
+ for (const [k, v] of Object.entries(rawHeaders)) {
107
+ headers[k.toLowerCase()] = v;
108
+ }
109
+
110
+ const cookieHeader = (headers['cookie'] as string | undefined) ?? undefined;
111
+
112
+ const req: GenericRequest = {
113
+ method: payload.request?.method || 'RPC',
114
+ // HightJSRequest validates URLs; keep it safe & local.
115
+ url: payload.request?.url || 'http://localhost/_rpc',
116
+ headers,
117
+ body: null,
118
+ query: {},
119
+ params: {},
120
+ cookies: payload.request?.cookies || toCookies(cookieHeader),
121
+ raw: null
122
+ };
123
+
124
+ return new HightJSRequest(req);
125
+ }
126
+
127
+ export async function executeRpc(ctx: RpcExecutionContext, payload: any): Promise<RpcResponsePayload> {
128
+ try {
129
+ if (!validatePayload(payload)) {
130
+ return { success: false, error: 'Invalid RPC payload' };
131
+ }
132
+
133
+ const fileInput = payload.file;
134
+ if (isDisallowedPathInput(fileInput)) {
135
+ return { success: false, error: 'Invalid file path' };
136
+ }
137
+
138
+ const allowedDirs = (ctx.allowedServerDirs || [...DEFAULT_ALLOWED_SERVER_DIRS]).map(normalizeToPosix);
139
+ const absFile = tryResolveWithinAllowedDirs(ctx.projectDir, allowedDirs, fileInput);
140
+ if (!absFile) {
141
+ return { success: false, error: 'File not allowed for RPC' };
142
+ }
143
+
144
+ if (!absFile.startsWith(path.resolve(ctx.projectDir) + path.sep)) {
145
+ return { success: false, error: 'Invalid file path' };
146
+ }
147
+
148
+ // Ensure fresh code in dev
149
+ try {
150
+ const resolved = require.resolve(absFile);
151
+ if (require.cache[resolved]) delete require.cache[resolved];
152
+ } catch {
153
+ // ignore
154
+ }
155
+
156
+ let mod: any;
157
+ try {
158
+ mod = require(absFile);
159
+ } catch {
160
+ // try again letting require do extension resolution from absFile without explicit extension
161
+ try {
162
+ mod = require(absFile);
163
+ } catch {
164
+ return { success: false, error: 'RPC file not found' };
165
+ }
166
+ }
167
+
168
+ // Support multiple TS/CJS interop shapes:
169
+ // - module.exports.fn
170
+ // - exports.fn
171
+ // - module.exports.default (function)
172
+ // - module.exports.default.fn
173
+ const fnName = payload.fn;
174
+ const fnValue =
175
+ (mod && typeof mod[fnName] === 'function' && mod[fnName]) ||
176
+ (mod?.default && typeof mod.default === 'function' && fnName === 'default' && mod.default) ||
177
+ (mod?.default && typeof mod.default[fnName] === 'function' && mod.default[fnName]) ||
178
+ undefined;
179
+
180
+ if (typeof fnValue !== 'function') {
181
+ return { success: false, error: `RPC function not found: ${fnName}` };
182
+ }
183
+
184
+ const rpcRequest = ctx.request ? new HightJSRequest(ctx.request) : buildRpcRequestFromPayload(payload);
185
+ const result = await fnValue(rpcRequest, ...payload.args);
186
+ return { success: true, return: result };
187
+ } catch (err: any) {
188
+ const message = typeof err?.message === 'string' ? err.message : 'Unknown RPC error';
189
+ return { success: false, error: message };
190
+ }
191
+ }
@@ -0,0 +1,45 @@
1
+ /*
2
+ * This file is part of the HightJS Project.
3
+ * Copyright (c) 2025 itsmuzin
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ export const RPC_ENDPOINT = '/api/rpc' as const;
19
+
20
+ export type RpcArgs = unknown[];
21
+
22
+ export interface RpcRequestPayload {
23
+ file: string;
24
+ fn: string;
25
+ args: RpcArgs;
26
+ /** Optional: minimal request data so server can provide a HightJSRequest to the called function */
27
+ request?: {
28
+ url?: string;
29
+ method?: string;
30
+ headers?: Record<string, string>;
31
+ cookies?: Record<string, string>;
32
+ };
33
+ }
34
+
35
+ export type RpcSuccessResponse = {
36
+ success: true;
37
+ return: unknown;
38
+ };
39
+
40
+ export type RpcErrorResponse = {
41
+ success: false;
42
+ error: string;
43
+ };
44
+
45
+ export type RpcResponsePayload = RpcSuccessResponse | RpcErrorResponse;