sibujs 1.2.0 → 1.4.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 (95) 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 +655 -237
  7. package/dist/build.js +15 -93
  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-CNZ35WI2.js +178 -0
  20. package/dist/chunk-DAHRH4ON.js +331 -0
  21. package/dist/chunk-EBGIRKQY.js +616 -0
  22. package/dist/chunk-EUZND3CB.js +27 -0
  23. package/dist/chunk-F3FA4F32.js +292 -0
  24. package/dist/chunk-JAKHTMQU.js +1000 -0
  25. package/dist/chunk-JCI5M6U6.js +956 -0
  26. package/dist/chunk-KQPDEVVS.js +398 -0
  27. package/dist/chunk-M4NLBH4I.js +725 -0
  28. package/dist/chunk-NEKUBFPT.js +60 -0
  29. package/dist/chunk-NYVAC6P5.js +37 -0
  30. package/dist/chunk-PTQJDMRT.js +146 -0
  31. package/dist/chunk-QWZG56ET.js +2744 -0
  32. package/dist/chunk-TSOKIX5Z.js +654 -0
  33. package/dist/chunk-UHNL42EF.js +2730 -0
  34. package/dist/chunk-VRW3FULF.js +725 -0
  35. package/dist/chunk-WZSPOOER.js +84 -0
  36. package/dist/chunk-YT6HQ6AM.js +14 -0
  37. package/dist/chunk-ZD6OAMTH.js +277 -0
  38. package/dist/chunk-ZWKZCBO6.js +317 -0
  39. package/dist/contracts-DDrwxvJ-.d.cts +245 -0
  40. package/dist/contracts-DDrwxvJ-.d.ts +245 -0
  41. package/dist/contracts-xo5ckdRP.d.cts +240 -0
  42. package/dist/contracts-xo5ckdRP.d.ts +240 -0
  43. package/dist/data.cjs +35 -2
  44. package/dist/data.d.cts +7 -0
  45. package/dist/data.d.ts +7 -0
  46. package/dist/data.js +9 -8
  47. package/dist/devtools.cjs +122 -0
  48. package/dist/devtools.d.cts +69 -461
  49. package/dist/devtools.d.ts +69 -461
  50. package/dist/devtools.js +127 -6
  51. package/dist/ecosystem.cjs +23 -6
  52. package/dist/ecosystem.d.cts +1 -1
  53. package/dist/ecosystem.d.ts +1 -1
  54. package/dist/ecosystem.js +10 -9
  55. package/dist/extras.cjs +1208 -88
  56. package/dist/extras.d.cts +6 -6
  57. package/dist/extras.d.ts +6 -6
  58. package/dist/extras.js +70 -33
  59. package/dist/index.cjs +663 -158
  60. package/dist/index.d.cts +398 -40
  61. package/dist/index.d.ts +398 -40
  62. package/dist/index.js +39 -21
  63. package/dist/introspect-BumjnBKr.d.cts +477 -0
  64. package/dist/introspect-CZrlcaYy.d.ts +477 -0
  65. package/dist/introspect-Cb0zgpi2.d.cts +477 -0
  66. package/dist/introspect-Y2xNXGSf.d.ts +477 -0
  67. package/dist/motion.js +4 -4
  68. package/dist/patterns.cjs +51 -24
  69. package/dist/patterns.d.cts +19 -57
  70. package/dist/patterns.d.ts +19 -57
  71. package/dist/patterns.js +8 -16
  72. package/dist/performance.js +4 -4
  73. package/dist/plugins.cjs +429 -82
  74. package/dist/plugins.d.cts +27 -4
  75. package/dist/plugins.d.ts +27 -4
  76. package/dist/plugins.js +156 -37
  77. package/dist/ssr-4PBXAOO3.js +40 -0
  78. package/dist/ssr-Do_SiVoL.d.cts +201 -0
  79. package/dist/ssr-Do_SiVoL.d.ts +201 -0
  80. package/dist/ssr.cjs +312 -60
  81. package/dist/ssr.d.cts +10 -1
  82. package/dist/ssr.d.ts +10 -1
  83. package/dist/ssr.js +13 -10
  84. package/dist/tagFactory-DaJ0YWX6.d.cts +47 -0
  85. package/dist/tagFactory-DaJ0YWX6.d.ts +47 -0
  86. package/dist/testing.cjs +233 -2
  87. package/dist/testing.d.cts +42 -1
  88. package/dist/testing.d.ts +42 -1
  89. package/dist/testing.js +129 -2
  90. package/dist/ui.cjs +374 -8
  91. package/dist/ui.d.cts +252 -2
  92. package/dist/ui.d.ts +252 -2
  93. package/dist/ui.js +329 -11
  94. package/dist/widgets.js +7 -7
  95. package/package.json +1 -1
