Menu
AkurAI-Build
publicLatest change 1c2159692a31765cd66ed709791ba11468054873 - Initial commit: bunfork v0.1.0 source tree by Olafur Bui
## Decision
Build a Bunfork-native Rust MPA kernel beside `bunfork-static-v1`. Do not extend the static artifact format into SSR or server actions.
This is the smallest truthful “full-stack” boundary: Rust owns request behavior; declarative files own HTML composition. SvelteKit, Next, Rari, Leptos, and Dioxus all separate route rendering from loaders/actions, response control, errors, and layouts—but their implementations also depend on JS/WASM, macros, RSC, hydration, or server-function code generation. Bunfork should copy the observable HTTP contract, not their implementation models. See SvelteKit server loads/actions and endpoints ([SvelteKit fixture](</home/olafurbui/bunfork/.refrepos/sveltekit/packages/adapter-vercel/test/apps/basic/src/routes/server-data/+page.server.ts:1>), [endpoint](</home/olafurbui/bunfork/.refrepos/sveltekit/packages/adapter-vercel/test/apps/basic/src/routes/api/json/+server.ts:1>)); Next’s file conventions ([project structure](</home/olafurbui/bunfork/.refrepos/nextjs/docs/01-app/01-getting-started/02-project-structure.mdx:55>)); Leptos response mutation ([ResponseParts](</home/olafurbui/bunfork/.refrepos/leptos/integrations/axum/src/lib.rs:72>)); and Dioxus typed cookies/errors ([login form](</home/olafurbui/bunfork/.refrepos/dioxus/examples/07-fullstack/login_form.rs:1>), [errors](</home/olafurbui/bunfork/.refrepos/dioxus/examples/07-fullstack/handling_errors.rs:1>)).
## Exact application contract
Compiled Rust application crate:
```rust
pub trait App: Send + Sync + 'static {
fn routes(&self) -> &'static [Route];
}
pub struct Route {
pub pattern: &'static str,
pub methods: &'static [Method],
pub page: &'static str,
pub load: Option<LoadFn>,
pub action: Option<ActionFn>,
}
type LoadFn = fn(RequestContext) -> BoxFuture<'static, Result<Value, AppError>>;
type ActionFn = fn(RequestContext, Form) -> BoxFuture<'static, Result<ActionResult, AppError>>;
```
The kernel exposes:
```rust
pub struct RequestContext {
pub method: Method,
pub uri: Uri,
pub params: BTreeMap<String, String>,
pub query: Query,
pub headers: HeaderMap,
pub cookies: Cookies,
pub body: Bytes,
pub state: Arc<AppState>,
}
pub enum ActionResult {
Render(Value),
Redirect { location: String, status: StatusCode }, // 303 normally
Response(Response),
}
pub enum AppError {
Status { status: StatusCode, public: &'static str },
Redirect { location: String, status: StatusCode },
Internal(anyhow::Error),
}
```
Use ordinary Rust functions and a route table. No procedural macros, plugin discovery, dynamic loading, embedded JS, or generated client protocol.
Declarative tree:
```text
app/
pages/
__layout.html
index.html
account/
__layout.html
index.html
error.html
loading.html # optional; only used for explicit server-side staged rendering
__action.html # optional action result template
404.html
500.html
public/
```
`__layout.html` wraps the nearest child outward-to-root. `template.html` receives:
```text
params, query, pathname, request, data, flash, errors
```
`request` exposes only method, URL, safe headers, and authenticated identity—not secrets, raw authorization headers, or database keys. Templates remain MiniJinja with autoescaping, extending the current request-local rendering contract ([current implementation](</home/olafurbui/bunfork/src/server.rs:376>)).
A page route may have a Rust loader and action registered by exact pattern. Declarative files cannot execute Rust, access the database, perform authorization, or mutate state.
## Request precedence
1. Security middleware: size limits, method checks, path normalization, hidden-path rejection.
2. Exact route match and parameter extraction.
3. Authentication and CSRF checks.
4. Loader for `GET`/`HEAD`.
5. Action for permitted non-`GET` methods.
6. Render page through layouts.
7. Convert `AppError` into redirect/status/error template.
8. Apply security and response headers.
Static routes outrank parameter routes, which outrank catch-alls, preserving the existing router’s useful precedence ([router matching](</home/olafurbui/bunfork/src/server.rs:262>)).
`GET` and `HEAD` render. `POST` invokes the action. `PUT`, `PATCH`, and `DELETE` are opt-in per route. Unsupported methods return `405` with `Allow`. Successful form mutations default to `303 See Other`; validation failures re-render with `422`; missing routes use `404.html`; unexpected failures use `500.html` without exposing error details.
## CSRF and auth
- Require `Origin` or same-site `Referer` validation for browser mutations.
- Require a per-session CSRF token in a hidden form field plus an `HttpOnly`, `SameSite=Lax`, `Secure` cookie.
- Compare tokens in constant time.
- Reject cross-origin state-changing requests, missing tokens, oversized bodies, and malformed form encoding.
- Sessions must be opaque, random, revocable server-side identifiers; never encode identity or authorization in an unsigned cookie.
- Default cookies: `HttpOnly; Secure; SameSite=Lax; Path=/`.
- Authorization runs in Rust before loader/action execution.
- CSRF is not needed for authenticated `GET`; state changes through `GET` are forbidden.
Dioxus demonstrates cookie-based auth and typed request extraction ([source](</home/olafurbui/bunfork/.refrepos/dioxus/examples/07-fullstack/login_form.rs:72>)); Bunfork should provide the security policy centrally rather than expose raw header handling to templates.
## Optional encrypted data
Keep SQLCipher optional and outside the page kernel:
```rust
pub trait Store: Send + Sync {
fn get(&self, key: &str) -> Result<Option<Value>>;
fn put(&self, key: &str, value: &Value) -> Result<()>;
}
```
`AppState` may contain `Arc<dyn Store>`. If configured, use the existing SQLCipher database and secret-loading safeguards ([database API](</home/olafurbui/bunfork/src/db.rs:31>)); otherwise the MPA server starts with no database, key, token, migrations, or vector service. Application data must never be serialized into cookies. Encryption at rest is optional; TLS remains mandatory for deployment.
## Coexistence with `bunfork-static-v1`
Separate commands and artifacts:
```text
bunfork serve --static ... # immutable byte-serving kernel
bunfork serve --app target/app # compiled Rust MPA
bunfork serve --static ... --app target/app # explicit composition
```
Static lookup runs first only for paths declared by the static manifest. Application routes run second. `/api`, `/assets`, and other reserved paths must be explicit; no implicit shadowing.
The static artifact remains byte-preserving and JS-inert as required by reports 03–06 ([static boundary](</home/olafurbui/bunfork/docs/adversarial/06-frontend-protocol.md:1>)). It must never invoke loaders or actions. Conversely, MPA templates must not read or rewrite static files.
CLI values override manifest/config values; config overrides defaults. No framework config, package script, directory naming, or auto-detection changes behavior.
## Black-box tests
- Start MPA mode with no database or secrets.
- `GET`, `HEAD`, permitted actions, `405`, malformed paths, query decoding, repeated parameters, and body-size limits.
- Loader data reaches nested layouts and pages without request leakage under concurrency.
- HTML escaping covers params, query, form values, and errors.
- Valid form: `POST → mutation → 303 → GET`.
- Invalid form: `422`, preserved safe fields, no mutation.
- Missing/invalid CSRF, cross-origin `Origin`, replayed token, and missing session all fail.
- Authenticated loader/action sees identity; unauthenticated access returns `401` or `303` according to route policy.
- Custom nearest error template, root error template, `404`, and generic `500`.
- Cookie flags and redirect headers are exact.
- Encrypted store is absent in page-only mode; wrong SQLCipher key fails startup; no plaintext secret appears in response/logs.
- Static manifest tests from pass 6 remain unchanged.
## Explicit non-goals
No source compatibility with Svelte/Vue/TSX; no RSC, hydration, WASM, streaming, HMR, middleware/plugin system, server-function RPC protocol, automatic route discovery, framework adapters, arbitrary endpoint scripts, or runtime JavaScript.
Compile Rust because it is trusted, typed, and performs loaders/actions/auth/storage. Keep layouts, pages, and error presentation declarative because MiniJinja already provides that capability. This is enough to make “full-stack” truthful without pretending Bunfork implements the ecosystems it only hosts statically.