Skip to content

feat(web): serve a per-request CSP nonce so Emotion's styles apply under style-src 'self' - #874

Merged
mforce merged 10 commits into
mainfrom
feat/csp-nonce-for-emotion
Sep 16, 2026
Merged

mforce merged 10 commits into
mainfrom
feat/csp-nonce-for-emotion

Conversation

@mforce

@mforce mforce commented Sep 14, 2026 •

Copy link
Copy Markdown
Owner

Why

The Production CSP is style-src 'self'. MUI styles through Emotion, which injects a <style>
element at runtime, and the browser refuses to parse it under that policy — so no sx,
styled() or styleOverrides value reaches the screen, and nothing reports it. Measured on the
sim harness in #871: the element was in <head>, html computed box-sizing: content-box, and
/daily-entry laid out 419px wide in a 390px frame. It stayed invisible only because no MUI
component had rendered yet.

The owner's answer (2026-09-14) keeps the policy strict and gives style-src a per-response nonce.
SecurityHeaders mints it, index.html is served templated with the same value in a <meta>, and
FarmThemeProvider hands it to Emotion's cache. script-src is untouched.

Closes #873

Scope

  • SecurityHeaders.BuildContentSecurityPolicy(nonce) replaces the ContentSecurityPolicy constant.
    GetOrCreateNonce(HttpContext) mints 16 bytes of RandomNumberGenerator output per request and
    caches it on HttpContext.Items, so the document and the header read one value.
  • src/Cluckwork.Api/Hosting/SpaShell.cs reads wwwroot/index.html once at boot, splits it at
    <head>, and answers /, /index.html and the SPA fallback. app.UseDefaultFiles() is gone;
    MapFallbackToFile survives only for the no-wwwroot case. Hashed /assets/* are untouched.
  • /index.html is in that set deliberately: workbox precaches it and the service worker then
    answers every navigation from that cached response, so an untemplated copy there would cost MUI
    its styling on every client that installed the worker.
  • web/src/theme/FarmThemeProvider.tsx reads the meta once at module load into
    createCache({ key: "mui", nonce, prepend: true }) behind CacheProvider. @emotion/cache
    becomes a declared dependency; the regenerated lock adds one line and pins nothing new.
  • web/src/routes/Login.tsx renders no MUI component. Amendment (feat(web): serve a per-request CSP nonce so Emotion's styles apply under style-src 'self' #874 review, 2026-09-14): an
    earlier revision of this slice added a hidden .sr-only, aria-hidden MUI Chip solely so the
    Playwright spec had a real Emotion-styled element to read a computed background from. The owner
    removed it as production markup that exists only for a test. The end-to-end assertion — an MUI
    component's computed style applies under the real CSP, online and offline — moves to web: visual language for the MUI revamp, chosen and encoded as theme overrides #864's first
    rendered component; see the amendment comment on that issue.
  • docs/decisions/674-ui-component-library.md gains an amendment; docs/architecture.md gains the
    SpaShell placement row.
  • No new configuration key, so Sim harness (#243) rotted silently: 4 breakages, no CI ever ran it #370's sim harness (bootstrap.sh, docker-compose.sim.yml,
    verify-harness.sh) and Developer experience: add an Aspire AppHost for local orchestration and observability #565's AppHost need nothing. No GLOSSARY or Help change: this adds no
    concept and no user-visible behaviour.

Tradeoffs

'unsafe-inline' was rejected — it reopens CSS injection including attribute-selector
exfiltration. Build-time CSS extraction was rejected too: it fights #674's runtime
palette-from-the-document design.

The shell carries no ETag and no Last-Modified. A validator derived from the file would tell a
client its cached copy is fresh while the live header no longer admits that copy's nonce, which is
the failure this change exists to prevent. #141's no-cache is unchanged.

For a client running the service worker the nonce is per precache entry, not per request.
navigateFallback answers navigations from the cached shell, online and offline. That is safe
because workbox caches one whole Response, so its header and its body were minted together — the
offline test asserts that rather than assuming it. Not precaching the shell would retire #142's
offline guarantee, a worse trade.

Blast Radius

Every response's CSP header and every SPA document. An index.html that cannot be templated fails
the boot rather than serving an unstyled app. /api/* and /health/* keep their 404 guards ahead
of the fallback (#266), pinned by tests. /INDEX.HTML still 404s: the path match is ordinal,
mirroring the case-sensitive file lookup it replaces on Linux.

Verification

dotnet build Cluckwork.sln clean. dotnet test Cluckwork.sln under sg docker: 2625 passed, 0
failed. npm run typecheck and npm run test:coverage in web/: 3001 passed, ratchet green and
not re-baselined. verify-harness.sh green. The full Playwright smoke suite on a sim stack rebuilt
at this head: 48 passed, 1 skipped (the dispatch-only expiry spec).

Four mutations, each run red before the claim.

Mutation Layer Result
createCache drops nonce vitest red, nonce attribute null
nonce minted once per process integration red, 2 of 21
style-src drops the nonce integration red, 8 of 21
SpaShell drops the meta integration red, 5 of 10

Amendment (#874 review, 2026-09-14): this table originally carried two further rows — the same
two mutations (style-src drops the nonce; SpaShell drops the meta), run as source mutations
with an image rebuild against the Playwright layer — plus a probe-background measurement table and
six screenshots of a hidden MUI Chip on the login screen, showing its computed background under
the real CSP with and without the nonce. Both depended on that Chip, which existed only to be
measured; the owner removed it as production markup that exists only for a test (see Scope). The
nonce-agreement assertions csp-nonce.spec.ts keeps still exercise the same two mutations — a
dropped header nonce or a dropped meta both break the document-matches-its-own-header comparison
the surviving assertions make — so the regression class stays covered; what is gone is the specific
claim that a computed background caught them, since the apparatus that measured it no longer
exists.

This PR changes nothing a user sees.

@coderabbitai

coderabbitai Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: e5a93a63-00a9-4db1-89ac-e2c4237da11a


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mforce

mforce commented Sep 14, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

mforce added a commit that referenced this pull request Sep 14, 2026
…ring

Every_response_carries_the_security_headers compared the response against
SecurityHeaders.BuildContentSecurityPolicy(nonce) — the same function under
test — so a directive dropped from the builder changed both sides of the
assertion identically and stayed green. Confirmed by mutation: removing
form-action 'self' from the builder left the test passing until the expected
policy was rewritten by hand as a literal string with only the nonce
substituted, after which the same mutation went red.

Local Codex review on #874.
mforce added a commit that referenced this pull request Sep 14, 2026
…/HEAD

Two gaps in the #873 SPA shell, both measured against a real loopback
Kestrel listener before being fixed:

- UseSpaShell's exact-match check misses a raw "GET //index.html" request
  (Kestrel does not collapse a double leading slash the way it collapses a
  "/./" segment, which the app never even sees), so it fell through to
  UseStaticFiles. PhysicalFileProvider still resolved it to the on-disk
  index.html and served it raw — no nonce meta, plus an ETag/Last-Modified
  pair #873 deliberately never emits on this document. IndexHtmlHidingFileProvider
  wraps the static-file provider so index.html is unreachable under any
  spelling; the request then falls through further to MapFallback, which
  answers with the templated shell like any other client-side route.

- MapFallback(spaShell.WriteAsync) carried no HTTP method restriction, so
  POST / and DELETE /index.html both returned a 200 HTML shell. The
  StaticFileMiddleware + MapFallbackToFile pair this replaced served only
  GET/HEAD (measured against that pre-#873 pipeline directly: it answers 405
  with "Allow: GET, HEAD"). UseSpaShell's exact-match branch now falls
  through for any other method, and MapFallback carries the same
  HttpMethodMetadata, so both entry points answer the one 405.

/./index.html needed no fix: Kestrel's own PathNormalizer collapses it to
/index.html before the app runs, which SpaShellTests now pins directly.

Local Codex review on #874.
@mforce

mforce commented Sep 14, 2026 •

Copy link
Copy Markdown
Owner Author

Local Codex review (CodeRabbit rate limited)

Three findings from a local Codex pass, verified against the code and against
real behavior (a real loopback Kestrel listener, not just the in-memory
TestServer) before changing anything.

1. SecurityHeadersTests.cs:46 compared the response against itself.
Held. Every_response_carries_the_security_headers asserted the response's
CSP equals SecurityHeaders.BuildContentSecurityPolicy(nonce) — the same
function under test — so a directive dropped from the builder changed both
sides identically. Confirmed by mutation: removed form-action 'self' from
the builder, the test stayed green. Fixed by pinning an independently written
expected policy string with only the nonce substituted; the same mutation
now goes red. Commit a376e21.

2. //index.html and /./index.html bypass UseSpaShell's exact match.
Partially held, and the two examples split. //index.html is real: measured
over a raw socket against a real Kestrel listener, HttpContext.Request.Path
is literally //index.html (Kestrel does not collapse a double leading
slash), it misses the exact-match compare, and UseStaticFiles's
PhysicalFileProvider served the raw, untemplated index.html at 200 — no
nonce meta, plus the ETag/Last-Modified pair #873 exists specifically to
avoid on this document. /./index.html does not hold: Kestrel's own
PathNormalizer collapses the /./ segment to /index.html before the app
ever sees it
, verified the same way. Fixed by wrapping the static-file
provider (IndexHtmlHidingFileProvider) so index.html is unreachable under
any leading-slash or case spelling; the request then falls through to
MapFallback, which serves the templated shell like any other client-side
route (or 404s, when routing itself can't match the malformed path — also
observed). New tests: a raw-socket end-to-end case
(SpaShellRawPathTests.Double_leading_slash_never_serves_the_untemplated_document),
a unit test on the wrapper directly
(IndexHtmlHidingFileProviderTests), and a pinned regression proving
/./index.html stays safe without a fix
(SpaShellTests.Dot_segment_is_normalised_before_the_shell_ever_sees_it).
Commit d02cad9.

3. POST / and DELETE /index.html returned 200 HTML. Held, and
confirmed directly: MapFallback(spaShell.WriteAsync) carried no HTTP method
restriction, so both write methods got a 200 shell. Measured the pre-#873
pipeline (UseDefaultFiles + UseStaticFiles + MapFallbackToFile) the same
way and confirmed it answers 405 with Allow: GET, HEAD for both — that's
the behavior this restores. Fixed by adding HttpMethodMetadata([GET, HEAD])
to the MapFallback registration and gating UseSpaShell's exact-match
branch on GET/HEAD so a write method falls through to the same
method-restricted fallback rather than being served directly. New test:
SpaShellTests.Write_methods_cannot_get_a_successful_shell_response (POST /,
DELETE /index.html), asserting 405 with the Allow header. Commit d02cad9.

Verification. dotnet build Cluckwork.sln clean, 0 warnings. Touched
integration tests (SecurityHeaders, SpaShell*, StaticAssetCaching): 37
passed, 0 failed. Full Cluckwork.Api.IntegrationTests suite as a broader
check on the shared Program.cs pipeline wiring: 1842 passed, 0 failed (7m
24s). Both mutation checks (form-action removal for finding 1; measuring the
pre-#873 pipeline for finding 3) ran and were reverted before committing.


Round 2 (local Codex pass, at d02cad9).

4. SecurityHeadersTests.cs:98 asserted the nonce length against
SecurityHeaders.NonceByteCount itself.
Held — same shape as finding 1.
Style_src_keeps_self_and_adds_exactly_one_nonce compared the decoded nonce
length to the constant under test, so lowering NonceByteCount from 16 to 4
(32-bit nonces) changed both sides identically and stayed green. Confirmed
by mutation: set the constant to 4, the test stayed green until the
assertion was rewritten against a literal minimum of 16 decoded bytes
(128 bits, CSP's own floor for "unguessable"); the same mutation then went
red. Reverted the mutation before committing. Commit 21299c9.

Verification. dotnet build Cluckwork.sln clean, 0 warnings. Touched
integration tests: 37 passed, 0 failed.

Review loop stopped deliberately here: round 1 found two product defects
(fixed), round 2 found one test-scaffolding issue (fixed); per the repo's
rule 7 the loop stops when findings turn to the tests' own scaffolding.


Owner decision on #874 (2026-09-14), not a review finding.

The hidden CSP-probe Chip on the login screen was production markup that existed only for a
test. Removed rather than relocated: web/src/routes/Login.tsx no longer imports
@mui/material/Chip or renders it. Login.styles.test.ts and Login.test.tsx had no reference to
it (FarmThemeProvider.test.tsx/FarmThemeProvider.nonce.test.tsx render their own throwaway
Chip fixtures, unrelated to this probe — left untouched).

csp-nonce.spec.ts keeps the nonce-agreement assertions (served document's meta nonce matches its
own response's style-src nonce, online and after an offline service-worker reload) and the
no-CSP-violation assertion; the PROBE locator, the brandColour CSSOM helper, and the two
computed-background assertions that needed the probe are gone. The end-to-end "an MUI component's
computed style applies under the real CSP, online and offline" assertion moves to #864's first
rendered MUI component — amendment comment on that issue:
#864 (comment), and
docs/decisions/674-ui-component-library.md carries a matching amendment.

PR body's Scope and Verification updated to match: the Login.tsx bullet now describes the removal
instead of the probe; the mutation table dropped the two Playwright rows that depended on the probe
(kept the four that don't — createCache drops nonce, nonce-minted-once, style-src drops nonce,
SpaShell drops meta, all still integration/vitest-covered); the probe-background measurement table
and six screenshots are replaced with one sentence, since they described the same now-removed
apparatus: "This PR changes nothing a user sees."

Verification. npm run typecheck: clean. npm run test:coverage: 3001 passed (same count as
before — nothing in the vitest suite referenced the probe). Playwright CSP spec against the sim
stack: skipped. The running cluckwork-sim-app-1 stack was built at 2026-09-14T18:13:43Z,
before every commit from this PR's review rounds (a376e21, d02cad9, 21299c9, d591c8e) — not
at this head, and per instruction not rebuilt in case another agent is using it.

Commit d591c8e.

mforce added a commit that referenced this pull request Sep 14, 2026
…onstant

Style_src_keeps_self_and_adds_exactly_one_nonce asserted the decoded nonce
length equals SecurityHeaders.NonceByteCount — the same constant under
test — so shrinking NonceByteCount from 16 to 4 (32-bit nonces, well below
CSP's "unguessable" floor) changed both sides of the assertion identically
and stayed green. Confirmed by mutation: set the constant to 4, the test
stayed green until the assertion was rewritten against a literal minimum of
16 decoded bytes, after which the same mutation went red.

Local Codex review round 2 on #874, at d02cad9.
mforce added a commit that referenced this pull request Sep 14, 2026
Owner decision on #874 (2026-09-14): the hidden `.sr-only`, `aria-hidden`
MUI Chip on the login screen was production markup that existed only for a
test — the #873 CSP-nonce Playwright spec needed a real Emotion-styled
element to read a computed background from, and a probe is not the same
thing as a real component. Removed rather than relocated.

csp-nonce.spec.ts keeps the nonce-agreement assertions (the served
document's meta nonce matches its own response header's style-src nonce,
online and after an offline service-worker reload) and the no-CSP-violation
assertion; the computed-background assertions and the brandColour/PROBE
helpers they needed are gone with the probe. The end-to-end "an MUI
component's computed style applies under the real CSP, online and offline"
assertion moves to #864's first rendered MUI component — amendment comment
on that issue, and in docs/decisions/674-ui-component-library.md.

PR #874 body's Scope and Verification amended to match: the Login.tsx
bullet, the two Playwright mutation rows that depended on the probe (kept
the four that don't), and the six probe screenshots replaced with a
sentence — this PR changes nothing a user sees.
MUI styles through Emotion, which injects a <style> element at runtime.
Under `style-src 'self'` the browser refuses to parse it, so no MUI
styling reaches the screen and nothing reports it.

`BuildContentSecurityPolicy(nonce)` replaces the policy constant and
`GetOrCreateNonce` mints 16 bytes of RandomNumberGenerator output per
request, cached on `HttpContext.Items` so the document and the header
read one value. Nothing else in the policy moves, and the test now pins
the whole string rebuilt from the one token allowed to vary.
`SpaShell` reads the built index.html once at boot, splits it at
<head>, and answers `/`, `/index.html` and the SPA fallback with a
`<meta name="csp-nonce">` carrying that response's own nonce.

`/index.html` is in that set on purpose: workbox precaches it and the
service worker then answers every navigation from the cached response,
so an untemplated copy there costs MUI its styling on every client that
installed the worker.

The shell emits no ETag and no Last-Modified. A validator derived from
the file would tell a client its cached copy is fresh while the live
header no longer admits that copy's nonce. #141's `no-cache` is
unchanged and hashed /assets/* stay static and immutable.
`FarmThemeProvider` reads `<meta name="csp-nonce">` once at module load
and wraps its children in `CacheProvider` with a cache created from that
value. A missing meta yields no nonce, which is Vite's dev document and,
in a production build, the fail-closed outcome.

`@emotion/cache` moves from a transitive of `@emotion/react` to a
declared dependency; the regenerated lock adds one line and pins nothing
new.

The login screen renders one hidden MUI Chip. Emotion injects nothing
until an MUI component renders, so the end-to-end check needs one to
read. It is `.sr-only` and `aria-hidden` because this slice is
groundwork sequenced before every screen slice, and a visible Chip would
make a styling decision #864 owns.
The defect is invisible to every other instrument here: the DOM is
correct and only the computed style is wrong. So the spec reads a
rendered background in a real browser under the real header, and
compares the document's nonce with the one its own response admits.

The offline half is not redundant. The service worker answers
navigations from the precached shell, and that is only safe because
workbox caches one whole Response, so the header and the body it stores
were minted together.

mutants.ts records why these two mutants are source rebuilds rather than
network-boundary ones: a page.route rewrite lands on the first load and
lapses once the worker takes over, which is the half that matters.
The amendment carries the finding, the two rejected options, and the
invariant that every Emotion cache in this app carries the page nonce.
It also states the consequence a reader will not infer: for a client
running the service worker the nonce is per precache entry, not per
request.

architecture.md gains the SpaShell placement row, because a middleware
whose order is load-bearing belongs in the drawn pipeline.
…ring

Every_response_carries_the_security_headers compared the response against
SecurityHeaders.BuildContentSecurityPolicy(nonce) — the same function under
test — so a directive dropped from the builder changed both sides of the
assertion identically and stayed green. Confirmed by mutation: removing
form-action 'self' from the builder left the test passing until the expected
policy was rewritten by hand as a literal string with only the nonce
substituted, after which the same mutation went red.

Local Codex review on #874.
…/HEAD

Two gaps in the #873 SPA shell, both measured against a real loopback
Kestrel listener before being fixed:

- UseSpaShell's exact-match check misses a raw "GET //index.html" request
  (Kestrel does not collapse a double leading slash the way it collapses a
  "/./" segment, which the app never even sees), so it fell through to
  UseStaticFiles. PhysicalFileProvider still resolved it to the on-disk
  index.html and served it raw — no nonce meta, plus an ETag/Last-Modified
  pair #873 deliberately never emits on this document. IndexHtmlHidingFileProvider
  wraps the static-file provider so index.html is unreachable under any
  spelling; the request then falls through further to MapFallback, which
  answers with the templated shell like any other client-side route.

- MapFallback(spaShell.WriteAsync) carried no HTTP method restriction, so
  POST / and DELETE /index.html both returned a 200 HTML shell. The
  StaticFileMiddleware + MapFallbackToFile pair this replaced served only
  GET/HEAD (measured against that pre-#873 pipeline directly: it answers 405
  with "Allow: GET, HEAD"). UseSpaShell's exact-match branch now falls
  through for any other method, and MapFallback carries the same
  HttpMethodMetadata, so both entry points answer the one 405.

/./index.html needed no fix: Kestrel's own PathNormalizer collapses it to
/index.html before the app runs, which SpaShellTests now pins directly.

Local Codex review on #874.
…onstant

Style_src_keeps_self_and_adds_exactly_one_nonce asserted the decoded nonce
length equals SecurityHeaders.NonceByteCount — the same constant under
test — so shrinking NonceByteCount from 16 to 4 (32-bit nonces, well below
CSP's "unguessable" floor) changed both sides of the assertion identically
and stayed green. Confirmed by mutation: set the constant to 4, the test
stayed green until the assertion was rewritten against a literal minimum of
16 decoded bytes, after which the same mutation went red.

Local Codex review round 2 on #874, at d02cad9.
Owner decision on #874 (2026-09-14): the hidden `.sr-only`, `aria-hidden`
MUI Chip on the login screen was production markup that existed only for a
test — the #873 CSP-nonce Playwright spec needed a real Emotion-styled
element to read a computed background from, and a probe is not the same
thing as a real component. Removed rather than relocated.

csp-nonce.spec.ts keeps the nonce-agreement assertions (the served
document's meta nonce matches its own response header's style-src nonce,
online and after an offline service-worker reload) and the no-CSP-violation
assertion; the computed-background assertions and the brandColour/PROBE
helpers they needed are gone with the probe. The end-to-end "an MUI
component's computed style applies under the real CSP, online and offline"
assertion moves to #864's first rendered MUI component — amendment comment
on that issue, and in docs/decisions/674-ui-component-library.md.

PR #874 body's Scope and Verification amended to match: the Login.tsx
bullet, the two Playwright mutation rows that depended on the probe (kept
the four that don't), and the six probe screenshots replaced with a
sentence — this PR changes nothing a user sees.
@mforce
mforce force-pushed the feat/csp-nonce-for-emotion branch from d591c8e to feb46ef Compare September 16, 2026 04:26
@mforce

mforce commented Sep 16, 2026

Copy link
Copy Markdown
Owner Author

Rebased onto main after #871 merged (head feb46ef). One conflict, in docs/decisions/674-ui-component-library.md: both PRs appended an amendment; both are kept in commit order. Verified locally on the rebase: npm run typecheck clean, theme and bare-element guard suites 58/58, both ImagePin guards green.

…ch walk resolves it

The guard resolves a route handler through its declaring type; a method
group on a local variable (spaShell.WriteAsync) has none it can see and
the walk fails closed. A local function keeps the same delegate and the
same GET/HEAD metadata.
@mforce

mforce commented Sep 16, 2026

Copy link
Copy Markdown
Owner Author

CI on the rebase failed the application leg: #872's AdapterReachRealTreeTests and CouplingMatrixRealTreeTests (merged after this branch was written) cannot resolve app.MapFallback(spaShell.WriteAsync), a method group on a local. Fixed in ec48463 with a named local function carrying the same delegate and GET/HEAD metadata; the three architecture guards pass locally, no behaviour change.

@mforce
mforce merged commit ba4e6f3 into main Sep 16, 2026
17 checks passed
@mforce
mforce deleted the feat/csp-nonce-for-emotion branch September 16, 2026 04:43
mforce pushed a commit that referenced this pull request Sep 16, 2026
🤖 I have created a release *beep* *boop*
---


## [0.1.2](v0.1.1...v0.1.2)
(2026-09-16)


### Features

* **data:** standardize business record chronology
([#820](#820))
([6231b31](6231b31))
* **infra:** optional leader-lease endpoint for pooled deploys
([#869](#869))
([e9bc6a7](e9bc6a7))
* **sim:** seed a second farm for the README dashboard capture
([#867](#867))
([de407c6](de407c6))
* **web:** adopt MUI, themed from the farm palette tokens
([#674](#674))
([#860](#860))
([6c83c5c](6c83c5c))
* **web:** convert Daily entry to MUI, field-first on the phone
([#888](#888))
([b66f8b8](b66f8b8))
* **web:** convert the Dashboard and app shell to MUI
([#829](#829))
([#883](#883))
([2e94277](2e94277))
* **web:** retire the Slack-blue link colour for ink + a rule underline
([#884](#884))
([c08f9d8](c08f9d8))
* **web:** serve a per-request CSP nonce so Emotion's styles apply under
style-src 'self'
([#874](#874))
([ba4e6f3](ba4e6f3))
* **web:** visual language theme overrides for the MUI revamp
([#864](#864))
([#882](#882))
([0bb6b73](0bb6b73))
* **web:** whole-app MUI baseline, theme policy guard and the
[#740](#740) phone action rule
([#823](#823))
([#871](#871))
([af565e4](af565e4))


### Bug fixes

* **auth:** fail closed on unresolved flock-scope actors
([#787](#787))
([#868](#868))
([16d0350](16d0350))
* **auth:** make farm configuration owner-only
([#870](#870))
([42f9036](42f9036))
* **e2e:** repoint the canary at the markup two PRs replaced
([#844](#844))
([18b45dc](18b45dc))
* **i18n:** tl glossary uses the standard passive of ilagay
([#813](#813))
([20dec10](20dec10)),
closes [#738](#738)
* **sim:** stop the k6-baseline EXIT trap masking a clean run as failed
([#838](#838))
([f5ec96f](f5ec96f))
* **web:** declare the rule tokens the Dashboard reads, and guard
undeclared custom properties
([#885](#885))
([5bead1f](5bead1f))


### Performance

* **ci:** start the serialized integration collection first
([#861](#861))
([1dcc7f6](1dcc7f6)),
closes [#839](#839)


### Documentation

* **auth:** record the OAuth 2.1 decision for MCP authentication
([#801](#801))
([0510854](0510854))
* **designs:** MUI revamp design doc, component map, layout system, IA
([#862](#862))
([da49481](da49481))
* **readme:** recapture the daily entry, reports and sales screenshots
([#865](#865))
([f18e336](f18e336))
* **specs:** correct the sales_order_items column list in §10.5
([#812](#812))
([afe4a02](afe4a02)),
closes [#737](#737)

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: cluckwork-lockfix[bot] <309265648+cluckwork-lockfix[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

web: serve a per-request CSP nonce so MUI's Emotion styles apply under style-src 'self'

1 participant