Dstar.Scripts (dstar v0.3.0)

Copy Markdown View Source

Executes JavaScript on the client via SSE.

Appends a <script> tag to the body using datastar-patch-elements.

conn |> execute("alert('Hello!')")
conn |> execute("console.log('debug')", auto_remove: false)

Summary

Functions

Logs a message to the browser console via SSE.

Executes JavaScript on the client by appending a script tag to the body.

Redirects the client to the given URL via JavaScript.

Functions

console_log(conn, message, opts \\ [])

@spec console_log(Plug.Conn.t(), term(), keyword()) :: Plug.Conn.t()

Logs a message to the browser console via SSE.

Options

  • :level - Console method: :log, :warn, :error, :info, :debug (default: :log). Anything else raises — a mistyped level used to fall back to :log silently, which hid the typo rather than the message.
  • Plus all options from execute/3

Examples

conn |> console_log("Debug message")
conn |> console_log("Warning!", level: :warn)
conn |> console_log(%{user: "alice"}, level: :info)

execute(conn, script, opts \\ [])

@spec execute(Plug.Conn.t(), String.t(), keyword()) :: Plug.Conn.t()

Executes JavaScript on the client by appending a script tag to the body.

Options

  • :auto_remove - Remove script tag after execution (default: true)
  • :attributes - Map of additional script tag attributes
  • :event_id - Event ID for client tracking
  • :retry - Retry duration in milliseconds

Examples

# Simple script execution
conn |> execute("alert('Hello!')")

# Keep script in DOM
conn |> execute("window.myVar = 42", auto_remove: false)

# ES module script
conn |> execute("import {...} from 'module'", attributes: %{type: "module"})

redirect(conn, url, opts \\ [])

@spec redirect(Plug.Conn.t(), String.t(), keyword()) :: Plug.Conn.t()

Redirects the client to the given URL via JavaScript.

Uses setTimeout so the browser records a history entry.

Destination policy

Default: same-origin, path-absolute destinations only. Query-only and fragment-only references (?x=1, #frag) are also allowed — they stay on the current path. Validation uses parsed URL components (via URI), not a case-sensitive prefix check, and raises ArgumentError before any SSE patch is emitted.

Allowed without opt-in:

conn |> redirect("/workspaces")
conn |> redirect("/search?q=1#results")
conn |> redirect("?next=1")
conn |> redirect("#section")
# same-origin absolute URL (scheme/host/port match the request)
conn |> redirect("http://www.example.com/path")

Rejected:

  • javascript:, data:, vbscript: (any case)
  • those schemes hidden behind leading, control, or Unicode whitespace (" DATA:…", "JavaScript:…", "java\tscript:…")
  • protocol-relative URLs (//evil.example, including tab/LF/CR smuggled into a path so browsers parse / /evil as //evil)
  • URLs with userinfo (https://trusted.example@evil.example/)
  • off-origin http/https (including https://example.com)

Off-origin http/https needs an explicit opt-in — either external: true or a host allowlist. Hosts are matched exactly (case-insensitive) against the parsed host. Dangerous schemes, protocol-relative URLs, and userinfo are still rejected.

conn |> redirect("https://ok.example/docs", external: true)
conn |> redirect("https://ok.example/docs", allow: ["ok.example"])

Jason.encode!/1 (plus the </script neutralizer in execute/3) prevents the URL from breaking out of the generated JavaScript string. It does not make the destination itself safe — do not pass an untrusted return_to (or similar) without deciding the policy. For raw JavaScript, use execute/3; that remains a trusted-code API.

Options

  • :external - when true, allow any http/https URL
  • :allow - list of host strings permitted for http/https
  • plus all options from execute/3