package/dist/ssr.cjs CHANGED
@@ -33,6 +33,7 @@ __export(ssr_exports, {
33
33
  createWorkerPool: () => createWorkerPool,
34
34
  defineRemoteComponent: () => defineRemoteComponent,
35
35
  deserializeState: () => deserializeState,
36
+ escapeScriptJson: () => escapeScriptJson,
36
37
  generateStaticSite: () => generateStaticSite,
37
38
  hydrate: () => hydrate,
38
39
  hydrateIslands: () => hydrateIslands,
@@ -78,14 +79,63 @@ function devWarn(message) {
78
79
  }
79
80
  }
80
81
 
82
+ // src/utils/sanitize.ts
83
+ function sanitizeUrl(url) {
84
+ const trimmed = url.replace(/[\x00-\x20\x7f-\x9f]+/g, "").trim();
85
+ if (!trimmed) return "";
86
+ const lower = trimmed.toLowerCase();
87
+ if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:") || lower.startsWith("blob:")) {
88
+ return "";
89
+ }
90
+ return trimmed;
91
+ }
92
+ function sanitizeCSSValue(value) {
93
+ const lower = value.toLowerCase().replace(/\s+/g, "");
94
+ if (lower.includes("url(") || lower.includes("expression(") || lower.includes("javascript:") || lower.includes("-moz-binding")) {
95
+ return "";
96
+ }
97
+ return value;
98
+ }
99
+ var URL_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "cite", "poster", "background", "srcset"]);
100
+ function isUrlAttribute(attr) {
101
+ return URL_ATTRIBUTES.has(attr);
102
+ }
103
+
81
104
  // src/platform/ssr.ts
82
105
  var _isDev2 = isDev();
106
+ var SAFE_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
107
+ function isSafeAttrName(name) {
108
+ return SAFE_ATTR_NAME.test(name);
109
+ }
110
+ function isEventHandlerAttr(name) {
111
+ if (name.length < 3) return false;
112
+ const lower = name.toLowerCase();
113
+ return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
114
+ }
115
+ var URL_ATTRS = /* @__PURE__ */ new Set([
116
+ "href",
117
+ "src",
118
+ "action",
119
+ "formaction",
120
+ "cite",
121
+ "poster",
122
+ "background",
123
+ "srcset",
124
+ "ping",
125
+ "manifest",
126
+ "data",
127
+ "xlink:href"
128
+ ]);
83
129
  function ssrErrorComment(err) {
84
130
  if (_isDev2) {
85
- return `<!--SSR error: ${escapeHtml(err instanceof Error ? err.message : String(err))}-->`;
131
+ const msg = escapeHtml(err instanceof Error ? err.message : String(err));
132
+ return `<!--SSR error: ${safeCommentText(msg)}-->`;
86
133
  }
87
134
  return "<!--SSR error-->";
88
135
  }
