@fynixorg/ui 1.0.13 → 1.0.15
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/dist/hooks/nixAsync.d.ts +7 -1
- package/dist/hooks/nixAsync.d.ts.map +1 -1
- package/dist/hooks/nixAsync.js +84 -10
- package/dist/hooks/nixAsync.js.map +2 -2
- package/dist/hooks/nixAsyncCache.d.ts +6 -1
- package/dist/hooks/nixAsyncCache.d.ts.map +1 -1
- package/dist/hooks/nixAsyncCache.js +104 -26
- package/dist/hooks/nixAsyncCache.js.map +3 -3
- package/dist/hooks/nixComputed.d.ts.map +1 -1
- package/dist/hooks/nixComputed.js +4 -1
- package/dist/hooks/nixComputed.js.map +2 -2
- package/dist/hooks/nixEffect.d.ts.map +1 -1
- package/dist/hooks/nixEffect.js.map +2 -2
- package/dist/hooks/nixLocalStorage.d.ts +4 -1
- package/dist/hooks/nixLocalStorage.d.ts.map +1 -1
- package/dist/hooks/nixLocalStorage.js +118 -8
- package/dist/hooks/nixLocalStorage.js.map +2 -2
- package/dist/hooks/nixStore.d.ts +3 -0
- package/dist/hooks/nixStore.d.ts.map +1 -1
- package/dist/hooks/nixStore.js +57 -4
- package/dist/hooks/nixStore.js.map +2 -2
- package/dist/package.json +1 -1
- package/dist/plugins/vite-plugin-res.d.ts.map +1 -1
- package/dist/plugins/vite-plugin-res.js +248 -47
- package/dist/plugins/vite-plugin-res.js.map +3 -3
- package/dist/router/router.d.ts +13 -0
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +324 -16
- package/dist/router/router.js.map +2 -2
- package/dist/runtime.d.ts +62 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +489 -13
- package/dist/runtime.js.map +2 -2
- package/package.json +1 -1
package/dist/runtime.js
CHANGED
|
@@ -22,6 +22,387 @@ import { nixRef } from "./hooks/nixRef";
|
|
|
22
22
|
import { nixState } from "./hooks/nixState";
|
|
23
23
|
import { nixStore } from "./hooks/nixStore";
|
|
24
24
|
import createFynix from "./router/router";
|
|
25
|
+
class SimplePriorityQueue {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.items = [];
|
|
28
|
+
this.priorityOrder = {
|
|
29
|
+
immediate: 0,
|
|
30
|
+
high: 1,
|
|
31
|
+
normal: 2,
|
|
32
|
+
low: 3,
|
|
33
|
+
idle: 4,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
push(item, priority) {
|
|
37
|
+
this.items.push({ item, priority });
|
|
38
|
+
this.items.sort((a, b) => this.priorityOrder[a.priority] - this.priorityOrder[b.priority]);
|
|
39
|
+
}
|
|
40
|
+
pop() {
|
|
41
|
+
return this.items.shift()?.item;
|
|
42
|
+
}
|
|
43
|
+
peek() {
|
|
44
|
+
return this.items[0]?.item;
|
|
45
|
+
}
|
|
46
|
+
size() {
|
|
47
|
+
return this.items.length;
|
|
48
|
+
}
|
|
49
|
+
isEmpty() {
|
|
50
|
+
return this.items.length === 0;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
class FynixScheduler {
|
|
54
|
+
constructor() {
|
|
55
|
+
this.updateQueue = new SimplePriorityQueue();
|
|
56
|
+
this.batchedUpdates = new Set();
|
|
57
|
+
this.isScheduled = false;
|
|
58
|
+
this.isWorking = false;
|
|
59
|
+
this.currentPriority = "normal";
|
|
60
|
+
this.updateIdCounter = 0;
|
|
61
|
+
}
|
|
62
|
+
schedule(update, priority = "normal") {
|
|
63
|
+
update.id = `update_${this.updateIdCounter++}`;
|
|
64
|
+
update.priority = priority;
|
|
65
|
+
update.timestamp = performance.now();
|
|
66
|
+
if (priority === "immediate") {
|
|
67
|
+
this.flushUpdate(update);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
this.updateQueue.push(update, priority);
|
|
71
|
+
this.scheduleWork();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
batchUpdates(updates) {
|
|
75
|
+
updates.forEach((update) => this.batchedUpdates.add(update));
|
|
76
|
+
this.scheduleWork();
|
|
77
|
+
}
|
|
78
|
+
timeSlice(deadline) {
|
|
79
|
+
const startTime = performance.now();
|
|
80
|
+
const previousPriority = this.currentPriority;
|
|
81
|
+
while (!this.updateQueue.isEmpty() &&
|
|
82
|
+
performance.now() - startTime < deadline) {
|
|
83
|
+
const update = this.updateQueue.pop();
|
|
84
|
+
if (update) {
|
|
85
|
+
if (this.shouldYield() && update.priority !== "immediate") {
|
|
86
|
+
this.updateQueue.push(update, update.priority);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
this.flushUpdate(update);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
this.currentPriority = previousPriority;
|
|
93
|
+
return this.updateQueue.isEmpty();
|
|
94
|
+
}
|
|
95
|
+
flush() {
|
|
96
|
+
if (this.isWorking)
|
|
97
|
+
return;
|
|
98
|
+
this.isWorking = true;
|
|
99
|
+
try {
|
|
100
|
+
while (!this.updateQueue.isEmpty()) {
|
|
101
|
+
const update = this.updateQueue.peek();
|
|
102
|
+
if (update && update.priority === "immediate") {
|
|
103
|
+
this.flushUpdate(this.updateQueue.pop());
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
this.batchedUpdates.forEach((update) => this.flushUpdate(update));
|
|
110
|
+
this.batchedUpdates.clear();
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
this.isWorking = false;
|
|
114
|
+
this.isScheduled = false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
flushUpdate(update) {
|
|
118
|
+
const previousPriority = this.currentPriority;
|
|
119
|
+
this.currentPriority = update.priority;
|
|
120
|
+
try {
|
|
121
|
+
update.callback();
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
console.error("[FynixScheduler] Update error:", error);
|
|
125
|
+
showErrorOverlay(error);
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
this.currentPriority = previousPriority;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
scheduleWork() {
|
|
132
|
+
if (this.isScheduled)
|
|
133
|
+
return;
|
|
134
|
+
this.isScheduled = true;
|
|
135
|
+
const nextUpdate = this.updateQueue.peek();
|
|
136
|
+
if (nextUpdate) {
|
|
137
|
+
if (nextUpdate.priority === "high") {
|
|
138
|
+
requestAnimationFrame(() => this.workLoop(16.67));
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
if ("requestIdleCallback" in window) {
|
|
142
|
+
requestIdleCallback((deadline) => {
|
|
143
|
+
this.workLoop(deadline.timeRemaining());
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
setTimeout(() => this.workLoop(5), 0);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
workLoop(deadline) {
|
|
153
|
+
const hasMoreWork = !this.timeSlice(deadline);
|
|
154
|
+
if (hasMoreWork) {
|
|
155
|
+
this.isScheduled = false;
|
|
156
|
+
this.scheduleWork();
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
this.flush();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
getCurrentPriority() {
|
|
163
|
+
return this.currentPriority;
|
|
164
|
+
}
|
|
165
|
+
shouldYield() {
|
|
166
|
+
const nextUpdate = this.updateQueue.peek();
|
|
167
|
+
if (!nextUpdate)
|
|
168
|
+
return false;
|
|
169
|
+
const currentPriorityLevel = this.getPriorityLevel(this.currentPriority);
|
|
170
|
+
const nextPriorityLevel = this.getPriorityLevel(nextUpdate.priority);
|
|
171
|
+
return nextPriorityLevel < currentPriorityLevel;
|
|
172
|
+
}
|
|
173
|
+
getPriorityLevel(priority) {
|
|
174
|
+
const levels = { immediate: 0, high: 1, normal: 2, low: 3, idle: 4 };
|
|
175
|
+
return levels[priority];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const scheduler = new FynixScheduler();
|
|
179
|
+
class FiberRenderer {
|
|
180
|
+
constructor() {
|
|
181
|
+
this.workInProgressRoot = null;
|
|
182
|
+
this.nextUnitOfWork = null;
|
|
183
|
+
this.currentRoot = null;
|
|
184
|
+
this.deletions = [];
|
|
185
|
+
}
|
|
186
|
+
scheduleWork(fiber) {
|
|
187
|
+
this.workInProgressRoot = {
|
|
188
|
+
...fiber,
|
|
189
|
+
alternate: this.currentRoot,
|
|
190
|
+
};
|
|
191
|
+
this.nextUnitOfWork = this.workInProgressRoot;
|
|
192
|
+
this.deletions = [];
|
|
193
|
+
scheduler.schedule({
|
|
194
|
+
id: "",
|
|
195
|
+
type: "layout",
|
|
196
|
+
priority: "high",
|
|
197
|
+
callback: () => this.workLoop(5),
|
|
198
|
+
timestamp: performance.now(),
|
|
199
|
+
}, "high");
|
|
200
|
+
}
|
|
201
|
+
workLoop(deadline) {
|
|
202
|
+
const startTime = performance.now();
|
|
203
|
+
while (this.nextUnitOfWork && performance.now() - startTime < deadline) {
|
|
204
|
+
this.nextUnitOfWork = this.performUnitOfWork(this.nextUnitOfWork);
|
|
205
|
+
}
|
|
206
|
+
if (!this.nextUnitOfWork && this.workInProgressRoot) {
|
|
207
|
+
this.commitRoot();
|
|
208
|
+
}
|
|
209
|
+
else if (this.nextUnitOfWork) {
|
|
210
|
+
scheduler.schedule({
|
|
211
|
+
id: "",
|
|
212
|
+
type: "layout",
|
|
213
|
+
priority: "normal",
|
|
214
|
+
callback: () => this.workLoop(5),
|
|
215
|
+
timestamp: performance.now(),
|
|
216
|
+
}, "normal");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
performUnitOfWork(fiber) {
|
|
220
|
+
this.reconcileChildren(fiber, fiber.props?.children || []);
|
|
221
|
+
if (fiber.child) {
|
|
222
|
+
return fiber.child;
|
|
223
|
+
}
|
|
224
|
+
let nextFiber = fiber;
|
|
225
|
+
while (nextFiber) {
|
|
226
|
+
if (nextFiber.sibling) {
|
|
227
|
+
return nextFiber.sibling;
|
|
228
|
+
}
|
|
229
|
+
nextFiber = nextFiber.parent;
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
reconcileChildren(wipFiber, elements) {
|
|
234
|
+
let index = 0;
|
|
235
|
+
let oldFiber = wipFiber.alternate?.child;
|
|
236
|
+
let prevSibling = null;
|
|
237
|
+
while (index < elements.length || oldFiber != null) {
|
|
238
|
+
const element = elements[index];
|
|
239
|
+
let newFiber = null;
|
|
240
|
+
const sameType = oldFiber && element && element.type === oldFiber.type;
|
|
241
|
+
if (sameType && oldFiber) {
|
|
242
|
+
newFiber = {
|
|
243
|
+
type: oldFiber.type,
|
|
244
|
+
props: element.props,
|
|
245
|
+
key: element.key,
|
|
246
|
+
_domNode: oldFiber._domNode,
|
|
247
|
+
parent: wipFiber,
|
|
248
|
+
alternate: oldFiber,
|
|
249
|
+
effectTag: "UPDATE",
|
|
250
|
+
updatePriority: "normal",
|
|
251
|
+
child: null,
|
|
252
|
+
sibling: null,
|
|
253
|
+
_rendered: null,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (element && !sameType) {
|
|
257
|
+
newFiber = {
|
|
258
|
+
type: element.type,
|
|
259
|
+
props: element.props,
|
|
260
|
+
key: element.key,
|
|
261
|
+
_domNode: null,
|
|
262
|
+
parent: wipFiber,
|
|
263
|
+
alternate: null,
|
|
264
|
+
effectTag: "PLACEMENT",
|
|
265
|
+
updatePriority: "normal",
|
|
266
|
+
child: null,
|
|
267
|
+
sibling: null,
|
|
268
|
+
_rendered: null,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (oldFiber && !sameType) {
|
|
272
|
+
oldFiber.effectTag = "DELETION";
|
|
273
|
+
this.deletions.push(oldFiber);
|
|
274
|
+
}
|
|
275
|
+
if (oldFiber) {
|
|
276
|
+
oldFiber = oldFiber.sibling;
|
|
277
|
+
}
|
|
278
|
+
if (index === 0) {
|
|
279
|
+
wipFiber.child = newFiber;
|
|
280
|
+
}
|
|
281
|
+
else if (newFiber && prevSibling) {
|
|
282
|
+
prevSibling.sibling = newFiber;
|
|
283
|
+
}
|
|
284
|
+
prevSibling = newFiber;
|
|
285
|
+
index++;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
commitRoot() {
|
|
289
|
+
this.deletions.forEach((fiber) => this.commitWork(fiber));
|
|
290
|
+
if (this.workInProgressRoot?.child) {
|
|
291
|
+
this.commitWork(this.workInProgressRoot.child);
|
|
292
|
+
}
|
|
293
|
+
this.currentRoot = this.workInProgressRoot;
|
|
294
|
+
this.workInProgressRoot = null;
|
|
295
|
+
}
|
|
296
|
+
commitWork(fiber) {
|
|
297
|
+
if (!fiber)
|
|
298
|
+
return;
|
|
299
|
+
let domParentFiber = fiber.parent;
|
|
300
|
+
while (!domParentFiber?._domNode) {
|
|
301
|
+
domParentFiber = domParentFiber?.parent || null;
|
|
302
|
+
}
|
|
303
|
+
const domParent = domParentFiber?._domNode;
|
|
304
|
+
if (fiber.effectTag === "PLACEMENT" && fiber._domNode && domParent) {
|
|
305
|
+
domParent.appendChild(fiber._domNode);
|
|
306
|
+
}
|
|
307
|
+
else if (fiber.effectTag === "UPDATE" && fiber._domNode) {
|
|
308
|
+
this.updateDom(fiber._domNode, fiber.alternate?.props || {}, fiber.props);
|
|
309
|
+
}
|
|
310
|
+
else if (fiber.effectTag === "DELETION" && domParent) {
|
|
311
|
+
this.commitDeletion(fiber, domParent);
|
|
312
|
+
}
|
|
313
|
+
this.commitWork(fiber.child);
|
|
314
|
+
this.commitWork(fiber.sibling);
|
|
315
|
+
}
|
|
316
|
+
commitDeletion(fiber, domParent) {
|
|
317
|
+
if (fiber._domNode) {
|
|
318
|
+
domParent.removeChild(fiber._domNode);
|
|
319
|
+
}
|
|
320
|
+
else if (fiber.child) {
|
|
321
|
+
this.commitDeletion(fiber.child, domParent);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
updateDom(dom, prevProps, nextProps) {
|
|
325
|
+
Object.keys(prevProps)
|
|
326
|
+
.filter((key) => key !== "children" && !(key in nextProps))
|
|
327
|
+
.forEach((name) => {
|
|
328
|
+
if (name.startsWith("on")) {
|
|
329
|
+
const eventType = name.toLowerCase().substring(2);
|
|
330
|
+
dom.removeEventListener(eventType, prevProps[name]);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
dom[name] = "";
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
Object.keys(nextProps)
|
|
337
|
+
.filter((key) => key !== "children")
|
|
338
|
+
.forEach((name) => {
|
|
339
|
+
if (prevProps[name] !== nextProps[name]) {
|
|
340
|
+
if (name.startsWith("on")) {
|
|
341
|
+
const eventType = name.toLowerCase().substring(2);
|
|
342
|
+
dom.addEventListener(eventType, nextProps[name]);
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
dom[name] = nextProps[name];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const fiberRenderer = new FiberRenderer();
|
|
352
|
+
export function useFiberRenderer() {
|
|
353
|
+
return fiberRenderer;
|
|
354
|
+
}
|
|
355
|
+
class HierarchicalStore {
|
|
356
|
+
constructor() {
|
|
357
|
+
this.root = new Map();
|
|
358
|
+
this.selectorCache = new Map();
|
|
359
|
+
this.stateSnapshot = {};
|
|
360
|
+
}
|
|
361
|
+
select(selector) {
|
|
362
|
+
const selectorKey = selector.toString();
|
|
363
|
+
if (this.selectorCache.has(selectorKey)) {
|
|
364
|
+
return this.selectorCache.get(selectorKey);
|
|
365
|
+
}
|
|
366
|
+
const result = selector(this.getState());
|
|
367
|
+
this.selectorCache.set(selectorKey, result);
|
|
368
|
+
return result;
|
|
369
|
+
}
|
|
370
|
+
optimisticUpdate(path, update, rollback) {
|
|
371
|
+
const original = this.get(path);
|
|
372
|
+
this.set(path, update);
|
|
373
|
+
return {
|
|
374
|
+
commit: () => this.clearRollback(path),
|
|
375
|
+
rollback: () => {
|
|
376
|
+
this.set(path, original);
|
|
377
|
+
rollback?.();
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
getState() {
|
|
382
|
+
return this.stateSnapshot;
|
|
383
|
+
}
|
|
384
|
+
get(path) {
|
|
385
|
+
return this.root.get(path)?.value;
|
|
386
|
+
}
|
|
387
|
+
set(path, value) {
|
|
388
|
+
const node = this.root.get(path);
|
|
389
|
+
if (node) {
|
|
390
|
+
node.value = value;
|
|
391
|
+
this.stateSnapshot = { ...this.stateSnapshot, [path]: value };
|
|
392
|
+
this.invalidateSelectors();
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
clearRollback(path) {
|
|
396
|
+
console.log(`[HierarchicalStore] Optimistic update committed for path: ${path}`);
|
|
397
|
+
}
|
|
398
|
+
invalidateSelectors() {
|
|
399
|
+
this.selectorCache.clear();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const hierarchicalStore = new HierarchicalStore();
|
|
403
|
+
export function useHierarchicalStore() {
|
|
404
|
+
return hierarchicalStore;
|
|
405
|
+
}
|
|
25
406
|
export const TEXT = Symbol("text");
|
|
26
407
|
export const Fragment = Symbol("Fragment");
|
|
27
408
|
const BOOLEAN_ATTRS = new Set([
|
|
@@ -52,6 +433,31 @@ const DOM_PROPERTIES = new Set([
|
|
|
52
433
|
"textContent",
|
|
53
434
|
"innerText",
|
|
54
435
|
]);
|
|
436
|
+
const DANGEROUS_HTML_PROPS = new Set([
|
|
437
|
+
"innerHTML",
|
|
438
|
+
"outerHTML",
|
|
439
|
+
"insertAdjacentHTML",
|
|
440
|
+
"srcdoc",
|
|
441
|
+
]);
|
|
442
|
+
const DANGEROUS_PROTOCOLS = new Set([
|
|
443
|
+
"javascript:",
|
|
444
|
+
"data:",
|
|
445
|
+
"vbscript:",
|
|
446
|
+
"file:",
|
|
447
|
+
"about:",
|
|
448
|
+
]);
|
|
449
|
+
const SAFE_PROTOCOLS = new Set([
|
|
450
|
+
"http:",
|
|
451
|
+
"https:",
|
|
452
|
+
"ftp:",
|
|
453
|
+
"ftps:",
|
|
454
|
+
"mailto:",
|
|
455
|
+
"tel:",
|
|
456
|
+
"#",
|
|
457
|
+
"/",
|
|
458
|
+
"./",
|
|
459
|
+
"../",
|
|
460
|
+
]);
|
|
55
461
|
export function createTextVNode(text) {
|
|
56
462
|
if (text == null || text === false) {
|
|
57
463
|
return { type: TEXT, props: { nodeValue: "" }, key: null };
|
|
@@ -302,7 +708,7 @@ export function renderComponent(Component, props = {}) {
|
|
|
302
708
|
catch (err) {
|
|
303
709
|
console.error("[Fynix] Component render error:", err);
|
|
304
710
|
showErrorOverlay(err);
|
|
305
|
-
return h("div", { style: "color:red" }, `Error: ${err
|
|
711
|
+
return h("div", { style: "color:red" }, `Error: ${sanitizeErrorMessage(err)}`);
|
|
306
712
|
}
|
|
307
713
|
finally {
|
|
308
714
|
endComponent();
|
|
@@ -346,6 +752,45 @@ function registerDelegatedHandler(el, eventName, fn) {
|
|
|
346
752
|
}
|
|
347
753
|
});
|
|
348
754
|
}
|
|
755
|
+
function sanitizeText(text) {
|
|
756
|
+
if (typeof text !== "string")
|
|
757
|
+
return String(text);
|
|
758
|
+
return text
|
|
759
|
+
.replace(/[<>"'&]/g, (match) => {
|
|
760
|
+
const entityMap = {
|
|
761
|
+
"<": "<",
|
|
762
|
+
">": ">",
|
|
763
|
+
'"': """,
|
|
764
|
+
"'": "'",
|
|
765
|
+
"&": "&",
|
|
766
|
+
};
|
|
767
|
+
return entityMap[match] || match;
|
|
768
|
+
})
|
|
769
|
+
.replace(/javascript:/gi, "blocked:")
|
|
770
|
+
.replace(/data:.*?base64/gi, "blocked:");
|
|
771
|
+
}
|
|
772
|
+
function sanitizeAttributeValue(value) {
|
|
773
|
+
if (typeof value !== "string")
|
|
774
|
+
return String(value);
|
|
775
|
+
return value
|
|
776
|
+
.replace(/["'<>]/g, (match) => {
|
|
777
|
+
const entityMap = {
|
|
778
|
+
'"': """,
|
|
779
|
+
"'": "'",
|
|
780
|
+
"<": "<",
|
|
781
|
+
">": ">",
|
|
782
|
+
};
|
|
783
|
+
return entityMap[match] || match;
|
|
784
|
+
})
|
|
785
|
+
.replace(/javascript:/gi, "blocked:")
|
|
786
|
+
.replace(/on\w+=/gi, "blocked=");
|
|
787
|
+
}
|
|
788
|
+
function sanitizeErrorMessage(error) {
|
|
789
|
+
if (!error)
|
|
790
|
+
return "Unknown error";
|
|
791
|
+
const message = error.message || error.toString() || "Unknown error";
|
|
792
|
+
return sanitizeText(String(message)).slice(0, 200);
|
|
793
|
+
}
|
|
349
794
|
function setProperty(el, key, value) {
|
|
350
795
|
const k = key.toLowerCase();
|
|
351
796
|
if (key === "r-class" || key === "rc") {
|
|
@@ -370,8 +815,40 @@ function setProperty(el, key, value) {
|
|
|
370
815
|
Object.assign(el.style, value);
|
|
371
816
|
return;
|
|
372
817
|
}
|
|
373
|
-
if (key
|
|
374
|
-
console.
|
|
818
|
+
if (DANGEROUS_HTML_PROPS.has(key)) {
|
|
819
|
+
console.error(`[Fynix] Security: ${key} is blocked for security reasons. Use textContent or children instead.`);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if ((key === "href" ||
|
|
823
|
+
key === "src" ||
|
|
824
|
+
key === "action" ||
|
|
825
|
+
key === "formaction") &&
|
|
826
|
+
typeof value === "string") {
|
|
827
|
+
const normalizedValue = value.trim().toLowerCase();
|
|
828
|
+
for (const protocol of DANGEROUS_PROTOCOLS) {
|
|
829
|
+
if (normalizedValue.startsWith(protocol)) {
|
|
830
|
+
console.error(`[Fynix] Security: ${protocol} protocol blocked in ${key}`);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
if (normalizedValue.includes(":")) {
|
|
835
|
+
const protocol = normalizedValue.split(":")[0] + ":";
|
|
836
|
+
if (!SAFE_PROTOCOLS.has(protocol) &&
|
|
837
|
+
!SAFE_PROTOCOLS.has(normalizedValue.charAt(0))) {
|
|
838
|
+
console.error(`[Fynix] Security: Protocol '${protocol}' not in safe list for ${key}`);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
if (normalizedValue.startsWith("data:")) {
|
|
843
|
+
if (normalizedValue.includes("javascript") ||
|
|
844
|
+
normalizedValue.includes("<script")) {
|
|
845
|
+
console.error(`[Fynix] Security: Suspicious data: URL blocked in ${key}`);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (key.toLowerCase().startsWith("on") && key !== "open") {
|
|
851
|
+
console.error(`[Fynix] Security: Inline event handler '${key}' blocked. Use r-${key.slice(2)} instead.`);
|
|
375
852
|
return;
|
|
376
853
|
}
|
|
377
854
|
if (BOOLEAN_ATTRS.has(k)) {
|
|
@@ -385,25 +862,24 @@ function setProperty(el, key, value) {
|
|
|
385
862
|
}
|
|
386
863
|
return;
|
|
387
864
|
}
|
|
388
|
-
if (DOM_PROPERTIES.has(key)) {
|
|
389
|
-
|
|
865
|
+
if (DOM_PROPERTIES.has(key) && !DANGEROUS_HTML_PROPS.has(key)) {
|
|
866
|
+
if (key === "textContent" || key === "innerText") {
|
|
867
|
+
el[key] = sanitizeText(value ?? "");
|
|
868
|
+
}
|
|
869
|
+
else {
|
|
870
|
+
el[key] = value ?? "";
|
|
871
|
+
}
|
|
390
872
|
return;
|
|
391
873
|
}
|
|
392
874
|
if (key.startsWith("data-") || key.startsWith("aria-")) {
|
|
393
875
|
if (value != null && value !== false) {
|
|
394
|
-
el.setAttribute(key, value);
|
|
876
|
+
el.setAttribute(key, sanitizeAttributeValue(String(value)));
|
|
395
877
|
}
|
|
396
878
|
else {
|
|
397
879
|
el.removeAttribute(key);
|
|
398
880
|
}
|
|
399
881
|
return;
|
|
400
882
|
}
|
|
401
|
-
if ((key === "href" || key === "src") && typeof value === "string") {
|
|
402
|
-
if (value.trim().toLowerCase().startsWith("javascript:")) {
|
|
403
|
-
console.warn("[Fynix] Security: javascript: protocol blocked in", key);
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
883
|
if (value != null && value !== false) {
|
|
408
884
|
el.setAttribute(key, value);
|
|
409
885
|
}
|
|
@@ -489,7 +965,7 @@ async function renderMaybeAsyncComponent(Component, props, vnode) {
|
|
|
489
965
|
showErrorOverlay(err);
|
|
490
966
|
ctx._isMounted = false;
|
|
491
967
|
endComponent();
|
|
492
|
-
return h("div", { style: "color:red" }, `Error: ${err
|
|
968
|
+
return h("div", { style: "color:red" }, `Error: ${sanitizeErrorMessage(err)}`);
|
|
493
969
|
}
|
|
494
970
|
}
|
|
495
971
|
export async function patch(parent, newVNode, oldVNode) {
|