Repository navigation
forma-server 0.2.2 (release A2)
Release A2, the bridge to 3.0.0 of the Forma Stack v2 programme (additive, with deprecations): forma-ir and forma-server 0.2.2, @getforma/core 2.1.0, @getforma/compiler 0.3.7, @getforma/build 0.3.1, @getforma/create-app 0.6.2. Published by this repository's release workflow through crates.io trusted publishing.
Before tagging: the packed-set test (FORMA-011) on the release bytes, and a whole-release audit on a second model ("tag after fixes"; its fixes are in this release).
Artefact: forma-server-0.2.2.crate (crates.io), SHA-256 e34bf322d400bba901d2862bf03395226c3a4f131415d6e955362ce869f802ba.
forma-server 0.2.2
Release A2's bridge (0.2.2): additive. Every 0.2.1 call compiles and behaves
as before, except one default: the service worker is off unless the
application turns it on (FORMA-130, under Changed). The old forms warn as
deprecated and go in 3.0.0. Everything else an application may notice is
listed first.
Upgrading from 0.2.1: what an application may meet
- No service worker by default. No page writes the registration line, and
/sw.jsserves the retiring worker, which clears Forma's v1 caches and
unregisters, until the application callssw::configure(..enabled())
(under Changed). Code that looks for the registration line finds none:
fAuth'srender_pagewrapperdebug_assert!s it, so its debug-build
renders panic on 0.2.2 until the wrapper changes (FORMA-150). Its
forma-server = "0.2.0"requirement lets a plaincargo updatetake
0.2.2: pin=0.2.1until the wrapper changes. - Deprecation warnings at compile time. Every
PageConfigliteral names
config_script, so every application warns at least once, even for
config_script: None; anAssetManifestliteral warns forwasm. A build
under-D warnings(RUSTFLAGS,#![deny(warnings)],
cargo clippy -- -D warnings) fails until each call moves to the
replacement listed under Deprecated or carries#[allow(deprecated)], as
create-app 0.6.2's templates do on their onePageConfigliteral. - Trusted Types, reported: a second policy header where it can be read.
Content-Security-Policy-Report-Onlywith Trusted Types (under Added) goes
out on a page when the application names a reporting endpoint
(PolicyExtensions::report_toorreport_uri), in every build, and with no
endpoint in development builds only, where Chromium and Firefox show its
violations in the console. A release build with no endpoint sends no
Report-Only header, so it adds no header to 0.2.1's page. It would report
to nobody, and WebKit (Safari) logs a console error on every page whose
Report-Only policy names noreport-to("… was delivered in report-only
mode, but does not specify a 'report-to'; the policy will have no
effect"), which FORMA-138's packed-set run found. With the header, nothing
is blocked; a page that registers its own Trusted Typesdefaultpolicy
has it run on string writes. The enforced policy is 0.2.1's, byte for
byte.- A development build with no endpoint still makes WebKit log that error
on every page, so an application's own WebKit test that fails on
console errors, run against a development server, meets it.
report_uri("/csp-reports")silences it in development too. - A check that Trusted Types raise zero reports or violations must name an
endpoint (report_uriis enough) or run against a debug build. A
release build with no endpoint sends no Report-Only policy, so such a
check passes without testing anything. - "Development build" is forma-server's own
debug_assertions, the switch
its loopback-endpoint rule uses: a release profile with debug assertions
on, or a[profile.release.package.forma-server] debug-assertions = true
override, gets the development behaviour.
- A development build with no endpoint still makes WebKit log that error
render_page's document is 0.2.1's, byte for byte, except the worker
registration line above.- Development-build log lines, once per process:
render_pageand
load_ir_moduleslog their deprecation, and a plain string in
body_prefixlogsRAW-101. Release builds log nothing new. - Requires forma-ir 0.2.2 (
cargo update -p forma-serverbrings it).
Its HTML is 0.2.1's; a list longer than the remaining iteration budget
now fails before its first row, sotry_renderreportsRENDER-302
ahead of an error the list's rows would have raised first. load_ir_modules_strict's failures each begin with their code
(LOAD-101toLOAD-103), and it no longer logs "Phase 1 fallback" for
them.
Added
-
PageResponse::frame, the preview frame's document (DR-14 tier (c);
L-12, L-17, L-27, L-33, L-44 (b), L-52; SR32; FORMA-125, the forma-server
half). For content nobody reviewed: a document with an opaque origin, its
requests closed but for the residuals below, the zero-build runtime inline.- Its enforced
Content-Security-Policyis the strict policy with nine
directives written in their place, the one exception to SR5's no-rewrite
rule:sandbox allow-scripts(neverallow-same-origin),
frame-ancestorsthe application's origin,script-srcthe runtime's
SHA-256 (no nonce, so no script it creates or imports runs),
connect-src 'none', andimg-src data:,style-src 'nonce-N'alone,
font-src 'none',worker-src 'none',form-action 'none': in an
opaque-origin document CSP's'self'still names the application, so
markup alone would otherwise send it requests with itsSameSite=None
cookies (L-52). In the enforced header only, since browsers ignore
sandboxin Report-Only andmeta. It carries Trusted Types in
Report-Only with no reporting endpoint, so, like any page without one,
in development builds only. - Residuals (documented in the
framemodule, SECURITY.md): WebRTC
(a script can send STUN packets and DNS lookups to any host; no CSP
directive closes it in Chrome 155), the frame navigating itself (the
host'sframe-srcnaming the frame's exact URL refuses every other URL,
save the frame's own path with another query, since CSP's path matching
ignores the query: that reaches the frame route, which ignores its
query, with the application'sSameSite=Nonecookies before the
teardown, so its data lands in the application's access log; a wider
frame-srclets a navigation out, with those cookies on the
application's origin), and whatever the module shows the user inside
the frame. Link hints andsrcdocare closed in core, not by the
policy: no CSP directive governs<link rel="preconnect">, and in
WebKit a module's preconnect (itshrefwritten or bound) or an
<iframe srcdoc>holding one connected to the host it named, from inside
the frame (the A2 audit's S1).@getforma/core2.1.0's frame runtime
mounts the module withtier: 'unreviewed', which removes every<link>
andsrcdocbefore the markup reaches the document, re-checks the live
subtree, and refuses the render if one appears (formajs#35); the frame
route's self-navigation residual above is unchanged. - The document is one root,
<div id="forma-frame">, and the runtime as
one inline script; it names no URL, so loading it fetches nothing. - The runtime comes from the application's own build: the manifest's
assetsentryforma-runtime.js(FRAME_RUNTIME_ASSET), read from the
embeddeddistand hashed once at start byPreviewFrame::load::<A>(&manifest, app_origin). A missing or unusable runtime is a codedFrameErrorat
start, never a frame without it:FRAME-101(no entry),FRAME-102
(named, not embedded),FRAME-103(empty, not UTF-8, or containing
</script,<!--, a carriage return or a NUL: inlined and hashed as it
is, never rewritten, and the parser rewrites CR and NUL),FRAME-104
(the application's origin is nothttps://host[:port]or loopback
http://, with a port of 1 to 65535 and no leading zero); per request,
FRAME-201(no nonce forstyle-src) andFRAME-202(no header value,
ruled out at start). - The host's side is documented in the
framemodule: theiframe's
sandbox="allow-scripts"set withsrcbefore insertion; the host
page'sframe-srcnaming the frame's exact URL, the only control on
where the frame may navigate; oneMessageChannelport on the first
load, carrying nothing else; no listener trusting origin"null"; only
ready,resizeanderror, schema-checked; teardown on any later
load. PagePolicy's fields are private tocsp.rs, so onlypage_policyand
frame_policywrite a policy, andframe_policy's one caller is
PageResponse::frame.- Needs, from the other repositories: core's frame runtime as a classic,
self-contained script with LF line endings and no source-map comment
(FORMA-128), taking the port only fromevent.source === parent; and
@getforma/buildwriting it intodistasforma-runtime.js.
Tests:tests/preview_frame.rs>
the_frame_policy_replaces_exactly_its_nine_directives,
the_frame_document_inlines_the_runtime_by_its_hash_and_names_nothing_to_fetch,
a_frame_without_its_runtime_is_a_coded_error_at_start,
page_response_frame_is_the_only_response_that_rewrites_the_policy;
frame::tests::frame_201_a_failed_nonce_draw_is_no_frame,
frame_202_a_policy_that_is_no_header_value_is_no_frame. Checked in Chrome
155 (forma#22 and its review's harness): markup alone (<img>, a
stylesheet,url()in astyleattribute, a font) reached neither the
application nor another origin; the runtime ran;import()and scripts it
created were refused;fetchwas refused byconnect-src 'none';
window.originwas"null", with or without the header. With core
2.1.0's runtime, a module's<link>(preconnect, dns-prefetch, prefetch,
preload, a boundhref, one re-created by foreign content) and an
<iframe srcdoc>open no connection in Chromium 151, Firefox 153 or
WebKit 26.5 (formajs'se2e/frame.spec.ts).
- Its enforced
-
Trusted Types in Report-Only (DR-20, SR15; FORMA-127, the
forma-server half). A page carries
Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; trusted-types forma forma-runtimebeside its enforced policy
when the application names a reporting endpoint (report_to,
report_uri), in every build, and with no endpoint in development builds
(cfg(debug_assertions)) only, where Chromium and Firefox show its
violations in the console. A release build with no endpoint sends no
Report-Only header: it would report to nobody, and WebKit logs a console
error on every page whose Report-Only policy names noreport-to("the
policy will have no effect", found by FORMA-138's packed-set run). With
the header, a string written to a
script-carrying DOM sink, or a Trusted Types policy other than core's
formaandforma-runtime, is reported and not blocked, for this release;
3.0.0 enforces it (FORMA-109). Written byPageResponse(try_render,
render_browser_shelland theinto_page_responsebridge) and by the
deprecatedPageOutput::headers(); the enforced policy,build_csp_header
andPageOutput::cspare byte-identical to 0.2.1, so the 0.2.1 goldens
(document and enforced policy) do not move. A page whose application sets
its headers fromPageOutput::cspby hand gets no Report-Only header.
Tests:tests/trusted_types_report_only.rs>
trusted_types_go_out_in_report_only_with_an_endpoint_or_in_a_debug_build_and_the_enforced_policy_is_unchanged
(on both profiles, as are the frame and property tests);
csp::tests::csp_is_strict(both directives, Forma's two policies, no
'allow-duplicates', and SR39:style-src-attr 'unsafe-inline'the
policy's only'unsafe-inline', with every extension too); the 2,000-case
property intests/page_response.rsasserts SR39 and the Report-Only
policy on every accepted extension set, and now names reporting
endpoints too (valid, loopback and the review's hostile ones).Not quite behaviour-neutral: with
require-trusted-types-for 'script'
in any policy, Report-Only included, a browser runs the page's own
Trusted Typesdefaultpolicy, if it registered one, on every string
written to a script-carrying sink and uses what it returns; under 0.2.1
nothing ran. An application with adefaultpolicy is already Trusted
Types-aware; it should check that policy before upgrading (review
5517672933, measured in Chrome for Testing 156). -
PolicyExtensions::report_to(endpoint)andreport_uri(endpoint), the reporting endpoint the
application names (DR-20, SR8, L-38): a path on its own origin, an
https://URL, orhttp://on a loopback host. It is written into both
policies asreport-to forma-cspwithReporting-Endpoints: forma-csp="<endpoint>"(the Reporting API: Chrome and Edge, Safari 16.4+,
Firefox 149+ for CSP) andreport-uri <endpoint>(Firefox before 149;
ignored wherereport-tois supported), per MDN's compatibility data at
4d6aed6. The endpoint therefore takes bothapplication/reports+json
andapplication/csp-reportbodies. Its obligations, documented on the
method, are the application's: store report URLs without query strings,
bound each report's size and the request rate, alert per route and
directive above a baseline, deduplicated. An endpoint carrying;,,, a
quote, a backslash, whitespace or a fragment, starting with//, or using
another scheme fails the page (PolicyErrorforreport-to). Without an
endpoint nothing changes. Over plain HTTP,http://localhost
included,report_uri(endpoint)instead: Chrome and Firefox take
Reporting-Endpointsonly from an HTTPS page and, once a policy names
report-to, ignorereport-uri, so an HTTP page with both reports
nothing, onlocalhosttoo, although it is a secure context;
report_uriwritesreport-urialone, which every current engine sends
from HTTP. Checked in Chrome 155 with this branch, and in Chrome 156 and
Firefox Nightly 155 by its review:report_toover HTTPS delivered one
application/reports+jsonreport for a string written toinnerHTML, and
the page rendered; the same over HTTP, onlocalhostor another host,
delivered none;report_uriover HTTP delivered one
application/csp-report. An absolute endpoint's authority is a host and
an optional port, never userinfo (a credential there would go to every
visitor), and a loopbackhttp://endpoint is accepted in development
builds only: in a release build it would send each visitor's reports to
their own machine, so it fails the page like any invalid endpoint. Tests:
tests/trusted_types_report_only.rs>
a_named_endpoint_is_written_into_both_policies_and_reporting_endpoints,
an_invalid_endpoint_fails_the_page_on_every_path;
csp::tests::endpoints_accept_a_path_https_or_loopback_http_and_refuse_the_rest. -
PolicyExtensions::relax_trusted_types(reason), the named per-route
relaxation (DR-20's bridge; one of SR8's allowlisted exits, so the lint
finds it by name). In 0.2.x it changes no header, since Trusted Types are
Report-Only everywhere; from 3.0.0 the route keeps them Report-Only while
the others enforce. Its violations are still reported. Each relaxed route
is logged, once per route and reason per process, as atracingwarning
with the route andreason; the log allocates nothing after a pair's
first render and names at most 1,024 pairs, then says once that
route_patternmust be a pattern, not a request path; an emptyreasonfails the page (PolicyErrorfor
trusted-types). Test:tests/trusted_types_report_only.rs>
a_relaxed_route_keeps_reporting_and_is_logged_once_per_route_and_reason;
tests/relaxation_log_bound.rs>
the_relaxation_log_names_at_most_1024_pairs_and_then_says_so_once. -
PageResponse, a page that cannot be sent without its headers (DR-17,
FORMA-124). As axum'sIntoResponseit always writesCache-Control: no-store, the strict policy for the nonce its HTML carries,
Cross-Origin-Opener-Policy,X-Content-Type-Options,Referrer-Policy,
Permissions-Policyand the HTML content type. Its HTML is reachable apart
from those headers only throughinto_parts_unchecked(), one named,
searchable exit returningPageParts;counts()returns the walk's bytes
and time (RenderCounts). Get one from today's render with
PageOutput::into_page_response, which refuses (aPolicyErrorfor
content-security-policy) acspedited as text, since the policy is
rebuilt from the nonce and the edit would be lost; it trusts the deprecated
nonceandhtmlas they are, so a fresh nonce per response holds only
when the render drew it. Tests:tests/page_response.rs>
page_response_writes_no_store_the_policy_and_the_document_headers,
two_fetches_get_no_store_and_two_different_nonces,
into_parts_unchecked_returns_exactly_what_the_response_writes,
a_page_response_counts_its_walk; thecompile_faildoctest on
PageResponse(the HTML is private). -
PolicyExtensions, the typed way to loosen a page's policy (L-38):
img_src,frame_src,form_actionadd one CSP source expression each
('self', a scheme such ashttps:orblob:, or a host), and
allow_popupssendsCross-Origin-Opener-Policy: same-origin-allow-popups
for a login popup. Nothing else can be added, nothing can be removed, and a
source carrying;,,, a quote, whitespace, a keyword other than
'self', a nonce, a hash or a bare*fails the page with aPolicyError
naming the directive and the source, instead of reaching a header; so do
thejavascriptandvbscriptschemes in either form (javascript:,
javascript://host, any case), anddata:orfilesystem:as a frame or
form target (images keepdata:). This
replaces rewritingPageOutput::cspas text (GateWASM's console,
Generative UI'sframe-src blob:). Tests:tests/page_response.rs>
no_policy_extension_yields_a_response_without_the_policy(2,000 generated
extension sets, valid and hostile: every accepted one carries the whole
strict policy),an_invalid_extension_fails_instead_of_sending_a_page_without_the_policy,
extensions_add_sources_to_their_own_directive_only;csp::tests>
extension_sources_accept_what_consumers_add,
extension_sources_refuse_injection_and_loosening,
frame_and_form_targets_refuse_data_and_filesystem;tests/page_response.rsinto_page_response_refuses_a_policy_edited_as_text. -
try_render(config, ctx) -> Result<PageResponse, RenderError>, beside
render_page(DR-11, DR-12, L-3; FORMA-120). It never serves an empty shell
in place of a failure: a missing module or slots, a walk the module or data
fails, an exceeded budget (with the counts reached, SR22), an invalid policy
extension, a failed nonce draw and a panic each return aRenderErrorwith a
stable code (RENDER-101toRENDER-399, by family), the route and the
component path; Forma logs none of them (a caught panic still runs the
process's panic hook, which by default prints it). A panic in the render is caught
(catch_unwind) asRenderError::Fault, so the handler answers 500 and the
next request renders, while the application unwinds on panic (SA8). The
page'sRenderCountsgainnodesandlist_iterations.RenderCtxcarries
the policy extensions and the walk'sWalkLimits(re-exported), never a
nonce. Its documentation gives thespawn_blockingrecipe for pages past
about 12,500 nodes. Tests:tests/try_render.rs>
a_panicking_component_returns_fault_the_handler_answers_500_and_the_next_request_renders
(SR44),a_component_failing_on_one_tenants_branding_fails_that_request_alone
(S2), one test per code (render_101_…torender_399_…, and
render::tests::render_202_a_failed_nonce_draw),
try_render_writes_render_page_s_document_and_counts_the_walk,
try_render_never_serves_a_shell_in_place_of_a_failure;
render::tests::a_panic_outside_the_walk_is_still_a_fault,
an_invalid_extension_fails_before_the_walk. -
RENDER-302 before the first row (FORMA-122, SR38): through forma-ir's
list pre-check,try_renderrefuses a list longer than the remaining
iteration budget before walking any of it, its counts saying so
(list_iterations0). Test:tests/try_render.rs>
render_302_a_60_000_row_list_fails_before_its_first_row_in_under_a_millisecond
(and a list of exactly 50,000 rows renders). The deprecatedrender_page
falls back to its shell sooner on the same input. -
render_browser_shell(config, ctx): the bundle and the same headers
around an empty root, for a route the build declaresssr: false; never in
place of a failure. Test:tests/try_render.rs>
render_browser_shell_serves_the_bundle_around_an_empty_root_with_the_page_headers. -
CspNonce::try_generateandNonceError(#[non_exhaustive]): the nonce
draw that returns an error instead of panicking (DR-12). -
Requires forma-ir 0.2.2 (
forma-ir = "0.2.2", FORMA-138): it calls
walk_to_html_counted,WalkCountsandIrError::Panicked, which 0.2.1
lacks, so a lockfile holding forma-ir 0.2.1 cannot build it. -
IrLoadError::code(): the failure's stable code,LOAD-101(declared but
not embedded),LOAD-102(magic, version or flags: not a module this reader
accepts) orLOAD-103(corrupt or truncated: sections, tables, opcodes,
names), read from the start ofreason, which now begins with it (FORMA-121). -
tests/page_response.rs's policy property and invalid-extension tests now
run over every path that makes aPageResponse: the bridge,try_render
andrender_browser_shell. -
sw::ServiceWorker(off,for_app,enabled,is_enabled,app),
sw::configure,sw::configured,sw::AppNameError,
sw::AlreadyConfigured: the setting, fixed once per process before the
first page or/sw.js.sw's module docs state the per-application cache
name contract the build follows (forma:<app>:<build>; FORMA-130's build
half). Tests:tests/service_worker_off.rs>
by_default_no_page_registers_a_worker,
by_default_sw_js_is_the_retiring_worker_even_with_a_built_worker_embedded,
configuring_after_the_first_use_is_refused;
tests/service_worker_named_off.rs;tests/service_worker_on.rs(pages
register, the embedded worker is served, configured once);
sw::tests::application_names_are_what_a_cache_name_can_carry, over
tests/js/app_name_vectors.json, the accepted and refused names shared with
@getforma/build'sapp(the trailing-hyphen, one-character and
double-hyphen edges included, and the hostile names);
sw::tests::the_retiring_worker_deletes_v1_s_caches_and_its_own_never_another_s,
which runs the worker in Node overtests/js/retiring_worker.mjs, whose
table of cache names is the contract's shared vectors (anchored, separator
and prefix near misses included; the script reports its own pass count, so
the check holds under any Node test reporter, and aself.onfetch
assignment counts as answering requests).cargo testnow
needs Node 18 or later onPATH(CONTRIBUTING). -
RawHtml::trusted(..), the one named raw-HTML type (DR-19, SR15, TH8;
FORMA-126). Markup a handler vouches for, written into the page unescaped;
made only byRawHtml::trusted(noFrom,Default,FromStror public
field), so a search for that name finds every raw entry, which is what
SR8's allowlist lint will look for.body_prefixtakes it bound first and
borrowed,let logo = RawHtml::trusted(..);thenbody_prefix: Some(&logo): it dereferences to the markup, so the 0.2.x field
(Option<&str>) accepts it and the same line compiles in 3.0.0, where the
field takes only aRawHtml. (Written inline,
Some(&RawHtml::trusted(..))borrows a temporary that Rust 1.85, the
MSRV, drops at the end of the statement: E0716 there.) Written exactly as
the same text passed as a string. Tests:tests/raw_html.rs>a_search_finds_one_raw_constructor
(in the crate's sources thehtmlfield is initialised only in
trusted, whatever type name orSelfprecedes it; the trait impls, the
inherent functions and the functions returning aRawHtml, however the
type is path-qualified, are exact lists; no other name for the type (a
typealias, ause .. asrename) and nor#html; and sixteen second
constructors, the two reviews' seven among them, each written into a copy
of the sources, are found),the_documented_body_prefix_form_compiles_on_the_msrv_and_renders
(the bound form, compiled by the 1.85 MSRV job),
a_raw_html_body_prefix_is_written_exactly_as_the_same_string_on_every_path
(render_page,render_page_with_nonce,try_render,
render_browser_shell); thecompile_faildoctests onRawHtml
(From<String>,From<&str>,parse,default, a struct literal). -
Typed homes for what applications put in
body_prefixthat is not their
own markup (DR-19; FORMA-126), written only bytry_renderand
render_browser_shell, throughRenderCtx:RenderCtx::with_module_preloads(&ModulePreloads): forma-server writes
the route'smodulepreloadlinks from the build's manifest (the
modulepreloadlist@getforma/buildwrites withcodeSplitting), each
with the page's nonce, after the route's own modules: the home for
Ledger's preload workaround.assets::try_load_module_preloads::<A>()
reads the table at start and refuses a manifest whose preload names a
file that is not one of the build'sassets(another origin, a path,
markup); the route's own modules and repeats are left out. Tests:
tests/typed_homes.rs>
try_render_writes_the_routes_manifest_modulepreload_links_nonced(three
routes, both renders, the nonce the policy blesses),
a_manifest_preload_that_is_not_one_of_the_builds_files_refuses_the_table;
head::tests::a_preload_must_be_one_of_the_builds_files.RenderCtx::with_head_link(HeadLink): a<link>in the head (an icon, a
canonical URL, a feed), every value escaped, written after the
stylesheets and preloads in the order added: the home for ksx Studio's
</head>splice.HeadLink::new(rel, href)takes a closed list of link
types, none of which loads style or script:icon,apple-touch-icon,
mask-icon(images, underimg-src),manifest,canonical,
alternate,author,license(metadata),preconnect,dns-prefetch
(connection hints), matched without regard to case. It refuses, with a
HeadLinkErrorand itscode():HEAD-101forstylesheet,
modulepreload,preload,prefetch,prerenderandimport(a
page's stylesheets and module preloads come from the build's manifest);
HEAD-102for any other word;HEAD-103for ajavascript:or
vbscript:href, in any case and with the tabs, newlines and leading
controls a browser strips;HEAD-104for an empty href.with_type,
with_sizesadd attributes. Amanifestlink needsmanifest-src,
which the default policy (default-src 'none') does not grant and
PolicyExtensionscannot add, so it is refused by the browser today
(documented onHeadLink). Tests:tests/typed_homes.rs>
head_links_are_written_escaped_in_the_head_in_the_order_added,
a_head_link_carries_only_the_listed_link_types(every kept type, every
loading type, and every other link type the HTML standard defines),
a_head_link_refuses_a_script_url_and_keeps_every_other;
head::tests::script_urls_are_recognised_as_a_browser_reads_them.- Data:
PageConfig::config_json, as before (and in 3.0.0 the state block).
The deprecatedrender_pagetakes noRenderCtxand writes the document
0.2.1 wrote (tests/compat_0_2_1.rs).
-
IrModules<A>: in development, a page's module is reread when the
build's watch mode rewrites it (FORMA-166; L-31, N7; FORMA-022 is the
build's half).IrModules::<Assets>::load(&manifest)is the strict loader
(the same check, the sameLOAD-1xxerrors, refuse to start on them), and
get(route)returns the route's module as anArc<IrModule>. In a debug
build (cfg(debug_assertions), the switch rust-embed itself uses to read
files from disk) eachgetrereads the manifest's entry and the module's
file, and loads a changed module through the same check: the next request
serves the edit with no restart. A module that fails to load keeps the
previous one and logs its coded error as atracingwarning, once per
distinct write; a route the manifest starts naming is loaded on its first
get. A release build does no file watching and no per-request check:
getis a map lookup over the modules loaded at start. No watcher thread
and no new dependency. Opt-in:load_ir_modules_strictis unchanged.
Tests:tests/dev_reload.rs>
an_edited_page_is_served_on_the_next_request_and_a_broken_edit_keeps_the_previous_page
(real rust-embed over files published by rename, debug builds);
ir_compat::reload_tests>
a_release_build_reads_nothing_per_request_and_a_debug_build_rereads(on
the release profile: no read after start, the start's module served),
the_reload_loads_through_the_strict_loader_at_start,
development::a_module_that_fails_to_load_keeps_the_previous_one_and_is_logged_once_with_its_code,
development::the_manifest_can_rename_a_module_or_name_a_new_one_and_a_bad_one_changes_nothing;
tests/strict_loader.rs>
ir_modules_load_refuses_exactly_what_the_strict_loader_refuses.
Deprecated
Each still compiles and behaves as at 0.2.1, warns, and goes in 3.0.0:
| Deprecated in 0.2.2 | Use instead (3.0.0's form) |
|---|---|
render_page(config) |
try_render(config, &RenderCtx::new()), which returns Result<PageResponse, RenderError>; render_browser_shell for a route the build declares ssr: false |
render_page_with_nonce(config, nonce) |
try_render: the render draws the nonce, and 3.0.0 takes none from a caller |
load_ir_modules(manifest), the lenient loader |
load_ir_modules_strict(manifest) at start, refusing to start on its errors (or IrModules, which loads the same way) |
assets::load_manifest() |
assets::try_load_manifest(); in 3.0.0 load_manifest itself returns the Result |
PageOutput's public html, csp and nonce fields, and PageOutput::headers() |
a PageResponse: from try_render, or PageOutput::into_page_response for 0.2.1 code; extend the policy with PolicyExtensions, never as text. The fields go private in 3.0.0 |
PageConfig::config_script |
PageConfig::config_json for data, and the page's bundle for code; set it to None until 3.0.0 removes it |
a plain string in PageConfig::body_prefix |
RawHtml::trusted(..), bound first and passed as Some(&raw); the route's preloads through RenderCtx::with_module_preloads, head links through RenderCtx::with_head_link, data through config_json |
AssetManifest::wasm and WasmAssets (and the window.__FORMA_WASM__ script) |
none in forma-server: islands re-render with @getforma/core's h() |
load_ir_modules, the lenient loader: it serves a corrupt module's route as
an empty shell for the life of the process.load_ir_modules_strictis the
default loader (DR-11, Q13; FORMA-121): call it at start and refuse to start
on its errors, before the port binds. The lenient loader behaves exactly as
at 0.2.1, and in debug builds logs its deprecation once per process. Tests:
tests/strict_loader.rs>
a_flipped_byte_fails_the_default_loader_naming_route_file_and_code,
a_truncated_module_fails_the_default_loader_naming_route_file_and_code,
with_a_bad_module_the_fixture_server_binds_no_port(and its other side,
the_same_server_binds_and_serves_with_good_modules_or_the_lenient_loader),
a_route_declared_ssr_false_needs_no_module,
the_deprecated_lenient_loader_behaves_as_at_0_2_1;
tests/load_ir_modules_deprecation.rs. The fixtures are written by code in
the test file and pinned to it (module_fixtures_are_what_their_generator_writes).
FMIR v2 has no checksum: a flipped byte that no structure check reaches (in
the source hash, or inside a text string) still loads, as the fixtures
record; FMIR v3's CRC-32 closes that (DR-28, FORMA-114).render_page: it serves the empty Phase 1 shell with a 200 on any failure.
Usetry_render(DR-11). It keeps its fallback until 3.0.0, and in debug
builds logs its deprecation once per process (tests/render_page_deprecation.rsrender_page_logs_its_deprecation_once_per_process_in_debug_builds).assets::load_manifest, which panics on a missing or malformed manifest:
usetry_load_manifest(L-13).PageOutput's public fieldshtml,cspandnonce, and
PageOutput::headers(), which drops an invalid policy silently: send a
PageResponse(DR-17). The fields go private in 3.0.0.PageConfig::config_script, which writes handler text into a nonced script
(TH4): pass data throughconfig_json, move code into the bundle (DR-18).
EveryPageConfigliteral names the field, soconfig_script: Nonewarns
too; it is removed in 3.0.0.render_page_with_nonce: from 3.0.0 the render draws every nonce and no
caller supplies one (L-14). Usetry_render.- A plain string in
PageConfig::body_prefix(DR-19; FORMA-126): it is
written exactly as before, and in debug builds the first one a process
renders logs a warning, codeRAW-101, naming the route; aRawHtmlnever
does, and release builds keep no record and check nothing. 3.0.0 accepts
only aRawHtml. To tell the two apart a debug build remembers the address
and length of every liveRawHtml's markup (raw::provenance). Tests:
tests/body_prefix_string_warning.rs>
a_string_body_prefix_warns_once_per_process_in_debug_builds_and_a_raw_html_never_does
(debug and release);raw::provenance::tests. AssetManifest::wasmandWasmAssets, the WASM walker's entry in the
manifest (FORMA-131, DR-23). The walker is deprecated across the stack:
@getforma/core/wasm, forma-ir'swasmfeature and@getforma/build's
wasmkey all go in 3.0.0. With them goes the nonced
window.__FORMA_WASM__script a page gets when the manifest names a
walker and its route has an.irmodule; until then that page is
unchanged. EveryAssetManifestliteral names the field, sowasm: None
warns too. Doctest pairs:ManifestWasm,ManifestWasmLiteral,
WasmAssetsType.- Each
#[deprecated]item (seven rows of the table above, eleven
attributes) is pinned by a doctest pair insrc/deprecations.rs(the 0.2.1
call compiles underallow(deprecated)and fails under
deny(deprecated)). The eighth row, a plain string in
PageConfig::body_prefix, carries no#[deprecated](the field's type is
unchanged) and warns only at run time in debug builds, pinned by
tests/body_prefix_string_warning.rs. Andtests/compat_0_2_1.rscalls the whole 0.2.1 page
API as an application does; its
render_page_writes_the_0_2_1_document_byte_for_bytecompares the
document and policy against goldens written by 0.2.1 itself.
Changed
-
The service worker is opt-in, and retired everywhere else (FORMA-130;
DR-25, Q15, M25). By default no page registers/sw.jsany more, and
serve_swanswers/sw.jswith forma-server's retiring worker instead of
thesw.jsthe build embedded: a browser that installed a worker from an
earlier release receives it on its next visits, and it deletes Forma's v1
cache names (forma-dev,forma-+ 12 hex, either with-on-demand-v1),
keeps every other cache, unregisters, and answers no request, so pages and
assets load from the network. An application that wants its worker (today
Ledger, Q15) turns it on once at start:
forma_server::sw::configure(ServiceWorker::for_app("ledger")?.enabled());
its pages then register/sw.jsas at 0.2.1 andserve_swserves the
embedded worker. Naming the application with the worker off (a rollback)
makes the retiring worker delete that application's own caches too. The
retiring worker is served through 3.x.render_page's document therefore
differs from 0.2.1's by exactly that one registration line:
tests/compat_0_2_1.rs>render_page_writes_the_0_2_1_document_byte_for_byte
takes it out of 0.2.1's goldens, once, and checks every other byte.Migration: delete any code that strips or asserts the registration
script. 0.2.2 writes none by default, so code that looks for it now finds
nothing. fAuth's (formerly GateWASM)render_pagewrapperdebug_assert!s
the line before stripping it, so every debug-build render panics on 0.2.2
until the wrapper changes (FORMA-150), and itsforma-server = "0.2.0"
requirement lets a plaincargo updatebring 0.2.2 in. No application
gates this release (L-53): fAuth is fixed forward when it migrates, and
pins=0.2.1until then.
An application that enables its worker with nosw.jsembedded is warned
once, at the first/sw.jsrequest (tests/service_worker_on.rs>
an_enabled_application_with_no_built_worker_is_told_once). -
load_ir_modules_strictno longer logs "Phase 1 fallback" for the failures
it returns: it takes no fallback, and the caller holds every failure. The
lenient loader still logs each one. Each failure'sreasonnow starts with
its code (LOAD-101toLOAD-103), soDisplaynames route, file and code. -
No
unwrap,expectorpanic!in the shipped code (DR-12, SR44):
Clippy denies them outside tests.serve_assetandserve_swanswer 500
rather than panic if a response fails to build (it cannot with today's
headers).CspNonce::generate,assets::assetand the deprecated
assets::load_manifestkeep their documented 0.2.1 panics under a scoped
allow, each beside its fallible twin; none is on the render path. -
build_csp_headerwrites from the directive table the extensions share; its
output is byte-identical to 0.2.1 (csp::tests>
build_csp_header_is_byte_identical_to_0_2_1).
Fixed (documentation only)
RouteAssets::ir's doc comment names its value as the build writes it, the
hashed.irfile name, not a.fmirmodule (FORMA-009).