suprnova live:add installs a Live component into your application from the
shipped library or from a third-party library. A third-party component can
carry Rust as well as a view, a stylesheet and a script, so installing one
adds its author's code to your application. live:add treats it that way:
it verifies the component's signature, scans every file without running any
of it, shows you everything it will write and every capability the component
uses, and writes nothing until you approve. Then the copy is yours, and
suprnova.toml records where it came from.
This chapter follows one install from the command to a rendered component. To publish a library of your own, see Live Component Libraries.
Quick start
live:add fetches the component from the newest release of the library at
github.com/acme/acme-ui, verifies it, scans it, and prints its plan. On a
terminal it asks you to approve each capability the component uses, to pin
the library's key, and to confirm the plan. Then it writes the files,
declares the component's Rust module, registers its Live component, and
records the install. A shipped component installs the same way, by its
name, with no questions:
Sources
live:add takes one of four sources:
| Source | Example | Installs from |
|---|---|---|
| A name | field |
The shipped library, embedded in the CLI. suprnova.field is the same. |
| A repository address | acme/acme-ui/counter, gitlab.com/acme/acme-ui/counter@1.2.0 |
The library at the root of that repository, at a release tag. The host is github.com when you omit it, or gitlab.com or codeberg.org. |
An https:// URL |
https://ui.acme.test/acme-ui/components/counter |
The library two levels above the component directory. |
| A path | ../acme/components/counter |
A library tree on disk. The path starts with ./, ../ or /, and names a component directory or its manifest.json. --manifest <file> is the same source. |
A repository address is [<host>/]<owner>/<library>/<component>[@<version>].
With no version, live:add takes the highest v<semver> tag the forge
lists, pre-releases excluded, and refuses a repository with no release tag.
@1.2.0 takes the tag v1.2.0.
Every source resolves to one library address and one component address
before anything is fetched. A repository's library address is
<host>/<owner>/<library>, lowercase, so Acme/Acme-UI/counter and
acme/acme-ui/counter are one component. A URL's is its lowercase scheme
and host, a port that is not the default, and the path two segments above
the component, with no trailing slash. A path's is the absolute library root
with symbolic links resolved. A library fetched by repository address or URL
must name that address as the source in its library.json, so a fork that
copies another library's files and signatures is refused. A path source is
an author's tree on disk, whose source names where it will be published.
Fetching follows fixed rules. Every request is HTTPS, except to a loopback host, and carries no credentials. A response over 2 MiB, one that takes over 30 seconds, a redirect to another origin, and a JSON document that is not one object or holds a duplicate key are refused, and so is a plan that fetches more than 64 MiB in all. Repository files are read raw at the commit the tag names, never as release assets, and a redirect to another repository path, as a renamed repository answers, is refused with the new address named. Only public repositories are supported.
The plan
Before it writes anything, live:add fetches, verifies and scans the whole
plan, dependencies included, and prints it. This is the plan for a first
install of the example component of a library that
live:registry new acme --source github.com/acme/acme-ui scaffolded and that
was published at that address:
framework: suprnova 3.3.0
github.com/acme/acme-ui/counter 0.1.0
commit 5b0f3c9e2a7d41e8c6b1f0a9d3e5c7b2a4f6e8d0
hash sha256:58982952bdf9cfc32fe817ffc459ab3a9302e68581b1eaef1a77b59d2049653e
templates/acme-ui/counter/manifest.json new
templates/acme-ui/counter/counter.html new
templates/acme-ui/counter/counter.css new
templates/acme-ui/counter/counter.js new
src/live/acme/counter.rs new
module src/live/mod.rs: pub mod acme;
module src/live/acme/mod.rs: pub mod counter;
register crate::live::acme::counter::Counter
github.com/acme/acme-ui/counter: capabilities: none
key: pin sha256:0d7344c941b351c781be6ee0ce4c3146a85a2df7d526d1f2c45f2160a21d27c7 for github.com/acme/acme-ui (no key is pinned for it yet)
serve its assets: call `router.try_live_ui_assets_for("acme")` when you build the router
| Line | Means |
|---|---|
framework: |
The suprnova version your application depends on, which each library's framework requirement was checked against. It comes from Cargo.lock, or, with no lock, from the v<version> tag of the suprnova git dependency in Cargo.toml. With neither, live:add refuses and asks you to run cargo generate-lockfile. |
<address> <version> |
One component and the library version it comes from. Dependencies come first. |
commit |
The commit the release tag resolved to. Every file of the library comes from it. |
hash |
The component's verification hash. The signature was verified over it. |
<path> <outcome> |
Each file, where it lands, and what happens to it: new, unchanged, replaced, kept, edited locally, kept, changed since the record, or removed: the library dropped it. See Updates, edits and --force. |
forced |
A refusal --force let through: a downgrade, a released version whose content changed, or a dependency an installed component recorded at another version. |
module |
A module declaration and the file it goes in. already declared marks one the file already holds, which live:add leaves as it is. |
register, unregister |
A Live component it registers in your registry builder, or removes because the new version no longer defines it. already registered marks one the builder already holds. |
depends on |
A dependency and the version it resolved to. |
capabilities: |
Each capability the scan found the component uses, or none. |
approval |
Each capability's approval: given at an earlier install, given by --allow, or needed from you. |
key: |
A key live:add would pin, or re-pin after a key change, by fingerprint. |
serve its assets: |
The router call that serves the library's stylesheets and scripts, shown at the library's first install. |
A library's files land under templates/<namespace>-ui/<component>/ (views,
stylesheets, scripts, the manifest and the install record) and
src/live/<namespace_module>/ (Rust), where <namespace_module> is the
namespace with each hyphen written as an underscore.
Capabilities
A component reaches a database, the network, files, mail, a queue, a cache, the session, the environment or a process only through Suprnova's API, and every part of that API carries a capability. The scan reports each capability a component uses, and you approve each one by name before it installs:
| Capability | Allows the component to |
|---|---|
database |
Read or write a database. |
network |
Open a connection to another host. |
files |
Read or write files, a storage disk included. |
mail |
Send mail. |
queue |
Push to or work a queue. |
cache |
Read or write a cache. |
session |
Read or write the session. |
environment |
Read the environment or the configuration. |
process |
Start a process. |
On a terminal, live:add asks about each capability in turn. Without a
terminal, approve them with --allow, once per capability:
Denying a capability, or leaving one unapproved without a terminal, refuses
the install with nothing written. suprnova.toml records each approval, so
an update that uses the same capabilities installs without asking again, and
an update that uses a new one asks for that one. A component that uses no
capability needs no approval.
Confirm the plan
live:add asks you to confirm a plan that holds any component from a
library but the shipped one. A terminal here means standard input and
standard error are both a terminal, and no is the default answer. Without a
terminal, live:add refuses unless you pass --yes.
--yes confirms the plan and does nothing else. It never approves a
capability, which takes --allow, and never pins or re-pins a key, which
takes a terminal or a pin you write yourself. It does not skip a key's
question either: on a terminal, live:add asks before it pins or re-pins a
key, with --yes or without it.
Trust on first use
The first install from a library pins the key its library.json names, in
suprnova.toml, once you confirm on a terminal. Every later install from
that library must be signed by the pinned key. A library whose key differs
from its pin is refused, with both fingerprints named, unless the pinned key
handed the library to the new key: then the plan shows both fingerprints, and
live:add asks on a terminal whether to re-pin. Yes re-pins the key; no
refuses the install and writes nothing. Without a terminal the change is
refused, even with --yes. A re-pin keeps the former key in the library's
pin table, under previous_keys, so the components you installed under the
former key still verify.
To install without a terminal, pin the key yourself first. Add the library's
table to suprnova.toml, with the key from its library.json:
[live.libraries."github.com/acme/acme-ui"]
key = "ed25519:6HedMVA4Z2cro3wHx0Fh7Vqb3WLDpUtZCsqhG7DpkJA="
A pinned key proves continuity, not identity: every version you install later comes from whoever held the key when you first installed, or from whoever that holder handed it to. It does not prove who that is. Decide whether to trust a library before its first install, from its address and its author.
A namespace stays with the library you first installed it from. A second library that claims the same namespace is refused, with both addresses named.
The provenance record
suprnova.toml, at your project root, is your application's record of
every component it installed. live:add creates it when it is missing and
edits it in place: every comment, key and table it does not own stays as it
was. This is the record the plan above wrote:
[live.components."github.com/acme/acme-ui/counter"]
source = "github.com/acme/acme-ui"
version = "0.1.0"
commit = "5b0f3c9e2a7d41e8c6b1f0a9d3e5c7b2a4f6e8d0"
library_json = "sha256:867b1291d119d68bee3e3a46ce007a7ca8ab60bbae23a04892ad7a6eb9e9ec81"
manifest = "sha256:7ff819c5e8fb1d4439ca865cbddd2a690f4d3b31433c56ebed0850e8c3cb99e8"
hash = "sha256:58982952bdf9cfc32fe817ffc459ab3a9302e68581b1eaef1a77b59d2049653e"
signature = "FeJtqDThMDVmGseOl4KmygWPQghVwD0dUyDWzQw9/+xBzWmLWfvlopI4n2O9LKVuNYhshCLDgqeKSdVoR1SuAA=="
files."counter.css" = "sha256:c44ae7cd896d867174fb62154a80e113c4a67b5bb56bf3d578ca4c3650a7bca2"
files."counter.html" = "sha256:174ccd0130ffc0e366ddc8a25888113e392e185f203d6d15d68d6120bd615f42"
files."counter.js" = "sha256:49a53e15ede8c5a59bf1bd577ad333211aac956aee507f7ad412b17a7ce19acd"
files."counter.rs" = "sha256:048febcec71d261406a1931ba59b42f9a2df630d0df8a093be448928cbd85c73"
registered = ["crate::live::acme::counter::Counter"]
[live.libraries."github.com/acme/acme-ui"]
namespace = "acme"
key = "ed25519:6HedMVA4Z2cro3wHx0Fh7Vqb3WLDpUtZCsqhG7DpkJA="
Each [live.components."<address>"] table holds exactly what arrived:
| Key | Holds |
|---|---|
source, version |
The library's source and the version installed. |
commit |
The commit the release tag resolved to. A URL or path source has none. |
library_json, manifest |
The digests of library.json and manifest.json as they arrived. |
hash, signature |
The verification hash and the signature over it. |
files |
The digest of each file the manifest names, as it arrived. |
dependencies |
Each dependency's address and the version it resolved to. |
registered |
The full path of each Live component it registered, as the registration line names it. |
capabilities |
Each capability the scan found, with how you approved it: terminal or flag. |
kept |
Each file the install kept because you had edited it. |
In suprnova.toml, a component's capabilities, kept and commit are
your own record of decisions made at install time, not part of the signed
statement, so live:check does not verify them and editing them by hand
changes no provenance.
Each [live.libraries."<address>"] table holds the namespace the library
owns in your application, the key pinned for it, and, after a key change,
previous_keys: the keys the pin replaced. A shipped component's
table holds only the CLI's version and digests, since the shipped library is
neither signed nor hashed:
[live.components."suprnova/field"]
source = "suprnova"
version = "3.3.0"
manifest = "sha256:9540febdf7b22bd79c2fdcede86e14b6e9693201de58620e564ced94f7e85620"
files."field.css" = "sha256:3b552944e95a30c31439afd0248491e77b301e9d26be9bf73beb78991a97b7c5"
files."field.html" = "sha256:f576888a8dbad260b3e42cf6be7981fc7d69a9c40cbadedddb95800be4d26d4b"
The record holds what arrived, never what is on disk now: editing a vendored
file changes no provenance. Each component directory's
.suprnova-installed.json holds the digest of the bytes live:add last
wrote at each path, which is how it tells your edits from its own files.
Render the component
An installed component is registered, but nothing renders it until a page
mounts it. Serve the library's stylesheets and scripts with the call the
plan named, and render the component as its own island. This page module,
src/counter_page.rs, does both:
//! The installed counter, rendered by its own island.
use BTreeMap;
use ;
use ;
use ;
use crateCounter;
/// Serves the acme library's stylesheets and scripts and the counter page.
async
Its view, templates/counter_page.html, links the component's stylesheet
and script from the namespace's route, with url::root() in front so the
links work under a path prefix too
(Serving under a path prefix):
Counter
{{ bootstrap|trusted_html }}
{{ counter|trusted_html }}
Declare the module in src/lib.rs with pub mod counter_page;, and install
its routes in src/live/mod.rs, inside the RenderCache install:
pub async
Build and start the application, and /counter renders the count and its
button through the component's own island, with its stylesheet served from
/acme-ui/counter/counter.css. The reserved Live routes of an application
suprnova new generated take AuthMiddleware::new(), so the button's action
runs for a signed-in visitor; Routes shows the guard with
AuthMiddleware::optional() for anonymous actions.
try_live_ui_assets_for("acme") serves templates/acme-ui/ at
/acme-ui/{component}/{file}: stylesheets and scripts only, never a view,
a Rust file, a manifest or the install record. Each library's namespace takes
one call; the shipped library's is try_live_ui_assets(). See Component
library for the shared rules.
Updates, edits and --force
Run live:add again to update a component. Each file is judged against the
install record: a file you never edited is replaced when the library changes
it, a file you edited is kept, and the plan says which. A Rust file is judged
the same way. When a kept Rust file belongs to a component whose new version
registers different types, live:add refuses and names the file to
reconcile. A file the new version no longer names is removed when you never
edited it, and kept when you did. --force replaces or removes every edited
file.
suprnova.toml pins the version you installed, and live:add guards it:
- A version lower than the recorded one is refused unless you pass
--force. - The recorded version with a different hash is refused, reporting that the
library changed a released version, unless you pass
--force. - An install that would move a dependency to another version than an
installed component recorded names that component and is refused unless
you pass
--force.
The plan shows each refusal --force let through as a forced line, and
marks what the application already holds. An application that installed
0.2.0 of the counter and goes back to 0.1.0 sees this plan:
framework: suprnova 3.3.0
github.com/acme/acme-ui/counter 0.1.0
commit 5b0f3c9e2a7d41e8c6b1f0a9d3e5c7b2a4f6e8d0
hash sha256:1f75c3d54657a2e51a102fb49b4f5e012e7124dc2aae68601012487109eee208
templates/acme-ui/counter/manifest.json unchanged
templates/acme-ui/counter/counter.html unchanged
templates/acme-ui/counter/counter.css replaced
templates/acme-ui/counter/counter.js unchanged
src/live/acme/counter.rs unchanged
forced downgrade from 0.2.0 to 0.1.0, let through by --force
module src/live/mod.rs: pub mod acme; already declared
module src/live/acme/mod.rs: pub mod counter; already declared
register crate::live::acme::counter::Counter already registered
github.com/acme/acme-ui/counter: capabilities: none
Dependencies
A component's dependencies install first, each at most once, a cycle
included. A bare dependency is a shipped component, and ./<component> is a
component of the same library at the same version. Any other dependency is a
repository address, which resolves as an address you type does, or the
https:// URL of a component on a public host: a manifest you fetched from
someone else cannot point live:add at a path on your disk, a loopback
address or a private network. A plan that needs one component at two
versions is refused before anything is written, naming the components that
require each, and a plan of more than 64 components is refused.
Dry run
--dry-run fetches, resolves, verifies, scans and reports the whole plan,
dependencies included, and makes the same framework check an install makes.
It writes nothing: not suprnova.toml, not the lock, not the journal.
The lock and the journal
An install holds an exclusive lock on the project, the file
.suprnova-live.lock at its root, from before it reads your records until it
finishes. A second live:add refuses to start while one runs.
Before its first write, an install writes .suprnova-live-journal.json,
naming every path it will change or create and the prior bytes of each it
will change. When any step after that fails, it puts every changed file back,
suprnova.toml included, removes every file it created, and deletes the
journal. When an install is killed partway, the next live:add or suprnova serve finds the journal with no lock held, restores from it before anything
else, and says so.
suprnova serve builds through suprnova live:wait. It starts no build
while an install holds the lock, and it holds a shared lock on the project
until the build finishes, so the dev server never builds a half-written
install and live:add refuses to start while a build is reading the
project. Both files are this machine's state: a scaffold's .gitignore names
them.
Verify installed components
Before it checks your views, live:check verifies every third-party
component suprnova.toml records, with no network: it recomputes each hash
from the recorded fields and verifies the recorded signature against the
key pinned for its library. A record altered by hand, a bad signature or a
missing pin fails the check. A vendored file whose bytes differ from what
arrived is reported as changed since install and does not fail it, since the
copy is yours to edit.
The project file
The CLI reads one project file, suprnova.toml, all lowercase, at the
project root. suprnova serve reads its extra dev
processes from it, and live:add writes
its records there.
An earlier CLI read Suprnova.toml; rename that file to suprnova.toml.
Until you do, serve and live:add refuse to run and name the rename. On a
file system that ignores case, rename it in two steps:
What live:add never does
- Run anything a component carries: no build script, procedural macro, script or view runs while it fetches, verifies, scans or installs.
- Start a process.
- Change
Cargo.tomlorCargo.lock, or add a crate to your build. - Write anything before the whole plan is fetched, verified, scanned and approved.
A component that passes the scan can still fail to compile, for example against another framework version than its author built it for. That failure runs none of its code; fix or remove the component as you would your own.
Why Suprnova diverges
shadcn's CLI copies a component's source from a registry into your project
with no signature and no scan. Laravel installs packages through Composer,
whose plugins run during the install, and a package's service provider runs
on every boot. live:add copies like shadcn, so the component is yours, but
trusts nothing it has not checked: the signature against a pinned key, every
file scanned as data, every capability approved by name, and a record in
suprnova.toml that live:check verifies again offline.
Next
- Live Component Libraries - write, sign and publish a library of your own
- Live - components, documents, islands and the shipped component library
- CLI Overview - every
live:*command - suprnova serve - the dev runner and
suprnova.toml
