A Live component library is a git repository of Live components that any
Suprnova application can install with suprnova live:add. A component can
carry a view, a stylesheet, a script, and the Rust of a Live component: its
state, actions and view binding. The application that installs it owns the
copy, as it owns the components of the shipped library.
Every library has the same tree, the shipped one included, and every
component in it is signed with the library's key. live:add verifies the
signature and scans every file as data before it writes anything, so nothing
a component carries runs during an install. This chapter takes you from an
empty directory to a signed, tagged release. Installing Live
Components is the other side: what an application sees when it
installs from your library.
Quick start
live:registry new scaffolds a library with one example component, a
preview application, and a signing key:
--source is required: the address the library will be published at,
which library.json records as its source and every signature covers;
see library.json. If you later empty or remove source by
hand, check, sign and rotate-key refuse until you set it again.
The argument is the library's namespace. It prefixes everything the library
installs: views and assets under templates/acme-ui/, Rust under
src/live/acme/, Live component names (acme.counter), and custom element
tags (acme-counter). A namespace is 1 to 32 bytes of lowercase letters,
digits and hyphens, starts with a letter, and its module form (each hyphen
written as an underscore) is not a Rust keyword. The namespaces suprnova,
sn and live belong to the shipped library.
new prints where it wrote your private signing key. Back that file up
now; The private key says why.
The library tree
live:registry new acme --source github.com/acme/acme-ui writes this tree
into ./acme:
acme/
library.json
components/
counter/
manifest.json
manifest.sig
counter.html
counter.css
counter.js
counter.rs
preview/
library.json describes the library, components/ holds one directory per
component, and preview/ is an application that renders each component from
where it sits. The library sits at the repository's root: live:add reads
library.json there and each component under components/.
library.json
library.json is one JSON object. This is the one new wrote above:
It holds the keys in this table and no others:
| Key | Holds |
|---|---|
namespace |
The namespace every component installs under. |
source |
The library's address where you publish it: github.com/<owner>/<repository>, gitlab.com/<owner>/<repository> or codeberg.org/<owner>/<repository>, all lowercase, or the https:// URL of the tree. new --source writes it; without --source it is empty, and check, sign and rotate-key refuse until you set it. live:add refuses a library whose source is not the address it fetched it from, so a fork that copies your files and signatures cannot install as yours. |
version |
The release, a semver version. It must equal the release tag: version 0.1.0 is the tag v0.1.0. |
framework |
A semver requirement on the suprnova version of the application. live:add refuses an application its requirement does not admit. new writes ^ and the version of the CLI that scaffolded the library. |
publicKey |
The library's signing key, ed25519: and the base64 of its 32 bytes. |
previousKeys |
Optional. A statement from each former key that hands the library to publicKey; see Change the signing key. |
title, description |
Optional text for people. |
manifest.json
Each component's manifest.json is one JSON object of at most 1 MiB. It
names the component's files and nothing more: a component has no version of
its own, since the library's version is the release, and it declares no
crates.
| Key | Holds |
|---|---|
name |
<namespace>.<directory>. |
files |
Each file the component carries, by name, each once. At least one is an .html view. |
root |
Optional. <namespace>-ui/<directory>, which is also the default. |
title, description |
Optional text for people. |
elements |
Optional. Each custom element tag the component's script defines, each starting with <namespace>-. |
register |
Optional. Each Live component the component's Rust defines, as <module>::<Type>, where <module> is the stem of one of its .rs files. |
dependencies |
Optional. Components to install first: a shipped component by its name (field), a component of the same library and version (./date-grid), a repository address that may carry a version (acme/acme-base/icon@1.2.0), or the https:// URL of a component on a public host. A path on disk is never a dependency. |
A file's extension decides where live:add writes it:
| Extension | Lands at | Name |
|---|---|---|
.html |
templates/<namespace>-ui/<directory>/ |
Lowercase letters, digits, hyphens and dots, at most 128 bytes |
.css, .js |
templates/<namespace>-ui/<directory>/, served at /<namespace>-ui/<directory>/<file> |
A stem of 1 to 64 lowercase letters, digits and hyphens, neither starting nor ending with a hyphen |
.rs |
src/live/<namespace_module>/ |
A lowercase Rust identifier that is not a keyword, mod or lib |
<namespace_module> is the namespace with each hyphen written as an
underscore. A component directory's name follows the same rule as a
stylesheet stem, because it is the asset route's component segment. Two
components of one library cannot install the same Rust file.
The example component
The example's Rust is an ordinary Live component, named for the namespace:
use ;
/// A counter rendered by `acme-ui/counter/counter.html`.
Its view names the view path it installs at, acme-ui/counter/counter.html.
Every Live component name a library's Rust defines starts with
<namespace>., and each #[live] type is exactly one entry of register.
When an application installs the component, live:add writes
src/live/acme/counter.rs, declares pub mod acme; and pub mod counter;,
and adds this line to the application's registry builder:
.?
The script defines a light DOM custom element, acme-counter, when no
other script has defined that name. The view works without it.
Preview your components
preview/ is an application suprnova new generated, with the library's
components wired in where they sit. Nothing is copied into it:
askama.tomlmakes../components/a second template root, and each view undertemplates/acme-ui/is a one-line stub that includes the library's view.src/live/acme/mod.rscompiles each component's Rust file from../components/with#[path].src/preview.rsserves the stylesheets and scripts straight from../components/at/acme-ui/<component>/<file>, the same URL an application serves them at after it installs the component, and renders one page per component.
Run it from the preview's directory:
Then open http://localhost:8765/preview/counter. The preview's own test
starts its server and reads the same page:
serve rebuilds and restarts the preview when a file under its own src/
or templates/ changes. The library's files under ../components/ sit
outside the preview, so serve does not watch them. A stylesheet or script
edit shows on the next reload, because the asset route reads the file on
each request. A view or Rust edit shows after the next rebuild: restart
serve, or save a file under preview/src/.
To preview a new component, give it a page in src/preview.rs: add its view
stub under templates/acme-ui/<component>/, a #[path] line in
src/live/acme/mod.rs, its registration in src/live/mod.rs, and its mount
and route, as the counter's are. live:registry check reads the preview's
registry, so a register entry the preview does not register fails the
check.
Check the library
check runs, from the library's root, every check live:add makes for a
reason the library alone decides:
- The tree:
library.json, the component directories, and their names. - Each manifest and each file it names: every named file must be a regular file inside its component's directory, never a symbolic link.
- Each signature, over the component's verification hash.
- The scan of every view, stylesheet, script and Rust file; see What the scan admits.
- The names: Live component names, element tags and
registerentries. - The dependencies, resolved as
live:addresolves them: a./dependency must be in your tree, a bare one in the shipped library, and a full address must fetch and verify.
It lists each component's capabilities as live:add shows them to the
developer who installs it:
counter: capabilities: none
A capability is an effect a component reaches through Suprnova's API, such
as database or network. Installing Live
Components lists all nine.
Sign the library
sign runs every check check runs except the signatures it is about to
replace. Then it signs every component, verifies every new signature, and
only then writes every component's manifest.sig. When any component
fails a check, it writes no signature at all. Signing is deterministic:
the same tree and key always sign to the same bytes, so signing an
unchanged tree changes nothing.
Sign again after any change to a component or to library.json. Each
component's signature covers the digest of library.json, so a new
version, source or key leaves every signature stale until you sign
again, and check reports each one.
The private key
live:registry new creates the key pair. It writes the public key into
library.json and the private key outside the library, into your
configuration directory:
| System | Key file |
|---|---|
| Linux and other Unix | $XDG_CONFIG_HOME/suprnova/library-keys/<fingerprint>.key, or ~/.config/... when XDG_CONFIG_HOME is not set |
| macOS | ~/Library/Application Support/suprnova/library-keys/<fingerprint>.key |
| Windows | %APPDATA%\suprnova\library-keys\<fingerprint>.key |
<fingerprint> is the hex digest of the public key, so each library has
its own file and sign finds it from library.json alone. On Unix the file
is readable only by you, and new never replaces a file that exists.
To sign on another machine, such as a release job, put the key file there
and name it with SUPRNOVA_LIBRARY_KEY:
SUPRNOVA_LIBRARY_KEY=/run/secrets/acme.key
sign refuses a key file inside the library, so a key never ships with a
release. Never commit it.
Losing the private key strands every pin. An application that installed your library pinned its key, and from then on it installs only versions that key signed, or that a key it handed the library to signed. Without the private key you can sign no new version and hand the library to no new key, so every application that pinned it stays on the version it has. Keep a backup of the key file somewhere safe.
What a signature covers
A component's signature covers its verification hash: the sha256 digest of its statement, one JSON object with no whitespace and these keys in this order:
| Key | Value |
|---|---|
format |
suprnova-component/1 |
library |
The library's source |
version |
The library's version |
component |
The component's directory name |
libraryJson |
The digest of library.json |
manifest |
The digest of manifest.json |
files |
An object of each named file's digest by file name, names in byte order |
Every digest is sha256: and the lowercase hex digest of the file's bytes.
The hash is sha256: and the lowercase hex digest of the statement's UTF-8
bytes. manifest.sig holds the Ed25519 signature, by the key library.json
names, over the ASCII bytes of the hash, as the standard padded base64 of
its 64 bytes and nothing else, not even a final newline. A key's
fingerprint is sha256: and the lowercase hex digest of the key's 32 bytes.
The statement names the directory and the library's source, so a
component signed under one name or address does not verify under another,
and one changed byte in any file, the manifest or library.json changes
the hash.
Test vector
This vector is the widget component of the fixture library the CLI's own
tests install, signed with a test key. A tool that builds the statement and
checks the signature the same way must reproduce the hash and accept the
signature. Never trust this key for anything else.
statement {"format":"suprnova-component/1","library":"github.com/acme/acme-ui","version":"1.0.0","component":"widget","libraryJson":"sha256:94f8914ee1f7889c447b8e0b52251e4e9faeb4badf5c530b3023671972e96b02","manifest":"sha256:3a266a23a32091ed78d8ad3894166799be2bb2a2c8c7f728e5b54734d79699bf","files":{"widget.css":"sha256:5c2f8bed08981ed1f07fbdc1e9ad4d6d0536ec3f1548679b0bf6c36dfed9d81f","widget.html":"sha256:4dff021923cf2408f82e6733a90c590d004f5598d6751dffcd164261c5270471"}}
hash sha256:25a0757c3fed1fcc9ec54986400f546bc1c51f3ba49c5404900e5450dda37a8e
publicKey ed25519:5z4SbonRkzk8kbuQFeoIuaaV0iVATslPzuqhE7XuKwg=
signature OiIoP3FOXZcbC8AqnvgENWiqPucexXxto2Y+b8scJDMVTtw0bvlshIfAL3TKDtIn9dvvi8bTPNUiTVj8ueaLCA==
Publish a release
A release is a v<version> tag in a public repository on GitHub, GitLab
or Codeberg. live:add reads the tag's files raw, at the commit the tag
names, and never reads release assets.
live:registry new creates no git repository. Run git init in the
library's root, and add the forge's repository as the origin remote. The
repository holds the whole tree, preview/ included; the preview's own
.gitignore keeps its build output and .env out. Then, for each release:
-
Make sure
sourceinlibrary.jsonis the repository's address, such asgithub.com/acme/acme-ui. -
Set
versionto the release, such as0.1.0. -
Check and sign the library:
-
Commit the signed tree, tag the commit with the version, and push both:
An application then installs a component by its address:
With no version, live:add takes the highest v<semver> tag, pre-releases
excluded; acme/acme-ui/counter@0.1.0 names one. A GitLab or Codeberg
library is addressed with its host first: gitlab.com/acme/acme-ui/counter.
For each later release, raise version, sign, commit, and tag again. Never
move a tag you published. An application records the hash of what it
installed, and live:add refuses the same version with different content,
reporting that the library changed a released version.
Keep a published repository's name. Every application that installed from
your library recorded its address and bound your namespace to it.
live:add refuses to follow a forge's redirect to a renamed repository, and
refuses a second address that claims a namespace an application already
gave to the first.
To publish without a forge, serve the same tree from any HTTPS host and set
source to its URL, such as https://ui.acme.test/acme-ui. An application
installs a component by its directory's URL,
https://ui.acme.test/acme-ui/components/counter, at the one version
library.json names.
Change the signing key
To move the library to a new key, run rotate-key from the library's root:
It reads the current private key as sign does, from SUPRNOVA_LIBRARY_KEY
or your configuration directory, and makes a new key pair. It writes the new
private key into your configuration directory, named by its fingerprint, as
new does. It updates library.json: publicKey becomes the new key, and
previousKeys is rewritten as one statement per former key, each handing
the library to the new key. The former keys are the key you are leaving and
every key the library used before it, and each one signs its own statement:
rotate-key reads each earlier key from its file in your configuration
directory. It also raises the patch part of version, from 1.0.0 to
1.0.1: a rotation changes the signed content of every component, and
live:add refuses content that changed at a version an application
recorded. Then it signs every component again with the new key, all or
nothing, as sign does. It prints both fingerprints and the path of the new
key file. Back up the new key file, then commit the result and tag the new
version as for any release.
Keep every former key file. An application installs a release under a
new key only when the key it pinned vouches for that key. A library on its
third key needs a statement from each of its two former keys, so an
application pinned to either one can follow. When a former key's file is
missing, rotate-key refuses and names the key. To go ahead without it,
pass --drop-key with that key's fingerprint:
--drop-key leaves that key's statement out of previousKeys. An
application still pinned to the dropped key then refuses every later
release, because its pin vouches for no change, and its developer must pin
the new key by hand. Tell your users the new key's fingerprint through a
channel they already trust before you publish a release without a
statement they need.
After a change, library.json looks like this:
previousKeys holds one such entry for each former key. Each former key
signs the UTF-8 bytes of one JSON object with no whitespace,
{"format":"suprnova-key-handover/1","library":"<source>","next":"<fingerprint>"},
where library is the source in this library.json and next is the new
key's fingerprint. The statement names the library, so a former key that
signs for several libraries hands over only this one. signature holds the
signature in standard padded base64. The format is open, so other tooling
can produce a statement too.
An application that pinned a former key installs the new release only if the statement from its pinned key verifies, and it re-pins only when its developer confirms the change on a terminal. The application keeps the former key in its pin table, so the components it installed under that key still verify.
A developer whose pinned key you dropped pins the new key by hand: in the
library's table of suprnova.toml, set key to the new key and move the
dropped key into previous_keys, so the components installed under it still
verify:
[live.libraries."github.com/acme/acme-ui"]
namespace = "acme"
key = "ed25519:<the new key>"
previous_keys = ["ed25519:<the dropped key>"]
What a pinned key proves
The first time an application installs from your library, its developer
pins the key library.json names. A pinned key proves continuity: every
later version that application installs comes from whoever held the key at
that first install, or from whoever that holder handed it to through
previousKeys. It does not prove who that is. An application trusts your
library because its developer chose to install from your address, and the
pin keeps that choice from silently changing hands.
What the scan admits
live:add scans every component from any library but the shipped one, and
live:registry check runs the same scan. The scan reads each file's syntax
tree, compiles and runs none of it, and admits only what it can classify.
A construct it cannot classify, or a name it cannot resolve, is refused,
and no finding can be overridden. A refusal names the check, the file and
the line. Acceptance proves what a component can reach, not that it
compiles.
In Rust, a component may name its own items, the modules of the components
it depends on, Suprnova's documented public API, including the crates
Suprnova re-exports through the suprnova:: path, and the part of std
that has no effects. The scan refuses:
- Any other path, under any alias, a
#[doc(hidden)]re-export included. unsafe,externblocks and functions, andmacro_rules!.- Every macro, attribute and derive not on its allowlist.
#[path], amoddeclaration without a body,env!,option_env!,include!,include_str!,include_bytes!andcfg_attr.TrustedHtml's constructors, so a view writes unescaped only markup the framework built from typed data.- A method call whose receiver type it cannot name, and a service resolved from the container whose type it cannot name.
- A test fake, an app-wide hook or an install, whatever capability the
facade's chapter grants:
Http::fakeand its siblings,Queue::fake,Mail::fake,Storage::fake,Process::fake,Http::global_optionsand the global request and response middleware,Queue::set_driver,Cache::set_connection, theinstallof a service, the test container and the test clock. Each changes what every other caller in the process sees, which is the application's to decide, not a component's. - The application's error handling:
Exceptions::reportable,dont_report,dont_report_when,dont_report_duplicates,dont_retryanddont_retry_when. A component that could change them could hide its own failures from the error tracker. A component still reports an error withExceptions::reportortry_report. - The request's CSP nonce (
Request::csp_nonce). Whoever holds it can write a script the page's policy runs, and a component's view holds no script. - The engine surfaces the framework holds for the host: the host ports a
bootstrap installs (
HostCapabilitiesand the action, subscription and upload authorization, continuity, credential and registry ports), the trusted contexts and fingerprints the engine mints, the snapshot key material, the wire protocol and proposal application, and the component hooks the macros implement. Beyond those, an item of the Live engine or of Magnetar, and any item undersuprnova::live,suprnova::render_cacheorsuprnova::view, reaches a component only when the generator's admit table names it, so neither a re-export made for applications nor a new Live item of the framework widens the allowlist by default. Magnetar's types are refused, the OAuth provider types at the crate root included: authentication is the application's, through the auth flows, never a component's.
Each capability the admitted paths carry goes into the plan. Resolving a trait your own library defines carries none, because the application binds it to its own implementation.
In a view, parsed with Askama's parser:
- An expression reads the component's state and calls only admitted paths that carry no capability, the framework's view helpers, and the component's own functions and methods that reach no capability, directly or through another of its own items. A view never calls into a dependency's Rust, because the scan of this component does not read it.
- No Rust macro, no filter that is neither Askama's nor the framework's, and
no unescaped output but
trusted_html: nosafe, and noescapeorewith an escaper other thanhtml. - An
include,importorextendsnames only a view of the component, of a component it depends on, or of the shipped library. - No
script,iframe,object,embed,base,linkormetaelement, no event handler attribute, nojavascript:ordata:URL, and no SVGanimate,setorforeignObject. - A URL attribute (
href,src,srcset,action,formaction,poster,data,xlink:href,ping) holds a constant on the application's origin, or a value the application passes in. - CSS, in a stylesheet or a
styleelement or attribute, has no@import, and every resource it names stays on the application's origin.
In a script, parsed as a JavaScript module:
- Every call resolves to a function the script defines or a standard browser
API, including calls made through
Reflect,call,apply,bind, getters, setters,Proxytraps and tagged templates. - No
evalorFunctionunder any spelling, no timer given anything but a function, no dynamicimport, noWorker, and nodocument.writeordocument.open. - A static
importis a relative path (./or../) to one of the component's own scripts or to a script a dependency's manifest names, so it still resolves when the application serves under a path prefix (Serving under a path prefix). An absolute path such as/acme-ui/x/x.jsand a URL are refused, even when they name the component's own script, and so is a relative path with one../more than the component is deep: it leaves the components' root, so under a prefix it would resolve outside the prefix. A shipped component's script counts only when the component depends on that shipped component. - No
script,iframe,objectorembedelement created, and no element created by a name the scan cannot trace to a constant. - No HTML parsed into the document (
innerHTML,outerHTML,insertAdjacentHTML,setHTMLUnsafe,createContextualFragment,DOMParser), noattachShadow, and no change to a built-in or to a built-in prototype. - A prototype may be read but not held.
Array.prototype.slice.call(list)andObject.prototype.toString.call(value)read a member of a prototype and are admitted. A prototype kept in a name, passed as an argument, returned, or put in an array or an object is refused, however the script reaches it:Array.prototype,Array["prototype"]orObject.getPrototypeOf(list). The scan cannot follow a value to where it is used, so it stops the prototype before it leaves the expression that reads it. Every write to a member of a prototype is refused, in any form (=,+=,??=,++, a destructuring orforloop target), and so isdeleteof one. So is a write to a member of a value read from a prototype, wherever the script keeps it: in a name, or in a parameter a call hands it to, asf(Array.prototype.slice)handsslicetopinfunction f(p) { p.call = g }. A method called on the prototype itself, such asArray.prototype.push(1), is refused too: it runs with the prototype asthisand changes it. So is destructuringprototypeout of an object (const { prototype } = Array), which puts the prototype in a name. - A built-in may be called, read and kept in a name, but not changed. A
write to a member of a built-in object or function is refused in every
form a prototype's is:
Object.keys = f,JSON.parse = f,Math.random = f, the same through the global object (globalThis.Object.keys = f,window.JSON.parse = f), andObject.keys.call = f. The scan follows a built-in through the names a script keeps it in, soconst k = Object; k.keys = fis refused, and so is a write to a member of a parameter given a built-in by its default or by a call of its function by name (function f(p) { p.call = g }withf(Object.keys)). - A member read under a built-in method's name (
slice,addEventListener,toString) from a value the script did not make is taken to be the method that value inherits, so[].slice.call = gandelement.addEventListener.call = gare refused. A member the script's own object or class defines under such a name is not, but a field that shares one is:form.search.value = ""is refused, andform.querySelector("[name=search]").value = ""is admitted. Reading such a member stays admitted. - A member read off a literal is the built-in method the literal inherits,
whatever its name:
(0).toPrecision,"".anchor,[].copyWithinand/x/.compile, and the same read off an operator's result such as-1or"a" + b, or off a constant that holds a literal (const n = 0; n.toPrecision). Writing a member of one is refused, and so is passing one on. A string's or an array'slengthand indices, and a regular expression'slastIndex,sourceand flags, are the literal's own and stay admitted. A class that extends a built-in inherits its statics, soclass A extends Promise {}hands overPromise.withResolversasA.withResolvers, unless the class declares a static of that name. A parameter or a variable that may hold a literal is not treated as one, because the scan reads notypeoftest. call,applyandbindare how every script borrows a built-in method, and any value may hold one under any name: the numberMath.random()returns holdstoPrecision, an input'svalueholdsanchor, an element holdsrequestFullscreen. So a member of one of these three names may be written only on a value the script made: an object, array, function or class literal, a function or class it declares, a new instance of a standard constructor such asnew Map(), or a name that holds only those.const o = {}; o.call = 1andthis.call = cin a class stay admitted;api.call = fon a parameter the scan cannot trace is refused, and so isthis.state.call = c, whosestatethe scan does not follow.- Where the scan stops following a built-in, it is refused: passed to
anything but a function the script calls by name or a browser API that
only calls it back (
list.map(Number)is admitted), put in an array or an object, written to a member, destructured (const { keys } = Object), returned, thrown or exported. That coversObject.defineProperty(Object, "keys", ...),Reflect.set(Math, "random", f)andPromise.resolve(Math).then((m) => { m.random = f; }). A constant such asNumber.MAX_SAFE_INTEGERis a number, not a built-in, and may go anywhere. A write to a member of the script's own object or function stays admitted (const o = {}; o.keys = f,function cache() {} cache.size = 0), and so does a call (Object.keys(o)). document,locationandhistoryhold the page, which a component may change:document.title,location.hashand a value read from them, such asdocument.body, stay under the other rules. Their methods are built-in functions every script on the page calls, so assigning one (document.createElement = f,location.assign = f) is refused, however the script reaches the object:this.ownerDocument, whatgetRootNode()returns, which is the document for any node in it (document.getRootNode().open = f), andObject(document)included. The page also reaches a script through paths the scan does not follow: an element'sparentNode, an event'scurrentTarget, and an array, a promise or a function that handsdocumentback. So a method of the page whose name is never ordinary data (createElement,querySelector,getElementById,addEventListener,pushStateand the rest of the page's own) may be assigned only on a value the script made, as above. An instance of the script's own class does not count, because its constructor may returndocument.- The window
window.openreturns is a global object likewindow, so a built-in read from it, or a method of itsdocument, is refused written or passed on as this window's is:window.open("/x").JSON.parse = f, andpopup.Object.keys = fafterconst popup = window.open("/x"). Using the window, aspopup.focus()andpopup.location.href = "/y"do, stays admitted. - A write to a member of the global object is refused, whatever the name
and in every form:
window.AcmeState = {},globalThis["Acme" + "State"] = 1,window.acmeState++anddelete window.fetchare all refused, and so is a write to a member of the windowwindow.openreturns (popup.name = "x"). Every script on the page shares the global object, so such a write could replace a browser API or a name the application or the Live runtime keeps there. Keep a component's state in its module scope instead: aletorconstat the top of the script, or a field of its element class. A write never unlocks a read: aftertry { window.localStorage = 1 } catch {}, readingwindow.localStorageis still refused, as is any read of a global the scan does not admit. - An attribute name given to
setAttributetraces to constants and is not an event handler,srcdocorstyle. - A URL given to
fetch,XMLHttpRequest,WebSocket,EventSource,sendBeacon,location,window.open, or a URL property or attribute is a constant on the application's origin. customElements.definetakes a constant name the manifest'selementsdeclares.
A component reaches the database, the network, files, mail, a queue, a cache,
the session, the environment or a process only through Suprnova's API, and
each of those carries a capability the installing developer approves.
live:add never adds a crate to the application, so nothing enters its
build that the scan did not read.
Why Suprnova diverges
Laravel packages arrive through Composer and run as soon as the application
boots: a package's service provider is code the application runs, chosen by
the package. shadcn copies a component's source into a project from a
registry, with no signature and no scan. A Suprnova library sits between the two. Like
shadcn, live:add copies the component into the application, which owns it
from then on. Unlike either, every component is signed by its library's key,
pinned on first use, and scanned as data before anything is written, and the
developer approves each capability by name. The library declares no
dependencies of its own, so the scan reads everything the component adds to
the build.
Next
- Installing Live Components - the plan, capabilities, pins, and the provenance record an install writes
- Live - components, views, documents and islands
- CLI Overview - every
live:*command - suprnova serve - the dev runner the preview uses
