Signed snippets, hiccup, and a dual-eval loop. The web that should have been.
Project description
evaleval
Browser DOM is modified ONLY through javascript code snippets sent over the wire to the browers eval function.
Backend actions execute ONLY through python code snippets sent over the wire to python's eval function.
import { Idiomorph } from 'idiomorph';
window.Idiomorph = Idiomorph;
const es = new EventSource('/sse');
es.addEventListener('exec', e => eval(e.data));
document.addEventListener('submit', async e => {
e.preventDefault();
const r = await fetch(e.target.action, { method: 'POST', body: new FormData(e.target) });
const t = await r.text();
if (t) eval(t);
});
from evaleval import SnippetExecutionError
@app.post("/")
async def do(request):
form = await request.form()
try:
snippet = signer.verify_snippet(form)
return eval(snippet)
except SnippetExecutionError as e:
return PlainTextResponse(e.message, status_code=e.status_code)
Example: evaleval-todo
evaleval also includes a quick implementation of clojure's hiccup, a data-driven embedded DSL for rendering DOM nodes in an ergonomic way.
Observe this example:
from evaleval import Signer, Three, Two, Selector, MORPH, APPEND, REMOVE
signer = Signer()
def add_form():
return ["form", {"action": "/", "method": "post"},
*signer.snippet_hidden("add($new-todo-body)"),
["input", {"type": "text", "name": "new-todo-body", "placeholder": "what needs doing?"}],
["button", {"type": "submit"}, "add"],
]
All forms have a handler. In a traditional stack, it would be pointed to by a url which points to a routing table which points to a handler function. In evaleval, the handler is embedded into the form itself.
The add($new-todo-body) is sent directly to python's eval with $new-todo-body sent through python's repr and spliced into the python source string. The source string must be an expression not a statement, as it must have a return value. Because as you'll see later, the result of eval is returned directly to the client.
So the handler function from the form is called directly with form arguments. And it returns javscript code. Now how do you write js snippets ergonomically in python? You could write them directly:
def add(text):
t = {"id": uuid.uuid4().hex[:8], "text": text, "done": False}
TODOS.append(t)
escaped = t["text"].replace("`", "\\`")
return PlainTextResponse(f"""
Idiomorph.morph(document.querySelector('#add-form'), `<form id="add-form">...</form>`);
document.querySelector('#todo-list').insertAdjacentHTML('beforeend', `<li id="todo-{t["id"]}">{escaped}</li>`);
Idiomorph.morph(document.querySelector('p.count'), `<p class="count">...</p>`);
console.log('todo added', {text!r});
""")
Ew.
However, I have instead built an embedded data-driven DSL much like specter, which lets you construct js snippets in fluent python. The number we are indexing into is the arity of how deep we can index into until it executes the path, rendering it into a js string. The details of this process are fairly simple and are described here. The indexable arity objects are also just very cool.
The most common arity path pattern is Three[dom selector][action][hiccup data].
def add(text):
t = {"id": uuid.uuid4().hex[:8], "text": text, "done": False}
TODOS.append(t)
return PlainTextResponse(";".join([
Three[Selector("#add-form")][MORPH][add_form()],
Three[Selector("#todo-list")][APPEND][todo_item(t)],
Three[Selector("p.count")][MORPH][remaining_count()],
f"console.log('todo added', {text})"
]))
def delete(todo_id):
TODOS.remove(_find(todo_id))
return PlainTextResponse(";".join([
Two[Selector(f"#todo-{todo_id}")][REMOVE],
Three[Selector("p.count")][MORPH][remaining_count()],
]))
These js snippets go directly into the browser's eval function, so you can do whatever you want.
Two[Selector("#progress-bar")][EvalOn(f"=> $.width = '{width}%'")]
Security
Verify snippet consumes the nonce, so for each GET you can only press each button once. Verify snippet checks the HMAC against the provided snippet, restricting code running on the server to be only code that the server itself produces. So if a user can't do an action, don't sign a snippet with that action for them.
Notice this line in the todo submit form handler:
Three[Selector("#add-form")][MORPH][add_form()],
This is neceseary. Because each action is only allowed exactly once per GET. But you don't want to have to reGET the page to send another todo. So a new nonce is required to be generated by add_form(), which returns hiccup, which is rendered to an htmlstring, which is morphed into the dom at #add-form.
Each snippet is not only a continuation, but also a capability ticket.
uv install evaleval
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file evaleval-0.2.8.tar.gz.
File metadata
- Download URL: evaleval-0.2.8.tar.gz
- Upload date:
- Size: 16.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fe2bf9d52b5684400a5ad701bd9fc67517281cf01676ca1ee8f047d9bc131c4c
|
|
| MD5 |
9f32a6f70cd6a41237e9d4dcd0f477b8
|
|
| BLAKE2b-256 |
379f2f3f4203a50334a52146eddba8f67fd36ff5faa536f368f63053d4ab3da6
|
Provenance
The following attestation bundles were made for evaleval-0.2.8.tar.gz:
Publisher:
publish.yml on tommy-mor/evaleval
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
evaleval-0.2.8.tar.gz -
Subject digest:
fe2bf9d52b5684400a5ad701bd9fc67517281cf01676ca1ee8f047d9bc131c4c - Sigstore transparency entry: 1182858838
- Sigstore integration time:
-
Permalink:
tommy-mor/evaleval@6246e475d08c59f91635c9e55852e0b74fcda90a -
Branch / Tag:
refs/tags/v0.2.8 - Owner: https://github.com/tommy-mor
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6246e475d08c59f91635c9e55852e0b74fcda90a -
Trigger Event:
release
-
Statement type:
File details
Details for the file evaleval-0.2.8-py3-none-any.whl.
File metadata
- Download URL: evaleval-0.2.8-py3-none-any.whl
- Upload date:
- Size: 12.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
470003a41a4931a7183280f3b108142e8fabd55e4ce2a7ed38d2fa49701828b8
|
|
| MD5 |
81edc0891b7d56f1ad7cbe10aae0ae91
|
|
| BLAKE2b-256 |
9ecc13ca9badc8a14a4f495801efd96d04b11f5ab48ff5726017db12368d7ac6
|
Provenance
The following attestation bundles were made for evaleval-0.2.8-py3-none-any.whl:
Publisher:
publish.yml on tommy-mor/evaleval
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
evaleval-0.2.8-py3-none-any.whl -
Subject digest:
470003a41a4931a7183280f3b108142e8fabd55e4ce2a7ed38d2fa49701828b8 - Sigstore transparency entry: 1182858858
- Sigstore integration time:
-
Permalink:
tommy-mor/evaleval@6246e475d08c59f91635c9e55852e0b74fcda90a -
Branch / Tag:
refs/tags/v0.2.8 - Owner: https://github.com/tommy-mor
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6246e475d08c59f91635c9e55852e0b74fcda90a -
Trigger Event:
release
-
Statement type: