Repository navigation
Commit 3c30afd
feat(cli): serve the Console document with a nonce-based content-security-policy (#25569)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoBVXzK6k7CamM6jB1PW7zzb)_
## Summary
The Console document (every HTML answer under `/_console/`) is now
served with a nonce-based `content-security-policy` response header.
Until now only API responses carried a policy
(`packages/runtime/src/security/security-headers.ts`), and the Console
shell carried none.
- `createConsoleStaticPlugin` (`packages/cli/src/utils/console.ts`)
draws a fresh 128-bit nonce per response. It names the nonce in the
header and stamps it on every script element of the served document: the
build's inline scripts, its module entry, and the server's own
react-pages disable script.
- The nonce is stamped before any request-time value (branding title and
meta tags) is written into the document, so no runtime value can carry
it.
- This plugin is the one place the document is served from. `os dev` and
`os start` both spawn `os serve`, which mounts it, and the Docker
image's command is `os start`. A host that mounts the plugin itself
through `@objectstack/cli/console` gets the same header.
- Runtime is unchanged.
Clause-②: no
Metadata-protocol: none
## The policy
Each allowance was measured against the published
`@objectstack/console@18.0.0` build (its `index.html`, its `assets/`)
and the objectui source at the pinned `.objectui-sha` (`2958f17e`). The
reason for each is written next to the policy in the code.
| Directive | Value | Why |
|:--|:--|:--|
| `default-src` | `'self'` | Covers maplibre's module workers (started
from its own file), the PWA manifest and same-origin frames (the Studio
live canvas). The build ships no web fonts. |
| `script-src` | `'self' 'nonce-…' 'wasm-unsafe-eval'`, plus
`'unsafe-eval'` only while the react-pages tier is on |
`'wasm-unsafe-eval'` is for shiki (the AI chat's code highlighter),
which compiles WebAssembly. The `kind:'react'` runtime (ADR-0081)
evaluates page source with `Function`, so `'unsafe-eval'` follows
`OS_PAGE_REACT`. That flag is read once per document, so the disable
script and the policy always agree. |
| `style-src` | `'self' 'unsafe-inline'` | The shell's boot-splash style
element, React/Radix `style` attributes, and style elements libraries
insert at run time without a nonce. |
| `img-src` | `'self' data: blob: https:` | Inline SVG icons in the
stylesheets, upload previews, and externally hosted branding logos and
favicons, avatars, image field values and email-template images. |
| `connect-src` | `'self' https://demotiles.maplibre.org`, plus the
error-reporting DSN's origin when the runtime serves one, plus
`OS_CONSOLE_CSP_CONNECT_SRC` | The API (`'self'` also covers the
same-origin WebSocket and event stream), and the map view's default
style host. The DSN origin is read from the same `GET
/api/v1/runtime/config` answer the document's branding is already read
from. |
| `object-src` | `'none'` | |
| `base-uri` | `'self'` | Only the server's own base element. |
| `frame-ancestors` | `'self'`, omitted on `/_console/f/*` | The
public-forms screen gives authors an iframe snippet that embeds the
public form page on their own site. |
### What an operator may notice
- **New `OS_CONSOLE_CSP_CONNECT_SRC`.** It is a space-separated list of
extra `connect-src` sources: a scheme (`https:`) or a scheme with a host
(`https://tiles.example.com`). It exists because a map view's authored
`mapStyle` is fetched straight from its host, and the server cannot know
that host ahead of the request. It is read once at boot. An entry
outside that grammar (a keyword, `*`, a bare host, or anything carrying
`;` or quotes) stops the boot with a message naming it.
- **Framing from another origin is refused**, except on the public form
page.
- **The Studio's code editor** (Monaco) loads its core from a public
CDN. The policy deliberately lists no CDN in `script-src`, because a CDN
host there admits every file that CDN serves. The editor therefore shows
the plain-text fallback objectui already ships for this case.
## Verification
Tests: a new suite,
`packages/cli/src/utils/console.content-security-policy.test.ts`, with
17 cases. It boots the real plugin on a real Hono app, and adds the real
`RuntimeConfigPlugin` for the DSN case. It checks:
- the header is present on the shell, on an SPA fallback and on a direct
`.html` hit, and absent on assets;
- the nonce differs across 5 responses;
- every script element in the served document carries the header's
nonce, including an uppercase tag and the injected react-pages disable
script;
- `'unsafe-eval'` is present by default and absent with
`OS_PAGE_REACT=off`;
- `frame-ancestors` is omitted on `/_console/f/*` and kept on
`/_console/forms/*`;
- the DSN origin appears without its key;
- `OS_CONSOLE_CSP_CONNECT_SRC` extras are added in order, and five
malformed values refuse the boot.
Local results, at head `9182f4b9`:
- The full `@objectstack/cli` unit tier (`vitest run --project unit`)
passes: 328 files, 4829 tests. The integration tier is left to CI,
because the diff touches no spawn entry.
- `vitest run --project unit` passes on the new suite plus
`console.branded-head`, `console.public-form-redirect`,
`console.asset-cache`, `console.not-built`, `console.sha-drift`,
`published-subpath-console.pin` and `runtime-assets`. The existing
suites pass unchanged.
- `pnpm --filter @objectstack/cli typecheck` (tsc plus
`check:test-typecheck`) exits 0. `eslint --no-inline-config` on both
touched files exits 0.
- Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 69 commands, and all 69 exit 0. The
`--ran` reconciliation reports 69 derived, 69 run, 0 not measured.
Browser smoke: `pnpm dev -- --fresh -p <port>` (the showcase), serving
the published `@objectstack/console@18.0.0` dist, driven by Playwright
Chromium.
- The run signed in as the seeded admin and visited eight routes: home,
the showcase app, a `kind:'react'` page (Task Desk), a map page, the
component gallery, the capability map, `/ai`, and a public form URL.
- A `securitypolicyviolation` listener was installed before any page
script ran, and console messages were captured too.
- A negative control at the end of each run proved the listener can see
a refusal. It inserts an inline script without a nonce, and a script
from the Monaco CDN path. Both were refused and both were reported.
| Run | Header on the wire | Violations on the 8 routes | Page errors |
|:--|:--|:--|:--|
| react pages **on** (default) | `'unsafe-eval'` present | **0** | 0 |
| `OS_PAGE_REACT=off` | `'unsafe-eval'` absent | 1 per document load,
all the same one (see Acceptance notes) | 0 |
In both runs the map rendered its canvas. The Task Desk react page
rendered with the tier on, and showed the disabled notice with it off.
`frame-ancestors` was absent only on the public form URL.
## Acceptance notes
- **With `OS_PAGE_REACT=off`, each document load reports one
`script-src` `eval` refusal from the Console bundle.** The source is zod
v4's capability probe: it tries `Function('')` once inside a try/catch,
to decide whether its JIT can be used, and falls back when refused.
Nothing breaks, and the page reports no error. The probe is skipped when
zod's `jitless` config is set, so the clean fix is objectui-side: set
`z.config({ jitless: true })` in the Console, at least when react pages
are off. That is outside this repository and is reported to the
maintainer rather than worked around here, for example by allowing
`'unsafe-eval'`.
- **Monaco.** The code editor's CDN load is refused by design, as above.
Self-hosting Monaco in objectui would restore the full editor under this
policy.
- **Out of scope.** Where the Console keeps its session token is a
separate decision, and this PR does not change it.
---
_Generated by [Claude
Code](https://claude.ai/code/session_013zqbSrrxA8RC7X5JQ4VHLV)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_013zqbSrrxA8RC7X5JQ4VHLV
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 5874d25 commit 3c30afd
4 files changed
Lines changed: 498 additions & 28 deletions
File tree
- .changeset
- content/docs/deployment
- packages/cli/src/utils
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| 31 | + | |
31 | 32 | | |
32 | 33 | | |
33 | 34 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
0 commit comments