|
|
@@ -0,0 +1,530 @@
|
|
|
+# Getting Started with Statum
|
|
|
+
|
|
|
+## What is Statum
|
|
|
+
|
|
|
+Statum is a stateful web application framework for Vala, built on top of
|
|
|
+[Astralis](https://github.com/search/astralis) (HTTP server),
|
|
|
+[Invercargill](https://github.com/search/invercargill) (data structures / JSON),
|
|
|
+and [Inversion](https://github.com/search/inversion) (dependency injection).
|
|
|
+
|
|
|
+It provides:
|
|
|
+
|
|
|
+- **Server-rendered HTML pages** composed from templates and fragments at build
|
|
|
+ time (`statum-mkpstm`), precompressed with gzip/zstd/brotli and served with
|
|
|
+ encoding negotiation and ETags.
|
|
|
+- **Client-carried state** — signed and (for private data) encrypted snapshots
|
|
|
+ that the browser holds and sends back, so the server is stateless between
|
|
|
+ requests (no session store, no database for ephemeral state).
|
|
|
+- **Entrypoint hydration** — after a page loads, a lightweight JS client fetches
|
|
|
+ directives that set the initial slot data and bind the DOM.
|
|
|
+- **GUID actions** — POST/GET handlers auto-mapped to `/_statum/action/{guid}`,
|
|
|
+ invoked from the client via `stm-action` attributes, carrying strongly-typed
|
|
|
+ private data.
|
|
|
+- **Realtime updates** — a SharedWorker-backed SSE channel pushes slot changes
|
|
|
+ to subscribed tabs.
|
|
|
+
|
|
|
+### Architecture at a glance
|
|
|
+
|
|
|
+```
|
|
|
+ Browser Server
|
|
|
+ ─────── ──────
|
|
|
+ GET / ──────────────────────► StatumPage (precompressed HTML)
|
|
|
+ page loads, statum.js boots
|
|
|
+ GET /_statum/entrypoint ────► StatumEntrypoint → directives (set/clear/...)
|
|
|
+ client applies directives
|
|
|
+ client binds stm-* attributes
|
|
|
+
|
|
|
+ POST /_statum/action/{guid} ► StatumAction → directives
|
|
|
+ (with X-Statum-Slot headers) (mutate state, push to channels)
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Quick Start Examples
|
|
|
+
|
|
|
+### 1. A page with an entrypoint
|
|
|
+
|
|
|
+**`home.html`** — the page template, composed with a layout:
|
|
|
+
|
|
|
+```html
|
|
|
+<!DOCTYPE html>
|
|
|
+<html>
|
|
|
+<head>
|
|
|
+ <pstm-uri>/</pstm-uri>
|
|
|
+ <pstm-template>main</pstm-template>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+ <p>Counter: <span stm-text="counter.value">0</span></p>
|
|
|
+ <button stm-action="counter.bump">Bump</button>
|
|
|
+</body>
|
|
|
+</html>
|
|
|
+```
|
|
|
+
|
|
|
+**`main.html`** — the layout template:
|
|
|
+
|
|
|
+```html
|
|
|
+<!DOCTYPE html>
|
|
|
+<html>
|
|
|
+<head>
|
|
|
+ <pstm-name>main</pstm-name>
|
|
|
+ <pstm-title>My App</pstm-title>
|
|
|
+ <script pstm-res-src="statum.js"></script>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+ <header><h1>My App</h1></header>
|
|
|
+ <main><pstm-content></pstm-content></main>
|
|
|
+</body>
|
|
|
+</html>
|
|
|
+```
|
|
|
+
|
|
|
+**`HomeEntrypoint.vala`** — hydrates the page:
|
|
|
+
|
|
|
+```vala
|
|
|
+public class CounterPublic : Object {
|
|
|
+ public int value { get; set; }
|
|
|
+ public Model.ActionDto bump { get; set; }
|
|
|
+}
|
|
|
+
|
|
|
+public class HomeEntrypoint : StatumEntrypoint {
|
|
|
+ public override async DirectiveBuilder handle() throws GLib.Error {
|
|
|
+ var counter = new CounterPublic();
|
|
|
+ counter.value = 0;
|
|
|
+ counter.bump = action_registry.author_private<BumpAction, BumpPrivate>(new BumpPrivate());
|
|
|
+ return directives().set_typed<CounterPublic>("counter", Scope.PAGE, counter);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**`Main.vala`** — wires it all up:
|
|
|
+
|
|
|
+```vala
|
|
|
+void main(string[] args) {
|
|
|
+ var app = new WebApplication(8080);
|
|
|
+ app.add_module<StatumModule>();
|
|
|
+
|
|
|
+ var statum = app.configure_with<StatumConfigurator>();
|
|
|
+ statum.add_page<HomePage, HomeEntrypoint>(); // route from <pstm-uri>
|
|
|
+ statum.action<BumpAction>(); // GUID auto-assigned
|
|
|
+ app.run();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 2. A guarded page
|
|
|
+
|
|
|
+```html
|
|
|
+<head>
|
|
|
+ <pstm-uri>/dashboard</pstm-uri>
|
|
|
+ <pstm-template>main</pstm-template>
|
|
|
+ <link rel="stm-guard" require="auth" href="/" pre-entrypoint>
|
|
|
+</head>
|
|
|
+```
|
|
|
+
|
|
|
+The guard redirects unauthenticated visitors to `/` before the entrypoint loads.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## HTML Templates
|
|
|
+
|
|
|
+Statum pages are HTML files processed at build time by `statum-mkpstm`.
|
|
|
+
|
|
|
+### Build-time directives (`pstm-*`)
|
|
|
+
|
|
|
+All `pstm-*` elements **must appear inside `<head>`** (except `pstm-content` and
|
|
|
+`pstm-fragment`, which live in `<body>`).
|
|
|
+
|
|
|
+| Directive | Where | Purpose |
|
|
|
+|---|---|---|
|
|
|
+| `<pstm-uri>/path</pstm-uri>` | head | The page's route. **Required.** Supports templates: `/cats/{species}`. |
|
|
|
+| `<pstm-template>main</pstm-template>` | head | Wraps this page in the named template (by `<pstm-name>`). |
|
|
|
+| `<pstm-name>main</pstm-name>` | head | Names this file so pages/fragments can reference it. |
|
|
|
+| `<pstm-title>…</pstm-title>` | head | Page title, merged up the template chain. |
|
|
|
+| `<pstm-content></pstm-content>` | body (template) | The outlet where the child page's body is inserted. |
|
|
|
+| `<pstm-fragment name="card" />` | body | Inlines a fragment (component) by name. |
|
|
|
+| `pstm-materialise-as="div"` | `<body>` attr | When this body is composited into a template or fragment, it is wrapped in the named element. |
|
|
|
+| `pstm-res-src="logo.png"` | any attr | Rewrites to `src="/_statum/resource/logo.png"`. Also `pstm-res-href`, etc. |
|
|
|
+
|
|
|
+### Head merging
|
|
|
+
|
|
|
+When a page is composed into a template (or a fragment is inlined), the child's
|
|
|
+`<head>` children are **merged** into the parent's `<head>`. This lets pages,
|
|
|
+templates, and fragments each declare their own `<style>`, `<script>`, `<meta>`,
|
|
|
+`<link>` tags.
|
|
|
+
|
|
|
+### Fragments (components)
|
|
|
+
|
|
|
+A fragment is an HTML file with a `<pstm-name>` and a `<body>` (no
|
|
|
+`<pstm-content>`). It is inlined wherever a `<pstm-fragment name="…">` appears.
|
|
|
+Fragment `<head>` tags merge into the composed document. If the fragment's
|
|
|
+`<body>` has `pstm-materialise-as`, the body becomes that element.
|
|
|
+
|
|
|
+### Guards (`stm-guard`)
|
|
|
+
|
|
|
+A `<link rel="stm-guard">` in `<head>` redirects the page when a predicate fails:
|
|
|
+
|
|
|
+```html
|
|
|
+<link rel="stm-guard" require="auth" href="/login" pre-entrypoint>
|
|
|
+```
|
|
|
+
|
|
|
+- `require` — an eval predicate (same language as `stm-if`). When falsy, redirect
|
|
|
+ to `href`.
|
|
|
+- `pre-entrypoint` — also evaluate on initial load (using hydrated state) before
|
|
|
+ the entrypoint request. Without it, the guard evaluates only on render cycles
|
|
|
+ (after hydration, and on every state change).
|
|
|
+
|
|
|
+Guards are client-side; they are a display/UX convenience, not a security
|
|
|
+boundary (authoritative access control lives at the entrypoint/action level).
|
|
|
+
|
|
|
+### Build tools
|
|
|
+
|
|
|
+```bash
|
|
|
+# Generate a page (from HTML → Vala StatumPage subclass)
|
|
|
+statum-mkpstm -o HomePage.vala -n HomePage --ns=MyApp home.html
|
|
|
+
|
|
|
+# Generate a precompressed resource (CSS, JS, images)
|
|
|
+statum-mkres -o LogoResource.vala -n logo.png -c image/png logo.png
|
|
|
+
|
|
|
+# Generate static signing/encryption keys for web-config.json
|
|
|
+statum-genkeys
|
|
|
+```
|
|
|
+
|
|
|
+In Meson, these are wired as `custom_target`s (see `example/meson.build`).
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Slots and Snapshots
|
|
|
+
|
|
|
+A **slot** is a unit of application state, identified by a type name and a
|
|
|
+random key. The server creates slots; the client holds them and sends them back
|
|
|
+on subsequent requests.
|
|
|
+
|
|
|
+### Slot scopes
|
|
|
+
|
|
|
+| Scope | Persistence | Use case |
|
|
|
+|---|---|---|
|
|
|
+| `Scope.PAGE` | In-memory, gone on navigate | Transient page data |
|
|
|
+| `Scope.FLOW` | Per-tab (`sessionStorage`) | Multi-step flow within a tab |
|
|
|
+| `Scope.SESSION` | Per-browser (`localStorage`) | Login state, preferences |
|
|
|
+| `Scope.DEVICE` | Per-device (`localStorage`) | Long-lived settings |
|
|
|
+
|
|
|
+### Snapshots and frames
|
|
|
+
|
|
|
+A slot's data is a **snapshot** (public + private properties), wrapped in a
|
|
|
+**frame** (signed by the server). The client holds frames and sends them back:
|
|
|
+
|
|
|
+- **Reference form** (`key=…; as_at=…`): sent while the snapshot is still cached
|
|
|
+ server-side (within `transmit_after`).
|
|
|
+- **Base64 frame form**: the full signed frame, sent when the server needs the
|
|
|
+ data re-sent (always-send slots, or after a cache miss).
|
|
|
+
|
|
|
+### The `X-Statum-Slot` header
|
|
|
+
|
|
|
+Browsers coalesce multiple headers into one comma-separated value. The server
|
|
|
+splits on `,` to recover individual slots. The client sends one `X-Statum-Slot`
|
|
|
+header per held slot on every entrypoint/action/slots request.
|
|
|
+
|
|
|
+### Private data
|
|
|
+
|
|
|
+Private slot data is encrypted (X25519-Seal + Ed25519 signature) under a
|
|
|
+namespace. For action private data, the namespace is the action type name (so
|
|
|
+one action cannot decrypt another's private blob).
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Entrypoint Handlers
|
|
|
+
|
|
|
+An entrypoint is a `StatumEntrypoint` subclass that returns directives to
|
|
|
+hydrate a page after it loads. The framework matches the `uri` query parameter
|
|
|
+against the page's `<pstm-uri>` route and dispatches to the bound entrypoint.
|
|
|
+
|
|
|
+```vala
|
|
|
+public class HomeEntrypoint : StatumEntrypoint {
|
|
|
+ public override async DirectiveBuilder handle() throws GLib.Error {
|
|
|
+ // Typed model with an action reference field
|
|
|
+ var counter = new CounterPublic();
|
|
|
+ counter.value = 0;
|
|
|
+ counter.bump = action_registry.author_private<BumpAction, BumpPrivate>(new BumpPrivate { delta = 1 });
|
|
|
+
|
|
|
+ return directives()
|
|
|
+ .set_typed<CounterPublic>("counter", Scope.PAGE, counter)
|
|
|
+ .set_typed<PagePublic>("page", Scope.PAGE, new PagePublic { login = action_registry.author<LoginAction>() });
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Available fields (injected)
|
|
|
+
|
|
|
+| Field | Type | Description |
|
|
|
+|---|---|---|
|
|
|
+| `request` | `StatumRequest` | URI, route params, held slots, query, form |
|
|
|
+| `state_service` | `StateService` | Low-level slot create/sign/update/clear |
|
|
|
+| `channel_service` | `ChannelService` | Realtime push (set/clear) |
|
|
|
+| `action_registry` | `ActionRegistry` | Author action references |
|
|
|
+| `encryption_provider` | `EncryptionProvider` | Seal/unseal private data |
|
|
|
+
|
|
|
+### Reading held slots (typed)
|
|
|
+
|
|
|
+```vala
|
|
|
+CounterPublic? cart = request.held_as<CounterPublic>("counter");
|
|
|
+string? species = request.route("species"); // from /cats/{species}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Action Handlers
|
|
|
+
|
|
|
+Actions are POST/GET handlers auto-mapped to GUID endpoints. They receive the
|
|
|
+client's held slots (via `X-Statum-Slot`) and optional private data (via
|
|
|
+`X-Statum-Private`), mutate state, and return directives.
|
|
|
+
|
|
|
+### Basic action (no typed private)
|
|
|
+
|
|
|
+```vala
|
|
|
+public class LogoutAction : StatumAction {
|
|
|
+ public override async DirectiveBuilder handle() throws GLib.Error {
|
|
|
+ HeldSlot auth;
|
|
|
+ if (request.held.try_get("auth", out auth)) {
|
|
|
+ yield state_service.clear_slot(auth.key);
|
|
|
+ return directives().clear(auth.key).navigate("/");
|
|
|
+ }
|
|
|
+ return directives().navigate("/");
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Typed action (with strongly-typed private data)
|
|
|
+
|
|
|
+```vala
|
|
|
+public class BumpAction : TypedStatumAction<BumpPrivate> {
|
|
|
+ public override async DirectiveBuilder handle() throws GLib.Error {
|
|
|
+ var delta = request_private != null ? ((!)request_private).delta : 1;
|
|
|
+ return directives().update_held<CounterPublic>("counter", c => c.value += delta);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+`TypedStatumAction<TPrivate>` decrypts the `X-Statum-Private` header into
|
|
|
+`request_private` (typed `TPrivate`). If the private data is absent or cannot be
|
|
|
+decrypted, the framework returns an `error` directive (code `"bad_private"`) and
|
|
|
+does **not** call `handle()`. Override `private_error_message` /
|
|
|
+`private_error_code` to customise.
|
|
|
+
|
|
|
+### Authoring action references
|
|
|
+
|
|
|
+Actions are not addressed by URL — they are embedded in slot public data as
|
|
|
+references, and invoked from the client via `stm-action`:
|
|
|
+
|
|
|
+```vala
|
|
|
+// No private data
|
|
|
+var ref = action_registry.author<LoginAction>();
|
|
|
+
|
|
|
+// With typed private data
|
|
|
+var ref = action_registry.author_private<CheckoutAction, CheckoutPrivate>(
|
|
|
+ new CheckoutPrivate { order_id = "abc" });
|
|
|
+```
|
|
|
+
|
|
|
+The returned `ActionDto` is embedded in a typed model's field
|
|
|
+(`public Model.ActionDto bump { get; set; }`) or set directly on a
|
|
|
+`PropertyDictionary`. It round-trips automatically through `update_held`.
|
|
|
+
|
|
|
+### Registration
|
|
|
+
|
|
|
+```vala
|
|
|
+statum.action<LoginAction>();
|
|
|
+statum.action<LogoutAction>();
|
|
|
+statum.action<BumpAction>();
|
|
|
+```
|
|
|
+
|
|
|
+No route is specified — a GUID is auto-assigned at registration.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## JS API Reference
|
|
|
+
|
|
|
+The Statum client (`statum.js`) is embedded in the library and served from
|
|
|
+`/_statum/resource/statum.js`. Include it in your template:
|
|
|
+
|
|
|
+```html
|
|
|
+<script pstm-res-src="statum.js"></script>
|
|
|
+```
|
|
|
+
|
|
|
+It auto-initialises on DOMContentLoaded.
|
|
|
+
|
|
|
+### Configuration overrides (body attributes)
|
|
|
+
|
|
|
+```html
|
|
|
+<body stm-entrypoint="/custom-entrypoint"
|
|
|
+ stm-slots="/custom-slots"
|
|
|
+ stm-channel="/custom-channel"
|
|
|
+ stm-worker="/custom-worker">
|
|
|
+```
|
|
|
+
|
|
|
+### HTML binding attributes (`stm-*`)
|
|
|
+
|
|
|
+| Attribute | Description |
|
|
|
+|---|---|
|
|
|
+| `stm-text="counter.value"` | Sets the element's text content from the expression. |
|
|
|
+| `stm-html="cart.summary"` | Sets the element's inner HTML. |
|
|
|
+| `stm-if="auth != null"` | Shows/hides the element (and its `stm-else-if`/`stm-else` siblings). |
|
|
|
+| `stm-else-if="auth.role == 'admin'"` | Next sibling of `stm-if`/`stm-else-if`. |
|
|
|
+| `stm-else` | Next sibling of `stm-if`/`stm-else-if`. |
|
|
|
+| `stm-class.active="cart.count > 0"` | Toggles the class `active` based on the predicate. |
|
|
|
+| `stm-disabled="!user.canSubmit"` | Disables the element + descendants when truthy. |
|
|
|
+| `stm-action="cart.checkout"` | Invokes the action reference resolved from state. On a `<form>`, collects form data; on a button, fires on click. |
|
|
|
+| `stm-data-key="value"` | Provides data to the enclosing `stm-action`. |
|
|
|
+
|
|
|
+### Guards
|
|
|
+
|
|
|
+```html
|
|
|
+<link rel="stm-guard" require="auth" href="/login" pre-entrypoint>
|
|
|
+```
|
|
|
+
|
|
|
+Evaluated on every render cycle; `pre-entrypoint` also evaluates before the
|
|
|
+entrypoint fetch. On failure, the page redirects to `href`.
|
|
|
+
|
|
|
+### Boot states
|
|
|
+
|
|
|
+```html
|
|
|
+<div stm-preloader>Loading…</div> <!-- shown until entrypoint loads -->
|
|
|
+<div stm-content> <!-- hidden until entrypoint loads -->
|
|
|
+ Page content here.
|
|
|
+</div>
|
|
|
+```
|
|
|
+
|
|
|
+### Public JS API
|
|
|
+
|
|
|
+```js
|
|
|
+// Read current state
|
|
|
+var cart = statum.state("cart"); // → { value: 0, bump: {…} }
|
|
|
+
|
|
|
+// Listen for changes
|
|
|
+statum.on("cart", function(data, old) {
|
|
|
+ console.log("cart changed", data);
|
|
|
+});
|
|
|
+
|
|
|
+// Perform an action programmatically
|
|
|
+statum.act("cart.checkout", { qty: 2 })
|
|
|
+ .then(function(directives) { … })
|
|
|
+ .catch(function(err) { … });
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Vala API Reference
|
|
|
+
|
|
|
+### `StatumModule`
|
|
|
+
|
|
|
+Registers the framework services and endpoints. Add it to your application:
|
|
|
+
|
|
|
+```vala
|
|
|
+app.add_module<StatumModule>();
|
|
|
+```
|
|
|
+
|
|
|
+### `StatumConfigurator`
|
|
|
+
|
|
|
+Binds pages, actions, and resources:
|
|
|
+
|
|
|
+```vala
|
|
|
+var statum = app.configure_with<StatumConfigurator>();
|
|
|
+statum.add_page<HomePage, HomeEntrypoint>(); // page + entrypoint
|
|
|
+statum.add_page<DashboardPage, DashboardEntrypoint>();
|
|
|
+statum.add_static_page<AboutPage>(); // page, no entrypoint
|
|
|
+statum.action<LoginAction>(); // GUID action
|
|
|
+statum.action<LogoutAction>();
|
|
|
+statum.add_resource<LogoResource>(); // CSS/JS/image resource
|
|
|
+```
|
|
|
+
|
|
|
+Routes are derived from `<pstm-uri>` (not passed manually). A page without a
|
|
|
+`<pstm-uri>` fails at startup.
|
|
|
+
|
|
|
+### `DirectiveBuilder`
|
|
|
+
|
|
|
+Started via `directives()` on any handler. Fluent, self-returning:
|
|
|
+
|
|
|
+| Method | Description |
|
|
|
+|---|---|
|
|
|
+| `.set(Slot)` | Signs a fresh slot → `set` directive. |
|
|
|
+| `.set_typed<T>(type, scope, model)` | Creates + signs from a typed public model. |
|
|
|
+| `.set_private_typed<T1,T2>(type, scope, pub, priv)` | Creates + signs from typed public + private models. |
|
|
|
+| `.update(key, State)` | Mutates + pushes to channels + signs → `set`. |
|
|
|
+| `.update_held<T>(type, c => …)` | Typed read-mutate-write of a held slot (preserves action refs). |
|
|
|
+| `.clear(key)` | `clear` directive. |
|
|
|
+| `.navigate(uri)` | Terminal: redirect the client. |
|
|
|
+| `.notify(kind, message)` | Surface a notification (`kind` is app-defined). |
|
|
|
+| `.error(message, code?, data?)` | Report an error to the client. |
|
|
|
+| `.subscribe(key)` | Subscribe to realtime updates for a slot. |
|
|
|
+| `.unsubscribe(key)` | Unsubscribe. |
|
|
|
+| `.set_frame(FrameDto)` | `set` from an already-signed frame. |
|
|
|
+| `.invalidate(signature)` | `invalidate` (drop a bad frame). |
|
|
|
+
|
|
|
+### `StatumRequest`
|
|
|
+
|
|
|
+| Member | Type | Description |
|
|
|
+|---|---|---|
|
|
|
+| `uri` | `string` | The request URI. |
|
|
|
+| `route_params` | `ReadOnlyAssociative<string,string>` | Route params from `<pstm-uri>` templates. |
|
|
|
+| `held` | `ReadOnlyAssociative<string,HeldSlot>` | Held slots by type name. |
|
|
|
+| `query` | `Catalogue<string,string>` | Query parameters. |
|
|
|
+| `action_private` | `Properties?` | Decrypted action private (untyped). |
|
|
|
+| `form` | `FormData?` | Parsed form body. |
|
|
|
+| `held_as<T>(type)` | `T?` | Typed read of a held slot's public data. |
|
|
|
+| `route(name)` | `string?` | Raw route parameter. |
|
|
|
+
|
|
|
+### `StateService`
|
|
|
+
|
|
|
+| Method | Description |
|
|
|
+|---|---|
|
|
|
+| `new_slot(scope, state)` | Creates + caches a slot, returns it. |
|
|
|
+| `get_slot(key)` | Reads a cached slot (pure, no mutation). |
|
|
|
+| `sign_slot(key)` | Signs a slot's current state → `FrameDto`. |
|
|
|
+| `update(key, state)` async | Mutates, signs, pushes to channels → `FrameDto`. |
|
|
|
+| `clear_slot(key)` async | Removes + pushes `clear` event. |
|
|
|
+| `restore_slot(frame)` | Re-caches a slot from a verified frame. |
|
|
|
+
|
|
|
+### `ActionRegistry`
|
|
|
+
|
|
|
+| Method | Description |
|
|
|
+|---|---|
|
|
|
+| `author<TAction>(method)` | Action reference (no private). |
|
|
|
+| `author_private<TAction,TPrivate>(model, method)` | Action reference with sealed typed private. |
|
|
|
+
|
|
|
+### `Scope` enum
|
|
|
+
|
|
|
+`PAGE`, `FLOW`, `SESSION`, `DEVICE`, `UNIDIRECTIONAL`.
|
|
|
+
|
|
|
+### `GObjectMapping`
|
|
|
+
|
|
|
+Maps GObject models to/from `Properties` via introspection (no per-model
|
|
|
+mapper). ActionDto fields round-trip as nested JSON via the mapper registry.
|
|
|
+
|
|
|
+| Method | Description |
|
|
|
+|---|---|
|
|
|
+| `to_properties(Object)` | Model → `Properties`. |
|
|
|
+| `from_properties(Type, Properties)` | `Properties` → model (`Object`). |
|
|
|
+| `from_properties_typed<T>(Type, Properties)` | Convenience: returns `T`. |
|
|
|
+| `register_mapper<T>(mapper)` | Register a nested-object mapper. |
|
|
|
+| `get_string/get_int/get_bool/get_double(props, key)` | Typed ad-hoc getters. |
|
|
|
+
|
|
|
+### Static keys (`web-config.json`)
|
|
|
+
|
|
|
+To make signed frames and encrypted private blobs survive a server restart,
|
|
|
+configure static keys:
|
|
|
+
|
|
|
+```bash
|
|
|
+statum-genkeys > web-config.json
|
|
|
+```
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "statum": {
|
|
|
+ "signing_secret_key": "…",
|
|
|
+ "signing_public_key": "…",
|
|
|
+ "encryption_signing_secret_key": "…",
|
|
|
+ "encryption_signing_public_key": "…",
|
|
|
+ "encryption_sealing_secret_key": "…",
|
|
|
+ "encryption_sealing_public_key": "…"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Without these, the server generates random keys per restart (fine for
|
|
|
+development; a warning is logged).
|