sibujs 1.2.0 → 1.3.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.
Files changed (89) hide show
  1. package/README.md +29 -25
  2. package/dist/browser.cjs +804 -2
  3. package/dist/browser.d.cts +591 -1
  4. package/dist/browser.d.ts +591 -1
  5. package/dist/browser.js +50 -8
  6. package/dist/build.cjs +654 -144
  7. package/dist/build.js +14 -12
  8. package/dist/cdn.global.js +188 -7
  9. package/dist/chunk-2BYQDGN3.js +742 -0
  10. package/dist/chunk-32DY64NT.js +282 -0
  11. package/dist/chunk-3AIRKM3B.js +1263 -0
  12. package/dist/chunk-3X2YG6YM.js +505 -0
  13. package/dist/chunk-5X6PP2UK.js +28 -0
  14. package/dist/chunk-77L6NL3X.js +1097 -0
  15. package/dist/chunk-BGN5ZMP4.js +26 -0
  16. package/dist/chunk-BTU3TJDS.js +365 -0
  17. package/dist/chunk-CHF5OHIA.js +61 -0
  18. package/dist/chunk-CMBFNA7L.js +27 -0
  19. package/dist/chunk-DAHRH4ON.js +331 -0
  20. package/dist/chunk-EBGIRKQY.js +616 -0
  21. package/dist/chunk-EUZND3CB.js +27 -0
  22. package/dist/chunk-F3FA4F32.js +292 -0
  23. package/dist/chunk-JAKHTMQU.js +1000 -0
  24. package/dist/chunk-JCI5M6U6.js +956 -0
  25. package/dist/chunk-KQPDEVVS.js +398 -0
  26. package/dist/chunk-NEKUBFPT.js +60 -0
  27. package/dist/chunk-NYVAC6P5.js +37 -0
  28. package/dist/chunk-PTQJDMRT.js +146 -0
  29. package/dist/chunk-QWZG56ET.js +2744 -0
  30. package/dist/chunk-TSOKIX5Z.js +654 -0
  31. package/dist/chunk-VRW3FULF.js +725 -0
  32. package/dist/chunk-WZSPOOER.js +84 -0
  33. package/dist/chunk-YT6HQ6AM.js +14 -0
  34. package/dist/chunk-ZD6OAMTH.js +277 -0
  35. package/dist/contracts-DDrwxvJ-.d.cts +245 -0
  36. package/dist/contracts-DDrwxvJ-.d.ts +245 -0
  37. package/dist/data.cjs +35 -2
  38. package/dist/data.d.cts +7 -0
  39. package/dist/data.d.ts +7 -0
  40. package/dist/data.js +9 -8
  41. package/dist/devtools.cjs +122 -0
  42. package/dist/devtools.d.cts +69 -461
  43. package/dist/devtools.d.ts +69 -461
  44. package/dist/devtools.js +127 -6
  45. package/dist/ecosystem.cjs +23 -6
  46. package/dist/ecosystem.d.cts +1 -1
  47. package/dist/ecosystem.d.ts +1 -1
  48. package/dist/ecosystem.js +10 -9
  49. package/dist/extras.cjs +1207 -65
  50. package/dist/extras.d.cts +5 -5
  51. package/dist/extras.d.ts +5 -5
  52. package/dist/extras.js +69 -24
  53. package/dist/index.cjs +663 -144
  54. package/dist/index.d.cts +397 -17
  55. package/dist/index.d.ts +397 -17
  56. package/dist/index.js +39 -17
  57. package/dist/introspect-BumjnBKr.d.cts +477 -0
  58. package/dist/introspect-CZrlcaYy.d.ts +477 -0
  59. package/dist/introspect-Cb0zgpi2.d.cts +477 -0
  60. package/dist/introspect-Y2xNXGSf.d.ts +477 -0
  61. package/dist/motion.js +4 -4
  62. package/dist/patterns.cjs +51 -2
  63. package/dist/patterns.d.cts +18 -8
  64. package/dist/patterns.d.ts +18 -8
  65. package/dist/patterns.js +7 -7
  66. package/dist/performance.js +4 -4
  67. package/dist/plugins.cjs +428 -81
  68. package/dist/plugins.d.cts +27 -4
  69. package/dist/plugins.d.ts +27 -4
  70. package/dist/plugins.js +156 -37
  71. package/dist/ssr-4PBXAOO3.js +40 -0
  72. package/dist/ssr-Do_SiVoL.d.cts +201 -0
  73. package/dist/ssr-Do_SiVoL.d.ts +201 -0
  74. package/dist/ssr.cjs +312 -60
  75. package/dist/ssr.d.cts +10 -1
  76. package/dist/ssr.d.ts +10 -1
  77. package/dist/ssr.js +13 -10
  78. package/dist/tagFactory-DaJ0YWX6.d.cts +47 -0
  79. package/dist/tagFactory-DaJ0YWX6.d.ts +47 -0
  80. package/dist/testing.cjs +233 -2
  81. package/dist/testing.d.cts +42 -1
  82. package/dist/testing.d.ts +42 -1
  83. package/dist/testing.js +129 -2
  84. package/dist/ui.cjs +374 -3
  85. package/dist/ui.d.cts +252 -2
  86. package/dist/ui.d.ts +252 -2
  87. package/dist/ui.js +328 -8
  88. package/dist/widgets.js +7 -7
  89. package/package.json +1 -1
package/dist/plugins.cjs CHANGED
@@ -42,11 +42,40 @@ var init_dev = __esm({
42
42
  }
43
43
  });
44
44
 