136
+ function safeCommentText(text2) {
137
+ return text2.replace(/-->/g, "--&gt;").replace(/--!>/g, "--!&gt;").replace(/<!--/g, "&lt;!--").replace(/--$/g, "--&#45;");
138
+ }
89
139
  var VOID_ELEMENTS = /* @__PURE__ */ new Set([
90
140
  "area",
91
141
  "base",
@@ -116,16 +166,30 @@ function renderToString(element) {
116
166
  return escapeHtml(element.textContent || "");
117
167
  }
118
168
  if (element.nodeType === 8) {
119
- const content = (element.textContent || "").replace(/-->/g, "--&gt;");
120
- return `<!--${content}-->`;
169
+ return `<!--${safeCommentText(element.textContent || "")}-->`;
121
170
  }
122
171
  if (!(element instanceof HTMLElement)) {
123
- return element.textContent || "";
172
+ return escapeHtml(element.textContent || "");
124
173
  }
125
174
  const tag = element.tagName.toLowerCase();
175
+ if (tag === "script" || tag === "style") {
176
+ return _isDev2 ? `<!--ssr:${tag}-stripped-->` : "";
177
+ }
178
+ if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
179
+ return _isDev2 ? "<!--ssr:invalid-tag-->" : "";
180
+ }
126
181
  let html2 = `<${tag}`;
127
182
  for (const attr of Array.from(element.attributes)) {
128
- html2 += ` ${attr.name}="${escapeAttr(attr.value)}"`;
183
+ const rawName = attr.name;
184
+ if (!isSafeAttrName(rawName)) continue;
185
+ if (isEventHandlerAttr(rawName)) continue;
186
+ const lowerName = rawName.toLowerCase();
187
+ let value = attr.value;
188
+ if (URL_ATTRS.has(lowerName)) {
189
+ value = sanitizeUrl(value);
190
+ if (!value) continue;
191
+ }
192
+ html2 += ` ${rawName}="${escapeAttr(value)}"`;
129
193
  }
130
194
  if (element.dataset && !element.dataset.sibuHydrate) {
131
195
  html2 += ` data-sibu-ssr="true"`;
@@ -144,8 +208,25 @@ function renderToString(element) {
144
208
  html2 += `</${tag}>`;
145
209
  return html2;
146
210
  }
147
- function hydrate(component, container) {
211
+ function hydrate(component, container, options = {}) {
148
212
  const clientTree = component();
213
+ if (options.diagnostics) {
214
+ const mismatches = [];
215
+ collectMismatches(container.firstElementChild, clientTree, "", mismatches);
216
+ if (mismatches.length > 0) {
217
+ const first = mismatches[0];
218
+ if (options.onMismatch) {
219
+ options.onMismatch(first);
220
+ } else if (_isDev2) {
221
+ console.warn(
222
+ `[Sibu hydration] ${first.message}
223
+ at ${first.path}
224
+ server: ${first.serverValue}
225
+ client: ${first.clientValue}`
226
+ );
227
+ }
228
+ }
229
+ }
149
230
  hydrateNode(container.firstElementChild, clientTree);
150
231
  container.setAttribute("data-sibu-hydrated", "true");
151
232
  }
@@ -157,9 +238,119 @@ function hydrateNode(serverNode, clientNode) {
157
238
  hydrateNode(serverChildren[i2], clientChildren[i2]);
158
239
  }
159
240
  }
241
+ function collectMismatches(serverNode, clientNode, path2, out, max = 5) {
242
+ if (out.length >= max) return;
243
+ const nodePath = path2 || clientNode?.tagName?.toLowerCase() || "(root)";
244
+ if (!serverNode && clientNode) {
245
+ out.push({
246
+ kind: "child-count",
247
+ path: nodePath,
248
+ serverValue: "(missing)",
249
+ clientValue: clientNode.tagName.toLowerCase(),
250
+ message: "Client rendered a node that the server did not emit."
251
+ });
252
+ return;
253
+ }
254
+ if (serverNode && !clientNode) {
255
+ out.push({
256
+ kind: "child-count",
257
+ path: nodePath,
258
+ serverValue: serverNode.tagName.toLowerCase(),
259
+ clientValue: "(missing)",
260
+ message: "Server rendered a node that the client did not produce."
261
+ });
262
+ return;
263
+ }
264
+ if (!serverNode || !clientNode) return;
265
+ if (serverNode.tagName !== clientNode.tagName) {
266
+ out.push({
267
+ kind: "tag",
268
+ path: nodePath,
269
+ serverValue: serverNode.tagName.toLowerCase(),
270
+ clientValue: clientNode.tagName.toLowerCase(),
271
+ message: "Element tag mismatch \u2014 server and client disagree on the element type."
272
+ });
273
+ return;
274
+ }
275
+ const skipAttrs = /* @__PURE__ */ new Set(["data-sibu-ssr", "data-sibu-hydrated", "data-sibu-island"]);
276
+ const serverAttrs = /* @__PURE__ */ new Map();
277
+ for (const a2 of Array.from(serverNode.attributes)) {
278
+ if (!skipAttrs.has(a2.name)) serverAttrs.set(a2.name, a2.value);
279
+ }
280
+ const clientAttrs = /* @__PURE__ */ new Map();
281
+ for (const a2 of Array.from(clientNode.attributes)) {
282
+ if (!skipAttrs.has(a2.name)) clientAttrs.set(a2.name, a2.value);
283
+ }
284
+ for (const [name, value] of serverAttrs) {
285
+ if (out.length >= max) return;
286
+ if (!clientAttrs.has(name)) {
287
+ out.push({
288
+ kind: "attribute",
289
+ path: `${nodePath}[${name}]`,
290
+ serverValue: value,
291
+ clientValue: "(missing)",
292
+ message: `Attribute "${name}" present on server but missing on client.`
293
+ });
294
+ } else if (clientAttrs.get(name) !== value) {
295
+ out.push({
296
+ kind: "attribute",
297
+ path: `${nodePath}[${name}]`,
298
+ serverValue: value,
299
+ clientValue: clientAttrs.get(name) ?? "",
300
+ message: `Attribute "${name}" differs between server and client.`
301
+ });
302
+ }
303
+ }
304
+ for (const [name, value] of clientAttrs) {
305
+ if (out.length >= max) return;
306
+ if (!serverAttrs.has(name)) {
307
+ out.push({
308
+ kind: "attribute",
309
+ path: `${nodePath}[${name}]`,
310
+ serverValue: "(missing)",
311
+ clientValue: value,
312
+ message: `Attribute "${name}" present on client but missing on server.`
313
+ });
314
+ }
315
+ }
316
+ const serverChildren = Array.from(serverNode.children);
317
+ const clientChildren = Array.from(clientNode.children);
318
+ const max2 = Math.max(serverChildren.length, clientChildren.length);
319
+ for (let i2 = 0; i2 < max2; i2++) {
320
+ if (out.length >= max) return;
321
+ const childPath = `${nodePath} > ${clientChildren[i2]?.tagName?.toLowerCase() ?? serverChildren[i2]?.tagName?.toLowerCase() ?? "?"}:nth-child(${i2 + 1})`;
322
+ collectMismatches(serverChildren[i2] ?? null, clientChildren[i2] ?? null, childPath, out, max);
323
+ }
324
+ }
160
325
  function trustHTML(html2) {
161
326
  return html2;
162
327
  }
328
+ function buildAttrString(attrs, { allowEventHandlers = false } = {}) {
329
+ if (!attrs) return "";
330
+ const out = [];
331
+ for (const rawKey of Object.keys(attrs)) {
332
+ if (!Object.hasOwn(attrs, rawKey)) continue;
333
+ if (!isSafeAttrName(rawKey)) continue;
334
+ if (!allowEventHandlers && isEventHandlerAttr(rawKey)) continue;
335
+ const lowerKey = rawKey.toLowerCase();
336
+ let value = String(attrs[rawKey]);
337
+ if (URL_ATTRS.has(lowerKey)) {
338
+ value = sanitizeUrl(value);
339
+ if (!value) continue;
340
+ }
341
+ out.push(`${rawKey}="${escapeAttr(value)}"`);
342
+ }
343
+ return out.join(" ");
344
+ }
345
+ function isDangerousMetaRefresh(metaProps) {
346
+ const httpEquiv = metaProps["http-equiv"];
347
+ if (typeof httpEquiv !== "string") return false;
348
+ if (httpEquiv.toLowerCase() !== "refresh") return false;
349
+ const content = metaProps.content;
350
+ if (typeof content !== "string") return false;
351
+ const normalized = content.replace(/[\x00-\x20\x7f-\x9f]+/g, "").toLowerCase();
352
+ return normalized.includes("url=javascript:") || normalized.includes("url=data:") || normalized.includes("url=vbscript:") || normalized.includes("url=blob:");
353
+ }
163
354
  function renderToDocument(component, options = {}) {
164
355
  let content;
165
356
  try {
@@ -167,14 +358,22 @@ function renderToDocument(component, options = {}) {
167
358
  } catch (err) {
168
359
  content = ssrErrorComment(err);
169
360
  }
170
- const metaTags = (options.meta || []).map(
171
- (attrs) => `<meta ${Object.entries(attrs).map(([k, v]) => `${k}="${escapeAttr(v)}"`).join(" ")} />`
172
- ).join("\n ");
173
- const linkTags = (options.links || []).map(
174
- (attrs) => `<link ${Object.entries(attrs).map(([k, v]) => `${k}="${escapeAttr(v)}"`).join(" ")} />`
175
- ).join("\n ");
176
- const scriptTags = (options.scripts || []).map((src) => `<script src="${escapeAttr(src)}"></script>`).join("\n ");
177
- const bodyAttrs = options.bodyAttrs ? " " + Object.entries(options.bodyAttrs).map(([k, v]) => `${k}="${escapeAttr(v)}"`).join(" ") : "";
361
+ const metaTags = (options.meta || []).map((attrs) => {
362
+ if (isDangerousMetaRefresh(attrs)) return "";
363
+ const pairs = buildAttrString(attrs);
364
+ return pairs ? `<meta ${pairs} />` : "";
365
+ }).filter(Boolean).join("\n ");
366
+ const linkTags = (options.links || []).map((attrs) => {
367
+ const pairs = buildAttrString(attrs);
368
+ return pairs ? `<link ${pairs} />` : "";
369
+ }).filter(Boolean).join("\n ");
370
+ const scriptTags = (options.scripts || []).map((src) => {
371
+ const safe = sanitizeUrl(String(src));
372
+ if (!safe) return "";
373
+ return `<script src="${escapeAttr(safe)}"></script>`;
374
+ }).filter(Boolean).join("\n ");
375
+ const bodyAttrPairs = buildAttrString(options.bodyAttrs);
376
+ const bodyAttrs = bodyAttrPairs ? ` ${bodyAttrPairs}` : "";
178
377
  return `<!DOCTYPE html>
179
378
  <html>
180
379
  <head>
@@ -207,18 +406,34 @@ async function* renderToStream(element) {
207
406
  return;
208
407
  }
209
408
  if (element.nodeType === 8) {
210
- const content = (element.textContent || "").replace(/-->/g, "--&gt;");
211
- yield `<!--${content}-->`;
409
+ yield `<!--${safeCommentText(element.textContent || "")}-->`;
212
410
  return;
213
411
  }
214
412
  if (!(element instanceof HTMLElement)) {
215
- yield element.textContent || "";
413
+ yield escapeHtml(element.textContent || "");
216
414
  return;
217
415
  }
218
416
  const tag = element.tagName.toLowerCase();
417
+ if (tag === "script" || tag === "style") {
418
+ if (_isDev2) yield `<!--ssr:${tag}-stripped-->`;
419
+ return;
420
+ }
421
+ if (!/^[a-z][a-z0-9-]*$/i.test(tag)) {
422
+ if (_isDev2) yield "<!--ssr:invalid-tag-->";
423
+ return;
424
+ }
219
425
  let openTag = `<${tag}`;
220
426
  for (const attr of Array.from(element.attributes)) {
221
- openTag += ` ${attr.name}="${escapeAttr(attr.value)}"`;
427
+ const rawName = attr.name;
428
+ if (!isSafeAttrName(rawName)) continue;
429
+ if (isEventHandlerAttr(rawName)) continue;
430
+ const lowerName = rawName.toLowerCase();
431
+ let value = attr.value;
432
+ if (URL_ATTRS.has(lowerName)) {
433
+ value = sanitizeUrl(value);
434
+ if (!value) continue;
435
+ }
436
+ openTag += ` ${rawName}="${escapeAttr(value)}"`;
222
437
  }
223
438
  if (VOID_ELEMENTS.has(tag)) {
224
439
  yield `${openTag} />`;
@@ -266,8 +481,9 @@ function hydrateIslands(container, islands) {
266
481
  const markers = container.querySelectorAll("[data-sibu-island]");
267
482
  for (const marker2 of Array.from(markers)) {
268
483
  const id = marker2.getAttribute("data-sibu-island") ?? "";
484
+ if (!Object.hasOwn(islands, id)) continue;
269
485
  const factory = islands[id];
270
- if (!factory) continue;
486
+ if (typeof factory !== "function") continue;
271
487
  const clientTree = factory();
272
488
  hydrateNode(marker2, clientTree);
273
489
  marker2.setAttribute("data-sibu-hydrated", "true");
@@ -279,8 +495,9 @@ function hydrateProgressively(container, islands, options) {
279
495
  const cleanups = [];
280
496
  for (const marker2 of Array.from(markers)) {
281
497
  const id = marker2.getAttribute("data-sibu-island") ?? "";
498
+ if (!Object.hasOwn(islands, id)) continue;
282
499
  const factory = islands[id];
283
- if (!factory) continue;
500
+ if (typeof factory !== "function") continue;
284
501
  const observer = new IntersectionObserver(
285
502
  (entries) => {
286
503
  for (const entry of entries) {
@@ -319,24 +536,33 @@ function ssrSuspense(props) {
319
536
  }));
320
537
  return { element: wrapper, promise };
321
538
  }
539
+ var SAFE_SUSPENSE_ID = /^[A-Za-z0-9_-]+$/;
322
540
  function suspenseSwapScript(id, nonce) {
323
- const safeId = id.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
541
+ if (!SAFE_SUSPENSE_ID.test(id)) {
542
+ throw new Error(
543
+ `[SibuJS SSR] suspenseSwapScript: id must match [A-Za-z0-9_-]+ (got: ${JSON.stringify(id.slice(0, 32))})`
544
+ );
545
+ }
324
546
  const nonceAttr = nonce ? ` nonce="${escapeAttr(nonce)}"` : "";
325
- 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>`;
547
+ 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>`;
326
548
  }
327
- async function* renderToSuspenseStream(element, pendingBoundaries = []) {
549
+ async function* renderToSuspenseStream(element, pendingBoundaries = [], options) {
328
550
  yield* renderToStream(element);
329
551
  if (pendingBoundaries.length > 0) {
330
552
  const resolved = await Promise.all(pendingBoundaries);
331
553
  for (const { id, html: html2 } of resolved) {
332
- yield `<div hidden id="sibu-resolved-${escapeAttr(id)}">${html2}</div>`;
333
- yield suspenseSwapScript(id);
554
+ if (!SAFE_SUSPENSE_ID.test(id)) continue;
555
+ yield `<div hidden id="sibu-resolved-${id}">${html2}</div>`;
556
+ yield suspenseSwapScript(id, options?.nonce);
334
557
  }
335
558
  }
336
559
  }
337
560
  var SSR_DATA_ATTR = "__SIBU_SSR_DATA__";
561
+ function escapeScriptJson(json) {
562
+ return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
563
+ }
338
564
  function serializeState(state, nonce) {
339
- const json = JSON.stringify(state).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
565
+ const json = escapeScriptJson(JSON.stringify(state));
340
566
  const nonceAttr = nonce ? ` nonce="${escapeAttr(nonce)}"` : "";
341
567
  return `<script${nonceAttr}>window.${SSR_DATA_ATTR}=${json}</script>`;
342
568
  }
@@ -351,7 +577,7 @@ function escapeHtml(str) {
351
577
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
352
578
  }
353
579
  function escapeAttr(str) {
354
- return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
580
+ return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
355
581
  }
356
582
 
357
583
  // src/reactivity/track.ts
@@ -622,34 +848,35 @@ function effect(effectFn, options) {
622
848
  };
623
849
  }
624
850
 
625
- // src/utils/sanitize.ts
626
- function sanitizeUrl(url) {
627
- const trimmed = url.replace(/[\x00-\x20\x7f-\x9f]+/g, "").trim();
628
- if (!trimmed) return "";
629
- const lower = trimmed.toLowerCase();
630
- if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:") || lower.startsWith("blob:")) {
631
- return "";
632
- }
633
- return trimmed;
634
- }
635
- function sanitizeCSSValue(value) {
636
- const lower = value.toLowerCase().replace(/\s+/g, "");
637
- if (lower.includes("url(") || lower.includes("expression(") || lower.includes("javascript:") || lower.includes("-moz-binding")) {
638
- return "";
639
- }
640
- return value;
641
- }
642
- var URL_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "cite", "poster", "background", "srcset"]);
643
- function isUrlAttribute(attr) {
644
- return URL_ATTRIBUTES.has(attr);
645
- }
646
-
647
851
  // src/platform/head.ts
648
- var HEAD_URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "content"]);
852
+ var HEAD_URL_ATTRS = /* @__PURE__ */ new Set(["href", "src"]);
649
853
  function sanitizeHeadAttr(key, value) {
650
854
  if (HEAD_URL_ATTRS.has(key)) return sanitizeUrl(value);
651
855
  return value;
652
856
  }
857
+ function isDangerousMetaRefresh2(metaProps) {
858
+ const httpEquiv = metaProps["http-equiv"];
859
+ if (typeof httpEquiv !== "string") return false;
860
+ if (httpEquiv.toLowerCase() !== "refresh") return false;
861
+ const content = metaProps.content;
862
+ if (typeof content !== "string") return false;
863
+ const normalized = content.replace(/[\x00-\x20\x7f-\x9f]+/g, "").toLowerCase();
864
+ return normalized.includes("url=javascript:") || normalized.includes("url=data:") || normalized.includes("url=vbscript:") || normalized.includes("url=blob:");
865
+ }
866
+ var SAFE_HEAD_ATTR_NAME = /^[A-Za-z_:][-A-Za-z0-9_.:]*$/;
867
+ function isEventHandlerAttr2(name) {
868
+ if (name.length < 3) return false;
869
+ const lower = name.toLowerCase();
870
+ return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
871
+ }
872
+ function isSafeHeadAttr(name) {
873
+ if (!SAFE_HEAD_ATTR_NAME.test(name)) return false;
874
+ if (isEventHandlerAttr2(name)) return false;
875
+ return true;
876
+ }
877
+ function escapeScriptJsonLocal(json) {
878
+ return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
879
+ }
653
880
  function Head(props) {
654
881
  const anchor = document.createComment("sibu-head");
655
882
  const managedElements = [];
@@ -676,15 +903,17 @@ function Head(props) {
676
903
  }
677
904
  if (props.meta) {
678
905
  for (const metaProps of props.meta) {
906
+ if (isDangerousMetaRefresh2(metaProps)) continue;
679
907
  const el = document.createElement("meta");
680
908
  for (const [key, value] of Object.entries(metaProps)) {
909
+ if (!isSafeHeadAttr(key)) continue;
681
910
  if (typeof value === "function") {
682
911
  const cleanupFn = effect(() => {
683
- el.setAttribute(key, value());
912
+ el.setAttribute(key, sanitizeHeadAttr(key, value()));
684
913
  });
685
914
  effectCleanups.push(cleanupFn);
686
915
  } else {
687
- el.setAttribute(key, value);
916
+ el.setAttribute(key, sanitizeHeadAttr(key, value));
688
917
  }
689
918
  }
690
919
  document.head.appendChild(el);
@@ -695,6 +924,7 @@ function Head(props) {
695
924
  for (const linkProps of props.link) {
696
925
  const el = document.createElement("link");
697
926
  for (const [key, value] of Object.entries(linkProps)) {
927
+ if (!isSafeHeadAttr(key)) continue;
698
928
  el.setAttribute(key, sanitizeHeadAttr(key, value));
699
929
  }
700
930
  document.head.appendChild(el);
@@ -705,6 +935,7 @@ function Head(props) {
705
935
  for (const scriptProps of props.script) {
706
936
  const el = document.createElement("script");
707
937
  for (const [key, value] of Object.entries(scriptProps)) {
938
+ if (!isSafeHeadAttr(key)) continue;
708
939
  el.setAttribute(key, sanitizeHeadAttr(key, value));
709
940
  }
710
941
  document.head.appendChild(el);
@@ -715,7 +946,10 @@ function Head(props) {
715
946
  const existing = document.head.querySelector("base");
716
947
  if (existing) existing.remove();
717
948
  const el = document.createElement("base");
718
- if (props.base.href) el.href = props.base.href;
949
+ if (props.base.href) {
950
+ const safeHref = sanitizeUrl(props.base.href);
951
+ if (safeHref) el.href = safeHref;
952
+ }
719
953
  if (props.base.target) el.target = props.base.target;
720
954
  document.head.appendChild(el);
721
955
  managedElements.push(el);
@@ -730,7 +964,7 @@ function setStructuredData(data2) {
730
964
  const script2 = document.createElement("script");
731
965
  script2.type = "application/ld+json";
732
966
  script2.setAttribute("data-sibu", "true");
733
- script2.textContent = JSON.stringify(data2);
967
+ script2.textContent = escapeScriptJsonLocal(JSON.stringify(data2));
734
968
  document.head.appendChild(script2);
735
969
  }
736
970
  function setCanonical(url) {
@@ -957,7 +1191,20 @@ function createMiddlewareChain() {
957
1191
 
958
1192
  // src/reactivity/bindAttribute.ts
959
1193
  var _isDev5 = isDev();
1194
+ function isEventHandlerAttr3(name) {
1195
+ if (name.length < 3) return false;
1196
+ const lower = name.toLowerCase();
1197
+ return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
1198
+ }
960
1199
  function bindAttribute(el, attr, getter) {
1200
+ if (isEventHandlerAttr3(attr)) {
1201
+ if (_isDev5)
1202
+ devWarn(
1203
+ `bindAttribute: refusing to bind event-handler attribute "${attr}". Use on:{ ${attr.slice(2)}: fn } instead.`
1204
+ );
1205
+ return () => {
1206
+ };
1207
+ }
961
1208
  function commit() {
962
1209
  let value;
963
1210
  try {
@@ -1200,16 +1447,20 @@ function appendChildren(el, nodes) {
1200
1447
  var tagFactory = (tag, ns) => (first, second) => {
1201
1448
  const el = ns ? document.createElementNS(ns, tag) : document.createElement(tag);
1202
1449
  if (first === void 0) return el;
1203
- if (second === void 0 && typeof first === "string") {
1450
+ if (typeof first === "string") {
1451
+ if (second !== void 0) {
1452
+ el.setAttribute("class", first);
1453
+ appendChildren(el, second);
1454
+ return el;
1455
+ }
1204
1456
  el.textContent = first;
1205
1457
  return el;
1206
1458
  }
1207
- if (second !== void 0) {
1208
- el.setAttribute("class", first);
1209
- appendChildren(el, second);
1459
+ if (typeof first === "number") {
1460
+ el.textContent = String(first);
1210
1461
  return el;
1211
1462
  }
1212
- if (Array.isArray(first) || first instanceof Node) {
1463
+ if (Array.isArray(first) || first instanceof Node || typeof first === "function") {
1213
1464
  appendChildren(el, first);
1214
1465
  return el;
1215
1466
  }
@@ -1218,7 +1469,7 @@ var tagFactory = (tag, ns) => (first, second) => {
1218
1469
  if (pClass != null) applyClass(el, pClass);
1219
1470
  const pId = props.id;
1220
1471
  if (pId != null) el.id = pId;
1221
- const pNodes = props.nodes;
1472
+ const pNodes = second !== void 0 ? second : props.nodes;
1222
1473
  if (pNodes != null) appendChildren(el, pNodes);
1223
1474
  const pOn = props.on;
1224
1475
  if (pOn) {
@@ -1846,6 +2097,7 @@ function isWasmCached(key) {
1846
2097
  createWorkerPool,
1847
2098
  defineRemoteComponent,
1848
2099
  deserializeState,
2100
+ escapeScriptJson,
1849
2101
  generateStaticSite,
1850
2102
  hydrate,
1851
2103
  hydrateIslands,
package/dist/ssr.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- export { T as TrustedHTML, c as collectStream, d as deserializeState, h as hydrate, a as hydrateIslands, b as hydrateProgressively, i as island, r as renderToDocument, e as renderToReadableStream, f as renderToStream, g as renderToString, j as renderToSuspenseStream, k as resetSSRState, s as serializeState, l as ssrSuspense, m as suspenseSwapScript, t as trustHTML } from './ssr-BA6sxxUd.cjs';
1
+ export { H as HydrateOptions, a as HydrationMismatch, T as TrustedHTML, c as collectStream, d as deserializeState, e as escapeScriptJson, h as hydrate, b as hydrateIslands, f as hydrateProgressively, i as island, r as renderToDocument, g as renderToReadableStream, j as renderToStream, k as renderToString, l as renderToSuspenseStream, m as resetSSRState, s as serializeState, n as ssrSuspense, o as suspenseSwapScript, t as trustHTML } from './ssr-Do_SiVoL.cjs';
2
2
 
3
3
  interface HeadProps {
4
4
  title?: string | (() => string);
@@ -18,6 +18,15 @@ interface HeadProps {
18
18
  declare function Head(props: HeadProps): Comment;
19
19
  /**
20
20
  * Sets structured data (JSON-LD) for SEO.
21
+ *
22
+ * Security: the serialized JSON is passed through `escapeScriptJsonLocal`
23
+ * which unicode-escapes `<`, `>`, `&`, `U+2028`, and `U+2029`. This is
24
+ * defense-in-depth: when the element is inserted via `document.createElement`
25
+ * + `textContent` the browser will NOT re-parse the body, so `</script>`
26
+ * cannot break out of the tag at insertion time. However, tools that
27
+ * later serialize `document.head.innerHTML` DO re-parse, and the server
28
+ * side of any SSR roundtrip would see the raw text. Escaping here makes
29
+ * both paths safe.
21
30
  */
22
31
  declare function setStructuredData(data: Record<string, unknown>): void;
23
32
  /**
package/dist/ssr.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { T as TrustedHTML, c as collectStream, d as deserializeState, h as hydrate, a as hydrateIslands, b as hydrateProgressively, i as island, r as renderToDocument, e as renderToReadableStream, f as renderToStream, g as renderToString, j as renderToSuspenseStream, k as resetSSRState, s as serializeState, l as ssrSuspense, m as suspenseSwapScript, t as trustHTML } from './ssr-BA6sxxUd.js';
1
+ export { H as HydrateOptions, a as HydrationMismatch, T as TrustedHTML, c as collectStream, d as deserializeState, e as escapeScriptJson, h as hydrate, b as hydrateIslands, f as hydrateProgressively, i as island, r as renderToDocument, g as renderToReadableStream, j as renderToStream, k as renderToString, l as renderToSuspenseStream, m as resetSSRState, s as serializeState, n as ssrSuspense, o as suspenseSwapScript, t as trustHTML } from './ssr-Do_SiVoL.js';
2
2
 
3
3
  interface HeadProps {
4
4
  title?: string | (() => string);
@@ -18,6 +18,15 @@ interface HeadProps {
18
18
  declare function Head(props: HeadProps): Comment;
19
19
  /**
20
20
  * Sets structured data (JSON-LD) for SEO.
21
+ *
22
+ * Security: the serialized JSON is passed through `escapeScriptJsonLocal`
23
+ * which unicode-escapes `<`, `>`, `&`, `U+2028`, and `U+2029`. This is
24
+ * defense-in-depth: when the element is inserted via `document.createElement`
25
+ * + `textContent` the browser will NOT re-parse the body, so `</script>`
26
+ * cannot break out of the tag at insertion time. However, tools that
27
+ * later serialize `document.head.innerHTML` DO re-parse, and the server
28
+ * side of any SSR roundtrip would see the raw text. Escaping here makes
29
+ * both paths safe.
21
30
  */
22
31
  declare function setStructuredData(data: Record<string, unknown>): void;
23
32
  /**
package/dist/ssr.js CHANGED
@@ -22,10 +22,11 @@ import {
22
22
  wasm,
23
23
  worker,
24
24
  workerFn
25
- } from "./chunk-VMVDTCXB.js";
25
+ } from "./chunk-2BYQDGN3.js";
26
26
  import {
27
27
  collectStream,
28
28
  deserializeState,
29
+ escapeScriptJson,
29
30
  hydrate,
30
31
  hydrateIslands,
31
32
  hydrateProgressively,
@@ -40,15 +41,16 @@ import {
40
41
  ssrSuspense,
41
42
  suspenseSwapScript,
42
43
  trustHTML
43
- } from "./chunk-WUHJISPP.js";
44
- import "./chunk-GCOK2LC3.js";
45
- import "./chunk-B7SWRFUT.js";
46
- import "./chunk-23VV7YD3.js";
47
- import "./chunk-6SA3QQES.js";
48
- import "./chunk-CHJ27IGK.js";
49
- import "./chunk-V2XTI523.js";
50
- import "./chunk-UNXCEF6S.js";
51
- import "./chunk-MLKGABMK.js";
44
+ } from "./chunk-3X2YG6YM.js";
45
+ import "./chunk-32DY64NT.js";
46
+ import "./chunk-F3FA4F32.js";
47
+ import "./chunk-PTQJDMRT.js";
48
+ import "./chunk-CMBFNA7L.js";
49
+ import "./chunk-CHF5OHIA.js";
50
+ import "./chunk-EUZND3CB.js";
51
+ import "./chunk-WZSPOOER.js";
52
+ import "./chunk-ZD6OAMTH.js";
53
+ import "./chunk-5X6PP2UK.js";
52
54
  export {
53
55
  Head,
54
56
  clearWasmCache,
@@ -63,6 +65,7 @@ export {
63
65
  createWorkerPool,
64
66
  defineRemoteComponent,
65
67
  deserializeState,
68
+ escapeScriptJson,
66
69
  generateStaticSite,
67
70
  hydrate,
68
71
  hydrateIslands,