fix(js): keep standard sse same-origin
Require non-empty data-slhx-sse values at build time and have the standard runtime reject cross-origin EventSource URLs with slhx:sse-error instead of opening an implicit external stream. req: push/006 req: diagnostics/002
This commit is contained in:
@@ -484,6 +484,9 @@ what a valid business email is.
|
|||||||
### req: push/005
|
### req: push/005
|
||||||
005 Push is one-way server-to-client delivery of EffectBatch. It does not define client mutation, optimistic queues, reconciliation, or conflict handling.
|
005 Push is one-way server-to-client delivery of EffectBatch. It does not define client mutation, optimistic queues, reconciliation, or conflict handling.
|
||||||
|
|
||||||
|
### req: push/006
|
||||||
|
006 `data-slhx-sse` opens only non-empty same-origin SSE URLs by default. Empty static URLs are build errors; cross-origin streams belong to explicit integration code rather than the standard runtime convention.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## sync
|
## sync
|
||||||
|
|||||||
@@ -1041,6 +1041,14 @@ fn reject_invalid_slhx_attr_values(path: &Path, attrs: &[SurfaceAttribute]) -> i
|
|||||||
"expected a non-empty confirmation message",
|
"expected a non-empty confirmation message",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
"data-slhx-sse" if value.trim().is_empty() => {
|
||||||
|
return Err(invalid_slhx_value(
|
||||||
|
path,
|
||||||
|
&attr.name,
|
||||||
|
value,
|
||||||
|
"expected a non-empty same-origin SSE URL",
|
||||||
|
));
|
||||||
|
}
|
||||||
"data-slhx-debounce" | "data-slhx-throttle" | "data-slhx-every"
|
"data-slhx-debounce" | "data-slhx-throttle" | "data-slhx-every"
|
||||||
if !valid_duration(value) =>
|
if !valid_duration(value) =>
|
||||||
{
|
{
|
||||||
@@ -1578,6 +1586,12 @@ fn main() {{
|
|||||||
"data-slhx-confirm",
|
"data-slhx-confirm",
|
||||||
"non-empty",
|
"non-empty",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"empty-sse",
|
||||||
|
r#"<section data-slhx-root="notifications" data-slhx-sse=""></section>"#,
|
||||||
|
"data-slhx-sse",
|
||||||
|
"same-origin SSE URL",
|
||||||
|
),
|
||||||
] {
|
] {
|
||||||
let base = test_dir(&format!("slhx-build-invalid-convention-{case}-test"));
|
let base = test_dir(&format!("slhx-build-invalid-convention-{case}-test"));
|
||||||
let templates = base.join("templates");
|
let templates = base.join("templates");
|
||||||
|
|||||||
@@ -637,7 +637,12 @@
|
|||||||
function bindSse(root) {
|
function bindSse(root) {
|
||||||
const url = root.getAttribute("data-slhx-sse");
|
const url = root.getAttribute("data-slhx-sse");
|
||||||
if (!url || sseSources.has(root) || typeof EventSource === "undefined") return;
|
if (!url || sseSources.has(root) || typeof EventSource === "undefined") return;
|
||||||
const source = new EventSource(new URL(url, location.href).href);
|
const href = new URL(url, location.href);
|
||||||
|
if (href.origin !== location.origin) {
|
||||||
|
emit(root, "slhx:sse-error", url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const source = new EventSource(href.href);
|
||||||
source.addEventListener("slhx", (event) => applySseMessage(root, event));
|
source.addEventListener("slhx", (event) => applySseMessage(root, event));
|
||||||
source.addEventListener("message", (event) => applySseMessage(root, event));
|
source.addEventListener("message", (event) => applySseMessage(root, event));
|
||||||
source.addEventListener("error", () => emit(root, "slhx:sse-error", url));
|
source.addEventListener("error", () => emit(root, "slhx:sse-error", url));
|
||||||
|
|||||||
@@ -218,12 +218,15 @@ fn runtime_refuses_partial_updates_on_fingerprint_mismatch() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_applies_sse_effect_batches_inside_roots() {
|
fn runtime_applies_sse_effect_batches_inside_roots() {
|
||||||
// req: push/001 req: push/003 req: runtime/001
|
// req: push/001 req: push/003 req: push/006 req: runtime/001
|
||||||
let source = slhx_js::RUNTIME_JS;
|
let source = slhx_js::RUNTIME_JS;
|
||||||
|
|
||||||
assert!(source.contains("const sseSources = new WeakMap()"));
|
assert!(source.contains("const sseSources = new WeakMap()"));
|
||||||
assert!(source.contains("const url = root.getAttribute(\"data-slhx-sse\")"));
|
assert!(source.contains("const url = root.getAttribute(\"data-slhx-sse\")"));
|
||||||
assert!(source.contains("new EventSource(new URL(url, location.href).href)"));
|
assert!(source.contains("const href = new URL(url, location.href)"));
|
||||||
|
assert!(source.contains("if (href.origin !== location.origin)"));
|
||||||
|
assert!(source.contains("emit(root, \"slhx:sse-error\", url)"));
|
||||||
|
assert!(source.contains("new EventSource(href.href)"));
|
||||||
assert!(source.contains("source.addEventListener(\"slhx\", (event) => applySseMessage(root, event))"));
|
assert!(source.contains("source.addEventListener(\"slhx\", (event) => applySseMessage(root, event))"));
|
||||||
assert!(source.contains("source.addEventListener(\"message\", (event) => applySseMessage(root, event))"));
|
assert!(source.contains("source.addEventListener(\"message\", (event) => applySseMessage(root, event))"));
|
||||||
assert!(source.contains("applyBatch(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength), root)"));
|
assert!(source.contains("applyBatch(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength), root)"));
|
||||||
|
|||||||
Reference in New Issue
Block a user