45
+ // src/utils/sanitize.ts
46
+ function sanitizeUrl(url) {
47
+ const trimmed = url.replace(/[\x00-\x20\x7f-\x9f]+/g, "").trim();
48
+ if (!trimmed) return "";
49
+ const lower = trimmed.toLowerCase();
50
+ if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:") || lower.startsWith("blob:")) {
51
+ return "";
52
+ }
53
+ return trimmed;
54
+ }
55
+ function sanitizeCSSValue(value) {
56
+ const lower = value.toLowerCase().replace(/\s+/g, "");
57
+ if (lower.includes("url(") || lower.includes("expression(") || lower.includes("javascript:") || lower.includes("-moz-binding")) {
58
+ return "";
59
+ }
60
+ return value;
61
+ }
62
+ function isUrlAttribute(attr) {
63
+ return URL_ATTRIBUTES.has(attr);
64
+ }
65
+ var URL_ATTRIBUTES;
66
+ var init_sanitize = __esm({
67
+ "src/utils/sanitize.ts"() {
68
+ "use strict";
69
+ URL_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "cite", "poster", "background", "srcset"]);
70
+ }
71
+ });
72
+
45
73
  // src/platform/ssr.ts
46
74
  var ssr_exports = {};
47
75
  __export(ssr_exports, {
48
76
  collectStream: () => collectStream,
49
77
  deserializeState: () => deserializeState,
78
+ escapeScriptJson: () => escapeScriptJson,
50
79
  hydrate: () => hydrate,
51
80
  hydrateIslands: () => hydrateIslands,
52
81
  hydrateProgressively: () => hydrateProgressively,
@@ -62,12 +91,24 @@ __export(ssr_exports, {
62
91
  suspenseSwapScript: () => suspenseSwapScript,
63
92
  trustHTML: () => trustHTML
64
93
  });
94
+ function isSafeAttrName(name) {
95
+ return SAFE_ATTR_NAME.test(name);
96
+ }
97
+ function isEventHandlerAttr2(name) {
98
+ if (name.length < 3) return false;
99
+ const lower = name.toLowerCase();
100
+ return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
101
+ }
65
102
  function ssrErrorComment(err) {
66
103
  if (_isDev7) {
67
- return `<!--SSR error: ${escapeHtml(err instanceof Error ? err.message : String(err))}-->`;
104
+ const msg = escapeHtml(err instanceof Error ? err.message : String(err));
105
+ return `<!--SSR error: ${safeCommentText(msg)}-->`;
68
106
  }
69
107
  return "<!--SSR error-->";
70
108
  }
109
+ function safeCommentText(text2) {
110
+ return text2.replace(/-->/g, "--&gt;").replace(/--!>/g, "--!&gt;").replace(/<!--/g, "&lt;!--").replace(/--$/g, "--&#45;");
111
+ }
71
112
  function renderToString(element) {
72
113
  if (element instanceof DocumentFragment) {
73
114
  return Array.from(element.childNodes).map((child) => {
@@ -82,16 +123,30 @@ function renderToString(element) {
82
123
  return escapeHtml(element.textContent || "");
83
124
  }
84
125
  if (element.nodeType === 8) {
85
- const content = (element.textContent || "").replace(/-->/g, "--&gt;");
86
- return `<!--${content}-->`;
126
+ return `<!--${safeCommentText(element.textContent || "")}-->`;
87
127
  }
88
128
  if (!(element instanceof HTMLElement)) {
89
- return element.textContent || "";
129
+ return escapeHtml(element.textContent || "");
90
130
  }
91
131
  const tag = element.tagName.toLowerCase();
132
+ if (tag === "script" || tag === "style") {
133
+ return _isDev7 ? `<!--ssr:${tag}-stripped-->` : "";
134
+ }
135
+ if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
136
+ return _isDev7 ? "<!--ssr:invalid-tag-->" : "";
137
+ }
92
138
  let html2 = `<${tag}`;
93
139
  for (const attr of Array.from(element.attributes)) {
94
- html2 += ` ${attr.name}="${escapeAttr(attr.value)}"`;
140
+ const rawName = attr.name;
141
+ if (!isSafeAttrName(rawName)) continue;
142
+ if (isEventHandlerAttr2(rawName)) continue;
143
+ const lowerName = rawName.toLowerCase();
144
+ let value = attr.value;
145
+ if (URL_ATTRS.has(lowerName)) {
146
+ value = sanitizeUrl(value);
147
+ if (!value) continue;
148
+ }
149
+ html2 += ` ${rawName}="${escapeAttr(value)}"`;
95
150
  }
96
151
  if (element.dataset && !element.dataset.sibuHydrate) {
97
152
  html2 += ` data-sibu-ssr="true"`;
@@ -110,8 +165,25 @@ function renderToString(element) {
110
165
  html2 += `</${tag}>`;
111
166
  return html2;
112
167
  }
113
- function hydrate(component, container) {
168
+ function hydrate(component, container, options = {}) {
114
169
  const clientTree = component();
170
+ if (options.diagnostics) {
171
+ const mismatches = [];
172
+ collectMismatches(container.firstElementChild, clientTree, "", mismatches);
173
+ if (mismatches.length > 0) {
174
+ const first = mismatches[0];
175
+ if (options.onMismatch) {
176
+ options.onMismatch(first);
177
+ } else if (_isDev7) {
178
+ console.warn(
179
+ `[Sibu hydration] ${first.message}
180
+ at ${first.path}
181
+ server: ${first.serverValue}
182
+ client: ${first.clientValue}`
183
+ );
184
+ }
185
+ }
186
+ }
115
187
  hydrateNode(container.firstElementChild, clientTree);
116
188
  container.setAttribute("data-sibu-hydrated", "true");
117
189
  }
@@ -123,9 +195,119 @@ function hydrateNode(serverNode, clientNode) {
123
195
  hydrateNode(serverChildren[i2], clientChildren[i2]);
124
196
  }
125
197
  }
198
+ function collectMismatches(serverNode, clientNode, path2, out, max = 5) {
199
+ if (out.length >= max) return;
200
+ const nodePath = path2 || clientNode?.tagName?.toLowerCase() || "(root)";
201
+ if (!serverNode && clientNode) {
202
+ out.push({
203
+ kind: "child-count",
204
+ path: nodePath,
205
+ serverValue: "(missing)",
206
+ clientValue: clientNode.tagName.toLowerCase(),
207
+ message: "Client rendered a node that the server did not emit."
208
+ });
209
+ return;
210
+ }
211
+ if (serverNode && !clientNode) {
212
+ out.push({
213
+ kind: "child-count",
214
+ path: nodePath,
215
+ serverValue: serverNode.tagName.toLowerCase(),
216
+ clientValue: "(missing)",
217
+ message: "Server rendered a node that the client did not produce."
218
+ });
219
+ return;
220
+ }
221
+ if (!serverNode || !clientNode) return;
222
+ if (serverNode.tagName !== clientNode.tagName) {
223
+ out.push({
224
+ kind: "tag",
225
+ path: nodePath,
226
+ serverValue: serverNode.tagName.toLowerCase(),
227
+ clientValue: clientNode.tagName.toLowerCase(),
228
+ message: "Element tag mismatch \u2014 server and client disagree on the element type."
229
+ });
230
+ return;
231
+ }
232
+ const skipAttrs = /* @__PURE__ */ new Set(["data-sibu-ssr", "data-sibu-hydrated", "data-sibu-island"]);
233
+ const serverAttrs = /* @__PURE__ */ new Map();
234
+ for (const a2 of Array.from(serverNode.attributes)) {
235
+ if (!skipAttrs.has(a2.name)) serverAttrs.set(a2.name, a2.value);
236
+ }
237
+ const clientAttrs = /* @__PURE__ */ new Map();
238
+ for (const a2 of Array.from(clientNode.attributes)) {
239
+ if (!skipAttrs.has(a2.name)) clientAttrs.set(a2.name, a2.value);
240
+ }
241
+ for (const [name, value] of serverAttrs) {
242
+ if (out.length >= max) return;
243
+ if (!clientAttrs.has(name)) {
244
+ out.push({
245
+ kind: "attribute",
246
+ path: `${nodePath}[${name}]`,
247
+ serverValue: value,
248
+ clientValue: "(missing)",
249
+ message: `Attribute "${name}" present on server but missing on client.`
250
+ });
251
+ } else if (clientAttrs.get(name) !== value) {
252
+ out.push({
253
+ kind: "attribute",
254
+ path: `${nodePath}[${name}]`,
255
+ serverValue: value,
256
+ clientValue: clientAttrs.get(name) ?? "",
257
+ message: `Attribute "${name}" differs between server and client.`
258
+ });
259
+ }
260
+ }
261
+ for (const [name, value] of clientAttrs) {
262
+ if (out.length >= max) return;
263
+ if (!serverAttrs.has(name)) {
264
+ out.push({
265
+ kind: "attribute",
266
+ path: `${nodePath}[${name}]`,
267
+ serverValue: "(missing)",
268
+ clientValue: value,
269
+ message: `Attribute "${name}" present on client but missing on server.`
270
+ });
271
+ }
272
+ }
273
+ const serverChildren = Array.from(serverNode.children);
274
+ const clientChildren = Array.from(clientNode.children);
275
+ const max2 = Math.max(serverChildren.length, clientChildren.length);
276
+ for (let i2 = 0; i2 < max2; i2++) {
277
+ if (out.length >= max) return;
278
+ const childPath = `${nodePath} > ${clientChildren[i2]?.tagName?.toLowerCase() ?? serverChildren[i2]?.tagName?.toLowerCase() ?? "?"}:nth-child(${i2 + 1})`;
279
+ collectMismatches(serverChildren[i2] ?? null, clientChildren[i2] ?? null, childPath, out, max);
280
+ }
281
+ }
126
282
  function trustHTML(html2) {
127
283
  return html2;
128
284
  }
285
+ function buildAttrString(attrs, { allowEventHandlers = false } = {}) {
286
+ if (!attrs) return "";
287
+ const out = [];
288
+ for (const rawKey of Object.keys(attrs)) {
289
+ if (!Object.hasOwn(attrs, rawKey)) continue;
290
+ if (!isSafeAttrName(rawKey)) continue;
291
+ if (!allowEventHandlers && isEventHandlerAttr2(rawKey)) continue;
292
+ const lowerKey = rawKey.toLowerCase();
293
+ let value = String(attrs[rawKey]);
294
+ if (URL_ATTRS.has(lowerKey)) {
295
+ value = sanitizeUrl(value);
296
+ if (!value) continue;
297
+ }
298
+ out.push(`${rawKey}="${escapeAttr(value)}"`);
299
+ }
300
+ return out.join(" ");
301
+ }
302
+ function isDangerousMetaRefresh(metaProps) {
303
+ const httpEquiv = metaProps["http-equiv"];
304
+ if (typeof httpEquiv !== "string") return false;
305
+ if (httpEquiv.toLowerCase() !== "refresh") return false;
306
+ const content = metaProps.content;
307
+ if (typeof content !== "string") return false;
308
+ const normalized = content.replace(/[\x00-\x20\x7f-\x9f]+/g, "").toLowerCase();
309
+ return normalized.includes("url=javascript:") || normalized.includes("url=data:") || normalized.includes("url=vbscript:") || normalized.includes("url=blob:");
310
+ }
129
311
  function renderToDocument(component, options = {}) {
130
312
  let content;
131
313
  try {
@@ -133,14 +315,22 @@ function renderToDocument(component, options = {}) {
133
315
  } catch (err) {
134
316
  content = ssrErrorComment(err);
135
317
  }
136
- const metaTags = (options.meta || []).map(
137
- (attrs) => `<meta ${Object.entries(attrs).map(([k, v]) => `${k}="${escapeAttr(v)}"`).join(" ")} />`
138
- ).join("\n ");
139
- const linkTags = (options.links || []).map(
140
- (attrs) => `<link ${Object.entries(attrs).map(([k, v]) => `${k}="${escapeAttr(v)}"`).join(" ")} />`
141
- ).join("\n ");
142
- const scriptTags = (options.scripts || []).map((src) => `<script src="${escapeAttr(src)}"></script>`).join("\n ");
143
- const bodyAttrs = options.bodyAttrs ? " " + Object.entries(options.bodyAttrs).map(([k, v]) => `${k}="${escapeAttr(v)}"`).join(" ") : "";
318
+ const metaTags = (options.meta || []).map((attrs) => {
319
+ if (isDangerousMetaRefresh(attrs)) return "";
320
+ const pairs = buildAttrString(attrs);
321
+ return pairs ? `<meta ${pairs} />` : "";
322
+ }).filter(Boolean).join("\n ");
323
+ const linkTags = (options.links || []).map((attrs) => {
324
+ const pairs = buildAttrString(attrs);
325
+ return pairs ? `<link ${pairs} />` : "";
326
+ }).filter(Boolean).join("\n ");
327
+ const scriptTags = (options.scripts || []).map((src) => {
328
+ const safe = sanitizeUrl(String(src));
329
+ if (!safe) return "";
330
+ return `<script src="${escapeAttr(safe)}"></script>`;
331
+ }).filter(Boolean).join("\n ");
332
+ const bodyAttrPairs = buildAttrString(options.bodyAttrs);
333
+ const bodyAttrs = bodyAttrPairs ? ` ${bodyAttrPairs}` : "";
144
334
  return `<!DOCTYPE html>
145
335
  <html>
146
336
  <head>
@@ -173,18 +363,34 @@ async function* renderToStream(element) {
173
363
  return;
174
364
  }
175
365
  if (element.nodeType === 8) {
176
- const content = (element.textContent || "").replace(/-->/g, "--&gt;");
177
- yield `<!--${content}-->`;
366
+ yield `<!--${safeCommentText(element.textContent || "")}-->`;
178
367
  return;
179
368
  }
180
369
  if (!(element instanceof HTMLElement)) {
181
- yield element.textContent || "";
370
+ yield escapeHtml(element.textContent || "");
182
371
  return;
183
372
  }
184
373
  const tag = element.tagName.toLowerCase();
374
+ if (tag === "script" || tag === "style") {
375
+ if (_isDev7) yield `<!--ssr:${tag}-stripped-->`;
376
+ return;
377
+ }
378
+ if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
379
+ if (_isDev7) yield "<!--ssr:invalid-tag-->";
380
+ return;
381
+ }
185
382
  let openTag = `<${tag}`;
186
383
  for (const attr of Array.from(element.attributes)) {
187
- openTag += ` ${attr.name}="${escapeAttr(attr.value)}"`;
384
+ const rawName = attr.name;
385
+ if (!isSafeAttrName(rawName)) continue;
386
+ if (isEventHandlerAttr2(rawName)) continue;
387
+ const lowerName = rawName.toLowerCase();
388
+ let value = attr.value;
389
+ if (URL_ATTRS.has(lowerName)) {
390
+ value = sanitizeUrl(value);
391
+ if (!value) continue;
392
+ }
393
+ openTag += ` ${rawName}="${escapeAttr(value)}"`;
188
394
  }
189
395
  if (VOID_ELEMENTS.has(tag)) {
190
396
  yield `${openTag} />`;
@@ -232,8 +438,9 @@ function hydrateIslands(container, islands) {
232
438
  const markers = container.querySelectorAll("[data-sibu-island]");
233
439
  for (const marker2 of Array.from(markers)) {
234
440
  const id = marker2.getAttribute("data-sibu-island") ?? "";
441
+ if (!Object.hasOwn(islands, id)) continue;
235
442
  const factory = islands[id];
236
- if (!factory) continue;
443
+ if (typeof factory !== "function") continue;
237
444
  const clientTree = factory();
238
445
  hydrateNode(marker2, clientTree);
239
446
  marker2.setAttribute("data-sibu-hydrated", "true");
@@ -245,8 +452,9 @@ function hydrateProgressively(container, islands, options) {
245
452
  const cleanups = [];
246
453
  for (const marker2 of Array.from(markers)) {
247
454
  const id = marker2.getAttribute("data-sibu-island") ?? "";
455
+ if (!Object.hasOwn(islands, id)) continue;
248
456
  const factory = islands[id];
249
- if (!factory) continue;
457
+ if (typeof factory !== "function") continue;
250
458
  const observer = new IntersectionObserver(
251
459
  (entries) => {
252
460
  for (const entry of entries) {
@@ -285,22 +493,30 @@ function ssrSuspense(props) {
285
493
  return { element: wrapper, promise };
286
494
  }
287
495
  function suspenseSwapScript(id, nonce) {
288
- const safeId = id.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
496
+ if (!SAFE_SUSPENSE_ID.test(id)) {
497
+ throw new Error(
498
+ `[SibuJS SSR] suspenseSwapScript: id must match [A-Za-z0-9_-]+ (got: ${JSON.stringify(id.slice(0, 32))})`
499
+ );
500
+ }
289
501
  const nonceAttr = nonce ? ` nonce="${escapeAttr(nonce)}"` : "";
290
- return `<script${nonceAttr}>(function(){var t=document.getElementById("sibu-resolved-${safeId}");var f=document.querySelector('[data-sibu-suspense-id="${safeId}"]');if(t&&f){while(t.firstChild)f.appendChild(t.firstChild);t.remove();f.removeAttribute("data-sibu-suspense-id");}})()</script>`;
502
+ return `<script${nonceAttr}>(function(){var t=document.getElementById("sibu-resolved-${id}");var f=document.querySelector('[data-sibu-suspense-id="${id}"]');if(t&&f){while(t.firstChild)f.appendChild(t.firstChild);t.remove();f.removeAttribute("data-sibu-suspense-id");}})()</script>`;
291
503
  }
292
- async function* renderToSuspenseStream(element, pendingBoundaries = []) {
504
+ async function* renderToSuspenseStream(element, pendingBoundaries = [], options) {
293
505
  yield* renderToStream(element);
294
506
  if (pendingBoundaries.length > 0) {
295
507
  const resolved = await Promise.all(pendingBoundaries);
296
508
  for (const { id, html: html2 } of resolved) {
297
- yield `<div hidden id="sibu-resolved-${escapeAttr(id)}">${html2}</div>`;
298
- yield suspenseSwapScript(id);
509
+ if (!SAFE_SUSPENSE_ID.test(id)) continue;
510
+ yield `<div hidden id="sibu-resolved-${id}">${html2}</div>`;
511
+ yield suspenseSwapScript(id, options?.nonce);
299
512
  }
300
513
  }
301
514
  }
515
+ function escapeScriptJson(json) {
516
+ return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
517
+ }
302
518
  function serializeState(state, nonce) {
303
- const json = JSON.stringify(state).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
519
+ const json = escapeScriptJson(JSON.stringify(state));
304
520
  const nonceAttr = nonce ? ` nonce="${escapeAttr(nonce)}"` : "";
305
521
  return `<script${nonceAttr}>window.${SSR_DATA_ATTR}=${json}</script>`;
306
522
  }
@@ -315,14 +531,30 @@ function escapeHtml(str) {
315
531
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
316
532
  }
317
533
  function escapeAttr(str) {
318
- return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
534
+ return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
319
535
  }
320
- var _isDev7, VOID_ELEMENTS, suspenseIdCounter, SSR_DATA_ATTR;
536
+ var _isDev7, SAFE_ATTR_NAME, URL_ATTRS, VOID_ELEMENTS, suspenseIdCounter, SAFE_SUSPENSE_ID, SSR_DATA_ATTR;
321
537
  var init_ssr = __esm({
322
538
  "src/platform/ssr.ts"() {
323
539
  "use strict";
324
540
  init_dev();
541
+ init_sanitize();
325
542
  _isDev7 = isDev();
543
+ SAFE_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
544
+ URL_ATTRS = /* @__PURE__ */ new Set([
545
+ "href",
546
+ "src",
547
+ "action",
548
+ "formaction",
549
+ "cite",
550
+ "poster",
551
+ "background",
552
+ "srcset",
553
+ "ping",
554
+ "manifest",
555
+ "data",
556
+ "xlink:href"
557
+ ]);
326
558
  VOID_ELEMENTS = /* @__PURE__ */ new Set([
327
559
  "area",
328
560
  "base",
@@ -340,6 +572,7 @@ var init_ssr = __esm({
340
572
  "wbr"
341
573
  ]);
342
574
  suspenseIdCounter = 0;
575
+ SAFE_SUSPENSE_ID = /^[A-Za-z0-9_-]+$/;
343
576
  SSR_DATA_ATTR = "__SIBU_SSR_DATA__";
344
577
  }
345
578
  });
@@ -424,28 +657,7 @@ module.exports = __toCommonJS(plugins_exports);
424
657
 
425
658
  // src/reactivity/bindAttribute.ts
426
659
  init_dev();
427
-
428
- // src/utils/sanitize.ts
429
- function sanitizeUrl(url) {
430
- const trimmed = url.replace(/[\x00-\x20\x7f-\x9f]+/g, "").trim();
431
- if (!trimmed) return "";
432
- const lower = trimmed.toLowerCase();
433
- if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:") || lower.startsWith("blob:")) {
434
- return "";
435
- }
436
- return trimmed;
437
- }
438
- function sanitizeCSSValue(value) {
439
- const lower = value.toLowerCase().replace(/\s+/g, "");
440
- if (lower.includes("url(") || lower.includes("expression(") || lower.includes("javascript:") || lower.includes("-moz-binding")) {
441
- return "";
442
- }
443
- return value;
444
- }
445
- var URL_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "cite", "poster", "background", "srcset"]);
446
- function isUrlAttribute(attr) {
447
- return URL_ATTRIBUTES.has(attr);
448
- }
660
+ init_sanitize();
449
661
 
450
662
  // src/reactivity/track.ts
451
663
  init_dev();
@@ -655,7 +867,20 @@ function cleanup(subscriber) {
655
867
 
656
868
  // src/reactivity/bindAttribute.ts
657
869
  var _isDev3 = isDev();
870
+ function isEventHandlerAttr(name) {
871
+ if (name.length < 3) return false;
872
+ const lower = name.toLowerCase();
873
+ return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
874
+ }
658
875
  function bindAttribute(el, attr, getter) {
876
+ if (isEventHandlerAttr(attr)) {
877
+ if (_isDev3)
878
+ devWarn(
879
+ `bindAttribute: refusing to bind event-handler attribute "${attr}". Use on:{ ${attr.slice(2)}: fn } instead.`
880
+ );
881
+ return () => {
882
+ };
883
+ }
659
884
  function commit() {
660
885
  let value;
661
886
  try {
@@ -756,6 +981,9 @@ function bindChildNode(placeholder, getter) {
756
981
  return track(commit);
757
982
  }
758
983
 
984
+ // src/core/rendering/tagFactory.ts
985
+ init_sanitize();
986
+
759
987
  // src/core/rendering/dispose.ts
760
988
  init_dev();
761
989
  var elementDisposers = /* @__PURE__ */ new WeakMap();
@@ -921,16 +1149,20 @@ function appendChildren(el, nodes) {
921
1149
  var tagFactory = (tag, ns) => (first, second) => {
922
1150
  const el = ns ? document.createElementNS(ns, tag) : document.createElement(tag);
923
1151
  if (first === void 0) return el;
924
- if (second === void 0 && typeof first === "string") {
1152
+ if (typeof first === "string") {
1153
+ if (second !== void 0) {
1154
+ el.setAttribute("class", first);
1155
+ appendChildren(el, second);
1156
+ return el;
1157
+ }
925
1158
  el.textContent = first;
926
1159
  return el;
927
1160
  }
928
- if (second !== void 0) {
929
- el.setAttribute("class", first);
930
- appendChildren(el, second);
1161
+ if (typeof first === "number") {
1162
+ el.textContent = String(first);
931
1163
  return el;
932
1164
  }
933
- if (Array.isArray(first) || first instanceof Node) {
1165
+ if (Array.isArray(first) || first instanceof Node || typeof first === "function") {
934
1166
  appendChildren(el, first);
935
1167
  return el;
936
1168
  }
@@ -939,7 +1171,7 @@ var tagFactory = (tag, ns) => (first, second) => {
939
1171
  if (pClass != null) applyClass(el, pClass);
940
1172
  const pId = props.id;
941
1173
  if (pId != null) el.id = pId;
942
- const pNodes = props.nodes;
1174
+ const pNodes = second !== void 0 ? second : props.nodes;
943
1175
  if (pNodes != null) appendChildren(el, pNodes);
944
1176
  const pOn = props.on;
945
1177
  if (pOn) {
@@ -1245,6 +1477,11 @@ function effect(effectFn, options) {
1245
1477
  }
1246
1478
 
1247
1479
  // src/plugins/router.ts
1480
+ init_sanitize();
1481
+ function isSafeNavigationTarget(path2) {
1482
+ if (path2 === "") return true;
1483
+ return sanitizeUrl(path2) !== "";
1484
+ }
1248
1485
  var LRUCache = class {
1249
1486
  constructor(maxSize = 100) {
1250
1487
  this.cache = /* @__PURE__ */ new Map();
@@ -1730,6 +1967,11 @@ var _SibuRouter = class _SibuRouter {
1730
1967
  try {
1731
1968
  await this.navigator.navigate(async (signal2) => {
1732
1969
  const targetPath = this.resolvePath(to);
1970
+ if (!isSafeNavigationTarget(targetPath)) {
1971
+ const from2 = this.currentRouteGetter();
1972
+ const toContext2 = this.createRouteContext(targetPath);
1973
+ throw new NavigationFailureError("aborted", from2, toContext2);
1974
+ }
1733
1975
  const from = this.currentRouteGetter();
1734
1976
  const toContext = this.createRouteContext(targetPath);
1735
1977
  if (this.isSameRoute(from, toContext)) {
@@ -1759,6 +2001,9 @@ var _SibuRouter = class _SibuRouter {
1759
2001
  const beforeEachResult = await this.guards.runBeforeEach(to, from, signal2);
1760
2002
  if (beforeEachResult !== true) {
1761
2003
  if (typeof beforeEachResult === "string") {
2004
+ if (!isSafeNavigationTarget(beforeEachResult)) {
2005
+ throw new NavigationFailureError("aborted", from, to);
2006
+ }
1762
2007
  return this.performNavigation(this.createRouteContext(beforeEachResult), from, options, signal2, depth + 1);
1763
2008
  }
1764
2009
  throw new NavigationFailureError("aborted", from, to);
@@ -1774,6 +2019,9 @@ var _SibuRouter = class _SibuRouter {
1774
2019
  const result = await guard(to, from);
1775
2020
  if (result !== true) {
1776
2021
  if (typeof result === "string") {
2022
+ if (!isSafeNavigationTarget(result)) {
2023
+ throw new NavigationFailureError("aborted", from, to);
2024
+ }
1777
2025
  return this.performNavigation(this.createRouteContext(result), from, options, signal2, depth + 1);
1778
2026
  }
1779
2027
  throw new NavigationFailureError("aborted", from, to);
@@ -1788,12 +2036,18 @@ var _SibuRouter = class _SibuRouter {
1788
2036
  `[SibuJS Router] Redirect to absolute URL "${redirectPath}" detected. Use relative paths for safer redirects.`
1789
2037
  );
1790
2038
  }
2039
+ if (typeof redirectPath === "string" && !isSafeNavigationTarget(redirectPath)) {
2040
+ throw new NavigationFailureError("aborted", from, to);
2041
+ }
1791
2042
  return this.performNavigation(this.createRouteContext(redirectPath), from, options, signal2, depth + 1);
1792
2043
  }
1793
2044
  }
1794
2045
  const beforeResolveResult = await this.guards.runBeforeResolve(to, from, signal2);
1795
2046
  if (beforeResolveResult !== true) {
1796
2047
  if (typeof beforeResolveResult === "string") {
2048
+ if (!isSafeNavigationTarget(beforeResolveResult)) {
2049
+ throw new NavigationFailureError("aborted", from, to);
2050
+ }
1797
2051
  return this.performNavigation(this.createRouteContext(beforeResolveResult), from, options, signal2, depth + 1);
1798
2052
  }
1799
2053
  throw new NavigationFailureError("aborted", from, to);
@@ -1953,13 +2207,31 @@ var NavigationFailureError = class extends Error {
1953
2207
  }
1954
2208
  };
1955
2209
  var globalRouter = null;
2210
+ function normalizeRoutes(routes) {
2211
+ return routes.map((route2) => {
2212
+ const normalizedChildren = route2.children && route2.children.length > 0 ? normalizeRoutes(route2.children) : route2.children;
2213
+ if ("lazy" in route2 && typeof route2.lazy === "function") {
2214
+ const { lazy: importFn, ...rest } = route2;
2215
+ const asyncRoute = {
2216
+ ...rest,
2217
+ component: lazy(importFn),
2218
+ children: normalizedChildren
2219
+ };
2220
+ return asyncRoute;
2221
+ }
2222
+ if (normalizedChildren !== route2.children) {
2223
+ return { ...route2, children: normalizedChildren };
2224
+ }
2225
+ return route2;
2226
+ });
2227
+ }
1956
2228
  function createRouter(routesOrOptions, options = {}) {
1957
2229
  if (globalRouter) {
1958
2230
  globalRouter.destroy();
1959
2231
  }
1960
2232
  let routes;
1961
2233
  if (Array.isArray(routesOrOptions)) {
1962
- routes = routesOrOptions;
2234
+ routes = normalizeRoutes(routesOrOptions);
1963
2235
  } else {
1964
2236
  options = routesOrOptions;
1965
2237
  routes = [];
@@ -1969,7 +2241,7 @@ function createRouter(routesOrOptions, options = {}) {
1969
2241
  }
1970
2242
  function setRoutes(routes) {
1971
2243
  if (!globalRouter) throw new Error("Router not initialized. Call createRouter() first.");
1972
- globalRouter.updateRoutes(routes);
2244
+ globalRouter.updateRoutes(normalizeRoutes(routes));
1973
2245
  }
1974
2246
  function route() {
1975
2247
  if (!globalRouter) throw new Error("Router not initialized. Call createRouter() first.");
@@ -2550,6 +2822,20 @@ function createMemoryRouter(routes, _initialPath = "/") {
2550
2822
 
2551
2823
  // src/plugins/routerSSR.ts
2552
2824
  init_ssr();
2825
+ var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
2826
+ function isForbiddenKey(key) {
2827
+ return FORBIDDEN_KEYS.has(key);
2828
+ }
2829
+ function safeDecode(raw) {
2830
+ try {
2831
+ return decodeURIComponent(raw);
2832
+ } catch {
2833
+ return raw;
2834
+ }
2835
+ }
2836
+ function nullObject() {
2837
+ return /* @__PURE__ */ Object.create(null);
2838
+ }
2553
2839
  function parseURL(url) {
2554
2840
  let remaining = url;
2555
2841
  let hash = "";
@@ -2565,19 +2851,23 @@ function parseURL(url) {
2565
2851
  remaining = remaining.slice(0, queryIndex);
2566
2852
  }
2567
2853
  const path2 = remaining || "/";
2568
- const query = {};
2854
+ const query = nullObject();
2569
2855
  if (queryString) {
2570
2856
  const pairs = queryString.split("&");
2571
2857
  for (const pair of pairs) {
2572
2858
  if (!pair) continue;
2573
2859
  const eqIndex = pair.indexOf("=");
2860
+ let key;
2861
+ let value;
2574
2862
  if (eqIndex === -1) {
2575
- query[decodeURIComponent(pair)] = "";
2863
+ key = safeDecode(pair);
2864
+ value = "";
2576
2865
  } else {
2577
- const key = decodeURIComponent(pair.slice(0, eqIndex));
2578
- const value = decodeURIComponent(pair.slice(eqIndex + 1));
2579
- query[key] = value;
2866
+ key = safeDecode(pair.slice(0, eqIndex));
2867
+ value = safeDecode(pair.slice(eqIndex + 1));
2580
2868
  }
2869
+ if (isForbiddenKey(key)) continue;
2870
+ query[key] = value;
2581
2871
  }
2582
2872
  }
2583
2873
  return { path: path2, query, hash };
@@ -2635,10 +2925,12 @@ function matchRoute(path2, routes, parentPath = "", parentChain = []) {
2635
2925
  const compiled = compilePattern(fullPath);
2636
2926
  const match = path2.match(compiled.regex);
2637
2927
  if (match) {
2638
- const params = {};
2928
+ const params = nullObject();
2639
2929
  for (let i2 = 0; i2 < compiled.keys.length; i2++) {
2930
+ const key = compiled.keys[i2];
2931
+ if (isForbiddenKey(key)) continue;
2640
2932
  if (match[i2 + 1] !== void 0) {
2641
- params[compiled.keys[i2]] = decodeURIComponent(match[i2 + 1]);
2933
+ params[key] = safeDecode(match[i2 + 1]);
2642
2934
  }
2643
2935
  }
2644
2936
  return {
@@ -2673,7 +2965,7 @@ function resolveServerRouteInternal(url, routes, depth) {
2673
2965
  return {
2674
2966
  route: {
2675
2967
  path: normalizedPath,
2676
- params: {},
2968
+ params: nullObject(),
2677
2969
  query,
2678
2970
  hash,
2679
2971
  meta: {}
@@ -2733,20 +3025,26 @@ function renderRouteToString(url, routes, _options) {
2733
3025
  function renderRouteToDocument(url, routes, options) {
2734
3026
  const { html: html2, state } = renderRouteToString(url, routes, options);
2735
3027
  const opts = options || {};
2736
- const metaTags = (opts.meta || []).map(
2737
- (attrs) => `<meta ${Object.entries(attrs).map(([k, v]) => `${k}="${escapeAttr2(v)}"`).join(" ")} />`
2738
- ).join("\n ");
2739
- const linkTags = (opts.links || []).map(
2740
- (attrs) => `<link ${Object.entries(attrs).map(([k, v]) => `${k}="${escapeAttr2(v)}"`).join(" ")} />`
2741
- ).join("\n ");
2742
- const scriptTags = (opts.scripts || []).map((src) => `<script src="${escapeAttr2(src)}"></script>`).join("\n ");
2743
- const stateScript = serializeRouteState(state);
3028
+ const metaTags = (opts.meta || []).map((attrs) => {
3029
+ const pairs = buildSafeAttrString(attrs);
3030
+ return pairs ? `<meta ${pairs} />` : "";
3031
+ }).filter(Boolean).join("\n ");
3032
+ const linkTags = (opts.links || []).map((attrs) => {
3033
+ const pairs = buildSafeAttrString(attrs);
3034
+ return pairs ? `<link ${pairs} />` : "";
3035
+ }).filter(Boolean).join("\n ");
3036
+ const scriptTags = (opts.scripts || []).map((src) => {
3037
+ const safe = sanitizeUrlLocal(String(src));
3038
+ if (!safe) return "";
3039
+ return `<script src="${escapeAttrLocal(safe)}"></script>`;
3040
+ }).filter(Boolean).join("\n ");
3041
+ const stateScript = serializeRouteState(state, opts.nonce);
2744
3042
  return `<!DOCTYPE html>
2745
3043
  <html>
2746
3044
  <head>
2747
3045
  <meta charset="UTF-8" />
2748
3046
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
2749
- ${opts.title ? `<title>${escapeHtml2(opts.title)}</title>` : ""}
3047
+ ${opts.title ? `<title>${escapeHtmlLocal(opts.title)}</title>` : ""}
2750
3048
  ${metaTags}
2751
3049
  ${linkTags}
2752
3050
  ${opts.headExtra || ""}
@@ -2758,9 +3056,10 @@ function renderRouteToDocument(url, routes, options) {
2758
3056
  </body>
2759
3057
  </html>`;
2760
3058
  }
2761
- function serializeRouteState(state) {
2762
- const json = JSON.stringify(state).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
2763
- return `<script>window.${SSR_ROUTE_STATE_KEY}=${json}</script>`;
3059
+ function serializeRouteState(state, nonce) {
3060
+ const json = escapeScriptJson(JSON.stringify(state));
3061
+ const nonceAttr = nonce ? ` nonce="${escapeAttrLocal(nonce)}"` : "";
3062
+ return `<script${nonceAttr}>window.${SSR_ROUTE_STATE_KEY}=${json}</script>`;
2764
3063
  }
2765
3064
  function deserializeRouteState() {
2766
3065
  if (typeof window === "undefined") return void 0;
@@ -2798,11 +3097,59 @@ function createSSRRouter(routes) {
2798
3097
  }
2799
3098
  };
2800
3099
  }
2801
- function escapeHtml2(str) {
3100
+ var SAFE_ATTR_NAME2 = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
3101
+ function isSafeAttrName2(name) {
3102
+ return SAFE_ATTR_NAME2.test(name);
3103
+ }
3104
+ function isEventHandlerAttr3(name) {
3105
+ if (name.length < 3) return false;
3106
+ const lower = name.toLowerCase();
3107
+ return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
3108
+ }
3109
+ var URL_ATTRS2 = /* @__PURE__ */ new Set([
3110
+ "href",
3111
+ "src",
3112
+ "action",
3113
+ "formaction",
3114
+ "cite",
3115
+ "poster",
3116
+ "background",
3117
+ "srcset",
3118
+ "ping",
3119
+ "manifest",
3120
+ "data",
3121
+ "xlink:href"
3122
+ ]);
3123
+ function sanitizeUrlLocal(url) {
3124
+ const trimmed = url.replace(/[\x00-\x20\x7f-\x9f]+/g, "").trim();
3125
+ if (!trimmed) return "";
3126
+ const lower = trimmed.toLowerCase();
3127
+ if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:") || lower.startsWith("blob:")) {
3128
+ return "";
3129
+ }
3130
+ return trimmed;
3131
+ }
3132
+ function buildSafeAttrString(attrs) {
3133
+ const out = [];
3134
+ for (const rawKey of Object.keys(attrs)) {
3135
+ if (!Object.hasOwn(attrs, rawKey)) continue;
3136
+ if (!isSafeAttrName2(rawKey)) continue;
3137
+ if (isEventHandlerAttr3(rawKey)) continue;
3138
+ const lowerKey = rawKey.toLowerCase();
3139
+ let value = String(attrs[rawKey]);
3140
+ if (URL_ATTRS2.has(lowerKey)) {
3141
+ value = sanitizeUrlLocal(value);
3142
+ if (!value) continue;
3143
+ }
3144
+ out.push(`${rawKey}="${escapeAttrLocal(value)}"`);
3145
+ }
3146
+ return out.join(" ");
3147
+ }
3148
+ function escapeHtmlLocal(str) {
2802
3149
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2803
3150
  }
2804
- function escapeAttr2(str) {
2805
- return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3151
+ function escapeAttrLocal(str) {
3152
+ return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2806
3153
  }
2807
3154
 
2808
3155
  // src/plugins/plugin.ts